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 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.
Where facets live
Section titled “Where facets live”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. Polytoken searches shipped and
global facets first, then each daemon.discovery.extra_facets_dirs entry in
config order, then each --facets-dir directory supplied when you create a
session, and finally project .polytoken/facets/. A later directory replaces a
facet with the same name. Project facets take precedence over session extras,
session extras over config extras, and config extras over global and shipped
facets.
Set daemon.discovery.extra_facets_dirs in your user configuration file to add
facet directories. Use absolute paths or paths beginning with ~/; ~ alone
selects your home directory. Polytoken rejects daemon.discovery in project
configuration. The interactive configurator does not expose this setting.
Polytoken skips missing directories and follows symlinks according to
daemon.follow_symlinks_for_configs. For a single invocation,
exec --facets-dir adds facets only for that run.
A facet defined under the config facets key takes precedence over every
filesystem definition with the same name. A custom plan replaces the shipped
plan rather than extending it.
The body is a template
Section titled “The body is a template”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.
Because facet bodies always render as templates, Polytoken rejects the retired
polytoken.jinja frontmatter key in facet files. Remove the key from older
facets. Skill files differ: they still accept and ignore polytoken.jinja.
Run polytoken validate facet <path> to check a custom facet before loading it.
Polytoken applies the same frontmatter checks when loading and validating facets.
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.
@skill: references in a facet template
Section titled “@skill: references in a facet template”A facet template can include @skill:<name>. When the facet activates,
Polytoken resolves the reference using the facet’s polytoken.skills_allow and
polytoken.skills_deny lists. A broken or denied reference, or a skill with
polytoken.disable_model_invocation: true, prevents activation. The HTTP facet
route returns 422, and switch_facet returns an error. During startup or rewind,
Polytoken restores a fallback facet instead.
Repeated facet switches do not add duplicate skill reminders. Compaction does not restore skills from a facet you left.
Customizing the shipped plan facet
Section titled “Customizing the shipped plan facet”The shipped plan facet investigates your repository with read-only shell
access. Polytoken withholds project editing tools during planning. The model
writes a handoff plan, runs the built-in plan-reviewer, and asks you before
moving work into execute.
For repository-state commands that built-in tools cannot cover, such as
git status, git log, and gh, the plan facet exposes shell_exec. Its prompt
steers the model toward grep, glob, and file_read for file inspection.
Both the prompt and the autonomous classifier prohibit shell commands that
modify files, the working tree, or system state.
The reviewer checks the plan against your project override when you set
project_vars.plan_facet.plan_spec_override. Otherwise, the reviewer uses
Polytoken’s default plan shape.
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.
A small facet
Section titled “A small facet”This example defines a scribe facet in .polytoken/facets/scribe.md, giving
the model a documents-assistant framing and Notion tools:
---name: scribepolytoken: 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 theirNotion 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]The polytoken.tools list is the complete set of tools the facet may use.
Connected MCP servers and installed extensions add nothing by themselves. An
MCP tool appears only through tag!ALL_MCP, mcp__<server>, or the full
mcp__<server>__<tool> name, and an extension tool appears only through its
literal name in the list.
Three helper tools are Polytoken’s own, not MCP or extension tools, and the
polytoken.tools list does not control them. Polytoken adds tool_search
when any model in the configuration sets tool_loading to native_deferred,
or when the configuration includes an MCP server. mcp_list_resources and
mcp_read_resource appear when the configuration includes an MCP server. A
facet does not need to declare these helpers. Naming a helper in
polytoken.tools_deny removes it.
The model needs tool_search to discover tools under deferred tool loading.
Removing tool_search through a facet or permission rule makes the deferred
tools inaccessible. Polytoken rejects a facet switch that would cause this
problem and explains how to fix the configuration. A subagent spawn with the
same tool settings fails with an error. If the problem arises when a turn
starts, Polytoken ends the turn immediately with an error explaining the remedy.
Keep tool_search available by removing it from polytoken.tools_deny or the
denying permission rule. Alternatively, switch the governing model to eager
tool loading.
Frontmatter keys
Section titled “Frontmatter keys”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.
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | required | The facet’s name. Must match the filename. |
description | string | none | A short description that Polytoken shows in the facet catalog. |
polytoken.model | string | facet-bound model group for the shipped execute, plan, and orchestrate facets; the active model otherwise | Pins the facet to a model or a model group (@mg:<group>). A facet pinned to a group follows the group’s failover order. Without a model pin, the facet uses the active model. The shipped execute, plan, and orchestrate facets pin their facet-bound groups. See Pinning a model group. |
polytoken.fallback_models | list | none | Models to fall back to when the primary is unavailable. |
polytoken.tools | list | none | The tools the facet permits. Connected MCP servers and installed extensions do not grant undeclared tools. Accepts literal names (including the full mcp__<server>__<tool> name for an MCP tool and the extension tool’s literal name for an extension tool), mcp__<server> for all of a server’s tools, tag!ALL for every core tool, and tag!ALL_MCP for every MCP tool. Polytoken also adds its own helpers regardless of the list: tool_search when any model sets tool_loading to native_deferred or the configuration includes an MCP server, mcp_list_resources and mcp_read_resource when the configuration includes an MCP server, and polytoken.tools_deny removes any helper it names. |
polytoken.tools_deny | list | none | Tools to remove from the granted set. Literal names only, using the full mcp__<server>__<tool> name for an MCP tool. |
polytoken.undeferred_tools | list | none | The granted tools whose full definitions appear up front rather than through tool search. Literal names only. |
polytoken.skills_allow | list | allow all | Permits skills by name, tag!ALL for every model-invocable skill, or lowercase tag!<name> groups. Omitting the list leaves skill access unrestricted. |
polytoken.skills_deny | list | none | Blocks skills by name, tag!ALL for every model-invocable skill, or lowercase tag!<name> groups. Deny wins over allow. |
polytoken.color | string | none | The facet’s accent color, as #RRGGBB. Sets both light and dark. |
polytoken.color_light | string | none | Sets the accent color for light themes, overriding polytoken.color. |
polytoken.color_dark | string | none | Sets the accent color for dark themes, overriding polytoken.color. |
polytoken.autonomous_hint | string | none | Guidance 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.auto_facet_hint | string | none | Legacy alias for polytoken.autonomous_hint. Polytoken uses polytoken.autonomous_hint when you set both keys. |
polytoken.compaction_hint | string | none | Guidance 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>.allowed | boolean | required for each declared target | Controls whether this facet may switch to <target>. Polytoken requires a boolean for each declared target and permits every switch when you declare no transitions. |
polytoken.facet_transitions.<target>.condition | string | none | When 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.
Pinning a model group
Section titled “Pinning a model group”polytoken.model accepts a model-group reference (@mg:<group>), not just a
single model name. A project-local facet pinned to a group activates the group’s
current candidate. When that candidate fails over because of rate limits or
terminal provider errors, the session follows the group, including across
detach and resume. Put the facet file in your project’s .polytoken/facets/
directory:
---name: reviewpolytoken: model: "@mg:primary" tools: - file_read---Review the current diff.With [modelgroups] primary = ["model-a", "model-b"] in your configuration,
switching to the review facet routes through the group.
By design, the shipped execute, plan, and orchestrate facets
pin their own facet-bound groups: polytoken:execute_facet,
polytoken:plan_facet, and polytoken:orchestrate_facet. Each group resolves to the full tier’s default
model (see the [modelgroups] section of the configuration reference).
Switching to a shipped facet through handoff_plan or switch_facet selects
that group’s current candidate instead of retaining the session’s previous
model.
To override a facet-bound group, define its reserved name in [modelgroups].
Your definition replaces the shipped value, and the facet follows your
selection. When the bound group has no usable selection, for example when no
full-tier default resolves, Polytoken refuses the switch with an error naming
the unresolved reference. The session keeps its previous model.
An explicit startup --model takes precedence over the facet’s model pin and
pins the named model directly. For example,
polytoken new --model X --facet Y starts the session on X.
Tool and skill access notes
Section titled “Tool and skill access notes”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 the model can name orchestrate there as well. 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. An approved handoff that
names a shipped facet switches through that facet’s group, so the session
continues on the group’s current candidate.
Before approval, Polytoken checks the files in the session plan directory. The
handoff fails if the directory contains another plan you have not already
approved or if a previously approved plan changed. Polytoken leaves the facet
and context unchanged when it refuses the handoff. After reviewing the newer
or changed files, set confirm_plan_path to the active plan filename or its
full path 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.
Omitting polytoken.skills_allow leaves skill access unrestricted. A non-empty
list permits only the skills it matches. If the entire list matches nothing,
for example because of a misspelled name or an empty tag! group, Polytoken
runs the facet with no skills rather than allowing every skill.
In polytoken.tools, tag!ALL grants every core tool and tag!ALL_MCP grants
every MCP tool. Both polytoken.skills_allow and polytoken.skills_deny accept
tag!ALL for every model-invocable skill, or lowercase tag!<name> for a skill
group. Polytoken accepts tag!ALL_MCP only in polytoken.tools and rejects
lowercase skill tags in tool lists.
Goal tools and unresolved tool names
Section titled “Goal tools and unresolved tool names”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.
Loading changes
Section titled “Loading changes”Polytoken loads facets when it starts and when you reload its configuration. Editing a facet file does not take effect until then.