pi-code 1.0.55 → 1.0.57
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 +2 -2
- package/extensions/commands.ts +6 -12
- package/extensions/context-imports.ts +2 -10
- package/extensions/env-settings.ts +1 -5
- package/extensions/git-checkpoint.ts +4 -12
- package/extensions/goal.ts +2 -2
- package/extensions/hooks/config.ts +6 -21
- package/extensions/hooks/decisions.ts +3 -2
- package/extensions/hooks/index.ts +4 -16
- package/extensions/hooks/matcher.ts +2 -1
- package/extensions/hooks/runners.ts +10 -4
- package/extensions/internal/command-file.ts +7 -241
- package/extensions/internal/command-spans.ts +246 -0
- package/extensions/internal/managed-settings.ts +3 -5
- package/extensions/internal/plugins.ts +2 -2
- package/extensions/internal/settings-chain.ts +19 -0
- package/extensions/internal/values.ts +38 -0
- package/extensions/mcp/index.ts +6 -5
- package/extensions/mcp/listing.ts +2 -1
- package/extensions/mcp/oauth-flow.ts +2 -1
- package/extensions/mcp/policy.ts +9 -2
- package/extensions/memory.ts +10 -15
- package/extensions/output-styles.ts +4 -17
- package/extensions/plan-mode/index.ts +9 -9
- package/extensions/plan-mode/utils.ts +31 -0
- package/extensions/session-title.ts +2 -12
- package/extensions/skills.ts +6 -18
- package/extensions/status-line.ts +2 -8
- package/extensions/subagent/README.md +15 -5
- package/extensions/subagent/agents.ts +2 -2
- package/extensions/subagent/background.ts +2 -1
- package/extensions/subagent/child.ts +197 -0
- package/extensions/subagent/concurrency.ts +23 -0
- package/extensions/subagent/index.ts +36 -1426
- package/extensions/subagent/modes.ts +405 -0
- package/extensions/subagent/params.ts +56 -0
- package/extensions/subagent/registry-text.ts +105 -0
- package/extensions/subagent/render-result.ts +306 -0
- package/extensions/subagent/run.ts +375 -0
- package/extensions/subagent/types.ts +41 -0
- package/extensions/subagent/worktree.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a child agent is configured before it is spawned: its system prompt, its own
|
|
3
|
+
* memory store, the tools it resolves to, its CLI arguments and its hook environment.
|
|
4
|
+
*
|
|
5
|
+
* Separate from the runner so the shape of a child can be asserted without spawning
|
|
6
|
+
* one, and from the extension body so the factory keeps only schema and dispatch.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs'
|
|
10
|
+
import * as os from 'node:os'
|
|
11
|
+
import * as path from 'node:path'
|
|
12
|
+
|
|
13
|
+
import type { AgentRunRequest } from '../internal/agent-run.js'
|
|
14
|
+
import { claudeConfigDir } from '../internal/config-dir.js'
|
|
15
|
+
import { repoRoot } from '../internal/project-root.js'
|
|
16
|
+
import { autoMemoryEnabled, capIndexForPrompt, INDEX_MAX_BYTES, INDEX_MAX_LINES, memorySettingsFiles, readMemorySettings } from '../memory.js'
|
|
17
|
+
import { type AgentConfig, type AgentMemoryScope, expandMcpToolPatterns, withPreloadedSkills } from './agents.js'
|
|
18
|
+
/** The system prompt for Claude's experimental `type: "agent"` hooks: the subagent
|
|
19
|
+
* inspects with read-only tools and returns the same JSON decision a command hook's
|
|
20
|
+
* stdout carries. A hook-supplied `systemPrompt` is appended after it. */
|
|
21
|
+
export const AGENT_HOOK_SYSTEM = [
|
|
22
|
+
'You are a Claude Code agent hook verifying whether an action should proceed.',
|
|
23
|
+
'Use the Read, Grep, and Glob tools to inspect files as needed before deciding.',
|
|
24
|
+
'When done, respond with ONLY a JSON object and nothing else:',
|
|
25
|
+
'{"hookSpecificOutput":{"permissionDecision":"allow"|"deny"|"ask","permissionDecisionReason":"<short reason>"}}',
|
|
26
|
+
'Use "allow" to let the action proceed, "deny" to block it, "ask" to require the user to confirm.',
|
|
27
|
+
].join('\n')
|
|
28
|
+
|
|
29
|
+
/** A throwaway agent config for one agent-hook run: read-only inspection tools, the
|
|
30
|
+
* hook's model (a fast default when unset), and the decision-returning system prompt. */
|
|
31
|
+
/** The agent a context: fork skill runs as when it names none: full toolset, no
|
|
32
|
+
* extra system prompt (the child keeps pi's default), the skill content as the
|
|
33
|
+
* task. */
|
|
34
|
+
export function forkAgent(request: Pick<AgentRunRequest, 'model' | 'systemPrompt'>): AgentConfig {
|
|
35
|
+
return {
|
|
36
|
+
name: 'fork',
|
|
37
|
+
description: 'forked skill run',
|
|
38
|
+
systemPrompt: request.systemPrompt ?? '',
|
|
39
|
+
...(request.model ? { model: request.model } : {}),
|
|
40
|
+
source: 'builtin',
|
|
41
|
+
filePath: '',
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildHookAgent(request: Pick<AgentRunRequest, 'model' | 'systemPrompt'>): AgentConfig {
|
|
46
|
+
return {
|
|
47
|
+
name: 'agent-hook',
|
|
48
|
+
description: 'Verifies a hook condition using read-only inspection tools.',
|
|
49
|
+
tools: ['read', 'grep', 'find'],
|
|
50
|
+
model: request.model,
|
|
51
|
+
systemPrompt: request.systemPrompt ? `${AGENT_HOOK_SYSTEM}\n\n${request.systemPrompt}` : AGENT_HOOK_SYSTEM,
|
|
52
|
+
source: 'builtin',
|
|
53
|
+
filePath: '',
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The file-management tools a memory-enabled child needs for its store. */
|
|
58
|
+
const MEMORY_TOOLS = ['read', 'write', 'edit']
|
|
59
|
+
|
|
60
|
+
/** Where an agent's own persistent memory lives, per its `memory:` scope (Claude:
|
|
61
|
+
* user -> ~/.claude/agent-memory/<name>, project -> <root>/.claude/agent-memory/<name>,
|
|
62
|
+
* local -> <root>/.claude/agent-memory-local/<name>). The name comes from frontmatter
|
|
63
|
+
* a repository can control, so it is sanitized before becoming a path segment. */
|
|
64
|
+
export function agentMemoryDir(scope: AgentMemoryScope, name: string, cwd: string, home: string): string {
|
|
65
|
+
const sanitized = name.replace(/[^\w.-]+/g, '_')
|
|
66
|
+
// A name of only dots ('.', '..') survives the character filter but still traverses.
|
|
67
|
+
const segment = /^\.+$/.test(sanitized) ? '_' : sanitized
|
|
68
|
+
if (scope === 'user') return path.join(claudeConfigDir(home), 'agent-memory', segment)
|
|
69
|
+
const root = repoRoot(cwd) ?? cwd
|
|
70
|
+
return path.join(root, '.claude', scope === 'project' ? 'agent-memory' : 'agent-memory-local', segment)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The prompt section giving a memory-enabled child its own persistent store: the
|
|
74
|
+
* directory, read/write/curation instructions, and its MEMORY.md capped like the
|
|
75
|
+
* parent's index load (first 200 lines or 25KB, whichever comes first). */
|
|
76
|
+
export function agentMemorySection(dir: string, memoryMd: string): string {
|
|
77
|
+
const indexPath = path.join(dir, 'MEMORY.md')
|
|
78
|
+
const capped = capIndexForPrompt(memoryMd)
|
|
79
|
+
const current = capped.trim() ? `Current ${indexPath}:\n\n${capped}` : `${indexPath} does not exist yet; create it once you have something worth keeping.`
|
|
80
|
+
return [
|
|
81
|
+
'## Agent memory',
|
|
82
|
+
'',
|
|
83
|
+
`You have a persistent memory directory at ${dir} that survives across sessions.`,
|
|
84
|
+
'Use the read, write, and edit tools to record durable insights, project patterns, and lessons learned there, and consult them when relevant.',
|
|
85
|
+
`Only the first ${INDEX_MAX_LINES} lines or ${INDEX_MAX_BYTES} bytes of ${indexPath} are loaded at startup, so keep it a concise, curated index and move details into separate files in the directory.`,
|
|
86
|
+
'',
|
|
87
|
+
current,
|
|
88
|
+
].join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The memory section for one run, or undefined when the agent declares no memory,
|
|
92
|
+
* auto memory is off, or a repo-scoped store is not approved. Subagent memory is part
|
|
93
|
+
* of auto memory, so the same settings chain and env kill switch gate it. */
|
|
94
|
+
export function agentMemoryPromptSection(agent: Pick<AgentConfig, 'memory' | 'name'>, cwd: string, projectApproved: boolean): string | undefined {
|
|
95
|
+
if (!agent.memory) return undefined
|
|
96
|
+
// project and local stores live under the repository's .claude, a repo-controlled
|
|
97
|
+
// path; like rules, they are only read once the project is approved.
|
|
98
|
+
if (agent.memory !== 'user' && !projectApproved) return undefined
|
|
99
|
+
const settings = readMemorySettings(memorySettingsFiles(cwd, os.homedir(), projectApproved))
|
|
100
|
+
if (!autoMemoryEnabled(settings.autoMemoryEnabled, process.env)) return undefined
|
|
101
|
+
const dir = agentMemoryDir(agent.memory, agent.name, cwd, os.homedir())
|
|
102
|
+
let memoryMd = ''
|
|
103
|
+
try {
|
|
104
|
+
memoryMd = fs.readFileSync(path.join(dir, 'MEMORY.md'), 'utf-8')
|
|
105
|
+
} catch {
|
|
106
|
+
// no store yet: the section still tells the child where to create one
|
|
107
|
+
}
|
|
108
|
+
return agentMemorySection(dir, memoryMd)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Widen a restricted agent's allowlist so it can manage its memory files. An
|
|
112
|
+
* unrestricted agent (no allowlist) already has every tool. */
|
|
113
|
+
export function withMemoryTools(tools: string[] | undefined): string[] | undefined {
|
|
114
|
+
if (!tools || tools.length === 0) return tools
|
|
115
|
+
return [...tools, ...MEMORY_TOOLS.filter((tool) => !tools.includes(tool))]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The child's --append-system-prompt body: the skills-preloaded prompt plus the
|
|
119
|
+
* agent memory section, without a stray separator when either part is empty. */
|
|
120
|
+
export function childPromptBody(agent: AgentConfig, skillRoots: string[], memorySection: string | undefined): string {
|
|
121
|
+
const prompt = withPreloadedSkills(agent.systemPrompt, agent.skills, skillRoots)
|
|
122
|
+
if (!memorySection) return prompt
|
|
123
|
+
return [prompt, memorySection].filter((part) => part.trim()).join('\n\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The parent's MCP tool aliases, published by the mcp extension on the shared bus;
|
|
127
|
+
* the module-level seam matches setMcpToolCaller's. Children read the same MCP config
|
|
128
|
+
* files, so the parent's roster is the translation table for server-level patterns. */
|
|
129
|
+
let knownMcpAliases: ReadonlyArray<{ pi: string; claude: string }> = []
|
|
130
|
+
|
|
131
|
+
export function setKnownMcpAliases(aliases: ReadonlyArray<{ pi: string; claude: string }>): void {
|
|
132
|
+
knownMcpAliases = aliases
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** pi's built-in ToolName union (core/tools/index.d.ts; the package's export map
|
|
136
|
+
* does not expose allToolNames, so this mirrors it) plus the tools pi-code's own
|
|
137
|
+
* extensions register in a child. Claude's capitalized spellings fold onto these. */
|
|
138
|
+
const CHILD_TOOL_NAMES = new Set(['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls', 'web_fetch', 'web_search', 'list_mcp_resources', 'read_mcp_resource', 'todo', 'question', 'memory', 'slash_command', 'plan_mode_complete'])
|
|
139
|
+
|
|
140
|
+
/** Claude: when no entry in a `tools` list resolves to a tool, the subagent fails
|
|
141
|
+
* to launch with an error naming the entries, instead of running tool-less. */
|
|
142
|
+
export function unresolvedToolsError(agent: AgentConfig): string | undefined {
|
|
143
|
+
if (!agent.tools || agent.tools.length === 0) return undefined
|
|
144
|
+
const fold = (name: string): string => name.toLowerCase().replaceAll('-', '_')
|
|
145
|
+
const known = new Set(knownMcpAliases.map((alias) => fold(alias.pi)))
|
|
146
|
+
const resolves = expandMcpToolPatterns(agent.tools, knownMcpAliases).some((entry) => CHILD_TOOL_NAMES.has(fold(entry)) || known.has(fold(entry)))
|
|
147
|
+
if (resolves) return undefined
|
|
148
|
+
return `Agent "${agent.name}" would launch with zero tools: no entry in [${agent.tools.join(', ')}] resolves to a tool.`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** CLI args shared by foreground and background children, from the agent's config. */
|
|
152
|
+
export function agentInvocationArgs(agent: AgentConfig, aliasModel?: string): string[] {
|
|
153
|
+
const args: string[] = ['--mode', 'json', '-p', '--no-session']
|
|
154
|
+
// A concrete model wins; otherwise a Claude tier alias resolved against the models
|
|
155
|
+
// this user can actually run; then CLAUDE_CODE_SUBAGENT_MODEL, per Claude's model
|
|
156
|
+
// order (invocation model, frontmatter model, this variable, the session model).
|
|
157
|
+
// pi reads a thinking level from the model pattern's :suffix when a model is
|
|
158
|
+
// pinned, and from --thinking otherwise.
|
|
159
|
+
// Claude exempts the two built-ins from the environment variable: "Setting
|
|
160
|
+
// CLAUDE_CODE_SUBAGENT_MODEL by itself doesn't change the model the built-in Explore and
|
|
161
|
+
// Plan subagents run on." A model they name themselves, or one the invocation names,
|
|
162
|
+
// still applies.
|
|
163
|
+
const exemptFromEnvModel = agent.source === 'builtin' && (agent.name === 'Explore' || agent.name === 'Plan')
|
|
164
|
+
const model = agent.model ?? aliasModel ?? (exemptFromEnvModel ? undefined : process.env.CLAUDE_CODE_SUBAGENT_MODEL)
|
|
165
|
+
if (model) args.push('--model', agent.effort ? `${model}:${agent.effort}` : model)
|
|
166
|
+
else if (agent.effort) args.push('--thinking', agent.effort)
|
|
167
|
+
// Claude's mcp__<server> / mcp__* patterns expand against the parent's MCP roster;
|
|
168
|
+
// without this a server-level deny removed nothing (fail open) and a server-level
|
|
169
|
+
// grant granted nothing.
|
|
170
|
+
if (agent.tools && agent.tools.length > 0) args.push('--tools', expandMcpToolPatterns(agent.tools, knownMcpAliases).join(','))
|
|
171
|
+
if (agent.disallowedTools && agent.disallowedTools.length > 0) args.push('--exclude-tools', expandMcpToolPatterns(agent.disallowedTools, knownMcpAliases).join(','))
|
|
172
|
+
// Claude: "Explore and Plan are the only subagents that omit CLAUDE.md" (and no
|
|
173
|
+
// field or setting changes which agents skip them), to keep research fast.
|
|
174
|
+
if (agent.source === 'builtin' && (agent.name === 'Explore' || agent.name === 'Plan')) args.push('--no-context-files')
|
|
175
|
+
return args
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Claude's agent-frontmatter hooks ride to the child as env; the child's hooks
|
|
179
|
+
* extension merges them for the run only (they die with the process, matching
|
|
180
|
+
* "only while that subagent is running"). Stop converts to SubagentStop, the
|
|
181
|
+
* event the child fires when it completes, as Claude documents. */
|
|
182
|
+
export function agentHooksEnv(agent: AgentConfig, agentId: string): Record<string, string> {
|
|
183
|
+
if (!agent.hooks) return {}
|
|
184
|
+
const hooks: Record<string, unknown> = { ...agent.hooks }
|
|
185
|
+
const stop = hooks.Stop
|
|
186
|
+
delete hooks.Stop
|
|
187
|
+
if (Array.isArray(stop)) hooks.SubagentStop = [...(Array.isArray(hooks.SubagentStop) ? (hooks.SubagentStop as unknown[]) : []), ...stop]
|
|
188
|
+
return { PI_CODE_AGENT_HOOKS: JSON.stringify({ agent: agent.name, id: agentId, hooks }) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The task argument with any SubagentStart hook context ahead of it, per Claude:
|
|
192
|
+
* "added to the subagent's context at the start of its conversation, before its
|
|
193
|
+
* first prompt". */
|
|
194
|
+
export function taskWithStartContext(task: string, contexts: string[]): string {
|
|
195
|
+
const context = contexts.filter(Boolean).join('\n')
|
|
196
|
+
return context ? `${context}\n\nTask: ${task}` : `Task: ${task}`
|
|
197
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded fan-out for parallel subagent runs: the caps the tool advertises and the
|
|
3
|
+
* worker pool that honours them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const MAX_PARALLEL_TASKS = 8
|
|
7
|
+
export const MAX_CONCURRENCY = 4
|
|
8
|
+
|
|
9
|
+
export async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut>): Promise<TOut[]> {
|
|
10
|
+
if (items.length === 0) return []
|
|
11
|
+
const limit = Math.max(1, Math.min(concurrency, items.length))
|
|
12
|
+
const results: TOut[] = new Array(items.length)
|
|
13
|
+
let nextIndex = 0
|
|
14
|
+
const workers = new Array(limit).fill(null).map(async () => {
|
|
15
|
+
while (true) {
|
|
16
|
+
const current = nextIndex++
|
|
17
|
+
if (current >= items.length) return
|
|
18
|
+
results[current] = await fn(items[current], current)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
await Promise.all(workers)
|
|
22
|
+
return results
|
|
23
|
+
}
|