import { ExtensionAPI } from '@mariozechner/pi-coding-agent'; import { AutocompleteItem } from '@mariozechner/pi-tui'; import { Static, Type } from 'typebox'; /** * TypeBox schemas for pi-roles. * * These define: * - The role frontmatter contract (parsed from YAML in `.md` files). * - The pi-roles settings contract (read from `settings.json`). * - The persisted "active role" state (stored via `pi.appendEntry` so it * survives `/reload`). * * Everything that touches role data downstream imports from this file. If you * need to extend a contract, change it here first. * * We use `typebox` (1.x), not `@sinclair/typebox` 0.34 — pi-mono migrated and * its docs explicitly tell new extensions to depend on the `typebox` root * package. See pi-mono CHANGELOG for the migration notes. */ /** * Pi's thinking levels, mirrored from `ThinkingLevel` in @mariozechner/pi-ai. * * We don't import the type directly because we want to validate user-supplied * frontmatter and produce friendly error messages instead of letting Pi * complain later. */ declare const ThinkingLevelSchema: Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"minimal">, Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">, Type.TLiteral<"xhigh">]>; type ThinkingLevelValue = Static; /** * Intercom integration mode for a role or for the global default. * * - off: no intercom tool, no prompt addendum. * - receive: targetable by other sessions, no proactive sends. * - send: can send to other sessions, no inbound coordination expected. * - both: full bidirectional coordination. */ declare const IntercomModeSchema: Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>; type IntercomMode = Static; /** * Where a discovered role came from. Used in /role list output and for * shadowing detection. 'built-in' refers to roles bundled with the pi-roles * package (currently only `role-assistant`). */ declare const RoleSourceSchema: Type.TUnion<[Type.TLiteral<"project">, Type.TLiteral<"user">, Type.TLiteral<"built-in">]>; type RoleSource = Static; /** * Role frontmatter as it appears at the top of a role .md file. * * Notes on the design: * * - `tools` is intentionally kept as a raw string here. We need to distinguish * three states downstream: * - field absent in YAML -> inherit from parent or keep current * - field present but "" -> explicitly disable all tools * - field present with -> override exactly these tools * a value * YAML parsers collapse `tools:` (no value) and `tools: ~` to `null`, and * we get `undefined` when the field is missing from the object entirely. * The schema accepts `string | null`, and `roles.ts` handles the tri-state * semantics. Don't try to encode "absent" inside this schema — JSON Schema * can't distinguish `undefined` from "not validated", which is why we * handle this in code rather than in the type. * * - `model` is a free string here; resolution against * `ctx.modelRegistry.find(provider, id)` happens in `apply.ts`. Keeping it * loose lets users use either `provider/id` or just `id` syntax, matching * Pi's `--model` flag. * * - `name` is required and must equal the filename without extension. We * enforce that in `roles.ts` after parsing, not at the schema level. */ declare const RoleFrontmatterSchema: Type.TObject<{ /** Unique identifier; must match the filename without `.md`. */ name: Type.TString; /** One-line description shown in /role list and pickers. */ description: Type.TString; /** * Model identifier in `provider/id` or `id` form. Resolved against Pi's * model registry at apply time. If the model isn't available, we warn * and keep the session's current model. */ model: Type.TOptional; /** Reasoning level. Clamped to model capabilities by Pi. */ thinking: Type.TOptional, Type.TLiteral<"minimal">, Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">, Type.TLiteral<"xhigh">]>>; /** * Tool list as a raw, comma-separated string. Empty string means "no * tools". Use the `mcp:server-name` syntax for MCP tools (requires * pi-mcp-adapter at runtime). Parse and tri-state semantics live in * `roles.ts`. We accept `null` because YAML's `tools:` (no value) * deserializes to that. */ tools: Type.TOptional>; /** Per-role intercom mode override. Falls back to global `intercomMode`. */ intercom: Type.TOptional, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>>; /** * Name of a parent role to inherit from. Resolved against the same scope * the child was loaded from, then user, then built-in. Cycles are a hard * error. */ extends: Type.TOptional; }>; type RoleFrontmatter = Static; /** * The `tools` field after tri-state normalization. This is what apply.ts * actually consumes. * * - `{ kind: "inherit" }` -> field absent in frontmatter; do nothing on * apply unless an `extends` chain provides a different value. * - `{ kind: "set", names: [] }` -> explicitly empty; pass [] to setActiveTools. * - `{ kind: "set", names: [...] }` -> explicit list; pass through to * setActiveTools (after stripping mcp:* entries when pi-mcp-adapter is not * installed). */ type ToolsDirective = { kind: "inherit"; } | { kind: "set"; names: string[]; }; /** * A fully-resolved role: frontmatter + body, with `extends` chain merged in. * * `RawRole` is what we get from disk before merging. `ResolvedRole` is what * apply.ts consumes. The conversion happens in `roles.ts`. */ interface RawRole { /** Where the role was loaded from. */ source: RoleSource; /** Absolute path to the .md file (or a synthetic path for built-in roles). */ path: string; /** Parsed frontmatter, validated against RoleFrontmatterSchema. */ frontmatter: RoleFrontmatter; /** Markdown body — the system prompt. */ body: string; } interface ResolvedRole { /** Final name (always equals frontmatter.name). */ name: string; /** Final description. */ description: string; /** Final model id, or undefined to keep current. */ model?: string; /** Final thinking level, or undefined to keep current. */ thinking?: ThinkingLevelValue; /** Final tools directive after merging the extends chain. */ tools: ToolsDirective; /** Final intercom mode, or undefined to fall back to global. */ intercom?: IntercomMode; /** Final system prompt body (parent body prepended to child body when extending). */ body: string; /** Source of the leaf role file (always the file the user requested by name). */ source: RoleSource; /** Path of the leaf role file. */ path: string; /** Names of all parent roles in resolution order, leaf-first. Useful for diagnostics. */ extendsChain: string[]; } /** * The pi-roles section of Pi's settings.json. Project settings beat global * per Pi's standard precedence. * * All fields are optional; unset fields fall back to the documented defaults * (see README "Settings reference"). The schema is permissive so that * settings written by future versions don't break older code. */ declare const PiRolesSettingsSchema: Type.TObject<{ /** "user" | "project" | "both". Default: "both". */ roleScope: Type.TOptional, Type.TLiteral<"project">, Type.TLiteral<"both">]>>; /** * Default role name applied when no --role / PI_ROLE is supplied. * Default: "role-assistant" (the built-in fallback). * If set to a missing role, we warn and use the built-in role-assistant. */ defaultRole: Type.TOptional; /** Default intercom mode for roles that don't set `intercom:`. Default: "off". */ intercomMode: Type.TOptional, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>>; /** * Model used to summarize the first user message into the session-name * intent. Default: a small/cheap model when one is available, falling * back to the session's current model. */ titleModel: Type.TOptional; /** Whether to surface a warning when an mcp:* tool can't be resolved. Default: true. */ warnOnMissingMcp: Type.TOptional; }>; type PiRolesSettings = Static; /** * pi-roles extension entry point. * * Wires together discovery (roles.ts), application (apply.ts), and settings * (settings.ts) into the three Pi integration points the role lifecycle * actually needs: * * - `session_start` — restore from persisted state on reload/resume, * otherwise resolve a role name from the precedence chain (pendingReset * > --role > PI_ROLE > settings.defaultRole > built-in role-assistant) * and apply it. * - `before_agent_start` — re-inject the active role's body as the system * prompt every turn (Pi rebuilds the prompt per turn; this is the * stable hook). * - `/role` command — list, current, reload, switch (with optional * --reset to clear history first). * * The module-scoped state below is the source of truth for "what role is * live in this extension instance". Pi reloads spin up a fresh module, at * which point we restore from the most recent `pi-roles:active-role` entry * in the session log. */ interface RuntimeState { /** Live role applied to this session, or null before first apply. */ activeRole: ResolvedRole | null; /** Set by `/role --reset` so the next session_start (reason="new") applies it. */ pendingRoleAfterReset: string | null; /** Cached discovery result; refreshed on session_start, every `/role` invocation, and `/role reload`. */ roles: RawRole[]; /** Shadowed roles found at lower-precedence scopes; shown in `/role list`. */ shadowed: { name: string; source: string; path: string; }[]; /** Cached settings for the current cwd; refreshed on session_start. */ settings: PiRolesSettings; /** Carried across role swaps so the session-name intent survives a role change. */ intent: string | undefined; /** * True while a title-generation request is in flight. Prevents * `before_agent_start` from spawning a second concurrent summarization * if it fires again before the first resolves. Reset to false in * `generateAndApplyTitle`'s finally block. */ titleInFlight: boolean; /** * True after we've shown the one-time title-generation error hint. * Prevents spamming the user on every prompt when the title model * is misconfigured or lacks credentials. */ titleErrorShown: boolean; } declare function export_default(pi: ExtensionAPI): void; /** * Build the replacement system prompt for the current active role. * * Returns `undefined` when there's no active role (Pi keeps its default for * that turn). Otherwise returns `{ systemPrompt }` with the role body — and, * when intercom is requested AND the intercom tool is registered, a small * mode-specific addendum appended to the body. * * Exported for unit tests; the handler in `before_agent_start` is a one-line * delegation. */ declare function composeSystemPrompt(state: Pick, pi: Pick): { systemPrompt: string; } | undefined; /** * Pick the role to launch with on a fresh session_start (no pendingReset, no * persisted state to restore). Precedence per BUILD-STATUS.md: * * --role flag > PI_ROLE env > settings.defaultRole > built-in role-assistant * * If a configured `defaultRole` doesn't exist, we fall through to the * built-in rather than failing — a missing role shouldn't lock the user out * of the session. */ declare function pickInitialRoleName(pi: ExtensionAPI, settings: PiRolesSettings, roles: RawRole[]): string; /** * Provide tab completions for `/role `. Combines built-in subcommands * with discovered role names; case-insensitive prefix match. */ declare function roleCompletions(prefix: string, roles: RawRole[]): AutocompleteItem[] | null; export { composeSystemPrompt, export_default as default, pickInitialRoleName, roleCompletions };