pi-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Pure utility functions for plan mode.
3
+ * Extracted for testability.
4
+ */
5
+
6
+ // Destructive commands blocked in plan mode
7
+ const DESTRUCTIVE_PATTERNS = [
8
+ /\brm\b/i,
9
+ /\brmdir\b/i,
10
+ /\bmv\b/i,
11
+ /\bcp\b/i,
12
+ /\bmkdir\b/i,
13
+ /\btouch\b/i,
14
+ /\bchmod\b/i,
15
+ /\bchown\b/i,
16
+ /\bchgrp\b/i,
17
+ /\bln\b/i,
18
+ /\btee\b/i,
19
+ /\btruncate\b/i,
20
+ /\bdd\b/i,
21
+ /\bshred\b/i,
22
+ /(^|[^<])>(?!>)/,
23
+ />>/,
24
+ /\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
25
+ /\byarn\s+(add|remove|install|publish)/i,
26
+ /\bpnpm\s+(add|remove|install|publish)/i,
27
+ /\bpip\s+(install|uninstall)/i,
28
+ /\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
29
+ /\bbrew\s+(install|uninstall|upgrade)/i,
30
+ /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
31
+ /\bsudo\b/i,
32
+ /\bsu\b/i,
33
+ /\bkill\b/i,
34
+ /\bpkill\b/i,
35
+ /\bkillall\b/i,
36
+ /\breboot\b/i,
37
+ /\bshutdown\b/i,
38
+ /\bsystemctl\s+(start|stop|restart|enable|disable)/i,
39
+ /\bservice\s+\S+\s+(start|stop|restart)/i,
40
+ /\b(vim?|nano|emacs|code|subl)\b/i,
41
+ ]
42
+
43
+ // Safe read-only commands allowed in plan mode
44
+ const SAFE_PATTERNS = [
45
+ /^\s*cat\b/,
46
+ /^\s*head\b/,
47
+ /^\s*tail\b/,
48
+ /^\s*less\b/,
49
+ /^\s*more\b/,
50
+ /^\s*grep\b/,
51
+ /^\s*find\b/,
52
+ /^\s*ls\b/,
53
+ /^\s*pwd\b/,
54
+ /^\s*echo\b/,
55
+ /^\s*printf\b/,
56
+ /^\s*wc\b/,
57
+ /^\s*sort\b/,
58
+ /^\s*uniq\b/,
59
+ /^\s*diff\b/,
60
+ /^\s*file\b/,
61
+ /^\s*stat\b/,
62
+ /^\s*du\b/,
63
+ /^\s*df\b/,
64
+ /^\s*tree\b/,
65
+ /^\s*which\b/,
66
+ /^\s*whereis\b/,
67
+ /^\s*type\b/,
68
+ /^\s*env\b/,
69
+ /^\s*printenv\b/,
70
+ /^\s*uname\b/,
71
+ /^\s*whoami\b/,
72
+ /^\s*id\b/,
73
+ /^\s*date\b/,
74
+ /^\s*cal\b/,
75
+ /^\s*uptime\b/,
76
+ /^\s*ps\b/,
77
+ /^\s*top\b/,
78
+ /^\s*htop\b/,
79
+ /^\s*free\b/,
80
+ /^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
81
+ /^\s*git\s+ls-/i,
82
+ /^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
83
+ /^\s*yarn\s+(list|info|why|audit)/i,
84
+ /^\s*node\s+--version/i,
85
+ /^\s*python\s+--version/i,
86
+ /^\s*curl\s/i,
87
+ /^\s*wget\s+-O\s*-/i,
88
+ /^\s*jq\b/,
89
+ /^\s*sed\s+-n/i,
90
+ /^\s*awk\b/,
91
+ /^\s*rg\b/,
92
+ /^\s*fd\b/,
93
+ /^\s*bat\b/,
94
+ /^\s*eza\b/,
95
+ ]
96
+
97
+ export function isSafeCommand(command: string): boolean {
98
+ const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command))
99
+ const isSafe = SAFE_PATTERNS.some((p) => p.test(command))
100
+ return !isDestructive && isSafe
101
+ }
102
+
103
+ export interface TodoItem {
104
+ step: number
105
+ text: string
106
+ completed: boolean
107
+ }
108
+
109
+ export function cleanStepText(text: string): string {
110
+ let cleaned = text
111
+ .replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1') // Remove bold/italic
112
+ .replace(/`([^`]+)`/g, '$1') // Remove code
113
+ .replace(/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i, '')
114
+ .replace(/\s+/g, ' ')
115
+ .trim()
116
+
117
+ if (cleaned.length > 0) {
118
+ cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1)
119
+ }
120
+ if (cleaned.length > 50) {
121
+ cleaned = `${cleaned.slice(0, 47)}...`
122
+ }
123
+ return cleaned
124
+ }
125
+
126
+ export function extractTodoItems(message: string): TodoItem[] {
127
+ const items: TodoItem[] = []
128
+ const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i)
129
+ if (!headerMatch) return items
130
+
131
+ const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length)
132
+ const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm
133
+
134
+ for (const match of planSection.matchAll(numberedPattern)) {
135
+ const text = match[2]
136
+ .trim()
137
+ .replace(/\*{1,2}$/, '')
138
+ .trim()
139
+ if (text.length > 5 && !text.startsWith('`') && !text.startsWith('/') && !text.startsWith('-')) {
140
+ const cleaned = cleanStepText(text)
141
+ if (cleaned.length > 3) {
142
+ items.push({ step: items.length + 1, text: cleaned, completed: false })
143
+ }
144
+ }
145
+ }
146
+ return items
147
+ }
148
+
149
+ export function extractDoneSteps(message: string): number[] {
150
+ const steps: number[] = []
151
+ for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
152
+ const step = Number(match[1])
153
+ if (Number.isFinite(step)) steps.push(step)
154
+ }
155
+ return steps
156
+ }
157
+
158
+ export function markCompletedSteps(text: string, items: TodoItem[]): number {
159
+ const doneSteps = extractDoneSteps(text)
160
+ for (const step of doneSteps) {
161
+ const item = items.find((t) => t.step === step)
162
+ if (item) item.completed = true
163
+ }
164
+ return doneSteps.length
165
+ }
166
+
167
+ /**
168
+ * Parse an explicitly submitted plan (from the plan_mode_complete tool) into
169
+ * todo items. Unlike extractTodoItems, the Plan: header is optional because
170
+ * the tool input is already known to be the plan itself.
171
+ */
172
+ export function planToTodos(plan: string): TodoItem[] {
173
+ const withHeader = /\*{0,2}Plan:\*{0,2}\s*\n/i.test(plan) ? plan : `Plan:\n${plan}`
174
+ return extractTodoItems(withHeader)
175
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Question Tool - Single question with options
3
+ * Full custom UI: options list + inline editor for "Type something..."
4
+ * Escape in editor returns to options, Escape in options cancels
5
+ */
6
+
7
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
8
+ import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from '@earendil-works/pi-tui'
9
+ import { Type } from 'typebox'
10
+
11
+ interface OptionWithDesc {
12
+ label: string
13
+ description?: string
14
+ }
15
+
16
+ type DisplayOption = OptionWithDesc & { isOther?: boolean }
17
+
18
+ interface QuestionDetails {
19
+ question: string
20
+ options: string[]
21
+ answer: string | null
22
+ wasCustom?: boolean
23
+ }
24
+
25
+ // Options with labels and optional descriptions
26
+ const OptionSchema = Type.Object({
27
+ label: Type.String({ description: 'Display label for the option' }),
28
+ description: Type.Optional(Type.String({ description: 'Optional description shown below label' })),
29
+ })
30
+
31
+ const QuestionParams = Type.Object({
32
+ question: Type.String({ description: 'The question to ask the user' }),
33
+ options: Type.Array(OptionSchema, { description: 'Options for the user to choose from' }),
34
+ })
35
+
36
+ export default function question(pi: ExtensionAPI) {
37
+ pi.registerTool({
38
+ name: 'question',
39
+ label: 'Question',
40
+ description: 'Ask the user a question and let them pick from options. Use when you need user input to proceed.',
41
+ parameters: QuestionParams,
42
+
43
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
44
+ if (!ctx.hasUI) {
45
+ return {
46
+ content: [{ type: 'text', text: 'Error: UI not available (running in non-interactive mode)' }],
47
+ details: {
48
+ question: params.question,
49
+ options: params.options.map((o) => o.label),
50
+ answer: null,
51
+ } as QuestionDetails,
52
+ }
53
+ }
54
+
55
+ if (params.options.length === 0) {
56
+ return {
57
+ content: [{ type: 'text', text: 'Error: No options provided' }],
58
+ details: { question: params.question, options: [], answer: null } as QuestionDetails,
59
+ }
60
+ }
61
+
62
+ const allOptions: DisplayOption[] = [...params.options, { label: 'Type something.', isOther: true }]
63
+
64
+ const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>((tui, theme, _kb, done) => {
65
+ let optionIndex = 0
66
+ let editMode = false
67
+ let cachedLines: string[] | undefined
68
+
69
+ const editorTheme: EditorTheme = {
70
+ borderColor: (s) => theme.fg('accent', s),
71
+ selectList: {
72
+ selectedPrefix: (t) => theme.fg('accent', t),
73
+ selectedText: (t) => theme.fg('accent', t),
74
+ description: (t) => theme.fg('muted', t),
75
+ scrollInfo: (t) => theme.fg('dim', t),
76
+ noMatch: (t) => theme.fg('warning', t),
77
+ },
78
+ }
79
+ const editor = new Editor(tui, editorTheme)
80
+
81
+ editor.onSubmit = (value) => {
82
+ const trimmed = value.trim()
83
+ if (trimmed) {
84
+ done({ answer: trimmed, wasCustom: true })
85
+ } else {
86
+ editMode = false
87
+ editor.setText('')
88
+ refresh()
89
+ }
90
+ }
91
+
92
+ function refresh() {
93
+ cachedLines = undefined
94
+ tui.requestRender()
95
+ }
96
+
97
+ function handleInput(data: string) {
98
+ if (editMode) {
99
+ if (matchesKey(data, Key.escape)) {
100
+ editMode = false
101
+ editor.setText('')
102
+ refresh()
103
+ return
104
+ }
105
+ editor.handleInput(data)
106
+ refresh()
107
+ return
108
+ }
109
+
110
+ if (matchesKey(data, Key.up)) {
111
+ optionIndex = Math.max(0, optionIndex - 1)
112
+ refresh()
113
+ return
114
+ }
115
+ if (matchesKey(data, Key.down)) {
116
+ optionIndex = Math.min(allOptions.length - 1, optionIndex + 1)
117
+ refresh()
118
+ return
119
+ }
120
+
121
+ if (matchesKey(data, Key.enter)) {
122
+ const selected = allOptions[optionIndex]
123
+ if (selected.isOther) {
124
+ editMode = true
125
+ refresh()
126
+ } else {
127
+ done({ answer: selected.label, wasCustom: false, index: optionIndex + 1 })
128
+ }
129
+ return
130
+ }
131
+
132
+ if (matchesKey(data, Key.escape)) {
133
+ done(null)
134
+ }
135
+ }
136
+
137
+ function render(width: number): string[] {
138
+ if (cachedLines) return cachedLines
139
+
140
+ const lines: string[] = []
141
+ const add = (s: string) => lines.push(truncateToWidth(s, width))
142
+
143
+ add(theme.fg('accent', '─'.repeat(width)))
144
+ add(theme.fg('text', ` ${params.question}`))
145
+ lines.push('')
146
+
147
+ for (let i = 0; i < allOptions.length; i++) {
148
+ const opt = allOptions[i]
149
+ const selected = i === optionIndex
150
+ const isOther = opt.isOther === true
151
+ const prefix = selected ? theme.fg('accent', '> ') : ' '
152
+
153
+ if (isOther && editMode) {
154
+ add(prefix + theme.fg('accent', `${i + 1}. ${opt.label} ✎`))
155
+ } else if (selected) {
156
+ add(prefix + theme.fg('accent', `${i + 1}. ${opt.label}`))
157
+ } else {
158
+ add(` ${theme.fg('text', `${i + 1}. ${opt.label}`)}`)
159
+ }
160
+
161
+ // Show description if present
162
+ if (opt.description) {
163
+ add(` ${theme.fg('muted', opt.description)}`)
164
+ }
165
+ }
166
+
167
+ if (editMode) {
168
+ lines.push('')
169
+ add(theme.fg('muted', ' Your answer:'))
170
+ for (const line of editor.render(width - 2)) {
171
+ add(` ${line}`)
172
+ }
173
+ }
174
+
175
+ lines.push('')
176
+ if (editMode) {
177
+ add(theme.fg('dim', ' Enter to submit • Esc to go back'))
178
+ } else {
179
+ add(theme.fg('dim', ' ↑↓ navigate • Enter to select • Esc to cancel'))
180
+ }
181
+ add(theme.fg('accent', '─'.repeat(width)))
182
+
183
+ cachedLines = lines
184
+ return lines
185
+ }
186
+
187
+ return {
188
+ render,
189
+ invalidate: () => {
190
+ cachedLines = undefined
191
+ },
192
+ handleInput,
193
+ }
194
+ })
195
+
196
+ // Build simple options list for details
197
+ const simpleOptions = params.options.map((o) => o.label)
198
+
199
+ if (!result) {
200
+ return {
201
+ content: [{ type: 'text', text: 'User cancelled the selection' }],
202
+ details: { question: params.question, options: simpleOptions, answer: null } as QuestionDetails,
203
+ }
204
+ }
205
+
206
+ if (result.wasCustom) {
207
+ return {
208
+ content: [{ type: 'text', text: `User wrote: ${result.answer}` }],
209
+ details: {
210
+ question: params.question,
211
+ options: simpleOptions,
212
+ answer: result.answer,
213
+ wasCustom: true,
214
+ } as QuestionDetails,
215
+ }
216
+ }
217
+ return {
218
+ content: [{ type: 'text', text: `User selected: ${result.index}. ${result.answer}` }],
219
+ details: {
220
+ question: params.question,
221
+ options: simpleOptions,
222
+ answer: result.answer,
223
+ wasCustom: false,
224
+ } as QuestionDetails,
225
+ }
226
+ },
227
+
228
+ renderCall(args, theme, _context) {
229
+ let text = theme.fg('toolTitle', theme.bold('question ')) + theme.fg('muted', args.question)
230
+ const opts = Array.isArray(args.options) ? args.options : []
231
+ if (opts.length) {
232
+ const labels = opts.map((o: OptionWithDesc) => o.label)
233
+ const numbered = [...labels, 'Type something.'].map((o, i) => `${i + 1}. ${o}`)
234
+ text += `\n${theme.fg('dim', ` Options: ${numbered.join(', ')}`)}`
235
+ }
236
+ return new Text(text, 0, 0)
237
+ },
238
+
239
+ renderResult(result, _options, theme, _context) {
240
+ const details = result.details as QuestionDetails | undefined
241
+ if (!details) {
242
+ const text = result.content[0]
243
+ return new Text(text?.type === 'text' ? text.text : '', 0, 0)
244
+ }
245
+
246
+ if (details.answer === null) {
247
+ return new Text(theme.fg('warning', 'Cancelled'), 0, 0)
248
+ }
249
+
250
+ if (details.wasCustom) {
251
+ return new Text(theme.fg('success', '✓ ') + theme.fg('muted', '(wrote) ') + theme.fg('accent', details.answer), 0, 0)
252
+ }
253
+ const idx = details.options.indexOf(details.answer) + 1
254
+ const display = idx > 0 ? `${idx}. ${details.answer}` : details.answer
255
+ return new Text(theme.fg('success', '✓ ') + theme.fg('accent', display), 0, 0)
256
+ },
257
+ })
258
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Claude Skills Extension
3
+ *
4
+ * Bridges Claude Code's skills into pi. On resources_discover it hands pi the
5
+ * existing `.claude/skills` directories (user then project) as skill paths, so
6
+ * pi discovers Claude Code skills the same way it loads its own `.pi/skills`.
7
+ * pi implements the Agent Skills standard, so `SKILL.md` directories work
8
+ * unchanged and register as `/skill:name`.
9
+ *
10
+ * Docs: https://code.claude.com/docs/en/skills.md, https://agentskills.io
11
+ */
12
+
13
+ import * as fs from 'node:fs'
14
+ import * as os from 'node:os'
15
+ import * as path from 'node:path'
16
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
17
+
18
+ function isDirectory(target: string): boolean {
19
+ try {
20
+ return fs.statSync(target).isDirectory()
21
+ } catch {
22
+ return false
23
+ }
24
+ }
25
+
26
+ /** Existing `.claude/skills` directories, user first then project. */
27
+ export function skillDirs(cwd: string, home: string): string[] {
28
+ const candidates = [path.join(home, '.claude', 'skills'), path.join(cwd, '.claude', 'skills')]
29
+ const dirs: string[] = []
30
+ for (const dir of candidates) {
31
+ if (!dirs.includes(dir) && isDirectory(dir)) dirs.push(dir)
32
+ }
33
+ return dirs
34
+ }
35
+
36
+ export default function skillsExtension(pi: ExtensionAPI) {
37
+ pi.on('resources_discover', async (_event, ctx) => {
38
+ const skillPaths = skillDirs(ctx.cwd, os.homedir())
39
+ return skillPaths.length > 0 ? { skillPaths } : undefined
40
+ })
41
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Status Line Extension
3
+ *
4
+ * Adds a Claude Code style status segment to pi's footer: turn state plus
5
+ * running session cost. Cost is summed from per-message usage on the current
6
+ * branch, so it stays correct across /tree navigation and forks.
7
+ *
8
+ * pi's built-in footer already shows path, branch, context, and model;
9
+ * this extension only adds what is missing instead of replacing the footer.
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
13
+
14
+ interface UsageEntry {
15
+ type: string
16
+ message?: { usage?: { cost?: { total?: number } } }
17
+ }
18
+
19
+ function sessionCost(ctx: ExtensionContext): number {
20
+ let total = 0
21
+ for (const entry of ctx.sessionManager.getBranch() as UsageEntry[]) {
22
+ total += entry.message?.usage?.cost?.total ?? 0
23
+ }
24
+ return total
25
+ }
26
+
27
+ function formatCost(cost: number): string {
28
+ return cost >= 0.01 ? `$${cost.toFixed(2)}` : `$${cost.toFixed(4)}`
29
+ }
30
+
31
+ export default function statusLine(pi: ExtensionAPI) {
32
+ let turnCount = 0
33
+
34
+ function showIdle(ctx: ExtensionContext, symbol: string): void {
35
+ const theme = ctx.ui.theme
36
+ const cost = sessionCost(ctx)
37
+ const costText = cost > 0 ? theme.fg('muted', ` ${formatCost(cost)}`) : ''
38
+ const turnText = turnCount > 0 ? theme.fg('dim', ` turn ${turnCount}`) : theme.fg('dim', ' ready')
39
+ ctx.ui.setStatus('pi-code-status', symbol + turnText + costText)
40
+ }
41
+
42
+ pi.on('session_start', async (_event, ctx) => {
43
+ showIdle(ctx, ctx.ui.theme.fg('dim', '○'))
44
+ })
45
+
46
+ pi.on('turn_start', async (_event, ctx) => {
47
+ turnCount++
48
+ const theme = ctx.ui.theme
49
+ ctx.ui.setStatus('pi-code-status', theme.fg('accent', '●') + theme.fg('dim', ` turn ${turnCount}...`))
50
+ })
51
+
52
+ pi.on('turn_end', async (_event, ctx) => {
53
+ showIdle(ctx, ctx.ui.theme.fg('success', '✓'))
54
+ })
55
+
56
+ pi.on('agent_end', async (_event, ctx) => {
57
+ showIdle(ctx, ctx.ui.theme.fg('success', '✓'))
58
+ })
59
+ }
@@ -0,0 +1,172 @@
1
+ # Subagent Example
2
+
3
+ Delegate tasks to specialized subagents with isolated context windows.
4
+
5
+ ## Features
6
+
7
+ - **Isolated context**: Each subagent runs in a separate `pi` process
8
+ - **Streaming output**: See tool calls and progress as they happen
9
+ - **Parallel streaming**: All parallel tasks stream updates simultaneously
10
+ - **Markdown rendering**: Final output rendered with proper formatting (expanded view)
11
+ - **Usage tracking**: Shows turns, tokens, cost, and context usage per agent
12
+ - **Abort support**: Ctrl+C propagates to kill subagent processes
13
+
14
+ ## Structure
15
+
16
+ ```
17
+ subagent/
18
+ ├── README.md # This file
19
+ ├── index.ts # The extension (entry point)
20
+ ├── agents.ts # Agent discovery logic
21
+ ├── agents/ # Sample agent definitions
22
+ │ ├── scout.md # Fast recon, returns compressed context
23
+ │ ├── planner.md # Creates implementation plans
24
+ │ ├── reviewer.md # Code review
25
+ │ └── worker.md # General-purpose (full capabilities)
26
+ └── prompts/ # Workflow presets (prompt templates)
27
+ ├── implement.md # scout -> planner -> worker
28
+ ├── scout-and-plan.md # scout -> planner (no implementation)
29
+ └── implement-and-review.md # worker -> reviewer -> worker
30
+ ```
31
+
32
+ ## Installation
33
+
34
+ From the repository root, symlink the files:
35
+
36
+ ```bash
37
+ # Symlink the extension (must be in a subdirectory with index.ts)
38
+ mkdir -p ~/.pi/agent/extensions/subagent
39
+ ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/index.ts" ~/.pi/agent/extensions/subagent/index.ts
40
+ ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/agents.ts" ~/.pi/agent/extensions/subagent/agents.ts
41
+
42
+ # Symlink agents
43
+ mkdir -p ~/.pi/agent/agents
44
+ for f in packages/coding-agent/examples/extensions/subagent/agents/*.md; do
45
+ ln -sf "$(pwd)/$f" ~/.pi/agent/agents/$(basename "$f")
46
+ done
47
+
48
+ # Symlink workflow prompts
49
+ mkdir -p ~/.pi/agent/prompts
50
+ for f in packages/coding-agent/examples/extensions/subagent/prompts/*.md; do
51
+ ln -sf "$(pwd)/$f" ~/.pi/agent/prompts/$(basename "$f")
52
+ done
53
+ ```
54
+
55
+ ## Security Model
56
+
57
+ This tool executes a separate `pi` subprocess with a delegated system prompt and tool/model configuration.
58
+
59
+ **Project-local agents** (`.pi/agents/*.md`) are repo-controlled prompts that can instruct the model to read files, run bash commands, etc.
60
+
61
+ **Default behavior:** Only loads **user-level agents** from `~/.pi/agent/agents`.
62
+
63
+ To enable project-local agents, pass `agentScope: "both"` (or `"project"`). Only do this for repositories you trust.
64
+
65
+ When running interactively, the tool prompts for confirmation before running project-local agents. Set `confirmProjectAgents: false` to disable.
66
+
67
+ ## Usage
68
+
69
+ ### Single agent
70
+ ```
71
+ Use scout to find all authentication code
72
+ ```
73
+
74
+ ### Parallel execution
75
+ ```
76
+ Run 2 scouts in parallel: one to find models, one to find providers
77
+ ```
78
+
79
+ ### Chained workflow
80
+ ```
81
+ Use a chain: first have scout find the read tool, then have planner suggest improvements
82
+ ```
83
+
84
+ ### Workflow prompts
85
+ ```
86
+ /implement add Redis caching to the session store
87
+ /scout-and-plan refactor auth to support OAuth
88
+ /implement-and-review add input validation to API endpoints
89
+ ```
90
+
91
+ ## Tool Modes
92
+
93
+ | Mode | Parameter | Description |
94
+ |------|-----------|-------------|
95
+ | Single | `{ agent, task }` | One agent, one task |
96
+ | Parallel | `{ tasks: [...] }` | Multiple agents run concurrently (max 8, 4 concurrent) |
97
+ | Chain | `{ chain: [...] }` | Sequential with `{previous}` placeholder |
98
+
99
+ ## Output Display
100
+
101
+ **Collapsed view** (default):
102
+ - Status icon (✓/✗/⏳) and agent name
103
+ - Last 5-10 items (tool calls and text)
104
+ - Usage stats: `3 turns ↑input ↓output RcacheRead WcacheWrite $cost ctx:contextTokens model`
105
+
106
+ **Expanded view** (Ctrl+O):
107
+ - Full task text
108
+ - All tool calls with formatted arguments
109
+ - Final output rendered as Markdown
110
+ - Per-task usage (for chain/parallel)
111
+
112
+ **Parallel mode streaming**:
113
+ - Shows all tasks with live status (⏳ running, ✓ done, ✗ failed)
114
+ - Updates as each task makes progress
115
+ - Shows "2/3 done, 1 running" status
116
+
117
+ **Tool call formatting** (mimics built-in tools):
118
+ - `$ command` for bash
119
+ - `read ~/path:1-10` for read
120
+ - `grep /pattern/ in ~/path` for grep
121
+ - etc.
122
+
123
+ ## Agent Definitions
124
+
125
+ Agents are markdown files with YAML frontmatter:
126
+
127
+ ```markdown
128
+ ---
129
+ name: my-agent
130
+ description: What this agent does
131
+ tools: read, grep, find, ls
132
+ model: claude-haiku-4-5
133
+ ---
134
+
135
+ System prompt for the agent goes here.
136
+ ```
137
+
138
+ **Locations:**
139
+ - `~/.pi/agent/agents/*.md` - User-level (always loaded)
140
+ - `.pi/agents/*.md` - Project-level (only with `agentScope: "project"` or `"both"`)
141
+
142
+ Project agents override user agents with the same name when `agentScope: "both"`.
143
+
144
+ ## Sample Agents
145
+
146
+ | Agent | Purpose | Model | Tools |
147
+ |-------|---------|-------|-------|
148
+ | `scout` | Fast codebase recon | Haiku | read, grep, find, ls, bash |
149
+ | `planner` | Implementation plans | Sonnet | read, grep, find, ls |
150
+ | `reviewer` | Code review | Sonnet | read, grep, find, ls, bash |
151
+ | `worker` | General-purpose | Sonnet | (all default) |
152
+
153
+ ## Workflow Prompts
154
+
155
+ | Prompt | Flow |
156
+ |--------|------|
157
+ | `/implement <query>` | scout → planner → worker |
158
+ | `/scout-and-plan <query>` | scout → planner |
159
+ | `/implement-and-review <query>` | worker → reviewer → worker |
160
+
161
+ ## Error Handling
162
+
163
+ - **Exit code != 0**: Tool returns error with stderr/output
164
+ - **stopReason "error"**: LLM error propagated with error message
165
+ - **stopReason "aborted"**: User abort (Ctrl+C) kills subprocess, throws error
166
+ - **Chain mode**: Stops at first failing step, reports which step failed
167
+
168
+ ## Limitations
169
+
170
+ - Output truncated to last 10 items in collapsed view (expand to see all)
171
+ - Agents discovered fresh on each invocation (allows editing mid-session)
172
+ - Parallel mode limited to 8 tasks, 4 concurrent