Merge pull request #3656 from code-yeongyu/feature/hide-grep-glob-for-frontier-agents

fix(agents): hide grep glob for frontier agents
This commit is contained in:
YeonGyu-Kim
2026-04-27 14:31:30 +09:00
committed by GitHub
16 changed files with 594 additions and 55 deletions
@@ -8,6 +8,7 @@ import { applyEnvironmentContext } from "./environment-context"
import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides"
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"
import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard"
export function maybeCreateHephaestusConfig(input: {
disabledAgents: string[]
@@ -89,6 +90,13 @@ export function maybeCreateHephaestusConfig(input: {
}
const resolvedModel = hephaestusConfig.model ?? ""
hephaestusConfig.permission = applyFrontierToolSchemaPermission(
hephaestusConfig.permission,
resolvedModel,
hephaestusOverride?.permission,
(hephaestusOverride as { tools?: Record<string, boolean> } | undefined)?.tools
)
const gptDeny = getGptApplyPatchPermission(resolvedModel)
if (Object.keys(gptDeny).length > 0 && hephaestusConfig.permission) {
Object.assign(hephaestusConfig.permission, gptDeny)
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test";
import { maybeCreateSisyphusConfig } from "./sisyphus-agent";
import type { AgentOverrides } from "../types";
@@ -12,7 +14,7 @@ describe("maybeCreateSisyphusConfig", () => {
model: "openai/gpt-5.4",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -46,7 +48,7 @@ describe("maybeCreateSisyphusConfig", () => {
model: "anthropic/claude-opus-4-7",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -73,6 +75,178 @@ describe("maybeCreateSisyphusConfig", () => {
});
});
describe("#given Opus 4.7 model with user override allowing grep and glob", () => {
test("#when config is created #then grep and glob are still denied", () => {
// given
const agentOverrides: AgentOverrides = {
sisyphus: {
model: "anthropic/claude-opus-4-7",
permission: {
grep: "allow",
glob: "allow",
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateSisyphusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "anthropic/claude-opus-4-7",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given GPT 5.5 model with user override allowing grep and glob", () => {
test("#when config is created #then grep and glob are still denied", () => {
// given
const agentOverrides: AgentOverrides = {
sisyphus: {
model: "openai/gpt-5.5",
permission: {
grep: "allow",
glob: "allow",
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateSisyphusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.5"]),
systemDefaultModel: "openai/gpt-5.5",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given frontier default model with category override to non-frontier model", () => {
test("#when config is created #then stale grep and glob denies are cleared", () => {
// given
const agentOverrides: AgentOverrides = {
sisyphus: {
category: "non-frontier",
},
};
const mergedCategories: Record<string, CategoryConfig> = {
"non-frontier": {
model: "openai/gpt-5.4",
},
};
// when
const config = maybeCreateSisyphusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"]),
systemDefaultModel: "anthropic/claude-opus-4-7",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.model).toBe("openai/gpt-5.4");
expect(config?.permission).not.toHaveProperty("grep");
expect(config?.permission).not.toHaveProperty("glob");
});
});
describe("#given non-frontier model with user override denying grep and glob", () => {
test("#when config is created #then explicit user denies are preserved", () => {
// given
const agentOverrides: AgentOverrides = {
sisyphus: {
model: "openai/gpt-5.4",
permission: {
grep: "deny",
glob: "deny",
} as Record<string, "deny">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateSisyphusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.4"]),
systemDefaultModel: "openai/gpt-5.4",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given non-frontier model with legacy user tools denying grep and glob", () => {
test("#when config is created #then explicit legacy denies are preserved", () => {
// given
const legacyOverride = {
model: "openai/gpt-5.4",
tools: {
grep: false,
glob: false,
},
};
const agentOverrides: AgentOverrides = {
sisyphus: legacyOverride,
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateSisyphusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.4"]),
systemDefaultModel: "openai/gpt-5.4",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given generic GPT model with user override allowing apply_patch", () => {
test("#when config is created #then apply_patch is still denied", () => {
// given
@@ -81,7 +255,7 @@ describe("maybeCreateSisyphusConfig", () => {
model: "openai/gpt-4o",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -8,6 +8,7 @@ import { applyOverrides } from "./agent-overrides"
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
import { createSisyphusAgent } from "../sisyphus"
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"
import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard"
export function maybeCreateSisyphusConfig(input: {
disabledAgents: string[]
@@ -83,6 +84,13 @@ export function maybeCreateSisyphusConfig(input: {
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory)
const resolvedModel = sisyphusConfig.model ?? ""
sisyphusConfig.permission = applyFrontierToolSchemaPermission(
sisyphusConfig.permission,
resolvedModel,
sisyphusOverride?.permission,
(sisyphusOverride as { tools?: Record<string, boolean> } | undefined)?.tools
)
const gptDeny = getGptApplyPatchPermission(resolvedModel)
if (Object.keys(gptDeny).length > 0 && sisyphusConfig.permission) {
Object.assign(sisyphusConfig.permission, gptDeny)
+41
View File
@@ -0,0 +1,41 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import { isGpt5_5Model } from "./types"
import type { PermissionValue } from "../shared/permission-compat"
const FRONTIER_TOOL_SCHEMA_NAMES = ["grep", "glob"] as const
type MutablePermission = Record<string, PermissionValue | Record<string, PermissionValue>>
function isOpus47Model(model: string): boolean {
const modelName = model.includes("/") ? (model.split("/").pop() ?? model) : model
return modelName.toLowerCase().includes("claude-opus-4-7")
}
export function getFrontierToolSchemaPermission(model: string): Record<string, "deny"> {
return isOpus47Model(model) || isGpt5_5Model(model)
? { grep: "deny" as const, glob: "deny" as const }
: {}
}
export function applyFrontierToolSchemaPermission(
permission: AgentConfig["permission"] | undefined,
model: string,
explicitPermission?: AgentConfig["permission"],
explicitTools?: Record<string, boolean>
): AgentConfig["permission"] | undefined {
if (!permission) return permission
const nextPermission: MutablePermission = { ...permission }
const explicitPermissionMap = explicitPermission as MutablePermission | undefined
const frontierDeny = getFrontierToolSchemaPermission(model)
if (Object.keys(frontierDeny).length > 0) {
Object.assign(nextPermission, frontierDeny)
return nextPermission as AgentConfig["permission"]
}
for (const toolName of FRONTIER_TOOL_SCHEMA_NAMES) {
if (explicitPermissionMap?.[toolName] === "deny") continue
if (explicitTools?.[toolName] === false) continue
delete nextPermission[toolName]
}
return nextPermission as AgentConfig["permission"]
}
+177 -3
View File
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test";
import {
getHephaestusPromptSource,
@@ -321,7 +323,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
model: "openai/gpt-5.4",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -355,7 +357,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
model: "anthropic/claude-opus-4-7",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -389,7 +391,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
model: "openai/gpt-4o",
permission: {
apply_patch: "allow",
},
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
@@ -414,4 +416,176 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
expect(config?.permission).toHaveProperty("apply_patch", "deny");
});
});
describe("#given Opus 4.7 model with user override allowing grep and glob", () => {
test("#when config is created #then grep and glob are still denied", () => {
// given
const agentOverrides: AgentOverrides = {
hephaestus: {
model: "anthropic/claude-opus-4-7",
permission: {
grep: "allow",
glob: "allow",
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateHephaestusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["anthropic/claude-opus-4-7"]),
systemDefaultModel: "anthropic/claude-opus-4-7",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given GPT 5.5 model with user override allowing grep and glob", () => {
test("#when config is created #then grep and glob are still denied", () => {
// given
const agentOverrides: AgentOverrides = {
hephaestus: {
model: "openai/gpt-5.5",
permission: {
grep: "allow",
glob: "allow",
} as Record<string, "allow">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateHephaestusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.5"]),
systemDefaultModel: "openai/gpt-5.5",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given frontier default model with category override to non-frontier model", () => {
test("#when config is created #then stale grep and glob denies are cleared", () => {
// given
const agentOverrides: AgentOverrides = {
hephaestus: {
category: "non-frontier",
},
};
const mergedCategories: Record<string, CategoryConfig> = {
"non-frontier": {
model: "openai/gpt-5.4",
},
};
// when
const config = maybeCreateHephaestusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.5", "openai/gpt-5.4"]),
systemDefaultModel: "openai/gpt-5.5",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.model).toBe("openai/gpt-5.4");
expect(config?.permission).not.toHaveProperty("grep");
expect(config?.permission).not.toHaveProperty("glob");
});
});
describe("#given non-frontier model with user override denying grep and glob", () => {
test("#when config is created #then explicit user denies are preserved", () => {
// given
const agentOverrides: AgentOverrides = {
hephaestus: {
model: "openai/gpt-5.4",
permission: {
grep: "deny",
glob: "deny",
} as Record<string, "deny">,
},
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateHephaestusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.4"]),
systemDefaultModel: "openai/gpt-5.4",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
describe("#given non-frontier model with legacy user tools denying grep and glob", () => {
test("#when config is created #then explicit legacy denies are preserved", () => {
// given
const legacyOverride = {
model: "openai/gpt-5.4",
tools: {
grep: false,
glob: false,
},
};
const agentOverrides: AgentOverrides = {
hephaestus: legacyOverride,
};
const mergedCategories: Record<string, CategoryConfig> = {};
// when
const config = maybeCreateHephaestusConfig({
disabledAgents: [],
agentOverrides,
availableModels: new Set(["openai/gpt-5.4"]),
systemDefaultModel: "openai/gpt-5.4",
isFirstRunNoCache: false,
availableAgents: [],
availableSkills: [],
availableCategories: [],
mergedCategories,
useTaskSystem: false,
});
// then
expect(config?.permission).toHaveProperty("grep", "deny");
expect(config?.permission).toHaveProperty("glob", "deny");
});
});
});
+2
View File
@@ -9,6 +9,7 @@ import type {
} from "../dynamic-agent-prompt-builder";
import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder";
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard";
import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard";
import { buildHephaestusPrompt as buildGptPrompt } from "./gpt";
import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex";
@@ -139,6 +140,7 @@ export function createHephaestusAgent(
permission: {
question: "allow",
call_omo_agent: "deny",
...getFrontierToolSchemaPermission(model),
...getGptApplyPatchPermission(model),
} as AgentConfig["permission"],
reasoningEffort: "medium",
+4
View File
@@ -13,6 +13,7 @@ import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4";
import { buildGpt55SisyphusPrompt } from "./sisyphus/gpt-5-5";
import { buildTaskManagementSection } from "./sisyphus/default";
import { getGptApplyPatchPermission } from "./gpt-apply-patch-guard";
import { getFrontierToolSchemaPermission } from "./frontier-tool-schema-guard";
const MODE: AgentMode = "primary";
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
@@ -501,6 +502,7 @@ export function createSisyphusAgent(
permission: {
question: "allow",
call_omo_agent: "deny",
...getFrontierToolSchemaPermission(model),
...getGptApplyPatchPermission(model),
} as AgentConfig["permission"],
reasoningEffort: "medium",
@@ -527,6 +529,7 @@ export function createSisyphusAgent(
permission: {
question: "allow",
call_omo_agent: "deny",
...getFrontierToolSchemaPermission(model),
...getGptApplyPatchPermission(model),
} as AgentConfig["permission"],
reasoningEffort: "medium",
@@ -567,6 +570,7 @@ export function createSisyphusAgent(
const permission = {
question: "allow",
call_omo_agent: "deny",
...getFrontierToolSchemaPermission(model),
...getGptApplyPatchPermission(model),
} as AgentConfig["permission"];
const base = {
+46
View File
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, test, expect } from "bun:test"
import { createOracleAgent } from "./oracle"
import { createLibrarianAgent } from "./librarian"
@@ -6,6 +8,7 @@ import { createMomusAgent } from "./momus"
import { createMetisAgent } from "./metis"
import { createAtlasAgent } from "./atlas"
import { createSisyphusAgent } from "./sisyphus"
import { createHephaestusAgent } from "./hephaestus"
const TEST_MODEL = "anthropic/claude-sonnet-4-5"
@@ -131,4 +134,47 @@ describe("read-only agent tool restrictions", () => {
expect(claudePermission["apply_patch"]).toBeUndefined()
})
})
describe("Sisyphus and Hephaestus frontier tool schema restrictions", () => {
test("deny grep and glob for Opus 4.7 and GPT 5.5 models", () => {
// given
const frontierAgents = [
createSisyphusAgent("anthropic/claude-opus-4-7"),
createSisyphusAgent("openai/gpt-5.5"),
createHephaestusAgent("anthropic/claude-opus-4-7"),
createHephaestusAgent("openai/gpt-5.5"),
]
// when
const permissions = frontierAgents.map(
(agent) => (agent.permission ?? {}) as Record<string, string>,
)
// then
for (const permission of permissions) {
expect(permission.grep).toBe("deny")
expect(permission.glob).toBe("deny")
}
})
test("keeps grep and glob available for other models", () => {
// given
const otherAgents = [
createSisyphusAgent("anthropic/claude-sonnet-4-5"),
createSisyphusAgent("openai/gpt-5.4"),
createHephaestusAgent("openai/gpt-5.4"),
]
// when
const permissions = otherAgents.map(
(agent) => (agent.permission ?? {}) as Record<string, string>,
)
// then
for (const permission of permissions) {
expect(permission.grep).toBeUndefined()
expect(permission.glob).toBeUndefined()
}
})
})
})
+90 -15
View File
@@ -3,7 +3,7 @@ import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:t
import type { TmuxConfig } from '../../config/schema'
import type { WindowState, PaneAction } from './types'
import type { ActionResult, ExecuteContext } from './action-executor'
import type { TmuxUtilDeps } from './manager'
import type { TmuxSessionManager as TmuxSessionManagerType, TmuxUtilDeps } from './manager'
import * as sharedModule from '../../shared'
type ExecuteActionsResult = {
@@ -287,20 +287,95 @@ describe('TmuxSessionManager', () => {
test('falls back to default port when serverUrl has port 0', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true,
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40, })
const previousOpenCodePort = process.env.OPENCODE_PORT
delete process.env.OPENCODE_PORT
let manager: TmuxSessionManagerType | undefined
try {
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true,
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40, })
// when
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
} finally {
if (previousOpenCodePort === undefined) {
delete process.env.OPENCODE_PORT
} else {
process.env.OPENCODE_PORT = previousOpenCodePort
}
}
// then
expect((manager as any).serverUrl).toBe('http://localhost:4096')
})
test('falls back to configured OPENCODE_PORT when serverUrl has port 0', async () => {
// given
const previousOpenCodePort = process.env.OPENCODE_PORT
process.env.OPENCODE_PORT = '5678'
let manager: TmuxSessionManagerType | undefined
try {
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true,
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40, })
// when
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
} finally {
if (previousOpenCodePort === undefined) {
delete process.env.OPENCODE_PORT
} else {
process.env.OPENCODE_PORT = previousOpenCodePort
}
}
// then
expect((manager as any).serverUrl).toBe('http://localhost:5678')
})
test('ignores invalid OPENCODE_PORT when serverUrl has port 0', async () => {
// given
const previousOpenCodePort = process.env.OPENCODE_PORT
process.env.OPENCODE_PORT = 'not-a-port'
let manager: TmuxSessionManagerType | undefined
try {
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true,
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40, })
// when
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
} finally {
if (previousOpenCodePort === undefined) {
delete process.env.OPENCODE_PORT
} else {
process.env.OPENCODE_PORT = previousOpenCodePort
}
}
// then
expect((manager as any).serverUrl).toBe('http://localhost:4096')
@@ -1989,7 +2064,7 @@ describe('TmuxSessionManager', () => {
const cleanupPromise = manager.cleanup()
// then
await expect(cleanupPromise).resolves.toBeUndefined()
expect(await cleanupPromise).toBeUndefined()
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1)
})
})
+5 -1
View File
@@ -72,7 +72,11 @@ export class TmuxSessionManager {
this.client = ctx.client
this.tmuxConfig = tmuxConfig
this.deps = deps
const defaultPort = process.env.OPENCODE_PORT ?? "4096"
const configuredPort = process.env.OPENCODE_PORT
const parsedPort = configuredPort ? Number(configuredPort) : 4096
const defaultPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
? String(parsedPort)
: "4096"
const fallbackUrl = `http://localhost:${defaultPort}`
const rawServerUrl = ctx.serverUrl?.toString()
try {
@@ -1,8 +1,9 @@
/// <reference types="bun-types" />
import { afterEach, describe, expect, it, spyOn } from "bun:test"
import type { LoadedSkill } from "../../features/opencode-skill-loader"
import * as shared from "../../shared"
import * as slashcommand from "../../tools/slashcommand"
import { executeSlashCommand } from "./executor"
import * as slashcommand from "../../tools/slashcommand/command-discovery"
let resolveCommandsInTextSpy: { mockRestore: () => void } | undefined
let resolveFileReferencesInTextSpy: { mockRestore: () => void } | undefined
@@ -38,6 +39,11 @@ function restoreExecutorSpies(): void {
discoverCommandsSyncSpy = undefined
}
async function executeSlashCommand(...args: Parameters<typeof import("./executor").executeSlashCommand>): ReturnType<typeof import("./executor").executeSlashCommand> {
const module = await import(`./executor?test=${Date.now()}-${Math.random()}`)
return module.executeSlashCommand(...args)
}
afterEach(restoreExecutorSpies)
function createRestrictedSkill(): LoadedSkill {
+4 -6
View File
@@ -1,10 +1,8 @@
import { dirname } from "path"
import {
resolveCommandsInText,
resolveFileReferencesInText,
} from "../../shared"
import { resolveCommandsInText } from "../../shared/command-executor/resolve-commands-in-text"
import { resolveFileReferencesInText } from "../../shared/file-reference-resolver"
import { discoverAllSkills, type LoadedSkill, type LazyContentLoader } from "../../features/opencode-skill-loader"
import { discoverCommandsSync } from "../../tools/slashcommand"
import * as commandDiscovery from "../../tools/slashcommand/command-discovery"
import type { CommandInfo as DiscoveredCommandInfo, CommandMetadata } from "../../tools/slashcommand/types"
import type { ParsedSlashCommand } from "./types"
@@ -47,7 +45,7 @@ export interface ExecutorOptions {
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
const discoveredCommands = discoverCommandsSync(options?.directory ?? process.cwd(), {
const discoveredCommands = commandDiscovery.discoverCommandsSync(options?.directory ?? process.cwd(), {
pluginsEnabled: options?.pluginsEnabled,
enabledPluginsOverride: options?.enabledPluginsOverride,
})
+11 -12
View File
@@ -4,13 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te
import type { ToolContext } from "@opencode-ai/plugin/tool"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import * as skillContent from "../../features/opencode-skill-loader/skill-content"
import * as commandDiscovery from "../slashcommand/command-discovery"
const discoverCommandsSync = mock(() => [])
mock.module("../slashcommand/command-discovery", () => ({
discoverCommandsSync,
}))
function createMockSkill(name: string): LoadedSkill {
return {
name,
@@ -49,7 +46,13 @@ function createMockContext(sessionID: string): ToolContext {
}
}
async function createSkillTool(...args: Parameters<typeof import("./tools").createSkillTool>): ReturnType<typeof import("./tools").createSkillTool> {
const module = await import(`./tools?test=${Date.now()}-${Math.random()}`)
return module.createSkillTool(...args)
}
beforeEach(() => {
spyOn(commandDiscovery, "discoverCommandsSync").mockImplementation(discoverCommandsSync)
spyOn(skillContent, "getAllSkills").mockImplementation(getAllSkills)
spyOn(skillContent, "clearSkillCache").mockImplementation(clearSkillCache)
})
@@ -65,8 +68,7 @@ describe("createSkillTool", () => {
const baselineDiscoverCommandsSyncCalls = discoverCommandsSync.mock.calls.length
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
const skillTool = await createSkillTool({})
// then
expect(discoverCommandsSync.mock.calls.length).toBe(baselineDiscoverCommandsSyncCalls)
@@ -82,8 +84,7 @@ describe("createSkillTool", () => {
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
const skillTool = await createSkillTool({})
// then
expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls)
@@ -99,8 +100,7 @@ describe("createSkillTool", () => {
const sessionContext = createMockContext("session-clear-once")
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
const skillTool = await createSkillTool({})
void skillTool.description
await flushMicrotasks()
await skillTool.execute({ name: "lazy-skill" }, sessionContext)
@@ -116,8 +116,7 @@ describe("createSkillTool", () => {
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
const sessionAContext = createMockContext("session-a")
const sessionBContext = createMockContext("session-b")
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
const skillTool = await createSkillTool({})
// when
await skillTool.execute({ name: "lazy-skill" }, sessionAContext)
+3 -10
View File
@@ -7,7 +7,7 @@ import type { SkillArgs, SkillLoadOptions } from "./types"
import type { LoadedSkill } from "../../features/opencode-skill-loader"
import { clearSkillCache, getAllSkills } from "../../features/opencode-skill-loader/skill-content"
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
import { discoverCommandsSync } from "../slashcommand/command-discovery"
import * as commandDiscovery from "../slashcommand/command-discovery"
import type { CommandInfo } from "../slashcommand/types"
import { formatLoadedCommand } from "../slashcommand/command-output-formatter"
import { formatCombinedDescription } from "./description-formatter"
@@ -37,14 +37,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
disabledSkills: options?.disabledSkills,
browserProvider: options?.browserProvider,
})) ?? []
const allSkills = !options.skills
? discovered
: [
...discovered,
...options.skills.filter(
(skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name)
),
]
const allSkills = options.skills ? [...options.skills] : discovered
if (options.nativeSkills) {
try {
@@ -58,7 +51,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
}
const getCommands = (): CommandInfo[] => {
return discoverCommandsSync(undefined, {
return commandDiscovery.discoverCommandsSync(undefined, {
pluginsEnabled: options.pluginsEnabled,
enabledPluginsOverride: options.enabledPluginsOverride,
}) ?? []
@@ -0,0 +1,6 @@
export { EXCLUDED_DIRS } from "../../shared/excluded-dirs"
export { parseFrontmatter } from "../../shared/frontmatter"
export { sanitizeModelField } from "../../shared/model-sanitizer"
export { getOpenCodeCommandDirs } from "../../shared/opencode-command-dirs"
export { discoverPluginCommandDefinitions } from "../../shared/plugin-command-discovery"
export { findProjectOpencodeCommandDirs } from "../../shared/project-discovery-dirs"
+4 -3
View File
@@ -7,11 +7,12 @@ import {
getOpenCodeCommandDirs,
discoverPluginCommandDefinitions,
EXCLUDED_DIRS,
} from "../../shared"
} from "./command-discovery-deps"
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir, log } from "../../shared"
import { loadBuiltinCommands } from "../../features/builtin-commands"
import { getClaudeConfigDir } from "../../shared/claude-config-dir"
import { log } from "../../shared/logger"
import { loadBuiltinCommands } from "../../features/builtin-commands/commands"
import type { CommandInfo, CommandMetadata, CommandScope } from "./types"
export interface CommandDiscoveryOptions {