Role-based session configuration for pi coding agent. Launch a session as a named role (architect, planner, marketing-strategist, …) and hot-swap roles mid-session — without restarting Pi.
pi-roles is to top-level pi sessions what pi-subagents is to sub-agents: same .md + YAML frontmatter convention, same project/user scope rules, drop-in flow. The extension is agnostic of which roles exist — roles are just markdown files you create.
pi --role architect # launch as architect
PI_ROLE=planner pi # or via env
/role list # inside the session
/role planner # swap role mid-session, keep history
/role planner --reset # swap role and clear history
/role current
/role reload # re-read the active role file from diskWhen you swap roles, the session's system prompt, model, thinking level, and active tool set are replaced according to the new role's definition. Conversation history is preserved by default.
pi install npm:pi-rolesThen restart pi. The extension is auto-discovered; the --role flag and /role command become available.
To try without installing:
pi -e git:github.com/lojacobs/pi-rolesWhen you build a multi-agent dev workflow with specialized roles — architect (design), planner (decompose), orchestrator (dispatch), or any equivalent for marketing, research, ops — the top-level role is a property of the whole session, not of an individual sub-agent dispatch. You want different system prompts, different models, different tool restrictions per role, and you want to switch between them without restarting.
pi-roles is the cleanest way to do that. No shell aliases, no separate workspace directories, no forking pi-subagents into something it isn't.
Roles are markdown files with YAML frontmatter, identical in shape to pi-subagents agent files:
---
name: architect
description: Defines the WHAT. Owns architecture and specs. Never codes.
model: anthropic/claude-opus-4-7
thinking: high
tools: read, grep, find, ls, write, edit
intercom: send # optional, per-role override
extends: base-reviewer # optional, role inheritance
---
# Role
You are the Architect. Your job is to define WHAT the system should
do, never HOW to build it...
(everything below the frontmatter is the system prompt body)| Field | Required | Behavior when omitted |
|---|---|---|
name |
yes | — must match the filename without .md |
description |
yes | — shown in /role list and selectors |
model |
no | keeps the session's current model |
thinking |
no | keeps the session's current thinking level (off, minimal, low, medium, high, xhigh) |
tools |
no — see below | inherits from parent (if extends) or keeps current active set |
intercom |
no | falls back to the global intercomMode setting |
extends |
no | role inherits from another role |
Three explicit states with three different meanings:
| YAML | Meaning |
|---|---|
tools: read, bash |
set — exactly these tools, nothing else |
tools: (present, empty) |
disable all tools — read-only conversation, no actions |
| field absent | inherit — from parent role (extends), else keep session default |
You can also include MCP tool refs in the same field, mirroring pi-subagents:
tools: read, grep, mcp:chrome-devtools, mcp:githubmcp:server-name entries require pi-mcp-adapter to be installed. If it's not, the entry is logged and skipped — the role still loads.
Matches Pi's CLI --model syntax:
model: anthropic/claude-opus-4-7
model: openai/gpt-5
model: deepseek/deepseek-v4-proIf the model isn't available (no API key, unknown provider), the role load surfaces a warning and the session keeps its current model.
Values: off, receive, send, both. Defaults to the global intercomMode setting (which itself defaults to off). See pi-intercom integration below.
extends: architectA child role inherits everything from its parent and overrides selectively. Chains are supported (A extends B extends C); cycles are detected at load time and produce a hard error.
Merge rules:
model,thinking,description,intercom: child wins if set, else parent value.tools: see the tri-state above. Set overrides; empty disables; absent inherits.- Markdown body: parent's body is prepended to child's body with a separator. Useful for "stricter variant" patterns (
architect-strict extends architect). name: never inherited — always the child's own filename.
Roles are looked up in this order, first match wins for any given name:
| Scope | Path | Marker in /role list |
|---|---|---|
| project | <repo>/.pi/roles/<name>.md (or any ancestor) |
[project] |
| user | ~/.pi/agent/roles/<name>.md |
[user] |
| built-in | bundled with the package | [built-in] |
When a project-scope role shadows a user-scope role of the same name, the user-scope entry is listed under a separate "Shadowed" heading in /role list output so you know it exists but won't load.
The default roleScope is both (project + user + built-in). Override via settings:
// ~/.pi/agent/settings.json
{
"pi-roles": {
"roleScope": "both", // "user" | "project" | "both"
"defaultRole": "architect", // optional; falls back to role-assistant
"intercomMode": "off", // "off" | "receive" | "send" | "both"
"titleModel": "openai/gpt-4o-mini"
}
}pi-roles ships one built-in role: role-assistant. It's the default fallback when no defaultRole is configured and you don't pass --role or PI_ROLE.
The role-assistant:
- Lists the roles available on your machine (project + user + built-in) on its first turn.
- Shows the exact command to switch to one (e.g.
/role architect). - Offers to help you build a new role: it asks the questions, drafts the markdown, shows it for your approval, writes it to project or user scope (your choice), and then prints the command for you to launch it (
/role <new-name> --reset).
You can override the built-in by dropping a role-assistant.md into your project or user roles directory — the same priority rules apply.
You can also set any other role as your default:
{ "pi-roles": { "defaultRole": "architect" } }If defaultRole points to a missing role, the built-in role-assistant is used and a warning is shown.
/role <subcommand> — Tab-completes role names against the current scope.
| Form | Behavior |
|---|---|
/role <name> |
Switch to <name>. Preserves history. Re-reads the file from disk (no caching). |
/role <name> --reset |
Switch to <name> and clear history (equivalent to /new + apply role). |
/role list |
List discovered roles with scope markers and shadowing info. |
/role current |
Show the currently active role's name, extends chain, description, and source path. |
/role reload |
Re-read the currently active role's file from disk and re-apply. Useful while you're iterating on a prompt. |
pi --role architect "Help me design the auth schema"
PI_ROLE=architect piResolution order: --role > PI_ROLE > defaultRole setting > built-in role-assistant.
Each session is named <role-name> (and, once the title-generation phase ships, <role-name> — <intent>, where <intent> is a short summary of your first user message). The role-name prefix updates when you /role to a different role.
The session name is set via Pi's native pi.setSessionName() API, so:
- It shows in Pi's session selector and
/resumelistings. pi-intercomautomatically uses it as the session target — making cross-session messaging work out of the box.
The role indicator also appears in Pi's footer (via ctx.ui.setStatus), composing cleanly with pi-powerline-footer if you have it installed. No extra dependency required.
Title generation model (planned). The titleModel setting is reserved for the future intent-summarization step; it has no effect today. The current release sets the session name to the bare role name and updates the prefix on swap.
pi-intercom is an optional peer dependency. pi-roles works without it; intercom features are no-ops when it's not installed.
When it is installed, the global intercomMode setting controls whether roles get the intercom tool added to their active tool set, plus a small system-prompt addendum telling the LLM how and when to use it:
| Mode | Behavior |
|---|---|
off |
Default. No intercom tools, no prompt mentions. |
receive |
Role can be targeted by other sessions but won't proactively send. |
send |
Role can send to other sessions but doesn't expect inbound coordination. |
both |
Full bidirectional coordination — intercom tool active, prompt encourages use. |
Per-role override via the intercom: frontmatter field. Common pattern: architect and planner set intercom: both, orchestrator sets intercom: off (you don't want the orchestrator distracted by chatter while it dispatches).
Inter-session messaging is always between named sessions on the same machine — pi-roles only opts roles in or out, it doesn't manage the broker, the protocol, or the message store. That's all pi-intercom.
When pi-mcp-adapter is installed, you can list MCP tools alongside built-ins in the tools field with the mcp:server-name syntax:
tools: read, grep, write, mcp:chrome-devtools, mcp:githubThis mirrors pi-subagents's convention exactly, so muscle memory transfers. If pi-mcp-adapter isn't installed, the mcp:* entries are logged and skipped — the role still loads with its built-in tools.
The first time you use a new MCP server, its tool metadata is cold-cached; you may need to restart Pi once for direct MCP tools to become available. This is a pi-mcp-adapter behavior, not something pi-roles controls.
/role reload re-reads the currently active role's file from disk and re-applies it. This is for iterating on a prompt without restarting your session.
/role <name> (without --reset) also always re-reads from disk — there's no hidden cache between switches.
For roles with extends, the entire chain is re-resolved on every load.
If you have Pi's auto-reload feature wired up via /reload, that triggers a full extension reload as well — your role re-applies from scratch.
// ~/.pi/agent/settings.json (global) or .pi/settings.json (project)
{
"pi-roles": {
"roleScope": "both",
"defaultRole": "role-assistant",
"intercomMode": "off",
"titleModel": "openai/gpt-4o-mini",
"warnOnMissingMcp": true
}
}| Key | Default | Description |
|---|---|---|
roleScope |
"both" |
Discovery scope. "user", "project", or "both". |
defaultRole |
"role-assistant" |
Role applied at session start when no --role or PI_ROLE. |
intercomMode |
"off" |
Default intercom behavior for roles that don't set it explicitly. |
titleModel |
null (auto) |
Model used for session-intent summarization. Falls back to a small built-in or session's current model. |
warnOnMissingMcp |
true |
Whether to surface a warning when a role's mcp:* entry can't be resolved. |
Project settings beat global settings, per Pi's standard precedence.
- Spawn sub-agents. That's
pi-subagents. The two compose: usepi-rolesfor top-level session roles,pi-subagentsfor delegated workers within a role. - Define any built-in roles other than
role-assistant. Roles are user content; the extension stays small. - Manage parallel sessions. Use multiple terminals or
tmux. Coordination between parallel sessions is whatpi-intercomhandles, optionally. - Persist which role was active across pi restarts — except via
--role/PI_ROLE/defaultRole. By design. - Restrict which tools a role can request. If a role lists
bash, it getsbash. Permission boundaries are your call — pair withpermission-gate.tsor a similar guard if you need them.
These are decided, not configurable, so the extension behaves predictably:
- Markdown body fully replaces Pi's default system prompt. No silent merging — most non-coding roles are polluted by the default "expert coding assistant" framing, so by design the role body is authoritative. The handler returns
{ systemPrompt: <role body> }frombefore_agent_startand ignores the upstream chain value. If you want to keep Pi's default content (or compose with another extension), include the relevant text in your role body explicitly. - Role inheritance:
model/thinkingoverride;toolsis tri-state (set/empty/absent); markdown body is prepended. - Cycle detection in
extendsis a hard error at load time, not a warning. A circular role is broken; refusing to load it is the only sane behavior. /role <name>always re-reads from disk. No staleness between switches, ever.--resetis explicit. The role-assistant prints the exact--resetcommand for you to run manually rather than auto-resetting; resetting is destructive enough to deserve a deliberate keystroke.- Title generation (planned, not yet implemented). The current release sets the session name to the bare role name; intent-summarization on first user message lands in a follow-up.
--resetalready clears the cached intent so the future implementation drops in cleanly. - Built-in
role-assistantlives at the lowest discovery priority. Drop a same-named file in user or project scope to override it. /role listshows shadowed entries with a(shadowed)marker — you can see what would load if the higher-priority file didn't exist.
After install, your roles live wherever you like — typical setup:
~/.pi/agent/roles/
architect.md
planner.md
orchestrator.md
marketing-strategist.md
campaign-manager.md
<repo>/.pi/roles/
architect.md # overrides the user one for this project
The extension itself ships only role-assistant.md (built-in scope) and the runtime code.
See examples/ for two reference role files:
architect.md— minimal: just system prompt + model + thinking.orchestrator.md— fully loaded: every frontmatter field, includingextends,intercom, MCP tools.
MIT. See LICENSE.
Inspired by and structurally indebted to pi-subagents (frontmatter convention, scope discovery), pi-prompt-template-model (per-trigger model/skill/thinking switching), and the preset.ts example in pi-mono. Thanks to those authors for both the patterns and the working code to learn from.