@ilya-lesikov/pi-pi 0.9.0 → 0.11.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.
Files changed (108) hide show
  1. package/3p/pi-ask-user/index.ts +71 -124
  2. package/3p/pi-ask-user/single-select-layout.ts +3 -14
  3. package/3p/pi-subagents/package.json +12 -7
  4. package/3p/pi-subagents/src/agent-manager.ts +243 -79
  5. package/3p/pi-subagents/src/agent-runner.ts +501 -365
  6. package/3p/pi-subagents/src/agent-types.ts +46 -57
  7. package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
  8. package/3p/pi-subagents/src/custom-agents.ts +30 -6
  9. package/3p/pi-subagents/src/default-agents.ts +25 -43
  10. package/3p/pi-subagents/src/enabled-models.ts +180 -0
  11. package/3p/pi-subagents/src/index.ts +599 -839
  12. package/3p/pi-subagents/src/model-resolver.ts +26 -7
  13. package/3p/pi-subagents/src/output-file.ts +21 -2
  14. package/3p/pi-subagents/src/prompts.ts +17 -3
  15. package/3p/pi-subagents/src/schedule-store.ts +153 -0
  16. package/3p/pi-subagents/src/schedule.ts +365 -0
  17. package/3p/pi-subagents/src/settings.ts +261 -0
  18. package/3p/pi-subagents/src/skill-loader.ts +77 -54
  19. package/3p/pi-subagents/src/status-note.ts +25 -0
  20. package/3p/pi-subagents/src/types.ts +97 -6
  21. package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
  22. package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
  23. package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
  24. package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
  25. package/3p/pi-subagents/src/usage.ts +60 -0
  26. package/3p/pi-subagents/src/worktree.ts +49 -20
  27. package/extensions/orchestrator/agents/advisor.ts +13 -8
  28. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
  29. package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
  30. package/extensions/orchestrator/agents/constraints.test.ts +26 -0
  31. package/extensions/orchestrator/agents/constraints.ts +12 -2
  32. package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
  33. package/extensions/orchestrator/agents/explore.ts +12 -6
  34. package/extensions/orchestrator/agents/librarian.ts +13 -5
  35. package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
  36. package/extensions/orchestrator/agents/planner.ts +8 -3
  37. package/extensions/orchestrator/agents/pools.test.ts +56 -0
  38. package/extensions/orchestrator/agents/prompts.test.ts +52 -10
  39. package/extensions/orchestrator/agents/registry.test.ts +245 -0
  40. package/extensions/orchestrator/agents/registry.ts +145 -10
  41. package/extensions/orchestrator/agents/reviewer.ts +14 -8
  42. package/extensions/orchestrator/agents/task.ts +12 -6
  43. package/extensions/orchestrator/agents/tool-routing.ts +213 -85
  44. package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
  45. package/extensions/orchestrator/ast-search.test.ts +124 -0
  46. package/extensions/orchestrator/billing-spoof.test.ts +67 -0
  47. package/extensions/orchestrator/billing-spoof.ts +95 -0
  48. package/extensions/orchestrator/cbm.more.test.ts +358 -0
  49. package/extensions/orchestrator/command-handlers.ts +6 -0
  50. package/extensions/orchestrator/commands.test.ts +15 -2
  51. package/extensions/orchestrator/commands.ts +1 -1
  52. package/extensions/orchestrator/config.test.ts +89 -1
  53. package/extensions/orchestrator/config.ts +102 -19
  54. package/extensions/orchestrator/context.test.ts +46 -0
  55. package/extensions/orchestrator/context.ts +18 -5
  56. package/extensions/orchestrator/custom-footer.test.ts +24 -10
  57. package/extensions/orchestrator/custom-footer.ts +4 -2
  58. package/extensions/orchestrator/doctor.more.test.ts +315 -0
  59. package/extensions/orchestrator/doctor.ts +6 -2
  60. package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
  61. package/extensions/orchestrator/event-handlers.test.ts +96 -9
  62. package/extensions/orchestrator/event-handlers.ts +344 -151
  63. package/extensions/orchestrator/exa.more.test.ts +118 -0
  64. package/extensions/orchestrator/flant-infra.more.test.ts +381 -0
  65. package/extensions/orchestrator/flant-infra.test.ts +127 -0
  66. package/extensions/orchestrator/flant-infra.ts +126 -41
  67. package/extensions/orchestrator/index.test.ts +76 -0
  68. package/extensions/orchestrator/index.ts +2 -0
  69. package/extensions/orchestrator/integration.test.ts +183 -65
  70. package/extensions/orchestrator/model-registry.test.ts +2 -1
  71. package/extensions/orchestrator/model-registry.ts +12 -2
  72. package/extensions/orchestrator/orchestrator.test.ts +67 -0
  73. package/extensions/orchestrator/orchestrator.ts +119 -27
  74. package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
  75. package/extensions/orchestrator/phases/brainstorm.ts +43 -6
  76. package/extensions/orchestrator/phases/implementation.ts +2 -0
  77. package/extensions/orchestrator/phases/machine.test.ts +17 -1
  78. package/extensions/orchestrator/phases/machine.ts +4 -1
  79. package/extensions/orchestrator/phases/planning.test.ts +47 -1
  80. package/extensions/orchestrator/phases/planning.ts +18 -3
  81. package/extensions/orchestrator/phases/review-task.ts +5 -0
  82. package/extensions/orchestrator/phases/review.test.ts +10 -0
  83. package/extensions/orchestrator/phases/review.ts +22 -7
  84. package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
  85. package/extensions/orchestrator/phases/verdict.ts +6 -5
  86. package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
  87. package/extensions/orchestrator/plannotator.test.ts +38 -1
  88. package/extensions/orchestrator/plannotator.ts +50 -3
  89. package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
  90. package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
  91. package/extensions/orchestrator/pp-menu.test.ts +114 -7
  92. package/extensions/orchestrator/pp-menu.ts +580 -91
  93. package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
  94. package/extensions/orchestrator/pp-state-tools.ts +65 -0
  95. package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
  96. package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
  97. package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
  98. package/extensions/orchestrator/review-files.test.ts +26 -0
  99. package/extensions/orchestrator/review-files.ts +3 -0
  100. package/extensions/orchestrator/state.test.ts +73 -1
  101. package/extensions/orchestrator/state.ts +95 -23
  102. package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
  103. package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
  104. package/extensions/orchestrator/validate-artifacts.ts +1 -1
  105. package/package.json +9 -2
  106. package/scripts/lib/smoke-resolve.mjs +99 -0
  107. package/scripts/test-3p.sh +52 -0
  108. package/scripts/test-package.sh +62 -0
@@ -5,33 +5,21 @@
5
5
  * User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
6
6
  */
7
7
 
8
- import type { AgentTool } from "@earendil-works/pi-agent-core";
9
- import {
10
- createBashTool,
11
- createEditTool,
12
- createFindTool,
13
- createGrepTool,
14
- createLsTool,
15
- createReadTool,
16
- createWriteTool,
17
- } from "@earendil-works/pi-coding-agent";
8
+ import { createCodingTools, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
18
9
  import { DEFAULT_AGENTS } from "./default-agents.js";
19
10
  import type { AgentConfig } from "./types.js";
20
11
 
21
- type ToolFactory = (cwd: string) => AgentTool<any>;
22
-
23
- const TOOL_FACTORIES: Record<string, ToolFactory> = {
24
- read: (cwd) => createReadTool(cwd),
25
- bash: (cwd) => createBashTool(cwd),
26
- edit: (cwd) => createEditTool(cwd),
27
- write: (cwd) => createWriteTool(cwd),
28
- grep: (cwd) => createGrepTool(cwd),
29
- find: (cwd) => createFindTool(cwd),
30
- ls: (cwd) => createLsTool(cwd),
31
- };
32
-
33
- /** All known built-in tool names, derived from the factory registry. */
34
- export const BUILTIN_TOOL_NAMES = Object.keys(TOOL_FACTORIES);
12
+ /**
13
+ * All known built-in tool names, derived from pi's own tool factories rather
14
+ * than hardcoded so the set tracks pi-mono if it adds/renames a built-in.
15
+ * `createCodingTools` → read/bash/edit/write; `createReadOnlyTools`
16
+ * read/grep/find/ls; their de-duplicated union is the 7 built-ins
17
+ * (read, bash, edit, write, grep, find, ls). The `cwd` only binds tool
18
+ * operations we never invoke here — we read each tool's `.name` and discard it.
19
+ */
20
+ export const BUILTIN_TOOL_NAMES: string[] = [
21
+ ...new Set([...createCodingTools("."), ...createReadOnlyTools(".")].map((t) => t.name)),
22
+ ];
35
23
 
36
24
  /** Unified runtime registry of all agents (defaults + user-defined). */
37
25
  const agents = new Map<string, AgentConfig>();
@@ -39,13 +27,20 @@ const agents = new Map<string, AgentConfig>();
39
27
  /** Extension-injected agents (highest priority, survive reloads). */
40
28
  const extensionAgents = new Map<string, AgentConfig>();
41
29
 
30
+ /** When true, DEFAULT_AGENTS are skipped during registration. */
31
+ let disableDefaults = false;
32
+
42
33
  /** When true, only extension-registered agents are used. Defaults and user .md files are skipped. */
43
34
  let extensionOnlyMode = false;
44
35
 
36
+ /** Check whether default agents are disabled. */
37
+ export function isDefaultsDisabled(): boolean { return disableDefaults; }
38
+
39
+ /** Set whether default agents are disabled. */
40
+ export function setDefaultsDisabled(b: boolean): void { disableDefaults = b; }
41
+
45
42
  /** Enable extension-only mode: skip defaults and user .md agents entirely. */
46
- export function setExtensionOnlyMode(enabled: boolean): void {
47
- extensionOnlyMode = enabled;
48
- }
43
+ export function setExtensionOnlyMode(enabled: boolean): void { extensionOnlyMode = enabled; }
49
44
 
50
45
  /**
51
46
  * Register agents into the unified registry.
@@ -57,9 +52,11 @@ export function registerAgents(userAgents: Map<string, AgentConfig>): void {
57
52
  agents.clear();
58
53
 
59
54
  if (!extensionOnlyMode) {
60
- // Start with defaults
61
- for (const [name, config] of DEFAULT_AGENTS) {
62
- agents.set(name, config);
55
+ // Start with defaults (unless disabled via settings)
56
+ if (!disableDefaults) {
57
+ for (const [name, config] of DEFAULT_AGENTS) {
58
+ agents.set(name, config);
59
+ }
63
60
  }
64
61
 
65
62
  // Overlay user agents (overrides defaults with same name)
@@ -68,14 +65,14 @@ export function registerAgents(userAgents: Map<string, AgentConfig>): void {
68
65
  }
69
66
  }
70
67
 
71
- // Overlay extension-injected agents (highest priority)
68
+ // Overlay extension-injected agents (highest priority, survive reloads)
72
69
  for (const [name, config] of extensionAgents) {
73
70
  agents.set(name, config);
74
71
  }
75
72
  }
76
73
 
77
74
  /**
78
- * Register agents from another extension (e.g. pi-pi).
75
+ * Register agents from another extension (e.g. pi-pi orchestrator).
79
76
  * These persist across reloadCustomAgents() calls and take highest priority.
80
77
  */
81
78
  export function registerExtensionAgents(newAgents: Map<string, AgentConfig>): void {
@@ -84,18 +81,14 @@ export function registerExtensionAgents(newAgents: Map<string, AgentConfig>): vo
84
81
  }
85
82
  }
86
83
 
87
- /**
88
- * Remove extension-registered agents by name.
89
- */
84
+ /** Remove extension-registered agents by name. */
90
85
  export function unregisterExtensionAgents(names: string[]): void {
91
86
  for (const name of names) {
92
87
  extensionAgents.delete(name);
93
88
  }
94
89
  }
95
90
 
96
- /**
97
- * Remove extension-registered agents whose names start with a prefix.
98
- */
91
+ /** Remove extension-registered agents whose names start with a prefix. */
99
92
  export function unregisterExtensionAgentsByPrefix(prefix: string): void {
100
93
  for (const name of extensionAgents.keys()) {
101
94
  if (name.startsWith(prefix)) {
@@ -104,9 +97,7 @@ export function unregisterExtensionAgentsByPrefix(prefix: string): void {
104
97
  }
105
98
  }
106
99
 
107
- /**
108
- * Remove all extension-registered agents.
109
- */
100
+ /** Remove all extension-registered agents. */
110
101
  export function clearExtensionAgents(): void {
111
102
  extensionAgents.clear();
112
103
  }
@@ -169,35 +160,30 @@ export function isValidType(type: string): boolean {
169
160
  const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
170
161
 
171
162
  /**
172
- * Get the tools needed for memory management (read, write, edit).
173
- * Only returns tools that are NOT already in the provided set.
163
+ * Get memory tool names (read/write/edit) not already in the provided set.
174
164
  */
175
- export function getMemoryTools(cwd: string, existingToolNames: Set<string>): AgentTool<any>[] {
176
- return MEMORY_TOOL_NAMES
177
- .filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
178
- .map(n => TOOL_FACTORIES[n](cwd));
165
+ export function getMemoryToolNames(existingToolNames: Set<string>): string[] {
166
+ return MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
179
167
  }
180
168
 
181
169
  /** Tool names needed for read-only memory access. */
182
170
  const READONLY_MEMORY_TOOL_NAMES = ["read"];
183
171
 
184
172
  /**
185
- * Get only the read tool for read-only memory access.
186
- * Only returns tools that are NOT already in the provided set.
173
+ * Get read-only memory tool names not already in the provided set.
187
174
  */
188
- export function getReadOnlyMemoryTools(cwd: string, existingToolNames: Set<string>): AgentTool<any>[] {
189
- return READONLY_MEMORY_TOOL_NAMES
190
- .filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
191
- .map(n => TOOL_FACTORIES[n](cwd));
175
+ export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
176
+ return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
192
177
  }
193
178
 
194
- /** Get built-in tools for a type (case-insensitive). */
195
- export function getToolsForType(type: string, cwd: string): AgentTool<any>[] {
179
+ /** Get built-in tool names for a type (case-insensitive). */
180
+ export function getToolNamesForType(type: string): string[] {
196
181
  const key = resolveKey(type);
197
182
  const raw = key ? agents.get(key) : undefined;
198
183
  const config = raw?.enabled !== false ? raw : undefined;
199
- const toolNames = config?.builtinToolNames?.length ? config.builtinToolNames : BUILTIN_TOOL_NAMES;
200
- return toolNames.filter((n) => n in TOOL_FACTORIES).map((n) => TOOL_FACTORIES[n](cwd));
184
+ // `undefined` (definition omitted the field) all built-ins; an explicit `[]`
185
+ // (`tools: none` or a `tools:` with only `ext:` entries) zero built-ins.
186
+ return config?.builtinToolNames ?? [...BUILTIN_TOOL_NAMES];
201
187
  }
202
188
 
203
189
  /** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
@@ -206,6 +192,7 @@ export function getConfig(type: string): {
206
192
  description: string;
207
193
  builtinToolNames: string[];
208
194
  extensions: true | string[] | false;
195
+ excludeExtensions?: string[];
209
196
  skills: true | string[] | false;
210
197
  promptMode: "replace" | "append";
211
198
  } {
@@ -217,6 +204,7 @@ export function getConfig(type: string): {
217
204
  description: config.description,
218
205
  builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
219
206
  extensions: config.extensions,
207
+ excludeExtensions: config.excludeExtensions,
220
208
  skills: config.skills,
221
209
  promptMode: config.promptMode,
222
210
  };
@@ -230,6 +218,7 @@ export function getConfig(type: string): {
230
218
  description: gp.description,
231
219
  builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
232
220
  extensions: gp.extensions,
221
+ excludeExtensions: gp.excludeExtensions,
233
222
  skills: gp.skills,
234
223
  promptMode: gp.promptMode,
235
224
  };
@@ -9,6 +9,8 @@
9
9
  * error → { success: false, error: string }
10
10
  */
11
11
 
12
+ import { type ModelRegistry, resolveModel } from "./model-resolver.js";
13
+
12
14
  /** Minimal event bus interface needed by the RPC handlers. */
13
15
  export interface EventBus {
14
16
  on(event: string, handler: (data: unknown) => void): () => void;
@@ -29,18 +31,10 @@ export interface SpawnCapable {
29
31
  abort(id: string): boolean;
30
32
  }
31
33
 
32
- export interface SpawnRequest {
33
- requestId: string;
34
- type: string;
35
- prompt: string;
36
- options?: any;
37
- }
38
-
39
34
  export interface RpcDeps {
40
35
  events: EventBus;
41
36
  pi: unknown; // passed through to manager.spawn
42
37
  getCtx: () => unknown | undefined; // returns current ExtensionContext
43
- isSubagentSession?: (ctx: unknown) => boolean;
44
38
  manager: SpawnCapable;
45
39
  }
46
40
 
@@ -50,10 +44,6 @@ export interface RpcHandle {
50
44
  unsubStop: () => void;
51
45
  }
52
46
 
53
- function emitReply(events: EventBus, channel: string, requestId: string, reply: RpcReply<unknown>): void {
54
- events.emit(`${channel}:reply:${requestId}`, reply);
55
- }
56
-
57
47
  /**
58
48
  * Wire a single RPC handler: listen on `channel`, run `fn(params)`,
59
49
  * emit the reply envelope on `channel:reply:${requestId}`.
@@ -83,47 +73,63 @@ function handleRpc<P extends { requestId: string }>(
83
73
  * Returns unsub functions for cleanup.
84
74
  */
85
75
  export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
86
- const { events, pi, getCtx, isSubagentSession, manager } = deps;
76
+ const { events, pi, getCtx, manager } = deps;
87
77
 
88
78
  const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
89
79
  return { version: PROTOCOL_VERSION };
90
80
  });
91
81
 
92
- const unsubSpawn = events.on("subagents:rpc:spawn", (raw: unknown) => {
93
- const params = raw as SpawnRequest;
94
- const ctx = getCtx();
95
- if (!ctx) {
96
- emitReply(events, "subagents:rpc:spawn", params.requestId, { success: false, error: "No active session" });
97
- return;
98
- }
99
- if (isSubagentSession?.(ctx)) {
100
- emitReply(events, "subagents:rpc:spawn", params.requestId, {
101
- success: false,
102
- error: "Cannot spawn subagents from a subagent session context",
103
- });
104
- return;
105
- }
82
+ const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
83
+ events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
84
+ const ctx = getCtx();
85
+ if (!ctx) throw new Error("No active session");
106
86
 
107
- queueMicrotask(() => {
108
- try {
109
- const rawOpts = params.options ?? {};
110
- const spawnOpts = { ...rawOpts, isBackground: rawOpts.isBackground ?? rawOpts.run_in_background ?? true };
111
- const id = manager.spawn(pi, ctx, params.type, params.prompt, spawnOpts);
112
- events.emit("subagents:created", {
113
- id,
114
- type: params.type,
115
- description: params.options?.description ?? params.type,
116
- isBackground: params.options?.run_in_background ?? true,
117
- });
118
- emitReply(events, "subagents:rpc:spawn", params.requestId, { success: true, data: { id } });
119
- } catch (err: any) {
120
- emitReply(events, "subagents:rpc:spawn", params.requestId, {
121
- success: false,
122
- error: err?.message ?? String(err),
123
- });
87
+ // Cross-extension RPC callers (e.g. pi-tasks TaskExecute) naturally
88
+ // forward serializable values, so options.model can be a string like
89
+ // "openai-codex/gpt-5.5". Resolve it to a real Model instance here
90
+ // same pattern the scheduler path already uses so the spawned
91
+ // agent's auth lookup doesn't crash with "No API key found for
92
+ // undefined".
93
+ let normalizedOptions = options ?? {};
94
+ if (typeof normalizedOptions.model === "string") {
95
+ const registry = (ctx as { modelRegistry?: ModelRegistry }).modelRegistry;
96
+ if (!registry) {
97
+ throw new Error(
98
+ `Model override "${normalizedOptions.model}" provided but ctx.modelRegistry is unavailable`,
99
+ );
100
+ }
101
+ const resolved = resolveModel(normalizedOptions.model, registry);
102
+ if (typeof resolved === "string") {
103
+ // resolveModel returns a human-readable error string when the
104
+ // input doesn't match any available model. Surface it instead of
105
+ // silently falling back so the caller sees the auth/typo issue.
106
+ throw new Error(resolved);
107
+ }
108
+ normalizedOptions = { ...normalizedOptions, model: resolved };
124
109
  }
125
- });
126
- });
110
+
111
+ // Cross-extension RPC callers (e.g. pi-pi orchestrator planners/
112
+ // reviewers) forward `run_in_background`; map it to the manager's
113
+ // `isBackground` flag so background RPC spawns are queued/tracked and
114
+ // stay visible in the widget rather than being treated as foreground.
115
+ if (normalizedOptions.isBackground === undefined && normalizedOptions.run_in_background !== undefined) {
116
+ normalizedOptions = { ...normalizedOptions, isBackground: normalizedOptions.run_in_background };
117
+ }
118
+
119
+ const id = manager.spawn(pi, ctx, type, prompt, normalizedOptions);
120
+ // Announce the spawn so orchestrating extensions can wire the widget's
121
+ // UICtx and track the agent. RPC spawns happen outside an LLM turn, so
122
+ // without this event the above-editor widget never receives a UICtx and
123
+ // stays blank while orchestrator subagents run.
124
+ events.emit("subagents:created", {
125
+ id,
126
+ type,
127
+ description: normalizedOptions.description ?? type,
128
+ isBackground: normalizedOptions.isBackground ?? true,
129
+ });
130
+ return { id };
131
+ },
132
+ );
127
133
 
128
134
  const unsubStop = handleRpc<{ requestId: string; agentId: string }>(
129
135
  events, "subagents:rpc:stop", ({ agentId }) => {
@@ -1,11 +1,10 @@
1
1
  /**
2
- * custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
2
+ * custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
3
3
  */
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync } from "node:fs";
6
- import { homedir } from "node:os";
7
6
  import { basename, join } from "node:path";
8
- import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
7
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
9
8
  import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
10
9
  import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
11
10
 
@@ -13,13 +12,13 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
13
12
  * Scan for custom agent .md files from multiple locations.
14
13
  * Discovery hierarchy (higher priority wins):
15
14
  * 1. Project: <cwd>/.pi/agents/*.md
16
- * 2. Global: ~/.pi/agent/agents/*.md
15
+ * 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
17
16
  *
18
17
  * Project-level agents override global ones with the same name.
19
18
  * Any name is allowed — names matching defaults (e.g. "Explore") override them.
20
19
  */
21
20
  export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
22
- const globalDir = join(homedir(), ".pi", "agent", "agents");
21
+ const globalDir = join(getAgentDir(), "agents");
23
22
  const projectDir = join(cwd, ".pi", "agents");
24
23
 
25
24
  const agents = new Map<string, AgentConfig>();
@@ -51,17 +50,23 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
51
50
 
52
51
  const { frontmatter: fm, body } = parseFrontmatter<Record<string, unknown>>(content);
53
52
 
53
+ const { builtinToolNames, extSelectors } = parseToolsField(fm.tools);
54
+
54
55
  agents.set(name, {
55
56
  name,
56
57
  displayName: str(fm.display_name),
57
58
  description: str(fm.description) ?? name,
58
- builtinToolNames: csvList(fm.tools, BUILTIN_TOOL_NAMES),
59
+ builtinToolNames,
60
+ extSelectors,
59
61
  disallowedTools: csvListOptional(fm.disallowed_tools),
60
62
  extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
63
+ excludeExtensions: csvListOptional(fm.exclude_extensions),
61
64
  skills: inheritField(fm.skills ?? fm.inherit_skills),
62
65
  model: str(fm.model),
63
66
  thinking: str(fm.thinking) as ThinkingLevel | undefined,
64
67
  maxTurns: nonNegativeInt(fm.max_turns),
68
+ persistSession: fm.persist_session != null ? fm.persist_session === true : undefined,
69
+ sessionDir: str(fm.session_dir),
65
70
  systemPrompt: body.trim(),
66
71
  promptMode: fm.prompt_mode === "append" ? "append" : "replace",
67
72
  inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
@@ -108,6 +113,25 @@ function csvList(val: unknown, defaults: string[]): string[] {
108
113
  return parseCsvField(val) ?? [];
109
114
  }
110
115
 
116
+ /**
117
+ * Partition the `tools:` CSV into the built-in tool allowlist and raw `ext:` selectors.
118
+ * `*` (and the case-insensitive alias `all`, for `tools: all`) expands to all
119
+ * built-ins; plain entries are built-in names; `ext:` entries are extension-tool
120
+ * selectors parsed later by the runner. omitted → all built-ins, no selectors.
121
+ * `tools:` present with only `ext:` entries → zero built-ins (use `*`).
122
+ */
123
+ function parseToolsField(val: unknown): { builtinToolNames: string[]; extSelectors: string[] | undefined } {
124
+ const entries = csvList(val, BUILTIN_TOOL_NAMES);
125
+ const isWildcard = (e: string) => e === "*" || e.toLowerCase() === "all";
126
+ const hasWildcard = entries.some(isWildcard);
127
+ const plain = entries.filter(e => !isWildcard(e) && !e.startsWith("ext:"));
128
+ const extEntries = entries.filter(e => e.startsWith("ext:"));
129
+ return {
130
+ builtinToolNames: hasWildcard ? [...new Set([...BUILTIN_TOOL_NAMES, ...plain])] : plain,
131
+ extSelectors: extEntries.length > 0 ? extEntries : undefined,
132
+ };
133
+ }
134
+
111
135
  /**
112
136
  * Parse an optional comma-separated list field.
113
137
  * omitted → undefined; "none"/empty → undefined; csv → listed items.
@@ -14,15 +14,14 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
14
14
  {
15
15
  name: "general-purpose",
16
16
  displayName: "Agent",
17
- description: "General-purpose agent for complex, multi-step tasks",
17
+ description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
+ // inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
20
+ // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
19
21
  extensions: true,
20
22
  skills: true,
21
23
  systemPrompt: "",
22
24
  promptMode: "append",
23
- inheritContext: false,
24
- runInBackground: false,
25
- isolated: false,
26
25
  isDefault: true,
27
26
  },
28
27
  ],
@@ -31,11 +30,14 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
31
30
  {
32
31
  name: "Explore",
33
32
  displayName: "Explore",
34
- description: "Fast codebase exploration agent (read-only)",
33
+ description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions.",
35
34
  builtinToolNames: READ_ONLY_TOOLS,
36
35
  extensions: true,
37
36
  skills: true,
38
- model: "anthropic/claude-haiku-4-5-20251001",
37
+ // Fast/cheap model for read-only search. Provider-preferred but resilient:
38
+ // resolveModel matches this fuzzily (date-stamp optional) and falls back to
39
+ // the same model under another provider if anthropic doesn't expose it.
40
+ model: "anthropic/claude-haiku-4-5",
39
41
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
40
42
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
41
43
  Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
@@ -49,32 +51,22 @@ You are STRICTLY PROHIBITED from:
49
51
  - Using redirect operators (>, >>, |) or heredocs to write to files
50
52
  - Running ANY commands that change system state
51
53
 
52
- # Tool routing pick the right tool for the question
54
+ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
53
55
 
54
- Discovery (don't know where to look):
55
- - cbm_search: natural-language search across all symbols (best first step)
56
- - cbm_search_code: graph-augmented grep finds text, deduplicates into containing functions
57
- - grep: fast text/regex search for known strings
58
-
59
- Structural patterns (find code shapes, not names):
60
- - ast_search: AST-aware pattern matching (e.g. 'if err != nil { $$$ }', 'type $N interface { $$$ }')
61
-
62
- File navigation:
63
- - find: locate files by name/glob pattern
64
- - read: read file contents
65
- - ls: list directory contents
66
-
67
- Use Bash ONLY for read-only operations: git status, git log, git diff.
68
- Make independent tool calls in parallel for efficiency.
56
+ # Tool Usage
57
+ - Use the find tool for file pattern matching (NOT the bash find command)
58
+ - Use the grep tool for content search (NOT bash grep/rg command)
59
+ - Use the read tool for reading files (NOT bash cat/head/tail)
60
+ - Use Bash ONLY for read-only operations
61
+ - Make independent tool calls in parallel for efficiency
62
+ - Adapt search approach based on thoroughness level specified
69
63
 
70
64
  # Output
71
65
  - Use absolute file paths in all references
66
+ - Report findings as regular messages
72
67
  - Do not use emojis
73
68
  - Be thorough and precise`,
74
69
  promptMode: "replace",
75
- inheritContext: false,
76
- runInBackground: false,
77
- isolated: false,
78
70
  isDefault: true,
79
71
  },
80
72
  ],
@@ -83,7 +75,7 @@ Make independent tool calls in parallel for efficiency.
83
75
  {
84
76
  name: "Plan",
85
77
  displayName: "Plan",
86
- description: "Software architect for implementation planning (read-only)",
78
+ description: "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.",
87
79
  builtinToolNames: READ_ONLY_TOOLS,
88
80
  extensions: true,
89
81
  skills: true,
@@ -101,22 +93,9 @@ You are STRICTLY PROHIBITED from:
101
93
  - Using redirect operators (>, >>, |) or heredocs to write to files
102
94
  - Running ANY commands that change system state
103
95
 
104
- # Tool routing — understand the codebase before planning
105
-
106
- Architecture overview:
107
- - cbm_search: natural-language search for relevant symbols
108
- - cbm_search_code: graph-augmented grep — deduplicates into containing functions
109
-
110
- Detailed understanding:
111
- - grep: fast text/regex search for known strings
112
- - read: read file contents
113
- - find: locate files by name/glob pattern
114
-
115
- Use Bash ONLY for read-only operations: git status, git log, git diff.
116
-
117
96
  # Planning Process
118
97
  1. Understand requirements
119
- 2. Explore thoroughly (search for patterns, understand architecture)
98
+ 2. Explore thoroughly (read files, find patterns, understand architecture)
120
99
  3. Design solution based on your assigned perspective
121
100
  4. Detail the plan with step-by-step implementation strategy
122
101
 
@@ -126,6 +105,12 @@ Use Bash ONLY for read-only operations: git status, git log, git diff.
126
105
  - Anticipate potential challenges
127
106
  - Follow existing patterns where appropriate
128
107
 
108
+ # Tool Usage
109
+ - Use the find tool for file pattern matching (NOT the bash find command)
110
+ - Use the grep tool for content search (NOT bash grep/rg command)
111
+ - Use the read tool for reading files (NOT bash cat/head/tail)
112
+ - Use Bash ONLY for read-only operations
113
+
129
114
  # Output Format
130
115
  - Use absolute file paths
131
116
  - Do not use emojis
@@ -135,9 +120,6 @@ Use Bash ONLY for read-only operations: git status, git log, git diff.
135
120
  List 3-5 files most critical for implementing this plan:
136
121
  - /absolute/path/to/file.ts - [Brief reason]`,
137
122
  promptMode: "replace",
138
- inheritContext: false,
139
- runInBackground: false,
140
- isolated: false,
141
123
  isDefault: true,
142
124
  },
143
125
  ],