From 054e601167714f066593fb8804f60ddc8a6e21c0 Mon Sep 17 00:00:00 2001 From: Gautam Tayal Date: Sat, 11 Apr 2026 18:20:33 +0530 Subject: [PATCH] refactor(opencode): replace OpenCode config with agents markdown file --- .../__tests__/session-manager/restore.test.ts | 12 +++--- .../__tests__/session-manager/spawn.test.ts | 28 +++--------- packages/core/src/index.ts | 7 ++- packages/core/src/opencode-agents-md.ts | 43 +++++++++++++++++++ packages/core/src/opencode-config.ts | 29 ------------- packages/core/src/session-manager.ts | 24 ++--------- .../plugins/agent-opencode/src/index.test.ts | 8 +--- 7 files changed, 61 insertions(+), 90 deletions(-) create mode 100644 packages/core/src/opencode-agents-md.ts delete mode 100644 packages/core/src/opencode-config.ts diff --git a/packages/core/src/__tests__/session-manager/restore.test.ts b/packages/core/src/__tests__/session-manager/restore.test.ts index 4d8e9c6ce..413425e6d 100644 --- a/packages/core/src/__tests__/session-manager/restore.test.ts +++ b/packages/core/src/__tests__/session-manager/restore.test.ts @@ -5,7 +5,7 @@ import { } from "node:fs"; import { join } from "node:path"; import { createSessionManager } from "../../session-manager.js"; -import { getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js"; +import { getWorkspaceAgentsMdPath } from "../../opencode-agents-md.js"; import { writeMetadata, readMetadataRaw, @@ -475,10 +475,10 @@ describe("restore", () => { expect(createCall.launchCommand).toBe("claude --resume abc123"); }); - it("passes OPENCODE_CONFIG when restoring OpenCode orchestrators", async () => { + it("does not need extra OpenCode config when restoring OpenCode orchestrators", async () => { const wsPath = join(tmpDir, "ws-app-orchestrator-opencode-restore"); - mkdirSync(join(wsPath, ".ao"), { recursive: true }); - writeFileSync(getWorkspaceOpenCodeConfigPath(wsPath), "{}\n", "utf-8"); + mkdirSync(wsPath, { recursive: true }); + writeFileSync(getWorkspaceAgentsMdPath(wsPath), "## Agent Orchestrator\n", "utf-8"); const mockOpenCodeAgentWithRestore: Agent = { ...mockAgent, @@ -537,8 +537,8 @@ describe("restore", () => { expect(mockRuntime.create).toHaveBeenCalledWith( expect.objectContaining({ - environment: expect.objectContaining({ - OPENCODE_CONFIG: getWorkspaceOpenCodeConfigPath(wsPath), + environment: expect.not.objectContaining({ + OPENCODE_CONFIG: expect.any(String), }), }), ); diff --git a/packages/core/src/__tests__/session-manager/spawn.test.ts b/packages/core/src/__tests__/session-manager/spawn.test.ts index b4842fb9e..b0f1065af 100644 --- a/packages/core/src/__tests__/session-manager/spawn.test.ts +++ b/packages/core/src/__tests__/session-manager/spawn.test.ts @@ -3,7 +3,7 @@ import { mkdirSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { createSessionManager } from "../../session-manager.js"; import { validateConfig } from "../../config.js"; -import { OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, getWorkspaceOpenCodeConfigPath } from "../../opencode-config.js"; +import { getWorkspaceAgentsMdPath } from "../../opencode-agents-md.js"; import { writeMetadata, readMetadata, @@ -1710,7 +1710,7 @@ describe("spawn", () => { expect(readFileSync(promptFile, "utf-8")).toBe("You are the orchestrator."); }); - it("generates .ao/opencode.json and passes OPENCODE_CONFIG for OpenCode orchestrators", async () => { + it("writes workspace AGENTS.md for OpenCode orchestrators", async () => { const opencodeAgent: Agent = { ...mockAgent, name: "opencode", @@ -1746,33 +1746,15 @@ describe("spawn", () => { systemPrompt: "You are the orchestrator.", }); - const opencodeConfigPath = getWorkspaceOpenCodeConfigPath("/tmp/ws"); - expect(existsSync(opencodeConfigPath)).toBe(true); - expect(JSON.parse(readFileSync(opencodeConfigPath, "utf-8"))).toEqual({ - $schema: "https://opencode.ai/config.json", - default_agent: OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, - agent: { - [OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME]: { - description: "Agent Orchestrator internal orchestrator agent", - mode: "primary", - prompt: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"), - }, - }, - }); + const agentsMdPath = getWorkspaceAgentsMdPath("/tmp/ws"); + expect(existsSync(agentsMdPath)).toBe(true); + expect(readFileSync(agentsMdPath, "utf-8")).toContain("You are the orchestrator."); expect(opencodeAgent.getLaunchCommand).toHaveBeenCalledWith( expect.objectContaining({ systemPromptFile: expect.stringContaining("orchestrator-prompt-app-orchestrator-1.md"), }), ); - - expect(mockRuntime.create).toHaveBeenCalledWith( - expect.objectContaining({ - environment: expect.objectContaining({ - OPENCODE_CONFIG: opencodeConfigPath, - }), - }), - ); }); it("throws for unknown project", async () => { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2f4b43841..fd2cf4604 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -102,10 +102,9 @@ export { } from "./scm-webhook-utils.js"; export { asValidOpenCodeSessionId } from "./opencode-session-id.js"; export { - OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, - getWorkspaceOpenCodeConfigPath, - writeWorkspaceOpenCodeConfig, -} from "./opencode-config.js"; + getWorkspaceAgentsMdPath, + writeWorkspaceOpenCodeAgentsMd, +} from "./opencode-agents-md.js"; export { normalizeOrchestratorSessionStrategy } from "./orchestrator-session-strategy.js"; // Activity log — JSONL activity tracking for agents without native JSONL diff --git a/packages/core/src/opencode-agents-md.ts b/packages/core/src/opencode-agents-md.ts new file mode 100644 index 000000000..2609562a9 --- /dev/null +++ b/packages/core/src/opencode-agents-md.ts @@ -0,0 +1,43 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const AO_OPENCODE_SECTION_START = ""; +const AO_OPENCODE_SECTION_END = ""; + +export function getWorkspaceAgentsMdPath(workspacePath: string): string { + return join(workspacePath, "AGENTS.md"); +} + +function stripExistingAoOpenCodeSection(content: string): string { + const start = content.indexOf(AO_OPENCODE_SECTION_START); + const end = content.indexOf(AO_OPENCODE_SECTION_END); + if (start === -1 || end === -1 || end < start) { + return content; + } + + const before = content.slice(0, start).trimEnd(); + const after = content.slice(end + AO_OPENCODE_SECTION_END.length).trimStart(); + + if (before && after) return `${before}\n\n${after}`; + return before || after; +} + +export function writeWorkspaceOpenCodeAgentsMd(workspacePath: string, promptFile: string): string { + const agentsMdPath = getWorkspaceAgentsMdPath(workspacePath); + mkdirSync(workspacePath, { recursive: true }); + + const existing = existsSync(agentsMdPath) ? readFileSync(agentsMdPath, "utf-8") : ""; + const baseContent = stripExistingAoOpenCodeSection(existing).trimEnd(); + const prompt = readFileSync(promptFile, "utf-8").trim(); + const aoSection = [ + AO_OPENCODE_SECTION_START, + "## Agent Orchestrator", + "", + prompt, + AO_OPENCODE_SECTION_END, + ].join("\n"); + + const nextContent = baseContent ? `${baseContent}\n\n${aoSection}\n` : `${aoSection}\n`; + writeFileSync(agentsMdPath, nextContent, "utf-8"); + return agentsMdPath; +} diff --git a/packages/core/src/opencode-config.ts b/packages/core/src/opencode-config.ts deleted file mode 100644 index a72a66414..000000000 --- a/packages/core/src/opencode-config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -export const OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME = "__ao_orchestrator_internal"; - -export function getWorkspaceOpenCodeConfigPath(workspacePath: string): string { - return join(workspacePath, ".ao", "opencode.json"); -} - -export function writeWorkspaceOpenCodeConfig(workspacePath: string, promptFile: string): string { - const configPath = getWorkspaceOpenCodeConfigPath(workspacePath); - const configDir = join(workspacePath, ".ao"); - mkdirSync(configDir, { recursive: true }); - - const config = { - $schema: "https://opencode.ai/config.json", - default_agent: OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME, - agent: { - [OPENCODE_INTERNAL_ORCHESTRATOR_AGENT_NAME]: { - description: "Agent Orchestrator internal orchestrator agent", - mode: "primary", - prompt: `{file:${promptFile}}`, - }, - }, - }; - - writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8"); - return configPath; -} diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 57c40dc80..a27405ee9 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -63,9 +63,8 @@ import { } from "./paths.js"; import { asValidOpenCodeSessionId } from "./opencode-session-id.js"; import { - getWorkspaceOpenCodeConfigPath, - writeWorkspaceOpenCodeConfig, -} from "./opencode-config.js"; + writeWorkspaceOpenCodeAgentsMd, +} from "./opencode-agents-md.js"; import { normalizeOrchestratorSessionStrategy } from "./orchestrator-session-strategy.js"; import { sessionFromMetadata } from "./utils/session-from-metadata.js"; import { safeJsonParse } from "./utils/validation.js"; @@ -75,12 +74,6 @@ const execFileAsync = promisify(execFile); const OPENCODE_DISCOVERY_TIMEOUT_MS = 10_000; const OPENCODE_INTERACTIVE_DISCOVERY_TIMEOUT_MS = 10_000; -function getExistingWorkspaceOpenCodeConfigPath(workspacePath: string | null | undefined): string | undefined { - if (!workspacePath) return undefined; - const configPath = getWorkspaceOpenCodeConfigPath(workspacePath); - return existsSync(configPath) ? configPath : undefined; -} - function errorIncludesSessionNotFound(err: unknown): boolean { if (!(err instanceof Error)) return false; const e = err as Error & { stderr?: string; stdout?: string }; @@ -1386,10 +1379,9 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM } } - let openCodeConfigPath: string | undefined; if (plugins.agent.name === "opencode" && systemPromptFile) { try { - openCodeConfigPath = writeWorkspaceOpenCodeConfig(workspacePath, systemPromptFile); + writeWorkspaceOpenCodeAgentsMd(workspacePath, systemPromptFile); } catch (err) { await cleanupWorktreeAndMetadata(systemPromptFile); throw err; @@ -1439,9 +1431,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM const launchCommand = plugins.agent.getLaunchCommand(agentLaunchConfig); const environment = plugins.agent.getEnvironment(agentLaunchConfig); - if (openCodeConfigPath) { - environment["OPENCODE_CONFIG"] = openCodeConfigPath; - } // Create runtime — clean up worktree and metadata on failure let handle: RuntimeHandle; @@ -2421,10 +2410,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM // 7. Get launch command — try restore command first, fall back to fresh launch let launchCommand: string; - const openCodeConfigPath = - selectedAgent === "opencode" - ? getExistingWorkspaceOpenCodeConfigPath(workspacePath) - : undefined; const projectConfigForLaunch: ProjectConfig = { ...project, agentConfig: { @@ -2452,9 +2437,6 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM } const environment = plugins.agent.getEnvironment(agentLaunchConfig); - if (openCodeConfigPath) { - environment["OPENCODE_CONFIG"] = openCodeConfigPath; - } // 8. Create runtime (reuse tmuxName from metadata) const tmuxName = raw["tmuxName"]; diff --git a/packages/plugins/agent-opencode/src/index.test.ts b/packages/plugins/agent-opencode/src/index.test.ts index 00c8835c4..9226d2fb1 100644 --- a/packages/plugins/agent-opencode/src/index.test.ts +++ b/packages/plugins/agent-opencode/src/index.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { mkdirSync, writeFileSync } from "node:fs"; import { type Session, type RuntimeHandle, type AgentLaunchConfig } from "@aoagents/ao-core"; const { mockAppendActivityEntry, mockReadLastActivityEntry, mockRecordTerminalActivity } = @@ -756,14 +755,9 @@ describe("getRestoreCommand", () => { expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'"); }); - it("does not inject an internal agent when workspace opencode.json exists", async () => { - const workspacePath = "/tmp/test-restore-opencode-config"; - mkdirSync(`${workspacePath}/.ao`, { recursive: true }); - writeFileSync(`${workspacePath}/.ao/opencode.json`, "{}\n", "utf-8"); - + it("does not inject an internal agent during restore", async () => { const cmd = await agent.getRestoreCommand!( makeSession({ - workspacePath, metadata: { opencodeSessionId: "ses_abc123" }, }), { name: "proj", repo: "o/r", path: "/p", defaultBranch: "main", sessionPrefix: "p" },