diff --git a/.dictate.toml b/.dictate.toml new file mode 100644 index 00000000000..9c4312debff --- /dev/null +++ b/.dictate.toml @@ -0,0 +1,99 @@ +# Dictator Configuration +# Decree-based structural enforcement for code quality +# Generated by: dictator occupy + +[decree.supreme] +# Universal structural rules (applies to ALL files unless overridden) +trailing_whitespace = "deny" +tabs_vs_spaces = "spaces" +tab_width = 2 +final_newline = "require" +line_endings = "lf" +blank_line_whitespace = "deny" +# max_line_length: opt-in per language (uncomment to enforce globally) + +# Per-rule ignores (decree-level, not language-level) +# - Makefiles require tabs for recipes +# - Markdown can contain tabs inside code blocks +[decree.supreme.ignore.tab-character] +filenames = ["Makefile", "GNUmakefile", "makefile"] +extensions = ["md", "mdx"] + +# Language-specific decrees (uncomment to enable) +# Remove the '#' from '[decree.LANGUAGE]' and its settings to activate + +# [decree.ruby] +# # Ruby-specific structural enforcement +# max_line_length = 120 +# max_lines = 300 +# ignore_comments = true +# ignore_blank_lines = true +# method_visibility_order = ["public", "protected", "private"] +# comment_spacing = true +# +# # External linter integration (uncomment to enable) +# [decree.ruby.linter] +# command = "rubocop" + +# [decree.typescript] +# # TypeScript/JavaScript structural enforcement +# max_line_length = 100 +# max_lines = 350 +# ignore_comments = true +# ignore_blank_lines = true +# import_order = ["system", "external", "internal"] +# +# # External linter integration (uncomment to enable) +# [decree.typescript.linter] +# command = "eslint" + +# [decree.golang] +# # Go structural enforcement +# # Go convention: tabs, not spaces (overrides decree.supreme) +# tabs_vs_spaces = "tabs" +# max_line_length = 120 +# max_lines = 450 +# ignore_comments = true +# ignore_blank_lines = true +# +# # External linter integration (uncomment to enable) +# [decree.golang.linter] +# command = "gofmt" + +# [decree.rust] +# # Rust structural enforcement +# tab_width = 4 +# max_line_length = 100 +# max_lines = 400 +# ignore_comments = true +# ignore_blank_lines = true +# visibility_order = ["pub", "private"] +# +# # External linter integration (uncomment to enable) +# [decree.rust.linter] +# command = "clippy" + +# [decree.python] +# # Python structural enforcement +# tab_width = 4 +# max_line_length = 88 +# max_lines = 380 +# ignore_comments = true +# ignore_blank_lines = true +# import_order = ["stdlib", "third_party", "local"] +# +# # External linter integration (uncomment to enable) +# [decree.python.linter] +# command = "ruff" + +# [decree.frontmatter] +# # YAML frontmatter ordering for .md and .mdx files only +# # Does NOT handle: .astro (JS/TS frontmatter), .yml/.yaml, .toml +# order = ["title", "slug", "pubDate", "description", "tags", "draft"] +# required = ["title", "slug"] + +# Custom decrees (WASM/native plugins) +# Uncomment and configure to enable custom linting rules +# [decree.kjr] +# enabled = true +# path = "path/to/dictator_kjr.wasm" diff --git a/packages/opencode/.dictate.toml b/packages/opencode/.dictate.toml new file mode 100644 index 00000000000..da471faea26 --- /dev/null +++ b/packages/opencode/.dictate.toml @@ -0,0 +1,97 @@ +# Dictator Configuration +# Decree-based structural enforcement for code quality +# Generated by: dictator occupy + +[decree.supreme] +# Universal structural rules (applies to ALL files unless overridden) +trailing_whitespace = "deny" +tabs_vs_spaces = "spaces" +tab_width = 2 +final_newline = "require" +line_endings = "lf" +blank_line_whitespace = "deny" +# max_line_length: opt-in per language (uncomment to enforce globally) + +# Per-rule ignores (decree-level, not language-level) +# - Makefiles require tabs for recipes +# - Markdown can contain tabs inside code blocks +[decree.supreme.ignore.tab-character] +filenames = ["Makefile", "GNUmakefile", "makefile"] +extensions = ["md", "mdx"] + +# Language-specific decrees (uncomment to enable) +# Remove the '#' from '[decree.LANGUAGE]' and its settings to activate + +# [decree.ruby] +# # Ruby-specific structural enforcement +# max_line_length = 120 +# max_lines = 300 +# ignore_comments = true +# ignore_blank_lines = true +# method_visibility_order = ["public", "protected", "private"] +# comment_spacing = true +# +# # External linter integration (uncomment to enable) +# [decree.ruby.linter] +# command = "rubocop" + +# [decree.typescript] +# # TypeScript/JavaScript structural enforcement +# max_line_length = 100 +# max_lines = 350 +# ignore_comments = true +# ignore_blank_lines = true +# import_order = ["system", "external", "internal"] +# +# # External linter integration (uncomment to enable) +# [decree.typescript.linter] +# command = "eslint" + +# [decree.golang] +# # Go structural enforcement +# # Go convention: tabs, not spaces (overrides decree.supreme) +# tabs_vs_spaces = "tabs" +# max_line_length = 120 +# max_lines = 450 +# ignore_comments = true +# ignore_blank_lines = true +# +# # External linter integration (uncomment to enable) +# [decree.golang.linter] +# command = "gofmt" + +# [decree.rust] +# # Rust structural enforcement +# max_line_length = 100 +# max_lines = 400 +# ignore_comments = true +# ignore_blank_lines = true +# visibility_order = ["pub", "private"] +# +# # External linter integration (uncomment to enable) +# [decree.rust.linter] +# command = "clippy" + +# [decree.python] +# # Python structural enforcement +# max_line_length = 88 # Black's default +# max_lines = 380 +# ignore_comments = true +# ignore_blank_lines = true +# import_order = ["stdlib", "third_party", "local"] +# +# # External linter integration (uncomment to enable) +# [decree.python.linter] +# command = "ruff" + +# [decree.frontmatter] +# # YAML frontmatter ordering for .md and .mdx files only +# # Does NOT handle: .astro (JS/TS frontmatter), .yml/.yaml, .toml +# order = ["title", "slug", "pubDate", "description", "tags", "draft"] +# required = ["title", "slug"] + +# Custom decrees (WASM/native plugins) +# Uncomment and configure to enable custom linting rules +# [decree.kjr] +# enabled = true +# path = "path/to/dictator_kjr.wasm" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 7b9a8c2076a..beb1f3f9f74 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -46,6 +46,13 @@ export namespace MCP { }), ) + export const InstructionsChanged = BusEvent.define( + "mcp.instructions.changed", + z.object({ + server: z.string(), + }), + ) + export const BrowserOpenFailed = BusEvent.define( "mcp.browser.open.failed", z.object({ @@ -166,7 +173,9 @@ export namespace MCP { const config = cfg.mcp ?? {} const clients: Record = {} const status: Record = {} + const serverInstructions: Record = {} + // Discover all configured MCP servers await Promise.all( Object.entries(config).map(async ([key, mcp]) => { if (!isMcpConfigured(mcp)) { @@ -187,12 +196,20 @@ export namespace MCP { if (result.mcpClient) { clients[key] = result.mcpClient + // Fetch and cache instructions during initialization + const instructions = await fetchServerInstructions(key, result.mcpClient) + if (instructions) { + serverInstructions[key] = instructions + log.info("cached server instructions", { key }) + } } }), ) + return { status, clients, + serverInstructions, } }, async (state) => { @@ -209,6 +226,23 @@ export namespace MCP { }, ) + // Helper function to fetch server instructions from an MCP client + // Server instructions are always fetched automatically per MCP protocol specification + async function fetchServerInstructions(serverName: string, client: MCPClient): Promise { + try { + const instructions = await client.getInstructions() + if (instructions?.trim()) { + return instructions + } + + log.debug("no server instructions available", { serverName }) + return undefined + } catch (error) { + log.debug("failed to fetch server instructions", { serverName, error }) + return undefined + } + } + // Helper function to fetch prompts for a specific client async function fetchPromptsForClient(clientName: string, client: Client) { const prompts = await client.listPrompts().catch((e) => { @@ -254,6 +288,56 @@ export namespace MCP { return commands } + /** + * Refresh server instructions for a specific MCP server. + */ + export async function refreshServerInstructions(serverName: string) { + const s = await state() + const client = s.clients[serverName] + + if (!client) { + log.warn("cannot refresh instructions: client not found", { serverName }) + return + } + + const instructions = await fetchServerInstructions(serverName, client) + if (instructions) { + // Update the server instructions in state + s.serverInstructions = s.serverInstructions || {} + s.serverInstructions[serverName] = instructions + log.info("refreshed server instructions", { serverName }) + // Publish event for instruction changes + Bus.publish(InstructionsChanged, { server: serverName }) + } + } + + /** + * Refresh server instructions for all connected MCP servers. + */ + export async function refreshAllServerInstructions() { + const s = await state() + + const refreshPromises = Object.keys(s.clients).map(async (serverName) => { + try { + const client = s.clients[serverName] + if (!client) return + + const instructions = await fetchServerInstructions(serverName, client) + if (instructions) { + s.serverInstructions = s.serverInstructions || {} + s.serverInstructions[serverName] = instructions + log.info("refreshed server instructions", { serverName }) + // Publish event for instruction changes + Bus.publish(InstructionsChanged, { server: serverName }) + } + } catch (error) { + log.error("failed to refresh server instructions", { serverName, error }) + } + }) + + await Promise.all(refreshPromises) + } + export async function add(name: string, mcp: Config.Mcp) { const s = await state() const result = await create(name, mcp) @@ -513,6 +597,10 @@ export namespace MCP { return state().then((state) => state.clients) } + export async function serverInstructions() { + return state().then((state) => state.serverInstructions || {}) + } + export async function connect(name: string) { const cfg = await Config.get() const config = cfg.mcp ?? {} diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 1029b45ea0d..498e4a359c4 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -66,19 +66,30 @@ export namespace LLM { const isCodex = provider.id === "openai" && auth?.type === "oauth" const system = SystemPrompt.header(input.model.providerID) - system.push( - [ - // use agent prompt otherwise provider prompt - // For Codex sessions, skip SystemPrompt.provider() since it's sent via options.instructions - ...(input.agent.prompt ? [input.agent.prompt] : isCodex ? [] : SystemPrompt.provider(input.model)), - // any custom prompt passed into this call - ...input.system, - // any custom prompt from last user message - ...(input.user.system ? [input.user.system] : []), - ] - .filter((x) => x) - .join("\n"), - ) + + // Get MCP server instructions - handle errors gracefully + let mcpInstructions: string[] = [] + try { + mcpInstructions = await SystemPrompt.mcpInstructions() + } catch (error) { + // Silently ignore MCP instruction errors to avoid breaking the system + } + + const systemText = [ + // use agent prompt otherwise provider prompt + // For Codex sessions, skip SystemPrompt.provider() since it's sent via options.instructions + ...(input.agent.prompt ? [input.agent.prompt] : isCodex ? [] : SystemPrompt.provider(input.model)), + // any custom prompt passed into this call + ...input.system, + // any custom prompt from last user message + ...(input.user.system ? [input.user.system] : []), + // Include MCP server instructions + ...mcpInstructions, + ] + .filter((x) => x) + .join("\n") + + system.push(systemText) const header = system[0] const original = clone(system) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index fff90808864..bd3a58e6de7 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -17,6 +17,10 @@ import PROMPT_CODEX from "./prompt/codex.txt" import PROMPT_CODEX_INSTRUCTIONS from "./prompt/codex_header.txt" import type { Provider } from "@/provider/provider" import { Flag } from "@/flag/flag" +import { MCP } from "../mcp" +import { Log } from "../util/log" + +const log = Log.create({ service: "system" }) export namespace SystemPrompt { export function header(providerID: string) { @@ -133,6 +137,36 @@ export namespace SystemPrompt { .catch(() => "") .then((x) => (x ? "Instructions from: " + url + "\n" + x : "")), ) - return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean)) + + // Fetch MCP server instructions + const mcpInstructions = await SystemPrompt.mcpInstructions() + + return Promise.all([...foundFiles, ...foundUrls, ...mcpInstructions]).then((result) => result.filter(Boolean)) + } + + /** + * Get MCP server instructions for system prompt integration. + * Returns formatted instructions from all connected MCP servers. + */ + export async function mcpInstructions(): Promise { + try { + const [status, serverInstructions] = await Promise.all([MCP.status(), MCP.serverInstructions()]) + const instructions: string[] = [] + + // Only include instructions from connected servers + for (const [serverName, serverStatus] of Object.entries(status)) { + if (serverStatus.status === "connected" && serverInstructions[serverName]) { + const instructionText = serverInstructions[serverName] + if (instructionText?.trim()) { + instructions.push(``) + } + } + } + + return instructions + } catch (error) { + log.debug("failed to get MCP instructions", { error }) + return [] // Return empty array on error to avoid breaking the system + } } }