Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Running the switch builds:
- Editor (Neovim config with the rose-pine moon theme)
- Terminal (WezTerm config with the rose-pine moon theme and dimmed unfocused windows)
- Agent configs (Claude, Codex, opencode all share one AGENTS.md)
- Optional Pi theme, generic UI settings, model overrides, and terminal-title extension
- Optional Pi theme and terminal-title extension, generic UI settings and model overrides, plus two deliberately pinned third-party Pi packages

## Prerequisites

Expand Down Expand Up @@ -129,7 +129,7 @@ If you don't use it, just remove it from `brews` in your copy.
- `home.nix` - user-level config: shell, packages, prompt, and the symlinks described below.
- `rebuild.sh` - re-applies the config after the first switch.
Run this every time you make a change.
- `home/` - the actual config files that get symlinked into place; the sections below explain the shared symlink model and Pi's narrower per-file setup.
- `home/` - the actual config files that get symlinked into place; the sections below explain the shared symlink model and Pi's narrower selective setup.

## How the symlinks work

Expand All @@ -151,9 +151,18 @@ npm install -g --ignore-scripts @earendil-works/pi-coding-agent
brew install --cask kunchenguid/tap/pi-launcher
```

Home Manager links only four authored Pi files: the theme, `models.json`, `settings.json`, and `terminal-status-title.js`. It deliberately does not manage `~/.pi/agent`, so `auth.json`, sessions, trust decisions, caches, and other runtime state remain local. The model overrides contain no credentials or endpoint settings, do not choose a default model, and only take effect after you authenticate Pi yourself. Pi may intentionally rewrite the tracked settings file. Review any drift and commit it only when it is a deliberate configuration change.
Home Manager owns exactly two repository-authored Pi directories: `~/.pi/agent/themes` and `~/.pi/agent/extensions`. It also links `models.json` and `settings.json` as individual files. The local extension directory is for public, repository-authored extensions only - third-party package code never belongs there. Run `/reload` after editing a local extension or other Pi resources. The terminal-title extension shows a spinner while Pi is working, then a completion mark with the session name or current directory. The `rose-pine-moon` theme was authored clean-room from the public [Rosé Pine Moon palette](https://rosepinetheme.com/palette) and Pi's [public theme schema](https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json), not from a private or live theme file.

The terminal-title extension shows a spinner while Pi is working, then a completion mark with the session name or current directory. Run `/reload` after editing it. The `rose-pine-moon` theme was authored clean-room from the public [Rosé Pine Moon palette](https://rosepinetheme.com/palette) and Pi's [public theme schema](https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json), not from a private or live theme file. This is an additive post-video layer; it installs no packages or launcher configuration.
Pi's package system declares two third-party sources in the linked global `settings.json`:

- `npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6` - the exact public npm release from `ryan_nookpi`.
- `git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055` - the exact public `algal` commit for experimental OpenAI server-side compaction.

The version and commit are immutable pins, so Pi does not move them during package updates. Deliberate updates require a new source and security audit, followed by an explicit pin change in `home/.pi/agent/settings.json`. On Pi 0.82.0, global settings declarations install missing pinned packages automatically at startup. No one-time install command is required. Pi keeps the downloaded npm and git package trees in its own unmanaged `~/.pi/agent/npm` and `~/.pi/agent/git` runtime directories, outside Home Manager and Git tracking.

Both packages execute with your full user permissions and must be trusted like any other executable code. The compaction package is experimental, sends the relevant OpenAI compaction and continuity data to OpenAI, and upstream declares the stale peer range `>=0.80.9 <0.81.0`; this exact immutable ref was locally proven to load and perform remote compaction on Pi 0.82.0. Do not treat that proof as a guarantee for a different Pi version or a different package ref.

Home Manager deliberately does not manage `~/.pi/agent` itself, or Pi authentication, sessions, trust decisions, caches, npm/git package trees, or any other runtime state. The model overrides contain no credentials or endpoint settings, do not choose a default model, and only take effect after you authenticate Pi yourself. This remains an additive post-video layer: it does not install Pi, a launcher, or package source code into this repository.

## Notes

Expand Down
36 changes: 30 additions & 6 deletions home.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ config, pkgs, user, ... }:
{ config, lib, pkgs, user, ... }:

let
dotfiles = "${config.home.homeDirectory}/.dotfiles";
Expand Down Expand Up @@ -63,15 +63,39 @@ in
home.file.".claude/settings.json".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";

# Keep Pi's credential and runtime state local by linking only authored files.
home.file.".pi/agent/themes/rose-pine-moon.json".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/themes/rose-pine-moon.json";
# Keep Pi's credential and runtime state local by linking only authored files and directories.
home.file.".pi/agent/themes".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/themes";
home.file.".pi/agent/extensions".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions";
home.file.".pi/agent/models.json".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/models.json";
home.file.".pi/agent/settings.json".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/settings.json";
home.file.".pi/agent/extensions/terminal-status-title.js".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/terminal-status-title.js";

# Remove only the two legacy managed child links before Home Manager adopts their directories.
home.activation.migratePiAuthoredDirectories = lib.hm.dag.entryBefore [ "checkLinkTargets" ] ''
removeLegacyPiLink() {
local target="$1"
local source="$2"
local relativeTarget="''${target#"$HOME/"}"
local linkTarget

[ -L "$target" ] || return 0
linkTarget="$(readlink "$target")"
case "$linkTarget" in
/nix/store/*-home-manager-files/"$relativeTarget") ;;
*) return 0 ;;
esac
[ "$(readlink -f "$target")" = "$(readlink -f "$source")" ] || return 0
$DRY_RUN_CMD rm "$target"
}

removeLegacyPiLink "$HOME/.pi/agent/themes/rose-pine-moon.json" "${dotfiles}/home/.pi/agent/themes/rose-pine-moon.json"
removeLegacyPiLink "$HOME/.pi/agent/extensions/terminal-status-title.js" "${dotfiles}/home/.pi/agent/extensions/terminal-status-title.js"
$DRY_RUN_CMD rmdir "$HOME/.pi/agent/themes" 2>/dev/null || true
$DRY_RUN_CMD rmdir "$HOME/.pi/agent/extensions" 2>/dev/null || true
'';

home.file.".claude/CLAUDE.md".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
Expand Down
6 changes: 5 additions & 1 deletion home/.pi/agent/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
"theme": "rose-pine-moon",
"steeringMode": "all",
"followUpMode": "all",
"collapseChangelog": true
"collapseChangelog": true,
"packages": [
"npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6",
"git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055"
]
}
99 changes: 99 additions & 0 deletions tests/pi-home-manager.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

python3 - "$repo_root" <<'PY'
import json
import sys
from pathlib import Path

root = Path(sys.argv[1])
settings = json.loads((root / "home/.pi/agent/settings.json").read_text())
expected_packages = [
"npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6",
"git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055",
]
assert settings.get("packages") == expected_packages, "Pi package declarations must be exactly the two audited pins"

home_nix = (root / "home.nix").read_text()
required_links = {
'.pi/agent/themes': '${dotfiles}/home/.pi/agent/themes',
'.pi/agent/extensions': '${dotfiles}/home/.pi/agent/extensions',
'.pi/agent/models.json': '${dotfiles}/home/.pi/agent/models.json',
'.pi/agent/settings.json': '${dotfiles}/home/.pi/agent/settings.json',
}
for destination, source in required_links.items():
declaration = f'home.file."{destination}".source =\n config.lib.file.mkOutOfStoreSymlink "{source}";'
assert declaration in home_nix, f"missing exact out-of-store link: {destination}"

for old_child in [
'.pi/agent/themes/rose-pine-moon.json',
'.pi/agent/extensions/terminal-status-title.js',
]:
assert f'home.file."{old_child}"' not in home_nix, f"legacy child link remains: {old_child}"

for forbidden in [
'.pi/agent', '.pi/agent/auth.json', '.pi/agent/sessions', '.pi/agent/trust.json',
'.pi/agent/npm', '.pi/agent/git', '.pi/agent/cache',
]:
assert f'home.file."{forbidden}"' not in home_nix, f"Pi runtime path became managed: {forbidden}"

assert 'entryBefore [ "checkLinkTargets" ]' in home_nix, "migration must run before Home Manager collision checks"
assert 'removeLegacyPiLink "$HOME/.pi/agent/themes/rose-pine-moon.json"' in home_nix
assert 'removeLegacyPiLink "$HOME/.pi/agent/extensions/terminal-status-title.js"' in home_nix
assert (root / "home/.pi/agent/themes/rose-pine-moon.json").is_file()
assert (root / "home/.pi/agent/extensions/terminal-status-title.js").is_file()
assert [p.relative_to(root / "home/.pi/agent/themes").as_posix() for p in (root / "home/.pi/agent/themes").rglob("*") if p.is_file()] == ["rose-pine-moon.json"]
assert [p.relative_to(root / "home/.pi/agent/extensions").as_posix() for p in (root / "home/.pi/agent/extensions").rglob("*") if p.is_file()] == ["terminal-status-title.js"]
PY

# Build only the Home Manager activation package. This never activates the captain's configuration.
activation=$(nix build --no-link --print-out-paths \
.#darwinConfigurations.mac.config.home-manager.users.kunchen.home.activationPackage)

probe=$(mktemp -d)
trap 'rm -rf "$probe"' EXIT
fake_home="$probe/home"
mkdir -p "$fake_home/.pi/agent/themes" "$fake_home/.pi/agent/extensions"
legacy_tree="$probe/home-manager-files"
mkdir -p "$legacy_tree/.pi/agent/themes" "$legacy_tree/.pi/agent/extensions"
ln -s "$repo_root/home/.pi/agent/themes/rose-pine-moon.json" \
"$legacy_tree/.pi/agent/themes/rose-pine-moon.json"
ln -s "$repo_root/home/.pi/agent/extensions/terminal-status-title.js" \
"$legacy_tree/.pi/agent/extensions/terminal-status-title.js"
legacy_home_manager_files=$(nix store add-path "$legacy_tree")
ln -s "$legacy_home_manager_files/.pi/agent/themes/rose-pine-moon.json" \
"$fake_home/.pi/agent/themes/rose-pine-moon.json"
ln -s "$legacy_home_manager_files/.pi/agent/extensions/terminal-status-title.js" \
"$fake_home/.pi/agent/extensions/terminal-status-title.js"

test "$(readlink -f "$fake_home/.pi/agent/themes/rose-pine-moon.json")" = \
"$repo_root/home/.pi/agent/themes/rose-pine-moon.json"

# Execute only the generated pre-check migration block against a disposable HOME.
awk '
/_iNote "Activating %s" "migratePiAuthoredDirectories"/ { enabled = 1; next }
/_iNote "Activating %s" "checkLinkTargets"/ { exit }
enabled { print }
' "$activation/activate" | \
sed "s|/Users/kunchen/.dotfiles|$repo_root|g" > "$probe/migrate.sh"
HOME="$fake_home" DRY_RUN_CMD='' bash -e "$probe/migrate.sh"

test ! -e "$fake_home/.pi/agent/themes"
test ! -e "$fake_home/.pi/agent/extensions"
ln -s "$repo_root/home/.pi/agent/themes" "$fake_home/.pi/agent/themes"
ln -s "$repo_root/home/.pi/agent/extensions" "$fake_home/.pi/agent/extensions"
test -L "$fake_home/.pi/agent/themes"
test -L "$fake_home/.pi/agent/extensions"

# Safe skip paths must succeed and leave unrelated user state untouched.
mkdir -p "$probe/unmanaged/.pi/agent/themes" "$probe/unmanaged/.pi/agent/extensions"
touch "$probe/unmanaged/.pi/agent/themes/user-theme.json"
ln -s "$repo_root/home/.pi/agent/extensions/terminal-status-title.js" \
"$probe/unmanaged/.pi/agent/extensions/user-extension.js"
HOME="$probe/unmanaged" DRY_RUN_CMD='' bash -e "$probe/migrate.sh"
test -f "$probe/unmanaged/.pi/agent/themes/user-theme.json"
test -L "$probe/unmanaged/.pi/agent/extensions/user-extension.js"

echo "Pi package declarations, runtime boundary, link shape, and child-to-parent migration passed."