Add a `pi` module that turns on the home-manager `programs.pi-coding-agent` integration for the primary user and freezes a single settings.json: the default provider set to Anthropic, the default model to Opus (claude-opus-4-8, Pi's own catalogue id), and analytics disabled. Everything else in ~/.pi/agent — the credential and all self-modification state — is left unmanaged, so login survives rebuilds and backing Pi out is a one-line enable flip. Enabled on neogaia alone.
10 KiB
Problem Statement
I run Claude Code as my coding agent on neogaia, wired deeply into this flake: skills, a sudo-guard hook, shared agent instructions, and the whole .claude/ workflow.
Pi is a young, fast-moving, self-modifying terminal coding agent that I want to evaluate as an alternative harness.
I need to install it on the laptop in a way that lets me try it honestly — same model, same account — without unpicking any of the Claude Code setup and without committing myself to Pi before it has earned a permanent place.
The evaluation only means something if the one variable under test is the harness itself, and if backing Pi out later is trivial.
Solution
Add a new pi Module that installs Pi for the primary user on neogaia, enabled by the Enable convention like every other feature.
It flips the home-manager programs.pi-coding-agent module on and freezes exactly one file — settings.json — pinning the provider and model so Pi runs the same brain as Claude Code (Anthropic, Opus) and disabling analytics so Pi never attempts a runtime write to that frozen file.
Everything else is left to Pi's own writable state directory (~/.pi/agent/): no agent context, no skills, no extensions, no keybindings, no custom model providers.
Pi authenticates by reusing my existing Claude subscription, and that credential is deliberately left unmanaged by the flake so no secret touches the repo and re-auth survives rebuilds — exactly as the claude-code Module already treats its login.
The result is a minimal, non-disruptive, side-by-side experiment: Claude Code stays the daily driver, Pi sits alongside it, and removing Pi is a one-line enable flip.
User Stories
- As the operator, I want Pi installed as its own auto-discovered Module that stays inert until a Host enables it, so that Pi reads as one more
enable = trueline onneogaiaand never rides silently onto future Hosts. - As the operator, I want Pi enabled only on
neogaia, so that the experiment is contained to the machine I actually drive. - As the operator, I want Pi to run Anthropic's Opus by default, matching Claude Code's model, so that any difference I observe between the two is attributable to the harness and not the model.
- As the operator, I want Pi's provider and default model pinned reproducibly in the flake, so that the same Pi configuration would rebuild identically on any Host.
- As the operator, I want Pi's analytics disabled in that same pinned configuration, so that Pi never attempts the one runtime write it would otherwise make to the frozen settings file.
- As the operator, I want Pi to authenticate by reusing my existing Claude subscription rather than a separate API key, so that the comparison hits the same account at zero marginal cost.
- As the operator, I want Pi's credential left unmanaged by the flake, so that no secret is committed to a public repo and my authentication survives rebuilds.
- As the operator, I want Pi installed with no agent context, no skills, and no extensions, so that I see Pi's native behaviour rather than a port of the Claude Code setup.
- As the operator, I want Pi to keep full ownership of its writable state directory, so that its self-modifying behaviour — generated extensions, skills, prompt templates, installed packages, sessions — works unimpeded.
- As the operator, I want Pi sourced from the base package set and bumped with the normal flake update, so that it stays reasonably fresh without a second package set evaluated for one tool.
- As the operator, I want the Module laid out as a directory rather than a single file, so that promoting Pi later — adding rendered skills or extensions — is an additive change rather than a restructure.
- As the operator, I want backing Pi out to be a single
enableflip, so that an experiment that does not pan out leaves no residue.
Implementation Decisions
pi Module
- A new Module under its own directory, following the shape of the existing
claude-codeModule, declaring a singleenableoption under themodulestree and guarding its body with the Enable convention. - The directory layout (rather than a single file) is chosen so that later rendering of a skills or extensions source is an additive edit, not a move.
- On enable, the Module turns on the home-manager
programs.pi-coding-agentmodule for the primary user. That upstream module ships thepi-coding-agentpackage (base package set) and manages the state directory's declared files. - The Module freezes exactly one file through that upstream module's
settingsoption: the default provider set to Anthropic, the default model set to Opus (the exact model-id string confirmed against Pi's own model catalogue at build time), and analytics disabled. - No other upstream option is set:
context,models,keybindings,extraPackages, andconfigDirare all left at their defaults, so home-manager renders nothing butsettings.jsoninto~/.pi/agent/and Pi owns every other path there. - Rationale for the single frozen file: among the files the upstream module can render,
settings.jsonis the only one Pi writes at runtime, and only its analytics keys — disabling analytics removes even that, so freezing it is safe and never fights Pi's self-modification, which targets other paths entirely. - Credentials are out of the flake by design. Pi reuses the Claude subscription via its own login, and the resulting token lives under Pi's state directory, which home-manager does not overwrite — mirroring the
claude-codeModule's treatment of its login. - Security posture is inherited, not added: the
claude-codeModule already widens the sudo credential cache system-wide for the primary user, so Pi'sbashtool can spend a warm credential. Pi does not pass through Claude Code's cold-cache sudo guard, and no equivalent guard is added for Pi in this deliverable. This is an accepted, bounded posture for a supervised single-user experiment.
neogaia Host
- The
neogaiaHost enables the new Module with a singleenable = true, alongside its existing feature list. - No other Host is touched; the desktop and servers do not yet exist in the flake and would each opt in on their own terms.
Testing Decisions
- A good test here asserts externally-observable evaluation and build success of the whole
neogaiaHost, not the internals of the Module. Installing a Module is config authoring, and the meaningful unit is the Host it composes into. - Primary seam (required, reused): the
neogaiaHost evaluates and its system toplevel builds vianix flake check(thechecks.x86_64-linux.neogaiatarget). Building the toplevel drives the Auto-loader discovering the new Module, theprograms.pi-coding-agenthome-manager integration resolving, the frozensettings.jsonrendering, and every enabled Module's config merging without conflict. - No new seam is introduced. This is the single high seam that
laptop-mvi.mdestablished and that every Module in this repo is verified through; an install-a-Module feature does not justify a second one. - No unit-level test of the Module in isolation. The config-merge model makes the whole-Host build the highest and most meaningful seam.
- Prior art: the existing
claude-code,gitea-axi,fish,tmux, andnvimModules are all verified this way — enabled onneogaia, exercised by the toplevel build. - The genuine end-to-end confirmation — launching
pi, authenticating against the Claude subscription, and running the agent — is a manual post-build action on the real machine and is not automated, consistent with how interactive login is handled for Claude Code.
Out of Scope
- Any harvest/promote pipeline for sharing Pi's self-modifications across machines — rendering a repo-held skills or extensions source into Pi's state directory. Deferred until Pi earns a permanent slot.
- A shared skill source between Pi and Claude Code for a fairer comparison, exploiting their common
SKILL.mdskill format. - A Pi-specific sudo guard (or any tool-permission guard) built as a Pi extension.
- An
AGENTS.mdcontext carrying the operator's cross-agent house rules (commit conventions, no attribution trailer, markdown and filename rules). Its absence means Pi's commits will not automatically follow those conventions during the experiment; this is accepted. - Custom model providers (
models.json), custom keybindings, and any non-Anthropic provider. - Moving Pi to the
unstableoverlay for head-of-channel freshness. - Wiring the credential through a secret store; sops is not yet wired on this Host, and Pi follows the same unmanaged-credential path as Claude Code until it is.
- Enabling Pi on any Host other than
neogaia. - An ADR recording the "promote agent self-modifications into the flake rather than sync mutable agent state" stance. It is not enacted by this deliverable; if Pi is promoted and the harvest pipeline is built, that becomes a real, repo-wide decision — covering Claude Code too — worth recording then.
Further Notes
- Why the single frozen file matters: Pi's real self-modification surface (generated extensions, skills, prompt templates, installed package code, sessions, trust decisions) lives in paths under
~/.pi/agent/that the upstream home-manager module never manages, regardless of what the Module declares. Freezingsettings.jsontherefore constrains none of it, and disabling analytics removes the only runtime write that file would otherwise receive. - Promotion path is left open by construction: the directory-shaped Module and the untouched
configDirmean that, if Pi sticks, a repo-held skills or extensions source can be rendered into the state directory with a writable-directory / read-only-leaf layout — the same mechanism this repo already uses for Claude Code skills — without restructuring anything decided here. - Fair-comparison intent: matching the model (Opus) and the account (the Claude subscription) is deliberate, so the experiment isolates the harness. Choosing a lighter model or a separate key would introduce a second variable and blur the read.
- Reversibility: because only
settings.jsonis frozen and the credential and all self-modification state live outside the flake, disabling the Module removes Pi cleanly, leaving Pi's own state directory as the only residue on disk.