diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts
index 231a4110..ad4da188 100644
--- a/packages/agent/src/types.ts
+++ b/packages/agent/src/types.ts
@@ -4,6 +4,7 @@ import type {
AssistantMessageEvent,
Message,
Model,
+ ToolResultMessage,
UserMessage,
} from "@mariozechner/pi-ai";
@@ -87,7 +88,7 @@ export type AgentEvent =
| { type: "agent_end"; messages: AppMessage[] }
// Turn lifecycle - a turn is one assistant response + any tool calls/results
| { type: "turn_start" }
- | { type: "turn_end"; message: AppMessage; toolResults: AppMessage[] }
+ | { type: "turn_end"; message: AppMessage; toolResults: ToolResultMessage[] }
// Message lifecycle - emitted for user, assistant, and toolResult messages
| { type: "message_start"; message: AppMessage }
// Only emitted for assistant messages during streaming
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 2f56e413..e10c392e 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,6 +2,16 @@
## [Unreleased]
+### Changed
+
+- Improved system prompt documentation section with clearer pointers to specific doc files for custom models, themes, skills, hooks, custom tools, and RPC.
+
+- Cleaned up documentation: `theme.md` (added missing color tokens), `skills.md` (rewrote with better framing and examples), `hooks.md` (fixed timeout/error handling docs, added examples).
+
+### Breaking Changes
+
+- **Hooks**: `turn_end` event's `toolResults` type changed from `AppMessage[]` to `ToolResultMessage[]`. If you have hooks that handle `turn_end` events and explicitly type the results, update your type annotations.
+
## [0.23.2] - 2025-12-17
### Fixed
diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md
index 822e8207..72fd2aa7 100644
--- a/packages/coding-agent/README.md
+++ b/packages/coding-agent/README.md
@@ -488,66 +488,41 @@ Usage: `/component Button "onClick handler" "disabled support"`
### Skills
-Skills are instruction files loaded on-demand when tasks match their descriptions. Compatible with Claude Code and Codex CLI skill formats.
+Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. Skills are loaded when the agent decides a task matches the description, or when you explicitly ask to use one.
**Skill locations:**
-- Pi user: `~/.pi/agent/skills/**/SKILL.md` (recursive, colon-separated names)
-- Pi project: `.pi/skills/**/SKILL.md` (recursive, colon-separated names)
-- Claude Code user: `~/.claude/skills/*/SKILL.md` (one level)
-- Claude Code project: `.claude/skills/*/SKILL.md` (one level)
+- Pi user: `~/.pi/agent/skills/**/SKILL.md` (recursive)
+- Pi project: `.pi/skills/**/SKILL.md` (recursive)
+- Claude Code: `~/.claude/skills/*/SKILL.md` and `.claude/skills/*/SKILL.md`
- Codex CLI: `~/.codex/skills/**/SKILL.md` (recursive)
-Later locations win on name collisions (Pi skills override Claude/Codex).
-
-Pi skills in subdirectories use colon-separated names: `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate`
-
**Format:**
```markdown
---
-description: Extract text and tables from PDF files
+description: Web search via Brave Search API. Use for documentation, facts, or web content.
---
-# PDF Processing
+# Brave Search
-Use `pdftotext` for plain text extraction.
-For tables, use `tabula-py`.
+## Setup
+\`\`\`bash
+cd {baseDir} && npm install
+\`\`\`
-Helper scripts: {baseDir}/scripts/
+## Usage
+\`\`\`bash
+{baseDir}/search.js "query" # Basic search
+{baseDir}/search.js "query" --content # Include page content
+\`\`\`
```
-- `description`: Required. Shown in system prompt for agent to decide when to load.
-- `name`: Optional. Overrides directory name.
-- `{baseDir}`: Placeholder for the skill's directory. Agent substitutes it when following instructions.
+- `description`: Required. Determines when the skill is loaded.
+- `{baseDir}`: Placeholder for the skill's directory.
-**How it works:**
+**Disable skills:** `pi --no-skills` or set `skills.enabled: false` in settings.
-Skills are listed in the system prompt with descriptions:
-
-```
-
-- pdf-extract: Extract text and tables from PDF files
- File: ~/.pi/agent/skills/pdf-extract/SKILL.md
- Base directory: ~/.pi/agent/skills/pdf-extract
-
-```
-
-Agent uses `read` tool to load full instructions when needed.
-
-**Disable skills:**
-
-CLI: `pi --no-skills`
-
-Settings (`~/.pi/agent/settings.json`):
-```json
-{
- "skills": {
- "enabled": false
- }
-}
-```
-
-See [docs/skills.md](docs/skills.md) for details.
+See [docs/skills.md](docs/skills.md) for details, examples, and links to skill repositories.
### Hooks
diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md
index c08f1bd3..768f5f15 100644
--- a/packages/coding-agent/docs/hooks.md
+++ b/packages/coding-agent/docs/hooks.md
@@ -2,6 +2,15 @@
Hooks are TypeScript modules that extend the coding agent's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user for input, modify results, and more.
+**Example use cases:**
+- Block dangerous commands (permission gates for `rm -rf`, `sudo`, etc.)
+- Checkpoint code state (git stash at each turn, restore on `/branch`)
+- Protect paths (block writes to `.env`, `node_modules/`, etc.)
+- Modify tool output (filter or transform results before the LLM sees them)
+- Inject messages from external sources (file watchers, webhooks, CI systems)
+
+See [examples/hooks/](../examples/hooks/) for working implementations.
+
## Hook Locations
Hooks are automatically discovered from two locations:
@@ -33,7 +42,7 @@ You can also add explicit hook paths in `~/.pi/agent/settings.json`:
```
- `hooks`: Additional hook file paths (supports `~` expansion)
-- `hookTimeout`: Timeout in milliseconds for non-interactive hook operations (default: 30000)
+- `hookTimeout`: Timeout in milliseconds for hook operations (default: 30000). Does not apply to `tool_call` events, which have no timeout since they may prompt the user.
## Writing a Hook
@@ -51,23 +60,17 @@ export default function (pi: HookAPI) {
### Setup
-Create a hooks directory and initialize it:
+Create a hooks directory:
```bash
# Global hooks
mkdir -p ~/.pi/agent/hooks
-cd ~/.pi/agent/hooks
-npm init -y
-npm install @mariozechner/pi-coding-agent
# Or project-local hooks
mkdir -p .pi/hooks
-cd .pi/hooks
-npm init -y
-npm install @mariozechner/pi-coding-agent
```
-Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
+Then create `.ts` files directly in these directories. Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation. The import from `@mariozechner/pi-coding-agent/hooks` resolves to the globally installed package automatically.
## Events
@@ -121,7 +124,7 @@ Fired on startup and when session changes.
```typescript
pi.on("session", async (event, ctx) => {
// event.entries: SessionEntry[] - all session entries
- // event.sessionFile: string | null - current session file
+ // event.sessionFile: string | null - current session file (null with --no-session)
// event.previousSessionFile: string | null - previous session file
// event.reason: "start" | "switch" | "clear"
});
@@ -171,24 +174,24 @@ pi.on("turn_start", async (event, ctx) => {
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex: number
// event.message: AppMessage - assistant's response
- // event.toolResults: AppMessage[]
+ // event.toolResults: ToolResultMessage[] - tool results from this turn
});
```
### tool_call
-Fired before tool executes. **Can block.**
+Fired before tool executes. **Can block.** No timeout (user prompts can take any time).
```typescript
pi.on("tool_call", async (event, ctx) => {
- // event.toolName: "bash" | "read" | "write" | "edit" | "ls" | "find" | "grep"
+ // event.toolName: string (built-in or custom tool name)
// event.toolCallId: string
// event.input: Record
return { block: true, reason: "..." }; // or undefined to allow
});
```
-Tool inputs:
+Built-in tool inputs:
- `bash`: `{ command, timeout? }`
- `read`: `{ path, offset?, limit? }`
- `write`: `{ path, content }`
@@ -197,6 +200,8 @@ Tool inputs:
- `find`: `{ pattern, path?, limit? }`
- `grep`: `{ pattern, path?, glob?, ignoreCase?, literal?, context?, limit? }`
+Custom tools are also intercepted with their own names and input schemas.
+
### tool_result
Fired after tool executes. **Can modify result.**
@@ -274,7 +279,7 @@ console.log(`Working in: ${ctx.cwd}`);
### ctx.sessionFile
-Path to the session file, or `null` if running with `--no-session`.
+Path to the current session file, or `null` when running with `--no-session` (ephemeral mode).
```typescript
if (ctx.sessionFile) {
@@ -490,7 +495,8 @@ In print mode, `select()` returns `null`, `confirm()` returns `false`, and `inpu
## Error Handling
- If a hook throws an error, it's logged and the agent continues
-- If a `tool_call` hook errors or times out, the tool is **blocked** (fail-safe)
+- If a `tool_call` hook throws an error, the tool is **blocked** (fail-safe)
+- Other events have a timeout (default 30s); timeout errors are logged but don't block
- Hook errors are displayed in the UI with the hook path and error message
## Debugging
@@ -563,9 +569,9 @@ class HookRunner {
Key behaviors:
- `emit()` has a timeout (default 30s) for safety
-- `emitToolCall()` has **no timeout** (user prompts can take any amount of time)
-- Errors in `emit()` are caught and reported via `onError()`
-- Errors in `emitToolCall()` propagate (causing tool to be blocked)
+- `emitToolCall()` has **no timeout** (user prompts can take any time)
+- Errors in `emit()` are caught, logged via `onError()`, and execution continues
+- Errors in `emitToolCall()` propagate, causing the tool to be blocked (fail-safe)
## Event Flow
diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md
index 210886eb..745906f7 100644
--- a/packages/coding-agent/docs/skills.md
+++ b/packages/coding-agent/docs/skills.md
@@ -1,72 +1,190 @@
# Skills
-Skills are instruction files that the agent loads on-demand for specific tasks.
+Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks.
-## Skill Locations
+**Example use cases:**
+- Web search and content extraction (Brave Search API)
+- Browser automation via Chrome DevTools Protocol
+- Google Calendar, Gmail, Drive integration
+- PDF/DOCX processing and creation
+- Speech-to-text transcription
+- YouTube transcript extraction
-Skills are discovered from these locations (in order of priority, later wins on name collision):
+See [Skill Repositories](#skill-repositories) for ready-to-use skills.
-1. `~/.codex/skills/**/SKILL.md` (Codex CLI user skills, recursive)
-2. `~/.claude/skills/*/SKILL.md` (Claude Code user skills)
-3. `/.claude/skills/*/SKILL.md` (Claude Code project skills)
-4. `~/.pi/agent/skills/**/SKILL.md` (Pi user skills, recursive)
-5. `/.pi/skills/**/SKILL.md` (Pi project skills, recursive)
+## When to Use Skills
-Skill names and descriptions are listed in the system prompt. When a task matches a skill's description, the agent uses the `read` tool to load it.
+| Need | Solution |
+|------|----------|
+| Always-needed context (conventions, commands) | AGENTS.md |
+| User triggers a specific prompt template | Slash command |
+| Additional tool directly callable by the LLM (like read/write/edit/bash) | Custom tool |
+| On-demand capability package (workflows, scripts, setup) | Skill |
-## Creating Skills
+Skills are loaded when:
+- The agent decides the task matches a skill's description
+- The user explicitly asks to use a skill (e.g., "use the pdf skill to extract tables")
-A skill is a markdown file with YAML frontmatter containing a `description` field:
+**Good skill examples:**
+- Browser automation with helper scripts and CDP workflow
+- Google Calendar CLI with setup instructions and usage patterns
+- PDF processing with multiple tools and extraction patterns
+- Speech-to-text transcription with API setup
+
+**Not a good fit for skills:**
+- "Always use TypeScript strict mode" → put in AGENTS.md
+- "Review my code" → make a slash command
+- Need user confirmation dialogs or custom TUI rendering → make a custom tool
+
+## Skill Structure
+
+A skill is a directory with a `SKILL.md` file. Everything else is freeform. Example structure:
+
+```
+my-skill/
+├── SKILL.md # Required: frontmatter + instructions
+├── scripts/ # Helper scripts (bash, python, node)
+│ └── process.sh
+├── references/ # Detailed docs loaded on-demand
+│ └── api-reference.md
+└── assets/ # Templates, images, etc.
+ └── template.json
+```
+
+### SKILL.md Format
```markdown
---
-description: Extract text and tables from PDF files
+name: my-skill
+description: What this skill does and when to use it. Be specific.
---
-# PDF Processing Instructions
+# My Skill
-1. Use `pdftotext` to extract plain text
-2. For tables, use `tabula-py` or similar
-3. Always verify extraction quality
+## Setup
-Scripts are in: {baseDir}/scripts/
+Run once before first use:
+\`\`\`bash
+cd {baseDir}
+npm install
+\`\`\`
+
+## Usage
+
+\`\`\`bash
+{baseDir}/scripts/process.sh
+\`\`\`
+
+## Workflow
+
+1. First step
+2. Second step
+3. Third step
```
### Frontmatter Fields
| Field | Required | Description |
|-------|----------|-------------|
-| `description` | Yes | Short description for skill selection |
-| `name` | No | Override skill name (defaults to filename or directory name) |
+| `description` | Yes | What the skill does and when to use it |
+| `name` | No | Override skill name (defaults to directory name) |
-The parser only supports single-line `key: value` syntax. Multiline YAML blocks are not supported.
+The `description` is critical. It's shown in the system prompt and determines when the agent loads the skill. Be specific about both what it does and when to use it.
-### Variables
+### The `{baseDir}` Placeholder
-Use `{baseDir}` as a placeholder for the skill's directory. The agent is told each skill's base directory and will substitute it when following the instructions.
+Use `{baseDir}` to reference files in the skill's directory. The agent sees each skill's base directory and substitutes it when following instructions:
-### Subdirectories
+```markdown
+Helper scripts: {baseDir}/scripts/
+Config template: {baseDir}/assets/config.json
+```
-Pi and Codex skills in subdirectories use colon-separated names:
+## Skill Locations
+
+Skills are discovered from these locations (later wins on name collision):
+
+1. `~/.codex/skills/**/SKILL.md` (Codex CLI, recursive)
+2. `~/.claude/skills/*/SKILL.md` (Claude Code user, one level)
+3. `/.claude/skills/*/SKILL.md` (Claude Code project, one level)
+4. `~/.pi/agent/skills/**/SKILL.md` (Pi user, recursive)
+5. `/.pi/skills/**/SKILL.md` (Pi project, recursive)
+
+### Subdirectory Naming
+
+Pi skills in subdirectories use colon-separated names:
- `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate`
- `/.pi/skills/aws/s3/upload/SKILL.md` → `aws:s3:upload`
-## Claude Code Compatibility
+## How Skills Work
-Pi reads Claude Code skills from `~/.claude/skills/*/SKILL.md`. The `allowed-tools` and `model` frontmatter fields are ignored since Pi cannot enforce them.
+1. At startup, pi scans skill locations and extracts names + descriptions
+2. The system prompt includes a list of available skills with their descriptions
+3. When a task matches, the agent uses `read` to load the full SKILL.md
+4. The agent follows the instructions, using `{baseDir}` to reference scripts/assets
-## Codex CLI Compatibility
+This is progressive disclosure: only descriptions are always in context, full instructions load on-demand.
-Pi reads Codex CLI skills from `~/.codex/skills/`. Unlike Claude Code skills (one level deep), Codex skills are scanned recursively, matching Codex CLI's behavior. Hidden files/directories (starting with `.`) and symlinks are skipped.
+## Example: Web Search Skill
+
+```
+brave-search/
+├── SKILL.md
+├── search.js
+└── content.js
+```
+
+**SKILL.md:**
+```markdown
+---
+name: brave-search
+description: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.
+---
+
+# Brave Search
+
+## Setup
+
+\`\`\`bash
+cd {baseDir}
+npm install
+\`\`\`
+
+## Search
+
+\`\`\`bash
+{baseDir}/search.js "query" # Basic search
+{baseDir}/search.js "query" --content # Include page content
+\`\`\`
+
+## Extract Page Content
+
+\`\`\`bash
+{baseDir}/content.js https://example.com
+\`\`\`
+```
+
+## Compatibility
+
+**Claude Code**: Pi reads skills from `~/.claude/skills/*/SKILL.md`. The `allowed-tools` and `model` frontmatter fields are ignored.
+
+**Codex CLI**: Pi reads skills from `~/.codex/skills/` recursively. Hidden files/directories and symlinks are skipped.
+
+## Skill Repositories
+
+For inspiration and ready-to-use skills:
+
+- [Anthropic Skills](https://github.com/anthropics/skills) - Official skills for document processing (docx, pdf, pptx, xlsx), web development, and more
+- [Pi Skills](https://github.com/badlogic/pi-skills) - Skills for web search, browser automation, Google APIs, transcription
## Disabling Skills
-CLI flag:
+CLI:
```bash
pi --no-skills
```
-Or in `~/.pi/agent/settings.json`:
+Settings (`~/.pi/agent/settings.json`):
```json
{
"skills": {
@@ -74,25 +192,3 @@ Or in `~/.pi/agent/settings.json`:
}
}
```
-
-## Example
-
-```markdown
----
-description: Perform code review with security and performance analysis
----
-
-# Code Review
-
-Analyze:
-
-## Security
-- Input validation
-- SQL injection
-- XSS vulnerabilities
-
-## Performance
-- Algorithm complexity
-- Memory usage
-- Query efficiency
-```
diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md
index 2bf01781..06f674cb 100644
--- a/packages/coding-agent/docs/theme.md
+++ b/packages/coding-agent/docs/theme.md
@@ -74,7 +74,7 @@ Future-proofing for syntax highlighting support:
| `syntaxOperator` | Operators (`+`, `-`, etc) |
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
-### Thinking Level Borders (5 colors)
+### Thinking Level Borders (6 colors)
Editor border colors that indicate the current thinking/reasoning level:
@@ -84,11 +84,18 @@ Editor border colors that indicate the current thinking/reasoning level:
| `thinkingMinimal` | Border for minimal thinking |
| `thinkingLow` | Border for low thinking |
| `thinkingMedium` | Border for medium thinking |
-| `thinkingHigh` | Border for high thinking (most prominent) |
+| `thinkingHigh` | Border for high thinking |
+| `thinkingXhigh` | Border for xhigh thinking (most prominent, OpenAI codex-max only) |
-These create a visual hierarchy: off → minimal → low → medium → high
+These create a visual hierarchy: off → minimal → low → medium → high → xhigh
-**Total: 44 color tokens** (all required)
+### Bash Mode (1 color)
+
+| Token | Purpose |
+|-------|---------|
+| `bashMode` | Editor border color when in bash mode (! prefix) |
+
+**Total: 46 color tokens** (all required)
## Theme Format
@@ -420,8 +427,8 @@ class Theme {
// Text attributes (preserve current colors)
bold(text: string): string
- dim(text: string): string
italic(text: string): string
+ underline(text: string): string
}
```
@@ -453,6 +460,7 @@ TUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@mariozechn
export interface MarkdownTheme {
heading: (text: string) => string;
link: (text: string) => string;
+ linkUrl: (text: string) => string;
code: (text: string) => string;
codeBlock: (text: string) => string;
codeBlockBorder: (text: string) => string;
@@ -460,21 +468,10 @@ export interface MarkdownTheme {
quoteBorder: (text: string) => string;
hr: (text: string) => string;
listBullet: (text: string) => string;
-}
-
-export class Markdown {
- constructor(
- text: string,
- paddingX: number,
- paddingY: number,
- defaultTextStyle?: DefaultTextStyle,
- theme?: MarkdownTheme // Optional theme functions
- )
-
- // Usage in component
- renderHeading(text: string) {
- return this.theme.heading(text); // Applies color
- }
+ bold: (text: string) => string;
+ italic: (text: string) => string;
+ strikethrough: (text: string) => string;
+ underline: (text: string) => string;
}
```
@@ -490,6 +487,7 @@ function getMarkdownTheme(): MarkdownTheme {
return {
heading: (text) => theme.fg('mdHeading', text),
link: (text) => theme.fg('mdLink', text),
+ linkUrl: (text) => theme.fg('mdLinkUrl', text),
code: (text) => theme.fg('mdCode', text),
codeBlock: (text) => theme.fg('mdCodeBlock', text),
codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text),
@@ -497,6 +495,10 @@ function getMarkdownTheme(): MarkdownTheme {
quoteBorder: (text) => theme.fg('mdQuoteBorder', text),
hr: (text) => theme.fg('mdHr', text),
listBullet: (text) => theme.fg('mdListBullet', text),
+ bold: (text) => theme.bold(text),
+ italic: (text) => theme.italic(text),
+ underline: (text) => theme.underline(text),
+ strikethrough: (text) => chalk.strikethrough(text),
};
}
@@ -530,7 +532,7 @@ theme.bg('toolSuccessBg', output)
// Combine styles
theme.bold(theme.fg('accent', 'Title'))
-theme.dim(theme.fg('muted', 'metadata'))
+theme.italic(theme.fg('muted', 'metadata'))
// Nested foreground + background
const userMsg = theme.bg('userMessageBg',
diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts
index 546897fc..b47e1857 100644
--- a/packages/coding-agent/src/core/hooks/types.ts
+++ b/packages/coding-agent/src/core/hooks/types.ts
@@ -6,6 +6,7 @@
*/
import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core";
+import type { ToolResultMessage } from "@mariozechner/pi-ai";
import type { SessionEntry } from "../session-manager.js";
// ============================================================================
@@ -121,7 +122,7 @@ export interface TurnEndEvent {
type: "turn_end";
turnIndex: number;
message: AppMessage;
- toolResults: AppMessage[];
+ toolResults: ToolResultMessage[];
}
/**
diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts
index eaaf22b2..1b3d6ac0 100644
--- a/packages/coding-agent/src/core/system-prompt.ts
+++ b/packages/coding-agent/src/core/system-prompt.ts
@@ -235,9 +235,9 @@ Guidelines:
${guidelines}
Documentation:
-- Your own documentation (including custom model setup and theme creation) is at: ${readmePath}
-- Additional documentation (hooks, themes, RPC, etc.) is in: ${docsPath}
-- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, create a custom theme, or write a hook.`;
+- Main documentation: ${readmePath}
+- Additional docs: ${docsPath}
+- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)`;
if (appendSection) {
prompt += appendSection;