Skip to content

How I migrated PAI from v2.3 to v2.5 — community guide covering symlink architecture, git worktrees, pai-switch tooling, and lessons learned

Notifications You must be signed in to change notification settings

mellanon/pai-v25-migration-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

How I Migrated PAI from v2.3 to v2.5

I've been running PAI v2.3 with heavy customization — custom skills, CLI tools, observability stack, voice server, and a feature branch with signal processing work. When v2.5 dropped, I wanted to upgrade without losing any of that, and without any downtime. Here's how I did it.

The Setup That Makes This Possible

Before getting into the migration itself, the key thing to understand is the architecture that makes version switching painless. I use a three-layer symlink system combined with git worktrees. This is what lets me switch between PAI versions in under a second, and what made the v2.5 migration low-risk.

Three-Layer Symlinks

Layer 1: Claude Code entry point
  ~/.claude  →  ~/Developer/pai/versions/2.5-develop/.claude
               (changing this one symlink = instant version switch)

Layer 2: Personal data lives outside the repo
  .claude/.env            →  ~/.config/pai/.env          (API keys)
  .claude/MEMORY          →  ~/.config/pai/MEMORY        (session history)
  .claude/WORK            →  ~/.config/pai/WORK          (active work)
  .claude/settings.json   →  ~/pai-personal-data/settings.json
  .claude/profiles        →  ~/pai-personal-data/profiles
  .claude/skills/PAI/USER →  ~/.config/pai/CORE_USER     (my customizations)

Layer 3: Two external stores
  ~/.config/pai/          →  secrets + state (never in git)
  ~/pai-personal-data/    →  settings + profiles (private git repo)

Why this matters: Every PAI version points to the same personal data through Layer 2 symlinks. My API keys, memory, identity, and settings don't live inside any version's directory. When I switch from v2.3 to v2.5, all my personal data is already there.

Managing Config, Personal Data, and Secrets

This is probably the most important pattern in the whole setup. PAI has three categories of personal data, and each needs different handling:

Secrets (~/.config/pai/) — API keys, tokens, anything that would be a disaster if leaked. This directory follows XDG conventions, lives outside any repo, and is never committed anywhere. The .env file here has my Anthropic key, ElevenLabs key, Telegram tokens, etc.

State (~/.config/pai/MEMORY, WORK) — Session history and active work context. Also in ~/.config/pai/ because it's machine-specific and changes constantly. Not something you'd want in a git repo generating noise.

Configuration (~/pai-personal-data/) — My settings.json (DA identity, hooks, permissions), agent profiles, and user customizations (AI steering rules, DA personality). This is in a private git repo because I want version history and backup. If I break my settings.json, I can git revert. If my machine dies, I clone this repo on the new one.

User customizations (~/.config/pai/CORE_USER) — My personal AI steering rules, DA identity file, etc. These are symlinked into whichever PAI version is active at skills/PAI/USER/.

The key insight: none of this lives inside the PAI repo. The PAI repo is stock code + custom skills + custom tools. Personal data is entirely external. This means:

  • I can git clean -fdx the PAI repo without losing my identity
  • I can share the PAI repo without leaking secrets
  • Multiple PAI versions share the same identity and memory
  • Upgrading is just "point the symlinks at the new version"

Git Worktrees

Instead of one checkout with branches, each version gets its own directory:

~/Developer/pai/
├── versions/
│   ├── 1.2/              v1.2 (legacy, read-only)
│   ├── 2.3/              v2.3 main
│   ├── 2.3-develop/      v2.3 develop (worktree)
│   ├── 2.5/              v2.5 main
│   └── 2.5-develop/      v2.5 develop (worktree)
├── pai-personal-data/    private repo for settings
└── tooling/
    └── bin/pai-switch    version switcher

Every version is always checked out and ready. No git stash, no git checkout, no waiting. I can diff v2.3 against v2.5 side by side. Feature branches get their own worktrees too.

The Switcher: pai-switch

I wrote a pai-switch script that manages the Layer 1 symlink. It handles switching, baselines, and test environments:

pai-switch v25-dev   # → ~/.claude points to 2.5-develop
pai-switch v2        # → ~/.claude points to 2.3-develop (instant rollback)
pai-switch status    # → shows current environment
pai-switch save name # → tarballs current env as a named baseline
pai-switch baselines # → lists available baselines

Here's the full script:

#!/bin/bash
# pai-switch - Switch between PAI environments
#
# Usage:
#   pai-switch v1|prod           Switch to PAI v1.2 (production)
#   pai-switch v2|dev            Switch to PAI v2.3 (develop)
#   pai-switch v2-main|main      Switch to PAI v2.3 (production)
#   pai-switch v25|latest        Switch to PAI v2.5 (production)
#   pai-switch v25-dev           Switch to PAI v2.5 (develop)
#   pai-switch test              Switch to test sandbox
#   pai-switch status            Show current environment
#   pai-switch nuke-test         Wipe and recreate test environment
#   pai-switch restore           Restore test from baseline
#   pai-switch save              Save current test as baseline
#   pai-switch baselines         List available baselines

set -e

# ── Configure these paths for your setup ──
SRC_DIR="$HOME/Developer/pai"
CLAUDE_LINK="$HOME/.claude"
BASELINES_DIR="$SRC_DIR/baselines"
PAI_ENV_FILE="$HOME/.pai-env"

# Environment paths
V1_PATH="$SRC_DIR/versions/1.2/Personal_AI_Infrastructure/.claude"
V2_PATH="$SRC_DIR/versions/2.3/.claude"
DEV_PATH="$SRC_DIR/versions/2.3-develop/.claude"
V25_PATH="$SRC_DIR/versions/2.5/.claude"
V25_DEV_PATH="$SRC_DIR/versions/2.5-develop/.claude"
TEST_PATH="$SRC_DIR/test/.claude"

# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

show_status() {
    local current=$(readlink "$CLAUDE_LINK" 2>/dev/null || echo "not a symlink")
    echo -e "${BLUE}Current ~/.claude:${NC} $current"

    if [[ "$current" == *"/pai/versions/1.2"* ]]; then
        echo -e "${GREEN}Environment: v1.2 (production)${NC}"
    elif [[ "$current" == *"/pai/versions/2.5-develop"* ]]; then
        echo -e "${GREEN}Environment: v2.5 (develop)${NC}"
    elif [[ "$current" == *"/pai/versions/2.5"* ]]; then
        echo -e "${GREEN}Environment: v2.5 (production)${NC}"
    elif [[ "$current" == *"/pai/versions/2.3-develop"* ]]; then
        echo -e "${YELLOW}Environment: v2.3 (develop)${NC}"
    elif [[ "$current" == *"/pai/versions/2.3"* ]]; then
        echo -e "${YELLOW}Environment: v2.3 (production)${NC}"
    elif [[ "$current" == *"/pai/test"* ]]; then
        echo -e "${RED}Environment: test (sandbox)${NC}"
    else
        echo -e "${RED}Environment: unknown${NC}"
    fi
}

switch_to() {
    local target="$1"
    local label="$2"

    if [[ ! -d "$target" ]]; then
        echo -e "${RED}Error: Target does not exist: $target${NC}"
        exit 1
    fi

    if [[ -L "$CLAUDE_LINK" ]]; then
        rm "$CLAUDE_LINK"
    elif [[ -d "$CLAUDE_LINK" ]]; then
        echo -e "${RED}Error: ~/.claude is a directory, not a symlink${NC}"
        echo "Remove it manually if you want to switch: rm -rf ~/.claude"
        exit 1
    fi

    ln -s "$target" "$CLAUDE_LINK"

    cat > "$PAI_ENV_FILE" << 'EOF'
# Generated by pai-switch - do not edit manually
export PAI_DIR="$HOME/.claude"
EOF

    echo -e "${GREEN}Switched to $label${NC}"
    show_status
}

save_baseline() {
    local name="${2:-$(date +%Y-%m-%d)-snapshot}"
    local target="$BASELINES_DIR/$name"

    if [[ -d "$target" ]]; then
        echo -e "${RED}Error: Baseline already exists: $name${NC}"
        exit 1
    fi

    mkdir -p "$BASELINES_DIR"
    local current=$(readlink "$CLAUDE_LINK" 2>/dev/null)
    cp -R "$current" "$target"
    echo -e "${GREEN}Saved baseline: $name${NC}"
}

case "$1" in
    v1|prod)      switch_to "$V1_PATH" "v1.2 (production)" ;;
    v2|dev)       switch_to "$DEV_PATH" "v2.3 (develop)" ;;
    v2-main)      switch_to "$V2_PATH" "v2.3 (production)" ;;
    v25|latest)   switch_to "$V25_PATH" "v2.5 (production)" ;;
    v25-dev)      switch_to "$V25_DEV_PATH" "v2.5 (develop)" ;;
    test|sandbox) switch_to "$TEST_PATH" "test (sandbox)" ;;
    status|s)     show_status ;;
    save)         save_baseline "$@" ;;
    baselines)    ls -la "$BASELINES_DIR" 2>/dev/null || echo "No baselines" ;;
    *)
        echo "pai-switch - Switch between PAI environments"
        echo ""
        echo "Usage:"
        echo "  pai-switch v25-dev           Switch to PAI v2.5 (develop)"
        echo "  pai-switch v25|latest        Switch to PAI v2.5 (production)"
        echo "  pai-switch v2|dev            Switch to PAI v2.3 (develop)"
        echo "  pai-switch status            Show current environment"
        echo "  pai-switch save [name]       Save current as baseline"
        echo "  pai-switch baselines         List baselines"
        show_status
        ;;
esac

To install, put it somewhere on your PATH:

mkdir -p ~/Developer/pai/tooling/bin
# save the script as pai-switch
chmod +x ~/Developer/pai/tooling/bin/pai-switch
# add to PATH in your shell rc:
export PATH="$HOME/Developer/pai/tooling/bin:$PATH"

That's it. One symlink change, open a new terminal, run claude.


The Migration: Step by Step

Step 1: Get v2.5 Without Touching v2.3

I had an existing fork of danielmiessler/Personal_AI_Infrastructure. Fetched the upstream v2.5.0 tag, created branches, set up worktrees:

cd ~/Developer/pai/versions/2.5
git fetch upstream --tags
git checkout -b v2.5-main v2.5.0
git worktree add ../2.5-develop v2.5-develop

Discovery: v2.5 uses Packs. The .claude/ directory in the repo is a stub. The actual content comes from 23 Packs, each with a src/ directory that gets assembled into .claude/. I ran the assembly and ended up with a populated .claude/ containing all the stock skills, hooks, and agents.

Step 2: Wire Up Personal Data (Symlinks)

Same pattern as v2.3, just pointing to the new directories:

V25=~/Developer/pai/versions/2.5-develop/.claude

ln -sf ~/.config/pai/.env $V25/.env
ln -sf ~/.config/pai/MEMORY $V25/MEMORY
ln -sf ~/.config/pai/WORK $V25/WORK
ln -sf ~/Developer/pai/pai-personal-data/settings.json $V25/settings.json
ln -sf ~/Developer/pai/pai-personal-data/profiles $V25/profiles
ln -sf ~/.config/pai/CORE_USER $V25/skills/PAI/USER

Discovery: The CORE to PAI rename is conceptual only. The v2.5 release notes talk about renaming skills/CORE/ to skills/PAI/, but the upstream repo still uses skills/CORE/ internally in many places. The rename shows up in contextFiles paths in settings.json, but the actual directory structure depends on how Packs assemble. Don't waste time bulk-renaming things.

Step 3: Merge settings.json

This was the most important step. The settings.json schema changed:

  • paiVersion: "2.3""2.5"
  • contextFiles: went from 1 file (skills/CORE/SKILL.md) to 4 files (SKILL.md + AISTEERINGRULES + DAIDENTITY)
  • hooks: 15 → 17 (added RelationshipMemory, SoulEvolution; renamed FormatEnforcer → FormatReminder)
  • permissions.allow: new tools added (MultiEdit, LS, ExitPlanMode, Skill)
  • alwaysThinkingEnabled: new field, set to true

My approach: Start from the v2.5 stock settings.json, then overlay my personal values (DA identity, voice config, timezone, env vars, custom permissions). Don't try to patch v2.3 settings incrementally — you'll miss structural changes.

Step 4: Replace All Stock Hooks

v2.5 updated every hook. I replaced the entire hooks/, hooks/lib/, and hooks/handlers/ directories with v2.5 stock versions.

The notable rename: FormatEnforcer → FormatReminder. This isn't just a rename — the new version uses AI inference to classify response depth instead of regex matching. Significant improvement.

New hooks added: RelationshipMemory and SoulEvolution.

Step 5: Copy Custom Skills

My custom skills (underscore-prefixed: _CONTEXT, _COUPA, _JIRA, _JIRA_ANALYSIS, _DOCX, _DISPATCH, _AI_ONCHARGING, _SPECFIRST) just copied straight over. They're independent of the PAI version.

Only thing to check: grep -r "skills/CORE" skills/_*/ for any stale references that need updating to skills/PAI/.

v2.5 also brought new stock skills I didn't have: Documents, Evals, PromptInjection, SECUpdates, BeCreative, Algorithm.

Step 6: Copy Custom Tools and Infrastructure

Copied my bin/ tools (ctx, ingest, jira, docx, etc.), ran bun install in each directory. Copied Observability stack, VoiceServer, agents, security system, context taxonomies.

Step 7: Save a Baseline and Switch

# Tarball v2.3 as a safety net
tar -czf ~/Developer/pai/baselines/pai-2.3-final.tar.gz -C ~/.claude .

# Switch
pai-switch v25-dev

# Open new terminal, run claude

The One Problem I Hit

Hooks from a feature branch caused issues. I had been working on a feature/signal-agent-2 branch on v2.3 with modified hooks (custom LoadContext, ToolUseInstrumentation for observability). When I initially tried to bring those forward, some hooks referenced paths and patterns that didn't exist in v2.5.

The fix was simple: restore to stock v2.5 hooks. The custom hook modifications weren't worth the conflict resolution. v2.5's stock hooks were improved enough that my customizations were either already covered or not worth maintaining separately.

The session-end hooks (SessionSummary.hook.ts, WorkCompletionLearning.hook.ts) initially errored because they were referenced in settings.json but the files pointed to the old v2.3 paths via ${PAI_DIR}. Once ~/.claude was pointing at v2.5 with the stock hooks in place, everything resolved.

Lesson: If you have a feature branch with hook modifications, don't try to bring them forward during the migration. Get stock v2.5 working first, then selectively re-apply customizations.


What v2.5 Changed (From a Migration Perspective)

What v2.3 v2.5
Distribution .claude/ is the repo Packs → assembled .claude/
Core skill path skills/CORE/ skills/PAI/ (in config)
Context loading 1 file at startup 4 files (SKILL + steering rules + identity)
Format enforcement Regex-based (FormatEnforcer) AI inference (FormatReminder)
Hooks 15 17 (+RelationshipMemory, +SoulEvolution)
Permissions Basic set Extended (MultiEdit, LS, Skill, ExitPlanMode)
Thinking Not configurable alwaysThinkingEnabled: true
New skills Documents, Evals, PromptInjection, SECUpdates, BeCreative, Algorithm

Advice for the Community

  1. Don't upgrade in-place. Set up v2.5 alongside v2.3. The worktree approach means both are always ready and switching is instant.

  2. Set up the symlink architecture first if you haven't already. It's the single most valuable pattern in PAI — it decouples your identity and data from any specific version.

  3. Start settings.json from the v2.5 template. Overlay your personal values onto it. Don't try to patch your v2.3 settings.

  4. Replace all stock hooks wholesale. Don't try to merge. v2.5 hooks are all updated. Re-apply customizations afterward.

  5. Custom skills just copy over. The underscore-prefix convention means they're independent of the version.

  6. The CORE → PAI rename is mostly cosmetic. Don't panic about it. The contextFiles paths changed, but the actual directory structure varies.

  7. Save a baseline before switching. A tarball of your v2.3 .claude/ means instant rollback.

  8. Feature branch hooks are the risk. If you've modified stock hooks on a feature branch, restore to stock v2.5 first and get that working before re-applying changes.


Final State

After migration: 46 skills, 21 hooks, 12 agents, 12 bin tools. Everything working on v2.5-develop. The whole migration was done in a single Claude Code session.

About

How I migrated PAI from v2.3 to v2.5 — community guide covering symlink architecture, git worktrees, pai-switch tooling, and lessons learned

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •