diff --git a/src/cli/cli-installer.ts b/src/cli/cli-installer.ts
index 5db015531..173d1e624 100644
--- a/src/cli/cli-installer.ts
+++ b/src/cli/cli-installer.ts
@@ -4,10 +4,13 @@ import type { InstallArgs } from "./types"
import {
addPluginToOpenCodeConfig,
detectCurrentConfig,
+ getConfigDir,
getOpenCodeVersion,
isOpenCodeInstalled,
writeOmoConfig,
} from "./config-manager"
+import { loadPluginConfig } from "../plugin-config"
+import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
import {
SYMBOLS,
argsToConfig,
@@ -124,6 +127,14 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
}
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
+ const pluginConfig = loadPluginConfig(getConfigDir(), {})
+ const bootstrapResult = await syncBootstrapAgents({
+ directory: getConfigDir(),
+ targetDir: getConfigDir(),
+ pluginConfig,
+ })
+ printSuccess(`Bootstrapped ${bootstrapResult.agentCount} OpenCode agent files`)
+
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
if (!config.hasClaude) {
diff --git a/src/cli/config-manager.ts b/src/cli/config-manager.ts
index 43cbd6dab..a6047e0e4 100644
--- a/src/cli/config-manager.ts
+++ b/src/cli/config-manager.ts
@@ -2,6 +2,7 @@ export type { ConfigContext } from "./config-manager/config-context"
export {
initConfigContext,
getConfigContext,
+ getConfigDir,
resetConfigContext,
} from "./config-manager/config-context"
diff --git a/src/cli/tui-installer.ts b/src/cli/tui-installer.ts
index 4272557c0..8dceddb15 100644
--- a/src/cli/tui-installer.ts
+++ b/src/cli/tui-installer.ts
@@ -5,10 +5,13 @@ import type { InstallArgs } from "./types"
import {
addPluginToOpenCodeConfig,
detectCurrentConfig,
+ getConfigDir,
getOpenCodeVersion,
isOpenCodeInstalled,
writeOmoConfig,
} from "./config-manager"
+import { loadPluginConfig } from "../plugin-config"
+import { syncBootstrapAgents } from "../plugin-handlers/opencode-agent-bootstrap"
import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators"
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
import { promptInstallConfig } from "./tui-install-prompts"
@@ -70,6 +73,15 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
}
spinner.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
+ spinner.start("Bootstrapping OpenCode agent files")
+ const pluginConfig = loadPluginConfig(getConfigDir(), {})
+ const bootstrapResult = await syncBootstrapAgents({
+ directory: getConfigDir(),
+ targetDir: getConfigDir(),
+ pluginConfig,
+ })
+ spinner.stop(`Bootstrapped ${bootstrapResult.agentCount} OpenCode agent files`)
+
if (!config.hasClaude) {
p.log.info(
`${color.bold("Note:")} Sisyphus agent performs best with Claude Opus 4.5+.\n` +
diff --git a/src/index.ts b/src/index.ts
index b36e6c4b9..3084ca02e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,6 +19,7 @@ import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shar
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
import { lspManager } from "./tools/lsp/client"
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
+import { syncBootstrapAgents } from "./plugin-handlers/opencode-agent-bootstrap"
let activePluginDispose: PluginDispose | null = null
@@ -39,6 +40,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const pluginConfig = loadPluginConfig(ctx.directory, ctx)
+ syncBootstrapAgents({
+ directory: ctx.directory,
+ pluginConfig,
+ }).catch((error) => {
+ log("[OhMyOpenCodePlugin] bootstrap agent sync failed", { error: String(error) })
+ })
+
const posthog = createPluginPostHog()
const distinctId = getPostHogDistinctId()
try {
diff --git a/src/plugin-handlers/opencode-agent-bootstrap.test.ts b/src/plugin-handlers/opencode-agent-bootstrap.test.ts
new file mode 100644
index 000000000..49737b693
--- /dev/null
+++ b/src/plugin-handlers/opencode-agent-bootstrap.test.ts
@@ -0,0 +1,31 @@
+///
+
+import { describe, expect, test } from "bun:test"
+
+import { buildBootstrapAgentFiles } from "./opencode-agent-bootstrap"
+
+describe("buildBootstrapAgentFiles", () => {
+ test("keeps ZWSP runtime names while writing plain-key agent files", () => {
+ const files = buildBootstrapAgentFiles({
+ "\u200BSisyphus - Ultraworker": {
+ name: "\u200BSisyphus - Ultraworker",
+ mode: "primary",
+ model: "openai/gpt-5.4",
+ topP: 0.9,
+ prompt: "sisyphus prompt",
+ },
+ build: {
+ name: "build",
+ mode: "primary",
+ prompt: "build prompt",
+ },
+ })
+
+ expect(files).toHaveLength(1)
+ expect(files[0]?.fileName).toBe("sisyphus.md")
+ expect(files[0]?.content).toContain("name: \u200BSisyphus - Ultraworker")
+ expect(files[0]?.content).toContain("top_p: 0.9")
+ expect(files[0]?.content).toContain("sisyphus prompt")
+ expect(files[0]?.content).not.toContain("build prompt")
+ })
+})
diff --git a/src/plugin-handlers/opencode-agent-bootstrap.ts b/src/plugin-handlers/opencode-agent-bootstrap.ts
new file mode 100644
index 000000000..60ecd5f44
--- /dev/null
+++ b/src/plugin-handlers/opencode-agent-bootstrap.ts
@@ -0,0 +1,173 @@
+import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
+import { join } from "node:path"
+
+import yaml from "js-yaml"
+
+import type { OhMyOpenCodeConfig } from "../config"
+import { detectPluginConfigFile, getOpenCodeConfigDir, parseJsonc } from "../shared"
+import { getAgentConfigKey } from "../shared/agent-display-names"
+import { applyAgentConfig } from "./agent-config-handler"
+import { applyToolConfig } from "./tool-config-handler"
+
+const MANIFEST_FILE_NAME = ".oh-my-openagent-agent-bootstrap.json"
+const AGENT_DIRECTORY_NAME = "agent"
+const EXCLUDED_AGENT_KEYS = new Set(["build", "plan"])
+
+type PluginComponents = {
+ commands: Record
+ skills: Record
+ agents: Record
+ mcpServers: Record
+ hooksConfigs: Array<{ hooks?: Record }>
+ plugins: Array<{ name: string; version: string }>
+ errors: Array<{ pluginKey: string; installPath: string; error: string }>
+}
+
+type AgentFileEntry = {
+ fileName: string
+ content: string
+}
+
+type AgentManifest = {
+ files: string[]
+}
+
+function createEmptyPluginComponents(): PluginComponents {
+ return {
+ commands: {},
+ skills: {},
+ agents: {},
+ mcpServers: {},
+ hooksConfigs: [],
+ plugins: [],
+ errors: [],
+ }
+}
+
+function normalizeFrontmatterValue(value: Record): Record {
+ const normalized = { ...value }
+
+ if ("prompt" in normalized) {
+ delete normalized.prompt
+ }
+
+ if ("topP" in normalized && normalized.topP !== undefined) {
+ normalized["top_p"] = normalized.topP
+ delete normalized.topP
+ }
+
+ return normalized
+}
+
+export function buildBootstrapAgentFiles(agentResult: Record): AgentFileEntry[] {
+ const files: AgentFileEntry[] = []
+
+ for (const [runtimeKey, agentConfig] of Object.entries(agentResult)) {
+ if (!agentConfig || typeof agentConfig !== "object") {
+ continue
+ }
+
+ const configKey = getAgentConfigKey(runtimeKey)
+ if (!configKey || EXCLUDED_AGENT_KEYS.has(configKey)) {
+ continue
+ }
+
+ const normalized = normalizeFrontmatterValue(agentConfig as Record)
+ const prompt = typeof (agentConfig as Record).prompt === "string"
+ ? ((agentConfig as Record).prompt as string)
+ : ""
+
+ const frontmatter = yaml.dump(normalized, {
+ lineWidth: 1000,
+ noRefs: true,
+ sortKeys: true,
+ }).trim()
+
+ files.push({
+ fileName: `${configKey}.md`,
+ content: `---\n${frontmatter}\n---\n${prompt}\n`,
+ })
+ }
+
+ return files
+}
+
+function readManifest(manifestPath: string): AgentManifest {
+ if (!existsSync(manifestPath)) {
+ return { files: [] }
+ }
+
+ try {
+ const content = readFileSync(manifestPath, "utf-8")
+ const parsed = parseJsonc(content)
+ if (Array.isArray(parsed?.files)) {
+ return { files: parsed.files.filter((file) => typeof file === "string") }
+ }
+ } catch {
+ // Ignore malformed manifests and rebuild from scratch.
+ }
+
+ return { files: [] }
+}
+
+function writeManifest(manifestPath: string, files: string[]): void {
+ writeFileSync(manifestPath, `${JSON.stringify({ files }, null, 2)}\n`, "utf-8")
+}
+
+export function resolveBootstrapTargetDir(directory: string): string {
+ const projectPluginConfig = detectPluginConfigFile(join(directory, ".opencode"))
+ if (projectPluginConfig.format !== "none") {
+ return join(directory, ".opencode")
+ }
+
+ return getOpenCodeConfigDir({ binary: "opencode" })
+}
+
+export async function syncBootstrapAgents(params: {
+ directory: string
+ pluginConfig: OhMyOpenCodeConfig
+ targetDir?: string
+}): Promise<{ agentCount: number; targetDir: string }> {
+ const targetDir = params.targetDir ?? resolveBootstrapTargetDir(params.directory)
+ const config: Record = { agent: {} }
+
+ const agentResult = await applyAgentConfig({
+ config,
+ pluginConfig: params.pluginConfig,
+ ctx: { directory: params.directory },
+ pluginComponents: createEmptyPluginComponents(),
+ })
+
+ applyToolConfig({
+ config,
+ pluginConfig: params.pluginConfig,
+ agentResult,
+ })
+
+ const files = buildBootstrapAgentFiles(agentResult)
+ const agentDir = join(targetDir, AGENT_DIRECTORY_NAME)
+ const manifestPath = join(targetDir, MANIFEST_FILE_NAME)
+ const previousManifest = readManifest(manifestPath)
+
+ mkdirSync(agentDir, { recursive: true })
+
+ const nextFileNames = new Set(files.map((file) => file.fileName))
+ for (const previousFile of previousManifest.files) {
+ if (nextFileNames.has(previousFile)) {
+ continue
+ }
+
+ rmSync(join(agentDir, previousFile), { force: true })
+ }
+
+ for (const file of files) {
+ writeFileSync(join(agentDir, file.fileName), file.content, "utf-8")
+ }
+
+ writeManifest(manifestPath, files.map((file) => file.fileName))
+
+ return {
+ agentCount: files.length,
+ targetDir,
+ }
+}