Prepare beta.1 package release

Amolith created

Add package-level READMEs and refresh the root docs for the handoff and
personas packages.

Expose handoff extraction model configuration through Pi's extension
flag API, the existing PI_HANDOFF_MODEL env var, and the handoffModel
user setting.

Store the active persona in settings.json's persona key instead of the
old marker file.

Bump handoff and personas to 0.1.0-beta.1, keeping package-lock.json in
sync.

Change summary

README.md                                | 89 +++++++++++++++++++------
package-lock.json                        |  4 
packages/handoff/README.md               | 86 +++++++++++++++++++++++++
packages/handoff/package.json            |  2 
packages/handoff/src/handoff-command.ts  |  2 
packages/handoff/src/handoff-tool.ts     |  2 
packages/handoff/src/index.ts            |  2 
packages/handoff/src/session-analysis.ts | 84 +++++++++++++++++++----
packages/personas/README.md              | 85 ++++++++++++++++++++++++
packages/personas/package.json           |  2 
packages/personas/src/index.ts           | 81 ++++++++++++++++-------
11 files changed, 369 insertions(+), 70 deletions(-)

Detailed changes

README.md 🔗

@@ -6,50 +6,64 @@ SPDX-License-Identifier: CC0-1.0
 
 # pi-extensions
 
-A collection of [Pi](https://github.com/badlogic/pi-mono) extensions. They're
-all quite rough currently, having just been imported to this repo from ad-hoc,
-vibed individual scripts created by saying "go change
-`~/.config/pi/extensions/foo.ts` so it bars".
+[![Licensed under MutuaL-1.2](https://img.shields.io/badge/License-MutuaL--1.2-af2e1a?style=flat&labelColor=110402&link=https%3A%2F%2Fcodeberg.org%2FMutualism%2FMutualist-License)](https://codeberg.org/Mutualism/Mutualist-License)
+[![REUSE status](https://api.reuse.software/badge/git.secluded.site/pi-extensions)](https://api.reuse.software/info/git.secluded.site/pi-extensions)
+[![scratchanitch.dev badge](https://img.shields.io/badge/scratchanitch-dev-FFC4B5)](https://scratchanitch.dev)
+[![Liberapay donation status](https://img.shields.io/liberapay/receives/Amolith.svg?logo=liberapay)](https://liberapay.com/Amolith/)
 
-## Packages
+A collection of [Pi] extensions.
 
-### `@amolith/pi-handoff`
+[Pi]: https://github.com/earendil-works/pi/
 
-[![npm](https://img.shields.io/npm/v/@amolith/pi-handoff)](https://www.npmjs.com/package/@amolith/pi-handoff)
+## `@amolith/pi-handoff`
 
-Get to the end of the thing you're doing, then use a lightweight subagent to
-extract particular bits from the current session and create a new one from those
-bits with `/handoff thorough goal description`. You can also tell Pi to hand
-something off (it has a tool).
+[![pi-handoff on NPM](https://img.shields.io/npm/v/@amolith/pi-handoff)](https://www.npmjs.com/package/@amolith/pi-handoff)
+
+Get to the end of the thing you're doing, then use a subagent to extract
+relevant bits from the current session and create a new one from them with
+`/handoff thorough goal description`. You can tell Pi to hand something off
+because it has a handoff tool too.
+
+After extraction, whether triggered by you or Pi, the draft handoff goes in the
+new session's prompt editor and an auto-submit countdown starts that's cancelled
+by pressing most any key.
 
 ```sh
 pi install npm:@amolith/pi-handoff
 ```
 
-> Based on [handoff.ts](https://github.com/pasky/pi-amplike/blob/main/extensions/handoff.ts)
-> and [session-query.ts](https://github.com/pasky/pi-amplike/blob/main/extensions/session-query.ts)
-> by Petr Baudis, licensed MIT accordingly. Reworked so the subagent calls a
-> tool instead of expecting it to produce well-formed JSON, added an auto-submit
-> countdown, and make the extraction model configurable via `PI_HANDOFF_MODEL`.
+More info in the package's [README](packages/handoff/README.md)
+
+> Based on [handoff.ts] and [session-query.ts] by Petr Baudis, licensed MIT
+> accordingly. Reworked so the subagent calls a tool instead of expecting it to
+> produce well-formed JSON, added an auto-submit countdown, and made the
+> extraction model configurable.
+
+[handoff.ts]: https://github.com/pasky/pi-amplike/blob/main/extensions/handoff.ts
+[session-query.ts]: https://github.com/pasky/pi-amplike/blob/main/extensions/session-query.ts
 
-### `@amolith/pi-personas`
+## `@amolith/pi-personas`
 
-[![npm](https://img.shields.io/npm/v/@amolith/pi-personas)](https://www.npmjs.com/package/@amolith/pi-personas)
+[![pi-personas on NPM](https://img.shields.io/npm/v/@amolith/pi-personas)](https://www.npmjs.com/package/@amolith/pi-personas)
 
-Drop markdown files in `$PI_CODING_AGENT_DIR/personas/` and switch between them
-with `/persona`. Lets your agent be a grumpy senior today and a patient tutor
-tomorrow. The active persona gets injected into the system prompt and persists
-across sessions until you change it.
+Code with a grumpy senior today and work with a patient tutor tomorrow. Switch
+between them with `/persona`. The active one persists across sessions until
+changed. Setting a persona prior to startup appends it to the system prompt.
+Changing it mid-session sends the persona content as a steering message.
 
 ```sh
 pi install npm:@amolith/pi-personas
 ```
 
+More info in the package's [README](packages/personas/README.md)
+
 > All mine :)
 
 ## Development
 
-Prerequisites: [mise](https://mise.jdx.dev/) (installs Node and npm automatically).
+Prerequisites: [mise] (installs Node and npm automatically).
+
+[mise]: https://mise.jdx.dev/
 
 ```sh
 # Install dependencies
@@ -58,3 +72,32 @@ npm install
 # Format, lint, and type-check everything
 mise run check
 ```
+
+## Contributing
+
+Patch requests are in [amolith/llm-projects] on [pr.pico.sh]. You don't need a
+new account to contribute, you don't need to fork this repo, you don't need to
+fiddle with `git send-email`, you don't need to faff with your email client to
+get `git request-pull` working...
+
+You just need:
+
+- Git
+- SSH
+- An SSH key
+
+```bash
+# Clone this repo, make your changes, and commit them
+# Create a new patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr create amolith/llm-projects
+# After potential feedback, submit a revision to an existing patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr add {prID}
+# List patch requests
+ssh pr.pico.sh pr ls amolith/llm-projects
+```
+
+See "How do Patch Requests work?" on [pr.pico.sh]'s home page for a more
+complete example workflow.
+
+[amolith/llm-projects]: https://pr.pico.sh/r/amolith/llm-projects
+[pr.pico.sh]: https://pr.pico.sh

package-lock.json 🔗

@@ -3347,7 +3347,7 @@
 		},
 		"packages/handoff": {
 			"name": "@amolith/pi-handoff",
-			"version": "0.1.0-beta.0",
+			"version": "0.1.0-beta.1",
 			"peerDependencies": {
 				"@earendil-works/pi-ai": "*",
 				"@earendil-works/pi-coding-agent": "*",
@@ -3357,7 +3357,7 @@
 		},
 		"packages/personas": {
 			"name": "@amolith/pi-personas",
-			"version": "0.1.0-beta.0",
+			"version": "0.1.0-beta.1",
 			"peerDependencies": {
 				"@earendil-works/pi-coding-agent": "*"
 			}

packages/handoff/README.md 🔗

@@ -0,0 +1,86 @@
+<!--
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# `@amolith/pi-handoff`
+
+[![pi-handoff on NPM](https://img.shields.io/npm/v/@amolith/pi-handoff)](https://www.npmjs.com/package/@amolith/pi-handoff)
+[![Licensed under MutuaL-1.2](https://img.shields.io/badge/License-MutuaL--1.2-af2e1a?style=flat&labelColor=110402&link=https%3A%2F%2Fcodeberg.org%2FMutualism%2FMutualist-License)](https://codeberg.org/Mutualism/Mutualist-License)
+[![REUSE status](https://api.reuse.software/badge/git.secluded.site/pi-extensions)](https://api.reuse.software/info/git.secluded.site/pi-extensions)
+[![scratchanitch.dev badge](https://img.shields.io/badge/scratchanitch-dev-FFC4B5)](https://scratchanitch.dev)
+[![Liberapay donation status](https://img.shields.io/liberapay/receives/Amolith.svg?logo=liberapay)](https://liberapay.com/Amolith/)
+
+Get to the end of the thing you're doing, then use a subagent to extract
+relevant bits from the current session and create a new one from them with
+`/handoff thorough goal description`. You can tell Pi to hand something off
+because it has a handoff tool too.
+
+After extraction, whether triggered by you or Pi, the draft handoff goes in the
+new session's prompt editor and an auto-submit countdown starts that's cancelled
+by pressing most any key.
+
+## Install
+
+```sh
+pi install npm:@amolith/pi-handoff
+```
+
+## Do it yourself
+
+Run `/handoff` with whatever the next session should do:
+
+```text
+/handoff finish the release notes and publish both packages
+```
+
+If you omit the goal, Pi asks for one. Before the new session starts, the
+extension opens the generated handoff draft in the editor so you can review or
+tweak it. After the session switch, it starts an auto-submit countdown and
+pressing most any key cancels it.
+
+To use a different model in the new session, pass `-model provider/modelId`:
+
+```text
+/handoff -model neuralwatt/glm-5.1 finish the release notes
+```
+
+## Let the agent do it
+
+The package also registers a `handoff` tool. The agent should only use it when
+you explicitly ask for a handoff. It accepts:
+
+- `goal`: the goal for the new session
+- `model`: optional `provider/modelId` for the new session
+
+The handoff happens after the current agent turn completes and still opens the
+editor so you can tweak before starting the new session.
+
+## Configure the extraction model
+
+It uses the current session model by default. To use a cheaper/faster model, set
+a `provider/modelId` with one of these:
+
+1. `--handoff-model provider/modelId`
+2. `$PI_HANDOFF_MODEL`
+3. `handoffModel` in `$PI_CODING_AGENT_DIR/settings.json`
+
+If you set more than one, that list is the precedence order.
+
+Example settings:
+
+```json
+{
+  "handoffModel": "neuralwatt/qwen3.6-35b-fast"
+}
+```
+
+If the configured value is malformed or doesn't match a registered model, the
+extension falls back to the current session model.
+
+## Ask about the old session
+
+New handoff prompts include the parent session path when available. The package
+registers a `session_query` tool so the next agent can ask focused questions
+about that prior session when the handoff summary isn't enough.

packages/handoff/package.json 🔗

@@ -1,6 +1,6 @@
 {
 	"name": "@amolith/pi-handoff",
-	"version": "0.1.0-beta.0",
+	"version": "0.1.0-beta.1",
 	"peerDependencies": {
 		"@earendil-works/pi-ai": "*",
 		"@earendil-works/pi-coding-agent": "*",

packages/handoff/src/handoff-command.ts 🔗

@@ -63,7 +63,7 @@ export function registerHandoffCommand(pi: ExtensionAPI, startCountdown: (ctx: E
 			const currentSessionFile = ctx.sessionManager.getSessionFile();
 			const candidateFiles = [...extractCandidateFiles(branch, conversationText)];
 			const loadedSkills = extractLoadedSkills(branch);
-			const extractionModel = resolveExtractionModel(ctx) ?? ctx.model;
+			const extractionModel = resolveExtractionModel(pi, ctx) ?? ctx.model;
 
 			const result = await ctx.ui.custom<HandoffExtraction | null>((tui, theme, _kb, done) => {
 				const loader = new BorderedLoader(tui, theme, "Extracting handoff context...");

packages/handoff/src/handoff-tool.ts 🔗

@@ -61,7 +61,7 @@ export function registerHandoffTool(pi: ExtensionAPI, setPendingHandoff: (h: Pen
 			const candidateFiles = [...extractCandidateFiles(branch, conversationText)];
 			const loadedSkills = extractLoadedSkills(branch);
 
-			const extractionModel = resolveExtractionModel(ctx) ?? ctx.model;
+			const extractionModel = resolveExtractionModel(pi, ctx) ?? ctx.model;
 			const auth = await ctx.modelRegistry.getApiKeyAndHeaders(extractionModel);
 			if (!auth.ok) {
 				return {

packages/handoff/src/index.ts 🔗

@@ -9,6 +9,7 @@ import { Key, matchesKey, parseKey } from "@earendil-works/pi-tui";
 import { registerHandoffCommand } from "./handoff-command.js";
 import { registerHandoffTool, type PendingHandoff } from "./handoff-tool.js";
 import { parseAndSetModelWithNotify } from "./model-utils.js";
+import { registerHandoffModelFlag } from "./session-analysis.js";
 import { registerSessionQueryTool } from "./session-query-tool.js";
 
 const STATUS_KEY = "handoff";
@@ -197,6 +198,7 @@ export default function (pi: ExtensionAPI) {
 
 	// --- Register tools and commands ---
 
+	registerHandoffModelFlag(pi);
 	registerSessionQueryTool(pi);
 	registerHandoffTool(pi, (h) => {
 		pendingHandoff = h;

packages/handoff/src/session-analysis.ts 🔗

@@ -3,15 +3,61 @@
 //
 // SPDX-License-Identifier: MIT
 
-import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
+import {
+	getAgentDir,
+	SettingsManager,
+	type ExtensionAPI,
+	type ExtensionContext,
+	type SessionEntry,
+} from "@earendil-works/pi-coding-agent";
 import * as path from "node:path";
 
 /**
- * Model used for handoff extraction calls. Set PI_HANDOFF_MODEL env var
- * as "provider/modelId" (e.g. "anthropic/claude-haiku-4-5") to use a
- * different model for extraction than the session's current model.
+ * Model override sources for handoff extraction calls. Use provider/modelId
+ * values (e.g. "anthropic/claude-haiku-4-5"). Precedence is:
+ * --handoff-model, PI_HANDOFF_MODEL, user settings handoffModel, session model.
  */
-const HANDOFF_MODEL_OVERRIDE = process.env.PI_HANDOFF_MODEL;
+const HANDOFF_MODEL_FLAG = "handoff-model";
+const HANDOFF_MODEL_ENV = "PI_HANDOFF_MODEL";
+const HANDOFF_MODEL_SETTING = "handoffModel";
+
+export function registerHandoffModelFlag(pi: ExtensionAPI): void {
+	pi.registerFlag(HANDOFF_MODEL_FLAG, {
+		description: "Model for handoff extraction as provider/modelId",
+		type: "string",
+	});
+}
+
+function nonEmptyString(value: string | undefined): string | undefined {
+	const trimmed = value?.trim();
+	return trimmed ? trimmed : undefined;
+}
+
+function stringProperty(source: object, key: string): string | undefined {
+	const descriptor = Object.getOwnPropertyDescriptor(source, key);
+	if (!descriptor || !("value" in descriptor)) return undefined;
+	return typeof descriptor.value === "string" ? nonEmptyString(descriptor.value) : undefined;
+}
+
+function userSettingsHandoffModel(cwd: string): string | undefined {
+	const settingsManager = SettingsManager.create(cwd, getAgentDir());
+	return stringProperty(settingsManager.getGlobalSettings(), HANDOFF_MODEL_SETTING);
+}
+
+function configuredHandoffModel(pi: ExtensionAPI, ctx: { cwd: string }): string | undefined {
+	const flagValue = pi.getFlag(HANDOFF_MODEL_FLAG);
+	const flagModel = typeof flagValue === "string" ? nonEmptyString(flagValue) : undefined;
+	return flagModel ?? nonEmptyString(process.env[HANDOFF_MODEL_ENV]) ?? userSettingsHandoffModel(ctx.cwd);
+}
+
+function parseModelSpec(spec: string): { provider: string; modelId: string } | undefined {
+	const slashIdx = spec.indexOf("/");
+	if (slashIdx <= 0 || slashIdx === spec.length - 1) return undefined;
+	return {
+		provider: spec.slice(0, slashIdx),
+		modelId: spec.slice(slashIdx + 1),
+	};
+}
 
 /**
  * Build a candidate file set from two sources:
@@ -90,17 +136,21 @@ export function extractLoadedSkills(entries: SessionEntry[]): string[] {
 }
 
 /**
- * Resolve the model to use for handoff extraction calls. Uses the
- * PI_HANDOFF_MODEL env var if set, otherwise falls back to the session model.
+ * Resolve the model to use for handoff extraction calls. Uses --handoff-model,
+ * PI_HANDOFF_MODEL, then handoffModel in user settings, falling back to the
+ * session model if no valid override is configured.
  */
-export function resolveExtractionModel(ctx: {
-	model: ExtensionContext["model"];
-	modelRegistry: ExtensionContext["modelRegistry"];
-}): ExtensionContext["model"] {
-	if (!HANDOFF_MODEL_OVERRIDE) return ctx.model;
-	const slashIdx = HANDOFF_MODEL_OVERRIDE.indexOf("/");
-	if (slashIdx <= 0) return ctx.model;
-	const provider = HANDOFF_MODEL_OVERRIDE.slice(0, slashIdx);
-	const modelId = HANDOFF_MODEL_OVERRIDE.slice(slashIdx + 1);
-	return ctx.modelRegistry.find(provider, modelId) ?? ctx.model;
+export function resolveExtractionModel(
+	pi: ExtensionAPI,
+	ctx: {
+		cwd: string;
+		model: ExtensionContext["model"];
+		modelRegistry: ExtensionContext["modelRegistry"];
+	},
+): ExtensionContext["model"] {
+	const modelSpec = configuredHandoffModel(pi, ctx);
+	if (!modelSpec) return ctx.model;
+	const parsed = parseModelSpec(modelSpec);
+	if (!parsed) return ctx.model;
+	return ctx.modelRegistry.find(parsed.provider, parsed.modelId) ?? ctx.model;
 }

packages/personas/README.md 🔗

@@ -0,0 +1,85 @@
+<!--
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# `@amolith/pi-personas`
+
+[![pi-personas on NPM](https://img.shields.io/npm/v/@amolith/pi-personas)](https://www.npmjs.com/package/@amolith/pi-personas)
+[![Licensed under MutuaL-1.2](https://img.shields.io/badge/License-MutuaL--1.2-af2e1a?style=flat&labelColor=110402&link=https%3A%2F%2Fcodeberg.org%2FMutualism%2FMutualist-License)](https://codeberg.org/Mutualism/Mutualist-License)
+[![REUSE status](https://api.reuse.software/badge/git.secluded.site/pi-extensions)](https://api.reuse.software/info/git.secluded.site/pi-extensions)
+[![scratchanitch.dev badge](https://img.shields.io/badge/scratchanitch-dev-FFC4B5)](https://scratchanitch.dev)
+[![Liberapay donation status](https://img.shields.io/liberapay/receives/Amolith.svg?logo=liberapay)](https://liberapay.com/Amolith/)
+
+Code with a grumpy senior today and work with a patient tutor tomorrow. Switch
+between them with `/persona`. The active one persists across sessions until
+changed. Setting a persona prior to startup appends it to the system prompt.
+Changing it mid-session sends the persona content as a steering message.
+
+Personas are loaded from `persona-name.md` files in
+`$PI_CODING_AGENT_DIR/personas/`. `/persona` is interactive and `/persona grug`
+sets it directly.
+
+## Install
+
+```sh
+pi install npm:@amolith/pi-personas
+```
+
+## Write personas
+
+Create one markdown file per persona in `$PI_CODING_AGENT_DIR/personas/`:
+
+```text
+$PI_CODING_AGENT_DIR/personas/grumpy-senior.md
+$PI_CODING_AGENT_DIR/personas/patient-tutor.md
+```
+
+The file name without `.md` is the persona name. That's what `/persona` and the
+`persona` setting use.
+
+## Switch personas
+
+Open an interactive picker:
+
+```text
+/persona
+```
+
+Switch directly:
+
+```text
+/persona patient-tutor
+```
+
+Clear the active persona:
+
+```text
+/persona none
+```
+
+`unset` and `clear` also clear the active persona.
+
+## Remember the active persona
+
+The active persona persists as the `persona` key in
+`$PI_CODING_AGENT_DIR/settings.json`:
+
+```json
+{
+  "persona": "patient-tutor"
+}
+```
+
+The extension only accepts persona names that match markdown files under
+`$PI_CODING_AGENT_DIR/personas/`.
+
+## What happens when it changes
+
+If a persona is set before startup, the extension reads it at session start and
+appends it to the system prompt for the lifetime of the session.
+
+When you set or clear a persona mid-session, the extension updates the setting
+immediately and sends a one-shot hidden reminder message. It does not rewrite
+the session's system prompt mid-stream.

packages/personas/package.json 🔗

@@ -1,6 +1,6 @@
 {
 	"name": "@amolith/pi-personas",
-	"version": "0.1.0-beta.0",
+	"version": "0.1.0-beta.1",
 	"description": "Switchable agent personas for Pi",
 	"keywords": [
 		"pi-package"

packages/personas/src/index.ts 🔗

@@ -15,22 +15,26 @@
  *   /persona              — Interactive picker to switch persona
  *   /persona none|unset|clear — Remove active persona
  *
- * The active persona persists across sessions in a plain text file
- * at persona in the Pi agent directory.
+ * The active persona persists across sessions as the "persona" key
+ * in the Pi agent directory's settings.json.
  */
 
 import * as fs from "node:fs";
 import * as path from "node:path";
-import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
+import { getAgentDir, type ExtensionAPI, SettingsManager } from "@earendil-works/pi-coding-agent";
 
 const CLEAR_KEYWORDS = ["none", "unset", "clear"];
+const ACTIVE_PERSONA_SETTING = "persona";
+
+type JsonValue = JsonObject | JsonValue[] | string | number | boolean | null;
+type JsonObject = { [key: string]: JsonValue };
 
 function getPersonasDir(): string {
 	return path.join(getAgentDir(), "personas");
 }
 
-function getActivePersonaPath(): string {
-	return path.join(getAgentDir(), "persona");
+function getSettingsPath(): string {
+	return path.join(getAgentDir(), "settings.json");
 }
 
 /** List available persona names (without .md extension). */
@@ -50,23 +54,61 @@ function listPersonas(): string[] {
 	}
 }
 
+function parseSettingsJson(content: string): JsonObject {
+	if (!content.trim()) return {};
+	const parsed: JsonValue = JSON.parse(content);
+	if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+		throw new Error("settings.json must contain a JSON object");
+	}
+	return parsed;
+}
+
+function readPersonaSetting(cwd: string): string | null {
+	const settingsManager = SettingsManager.create(cwd, getAgentDir());
+	const settings = settingsManager.getGlobalSettings();
+	const descriptor = Object.getOwnPropertyDescriptor(settings, ACTIVE_PERSONA_SETTING);
+	if (!descriptor || !("value" in descriptor)) return null;
+	if (typeof descriptor.value !== "string") return null;
+	const name = descriptor.value.trim();
+	return name ? name : null;
+}
+
+function writePersonaSetting(name: string | null): void {
+	const filePath = getSettingsPath();
+	try {
+		const settings = fs.existsSync(filePath) ? parseSettingsJson(fs.readFileSync(filePath, "utf-8")) : {};
+
+		if (name) {
+			settings[ACTIVE_PERSONA_SETTING] = name;
+		} else {
+			delete settings[ACTIVE_PERSONA_SETTING];
+		}
+
+		const dir = path.dirname(filePath);
+		if (!fs.existsSync(dir)) {
+			fs.mkdirSync(dir, { recursive: true });
+		}
+
+		fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
+	} catch (err) {
+		console.error(`Failed to write active persona setting: ${err}`);
+	}
+}
+
 /**
  * Read the persisted active persona name, or null if none.
  * Validates the name against available personas to prevent
- * path traversal via a tampered persistence file.
+ * path traversal via a tampered settings file.
  */
-function readActivePersona(): string | null {
-	const filePath = getActivePersonaPath();
-	if (!fs.existsSync(filePath)) return null;
-
+function readActivePersona(cwd: string): string | null {
 	try {
-		const name = fs.readFileSync(filePath, "utf-8").trim();
+		const name = readPersonaSetting(cwd);
 		if (!name) return null;
 
 		// Reject names that could escape the personas directory
 		const available = listPersonas();
 		if (!available.includes(name)) {
-			console.error(`Persisted persona "${name}" not in available list, ignoring`);
+			console.error(`Configured persona "${name}" not in available list, ignoring`);
 			return null;
 		}
 
@@ -77,18 +119,9 @@ function readActivePersona(): string | null {
 	}
 }
 
-/** Persist the active persona name (or remove the file to clear). */
+/** Persist the active persona name (or remove the setting to clear). */
 function writeActivePersona(name: string | null): void {
-	const filePath = getActivePersonaPath();
-	try {
-		if (name) {
-			fs.writeFileSync(filePath, `${name}\n`, "utf-8");
-		} else if (fs.existsSync(filePath)) {
-			fs.unlinkSync(filePath);
-		}
-	} catch (err) {
-		console.error(`Failed to write active persona: ${err}`);
-	}
+	writePersonaSetting(name);
 }
 
 /** Read persona markdown content, or null if file missing. */
@@ -128,7 +161,7 @@ export default function (pi: ExtensionAPI) {
 	// Lock persona into the system prompt at session start.
 	// This content never changes for the lifetime of the session.
 	pi.on("session_start", async (_event, ctx) => {
-		const persisted = readActivePersona();
+		const persisted = readActivePersona(ctx.cwd);
 		pendingReminder = null;
 		sessionPersonaContent = null;