@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.
- package/CHANGELOG.md +72 -34
- package/README.md +100 -100
- package/docs/compaction.md +8 -8
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +8 -8
- package/docs/extension-loading.md +58 -58
- package/docs/hooks.md +11 -11
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +14 -14
- package/docs/session-tree-plan.md +1 -1
- package/docs/session.md +2 -2
- package/docs/skills.md +16 -16
- package/docs/theme.md +9 -9
- package/docs/tui.md +1 -1
- package/examples/README.md +1 -1
- package/examples/custom-tools/README.md +4 -4
- package/examples/custom-tools/subagent/README.md +13 -13
- package/examples/custom-tools/subagent/agents.ts +2 -2
- package/examples/custom-tools/subagent/index.ts +5 -5
- package/examples/hooks/README.md +3 -3
- package/examples/hooks/auto-commit-on-exit.ts +1 -1
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +2 -2
- package/package.json +13 -11
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +52 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/plugin-cli.ts +24 -19
- package/src/cli/update-cli.ts +10 -10
- package/src/config.ts +290 -6
- package/src/core/auth-storage.ts +32 -9
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-commands/loader.ts +44 -50
- package/src/core/custom-tools/index.ts +1 -0
- package/src/core/custom-tools/loader.ts +67 -69
- package/src/core/custom-tools/types.ts +10 -1
- package/src/core/hooks/loader.ts +13 -42
- package/src/core/index.ts +0 -1
- package/src/core/logger.ts +7 -7
- package/src/core/mcp/client.ts +1 -1
- package/src/core/mcp/config.ts +94 -146
- package/src/core/mcp/index.ts +0 -4
- package/src/core/mcp/loader.ts +26 -22
- package/src/core/mcp/manager.ts +18 -23
- package/src/core/mcp/tool-bridge.ts +9 -1
- package/src/core/mcp/types.ts +2 -0
- package/src/core/model-registry.ts +25 -8
- package/src/core/plugins/installer.ts +1 -1
- package/src/core/plugins/loader.ts +17 -11
- package/src/core/plugins/manager.ts +2 -2
- package/src/core/plugins/paths.ts +12 -7
- package/src/core/plugins/types.ts +3 -3
- package/src/core/sdk.ts +48 -27
- package/src/core/session-manager.ts +4 -4
- package/src/core/settings-manager.ts +45 -21
- package/src/core/skills.ts +208 -293
- package/src/core/slash-commands.ts +34 -165
- package/src/core/system-prompt.ts +58 -65
- package/src/core/timings.ts +2 -2
- package/src/core/tools/lsp/config.ts +38 -17
- package/src/core/tools/task/agents.ts +21 -0
- package/src/core/tools/task/artifacts.ts +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/commands.ts +30 -107
- package/src/core/tools/task/discovery.ts +75 -66
- package/src/core/tools/task/executor.ts +25 -10
- package/src/core/tools/task/index.ts +35 -10
- package/src/core/tools/task/model-resolver.ts +27 -25
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/web-fetch.ts +3 -3
- package/src/core/tools/web-search/auth.ts +40 -34
- package/src/core/tools/web-search/index.ts +1 -1
- package/src/core/tools/web-search/providers/anthropic.ts +1 -1
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +646 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +102 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +264 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +216 -0
- package/src/main.ts +14 -13
- package/src/migrations.ts +24 -3
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/plugin-settings.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +38 -2
- package/src/modes/interactive/components/settings-selector.ts +1 -0
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +233 -16
- package/src/modes/interactive/theme/theme-schema.json +1 -1
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +2 -2
- 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:
|
|
97
|
+
* Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
|
|
176
98
|
*/
|
|
177
99
|
export function discoverCommands(cwd: string): WorkflowCommand[] {
|
|
178
|
-
const
|
|
100
|
+
const resolvedCwd = path.resolve(cwd);
|
|
179
101
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
186
|
-
const
|
|
187
|
-
|
|
108
|
+
// Convert SlashCommand to WorkflowCommand format
|
|
109
|
+
for (const cmd of result.items) {
|
|
110
|
+
if (seen.has(cmd.name)) continue;
|
|
188
111
|
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
-
* - ~/.
|
|
6
|
-
* - ~/.
|
|
7
|
-
* -
|
|
8
|
-
* - .
|
|
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.
|
|
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):
|
|
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
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 `
|
|
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
|
-
|
|
21
|
+
OMP_BLOCKED_AGENT_ENV,
|
|
22
|
+
OMP_SPAWNS_ENV,
|
|
22
23
|
type SingleResult,
|
|
23
24
|
} from "./types";
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
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[
|
|
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(
|
|
239
|
+
const proc = spawn(OMP_CMD, args, {
|
|
225
240
|
cwd,
|
|
226
241
|
stdio: ["ignore", "pipe", "pipe"],
|
|
227
|
-
shell:
|
|
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
|
|
6
|
-
* - ~/.
|
|
7
|
-
* - .
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
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}/
|
|
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}/
|
|
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[
|
|
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[
|
|
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
|
-
* - "
|
|
11
|
+
* - "omp/slow" → configured slow model from settings
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { join } from "node:path";
|
|
15
|
+
import { type Settings, settingsCapability } from "../../../capability/settings";
|
|
16
|
+
import { loadSync } from "../../../discovery";
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
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
|
|
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 `
|
|
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(
|
|
44
|
+
const result = spawnSync(OMP_CMD, ["--list-models"], {
|
|
46
45
|
encoding: "utf-8",
|
|
47
46
|
timeout: 5000,
|
|
48
|
-
shell:
|
|
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
|
|
85
|
+
* Load model roles from settings files using capability API.
|
|
87
86
|
*/
|
|
88
87
|
function loadModelRoles(): Record<string, string> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|
|
152
|
-
if (p.toLowerCase().startsWith("
|
|
153
|
-
const role = p.slice(
|
|
154
|
-
const resolved =
|
|
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
|
|
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
|
|
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;
|