Skip to content

feat(pi-soul): configurable persistence, CLI flags, interactive picker#7

Open
titosemi wants to merge 11 commits into
VTSTech:mainfrom
titosemi:feat/pi-soul-configurable-persistence
Open

feat(pi-soul): configurable persistence, CLI flags, interactive picker#7
titosemi wants to merge 11 commits into
VTSTech:mainfrom
titosemi:feat/pi-soul-configurable-persistence

Conversation

@titosemi

@titosemi titosemi commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tl;dr: you can now choose where your active soul is stored, activate one at startup with --soul, and get a much nicer /soul experience — while keeping everything backwards compatible.

Here's what changed and why.


1. New config file

A dedicated config file to control where your active soul is stored and whether it auto-loads:

// ~/.pi/agent/soul-config.json
{ "persistence": "global", "autoLoad": true }

Auto-created with defaults on first extension load. No manual setup needed. You can also override per-project in .pi/soul-config.json.

Backward compatibility: if you don't add this file, the defaults match the original behavior exactly.


2. Three persistence modes

Mode What it does Survives /new? Fresh Pi?
global (default) Single active soul, stored in .active-soul.json ✅ (with autoLoad)
session One soul per directory, keyed by process.cwd() ✅ (with autoLoad)
none In-memory only, resets on reload/new

Session mode is handy if you switch between projects: /soul dave in project A, /soul iris in project B, and each keeps its own active soul. On /new, Pi restores the right soul for the directory you're in.


3. autoLoad — what it does per mode

Controls whether the persisted soul is auto-applied on fresh Pi startup.

  • global mode + autoLoad: true → loads the single active soul
  • session mode + autoLoad: true → loads the soul mapped to current project directory
  • none mode → ignored (no storage to load from)
  • autoLoad: false → no auto-load on startup (explicit /soul <name> or --soul <name> needed)

/new, /reload, /resume, and /fork always restore regardless of autoLoad or persistence mode — the soul was explicitly activated within this Pi process.


4. --soul and --soul-level CLI flags

pi --soul dave                  # Start with Dave (level 2)
pi --soul mike --soul-level 3   # Start with Mike at level 3
pi --soul off                   # Clear the persisted soul

Flags are processed before autoLoad, so --soul always wins. Handy for scripting or one-shot sessions.


5. Enhanced /soul command

  • /soul status — shows what soul is currently active (name + level)
  • /soul (no args) — opens an interactive picker to choose a soul, then asks for the disclosure level (1-3)
  • Everything else works exactly as before

The interactive picker respects terminal UIs that support ctx.ui.select() — no UI, no picker, just the familiar text list.


6. Lifecycle events

Companion extensions can react to soul changes:

pi.events.on("soul:activated", (payload) => {
  // soul, displayName, level, manifest, persistence, autoLoad, source
});

pi.events.on("soul:deactivated", (payload) => {
  // previousSoul, source, persistence, autoLoad
});

This lets companion extensions (like Telegram autoconnect) react without polling.


7. Status bar

Pi's standard ctx.ui.setStatus("pi-soul", displayName) API is called on activation and cleared on deactivation. The soul name shows in Pi's built-in TUI footer automatically. If pi-powerline-footer is installed, it also appears in the status bar — no config needed, the default preset includes extension_statuses.


Breaking changes?

None. If you don't add soul-config.json, the extension behaves exactly as before:

  • persistence: "global", autoLoad: true (original upstream behavior)
  • .active-soul.json format unchanged
  • /soul <name> and /soul off work identically

How the code changed

The approach was to keep changes to existing files as small as possible:

File What Lines changed
extensions/soul-core.ts New — all new logic (CLI flags, interactive picker, lifecycle events) +260
extensions/soul.ts Minimal hooks + 33 pre-existing type error fixes ~+60 net (160 with type fixes)
shared/soul-config.ts NewloadPiSoulConfig, createActiveSoulStore, persistence store classes +481
tests/soul.test.ts 37 unit tests for config, stores, and persistence modes new
tests/extension-soul.test.ts 21 integration tests for the extension lifecycle new
individual-packages/pi-soul/README.md Updated docs updated

extensions/soul.ts went from 1064 lines to ~1120 — the ~60 added lines are hooks that delegate to soul-core.ts. An additional ~100 lines changed to fix 33 pre-existing TypeScript errors (sensor/actuator types, enum index types, error type narrowing, AgentToolResult peer-dep conflicts — all upstream issues surfaced by running tsc on the imported file). No reformatting or restructuring of existing logic.


Tests

58 tests, 0 failures:

  • tests/soul.test.ts — config parsing, all three persistence stores, edge cases (malformed JSON, invalid values, file auto-creation)
  • tests/extension-soul.test.ts — extension registration, session lifecycle (reload/new/startup), commands (status, off, interactive picker), lifecycle events, powerline status

titosemi added 11 commits June 5, 2026 14:47
Add piSoul.persistence (global|session|none) and piSoul.autoLoad config
read from Pi settings files. Add --soul and --soul-level CLI flags via
pi.registerFlag. Add /soul status command. Emit soul:activated and
soul:deactivated lifecycle events on pi.events bus.

- shared/soul-config.ts: loadPiSoulConfig, ActiveSoulStore interface,
  GlobalFileActiveSoulStore, SessionActiveSoulStore, MemoryActiveSoulStore,
  isSoulClearValue, createActiveSoulStore factory
- shared/package.json: export ./soul-config
- extensions/soul.ts: replace local persistence helpers with store
  abstraction; add flag registration; rewrite session_start handler
  (all reasons: startup/new/resume/fork; skip reload); update /soul
  command (add status, events, store-based persistence)
- tests/soul.test.ts: unit tests for all soul-config helpers (31 tests)
- individual-packages/pi-soul/README.md: document config, CLI flags,
  /soul status, lifecycle events, companion extension pattern
- CHANGELOG.md: document new features under [Unreleased]

Missing config defaults to current behavior: persistence=global,
autoLoad=true. No migration required for existing users.
Add three tests for the clarified autoLoad rule:
- global+autoLoad=true → loads on startup ✓
- global+autoLoad=false → skips on startup ✓
- session+autoLoad=true → skips on startup (autoLoad only applies to global) ✓
…rites

Config (persistence mode) lives in soul-config.json. Do not duplicate
it in the runtime state file.
Also corrects autoLoad semantics description (only applies to global).
Add minimal hooks to soul.ts and put all new logic in soul-core.ts:

- Config in soul-config.json (dedicated file, not settings.json)
- Persistence modes: global, session, none (via shared/soul-config.ts)
- --soul and --soul-level CLI flags
- Interactive /soul picker with disclosure level selection
- /soul status command
- soul:activated / soul:deactivated lifecycle events
- Powerline integration via ctx.ui.setStatus
- autoLoad only applies to global persistence

Existing soul.ts changes are minimal: ~60 lines of additions.
All new logic lives in extensions/soul-core.ts (260 lines).
…n startup)

TDD: updated test from 'skips autoLoad in session' to 'autoLoads in
session+autoLoad=true with store entry'. Also updates docs and CHANGELOG.
- Fix success→info notify type, catch(error)→catch(error:any)
- Fix sensor/actuator type parsing with explicit Record casts
- Fix enum index type assertions
- Add @ts-ignore for pre-existing AgentToolResult peer-dep conflicts
- Add --experimental-test-module-mocks to npm test script
- Convert tab indentation to 2-space to match upstream
…h to usage error

When selecting 'status' in the /soul interactive picker, the handler
returned { type: 'none' } which caused the caller to fall through to
the text-based usage/help message. Now the picker always returns
cleanly — the picker handles the interaction completely regardless
of the chosen action.

Adds a regression test verifying no 'Usage:' message leaks through
after selecting status in the interactive picker.
@titosemi titosemi force-pushed the feat/pi-soul-configurable-persistence branch from 09eeac7 to 8f9ab2f Compare June 9, 2026 19:43
@titosemi

titosemi commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Hey 👋 bumping this — just squashed a bug where the interactive picker's status option fell through to the usage error.

  • Config persistence — choose where your active soul lives (global/session/none) and whether it auto-loads on startup
  • CLI flags--soul and --soul-level for scripting or one-shot sessions
  • Better /soul — interactive picker when no args, /soul status to check what's active
  • Lifecycle events + status bar — companion extensions can react to soul changes without polling

58 tests, zero failures. No breaking changes — defaults match the original behavior exactly.

Would love a review when you have a moment. Happy to address any feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant