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 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. 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 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.

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.

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.

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]

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.

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.
descriptionstringnoneA short description that Polytoken shows in the facet catalog.
polytoken.modelstringfacet-bound model group for the shipped execute, plan, and orchestrate facets; the active model otherwisePins 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_modelslistnoneModels to fall back to when the primary is unavailable.
polytoken.toolslistnoneThe 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_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 allPermits skills by name, tag!ALL for every model-invocable skill, or lowercase tag!<name> groups. Omitting the list leaves skill access unrestricted.
polytoken.skills_denylistnoneBlocks skills by name, tag!ALL for every model-invocable skill, or lowercase tag!<name> groups. Deny wins over allow.
polytoken.colorstringnoneThe facet’s accent color, as #RRGGBB. Sets both light and dark.
polytoken.color_lightstringnoneSets the accent color for light themes, overriding polytoken.color.
polytoken.color_darkstringnoneSets the accent color for dark themes, overriding polytoken.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.auto_facet_hintstringnoneLegacy alias for polytoken.autonomous_hint. Polytoken uses polytoken.autonomous_hint when you set both keys.
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>.allowedbooleanrequired for each declared targetControls 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>.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.

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: review
polytoken:
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.

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.

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.