pi-code 0.5.0 → 0.7.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.
- package/README.md +5 -5
- package/extensions/hooks.ts +182 -45
- package/extensions/internal/builtin-styles/explanatory.md +15 -0
- package/extensions/internal/builtin-styles/learning.md +15 -0
- package/extensions/internal/builtin-styles/proactive.md +9 -0
- package/extensions/internal/plan-mode-state.ts +15 -0
- package/extensions/internal/project-approval.ts +10 -0
- package/extensions/internal/subagent-events.ts +18 -0
- package/extensions/mcp.ts +52 -5
- package/extensions/output-styles.ts +43 -7
- package/extensions/plan-mode/index.ts +10 -0
- package/extensions/status-line.ts +154 -12
- package/extensions/subagent/README.md +15 -34
- package/extensions/subagent/agents/explore.md +18 -0
- package/extensions/subagent/agents/general-purpose.md +10 -0
- package/extensions/subagent/agents/plan.md +16 -0
- package/extensions/subagent/agents.ts +9 -4
- package/extensions/subagent/index.ts +40 -3
- package/package.json +1 -1
- package/extensions/subagent/agents/planner.md +0 -37
- package/extensions/subagent/agents/reviewer.md +0 -35
- package/extensions/subagent/agents/scout.md +0 -50
- package/extensions/subagent/agents/worker.md +0 -24
- package/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/extensions/subagent/prompts/implement.md +0 -10
- package/extensions/subagent/prompts/scout-and-plan.md +0 -9
|
@@ -17,6 +17,8 @@ import type { AssistantMessage, TextContent } from '@earendil-works/pi-ai'
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext, SessionEntry } from '@earendil-works/pi-coding-agent'
|
|
18
18
|
import { Key } from '@earendil-works/pi-tui'
|
|
19
19
|
import { Type } from 'typebox'
|
|
20
|
+
|
|
21
|
+
import { PLAN_MODE_CHANNEL } from '../internal/plan-mode-state.js'
|
|
20
22
|
import { extractTodoItems, isSafeCommand, markCompletedSteps, planToTodos, type TodoItem } from './utils.js'
|
|
21
23
|
|
|
22
24
|
// Tools
|
|
@@ -70,6 +72,11 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/** Hooks report Claude's permission_mode from this bus state. */
|
|
76
|
+
function publishPlanState(): void {
|
|
77
|
+
pi.events.emit(PLAN_MODE_CHANNEL, { active: planModeEnabled })
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
pi.registerFlag('plan', {
|
|
74
81
|
description: 'Start in plan mode (read-only exploration)',
|
|
75
82
|
type: 'boolean',
|
|
@@ -116,6 +123,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
116
123
|
}
|
|
117
124
|
// Persist the toggle so a resume does not restore a state the user left.
|
|
118
125
|
persistState()
|
|
126
|
+
publishPlanState()
|
|
119
127
|
updateStatus(ctx)
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -158,6 +166,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
158
166
|
executionMode = todoItems.length > 0
|
|
159
167
|
planFromTool = false
|
|
160
168
|
restoreTools()
|
|
169
|
+
publishPlanState()
|
|
161
170
|
updateStatus(ctx)
|
|
162
171
|
|
|
163
172
|
// Persist before the turn: a crash before the first turn_end must resume into
|
|
@@ -377,6 +386,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
377
386
|
todoItems = planModeEntry.data.todos ?? todoItems
|
|
378
387
|
executionMode = planModeEntry.data.executing ?? executionMode
|
|
379
388
|
}
|
|
389
|
+
publishPlanState()
|
|
380
390
|
|
|
381
391
|
// On resume: re-scan messages after the last "plan-mode-execute" to rebuild
|
|
382
392
|
// completion state without picking up [DONE:n] from previous plans
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status Line Extension
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Honors Claude Code's `statusLine` settings contract: a configured command runs
|
|
5
|
+
* with the session JSON on stdin (model, workspace, cost, context_window, effort,
|
|
6
|
+
* output_style, session ids) and its first stdout line becomes the footer segment,
|
|
7
|
+
* padded per `padding`. It re-runs, debounced 300ms as Claude does, at session
|
|
8
|
+
* start, after turns, after compaction, on plan-mode changes (the permission-mode
|
|
9
|
+
* analogue, off the shared bus), and on the optional `refreshInterval` timer
|
|
10
|
+
* (minimum 1s). A project-defined command is arbitrary shell, so project settings
|
|
11
|
+
* count only once the project is already approved, read without prompting.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* Without a configured statusLine, the built-in segment shows turn state plus
|
|
14
|
+
* running session cost, summed from per-message usage on the current branch so it
|
|
15
|
+
* stays correct across /tree navigation and forks. The built-in segment is also
|
|
16
|
+
* the fallback while a configured command produces no output. Multi-line output
|
|
17
|
+
* is truncated to its first line: the segment is one footer row in pi.
|
|
18
|
+
*
|
|
19
|
+
* Docs: https://code.claude.com/docs/en/statusline.md
|
|
10
20
|
*/
|
|
11
21
|
|
|
22
|
+
import * as fs from 'node:fs'
|
|
23
|
+
import * as os from 'node:os'
|
|
12
24
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
13
25
|
|
|
26
|
+
import { hookFiles, runHookCommand } from './hooks.js'
|
|
27
|
+
import { isPlanModeState, PLAN_MODE_CHANNEL } from './internal/plan-mode-state.js'
|
|
28
|
+
import { isProjectApprovedSilently } from './internal/project-approval.js'
|
|
29
|
+
import { readActiveStyleName, settingsFiles } from './output-styles.js'
|
|
30
|
+
|
|
31
|
+
const COMMAND_TIMEOUT_MS = 5_000
|
|
32
|
+
const DEBOUNCE_MS = 300
|
|
33
|
+
|
|
14
34
|
interface UsageEntry {
|
|
15
35
|
type: string
|
|
16
36
|
message?: { usage?: { cost?: { total?: number } } }
|
|
@@ -28,34 +48,156 @@ function formatCost(cost: number): string {
|
|
|
28
48
|
return cost >= 0.01 ? `$${cost.toFixed(2)}` : `$${cost.toFixed(4)}`
|
|
29
49
|
}
|
|
30
50
|
|
|
51
|
+
export interface StatusLineConfig {
|
|
52
|
+
command: string
|
|
53
|
+
padding: number
|
|
54
|
+
refreshInterval: number | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The `statusLine` recorded in settings, last file winning. Claude's shape is
|
|
58
|
+
* `{type: "command", command, padding?, refreshInterval?}`; entries without a
|
|
59
|
+
* command string are ignored, and refreshInterval has a documented minimum of 1. */
|
|
60
|
+
export function readStatusLineConfig(files: string[]): StatusLineConfig | undefined {
|
|
61
|
+
let found: StatusLineConfig | undefined
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
try {
|
|
64
|
+
const settings = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
65
|
+
const entry = settings.statusLine
|
|
66
|
+
if (!entry || typeof entry.command !== 'string') continue
|
|
67
|
+
if (entry.type !== undefined && entry.type !== 'command') continue
|
|
68
|
+
found = {
|
|
69
|
+
command: entry.command,
|
|
70
|
+
padding: typeof entry.padding === 'number' && entry.padding > 0 ? entry.padding : 0,
|
|
71
|
+
refreshInterval: typeof entry.refreshInterval === 'number' && entry.refreshInterval >= 1 ? entry.refreshInterval : undefined,
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// missing or invalid file: skip
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return found
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
export default function statusLine(pi: ExtensionAPI) {
|
|
32
81
|
let turnCount = 0
|
|
82
|
+
let config: StatusLineConfig | undefined
|
|
83
|
+
let sessionCtx: ExtensionContext | undefined
|
|
84
|
+
let commandLine: string | undefined
|
|
85
|
+
let permissionMode = 'default'
|
|
86
|
+
let refreshTimer: ReturnType<typeof setInterval> | undefined
|
|
87
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
|
88
|
+
let running = false
|
|
89
|
+
let rerunQueued = false
|
|
33
90
|
|
|
34
|
-
function
|
|
91
|
+
function segmentText(ctx: ExtensionContext, symbol: string): string {
|
|
35
92
|
const theme = ctx.ui.theme
|
|
36
93
|
const cost = sessionCost(ctx)
|
|
37
94
|
const costText = cost > 0 ? theme.fg('muted', ` ${formatCost(cost)}`) : ''
|
|
38
95
|
const turnText = turnCount > 0 ? theme.fg('dim', ` turn ${turnCount}`) : theme.fg('dim', ' ready')
|
|
39
|
-
|
|
96
|
+
return symbol + turnText + costText
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function show(ctx: ExtensionContext, builtIn: string): void {
|
|
100
|
+
ctx.ui.setStatus('pi-code-status', commandLine ?? builtIn)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The stdin payload per Claude's documented statusline contract. */
|
|
104
|
+
function buildPayload(ctx: ExtensionContext): Record<string, unknown> {
|
|
105
|
+
const usage = ctx.getContextUsage() ?? { tokens: null, contextWindow: 0, percent: null }
|
|
106
|
+
const styleName = readActiveStyleName(settingsFiles(ctx.cwd, os.homedir(), true))
|
|
107
|
+
const payload: Record<string, unknown> = {
|
|
108
|
+
session_id: ctx.sessionManager.getSessionId(),
|
|
109
|
+
cwd: ctx.cwd,
|
|
110
|
+
workspace: { current_dir: ctx.cwd, project_dir: ctx.cwd },
|
|
111
|
+
model: { id: (ctx.model as { id?: string } | undefined)?.id ?? '' },
|
|
112
|
+
cost: { total_cost_usd: sessionCost(ctx) },
|
|
113
|
+
context_window: { context_window_size: usage.contextWindow, used_percentage: usage.percent, total_input_tokens: usage.tokens },
|
|
114
|
+
permission_mode: permissionMode,
|
|
115
|
+
}
|
|
116
|
+
const transcript = ctx.sessionManager.getSessionFile()
|
|
117
|
+
if (transcript) payload.transcript_path = transcript
|
|
118
|
+
if (ctx.thinkingLevel) payload.effort = { level: ctx.thinkingLevel }
|
|
119
|
+
if (styleName) payload.output_style = { name: styleName }
|
|
120
|
+
return payload
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runCommand(ctx: ExtensionContext): Promise<void> {
|
|
124
|
+
if (!config) return
|
|
125
|
+
if (running) {
|
|
126
|
+
rerunQueued = true
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
running = true
|
|
130
|
+
try {
|
|
131
|
+
const result = await runHookCommand(config.command, buildPayload(ctx), COMMAND_TIMEOUT_MS)
|
|
132
|
+
const first = result.stdout.split('\n')[0].trimEnd()
|
|
133
|
+
const pad = ' '.repeat(config.padding)
|
|
134
|
+
commandLine = first ? `${pad}${first}${pad}` : undefined
|
|
135
|
+
show(ctx, segmentText(ctx, ctx.ui.theme.fg('dim', '○')))
|
|
136
|
+
} finally {
|
|
137
|
+
running = false
|
|
138
|
+
if (rerunQueued) {
|
|
139
|
+
rerunQueued = false
|
|
140
|
+
void runCommand(ctx)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
40
143
|
}
|
|
41
144
|
|
|
145
|
+
/** Claude debounces statusline updates at 300ms so rapid triggers batch. */
|
|
146
|
+
function scheduleRefresh(): void {
|
|
147
|
+
if (!config || !sessionCtx) return
|
|
148
|
+
const ctx = sessionCtx
|
|
149
|
+
clearTimeout(debounceTimer)
|
|
150
|
+
debounceTimer = setTimeout(() => {
|
|
151
|
+
void runCommand(ctx)
|
|
152
|
+
}, DEBOUNCE_MS)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pi.events.on(PLAN_MODE_CHANNEL, (data) => {
|
|
156
|
+
if (!isPlanModeState(data)) return
|
|
157
|
+
permissionMode = data.active ? 'plan' : 'default'
|
|
158
|
+
scheduleRefresh()
|
|
159
|
+
})
|
|
160
|
+
|
|
42
161
|
pi.on('session_start', async (_event, ctx) => {
|
|
43
|
-
// One instance serves every session, so a fresh session must not inherit
|
|
162
|
+
// One instance serves every session, so a fresh session must not inherit state.
|
|
44
163
|
turnCount = 0
|
|
45
|
-
|
|
164
|
+
commandLine = undefined
|
|
165
|
+
sessionCtx = ctx
|
|
166
|
+
clearInterval(refreshTimer)
|
|
167
|
+
// Reading config must never open a trust dialog: several extensions resolve
|
|
168
|
+
// approval at session start, and a second prompt stacks over the first and eats
|
|
169
|
+
// the keys meant for it. An undecided project simply skips project settings.
|
|
170
|
+
const trusted = isProjectApprovedSilently(ctx)
|
|
171
|
+
config = readStatusLineConfig(hookFiles(ctx.cwd, os.homedir(), trusted))
|
|
172
|
+
if (config?.refreshInterval) {
|
|
173
|
+
refreshTimer = setInterval(() => scheduleRefresh(), config.refreshInterval * 1000)
|
|
174
|
+
}
|
|
175
|
+
show(ctx, segmentText(ctx, ctx.ui.theme.fg('dim', '○')))
|
|
176
|
+
scheduleRefresh()
|
|
46
177
|
})
|
|
47
178
|
|
|
48
179
|
pi.on('turn_start', async (_event, ctx) => {
|
|
49
180
|
turnCount++
|
|
50
181
|
const theme = ctx.ui.theme
|
|
51
|
-
ctx
|
|
182
|
+
show(ctx, theme.fg('accent', '●') + theme.fg('dim', ` turn ${turnCount}...`))
|
|
52
183
|
})
|
|
53
184
|
|
|
54
185
|
pi.on('turn_end', async (_event, ctx) => {
|
|
55
|
-
|
|
186
|
+
show(ctx, segmentText(ctx, ctx.ui.theme.fg('success', '✓')))
|
|
187
|
+
scheduleRefresh()
|
|
56
188
|
})
|
|
57
189
|
|
|
58
190
|
pi.on('agent_end', async (_event, ctx) => {
|
|
59
|
-
|
|
191
|
+
show(ctx, segmentText(ctx, ctx.ui.theme.fg('success', '✓')))
|
|
192
|
+
scheduleRefresh()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
pi.on('session_compact', async (_event, _ctx) => {
|
|
196
|
+
scheduleRefresh()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
pi.on('session_shutdown', async () => {
|
|
200
|
+
clearInterval(refreshTimer)
|
|
201
|
+
clearTimeout(debounceTimer)
|
|
60
202
|
})
|
|
61
203
|
}
|
|
@@ -21,15 +21,10 @@ subagent/
|
|
|
21
21
|
├── index.ts # The extension (entry point)
|
|
22
22
|
├── agents.ts # Agent discovery logic
|
|
23
23
|
├── background.ts # Background run registry and spawning
|
|
24
|
-
├── agents/ #
|
|
25
|
-
│ ├──
|
|
26
|
-
│ ├──
|
|
27
|
-
│
|
|
28
|
-
│ └── worker.md # General-purpose (full capabilities)
|
|
29
|
-
└── prompts/ # Workflow presets (prompt templates)
|
|
30
|
-
├── implement.md # scout -> planner -> worker
|
|
31
|
-
├── scout-and-plan.md # scout -> planner (no implementation)
|
|
32
|
-
└── implement-and-review.md # worker -> reviewer -> worker
|
|
24
|
+
├── agents/ # Bundled builtin agents, always available (lowest precedence)
|
|
25
|
+
│ ├── explore.md # Explore: fast read-only codebase exploration
|
|
26
|
+
│ ├── plan.md # Plan: read-only implementation planning
|
|
27
|
+
│ └── general-purpose.md # general-purpose: full capabilities
|
|
33
28
|
```
|
|
34
29
|
|
|
35
30
|
## Installation
|
|
@@ -42,7 +37,7 @@ This tool executes a separate `pi` subprocess with a delegated system prompt and
|
|
|
42
37
|
|
|
43
38
|
**Project-local agents** (`.pi/agents/*.md`) are repo-controlled prompts that can instruct the model to read files, run bash commands, etc.
|
|
44
39
|
|
|
45
|
-
**Default behavior:**
|
|
40
|
+
**Default behavior:** Loads the bundled builtin agents (Explore, Plan, general-purpose) plus **user-level agents** from `~/.claude/agents` and `~/.pi/agent/agents`. A user or project agent with the same name overrides a builtin. Discovered agents and their descriptions are listed in the system prompt each turn, so the model can pick one itself; project agent descriptions appear only once the project is approved.
|
|
46
41
|
|
|
47
42
|
To enable project-local agents (`.claude/agents`, `.pi/agents`), pass `agentScope: "both"` (or `"project"`). Only do this for repositories you trust.
|
|
48
43
|
|
|
@@ -52,24 +47,17 @@ When running interactively, the tool prompts for confirmation before running pro
|
|
|
52
47
|
|
|
53
48
|
### Single agent
|
|
54
49
|
```
|
|
55
|
-
Use
|
|
50
|
+
Use Explore to find all authentication code
|
|
56
51
|
```
|
|
57
52
|
|
|
58
53
|
### Parallel execution
|
|
59
54
|
```
|
|
60
|
-
Run 2
|
|
55
|
+
Run 2 Explore agents in parallel: one to find models, one to find providers
|
|
61
56
|
```
|
|
62
57
|
|
|
63
58
|
### Chained workflow
|
|
64
59
|
```
|
|
65
|
-
Use a chain: first have
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Workflow prompts
|
|
69
|
-
```
|
|
70
|
-
/implement add Redis caching to the session store
|
|
71
|
-
/scout-and-plan refactor auth to support OAuth
|
|
72
|
-
/implement-and-review add input validation to API endpoints
|
|
60
|
+
Use a chain: first have Explore find the read tool, then have Plan suggest improvements
|
|
73
61
|
```
|
|
74
62
|
|
|
75
63
|
## Tool Modes
|
|
@@ -135,22 +123,15 @@ model. Fields with no pi equivalent are ignored: `skills`, `memory`,
|
|
|
135
123
|
|
|
136
124
|
Project agents override user agents with the same name when `agentScope: "both"`.
|
|
137
125
|
|
|
138
|
-
##
|
|
139
|
-
|
|
140
|
-
| Agent | Purpose | Model | Tools |
|
|
141
|
-
|-------|---------|-------|-------|
|
|
142
|
-
| `scout` | Fast codebase recon | Haiku | read, grep, find, ls, bash |
|
|
143
|
-
| `planner` | Implementation plans | Sonnet | read, grep, find, ls |
|
|
144
|
-
| `reviewer` | Code review | Sonnet | read, grep, find, ls, bash |
|
|
145
|
-
| `worker` | General-purpose | Sonnet | (all default) |
|
|
126
|
+
## Builtin Agents
|
|
146
127
|
|
|
147
|
-
|
|
128
|
+
| Agent | Purpose | Tools |
|
|
129
|
+
|-------|---------|-------|
|
|
130
|
+
| `Explore` | Fast read-only codebase exploration | read, grep, find, ls |
|
|
131
|
+
| `Plan` | Read-only implementation planning | read, grep, find, ls |
|
|
132
|
+
| `general-purpose` | Full capabilities, isolated context | (all default) |
|
|
148
133
|
|
|
149
|
-
|
|
150
|
-
|--------|------|
|
|
151
|
-
| `/implement <query>` | scout → planner → worker |
|
|
152
|
-
| `/scout-and-plan <query>` | scout → planner |
|
|
153
|
-
| `/implement-and-review <query>` | worker → reviewer → worker |
|
|
134
|
+
No model is pinned: each runs on the session's default model.
|
|
154
135
|
|
|
155
136
|
## Error Handling
|
|
156
137
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Explore
|
|
3
|
+
description: Fast read-only codebase exploration that returns compressed findings
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are an exploration agent. Quickly investigate the codebase and return structured findings that another agent can use without re-reading everything.
|
|
8
|
+
|
|
9
|
+
You must NOT make any changes: only read, search, and summarize.
|
|
10
|
+
|
|
11
|
+
Your output goes to an agent who has NOT seen the files you explored. Report:
|
|
12
|
+
|
|
13
|
+
1. Relevant files with paths and one-line roles
|
|
14
|
+
2. Key functions/types with `file:line` references
|
|
15
|
+
3. How the pieces connect (data flow, call flow)
|
|
16
|
+
4. Anything surprising or risky
|
|
17
|
+
|
|
18
|
+
Be selective: compressed, load-bearing findings beat exhaustive dumps.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: general-purpose
|
|
3
|
+
description: General-purpose agent with full capabilities in an isolated context
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a general-purpose agent with full capabilities, operating in an isolated context window to handle delegated tasks without polluting the main conversation.
|
|
7
|
+
|
|
8
|
+
Work autonomously to complete the assigned task, using the available tools as needed.
|
|
9
|
+
|
|
10
|
+
When finished, report: what was done, what was verified (commands run, tests passed), and anything the delegator must know (caveats, follow-ups, files changed).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Plan
|
|
3
|
+
description: Designs implementation plans from context and requirements, read-only
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a planning specialist. You receive context and requirements, then produce a clear implementation plan.
|
|
8
|
+
|
|
9
|
+
You must NOT make any changes: only read, analyze, and plan.
|
|
10
|
+
|
|
11
|
+
Deliver:
|
|
12
|
+
|
|
13
|
+
1. Step-by-step plan, each step small and independently verifiable
|
|
14
|
+
2. Files to touch per step, with `file:line` anchors where known
|
|
15
|
+
3. Risks and open questions, each with a suggested resolution
|
|
16
|
+
4. What to test and how the tests would fail without the change
|
|
@@ -68,7 +68,7 @@ function parseEffortField(raw: unknown): string | undefined {
|
|
|
68
68
|
const READ_ONLY_TOOLS = ['read', 'grep', 'find', 'ls']
|
|
69
69
|
|
|
70
70
|
/** Parse one agent markdown file; null when it is not a usable agent definition. */
|
|
71
|
-
function parseAgentFile(content: string, source:
|
|
71
|
+
function parseAgentFile(content: string, source: AgentSource, filePath: string): AgentConfig | null {
|
|
72
72
|
let parsed: { frontmatter: Record<string, unknown>; body: string }
|
|
73
73
|
try {
|
|
74
74
|
parsed = parseFrontmatter<Record<string, unknown>>(content)
|
|
@@ -106,7 +106,7 @@ export interface AgentConfig {
|
|
|
106
106
|
model?: string
|
|
107
107
|
effort?: string
|
|
108
108
|
systemPrompt: string
|
|
109
|
-
source:
|
|
109
|
+
source: AgentSource
|
|
110
110
|
filePath: string
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -115,7 +115,7 @@ export interface AgentDiscoveryResult {
|
|
|
115
115
|
projectAgentsDir: string | null
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function loadAgentsFromDir(dir: string, source:
|
|
118
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
119
119
|
const agents: AgentConfig[] = []
|
|
120
120
|
|
|
121
121
|
if (!fs.existsSync(dir)) {
|
|
@@ -202,6 +202,11 @@ function buildAgentMap(userAgents: AgentConfig[], projectAgents: AgentConfig[],
|
|
|
202
202
|
return agentMap
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
export type AgentSource = 'user' | 'project' | 'builtin'
|
|
206
|
+
|
|
207
|
+
/** Bundled default agents (Explore, Plan, general-purpose), lowest precedence. */
|
|
208
|
+
export const BUILTIN_AGENTS_DIR = path.join(import.meta.dirname, 'agents')
|
|
209
|
+
|
|
205
210
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
206
211
|
const userDir = path.join(getAgentDir(), 'agents')
|
|
207
212
|
const claudeUserDir = path.join(os.homedir(), '.claude', 'agents')
|
|
@@ -209,7 +214,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
209
214
|
const projectClaudeDir = findNearestDir(cwd, path.join('.claude', 'agents'))
|
|
210
215
|
|
|
211
216
|
// ~/.claude/agents loads first so ~/.pi/agent/agents wins on name conflicts
|
|
212
|
-
const userAgents = scope === 'project' ? [] : [...loadAgentsFromDir(claudeUserDir, 'user'), ...loadAgentsFromDir(userDir, 'user')]
|
|
217
|
+
const userAgents = scope === 'project' ? [] : [...loadAgentsFromDir(BUILTIN_AGENTS_DIR, 'builtin'), ...loadAgentsFromDir(claudeUserDir, 'user'), ...loadAgentsFromDir(userDir, 'user')]
|
|
213
218
|
// project .claude/agents loads first so project .pi/agents wins on name conflicts
|
|
214
219
|
const projectAgents = scope === 'user' ? [] : [...(projectClaudeDir ? loadAgentsFromDir(projectClaudeDir, 'project') : []), ...(projectPiDir ? loadAgentsFromDir(projectPiDir, 'project') : [])]
|
|
215
220
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { spawn } from 'node:child_process'
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
16
17
|
import * as fs from 'node:fs'
|
|
17
18
|
import * as os from 'node:os'
|
|
18
19
|
import * as path from 'node:path'
|
|
@@ -23,7 +24,8 @@ import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme, type Theme,
|
|
|
23
24
|
import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui'
|
|
24
25
|
import { type Static, Type } from 'typebox'
|
|
25
26
|
import { capForContext } from '../internal/output-guard.js'
|
|
26
|
-
import { isProjectApproved } from '../internal/project-approval.js'
|
|
27
|
+
import { isProjectApproved, isProjectApprovedSilently } from '../internal/project-approval.js'
|
|
28
|
+
import { SUBAGENT_CHANNEL } from '../internal/subagent-events.js'
|
|
27
29
|
import { type AgentConfig, type AgentScope, discoverAgents } from './agents.js'
|
|
28
30
|
import { activeBackgroundRuns, backgroundStatusText, MAX_BACKGROUND_RUNS, startBackgroundRun } from './background.js'
|
|
29
31
|
|
|
@@ -137,7 +139,7 @@ interface UsageStats {
|
|
|
137
139
|
|
|
138
140
|
interface SingleResult {
|
|
139
141
|
agent: string
|
|
140
|
-
agentSource: 'user' | 'project' | 'unknown'
|
|
142
|
+
agentSource: 'user' | 'project' | 'builtin' | 'unknown'
|
|
141
143
|
task: string
|
|
142
144
|
exitCode: number
|
|
143
145
|
messages: Message[]
|
|
@@ -256,9 +258,25 @@ interface RunAgentOptions {
|
|
|
256
258
|
signal?: AbortSignal
|
|
257
259
|
onUpdate?: OnUpdateCallback
|
|
258
260
|
makeDetails: (results: SingleResult[]) => SubagentDetails
|
|
261
|
+
onPhase?: SubagentPhaseSink
|
|
259
262
|
}
|
|
260
263
|
|
|
264
|
+
/** Publishes a child run's start/stop for the hooks extension's SubagentStart/Stop. */
|
|
265
|
+
type SubagentPhaseSink = (phase: 'start' | 'stop', agentType: string, agentId: string) => void
|
|
266
|
+
|
|
261
267
|
async function runSingleAgent(options: RunAgentOptions): Promise<SingleResult> {
|
|
268
|
+
const agent = options.agents.find((a) => a.name === options.agentName)
|
|
269
|
+
if (!agent) return runSingleAgentInner(options)
|
|
270
|
+
const agentId = `fg-${randomUUID().slice(0, 8)}`
|
|
271
|
+
options.onPhase?.('start', agent.name, agentId)
|
|
272
|
+
try {
|
|
273
|
+
return await runSingleAgentInner(options)
|
|
274
|
+
} finally {
|
|
275
|
+
options.onPhase?.('stop', agent.name, agentId)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResult> {
|
|
262
280
|
const { defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails } = options
|
|
263
281
|
const agent = agents.find((a) => a.name === agentName)
|
|
264
282
|
|
|
@@ -478,6 +496,7 @@ interface ModeContext {
|
|
|
478
496
|
signal: AbortSignal | undefined
|
|
479
497
|
onUpdate: OnUpdateCallback | undefined
|
|
480
498
|
makeDetails: MakeDetails
|
|
499
|
+
onPhase?: SubagentPhaseSink
|
|
481
500
|
}
|
|
482
501
|
|
|
483
502
|
async function checkProjectAgentGate(params: SubagentParamsStatic, agents: AgentConfig[], ctx: ExtensionContext, projectAgentsDir: string | null, gateMode: SubagentMode, makeDetails: MakeDetails): Promise<ToolResult | null> {
|
|
@@ -572,6 +591,7 @@ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConf
|
|
|
572
591
|
const invocation = getPiInvocation(args)
|
|
573
592
|
const id = startBackgroundRun(agent.name, task, { command: invocation.command, args: invocation.args, cwd: params.cwd ?? defaultCwd }, (run) => {
|
|
574
593
|
removeTmpPrompt(tmpPrompt)
|
|
594
|
+
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id })
|
|
575
595
|
const output = capForContext(run.output ?? '') || '(no output)'
|
|
576
596
|
pi.sendMessage(
|
|
577
597
|
{
|
|
@@ -587,6 +607,7 @@ async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConf
|
|
|
587
607
|
removeTmpPrompt(tmpPrompt)
|
|
588
608
|
return backgroundCapResult(makeDetails)
|
|
589
609
|
}
|
|
610
|
+
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'start', agentType: agent.name, agentId: id })
|
|
590
611
|
return {
|
|
591
612
|
content: [{ type: 'text', text: `Started background run ${id} (${agent.name}). A notification will arrive on completion; check progress with {status: true}.` }],
|
|
592
613
|
details: makeDetails('single')([]),
|
|
@@ -628,6 +649,7 @@ async function runChainMode(chain: ChainStepParam[], mode: ModeContext): Promise
|
|
|
628
649
|
signal,
|
|
629
650
|
onUpdate: chainUpdate,
|
|
630
651
|
makeDetails: makeDetails('chain'),
|
|
652
|
+
onPhase: mode.onPhase,
|
|
631
653
|
})
|
|
632
654
|
results.push(result)
|
|
633
655
|
|
|
@@ -695,6 +717,7 @@ async function runParallelMode(tasks: TaskItemParam[], mode: ModeContext): Promi
|
|
|
695
717
|
task: t.task,
|
|
696
718
|
cwd: t.cwd,
|
|
697
719
|
signal,
|
|
720
|
+
onPhase: mode.onPhase,
|
|
698
721
|
// Per-task update callback
|
|
699
722
|
onUpdate: (partial) => {
|
|
700
723
|
const live = partial.details?.results[0]
|
|
@@ -741,6 +764,7 @@ async function runSingleMode(agentName: string, task: string, cwd: string | unde
|
|
|
741
764
|
signal,
|
|
742
765
|
onUpdate,
|
|
743
766
|
makeDetails: makeDetails('single'),
|
|
767
|
+
onPhase: mode.onPhase,
|
|
744
768
|
})
|
|
745
769
|
const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
|
|
746
770
|
if (isError) {
|
|
@@ -1044,6 +1068,19 @@ function renderParallelResult(results: SingleResult[], expanded: boolean, theme:
|
|
|
1044
1068
|
}
|
|
1045
1069
|
|
|
1046
1070
|
export default function subagentExtension(pi: ExtensionAPI) {
|
|
1071
|
+
// Claude surfaces each agent's description so the model can pick one autonomously.
|
|
1072
|
+
// Rebuilt per turn (agents are rediscovered per invocation too); project agents are
|
|
1073
|
+
// included only when the project is already approved, read without prompting, since
|
|
1074
|
+
// a trust dialog must not appear mid-turn and their descriptions are project text.
|
|
1075
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
1076
|
+
const scope = isProjectApprovedSilently(ctx) ? 'both' : 'user'
|
|
1077
|
+
const { agents } = discoverAgents(ctx.cwd, scope)
|
|
1078
|
+
if (agents.length === 0) return
|
|
1079
|
+
const line = (text: string): string => text.replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
1080
|
+
const roster = agents.map((agent) => `- ${agent.name} (${agent.source}): ${line(agent.description)}`).join('\n')
|
|
1081
|
+
return { systemPrompt: `${event.systemPrompt}\n\n## Subagents\n\nDelegate isolated tasks with the subagent tool ({agent, task}). Available agents:\n${roster}` }
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1047
1084
|
pi.registerTool({
|
|
1048
1085
|
name: 'subagent',
|
|
1049
1086
|
label: 'Subagent',
|
|
@@ -1109,7 +1146,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1109
1146
|
|
|
1110
1147
|
if (params.background) return runBackgroundMode(params, agents, ctx.cwd, pi, makeDetails)
|
|
1111
1148
|
|
|
1112
|
-
const mode: ModeContext = { agents, defaultCwd: ctx.cwd, signal, onUpdate, makeDetails }
|
|
1149
|
+
const mode: ModeContext = { agents, defaultCwd: ctx.cwd, signal, onUpdate, makeDetails, onPhase: (phase, agentType, agentId) => pi.events.emit(SUBAGENT_CHANNEL, { phase, agentType, agentId }) }
|
|
1113
1150
|
|
|
1114
1151
|
if (params.chain?.length) return runChainMode(params.chain, mode)
|
|
1115
1152
|
if (params.tasks?.length) return runParallelMode(params.tasks, mode)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: planner
|
|
3
|
-
description: Creates implementation plans from context and requirements
|
|
4
|
-
tools: read, grep, find, ls
|
|
5
|
-
model: claude-sonnet-4-5
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
|
|
9
|
-
|
|
10
|
-
You must NOT make any changes. Only read, analyze, and plan.
|
|
11
|
-
|
|
12
|
-
Input format you'll receive:
|
|
13
|
-
- Context/findings from a scout agent
|
|
14
|
-
- Original query or requirements
|
|
15
|
-
|
|
16
|
-
Output format:
|
|
17
|
-
|
|
18
|
-
## Goal
|
|
19
|
-
One sentence summary of what needs to be done.
|
|
20
|
-
|
|
21
|
-
## Plan
|
|
22
|
-
Numbered steps, each small and actionable:
|
|
23
|
-
1. Step one - specific file/function to modify
|
|
24
|
-
2. Step two - what to add/change
|
|
25
|
-
3. ...
|
|
26
|
-
|
|
27
|
-
## Files to Modify
|
|
28
|
-
- `path/to/file.ts` - what changes
|
|
29
|
-
- `path/to/other.ts` - what changes
|
|
30
|
-
|
|
31
|
-
## New Files (if any)
|
|
32
|
-
- `path/to/new.ts` - purpose
|
|
33
|
-
|
|
34
|
-
## Risks
|
|
35
|
-
Anything to watch out for.
|
|
36
|
-
|
|
37
|
-
Keep the plan concrete. The worker agent will execute it verbatim.
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: reviewer
|
|
3
|
-
description: Code review specialist for quality and security analysis
|
|
4
|
-
tools: read, grep, find, ls, bash
|
|
5
|
-
model: claude-sonnet-4-5
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a senior code reviewer. Analyze code for quality, security, and maintainability.
|
|
9
|
-
|
|
10
|
-
Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
|
|
11
|
-
Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
|
12
|
-
|
|
13
|
-
Strategy:
|
|
14
|
-
1. Run `git diff` to see recent changes (if applicable)
|
|
15
|
-
2. Read the modified files
|
|
16
|
-
3. Check for bugs, security issues, code smells
|
|
17
|
-
|
|
18
|
-
Output format:
|
|
19
|
-
|
|
20
|
-
## Files Reviewed
|
|
21
|
-
- `path/to/file.ts` (lines X-Y)
|
|
22
|
-
|
|
23
|
-
## Critical (must fix)
|
|
24
|
-
- `file.ts:42` - Issue description
|
|
25
|
-
|
|
26
|
-
## Warnings (should fix)
|
|
27
|
-
- `file.ts:100` - Issue description
|
|
28
|
-
|
|
29
|
-
## Suggestions (consider)
|
|
30
|
-
- `file.ts:150` - Improvement idea
|
|
31
|
-
|
|
32
|
-
## Summary
|
|
33
|
-
Overall assessment in 2-3 sentences.
|
|
34
|
-
|
|
35
|
-
Be specific with file paths and line numbers.
|