# Prompt templates

> Custom system prompts and prompt templates as reusable session configuration, with SDK entry points and related prompt tests.

- Repository: earendil-works/pi
- GitHub: https://github.com/earendil-works/pi
- Human docs: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1
- Complete Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/sdk/08-prompt-templates.ts`
- `packages/coding-agent/examples/sdk/03-custom-prompt.ts`
- `packages/coding-agent/test/suite/agent-session-prompt.test.ts`
- `packages/coding-agent/README.md`
- `packages/coding-agent/examples/sdk/README.md`

---

---
title: "Prompt templates"
description: "Custom system prompts and prompt templates as reusable session configuration, with SDK entry points and related prompt tests."
---

Pi exposes two related prompt surfaces in `@earendil-works/pi-coding-agent`: **prompt templates** (Markdown files expanded when the user types `/name`) and **system prompts** (the session system message built by `buildSystemPrompt`, overridable via `SYSTEM.md`, CLI flags, or `DefaultResourceLoader` hooks). Templates are session configuration resources; system prompts shape every model turn.

## Concepts

| Surface | What it is | How it is invoked | Owns the text |
|--------|------------|-------------------|---------------|
| Prompt template | Reusable user-message Markdown snippet | `/template-name [args]` in the editor, `session.prompt("/name ...")`, or RPC/print input | Template body after argument substitution |
| Skill command | On-demand skill body | `/skill:name [args]` | Skill expansion path (not template substitution) |
| Extension command | Registered command handler | `/command [args]` | Extension handler; no provider prompt by default |
| System prompt | Model system message for the session | Built at session start / tool rebuild | `buildSystemPrompt` + loader overrides |

Slash input handling in `AgentSession.prompt()` runs in this order when `expandPromptTemplates` is true (default):

1. Extension commands (`pi.registerCommand`) — executed immediately; no LLM turn.
2. Extension `input` handlers (may transform or fully handle text).
3. Skill expansion (`/skill:name`).
4. Prompt template expansion (`/name` → template body with `$1` / `$@` substitution).

Pass `{ expandPromptTemplates: false }` to skip command handling and template expansion (used for internal message paths that must send literal text).

## Prompt template format

Templates are `.md` files. The filename without extension becomes the slash command name: `review.md` → `/review`.

```markdown
---
description: Review staged git changes
argument-hint: "[focus]"
---
Review the staged changes (`git diff --cached`). Focus on:
- Bugs and logic errors
- Security issues
- Error handling gaps

Optional focus: ${1:-general quality}
```

### Frontmatter fields

| Field | Required | Behavior |
|-------|----------|----------|
| `description` | No | Autocomplete label. If omitted, the first non-empty body line is used (truncated to 60 chars + `...`). |
| `argument-hint` | No | Shown before the description in autocomplete. Convention: `<required>` and `[optional]`. Empty values are ignored. |

### Loaded shape (`PromptTemplate`)

| Field | Type | Notes |
|-------|------|-------|
| `name` | `string` | Basename of the `.md` file |
| `description` | `string` | From frontmatter or first body line |
| `argumentHint` | `string?` | From `argument-hint` |
| `content` | `string` | Markdown body after frontmatter |
| `filePath` | `string` | Absolute path (or synthetic SDK path) |
| `sourceInfo` | `SourceInfo` | Provenance (`source`, `scope`, `origin`, optional `baseDir`) |

SDK-defined templates must set `filePath` and `sourceInfo` (typically via `createSyntheticSourceInfo`).

## Argument substitution

`parseCommandArgs` splits the text after `/name` with bash-style quoting (`"` / `'`). `substituteArgs` then rewrites the template body.

| Placeholder | Meaning |
|-------------|---------|
| `$1`, `$2`, … | Positional args (1-indexed). Missing indices become `""`. `$0` is empty. |
| `$@` or `$ARGUMENTS` | All args joined with spaces |
| `${N:-default}` | Arg `N` if present and non-empty; otherwise `default` |
| `${@:-default}` / `${ARGUMENTS:-default}` | All args, or `default` when empty |
| `${@:N}` | Args from Nth position onward (1-indexed; `0` treated as `1`) |
| `${@:N:L}` | `L` args starting at N |

Rules enforced in tests:

- Substitution is **not recursive**: values that contain `$1` / `$@` / `$ARGUMENTS` stay literal.
- Default values are not expanded either.
- There is no escape syntax for `$` (a leading `\` is kept as a literal character and does not protect `$100`-style text from the `$1` match rules).
- Case-sensitive: `$arguments` is not `$ARGUMENTS`.

```text
/component Button "onClick handler"
→ content with $1 = Button, $2 = onClick handler, $@ = Button onClick handler
```

## Discovery and load order

### File locations

| Source | Path | Trust / gate |
|--------|------|--------------|
| User auto | `~/.pi/agent/prompts/*.md` | Always considered when discovery is on |
| Project auto | `.pi/prompts/*.md` | Only when the project is trusted |
| Settings | `prompts` array (files or directories) in user/project settings | Project settings require trust |
| Packages | `pi.prompts` in package manifest, or conventional `prompts/` | Via package resolution |
| CLI | `--prompt-template <path>` (repeatable) | Temporary; still available with `--no-prompt-templates` |
| SDK | `additionalPromptTemplatePaths`, `promptsOverride` | Embedder-controlled |

`prompts/` discovery is **non-recursive**. Nested templates must be listed explicitly in settings `prompts`, package manifest entries, or CLI paths.

Disable auto-discovery with `--no-prompt-templates` / `-np` (or `noPromptTemplates: true` on `DefaultResourceLoader`). Explicit CLI/SDK paths still load; when discovery is off and no explicit paths remain, the prompt set is empty.

### Name collision precedence

Resolved prompt paths are ordered by resource precedence (lower rank wins; first matching name keeps the template; losers emit a collision diagnostic):

1. Project settings entry (`local` + `project`)
2. Project auto-discovered (`.pi/prompts`)
3. User settings entry
4. User auto-discovered (`~/.pi/agent/prompts`)
5. Package resources

CLI/temporary paths are merged ahead of discovered package/user/project paths in the resource loader path list. Duplicates of the same filesystem path are canonicalized away before load.

## System prompts

### Default construction

When no custom system prompt is set, `buildSystemPrompt` builds a coding-assistant prompt that includes:

- Available tools that supply one-line `toolSnippets`
- Guidelines (tool-derived + always-on + extension/tool `promptGuidelines`)
- Pointers to pi docs/examples absolute paths
- Optional `appendSystemPrompt` section
- `<project_context>` from loaded context files
- Skills section when the `read` tool is available
- `Current working directory: …`

### Replacing or appending

| Mechanism | Effect |
|-----------|--------|
| `.pi/SYSTEM.md` (trusted project) or `~/.pi/agent/SYSTEM.md` | Replaces the default base prompt text |
| `.pi/APPEND_SYSTEM.md` or `~/.pi/agent/APPEND_SYSTEM.md` | Appended after the base/custom prompt |
| `--system-prompt <text-or-file>` | Sets loader system prompt (file path if the path exists, else literal text) |
| `--append-system-prompt <text-or-file>` | Append entries (repeatable) |
| `systemPromptOverride` / `appendSystemPromptOverride` on `DefaultResourceLoader` | Function hooks over discovered base values |

**Important:** even when the base prompt is fully replaced (`customPrompt` / `SYSTEM.md` / `systemPromptOverride`), `buildSystemPrompt` still appends project context files and skills (skills only if `read` is selected), then the cwd line. To drop discovered append files in the SDK, set `appendSystemPromptOverride: () => []`.

### Effective system prompt in sessions

`AgentSession` rebuilds the system prompt from the resource loader, active tools, tool snippets, skills, and context files. Extensions can inspect it with `ctx.getSystemPrompt()` and `ctx.getSystemPromptOptions()` (see `examples/extensions/system-prompt-header.ts` and `prompt-customizer.ts`).

## CLI surface

| Flag | Role |
|------|------|
| `--prompt-template <path>` | Load a template file or directory (repeatable) |
| `--no-prompt-templates`, `-np` | Disable discovery; explicit `--prompt-template` paths still work |
| `--system-prompt <text>` | Replace default system prompt base |
| `--append-system-prompt <text>` | Append text or file contents (repeatable) |

Interactive usage: type `/` in the editor for autocomplete of templates (source `prompt`), skills (`skill:name`), and extension commands. `/reload` reloads prompts with other resources.

## SDK entry points

Public package surface used for prompts:

- `createAgentSession`, `DefaultResourceLoader`, `getAgentDir`, `SessionManager`
- `type PromptTemplate`
- `createSyntheticSourceInfo`
- Session: `session.prompt(text, options?)`, `session.promptTemplates`

### Custom system prompt

```typescript
import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const cwd = process.cwd();
const agentDir = getAgentDir();

// Replace base prompt; clear APPEND_SYSTEM.md discovery
const loader = new DefaultResourceLoader({
  cwd,
  agentDir,
  systemPromptOverride: () =>
    `You are a helpful assistant that speaks like a pirate.\nAlways end responses with "Arrr!"`,
  appendSystemPromptOverride: () => [],
});
await loader.reload();

const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});

await session.prompt("What is 2 + 2?");
session.dispose();
```

Append-only:

```typescript
const loader = new DefaultResourceLoader({
  cwd,
  agentDir,
  appendSystemPromptOverride: (base) => [
    ...base,
    "## Additional Instructions\n- Always be concise",
  ],
});
```

### Inject or replace prompt templates

```typescript
import {
  createAgentSession,
  createSyntheticSourceInfo,
  DefaultResourceLoader,
  getAgentDir,
  type PromptTemplate,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

const deployTemplate: PromptTemplate = {
  name: "deploy",
  description: "Deploy the application",
  filePath: "/virtual/prompts/deploy.md",
  sourceInfo: createSyntheticSourceInfo("/virtual/prompts/deploy.md", { source: "sdk" }),
  content: `# Deploy Instructions\n\n1. Build\n2. Test\n3. Deploy`,
};

const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  promptsOverride: (current) => ({
    prompts: [...current.prompts, deployTemplate],
    diagnostics: current.diagnostics,
  }),
});
await loader.reload();

const discovered = loader.getPrompts().prompts;
// session.prompt("/deploy") expands to deployTemplate.content
const { session } = await createAgentSession({
  resourceLoader: loader,
  sessionManager: SessionManager.inMemory(),
});
session.dispose();
```

Full isolation (no discovered prompts):

```typescript
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
// optionally also:
noPromptTemplates: true,
```

### Loader getters

| Method | Returns |
|--------|---------|
| `getPrompts()` | `{ prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }` |
| `getSystemPrompt()` | Base custom system prompt string or `undefined` |
| `getAppendSystemPrompt()` | `string[]` of append sections |
| `getSystemPromptSource()` / `getAppendSystemPromptSources()` | File provenance when loaded from disk |

### Prompt options

```typescript
await session.prompt("/review src/index.ts"); // expands template

await session.prompt("/review src/index.ts", {
  expandPromptTemplates: false, // send literal slash text
});
```

## Packages and settings

Share templates by shipping a pi package:

```json
{
  "name": "my-package",
  "keywords": ["pi-package"],
  "pi": {
    "prompts": ["./prompts"]
  }
}
```

Without a `pi` manifest, packages auto-load `prompts/*.md`. Settings:

```json
{
  "prompts": ["./prompts", "~/shared/review.md"],
  "packages": [
    {
      "source": "npm:my-package",
      "prompts": ["prompts/review.md"]
    }
  ]
}
```

Paths in `~/.pi/agent/settings.json` resolve relative to `~/.pi/agent`; paths in `.pi/settings.json` resolve relative to `.pi`.

## Runtime expansion behavior

Verified session behavior (`test/suite/agent-session-prompt.test.ts`):

- `session.prompt("/review src/index.ts")` with a template named `review` and body `Review this code: $1` yields user text `Review this code: src/index.ts`.
- `/skill:name` expands skill markup before the provider sees the message.
- Extension `/commands` run without consuming a provider response.
- Images and multi-tool turns are independent of template expansion; expansion only rewrites the text string before queue/send.

RPC mode exposes templates in `get_commands` with `source: "prompt"`.

## Verification

| Check | Signal |
|-------|--------|
| Template discovered | Startup resource listing / `loader.getPrompts().prompts` contains `name` |
| Expansion works | `session.prompt("/name arg")` stores substituted body as the user message |
| Collision | `getPrompts().diagnostics` includes `type: "collision"` for the losing path |
| No discovery | `--no-prompt-templates` yields empty prompts unless CLI/SDK paths override |
| System replace | Model system message starts with custom text; context/skills may still append |
| Append cleared | `appendSystemPromptOverride: () => []` avoids `APPEND_SYSTEM.md` |

Unit coverage lives in `test/prompt-templates.test.ts` (parse, substitute, defaults, slices, argument-hint, expand) and `test/system-prompt.test.ts` (tools, guidelines, docs path lines). Run those suites when changing prompt behavior; package policy is to avoid full e2e unless requested.

## Constraints and failure modes

| Case | Behavior |
|------|----------|
| Unknown `/name` | Left as literal text (not an error) |
| Unreadable template file | Skipped at load (`null`) |
| Missing CLI template path | Diagnostic: path does not exist; other templates still load |
| Compaction in progress | `prompt()` throws until compaction finishes |
| Streaming without `streamingBehavior` | Throws; after expansion, use `steer` or `followUp` |
| Untrusted project | Project `.pi/prompts`, project `SYSTEM.md` / `APPEND_SYSTEM.md`, and project settings paths are not loaded |
| Provider neutrality | Templates and system prompts are local files/strings; no fixed model provider is required |

## Related pages

<CardGroup>
  <Card title="Skills" href="/skills">
    `/skill:name` expansion, frontmatter rules, and collision precedence for skills.
  </Card>
  <Card title="Context files" href="/context-files">
    Project context injection into the system prompt after custom or default bases.
  </Card>
  <Card title="SDK" href="/sdk">
    `createAgentSession`, `DefaultResourceLoader`, and full control hooks.
  </Card>
  <Card title="SDK examples" href="/sdk-examples">
    Copy-paste recipes including custom prompts and prompt templates.
  </Card>
  <Card title="Themes and packages" href="/themes-and-packages">
    Packaging prompts with extensions, skills, and themes.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt queue, concurrent behavior, and turn lifecycle around `session.prompt()`.
  </Card>
</CardGroup>
