Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .dictate.toml
Original file line number Diff line number Diff line change
@@ -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"
97 changes: 97 additions & 0 deletions packages/opencode/.dictate.toml
Original file line number Diff line number Diff line change
@@ -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"
88 changes: 88 additions & 0 deletions packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -166,7 +173,9 @@ export namespace MCP {
const config = cfg.mcp ?? {}
const clients: Record<string, MCPClient> = {}
const status: Record<string, Status> = {}
const serverInstructions: Record<string, string> = {}

// Discover all configured MCP servers
await Promise.all(
Object.entries(config).map(async ([key, mcp]) => {
if (!isMcpConfigured(mcp)) {
Expand All @@ -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) => {
Expand All @@ -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<string | undefined> {
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) => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ?? {}
Expand Down
37 changes: 24 additions & 13 deletions packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading