From a3ffb794ea24dc936828b2f8aecfc0ca8127e62a Mon Sep 17 00:00:00 2001 From: kunchenguid Date: Fri, 31 Jul 2026 19:42:24 -0700 Subject: [PATCH 1/4] Configure pinned Pi packages and resource directories --- CLAUDE.md | 1 + README.md | 15 +++++-- home.nix | 28 ++++++++++--- home/.pi/agent/settings.json | 6 ++- tests/pi-home-manager.sh | 79 ++++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 10 deletions(-) create mode 120000 CLAUDE.md create mode 100755 tests/pi-home-manager.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index a4b5ae16..a75cb709 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/home.nix b/home.nix index a082c397..9a305556 100644 --- a/home.nix +++ b/home.nix @@ -1,4 +1,4 @@ -{ config, pkgs, user, ... }: +{ config, lib, pkgs, user, ... }: let dotfiles = "${config.home.homeDirectory}/.dotfiles"; @@ -63,15 +63,31 @@ 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" + if [ -L "$target" ] && [ "$(readlink "$target")" = "$source" ]; then + $DRY_RUN_CMD rm "$target" + fi + } + + 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"; diff --git a/home/.pi/agent/settings.json b/home/.pi/agent/settings.json index 3f3a85f1..625b9c51 100644 --- a/home/.pi/agent/settings.json +++ b/home/.pi/agent/settings.json @@ -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" + ] } diff --git a/tests/pi-home-manager.sh b/tests/pi-home-manager.sh new file mode 100755 index 00000000..97fb6ac6 --- /dev/null +++ b/tests/pi-home-manager.sh @@ -0,0 +1,79 @@ +#!/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" +ln -s /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json \ + "$fake_home/.pi/agent/themes/rose-pine-moon.json" +ln -s /Users/kunchen/.dotfiles/home/.pi/agent/extensions/terminal-status-title.js \ + "$fake_home/.pi/agent/extensions/terminal-status-title.js" + +# 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" > "$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" + +echo "Pi package declarations, runtime boundary, link shape, and child-to-parent migration passed." From 7e94454a1d9cb31ada1711d497ed2a5c6df6216e Mon Sep 17 00:00:00 2001 From: kunchenguid Date: Fri, 31 Jul 2026 19:45:27 -0700 Subject: [PATCH 2/4] no-mistakes(review): Recognize Home Manager links during Pi directory migration --- home.nix | 14 +++++++++++--- tests/pi-home-manager.sh | 10 ++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/home.nix b/home.nix index 9a305556..5372a2b4 100644 --- a/home.nix +++ b/home.nix @@ -78,9 +78,17 @@ in removeLegacyPiLink() { local target="$1" local source="$2" - if [ -L "$target" ] && [ "$(readlink "$target")" = "$source" ]; then - $DRY_RUN_CMD rm "$target" - fi + local relativeTarget="''${target#"$HOME/"}" + local linkTarget + + [ -L "$target" ] || return + linkTarget="$(readlink "$target")" + case "$linkTarget" in + /nix/store/*-home-manager-files/"$relativeTarget") ;; + *) return ;; + esac + [ "$(readlink -f "$target")" = "$(readlink -f "$source")" ] || return + $DRY_RUN_CMD rm "$target" } removeLegacyPiLink "$HOME/.pi/agent/themes/rose-pine-moon.json" "${dotfiles}/home/.pi/agent/themes/rose-pine-moon.json" diff --git a/tests/pi-home-manager.sh b/tests/pi-home-manager.sh index 97fb6ac6..b48db051 100755 --- a/tests/pi-home-manager.sh +++ b/tests/pi-home-manager.sh @@ -56,11 +56,17 @@ probe=$(mktemp -d) trap 'rm -rf "$probe"' EXIT fake_home="$probe/home" mkdir -p "$fake_home/.pi/agent/themes" "$fake_home/.pi/agent/extensions" -ln -s /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json \ +home_manager_files=$(readlink -f "$activation/home-files") +ln -s "$home_manager_files/.pi/agent/themes/rose-pine-moon.json" \ "$fake_home/.pi/agent/themes/rose-pine-moon.json" -ln -s /Users/kunchen/.dotfiles/home/.pi/agent/extensions/terminal-status-title.js \ +ln -s "$home_manager_files/.pi/agent/extensions/terminal-status-title.js" \ "$fake_home/.pi/agent/extensions/terminal-status-title.js" +test "$(readlink "$fake_home/.pi/agent/themes/rose-pine-moon.json")" != \ + /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json +test "$(readlink -f "$fake_home/.pi/agent/themes/rose-pine-moon.json")" = \ + /Users/kunchen/.dotfiles/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 } From 27d7d064923ca572df1e31ffa704353d0410c544 Mon Sep 17 00:00:00 2001 From: kunchenguid Date: Fri, 31 Jul 2026 19:47:36 -0700 Subject: [PATCH 3/4] no-mistakes(review): Canonicalize expected Pi migration test source --- tests/pi-home-manager.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/pi-home-manager.sh b/tests/pi-home-manager.sh index b48db051..136bb48e 100755 --- a/tests/pi-home-manager.sh +++ b/tests/pi-home-manager.sh @@ -64,8 +64,11 @@ ln -s "$home_manager_files/.pi/agent/extensions/terminal-status-title.js" \ test "$(readlink "$fake_home/.pi/agent/themes/rose-pine-moon.json")" != \ /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json +expected_theme_source=$(readlink -f \ + /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json) +test -n "$expected_theme_source" test "$(readlink -f "$fake_home/.pi/agent/themes/rose-pine-moon.json")" = \ - /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json + "$expected_theme_source" # Execute only the generated pre-check migration block against a disposable HOME. awk ' From f500d5bbfb6d58b0861018b471c8ab7c2ad11415 Mon Sep 17 00:00:00 2001 From: kunchenguid Date: Fri, 31 Jul 2026 19:53:35 -0700 Subject: [PATCH 4/4] no-mistakes(document): Refresh Pi link documentation --- README.md | 2 +- home.nix | 6 +++--- tests/pi-home-manager.sh | 31 +++++++++++++++++++++---------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a75cb709..76848059 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/home.nix b/home.nix index 5372a2b4..3f7753fe 100644 --- a/home.nix +++ b/home.nix @@ -81,13 +81,13 @@ in local relativeTarget="''${target#"$HOME/"}" local linkTarget - [ -L "$target" ] || return + [ -L "$target" ] || return 0 linkTarget="$(readlink "$target")" case "$linkTarget" in /nix/store/*-home-manager-files/"$relativeTarget") ;; - *) return ;; + *) return 0 ;; esac - [ "$(readlink -f "$target")" = "$(readlink -f "$source")" ] || return + [ "$(readlink -f "$target")" = "$(readlink -f "$source")" ] || return 0 $DRY_RUN_CMD rm "$target" } diff --git a/tests/pi-home-manager.sh b/tests/pi-home-manager.sh index 136bb48e..192eaed6 100755 --- a/tests/pi-home-manager.sh +++ b/tests/pi-home-manager.sh @@ -56,26 +56,28 @@ probe=$(mktemp -d) trap 'rm -rf "$probe"' EXIT fake_home="$probe/home" mkdir -p "$fake_home/.pi/agent/themes" "$fake_home/.pi/agent/extensions" -home_manager_files=$(readlink -f "$activation/home-files") -ln -s "$home_manager_files/.pi/agent/themes/rose-pine-moon.json" \ +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 "$home_manager_files/.pi/agent/extensions/terminal-status-title.js" \ +ln -s "$legacy_home_manager_files/.pi/agent/extensions/terminal-status-title.js" \ "$fake_home/.pi/agent/extensions/terminal-status-title.js" -test "$(readlink "$fake_home/.pi/agent/themes/rose-pine-moon.json")" != \ - /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json -expected_theme_source=$(readlink -f \ - /Users/kunchen/.dotfiles/home/.pi/agent/themes/rose-pine-moon.json) -test -n "$expected_theme_source" test "$(readlink -f "$fake_home/.pi/agent/themes/rose-pine-moon.json")" = \ - "$expected_theme_source" + "$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" > "$probe/migrate.sh" +' "$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" @@ -85,4 +87,13 @@ 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."