diff --git a/README.md b/README.md index 4ef413fbaa79c83e67d35dd640c31df9b758b5c4..688b163fd8f37823bcf8c457b798829b949cb4db 100644 --- a/README.md +++ b/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 diff --git a/package-lock.json b/package-lock.json index 9c79b007cfc22060ac3601bcad358000b7184817..ca2e2ce1697612d10931c5675a34c64be870f302 100644 --- a/package-lock.json +++ b/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": "*" } diff --git a/packages/handoff/README.md b/packages/handoff/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f261e4b3026bb0f93366357a4ae5b7abe87ba55d --- /dev/null +++ b/packages/handoff/README.md @@ -0,0 +1,86 @@ + + +# `@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. diff --git a/packages/handoff/package.json b/packages/handoff/package.json index c63df906b7285186880753a802952e9c94bb5b55..4d8fe048d4977713e2c4f331cd564bb7e8768f5d 100644 --- a/packages/handoff/package.json +++ b/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": "*", diff --git a/packages/handoff/src/handoff-command.ts b/packages/handoff/src/handoff-command.ts index e7fbe56b6eca6120663e381abce7ab6e891d9dad..8caa3c14476b062ca882fb8031f228b3b50aa614 100644 --- a/packages/handoff/src/handoff-command.ts +++ b/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((tui, theme, _kb, done) => { const loader = new BorderedLoader(tui, theme, "Extracting handoff context..."); diff --git a/packages/handoff/src/handoff-tool.ts b/packages/handoff/src/handoff-tool.ts index f022c160b5ac434e9f55dc9dce18fd8395df8f17..63bbc0df0b6b6d6538bd088d1753b21aff474fdd 100644 --- a/packages/handoff/src/handoff-tool.ts +++ b/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 { diff --git a/packages/handoff/src/index.ts b/packages/handoff/src/index.ts index 5bee69eab77bce83880a5ae01a9d416fa4eb8f9b..7131a10c81c6b5bf90371eeb0d99bcc7290c4787 100644 --- a/packages/handoff/src/index.ts +++ b/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; diff --git a/packages/handoff/src/session-analysis.ts b/packages/handoff/src/session-analysis.ts index 13dec684b4ee5756615f57e292dbe1044ee4e8f3..250567f8e996fb50de4c52a35a7b006e62e0d978 100644 --- a/packages/handoff/src/session-analysis.ts +++ b/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; } diff --git a/packages/personas/README.md b/packages/personas/README.md new file mode 100644 index 0000000000000000000000000000000000000000..81a2986500d24fd61097c2eb4b5534bbf378a91b --- /dev/null +++ b/packages/personas/README.md @@ -0,0 +1,85 @@ + + +# `@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. diff --git a/packages/personas/package.json b/packages/personas/package.json index 832b668cafc498182a2778412a97b7e6dd0b460d..c81e50973686349234920eaa93fc48db10cce415 100644 --- a/packages/personas/package.json +++ b/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" diff --git a/packages/personas/src/index.ts b/packages/personas/src/index.ts index ed0c912e1628e68018233c9637f1e4ba7cca3050..83887146ee0b9469dec1bbcf155c9e9b1dd60087 100644 --- a/packages/personas/src/index.ts +++ b/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;