Skip to content

Facets

A facet is a Markdown-defined persona that controls prompt framing, tool access, skill access, model pinning, and session color. The frontmatter declares the facet settings, and the body supplies the system-prompt framing the model works under. Where Instructions Live helps you choose between a facet, a skill, and an AGENTS.md file; Prompting and Input covers day-to-day switching.

Switching facets changes the system prompt and the available tools while keeping the same conversation. The shipped plan and execute facets work together: the model researches and writes a plan in plan, then handoff_plan moves the work into execute, which gives the model its edit and shell tools. The shipped orchestrate facet coordinates complex work through subagents while the parent model retains responsibility for dependencies, verification, and synthesis.

Each facet supplies its own system prompt, and that prompt sits ahead of the entire conversation in every request. Switching facets therefore causes a cache miss on the next call: the conversation arrives under a prompt prefix the provider has not cached, so the call gets no cache discount unless the provider still holds a live cache entry for the facet you switched to. That first call rebuilds the cache from the new prefix, and later calls return to the cached rate.

A facet file goes in .polytoken/facets/ in your project, or in $XDG_CONFIG_HOME/polytoken/facets/ (default: ~/.config/polytoken/facets/) for facets you want everywhere. The filename is the facet’s name: scribe.md defines a facet named scribe, and the name key in the frontmatter must match the filename, or Polytoken rejects the file.

Facets override by replacement, not by merge. When two facets share a name, the more specific one wins: a project facet overrides a global one, and either overrides a shipped facet such as plan, execute, or orchestrate. A custom plan is your plan, not an edited copy of the shipped one.

The body below the frontmatter is a template; see Templating for the syntax. Polytoken renders the facet body into the facet’s complete system-prompt framing and adds no base prompt of its own.

In most facets you will want Polytoken’s shipped base prompt under your own framing. Include it with transclude at the top of the body:

{{ transclude("polytoken://system_prompts/facet.md") }}
Your job is to help the user with their documents.

The shipped plan, execute, and orchestrate facets all start this way, then add their own lines. The base prompt carries tool-use guidance and other behavior the model depends on. Leaving the transclude out means writing that guidance yourself.

A facet template can include @skill:<name>. Polytoken resolves the reference when the facet activates and applies that facet’s own polytoken.skills_allow and polytoken.skills_deny lists. A broken, denied, or polytoken.disable_model_invocation: true reference fails activation. The HTTP facet route returns 422, and the switch_facet tool returns an error. During startup or rewind, Polytoken restores a fallback facet instead. Repeated switches do not stack duplicate skill reminders, and compaction does not resurrect skills from a facet you left.

The shipped plan facet investigates with read-only shell access for repository inspection but with project editing tools withheld, writes a handoff plan, runs the built-in plan-reviewer, and asks you before moving work into execute. The plan facet exposes shell_exec so the model can run repository-state commands that built-in tools cannot cover, such as git status, git log, and gh. The facet prompt steers the model toward grep, glob, and file_read for file inspection, and both the prompt and the autonomous classifier prohibit any shell command that modifies files, the working tree, or system state. The reviewer checks the plan against your project override when project_vars.plan_facet.plan_spec_override is set, or against Polytoken’s default plan shape otherwise.

Replace the whole facet with .polytoken/facets/plan.md when you want to change that behavior, the tools the facet exposes, or the prompt outside the plan format. The plan facet does not track its own planning work with todos, because todos persist into the execute facet after a handoff; the todo tools stay available for when you ask for them by name.

To keep the shipped behavior and change only the handoff-plan format, set a project variable for the shipped plan facet. The variable replaces the default plan format rather than adding to it, and plan-reviewer checks plans against that same override, so write the complete plan-format instructions you want both the model and the reviewer to follow. See Template Reference for the exact key and YAML shape.

This example defines a scribe facet in .polytoken/facets/scribe.md, giving the model a documents-assistant framing and Notion tools:

---
name: scribe
polytoken:
tools: [mcp__notion]
color: "#7c3aed"
---
{{ transclude("polytoken://system_prompts/facet.md") }}
Your job is to help the user with their documents. You have full access to their
Notion workspace through its tools. Work from what the user asks for.

mcp__notion grants every tool from the Notion MCP server. To grant the server but exclude one tool, add a tools_deny:

tools: [mcp__notion]
tools_deny: [mcp__notion__delete_page]

Conventional keys like name sit at the top level. Every Polytoken-specific key is polytoken.<key>. The table below lists the keys a facet can use.

KeyTypeDefaultMeaning
namestringrequiredThe facet’s name. Must match the filename.
polytoken.modelstringactive modelPins the facet to a model. Without it, the facet uses the active model.
polytoken.fallback_modelslistnoneModels to fall back to when the primary is unavailable.
polytoken.toolslistnoneThe tools the facet exposes. Accepts literal names, mcp__<server> for all of a server’s tools, tag!ALL for every tool, and tag!ALL_MCP for every MCP tool.
polytoken.tools_denylistnoneTools to remove from the granted set. Literal names only, using the full mcp__<server>__<tool> name for an MCP tool.
polytoken.undeferred_toolslistnoneThe granted tools whose full definitions appear up front rather than through tool search. Literal names only.
polytoken.skills_allowlistallow allThe skills the facet permits, as names or tag!<name> groups. Leaving it out means no restriction, so every skill is allowed.
polytoken.skills_denylistnoneThe skills the facet blocks. Deny wins over allow.
polytoken.colorstringnoneThe facet’s accent color, as #RRGGBB. Sets both light and dark.
polytoken.color_lightstringnoneThe accent color for light themes. Overrides color.
polytoken.color_darkstringnoneThe accent color for dark themes. Overrides color.
polytoken.autonomous_hintstringnoneGuidance for the autonomous permission classifier: in Autonomous mode, Polytoken adds this text to the classifier’s context for each tool call made in this facet, steering the allow, ask, and deny decision. See what the classifier sees for the complete input context.
polytoken.compaction_hintstringnoneGuidance Polytoken appends to the compaction summary prompt when this facet is active, naming the context the summary should preserve for this facet’s work.
polytoken.facet_transitions.<target>.allowedbooleanopenWhether this facet may switch to <target>. With no transitions declared, every switch is allowed.
polytoken.facet_transitions.<target>.conditionstringnoneWhen set, switching to <target> first asks you to confirm, showing this text.

When a facet omits all three color fields, Polytoken generates a deterministic accent from the facet name for both light and dark themes. The two generated colors share the same hue but differ in lightness so each reads against its background.

switch_facet is a regular tool. List it in polytoken.tools to let a facet switch to another facet. The shipped execute facet includes it; plan leaves it out, so the model cannot leave planning on its own and uses handoff_plan to transition the work to you.

handoff_plan submits the active plan for your approval; you can implement it in a fresh context or in the current one, or cancel. The model names the target facet when it calls handoff_plan; the facet argument defaults to execute, and orchestrate is available the same way. Polytoken has no config key for this default, so when you want a handoff to orchestrate, tell the model in the plan facet prompt or in your request.

Polytoken checks the plan files in the session plan directory before approval. Polytoken refuses the handoff when the directory contains another plan that has not already been approved or when a previously approved plan changed. The refusal does not switch facets or clear the context. Set confirm_plan_path to the active plan filename or its full path when you have reviewed the newer or changed files and want Polytoken to adopt the current directory contents.

The shipped plan facet runs the built-in plan-reviewer before handing off; the review is a recommendation, not a requirement. You decide at the approval step whether to proceed, and you can skip review entirely. The reviewer evaluates the current saved plan, so its findings correspond to the version you are approving.

Leaving polytoken.skills_allow out means no restriction, so the facet gets every skill. Once you write a non-empty list, though, it is a commitment: if your patterns match no skills, through a misspelled name or an empty tag! group, Polytoken fails the facet’s skill access closed rather than reverting to allow-all: the facet runs with no skills.

The tag!ALL and tag!ALL_MCP shorthands belong in polytoken.tools. The lowercase tag!<name> form is for skills only, in polytoken.skills_allow and polytoken.skills_deny, and Polytoken rejects it in a tool list.

The four goal lifecycle tools are propose_goal, read_goal, complete_goal, and block_goal. When your facet lists any of them in polytoken.tools, the model can call them directly. This holds for every model, including models that load tools on demand, where a facet tool would otherwise wait in a catalog until the model searches for it. Subagents never receive these tools; they belong to the main session.

A tool name your facet declares can fail to resolve. A typo, an extension tool that is not installed, or an MCP server that is not connected all leave the name without a backing tool. Polytoken warns instead of rejecting the facet, because membership depends on what is loaded at runtime. You see the warning in the daemon log when Polytoken starts and when you reload its configuration. polytoken validate facet prints a note for names it cannot recognize statically. The model receives a one-time reminder naming the unresolved tools the first time it would be affected. Fix the spelling or load the missing tool; a facet that declares an unresolvable name keeps working, and calls to the missing tool fail.

To see exactly which tools a facet resolves to, request the GET /tools/effective route with the daemon’s bearer token. It returns the active facet’s tool plan, or any named facet’s plan when you pass ?facet=<name>. The plan lists the tools the model can call directly, the tools waiting in the deferred catalog, the tools hidden by permission rules, and any unresolved names. A named facet’s plan reflects the model a switch to that facet would activate, so a facet that pins a different model shows that model’s tool loading. The request changes nothing: it never switches the facet or the model.

Polytoken loads facets when it starts and when you reload its configuration. Editing a facet file does not take effect until then.