Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. MAISON_CONSUMER_ROOT;
  2. MAISON_REPOSITORY or MAISON_REPO as compatibility aliases;
  3. 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/Lixmise
Nix daemon, caches, GC, and store policyUser tools and runtimes
PAM, sudoers, and privileged policyHomebrew formulae and casks
Hostname, timezone, locale, usersMac App Store applications
Root-owned files and SSH daemon policyUser dotfiles and preferences
System launchd/systemd servicesUser services
System-wide fonts and OS integrationsNon-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.md files store intended feature behavior and design decisions.
  • Reader-facing pages under docs/src/ describe current supported behavior.
  • Implemented-feature index.md files preserve feature-specific delivery and audit history.
  • planned-features.md provides 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 questionDocumentation 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:

  1. record the mismatch in Beads;
  2. determine whether the divergence was intentional;
  3. correct the implementation or update the appropriate authoritative artifact;
  4. preserve the rationale and validation evidence;
  5. 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.nix composes Maison’s public inputs and exports consumer host outputs;
  • flake.lock pins that consumer’s inputs;
  • inventory.toml describes users, hosts, profiles, and deployment targets;
  • hosts/ contains host-specific system overrides;
  • config/mise/ and dotfiles/ contain user policy; and
  • Git history is the source of deployment and authoring state.

Commands select the consumer in this order:

  1. MAISON_CONSUMER_ROOT;
  2. compatibility aliases MAISON_REPOSITORY or MAISON_REPO; and
  3. 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:

RepositoryRoleOwns
RobertDeRose/maisonPublic frameworkReusable Nix modules, mise orchestration, CLI, schemas, validation, tests, and neutral documentation
RobertDeRose/terroirPrivate consumerflake.nix, flake.lock, inventory, hosts, mise policy, dotfiles, deployment state, and personal/site configuration
archived private terroir.originalHistorical consumer sourceThe pre-split personal/site configuration used to seed the fresh Terroir history
archived nix-configHistorical framework sourceThe 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

  1. Freeze the archived private Terroir source and the source framework checkout; preserve both complete Git histories.
  2. 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.
  3. Obtain explicit approval of the manifest before copying, replacing, or deleting any candidate path.
  4. 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.
  5. 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.
  6. Validate both repositories without system activation: public-privacy scans, raw-secret scans, inventory validation, consumer validation, flake checks, documentation checks, and fresh-history checks.
  7. Confirm the new checkouts and their remotes before changing the source repository’s visibility or archive state.
  8. Make the historical nix-config repository private and archived only after all validation and remote checks pass.
  9. 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:

  1. uploads the committed consumer archive;
  2. validates the managed-user repository path;
  3. allocates a root-owned transaction root on the same filesystem;
  4. recovers incomplete transaction state;
  5. stages the new consumer tree and rollback source;
  6. records expected revisions and runs user convergence as the managed user;
  7. restores and verifies the prior consumer revision after user failure;
  8. runs restricted recovery from the restored consumer; and
  9. 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

  1. mise tool backend for versioned, user-scoped developer tools with prebuilt releases.
  2. mise built-in Homebrew formula for ordinary native CLI packages and shared-library applications.
  3. Homebrew cask or MAS through mise for ordinary user-facing macOS applications.
  4. 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; and
  • config/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

TaskBehavior
doctorDiagnose 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:updateTransactionally upgrade Maison from the consumer lock
deploy HOSTDeploy the consumer’s Linux system profile and user state
rollbackRoll back only the active Nix system generation
bootstrap [--consumer PATH]Verify Maison runtimes, select a consumer, and converge both layers
checkValidate Maison framework data, scripts, tests, and Nix outputs

System commands

TaskBehavior
system:planBuild the consumer system closure without activation
system:applyActivate the consumer’s Darwin or system-manager output
system:deployDeploy the consumer’s Linux system profile through deploy-rs
system:historyList canonical system-profile generations
system:rollbackSelect and activate the previous generation
system:clean [AGE]Remove old system generations and collect the store

User commands

TaskBehavior
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 --forceRestore a manifest-backed Maison dotfile backup
user:statusReport consumer user-environment drift
user:updateUpgrade consumer mise-managed tools
package:updateUpgrade consumer Homebrew formulae
app:updateUpgrade consumer casks and Mac App Store apps

Consumer configuration commands

TaskBehavior
host:addAdd a validated host to the consumer inventory and optional override
host:list / host:validateRead or validate the consumer inventory
tool:add / tool:removeTransactionally edit and focused-commit consumer tool config/lock
package:add / package:removeTransactionally edit and focused-commit consumer package config
package:searchSearch package sources without repository mutation
app:add / app:removeTransactionally edit and focused-commit consumer cask config
docs:build / docs:serveBuild 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:

  1. architecture consistency;
  2. simplicity and maintainability;
  3. documentation readiness;
  4. 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

FileContract
mise.tomlMaison development tools, environment, and task discovery.
mise.lockMaison development tool downloads; commit it.
hk.pklShared check/fix/pre-commit step map.
.config/rumdl.tomlMarkdown lint configuration.
contextlint.config.jsonDocumentation links, anchors, and image targets.
scripts/setup-tooling.pyResolves the locked toolchain, installs tools and hooks, and reports status.
schemas/inventory.tomlPublic inventory schema consumed by Python and Nix validators.
docs/src/reference/flake.mdPublic flake outputs, modules, helpers, and checks.
docs/src/reference/consumer.mdConsumer files, root selection, composition, and mutation boundaries.
.github/workflows/ci.ymlRepository 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

TaskBehavior
checkInstall Maison tools, then run data, shell, Python, and Nix checks.
consumer:validateValidate a consumer without activation or provider credentials.
self:updateUpgrade Maison from the selected consumer’s pinned input.
check:testsRun bounded Python unittest discovery across tests/test_*.py.
fixApply deterministic hk fixes to the working tree.
docs:checkBuild and validate documentation structure.
docs:buildBuild the mdBook site.
docs:serveServe 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

OutputContract
packages.<system>.maisonStore-backed Maison CLI package
apps.<system>.maisonRunnable app wrapper for the CLI package
darwinModulesReusable nix-darwin modules, including default, base, config, fonts, and system
systemManagerModulesReusable system-manager modules, including default and system
libMaison orchestration functions and inventory validation helpers
orchestrationAlias for the Maison orchestration library
schemas.inventoryPublic inventory schema path
schemas.fnoxProvider-neutral fnox contract schema
fixtures.inventoryNeutral valid and invalid inventory fixture corpus
fixtures.fnoxCredential-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;
  • profiles and 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

PathOwnerPurpose
flake.nixConsumerHost outputs and the Maison flake input
flake.lockConsumerPinned inputs for that consumer
inventory.tomlConsumerUsers, hosts, profiles, and deployment targets
README.mdConsumerMaison 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:

  1. MAISON_CONSUMER_ROOT;
  2. MAISON_REPOSITORY or MAISON_REPO;
  3. 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 type values remain opaque and consumer-selected;
  • secret names are shell-compatible logical names, while provider and key are 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

FieldRequiredContract
schemaYesMust equal 1.
[defaults] userOptionalDefault inventory user key for hosts that omit user.
[users.<key>]YesDeclares managed user identities.
[hosts.<name>]YesDeclares host system, user, profiles, optional features, and deployment fields.

Users

FieldDefaultContract
usernameUser keyPortable lowercase account name; root is not allowed as a managed user.
full_nameNoneRequired non-empty display name.
emailNoneRequired non-empty email string.
githubNoneRequired GitHub username.
allow_nonportablefalseAllows an existing non-portable username only on Darwin hosts.

Hosts

FieldContract
Host table nameOne DNS label.
systemaarch64-darwin, aarch64-linux, or x86_64-linux.
userExisting user key, or [defaults].user.
profilesNon-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.

FieldDefaultContract
enablefalseBoolean; deploy-rs is Linux-only when enabled.
hostnameHost table nameRemote host name.
ssh_usermaison-deployDeployment account; may be root for bootstrap but not the managed user.
user_ssh_userManaged usernameMust equal the managed inventory username.
repo_path/home/<managed-user>/.maisonNormalized descendant of /home/<managed-user>, not the home directory itself.
remote_buildfalseBoolean deploy-rs option.
auto_rollbacktrueBoolean deploy-rs rollback option.
magic_rollbacktrueBoolean 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 plan and validation are read-only. Activation and authoring operate on the selected consumer only.
  • maison self update transactionally 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

SlugFeatureBeads rootRoadmap stateDependenciesDesign
maison-001-root-owned-deployment-transactionsMAISON-001: Root-owned deployment transaction statemaison-mol-6y0DeliveredNonerecord
maison-002-revision-bound-commit-rollbackMAISON-002: Revision-bound commit and rollbackmaison-mol-quqDeliveredmaison-001-root-owned-deployment-transactionsrecord
maison-003-restricted-deployment-privilegeMAISON-003: Restricted deployment privilege modelmaison-mol-4v3Deliveredmaison-002-revision-bound-commit-rollbackrecord
maison-004-private-overlay-configurationMAISON-004: Private configuration split (superseded)maison-mol-e9tRetiredmaison-003-restricted-deployment-privilegerecord
maison-005-verified-bootstrap-artifactsMAISON-005: Verified bootstrap artifactsmaison-mol-pa6Deliveredmaison-004-private-overlay-configurationrecord
maison-006-review-gated-dependency-updatesMAISON-006: Review-gated dependency updatesmaison-mol-aquDeliveredmaison-005-verified-bootstrap-artifactsrecord
maison-007-parser-backed-toml-mutationsMAISON-007: Parser-backed TOML mutationsmaison-mol-ywgDeliveredmaison-006-review-gated-dependency-updatesrecord
maison-008-repository-mutation-lockingMAISON-008: Repository mutation locking and journalsmaison-mol-4evDeliveredmaison-007-parser-backed-toml-mutationsrecord
maison-009-authoring-checkout-guardMAISON-009: Authoring checkout guardmaison-mol-74lDeliveredmaison-008-repository-mutation-lockingrecord
maison-010-shared-inventory-schemaMAISON-010: Shared inventory schema validationmaison-mol-bvrDeliveredmaison-009-authoring-checkout-guardrecord
maison-011-deterministic-test-suiteMAISON-011: Bounded deterministic test suitemaison-mol-8mrDeliveredmaison-010-shared-inventory-schemarecord
maison-012-plan-apply-parityMAISON-012: Plan/apply semantic paritymaison-mol-6w9Deliveredmaison-011-deterministic-test-suiterecord
maison-013-dotfile-backup-manifestsMAISON-013: Exact dotfile backup manifestsmaison-mol-h8eDeliveredmaison-012-plan-apply-parityrecord
maison-014-remote-convergence-restorationMAISON-014: Remote convergence restorationmaison-mol-vhhyDeliveredmaison-013-dotfile-backup-manifestsrecord
maison-015-linux-runtime-verificationMAISON-015: Linux runtime activation verificationmaison-mol-cg3xDeliveredmaison-014-remote-convergence-restorationrecord
maison-016-pi-typescript-validationMAISON-016: Pi TypeScript validation boundarymaison-mol-fmudDeliveredmaison-015-linux-runtime-verificationrecord
maison-017-maison-terroir-repository-splitMAISON-017: Maison/Terroir repository splitmaison-mol-jfhsDeliveredmaison-016-pi-typescript-validationrecord
maison-overlay-copier-bootstrapCopier fresh consumer bootstrapmaison-mol-5s9Deliveredmaison-017-maison-terroir-repository-splitrecord
maison-overlay-authoring-lifecycleRetired alternate repository command surfacemaison-mol-3jbRetiredmaison-overlay-copier-bootstraprecord

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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
README.mdUpdateReplace managed-user-controlled transaction path claims with the root-owned transaction-root contracttransaction-contract, transaction-engine
docs/deployment.mdUpdateDocument transaction-root placement, validation, journals, locks, staging, and finalization command behaviortransaction-contract, transaction-engine
docs/recovery.mdUpdateDocument how to inspect incomplete root-owned transaction journals and where rollback state livestransaction-contract, transaction-engine
docs/architecture.mdUpdateRecord the privileged repository transaction boundary and ownership invarianttransaction-contract
docs/task-reference.mdUpdateSummarize the deploy task’s root-owned repository transaction behaviortransaction-engine
docs/src/features/maison-001-root-owned-deployment-transactions/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check after 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 use usage comments, 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.md and docs/.

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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/deployment.mdUpdateAlign reader-facing contract with MAISON-002: Revision-bound commit and rollbackImplementation tasks
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-002: Revision-bound commit and rollbackImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-002: Revision-bound commit and rollbackImplementation tasks
docs/task-reference.mdUpdateAlign reader-facing contract with MAISON-002: Revision-bound commit and rollbackImplementation tasks
docs/src/features/maison-002-revision-bound-commit-rollback/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check and uv 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 use usage comments, 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.md and docs/.

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-deploy as 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: ALL from 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-deploy separately 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateAlign reader-facing contract with MAISON-003: Restricted deployment privilege modelImplementation tasks
docs/deployment.mdUpdateAlign reader-facing contract with MAISON-003: Restricted deployment privilege modelImplementation tasks
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-003: Restricted deployment privilege modelImplementation tasks
docs/task-reference.mdUpdateAlign reader-facing contract with MAISON-003: Restricted deployment privilege modelImplementation tasks
docs/add-a-host.mdUpdateAlign reader-facing contract with MAISON-003: Restricted deployment privilege modelImplementation tasks
docs/src/features/maison-003-restricted-deployment-privilege/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Nix and Python inventory tests for separate deployment and managed users.
  • Text and behavioral tests rejecting NOPASSWD: ALL defaults and direct root deployment defaults.
  • Deployment adapter tests proving privileged command allowlist construction.
  • mise -E dev run check and uv 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 — Implement maison-deploy restricted 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 use usage comments, 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.md and docs/.

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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
README.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/architecture.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/deployment.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/add-a-host.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/add-a-tool.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/add-an-app.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/package-policy.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/task-reference.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-004: Private overlay configuration splitImplementation tasks
docs/src/features/maison-004-private-overlay-configuration/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check and uv 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 use usage comments, 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.md and docs/.

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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
README.mdUpdateAlign reader-facing contract with MAISON-005: Verified bootstrap artifactsImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-005: Verified bootstrap artifactsImplementation tasks
docs/deployment.mdUpdateDocument remote deployment bootstrap fallback verificationImplementation tasks
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-005: Verified bootstrap artifactsImplementation tasks
docs/task-reference.mdUpdateAlign reader-facing contract with MAISON-005: Verified bootstrap artifactsImplementation tasks
docs/src/features/maison-005-verified-bootstrap-artifacts/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Source-text guard tests banning curl ... | sh and 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 latest policy remains scoped to non-bootstrap workstation convergence when lockfiles capture resolved artifacts.
  • mise -E dev run check and uv 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 use usage comments, 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 latest with generated lockfiles.

Source Material

  • Maison review summary for commit ded7bbb745f34f1059930fc48eadafe267399ab2.
  • Current Maison documentation under README.md and docs/.

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 --admin or 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/operations.mdUpdateExplain that automated cache warming may prepare a flake update PR, but dependency approval remains review-gatedImplementation tasks
docs/src/development/tooling.mdUpdateDocument cache-refresh workflow behavior for contributors and CI maintainersImplementation tasks
docs/src/reference/tooling.mdUpdateList .github/workflows/cache-refresh.yml and its no-admin-merge contractImplementation tasks
docs/src/features/maison-006-review-gated-dependency-updates/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Workflow tests or source guards reject --admin merge, auto-merge enablement, and any dependency-automation job that invokes gh 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 check and uv 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 use usage comments, 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.md and docs/.

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 tomlkit for mutation scripts through a deterministic repository-controlled runtime path that does not rely on ambient Python site-packages.
  • Preserve tool:add and tool:remove repairability when mise project tool resolution is broken; parser-backed editing must still work on the --skip-tools path.
  • Replace section-splicing logic in config_edit.py with 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/src/development/tooling.mdUpdateDocument the pinned parser-backed TOML mutation runtime for contributorsImplementation tasks
docs/task-reference.mdUpdateDocument parser-backed transactional TOML mutation guarantees for configuration commandsImplementation tasks
docs/add-a-tool.mdUpdateExplain tool/package edits preserve supported TOML formatting and fail without partial writesImplementation tasks
docs/add-an-app.mdUpdateExplain app edits preserve supported TOML formatting and fail without partial writesImplementation tasks
docs/src/features/maison-007-parser-backed-toml-mutations/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Focused tests/test_config_edit.py regression corpus for comments, quoted keys, arrays of tables, CRLF input, malformed TOML, lock blocks, host inventory additions, and unrelated sections.
  • Tests proving the pinned tomlkit runtime is used without ambient site-packages and remains available on the tool:add/tool:remove --skip-tools path.
  • Behavioral authoring command tests.
  • mise -E dev run check and uv 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 use usage comments, 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.md and docs/.

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 fcntl lock 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, and update acquire 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateDocument the local repository mutation lock/journal boundary and its separation from remote deployment transactionsImplementation tasks
docs/operations.mdUpdateDescribe serialized mutation behavior, busy-lock failures, and affected commandsImplementation tasks
docs/recovery.mdUpdateDescribe local mutation journal recovery state, diagnostics to preserve, and safe operator responseImplementation tasks
docs/task-reference.mdUpdateDocument which mutation commands take the repository lock and which read-only commands do notImplementation tasks
docs/src/features/maison-008-repository-mutation-locking/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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, and update repository 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 check and uv 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 use usage comments, 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.md and docs/.

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 .git plus Maison revision metadata.
  • A .git directory or worktree .git file 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateDocument the authoring checkout versus deployed snapshot boundaryImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-009: Authoring checkout guardImplementation tasks
docs/task-reference.mdUpdateDistinguish authoring-only commands from deployed runtime commandsImplementation tasks
docs/deployment.mdUpdateExplain deployed archives are runtime snapshots, not authoring checkoutsImplementation tasks
docs/recovery.mdUpdateExplain rejected deployed-snapshot authoring commands and overlay/source recovery pathImplementation tasks
docs/src/features/maison-009-authoring-checkout-guard/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check and uv 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 use usage comments, 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.md and docs/.

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.toml as 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, including usage comments 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, and x86_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 check remains 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.toml becomes 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateDocument schemas/inventory.toml as the shared inventory schema contractImplementation tasks
docs/add-a-host.mdUpdateDocument inventory authoring constraints and overlay host override validationImplementation tasks
docs/deployment.mdUpdateDocument deploy inventory fields and shared validation constraintsImplementation tasks
docs/src/reference/inventory.mdCreateDefine inventory fields, defaults, constraints, and fixture contractImplementation tasks
docs/src/reference/tooling.mdUpdateMention shared inventory schema and fixture validation toolingImplementation tasks
docs/src/features/maison-010-shared-inventory-schema/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check and uv 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 use usage comments, 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.toml as 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.md and docs/.

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:tests deterministic 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, including usage comments 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, and x86_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 check remains 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 subprocess calls.
  • 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 pageCreate or updatePlanned changeOwning Beads task
docs/src/development/tooling.mdUpdateDocument bounded test helper behavior, focused subsystem test commands, and full-suite expectationsImplementation tasks
docs/src/reference/tooling.mdUpdateDefine validation partitions, subprocess timeout policy, and the shared test helper contractImplementation tasks
docs/operations.mdUpdateSummarize operator/contributor validation commands and bounded check expectationsImplementation tasks
docs/src/features/maison-011-deterministic-test-suite/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Focused tests for the subprocess helper itself.
  • Full mise -E dev run check completes 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 use usage comments, 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/ and docs/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:tests expected 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.md and docs/.

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-dotfiles is 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, aggregate maison plan, and aggregate maison apply use the same force-dotfile flag semantics: default false, explicit --force-dotfiles true.
  • 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/operations.mdUpdateDocument plan/apply parity, default no-force dotfile behavior, and --force-dotfiles preview/apply flowImplementation tasks
docs/recovery.mdUpdateReplace force-enabled dry-run wording with explicit user plan --force-dotfiles then user apply --force-dotfiles recovery guidanceImplementation tasks
docs/task-reference.mdUpdateDocument plan/user:plan force-dotfile flag behavior and dry-run-only command differencesImplementation tasks
docs/src/features/maison-012-plan-apply-parity/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • Focused tests comparing generated plan/apply structured command plans for default and --force-dotfiles modes.
  • Behavioral tests for aggregate plan and apply force-dotfile forwarding.
  • Behavioral tests for force-dotfile conflict previews and applies.
  • mise -E dev run check and uv 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 use usage comments, 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-dotfiles flag-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.md and docs/.

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, including usage comments 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, and x86_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 check remains 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 lstat object 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 pageCreate or updatePlanned changeOwning Beads task
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-013: Exact dotfile backup manifestsImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-013: Exact dotfile backup manifestsImplementation tasks
docs/task-reference.mdUpdateAlign reader-facing contract with MAISON-013: Exact dotfile backup manifestsImplementation tasks
docs/src/features/maison-013-dotfile-backup-manifests/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 $HOME or 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 check and uv 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 use usage comments, 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.md and docs/.

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-dotfiles choice 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 contains schema_version, kind, failed_revision, restored_revision, initial_convergence (exit code and package-phase status), recovery (status, exit code, steps, and force-dotfiles state), and external_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, or unknown based 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, including usage comments 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, and x86_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:

  1. dotfile conflict preparation/convergence, limited to the existing explicit force policy and without application archival or legacy Git migration;
  2. mise lockfile linking;
  3. non-package mise user state; and
  4. 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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/deployment.mdUpdateDocument rollback-before-recovery ordering, managed-user execution, report path, and original/recovery failure semanticsreconverge-implementation
docs/recovery.mdUpdateDocument restricted recovery steps, skipped package/app rollback, report fields, and manual follow-upreconverge-implementation
docs/operations.mdUpdateDocument the automatic recovery outcome and explicit --force-dotfiles behaviorreconverge-implementation
docs/task-reference.mdUpdateReference deploy’s restricted recovery contract and diagnostic artifactreconverge-implementation
docs/src/features/maison-014-remote-convergence-restoration/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateMark the roadmap state and retain the Beads root after deliveryClose-out documentation
docs/src/SUMMARY.mdUpdateRegister the delivered record while retaining the design linkClose-out documentation

Validation Strategy

  • Add fault-injection tests in tests/test_remote_convergence.py for 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, and tests.test_user_convergence.
  • Run python3 scripts/check-docs.py after reader-facing documentation changes.
  • Run mise run check for repository-wide validation.

Implementation Decomposition

  • maison-014-remote-convergence-restoration reconverge-contract — Create tests/test_remote_convergence.py with 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; update docs/deployment.md, docs/recovery.md, docs/operations.md, and docs/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 use usage comments, 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.md and docs/.

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 systemctl is 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 --static equals the configured host name and that the active timezone is America/New_York through systemd’s runtime state and /etc/localtime.
  • Validate the active SSH configuration with sshd -t, reload or restart the active ssh.service or sshd.service, and fail activation when the check or reload fails; no SSH reload failure may be suppressed.
  • Verify that system-manager.target and the Maison-managed prefill-authorized-keys.service reach 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateAlign reader-facing contract with MAISON-015: Linux runtime activation verificationImplementation tasks
docs/operations.mdUpdateAlign reader-facing contract with MAISON-015: Linux runtime activation verificationImplementation tasks
docs/recovery.mdUpdateAlign reader-facing contract with MAISON-015: Linux runtime activation verificationImplementation tasks
docs/deployment.mdUpdateAlign reader-facing contract with MAISON-015: Linux runtime activation verificationImplementation tasks
docs/src/features/maison-015-linux-runtime-verification/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / 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 check and uv 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 use usage comments, 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.md and docs/.

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/extensions workspace or its check:typescript task.

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/extensions or an equivalent project-local package.
  • Use actual Pi types or local checked interface definitions derived from documented Pi extension APIs.
  • Run tsc --noEmit in 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, including usage comments 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, and x86_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 check remains 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 pageCreate or updatePlanned changeOwning Beads task
docs/architecture.mdUpdateDocument the scoped repository Node runtime used for Pi validationImplementation tasks
docs/src/development/tooling.mdUpdateAlign reader-facing contract with MAISON-016: Pi TypeScript validation boundaryImplementation tasks
docs/src/reference/tooling.mdUpdateAlign reader-facing contract with MAISON-016: Pi TypeScript validation boundaryImplementation tasks
dotfiles/pi/AGENTS.mdUpdateAlign reader-facing contract with MAISON-016: Pi TypeScript validation boundaryImplementation tasks
dotfiles/opencode/README.mdUpdateAlign reader-facing contract with MAISON-016: Pi TypeScript validation boundaryImplementation tasks
docs/src/features/maison-016-pi-typescript-validation/index.mdCreate during close-outPreserve delivery and audit historyClose-out documentation
docs/src/planned-features.mdUpdateTrack roadmap status and Beads rootPlanning
docs/src/SUMMARY.mdUpdateRegister this design and delivered record linksPlanning / close-out

Validation Strategy

  • mise run check:typescript, which performs locked workspace installation, tsc --noEmit, and the focused node:test suite for Pi extensions.
  • Behavioral tests for Unicode, ANSI handling, navigation, deterministic timers, and terminal resizing.
  • mise -E dev run check and uv 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 deterministic node:test coverage 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 use usage comments, 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.md and docs/.

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/maison repository containing reusable framework code, neutral examples, tests, documentation, and dstack project controls.
  • Create a private RobertDeRose/terroir repository 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-config commits or metadata.
  • Preserve the complete original nix-config history by making RobertDeRose/nix-config private and archiving it only after both new repositories pass validation.
  • Create separate local checkouts at ../maison and ../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-config history; 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-config working 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/maison as a new public repository from a fresh dstack/Copier scaffold using /setup-project or its equivalent setup helper. Use the stable dstack template, main as the default branch, and the project brief fixed by this design.
  • Create RobertDeRose/terroir as a new private plain Git repository with main as 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-config private 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/maison and RobertDeRose/terroir as the canonical repository identities.
  • Keep nix-config private 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

  1. Freeze and audit

    • Treat the current nix-config checkout 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, or exclude), and rationale.
    • Present the manifest for explicit approval. No destructive or private-copy action occurs before approval.
  2. Create fresh repository shells

    • Create empty local destinations <maison-checkout> and <terroir-checkout> without reusing the source .git directory.
    • Run the stable /setup-project maison workflow 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, and typescript;
      • 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 RobertDeRose account with public/private visibility as specified, refusing conflicts.
  3. Populate and commit

    • Copy reusable framework files into Maison while preserving the newly generated dstack/Copier controls and excluding old .git, .beads runtime/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.git in the local migration checkout only after the private remote is available; do not commit machine-local overlay state.
  4. 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-config private and archived, then record the archive state and final repository URLs in the migration record.
  5. 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-config repository.
  • 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-config is 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 --overlay or 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 concernExact pageCreate or updatePlanned changeOwning Beads task
IntroductionREADME.mdUpdateExplain Maison/Terroir repository roles and first clone/bootstrap flowmaison-mol-ieqc.4
Introductiondocs/src/introduction/project-overview.mdUpdateState the public framework/private overlay split and Bitwarden boundarymaison-mol-ieqc.4
Architecturedocs/architecture.mdUpdateDefine public Maison, private Terroir, Bitwarden, and archived nix-config ownershipmaison-mol-ieqc.4
Usage / Operationsdocs/operations.mdUpdateDocument fresh setup, local checkouts, overlay selection, and transition recoverymaison-mol-ieqc.4
Recoverydocs/recovery.mdUpdateDocument archived-source recovery and failed split/remote-transition handlingmaison-mol-ieqc.4
Deploymentdocs/deployment.mdUpdateAlign deployment source and overlay repository guidance with the new canonical remotesmaison-mol-ieqc.4
Migration contractdocs/migration-contract.mdUpdateRecord the two-repository migration boundary and archive sequencingmaison-mol-ieqc.4
Inventory referencedocs/src/reference/inventory.mdUpdateClarify Terroir mirrored layout and secret exclusionsmaison-mol-ieqc.4
Developmentdocs/src/development/tooling.mdUpdateDocument dstack-managed Maison setup and fresh-repository validation commandsmaison-mol-ieqc.4
Navigationdocs/src/SUMMARY.mdUpdate if neededRegister the design and implemented record; register any new durable pagemaison-mol-ako9, maison-mol-ryxl
Roadmapdocs/src/planned-features.mdUpdateAdd MAISON-017 with migration sequencing and dependencymaison-mol-ako9
Feature indexdocs/src/features/index.mdUpdate during close-outAdd the delivered migration recordmaison-mol-ryxl
Implemented recorddocs/src/features/maison-017-maison-terroir-repository-split/index.mdCreate during close-outPreserve migration, remote, validation, and archive evidencemaison-mol-ryxl
Terroir READMEREADME.md in RobertDeRose/terroirCreateState overlay layout, no-secret policy, and Maison relationshipmaison-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-project verification for Maison: Copier provenance, dstack files, main branch, generated docs, Beads, locked tooling, and no legacy migration utilities.
  • Fresh-history checks for both repositories: no source .git reuse, 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.py and mise -E dev run check in 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/maison public, RobertDeRose/terroir private, both defaulting to main, and RobertDeRose/nix-config private 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 old nix-config private 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.2 and maison-mol-ieqc.3 may proceed in parallel after specification reconciliation, but neither may populate data before the candidate manifest is approved.
  • maison-mol-ieqc.4 depends on the approved manifest and both scaffolds.
  • maison-mol-ieqc.5 depends on the completed content split and fresh-history checks.
  • maison-mol-ieqc.6 depends on successful validation and is the only task allowed to archive nix-config.
  • Documentation reconciliation and final validation occur after the new public repository’s reader-facing pages are available.

Rollout and Migration

  1. Freeze the source checkout and create the candidate manifest.
  2. Obtain explicit approval of the manifest.
  3. Create both new repository shells and verify ownership/visibility before copying data.
  4. Populate fresh local histories and push only after local validation.
  5. Validate public/private boundaries, overlay loading, and dstack checks.
  6. Make nix-config private and archive it.
  7. 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-config private 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-config history.
  • Keeping personal configuration in the public Maison repository with only documentation-level separation.
  • Calling the private repository dotfiles when 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 is git@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-config checkout 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-config private and archive it after successful verification.
  • Public workflow: initialize maison through /setup-project and use dstack.
  • Private workflow: keep terroir as 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 maison and terroir checkouts.

Assumptions

  • The authenticated gh account is RobertDeRose and has permission to create both repositories and update/archive nix-config.
  • The existing nix-config checkout 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 dotfiles repository. It was refined into two new repositories: public maison and private terroir, while the existing nix-config becomes a private archive.
  • The private repository name changed from dotfiles to terroir.
  • 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.md
  • docs/src/features/maison-004-private-overlay-configuration/index.md
  • README.md
  • docs/architecture.md
  • docs/operations.md
  • docs/recovery.md
  • docs/migration-contract.md
  • docs/src/reference/inventory.md
  • docs/src/planned-features.md
  • Existing nix-config tree 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 via bootstrap.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 SOURCE and MAISON_OVERLAY=SOURCE as the supported explicit overlay inputs.
  • Preserve saved overlay state for subsequent runs and retain the legacy MAISON_OVERLAY_SOURCE input 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:add against 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:add task.
  • 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 uv runtime.

User-Facing Behavior

Overlay selection

Bootstrap resolves sources in this order:

  1. --overlay SOURCE for the current invocation;
  2. MAISON_OVERLAY=SOURCE;
  3. legacy MAISON_OVERLAY_SOURCE=SOURCE for compatibility;
  4. 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 from id -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 maison CLI, 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:

  1. initializes the destination Git repository when needed;
  2. uses the current short hostname unless bootstrap supplied --host;
  3. exports the destination as MAISON_OVERLAY_PATH;
  4. invokes mise -C <Maison checkout> run host:add with 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.py exposes 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 copier through a Maison-managed uv installation 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:add and 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.toml files remain readable.
  • Existing MAISON_OVERLAY_SOURCE users continue to work, but new docs use MAISON_OVERLAY.
  • Existing --overlay remote URLs continue to clone/update into the standard XDG data path.
  • Existing host:add behavior 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:

  1. resolve or clone a Maison checkout;
  2. install verified mise and link the Maison CLI;
  3. resolve an explicit/saved overlay, or run interactive onboarding;
  4. if onboarding is declined, print next steps and exit before Nix/system/user work;
  5. if onboarding succeeds, prepare and persist the overlay;
  6. 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:add as 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, and x86_64-linux systems through Maison’s shared host:add validation.
  • Repository mutations are locked, validated, and recoverable.

New Decisions Introduced

  • MAISON_OVERLAY is the canonical environment variable; MAISON_OVERLAY_SOURCE is 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 concernExact pageCreate or updatePlanned changeOwning Beads task
Introduction / usageREADME.mdUpdateReplace copy instructions with Copier and explain explicit/interactive setupmaison-mol-xke.3
Operationsdocs/operations.mdUpdateDocument bootstrap phases, prompt outcomes, and recoverymaison-mol-xke.3
Host authoringdocs/add-a-host.mdUpdateExplain generated host registration and manual host:addmaison-mol-xke.3
Tooling referencedocs/src/reference/tooling.mdUpdateDocument Copier/uvx setup and template update rulesmaison-mol-xke.3
Templateoverlay_template/README.mdUpdateMake the template’s Copier contract and private boundary explicitmaison-mol-xke.2
Feature navigationdocs/src/SUMMARY.mdUpdateRegister this design and its delivered recordmaison-mol-xke.3
Roadmapdocs/src/planned-features.mdUpdateAdd the planned feature and dependency/status entrymaison-mol-xke.3
Implemented Feature Recorddocs/src/features/maison-overlay-copier-bootstrap/index.mdCreate during close-outPreserve delivery and audit historyClose-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 mise executable and verify host:add receives 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, and mise 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 uvx and 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/repo match 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 -R as 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 --host option overrides it.
  • The later consumer-repository boundary supersedes Copier-created overlay destinations; MAISON_CONSUMER_ROOT identifies the authoritative configuration root.
  • MAISON_REQUIRE_CONSUMER=true is 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 workflow heading in Maison’s top-level help and add status and publish to the top-level command surface.
  • Add overlay-only maison status and maison publish commands 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, and app add/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 sync workflow 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 status or publish; 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:

  1. fetches the upstream before changing the worktree;
  2. fails without stashing if the upstream is unavailable, no upstream is configured, or local and remote history is not a fast-forward-publishable relationship;
  3. temporarily stashes tracked and untracked changes, but never ignored files;
  4. pushes already-committed local changes;
  5. restores the stash after a successful or failed push; and
  6. 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 status and publish tasks 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 sync as 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 sync continues 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.py and .mise/lib/overlay.sh for active-overlay resolution and XDG state.
  • .mise/lib/repository_mutation.py for authoring checks, lock ownership, recovery, and private state directories.
  • .mise/lib/transaction.sh for candidate replacement, journals, rollback, and signal handling.
  • Parser-backed config_edit.py edits 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 status and maison publish are 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 concernExact pageCreate or updatePlanned changeOwning Beads task
Introduction / command surfaceREADME.mdUpdateAdd top-level status/publish usage, overlay-only authoring, commit/publish lifecycle, exact help semantics, and the preserved configuration/profile boundarymaison-mol-0tm.3
Architecturedocs/architecture.mdUpdateDocument overlay authoring ownership, remote refresh, focused commits, and no-public-fallback invariantmaison-mol-0tm.2
Operationsdocs/operations.mdUpdateDocument status states, offline reporting, publish stash/push/restore behavior, and recoverymaison-mol-0tm.1
Referencedocs/task-reference.mdUpdateAdd status/publish contracts and revised software mutation guaranteesmaison-mol-0tm.3
Referencedocs/src/reference/tooling.mdUpdateDocument overlay status/publish commands, mutation commit behavior, and configuration/profile scopemaison-mol-0tm.3
Package/application authoringdocs/package-policy.md, docs/add-a-tool.md, docs/add-an-app.mdUpdateExplain overlay requirement, pre-mutation refresh, target cleanliness, and generated commit subjectsmaison-mol-0tm.2
Navigationdocs/src/SUMMARY.mdUpdateRegister this feature design; no new reader page is requiredPlanning/close-out
Roadmapdocs/src/planned-features.mdUpdateAdd this feature with dependency/status narrativemaison-mol-0tm.3
Implemented feature recorddocs/src/features/maison-overlay-authoring-lifecycle/index.mdCreate during close-outPreserve delivery and audit evidenceClose-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 no workflow command exists.
  • Calling full maison sync before 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 publish pushes 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 status fetches when possible and reports last-known comparison state when offline/unavailable; inspectable states exit zero.
  • publish uses 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 sync was 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/maison
  • scripts/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/sync
  • tests/test_repository_contracts.py
  • tests/test_repository_mutation.py
  • tests/test_transaction_behavior.py
  • README.md
  • docs/architecture.md
  • docs/operations.md
  • docs/task-reference.md
  • docs/package-policy.md
  • docs/add-a-tool.md
  • docs/add-an-app.md
  • docs/src/reference/tooling.md
  • docs/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

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>/.maison is /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, .git directories, 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.json for 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.lock for serialization;
  • active.json for the current incomplete transaction;
  • <transaction-id>/journal.jsonl for journal events;
  • <transaction-id>/staging/repository for extracted staged content before install;
  • <transaction-id>/rollback/repository for 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 stage and finalize; abort and inspect-incomplete remain 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-0lpv on 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-state sidecars 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.md
  • docs/architecture.md
  • docs/deployment.md
  • docs/migration-contract.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-001-root-owned-deployment-transactions/design.md
  • docs/src/features/maison-001-root-owned-deployment-transactions/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation: e1f5c8a (docs(features): reconcile MAISON-001 specification).
  • Validation blocker chore: 48a781a (removed unsupported Intel macOS scaffold targets, Beads maison-0lpv).
  • Contract and documentation task: ae664b3 (test(deploy): define root-owned transaction contract, Beads maison-mol-xjt.1).
  • Engine implementation task: b0ccaf7 (feat(deploy): use root-owned repository transactions, Beads maison-mol-xjt.2).
  • Implementation coordinator maison-mol-xjt closed 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.json and 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 deploy recovers 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.lock for serialization,
  • active.json for current in-flight transaction metadata,
  • <transaction-id>/journal.jsonl for durable journal events,
  • <transaction-id>/staging/repository for extracted archive contents,
  • <transaction-id>/rollback/repository for 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, explicit recover) remain planned for future ergonomics while preserving MAISON-002 scope.

Documentation Updated

  • docs/deployment.md
  • docs/recovery.md
  • docs/operations.md
  • docs/task-reference.md
  • docs/src/features/maison-002-revision-bound-commit-rollback/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/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, and finalize operations,
  • 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-only
  • maison deploy <host> --force-dotfiles
  • maison 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 fix and hk check — passed for the shellcheck/shebang housekeeping fix that unblocked hook validation.

Design Reconciliation

Delivered as Designed

  • maison-deploy is 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: ALL by 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 is maison-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: ALL for 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.md
  • docs/architecture.md
  • docs/deployment.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-003-restricted-deployment-privilege/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-9mi.
  • Implementation coordinator: maison-mol-u8y.
  • Contract-test implementation task: maison-mol-u8y.1, commit 09e4fdfd2b027db782d04eb7a10a20178b272c30.
  • Restricted privilege implementation task: maison-mol-u8y.2, commit 26a8411b1964b0a54103c6a7fa06584696698d61.
  • 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:

  1. --overlay <git-url-or-path> for the current bootstrap run.
  2. MAISON_OVERLAY_SOURCE, including a value supplied by mise secrets.
  3. ${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

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 --overlay and 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.
  • --overlay takes 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.py plus 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/*.toml and dotfiles/ 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.md
  • docs/add-a-host.md
  • docs/add-a-tool.md
  • docs/add-an-app.md
  • docs/architecture.md
  • docs/deployment.md
  • docs/operations.md
  • docs/package-policy.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-004-private-overlay-configuration/design.md
  • docs/src/features/maison-004-private-overlay-configuration/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-luq, commit 4d88caa.
  • Implementation coordinator: maison-mol-z6x.
  • Overlay contract task: maison-mol-z6x.1, commit 96136b0.
  • Overlay loader task: maison-mol-z6x.2, commit 8ae4710.
  • Overlay docs/examples task: maison-mol-z6x.3, commit 8d5c240.
  • 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 usage Maison runtime tool is pinned to 4.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 ... | sh and curl ... | bash paths 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.md
  • docs/deployment.md
  • docs/operations.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-005-verified-bootstrap-artifacts/design.md
  • docs/src/features/maison-005-verified-bootstrap-artifacts/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-0od, commit 63aa235.
  • Implementation coordinator: maison-mol-b2j.
  • Bootstrap contract task: maison-mol-b2j.1, commit da8eb56.
  • Bootstrap implementation task: maison-mol-b2j.2, commit 2a93428.
  • 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-pr job 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.md and docs/src/reference/tooling.md pages.

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.md
  • docs/src/development/tooling.md
  • docs/src/reference/tooling.md
  • docs/src/features/maison-006-review-gated-dependency-updates/design.md
  • docs/src/features/maison-006-review-gated-dependency-updates/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-642, commit 0f4e563.
  • Implementation coordinator: maison-mol-73y.
  • CI contract task: maison-mol-73y.1, commit b45e97f.
  • CI implementation task: maison-mol-73y.2, commit f3682a1.
  • 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.py with parser-backed edits.
  • Added a host inventory mutation operation and routed host:add through it.
  • Vendored and verified the pinned tomlkit runtime 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.md for 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.md
  • docs/task-reference.md
  • docs/add-a-tool.md
  • docs/add-an-app.md
  • docs/src/features/maison-007-parser-backed-toml-mutations/design.md
  • docs/src/features/maison-007-parser-backed-toml-mutations/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-nxg, commit ef9fde6.
  • Implementation coordinator: maison-mol-imq.
  • Contract-test task: maison-mol-imq.1, commit da88a62.
  • Parser implementation task: maison-mol-imq.2, commit dfb7a7f.
  • 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 fcntl lock and local journal helper in .mise/lib/repository_mutation.py.
  • Routed tool, package, app, host, and flake.lock update repository mutations through the shared lock.
  • Added journal helpers in .mise/lib/transaction.sh and 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, and app:update remain 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.md
  • docs/operations.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-008-repository-mutation-locking/design.md
  • docs/src/features/maison-008-repository-mutation-locking/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-0pc, commit 6b168ba.
  • Implementation coordinator: maison-mol-auy.
  • Contract-test task: maison-mol-auy.1, commit f9b4151.
  • Lock/journal implementation task: maison-mol-auy.2, commit 9e6562d.
  • 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.sh and called it from all repository-writing authoring tasks.
  • Detected deployed snapshots as .maison-revision without .git and returned clear source/overlay guidance.
  • Preserved runtime and read-only command behavior in gitless deployed snapshots.
  • Kept host:add target-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 .git markers 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.md
  • docs/operations.md
  • docs/deployment.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-009-authoring-checkout-guard/design.md
  • docs/src/features/maison-009-authoring-checkout-guard/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-her, commit 73d882b.
  • Implementation coordinator: maison-mol-kid.
  • Contract-test task: maison-mol-kid.1, commit 5ccf192.
  • Checkout-implementation task: maison-mol-kid.2, commit b44fb98.
  • 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.toml as 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_SCHEMA so 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.md
  • docs/add-a-host.md
  • docs/deployment.md
  • docs/src/reference/inventory.md
  • docs/src/reference/tooling.md
  • docs/src/features/maison-010-shared-inventory-schema/design.md
  • docs/src/features/maison-010-shared-inventory-schema/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-41t, commit 05ca64e.
  • Implementation coordinator: maison-mol-8ey.
  • Schema-fixtures task: maison-mol-8ey.1, commit d96b24a.
  • Schema-implementation task: maison-mol-8ey.2, commit 692bfca.
  • 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:tests after split cleanup — passed.

Design Reconciliation

Delivered as Designed

  • Added tests/support/processes.py with 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.py
    • tests/test_inventory_behavior.py
    • tests/test_migration_behavior.py
    • tests/test_ownership_boundary.py
    • tests/test_repository_contracts.py
    • tests/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.py evidence 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.md
  • docs/src/development/tooling.md
  • docs/src/reference/tooling.md
  • docs/src/features/maison-011-deterministic-test-suite/design.md
  • docs/src/features/maison-011-deterministic-test-suite/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-9hm, commit be3a910.
  • Implementation coordinator: maison-mol-0dm.
  • Test-helper task: maison-mol-0dm.1, commit 27449b0.
  • Test-suite-split task: maison-mol-0dm.2, commit 2fd4af3.
  • 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.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-012-plan-apply-parity/design.md
  • docs/src/features/maison-012-plan-apply-parity/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation task: maison-mol-88b, commit a7564c0.
  • Implementation coordinator: maison-mol-bmj.
  • Parity-contract task: maison-mol-bmj.1, commit b2e8227.
  • Parity-implementation task: maison-mol-bmj.2, commit 100dd31.
  • 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-dotfiles and standalone operational recovery instructions.

Intentional Changes

  • Specification review selected the explicit maison user restore-dotfiles <backup-directory> --force interface.
  • 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.md
  • docs/recovery.md
  • docs/task-reference.md
  • docs/src/features/maison-013-dotfile-backup-manifests/design.md
  • docs/src/features/maison-013-dotfile-backup-manifests/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation: maison-mol-c8i, commit 572a77b.
  • Backup contract: maison-mol-xg1.1, commit 4a98bdf.
  • Backup implementation: maison-mol-xg1.2, commit ec9f1bb.
  • 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/recover and shfmt -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 unknown when 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.md
  • docs/deployment.md
  • docs/recovery.md
  • docs/operations.md
  • docs/task-reference.md
  • docs/src/features/maison-014-remote-convergence-restoration/design.md
  • docs/src/features/maison-014-remote-convergence-restoration/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation: maison-mol-ugkm, commit 80f33aa1d3318e4cb4513af3cde0ba82f59be8dd.
  • Contract tests: maison-mol-75xd.1, commit 25183b23a3642e0468ce6886d1f7e36eb14df552.
  • Implementation: maison-mol-75xd.2, commit 0cbb56cdcb78e9093465095b227d0085094acadc.
  • 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 into dev)
  • 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 --static against the inventory host name;
  • manages and verifies the America/New_York timezone through systemd runtime state and /etc/localtime;
  • validates SSH configuration with sshd -t and reloads or restarts the active ssh.service or sshd.service;
  • fails when SSH validation or reload fails rather than suppressing the error; and
  • verifies that system-manager.target and prefill-authorized-keys.service are 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, and sshd commands — 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/apply and nixfmt --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 systemctl is unavailable.
  • Managed /etc/localtime from the pinned timezone data while retaining the compatibility /etc/timezone file.
  • Added post-activation verification for hostname, timezone, localtime, required service units, and SSH state.
  • Made sshd -t and 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.py and 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.md
  • docs/deployment.md
  • docs/operations.md
  • docs/recovery.md
  • docs/src/features/maison-015-linux-runtime-verification/design.md
  • docs/src/features/maison-015-linux-runtime-verification/index.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md

Audit Trail

  • Specification reconciliation and reviewed design: maison-mol-08vr, commit ca36568866916f0e1f96170ea97a75bf46f365e6.
  • Runtime contract tests: maison-mol-ln6g.1, commit f4e384adc165381aba24b3554254545bbd63f511.
  • Runtime implementation: maison-mol-ln6g.2, commit 88532ab9ed9024d251ea361a9282502eecb8f787.
  • 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 into dev at 410d571182fba2c9835f2f33f08bee351cb73ac0.

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

Validation Evidence

  • Historical delivery evidence: mise run check:typescript passed with tsc --noEmit and 10 focused behavioral tests before the workspace moved to Terroir.
  • Current Terroir evidence: mise run check and 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/typescript passed 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 --noEmit and focused node:test execution 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:data skips node_modules defensively 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.md
  • docs/src/development/tooling.md
  • docs/src/features/maison-016-pi-typescript-validation/design.md
  • docs/src/features/maison-016-pi-typescript-validation/index.md
  • docs/src/reference/tooling.md
  • docs/src/features/index.md
  • docs/src/planned-features.md
  • docs/src/SUMMARY.md
  • Terroir dotfiles/pi/AGENTS.md
  • dotfiles/opencode/README.md

Audit Trail

  • Specification reconciliation and reviewed design: maison-mol-40gy, commit 4e79de64c8505f50a58cef918b8981458ff4219f.
  • Workspace implementation: maison-mol-1h4n.1, commit 6c7328397ff54fb51eea15e4fb7b4f977e3e6a33.
  • Pager modules and behavioral tests: maison-mol-1h4n.2, commit 65fdd7398a4315ff5ab0db4944d8339a022a84b5.
  • 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

Validation Evidence

  • python3 scripts/setup-tooling.py --json succeeded 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 --noEmit and 10 focused behavioral tests.
  • Local and remote overlay preparation passed; saved overlay state is mode 0600 and host:list / host:validate resolve the Terroir inventory.
  • GitHub checks verified Maison public/unarchived, Terroir private/unarchived, and nix-config private/archived, all with main as 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-box builder 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.md
  • docs/architecture.md
  • docs/deployment.md
  • docs/operations.md
  • docs/recovery.md
  • docs/migration-contract.md
  • docs/src/introduction/project-overview.md
  • docs/src/reference/inventory.md
  • docs/src/development/tooling.md
  • docs/src/planned-features.md
  • docs/src/features/index.md
  • docs/src/SUMMARY.md
  • This implemented feature record

Audit Trail

  • Manifest: ${XDG_STATE_HOME:-$HOME/.local/state}/maison/maison-017/manifest.toml, owner-only mode 0600, explicitly approved and amended to exclude iTerm2.
  • Implementation children: maison-mol-ieqc.1 through .6, all closed in dependency order.
  • Current signed Maison content commits: 8300ed926ae785e05fe4ddd81c213f69edfac98c, 12455ed8ea6bea981fdaef561a88e7037b8469e1, and 774a602a1efc7e836bf14ba7dbe7d4afa652d4f8.
  • Current signed Terroir content commits: bfc6dc9b71b99dea1a0b4d4a5a38c16acb74a5ce, e655386928ec315286fc448bdc4a006c6b5160f3, and 97a4b41ebc7d57d04acbb759e4e7e10e999709c8.
  • Source planning/design commits: 43e5fada1b0b88c3d7e33bd2e82057fa83a7f7aa and 4ba83d44a31afdd909635650f4db42e76a980d93.

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 -n and shellcheck for bootstrap and generated host setup scripts — passed.
  • ruff check and ruff format --check for 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_OVERLAY and preserved explicit --overlay precedence and saved-state behavior.
  • Added interactive immediate/deferred onboarding with a safe no-activation path.
  • Delegated generated host setup to Maison’s existing host:add task.
  • 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_SOURCE is compatibility-only; new documentation uses MAISON_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.md
  • docs/operations.md
  • docs/add-a-host.md
  • docs/src/reference/tooling.md
  • overlay_template/README.md
  • overlay_template/dotfiles/README.md
  • docs/src/SUMMARY.md
  • docs/src/planned-features.md
  • docs/src/features/maison-overlay-copier-bootstrap/design.md
  • docs/src/features/maison-overlay-copier-bootstrap/index.md
  • docs/src/features/index.md

Audit Trail

  • Planning and reviewed design: maison-mol-5s9, commit 8cbd6eb.
  • Copier template and generated host setup: maison-mol-xke.2, commit e7b2c11.
  • Bootstrap onboarding and overlay source behavior: maison-mol-xke.1, commit 246aceb.
  • Reader documentation: maison-mol-xke.3, commit 11f37a8.
  • 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 into main at 14a5e92.

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 into main)
  • 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

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/maison and shellcheck -x bin/maison — passed.
  • uv run scripts/check-docs.py — passed.
  • rumdl check on 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-level status and publish commands.
  • 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/removed commits after transaction completion.
  • Preserved unrelated work, ignored files, public read-only/convergence fallback, and existing maison sync semantics.
  • 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 sync refresh 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.md
  • docs/architecture.md
  • docs/operations.md
  • docs/task-reference.md
  • docs/src/reference/tooling.md
  • docs/package-policy.md
  • docs/add-a-tool.md
  • docs/add-an-app.md
  • docs/src/planned-features.md
  • docs/src/features/index.md
  • docs/src/SUMMARY.md
  • docs/src/features/maison-overlay-authoring-lifecycle/design.md
  • docs/src/features/maison-overlay-authoring-lifecycle/index.md

Audit Trail

  • Specification reconciliation: maison-mol-43o, commit db23f2165d58e830223eb2d497b2d7f084ef294b.
  • Shared overlay Git lifecycle and commands: maison-mol-0tm.1, commit d2e7b151a31c0ad6e88a0c8a8632007c944d086a.
  • Transactional software mutation integration: maison-mol-0tm.2, commit 7c75189f4c7578e82a7b209ddcae67e0404de693.
  • Command surface and reader documentation: maison-mol-0tm.3, commit 10b467bcc04aea38d6dde41f9a9d54d8eada8cb6.
  • 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 into main at dc385400c78c80f81bd7cd9b31b20c629b86fc6d.