Compare commits

...

1 Commits

Author SHA1 Message Date
638c26c09b feat(pi): install the coding agent for a side-by-side trial (task 0020)
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.
2026-07-21 19:46:29 -04:00
4 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
## 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
1. 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 = true` line on `neogaia` and never rides silently onto future Hosts.
2. As the operator, I want Pi enabled only on `neogaia`, so that the experiment is contained to the machine I actually drive.
3. 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.
4. 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.
5. 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.
6. 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.
7. 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.
8. 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.
9. 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.
10. 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.
11. 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.
12. As the operator, I want backing Pi out to be a single `enable` flip, 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-code` Module, declaring a single `enable` option under the `modules` tree 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-agent` module for the primary user. That upstream module ships the `pi-coding-agent` package (base package set) and manages the state directory's declared files.
- The Module freezes exactly one file through that upstream module's `settings` option: 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`, and `configDir` are all left at their defaults, so home-manager renders nothing but `settings.json` into `~/.pi/agent/` and Pi owns every other path there.
- Rationale for the single frozen file: among the files the upstream module can render, `settings.json` is 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-code` Module's treatment of its login.
- Security posture is inherited, not added: the `claude-code` Module already widens the sudo credential cache system-wide for the primary user, so Pi's `bash` tool 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 `neogaia` Host enables the new Module with a single `enable = 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 `neogaia` Host, 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 `neogaia` Host evaluates and its system toplevel builds via `nix flake check` (the `checks.x86_64-linux.neogaia` target). Building the toplevel drives the Auto-loader discovering the new Module, the `programs.pi-coding-agent` home-manager integration resolving, the frozen `settings.json` rendering, and every enabled Module's config merging without conflict.
- No new seam is introduced. This is the single high seam that `laptop-mvi.md` established 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`, and `nvim` Modules are all verified this way — enabled on `neogaia`, 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.md` skill format.
- A Pi-specific sudo guard (or any tool-permission guard) built as a Pi extension.
- An `AGENTS.md` context 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 `unstable` overlay 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. Freezing `settings.json` therefore 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 `configDir` mean 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.json` is 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.

View File

@@ -0,0 +1,27 @@
---
spec: pi-coding-agent
---
## What to build
Add a `pi` module, auto-discovered like every other feature and inert until a host enables it, that installs Pi for the primary user through the home-manager `programs.pi-coding-agent` module.
On enable it freezes exactly one file — `settings.json` — pinning the default provider to Anthropic and the default model to Opus (the exact model-id string confirmed against Pi's own model catalogue), and disabling analytics.
Everything else — agent context, skills, extensions, keybindings, custom providers — is left at its default, so home-manager renders nothing but `settings.json` and Pi owns the rest of `~/.pi/agent/`.
Pi authenticates by reusing the existing Claude subscription, and that credential is left unmanaged by the flake so no secret enters the repo and re-auth survives rebuilds, mirroring how the `claude-code` module treats its login.
Lay the module out as a directory (not a single file), and enable it on `neogaia` alone with a single `enable = true`.
## Acceptance criteria
- [x] A `pi` module exists as its own directory, declares a single `enable` option under the `modules` tree, guards its body with the Enable convention, and stays inert until a host enables it.
- [x] On enable, the module turns on `programs.pi-coding-agent` for the primary user from the base package set, with no other host affected.
- [x] The frozen `settings.json` sets the default provider to Anthropic, the default model to Opus (exact model-id verified against Pi's catalogue), and disables analytics — and no other upstream option (`context`, `models`, `keybindings`, `extraPackages`, `configDir`) is set.
- [x] Pi's credential and all of its writable state (`~/.pi/agent/` beyond `settings.json`) are left unmanaged by the flake.
- [x] `neogaia` enables the module with a single `enable = true` and its system toplevel still builds via `nix flake check` (the `checks.x86_64-linux.neogaia` target).
- [x] Disabling the module is a one-line `enable` flip that leaves no flake-managed residue.
## Implementation Notes
- **Model-id and analytics key confirmed against Pi 0.80.7 at build time.** The `pi-coding-agent` package pins `0.80.7`; its `dist/core/model-resolver.js` defaults the `anthropic` provider to `claude-opus-4-8`, which is the Opus id used. The analytics key is `enableAnalytics` (boolean, default `false`), per the package's own `docs/settings.md`. Both were read from the built store path, not guessed.
- **Verified through the primary seam.** `config.modules.pi.enable` and `programs.pi-coding-agent.enable` both evaluate `true` on `neogaia`; the rendered `settings.json` is exactly `{"defaultModel":"claude-opus-4-8","defaultProvider":"anthropic","enableAnalytics":false}`; only one file (`settings.json`) is rendered under `~/.pi/agent`; and `checks.x86_64-linux.neogaia` builds green with `pi-coding-agent-0.80.7` included.
- **No deviations from the spec.** The diff is the module plus one `enable = true` line — every "Out of Scope" item (agent context/`AGENTS.md`, skills, extensions, keybindings, custom providers, Pi-specific sudo guard) is left out.
- **Review follow-through.** `/review-uncommitted` rated Risk **Low** and Spec **clean**. Standards flagged four comment-convention issues on the new module (a semicolon in a comment, an overloaded file-top header duplicating the inline rationale, and a cross-file clause on the model-id comment); all were fixed in the diff, so the header is now a two-sentence purpose line mirroring the sibling `claude-code` module and the frozen-settings rationale lives only at its inline site. No findings left unaddressed.

View File

@@ -50,6 +50,7 @@
modules.nvim.enable = true; modules.nvim.enable = true;
modules.claude-code.enable = true; modules.claude-code.enable = true;
modules.gitea-axi.enable = true; modules.gitea-axi.enable = true;
modules.pi.enable = true;
time.timeZone = "America/New_York"; time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8"; i18n.defaultLocale = "en_GB.UTF-8";

28
modules/pi/pi.nix Normal file
View File

@@ -0,0 +1,28 @@
{
config,
lib,
...
}:
# Pi, a terminal coding agent, for the primary user, configured through
# home-manager, which ships the package and manages ~/.pi/agent.
# The login credential is left unmanaged, so it survives rebuilds.
let
cfg = config.modules.pi;
user = config.user.name;
in
{
options.modules.pi.enable = lib.mkEnableOption "Pi, a terminal coding agent, configured via home-manager";
config = lib.mkIf cfg.enable {
home-manager.users.${user}.programs.pi-coding-agent = {
enable = true;
settings = {
defaultProvider = "anthropic";
# Pi's catalogue id for Opus.
defaultModel = "claude-opus-4-8";
enableAnalytics = false;
};
};
};
}