@oh-my-pi/pi-coding-agent 2.3.1337 → 3.1.1337

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 (117) hide show
  1. package/CHANGELOG.md +72 -34
  2. package/README.md +100 -100
  3. package/docs/compaction.md +8 -8
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +8 -8
  6. package/docs/extension-loading.md +58 -58
  7. package/docs/hooks.md +11 -11
  8. package/docs/rpc.md +4 -4
  9. package/docs/sdk.md +14 -14
  10. package/docs/session-tree-plan.md +1 -1
  11. package/docs/session.md +2 -2
  12. package/docs/skills.md +16 -16
  13. package/docs/theme.md +9 -9
  14. package/docs/tui.md +1 -1
  15. package/examples/README.md +1 -1
  16. package/examples/custom-tools/README.md +4 -4
  17. package/examples/custom-tools/subagent/README.md +13 -13
  18. package/examples/custom-tools/subagent/agents.ts +2 -2
  19. package/examples/custom-tools/subagent/index.ts +5 -5
  20. package/examples/hooks/README.md +3 -3
  21. package/examples/hooks/auto-commit-on-exit.ts +1 -1
  22. package/examples/hooks/custom-compaction.ts +1 -1
  23. package/examples/sdk/01-minimal.ts +1 -1
  24. package/examples/sdk/04-skills.ts +1 -1
  25. package/examples/sdk/05-tools.ts +1 -1
  26. package/examples/sdk/08-slash-commands.ts +1 -1
  27. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  28. package/examples/sdk/README.md +2 -2
  29. package/package.json +13 -11
  30. package/src/capability/context-file.ts +40 -0
  31. package/src/capability/extension.ts +48 -0
  32. package/src/capability/hook.ts +40 -0
  33. package/src/capability/index.ts +616 -0
  34. package/src/capability/instruction.ts +37 -0
  35. package/src/capability/mcp.ts +52 -0
  36. package/src/capability/prompt.ts +35 -0
  37. package/src/capability/rule.ts +52 -0
  38. package/src/capability/settings.ts +35 -0
  39. package/src/capability/skill.ts +49 -0
  40. package/src/capability/slash-command.ts +40 -0
  41. package/src/capability/system-prompt.ts +35 -0
  42. package/src/capability/tool.ts +38 -0
  43. package/src/capability/types.ts +166 -0
  44. package/src/cli/args.ts +2 -2
  45. package/src/cli/plugin-cli.ts +24 -19
  46. package/src/cli/update-cli.ts +10 -10
  47. package/src/config.ts +290 -6
  48. package/src/core/auth-storage.ts +32 -9
  49. package/src/core/bash-executor.ts +1 -1
  50. package/src/core/custom-commands/loader.ts +44 -50
  51. package/src/core/custom-tools/index.ts +1 -0
  52. package/src/core/custom-tools/loader.ts +67 -69
  53. package/src/core/custom-tools/types.ts +10 -1
  54. package/src/core/hooks/loader.ts +13 -42
  55. package/src/core/index.ts +0 -1
  56. package/src/core/logger.ts +7 -7
  57. package/src/core/mcp/client.ts +1 -1
  58. package/src/core/mcp/config.ts +94 -146
  59. package/src/core/mcp/index.ts +0 -4
  60. package/src/core/mcp/loader.ts +26 -22
  61. package/src/core/mcp/manager.ts +18 -23
  62. package/src/core/mcp/tool-bridge.ts +9 -1
  63. package/src/core/mcp/types.ts +2 -0
  64. package/src/core/model-registry.ts +25 -8
  65. package/src/core/plugins/installer.ts +1 -1
  66. package/src/core/plugins/loader.ts +17 -11
  67. package/src/core/plugins/manager.ts +2 -2
  68. package/src/core/plugins/paths.ts +12 -7
  69. package/src/core/plugins/types.ts +3 -3
  70. package/src/core/sdk.ts +48 -27
  71. package/src/core/session-manager.ts +4 -4
  72. package/src/core/settings-manager.ts +45 -21
  73. package/src/core/skills.ts +208 -293
  74. package/src/core/slash-commands.ts +34 -165
  75. package/src/core/system-prompt.ts +58 -65
  76. package/src/core/timings.ts +2 -2
  77. package/src/core/tools/lsp/config.ts +38 -17
  78. package/src/core/tools/task/agents.ts +21 -0
  79. package/src/core/tools/task/artifacts.ts +1 -1
  80. package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
  81. package/src/core/tools/task/bundled-agents/task.md +1 -0
  82. package/src/core/tools/task/commands.ts +30 -107
  83. package/src/core/tools/task/discovery.ts +75 -66
  84. package/src/core/tools/task/executor.ts +25 -10
  85. package/src/core/tools/task/index.ts +35 -10
  86. package/src/core/tools/task/model-resolver.ts +27 -25
  87. package/src/core/tools/task/types.ts +6 -2
  88. package/src/core/tools/web-fetch.ts +3 -3
  89. package/src/core/tools/web-search/auth.ts +40 -34
  90. package/src/core/tools/web-search/index.ts +1 -1
  91. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  92. package/src/discovery/agents-md.ts +75 -0
  93. package/src/discovery/builtin.ts +646 -0
  94. package/src/discovery/claude.ts +623 -0
  95. package/src/discovery/cline.ts +102 -0
  96. package/src/discovery/codex.ts +571 -0
  97. package/src/discovery/cursor.ts +264 -0
  98. package/src/discovery/gemini.ts +368 -0
  99. package/src/discovery/github.ts +120 -0
  100. package/src/discovery/helpers.test.ts +127 -0
  101. package/src/discovery/helpers.ts +249 -0
  102. package/src/discovery/index.ts +84 -0
  103. package/src/discovery/mcp-json.ts +127 -0
  104. package/src/discovery/vscode.ts +99 -0
  105. package/src/discovery/windsurf.ts +216 -0
  106. package/src/main.ts +14 -13
  107. package/src/migrations.ts +24 -3
  108. package/src/modes/interactive/components/hook-editor.ts +1 -1
  109. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  110. package/src/modes/interactive/components/settings-defs.ts +38 -2
  111. package/src/modes/interactive/components/settings-selector.ts +1 -0
  112. package/src/modes/interactive/components/welcome.ts +2 -2
  113. package/src/modes/interactive/interactive-mode.ts +233 -16
  114. package/src/modes/interactive/theme/theme-schema.json +1 -1
  115. package/src/utils/clipboard.ts +1 -1
  116. package/src/utils/shell-snapshot.ts +2 -2
  117. package/src/utils/shell.ts +7 -7
@@ -4,9 +4,9 @@
4
4
  * Commands are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
6
 
7
- import * as fs from "node:fs";
8
- import * as os from "node:os";
9
7
  import * as path from "node:path";
8
+ import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
9
+ import { loadSync } from "../../../discovery";
10
10
 
11
11
  // Embed command markdown files at build time
12
12
  import architectPlanMd from "./bundled-commands/architect-plan.md" with { type: "text" };
@@ -61,84 +61,6 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
61
61
  return { frontmatter, body };
62
62
  }
63
63
 
64
- /**
65
- * Load commands from a directory (for user/project commands).
66
- */
67
- function loadCommandsFromDir(dir: string, source: "user" | "project"): WorkflowCommand[] {
68
- const commands: WorkflowCommand[] = [];
69
-
70
- if (!fs.existsSync(dir)) {
71
- return commands;
72
- }
73
-
74
- let entries: fs.Dirent[];
75
- try {
76
- entries = fs.readdirSync(dir, { withFileTypes: true });
77
- } catch {
78
- return commands;
79
- }
80
-
81
- for (const entry of entries) {
82
- if (!entry.name.endsWith(".md")) continue;
83
-
84
- const filePath = path.join(dir, entry.name);
85
-
86
- try {
87
- if (!fs.statSync(filePath).isFile()) continue;
88
- } catch {
89
- continue;
90
- }
91
-
92
- let content: string;
93
- try {
94
- content = fs.readFileSync(filePath, "utf-8");
95
- } catch {
96
- continue;
97
- }
98
-
99
- const { frontmatter, body } = parseFrontmatter(content);
100
-
101
- // Name is filename without extension
102
- const name = entry.name.replace(/\.md$/, "");
103
-
104
- commands.push({
105
- name,
106
- description: frontmatter.description || "",
107
- instructions: body,
108
- source,
109
- filePath,
110
- });
111
- }
112
-
113
- return commands;
114
- }
115
-
116
- /**
117
- * Check if path is a directory.
118
- */
119
- function isDirectory(p: string): boolean {
120
- try {
121
- return fs.statSync(p).isDirectory();
122
- } catch {
123
- return false;
124
- }
125
- }
126
-
127
- /**
128
- * Find nearest directory by walking up from cwd.
129
- */
130
- function findNearestDir(cwd: string, relPath: string): string | null {
131
- let currentDir = cwd;
132
- while (true) {
133
- const candidate = path.join(currentDir, relPath);
134
- if (isDirectory(candidate)) return candidate;
135
-
136
- const parentDir = path.dirname(currentDir);
137
- if (parentDir === currentDir) return null;
138
- currentDir = parentDir;
139
- }
140
- }
141
-
142
64
  /** Cache for bundled commands */
143
65
  let bundledCommandsCache: WorkflowCommand[] | null = null;
144
66
 
@@ -172,43 +94,44 @@ export function loadBundledCommands(): WorkflowCommand[] {
172
94
  /**
173
95
  * Discover all available commands.
174
96
  *
175
- * Precedence: project > user > bundled
97
+ * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
176
98
  */
177
99
  export function discoverCommands(cwd: string): WorkflowCommand[] {
178
- const commandMap = new Map<string, WorkflowCommand>();
100
+ const resolvedCwd = path.resolve(cwd);
179
101
 
180
- // Bundled commands (lowest priority)
181
- for (const cmd of loadBundledCommands()) {
182
- commandMap.set(cmd.name, cmd);
183
- }
102
+ // Load slash commands from capability API
103
+ const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
104
+
105
+ const commands: WorkflowCommand[] = [];
106
+ const seen = new Set<string>();
184
107
 
185
- // User commands
186
- const userPiDir = path.join(os.homedir(), ".pi", "agent", "commands");
187
- const userClaudeDir = path.join(os.homedir(), ".claude", "commands");
108
+ // Convert SlashCommand to WorkflowCommand format
109
+ for (const cmd of result.items) {
110
+ if (seen.has(cmd.name)) continue;
188
111
 
189
- for (const cmd of loadCommandsFromDir(userClaudeDir, "user")) {
190
- commandMap.set(cmd.name, cmd);
191
- }
192
- for (const cmd of loadCommandsFromDir(userPiDir, "user")) {
193
- commandMap.set(cmd.name, cmd);
194
- }
112
+ const { frontmatter, body } = parseFrontmatter(cmd.content);
195
113
 
196
- // Project commands (highest priority)
197
- const projectPiDir = findNearestDir(cwd, ".pi/commands");
198
- const projectClaudeDir = findNearestDir(cwd, ".claude/commands");
114
+ // Map capability levels to WorkflowCommand source
115
+ const source: "bundled" | "user" | "project" = cmd.level === "native" ? "bundled" : cmd.level;
199
116
 
200
- if (projectClaudeDir) {
201
- for (const cmd of loadCommandsFromDir(projectClaudeDir, "project")) {
202
- commandMap.set(cmd.name, cmd);
203
- }
117
+ commands.push({
118
+ name: cmd.name,
119
+ description: frontmatter.description || "",
120
+ instructions: body,
121
+ source,
122
+ filePath: cmd.path,
123
+ });
124
+ seen.add(cmd.name);
204
125
  }
205
- if (projectPiDir) {
206
- for (const cmd of loadCommandsFromDir(projectPiDir, "project")) {
207
- commandMap.set(cmd.name, cmd);
208
- }
126
+
127
+ // Add bundled commands if not already present
128
+ for (const cmd of loadBundledCommands()) {
129
+ if (seen.has(cmd.name)) continue;
130
+ commands.push(cmd);
131
+ seen.add(cmd.name);
209
132
  }
210
133
 
211
- return Array.from(commandMap.values());
134
+ return commands;
212
135
  }
213
136
 
214
137
  /**
@@ -2,17 +2,19 @@
2
2
  * Agent discovery from filesystem.
3
3
  *
4
4
  * Discovers agent definitions from:
5
- * - ~/.pi/agent/agents/*.md (user-level, primary)
6
- * - ~/.claude/agents/*.md (user-level, fallback)
7
- * - .pi/agents/*.md (project-level, primary)
8
- * - .claude/agents/*.md (project-level, fallback)
5
+ * - ~/.omp/agent/agents/*.md (user-level, primary)
6
+ * - ~/.pi/agent/agents/*.md (user-level, legacy)
7
+ * - ~/.claude/agents/*.md (user-level, legacy)
8
+ * - .omp/agents/*.md (project-level, primary)
9
+ * - .pi/agents/*.md (project-level, legacy)
10
+ * - .claude/agents/*.md (project-level, legacy)
9
11
  *
10
12
  * Agent files use markdown with YAML frontmatter.
11
13
  */
12
14
 
13
15
  import * as fs from "node:fs";
14
- import * as os from "node:os";
15
16
  import * as path from "node:path";
17
+ import { findAllNearestProjectConfigDirs, getConfigDirs } from "../../../config";
16
18
  import { loadBundledAgents } from "./agents";
17
19
  import type { AgentDefinition, AgentSource } from "./types";
18
20
 
@@ -76,7 +78,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
76
78
  for (const entry of entries) {
77
79
  if (!entry.name.endsWith(".md")) continue;
78
80
 
79
- const filePath = path.join(dir, entry.name);
81
+ const filePath = path.resolve(dir, entry.name);
80
82
 
81
83
  // Handle both regular files and symlinks
82
84
  try {
@@ -104,6 +106,26 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
104
106
  .map((t) => t.trim())
105
107
  .filter(Boolean);
106
108
 
109
+ // Parse spawns field
110
+ let spawns: string[] | "*" | undefined;
111
+ if (frontmatter.spawns !== undefined) {
112
+ const spawnsRaw = frontmatter.spawns.trim();
113
+ if (spawnsRaw === "*") {
114
+ spawns = "*";
115
+ } else if (spawnsRaw) {
116
+ spawns = spawnsRaw
117
+ .split(",")
118
+ .map((s) => s.trim())
119
+ .filter(Boolean);
120
+ if (spawns.length === 0) spawns = undefined;
121
+ }
122
+ }
123
+
124
+ // Backward compat: infer spawns: "*" when tools includes "task"
125
+ if (spawns === undefined && tools?.includes("task")) {
126
+ spawns = "*";
127
+ }
128
+
107
129
  const recursive =
108
130
  frontmatter.recursive === undefined
109
131
  ? undefined
@@ -113,6 +135,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
113
135
  name: frontmatter.name,
114
136
  description: frontmatter.description,
115
137
  tools: tools && tools.length > 0 ? tools : undefined,
138
+ spawns,
116
139
  model: frontmatter.model,
117
140
  recursive,
118
141
  systemPrompt: body,
@@ -124,80 +147,66 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
124
147
  return agents;
125
148
  }
126
149
 
127
- /**
128
- * Check if path is a directory.
129
- */
130
- function isDirectory(p: string): boolean {
131
- try {
132
- return fs.statSync(p).isDirectory();
133
- } catch {
134
- return false;
135
- }
136
- }
137
-
138
- /**
139
- * Find nearest directory by walking up from cwd.
140
- */
141
- function findNearestDir(cwd: string, relPath: string): string | null {
142
- let currentDir = cwd;
143
- while (true) {
144
- const candidate = path.join(currentDir, relPath);
145
- if (isDirectory(candidate)) return candidate;
146
-
147
- const parentDir = path.dirname(currentDir);
148
- if (parentDir === currentDir) return null;
149
- currentDir = parentDir;
150
- }
151
- }
152
-
153
150
  /**
154
151
  * Discover agents from filesystem and merge with bundled agents.
155
152
  *
156
- * Precedence (highest wins): project > user > bundled
157
- * Within each level: .pi > .claude
153
+ * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
158
154
  *
159
155
  * @param cwd - Current working directory for project agent discovery
160
156
  */
161
157
  export function discoverAgents(cwd: string): DiscoveryResult {
162
- // Primary directories (.pi)
163
- const userPiDir = path.join(os.homedir(), ".pi", "agent", "agents");
164
- const projectPiDir = findNearestDir(cwd, ".pi/agents");
165
-
166
- // Fallback directories (.claude)
167
- const userClaudeDir = path.join(os.homedir(), ".claude", "agents");
168
- const projectClaudeDir = findNearestDir(cwd, ".claude/agents");
169
-
170
- const agentMap = new Map<string, AgentDefinition>();
171
-
172
- // 1. Bundled agents (lowest priority)
173
- for (const agent of loadBundledAgents()) {
174
- agentMap.set(agent.name, agent);
158
+ const resolvedCwd = path.resolve(cwd);
159
+ const agentSources = Array.from(new Set(getConfigDirs("", { project: false }).map((entry) => entry.source)));
160
+
161
+ // Get user directories (priority order: .omp, .pi, .claude, ...)
162
+ const userDirs = getConfigDirs("agents", { project: false })
163
+ .filter((entry) => agentSources.includes(entry.source))
164
+ .map((entry) => ({
165
+ ...entry,
166
+ path: path.resolve(entry.path),
167
+ }));
168
+
169
+ // Get project directories by walking up from cwd (priority order)
170
+ const projectDirs = findAllNearestProjectConfigDirs("agents", resolvedCwd)
171
+ .filter((entry) => agentSources.includes(entry.source))
172
+ .map((entry) => ({
173
+ ...entry,
174
+ path: path.resolve(entry.path),
175
+ }));
176
+
177
+ const orderedSources = agentSources.filter(
178
+ (source) =>
179
+ userDirs.some((entry) => entry.source === source) || projectDirs.some((entry) => entry.source === source),
180
+ );
181
+
182
+ const orderedDirs: Array<{ dir: string; source: AgentSource }> = [];
183
+ for (const source of orderedSources) {
184
+ const project = projectDirs.find((entry) => entry.source === source);
185
+ if (project) orderedDirs.push({ dir: project.path, source: "project" });
186
+ const user = userDirs.find((entry) => entry.source === source);
187
+ if (user) orderedDirs.push({ dir: user.path, source: "user" });
175
188
  }
176
189
 
177
- // 2. User agents (.claude then .pi - .pi overrides .claude)
178
- for (const agent of loadAgentsFromDir(userClaudeDir, "user")) {
179
- agentMap.set(agent.name, agent);
180
- }
181
- for (const agent of loadAgentsFromDir(userPiDir, "user")) {
182
- agentMap.set(agent.name, agent);
183
- }
190
+ const agents: AgentDefinition[] = [];
191
+ const seen = new Set<string>();
184
192
 
185
- // 3. Project agents (highest priority - .claude then .pi)
186
- if (projectClaudeDir) {
187
- for (const agent of loadAgentsFromDir(projectClaudeDir, "project")) {
188
- agentMap.set(agent.name, agent);
193
+ for (const { dir, source } of orderedDirs) {
194
+ for (const agent of loadAgentsFromDir(dir, source)) {
195
+ if (seen.has(agent.name)) continue;
196
+ agents.push(agent);
197
+ seen.add(agent.name);
189
198
  }
190
199
  }
191
- if (projectPiDir) {
192
- for (const agent of loadAgentsFromDir(projectPiDir, "project")) {
193
- agentMap.set(agent.name, agent);
194
- }
200
+
201
+ for (const agent of loadBundledAgents()) {
202
+ if (seen.has(agent.name)) continue;
203
+ agents.push(agent);
204
+ seen.add(agent.name);
195
205
  }
196
206
 
197
- return {
198
- agents: Array.from(agentMap.values()),
199
- projectAgentsDir: projectPiDir,
200
- };
207
+ const projectAgentsDir = projectDirs.length > 0 ? projectDirs[0].path : null;
208
+
209
+ return { agents, projectAgentsDir };
201
210
  }
202
211
 
203
212
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Subprocess execution for subagents.
3
3
  *
4
- * Spawns `pi` in JSON mode to execute tasks with isolated context.
4
+ * Spawns `omp` in JSON mode to execute tasks with isolated context.
5
5
  * Parses JSON events for progress tracking.
6
6
  */
7
7
 
@@ -18,15 +18,16 @@ import {
18
18
  type AgentProgress,
19
19
  MAX_OUTPUT_BYTES,
20
20
  MAX_OUTPUT_LINES,
21
- PI_BLOCKED_AGENT_ENV,
21
+ OMP_BLOCKED_AGENT_ENV,
22
+ OMP_SPAWNS_ENV,
22
23
  type SingleResult,
23
24
  } from "./types";
24
25
 
25
- /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
26
- const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
26
+ /** omp command: 'omp.cmd' on Windows, 'omp' elsewhere */
27
+ const OMP_CMD = process.platform === "win32" ? "omp.cmd" : "omp";
27
28
 
28
29
  /** Windows shell option for spawn */
29
- const PI_SHELL_OPT = process.platform === "win32";
30
+ const OMP_SHELL_OPT = process.platform === "win32";
30
31
 
31
32
  /** Options for subprocess execution */
32
33
  export interface ExecutorOptions {
@@ -143,7 +144,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
143
144
  const tempDir = os.tmpdir();
144
145
  const promptFile = path.join(
145
146
  tempDir,
146
- `pi-agent-${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
147
+ `omp-agent-${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
147
148
  );
148
149
 
149
150
  try {
@@ -193,7 +194,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
193
194
 
194
195
  // Add tools if specified
195
196
  if (agent.tools && agent.tools.length > 0) {
196
- args.push("--tools", agent.tools.join(","));
197
+ let toolList = agent.tools;
198
+ // Auto-include task tool if spawns defined but task not in tools
199
+ if (agent.spawns !== undefined && !toolList.includes("task")) {
200
+ toolList = [...toolList, "task"];
201
+ }
202
+ args.push("--tools", toolList.join(","));
197
203
  }
198
204
 
199
205
  // Resolve and add model
@@ -217,14 +223,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
217
223
  // Set up environment - block same-agent recursion unless explicitly recursive
218
224
  const env = { ...process.env };
219
225
  if (!agent.recursive) {
220
- env[PI_BLOCKED_AGENT_ENV] = agent.name;
226
+ env[OMP_BLOCKED_AGENT_ENV] = agent.name;
227
+ }
228
+
229
+ // Propagate spawn restrictions to subprocess
230
+ if (agent.spawns === undefined) {
231
+ env[OMP_SPAWNS_ENV] = ""; // No spawns = deny all
232
+ } else if (agent.spawns === "*") {
233
+ env[OMP_SPAWNS_ENV] = "*";
234
+ } else {
235
+ env[OMP_SPAWNS_ENV] = agent.spawns.join(",");
221
236
  }
222
237
 
223
238
  // Spawn subprocess
224
- const proc = spawn(PI_CMD, args, {
239
+ const proc = spawn(OMP_CMD, args, {
225
240
  cwd,
226
241
  stdio: ["ignore", "pipe", "pipe"],
227
- shell: PI_SHELL_OPT,
242
+ shell: OMP_SHELL_OPT,
228
243
  env,
229
244
  });
230
245
 
@@ -2,9 +2,9 @@
2
2
  * Task tool - Delegate tasks to specialized agents.
3
3
  *
4
4
  * Discovers agent definitions from:
5
- * - Bundled agents (shipped with pi-coding-agent)
6
- * - ~/.pi/agent/agents/*.md (user-level)
7
- * - .pi/agents/*.md (project-level)
5
+ * - Bundled agents (shipped with omp-coding-agent)
6
+ * - ~/.omp/agent/agents/*.md (user-level)
7
+ * - .omp/agents/*.md (project-level)
8
8
  *
9
9
  * Supports:
10
10
  * - Single agent execution
@@ -25,8 +25,9 @@ import {
25
25
  MAX_AGENTS_IN_DESCRIPTION,
26
26
  MAX_CONCURRENCY,
27
27
  MAX_PARALLEL_TASKS,
28
- PI_BLOCKED_AGENT_ENV,
29
- PI_NO_SUBAGENTS_ENV,
28
+ OMP_BLOCKED_AGENT_ENV,
29
+ OMP_NO_SUBAGENTS_ENV,
30
+ OMP_SPAWNS_ENV,
30
31
  type TaskToolDetails,
31
32
  taskSchema,
32
33
  } from "./types";
@@ -122,13 +123,13 @@ function buildDescription(cwd: string): string {
122
123
  `- tasks: Array of {agent, task, model?} - tasks to run in parallel (max ${MAX_PARALLEL_TASKS}, ${MAX_CONCURRENCY} concurrent)`,
123
124
  );
124
125
  lines.push(
125
- ' - model: (optional) Override the agent\'s default model with fuzzy matching (e.g., "sonnet", "codex", "5.2"). Supports comma-separated fallbacks: "gpt, opus" tries gpt first, then opus. Use "default" for pi\'s default model',
126
+ ' - model: (optional) Override the agent\'s default model with fuzzy matching (e.g., "sonnet", "codex", "5.2"). Supports comma-separated fallbacks: "gpt, opus" tries gpt first, then opus. Use "default" for omp\'s default model',
126
127
  );
127
128
  lines.push(
128
129
  "- context: (optional) Shared context string prepended to all task prompts - use this to avoid repeating instructions",
129
130
  );
130
131
  lines.push("");
131
- lines.push("Results are always written to {tempdir}/pi-task-{runId}/task_{agent}_{index}.md");
132
+ lines.push("Results are always written to {tempdir}/omp-task-{runId}/task_{agent}_{index}.md");
132
133
  lines.push("");
133
134
  lines.push("Example usage:");
134
135
  lines.push("");
@@ -173,7 +174,7 @@ function buildDescription(cwd: string): string {
173
174
  lines.push(' { "agent": "explore", "task": "Search in tests/" }');
174
175
  lines.push(" ]");
175
176
  lines.push("}");
176
- lines.push("Results → {tempdir}/pi-task-{runId}/task_explore_*.md");
177
+ lines.push("Results → {tempdir}/omp-task-{runId}/task_explore_*.md");
177
178
  lines.push("</example>");
178
179
 
179
180
  return lines.join("\n");
@@ -189,7 +190,7 @@ export function createTaskTool(
189
190
  ): AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
190
191
  const hasOutputTool = options?.availableTools?.has("output") ?? false;
191
192
  // Check if subagents are completely inhibited (legacy recursion prevention)
192
- if (process.env[PI_NO_SUBAGENTS_ENV]) {
193
+ if (process.env[OMP_NO_SUBAGENTS_ENV]) {
193
194
  return {
194
195
  name: "task",
195
196
  label: "Task",
@@ -207,7 +208,7 @@ export function createTaskTool(
207
208
  }
208
209
 
209
210
  // Check for same-agent blocking (allows other agent types)
210
- const blockedAgent = process.env[PI_BLOCKED_AGENT_ENV];
211
+ const blockedAgent = process.env[OMP_BLOCKED_AGENT_ENV];
211
212
 
212
213
  return {
213
214
  name: "task",
@@ -321,6 +322,30 @@ export function createTaskTool(
321
322
  }
322
323
  }
323
324
 
325
+ // Check spawn restrictions from parent
326
+ const parentSpawns = process.env[OMP_SPAWNS_ENV];
327
+ const isSpawnAllowed = (agentName: string): boolean => {
328
+ if (parentSpawns === undefined) return true; // Root = allow all
329
+ if (parentSpawns === "") return false; // Empty = deny all
330
+ if (parentSpawns === "*") return true; // Wildcard = allow all
331
+ const allowed = new Set(parentSpawns.split(",").map((s) => s.trim()));
332
+ return allowed.has(agentName);
333
+ };
334
+
335
+ for (const task of tasks) {
336
+ if (!isSpawnAllowed(task.agent)) {
337
+ const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
338
+ return {
339
+ content: [{ type: "text", text: `Cannot spawn '${task.agent}'. Allowed: ${allowed}` }],
340
+ details: {
341
+ projectAgentsDir,
342
+ results: [],
343
+ totalDurationMs: Date.now() - startTime,
344
+ },
345
+ };
346
+ }
347
+ }
348
+
324
349
  // Initialize progress for all tasks
325
350
  for (let i = 0; i < tasks.length; i++) {
326
351
  const agentCfg = getAgent(agents, tasks[i].agent);
@@ -8,19 +8,18 @@
8
8
  * - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
9
9
  * - Comma fallback: "gpt, opus" → tries gpt first, then opus
10
10
  * - "default" → undefined (use system default)
11
- * - "pi/slow" → configured slow model from settings
11
+ * - "omp/slow" → configured slow model from settings
12
12
  */
13
13
 
14
14
  import { spawnSync } from "node:child_process";
15
- import { existsSync, readFileSync } from "node:fs";
16
- import { homedir } from "node:os";
17
- import { join } from "node:path";
15
+ import { type Settings, settingsCapability } from "../../../capability/settings";
16
+ import { loadSync } from "../../../discovery";
18
17
 
19
- /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
20
- const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
18
+ /** omp command: 'omp.cmd' on Windows, 'omp' elsewhere */
19
+ const OMP_CMD = process.platform === "win32" ? "omp.cmd" : "omp";
21
20
 
22
21
  /** Windows shell option for spawn/spawnSync */
23
- const PI_SHELL_OPT = process.platform === "win32";
22
+ const OMP_SHELL_OPT = process.platform === "win32";
24
23
 
25
24
  /** Cache for available models (provider/modelId format) */
26
25
  let cachedModels: string[] | null = null;
@@ -31,7 +30,7 @@ let cacheExpiry = 0;
31
30
  const CACHE_TTL_MS = 5 * 60 * 1000;
32
31
 
33
32
  /**
34
- * Get available models from `pi --list-models`.
33
+ * Get available models from `omp --list-models`.
35
34
  * Returns models in "provider/modelId" format.
36
35
  * Caches the result for performance.
37
36
  */
@@ -42,10 +41,10 @@ export function getAvailableModels(): string[] {
42
41
  }
43
42
 
44
43
  try {
45
- const result = spawnSync(PI_CMD, ["--list-models"], {
44
+ const result = spawnSync(OMP_CMD, ["--list-models"], {
46
45
  encoding: "utf-8",
47
46
  timeout: 5000,
48
- shell: PI_SHELL_OPT,
47
+ shell: OMP_SHELL_OPT,
49
48
  });
50
49
 
51
50
  if (result.status !== 0 || !result.stdout) {
@@ -83,26 +82,29 @@ export function clearModelCache(): void {
83
82
  }
84
83
 
85
84
  /**
86
- * Load model roles from settings file.
85
+ * Load model roles from settings files using capability API.
87
86
  */
88
87
  function loadModelRoles(): Record<string, string> {
89
- try {
90
- const settingsPath = join(homedir(), ".pi", "agent", "settings.json");
91
- if (!existsSync(settingsPath)) return {};
92
- const content = readFileSync(settingsPath, "utf-8");
93
- const settings = JSON.parse(content);
94
- return settings.modelRoles ?? {};
95
- } catch {
96
- return {};
88
+ const result = loadSync<Settings>(settingsCapability.id, { cwd: process.cwd() });
89
+
90
+ // Merge all settings, prioritizing first (highest priority)
91
+ let modelRoles: Record<string, string> = {};
92
+ for (const settings of result.items.reverse()) {
93
+ const roles = settings.data.modelRoles as Record<string, string> | undefined;
94
+ if (roles) {
95
+ modelRoles = { ...modelRoles, ...roles };
96
+ }
97
97
  }
98
+
99
+ return modelRoles;
98
100
  }
99
101
 
100
102
  /**
101
- * Resolve a pi/<role> alias to a model string.
103
+ * Resolve an omp/<role> alias to a model string.
102
104
  * Looks up the role in settings.modelRoles and returns the configured model.
103
105
  * Returns undefined if the role isn't configured.
104
106
  */
105
- function resolvePiAlias(role: string, availableModels: string[]): string | undefined {
107
+ function resolveOmpAlias(role: string, availableModels: string[]): string | undefined {
106
108
  const roles = loadModelRoles();
107
109
 
108
110
  // Look up role in settings (case-insensitive)
@@ -148,10 +150,10 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
148
150
  .filter(Boolean);
149
151
 
150
152
  for (const p of patterns) {
151
- // Handle pi/<role> aliases - looks up role in settings.modelRoles
152
- if (p.toLowerCase().startsWith("pi/")) {
153
- const role = p.slice(3); // Remove "pi/" prefix
154
- const resolved = resolvePiAlias(role, models);
153
+ // Handle omp/<role> aliases - looks up role in settings.modelRoles
154
+ if (p.toLowerCase().startsWith("omp/")) {
155
+ const role = p.slice(4); // Remove "omp/" prefix
156
+ const resolved = resolveOmpAlias(role, models);
155
157
  if (resolved) return resolved;
156
158
  continue; // Role not configured, try next pattern
157
159
  }
@@ -28,10 +28,13 @@ export const MAX_OUTPUT_LINES = 5000;
28
28
  export const MAX_AGENTS_IN_DESCRIPTION = 10;
29
29
 
30
30
  /** Environment variable to inhibit subagent spawning (legacy, still checked for backwards compat) */
31
- export const PI_NO_SUBAGENTS_ENV = "PI_NO_SUBAGENTS";
31
+ export const OMP_NO_SUBAGENTS_ENV = "OMP_NO_SUBAGENTS";
32
32
 
33
33
  /** Environment variable containing blocked agent name (self-recursion prevention) */
34
- export const PI_BLOCKED_AGENT_ENV = "PI_BLOCKED_AGENT";
34
+ export const OMP_BLOCKED_AGENT_ENV = "OMP_BLOCKED_AGENT";
35
+
36
+ /** Environment variable containing allowed spawn list (propagated to subprocesses) */
37
+ export const OMP_SPAWNS_ENV = "OMP_SPAWNS";
35
38
 
36
39
  /** Task tool parameters */
37
40
  export const taskSchema = Type.Object({
@@ -74,6 +77,7 @@ export interface AgentDefinition {
74
77
  description: string;
75
78
  systemPrompt: string;
76
79
  tools?: string[];
80
+ spawns?: string[] | "*";
77
81
  model?: string;
78
82
  recursive?: boolean;
79
83
  source: AgentSource;