From 71886db9659ab8efe0b90f7b5396cb11c9c4df53 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:54:54 +0000 Subject: [PATCH 1/3] fix: clarify Roomote agent identities --- .../src/__tests__/system-prompt.test.ts | 11 +++++++++-- .../__tests__/fast-agent-prompt.test.ts | 16 ++++++++++++++-- .../src/server/fast-agent/fast-agent-prompt.ts | 2 +- .../workflows/__tests__/systemPrompt.test.ts | 6 +++++- packages/cloud-agents/src/style-guidance.ts | 2 +- packages/cloud-agents/src/system-prompt.ts | 2 +- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/cloud-agents/src/__tests__/system-prompt.test.ts b/packages/cloud-agents/src/__tests__/system-prompt.test.ts index 4c5673f1d..5da70c50d 100644 --- a/packages/cloud-agents/src/__tests__/system-prompt.test.ts +++ b/packages/cloud-agents/src/__tests__/system-prompt.test.ts @@ -1,9 +1,16 @@ import { ROOMOTE_SYSTEM_PROMPT } from '../system-prompt'; describe('ROOMOTE_SYSTEM_PROMPT', () => { - it('keeps Roomote identity without generic coding-agent policy', () => { + it('keeps the Roomote coding identity without conflicting identities or generic coding-agent policy', () => { expect(ROOMOTE_SYSTEM_PROMPT).toContain( - 'You are Roomote, a software engineering teammate.', + "You are Roomote's coding and workspace execution agent.", + ); + expect(ROOMOTE_SYSTEM_PROMPT).not.toContain('You are OpenCode'); + expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( + "You are Roomote's Fast conversational orchestrator", + ); + expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( + 'You are a deeply pragmatic, effective software engineer.', ); expect(ROOMOTE_SYSTEM_PROMPT).toContain( 'You are the product, not a generic assistant running inside a container.', diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts index 5b352c93d..a22d695a7 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts @@ -48,8 +48,16 @@ describe('buildFastAgentSystemPrompt', () => { }); expect(prompt).toContain( + "You are Roomote's Fast conversational orchestrator on Slack", + ); + expect(prompt).not.toContain('You are OpenCode'); + expect(prompt).not.toContain( + "You are Roomote's coding and workspace execution agent.", + ); + expect(prompt).not.toContain( 'You are a deeply pragmatic, effective software engineer.', ); + expect(prompt).toContain('Bring senior software engineering judgment'); expect(prompt).toContain('Roomote/example-app'); expect(prompt).toContain('Roomote/example-app [id: repo-1]'); expect(prompt).toContain( @@ -371,7 +379,9 @@ describe('buildFastAgentSystemPrompt', () => { surface: 'discord', }); - expect(prompt).toContain('fast mode on Discord'); + expect(prompt).toContain( + "Roomote's Fast conversational orchestrator on Discord", + ); expect(prompt).toContain('Emoji reactions are unavailable on this surface'); expect(prompt).not.toContain(''); expect(prompt).not.toContain( @@ -452,7 +462,9 @@ describe('buildFastAgentSystemPrompt', () => { platformEventVisibility: 'required', }); - expect(prompt).toContain('fast mode on a stored automation conversation'); + expect(prompt).toContain( + "Roomote's Fast conversational orchestrator on a stored automation conversation", + ); expect(prompt).toContain('Automation Platform Event'); expect(prompt).toContain('Execute the automation prompt now'); expect(prompt).not.toContain(''); diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts index a1e348f8d..4a9904ad9 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts @@ -142,7 +142,7 @@ export function buildFastAgentSystemPrompt({ ? `Roomote release ${releaseVersion}\n\n` : ''; - return `You are ${PRODUCT_NAME} in fast mode on ${surfaceName}. You are the conversational orchestrator for this conversation, not a router and not a transparent relay to a sandbox task. You own the conversation, answer directly when possible, and deliberately delegate execution work when useful. + return `You are ${PRODUCT_NAME}'s Fast conversational orchestrator on ${surfaceName}, not a router and not a transparent relay to a sandbox task. You own the conversation, answer directly when possible, and deliberately delegate execution work when useful. ${releaseIdentifier}## All Environments ${formatRepositoriesForPrompt(availableEnvironments)} diff --git a/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts b/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts index 69093a42e..4bda18885 100644 --- a/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts +++ b/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts @@ -7,7 +7,11 @@ import { DEFAULT_ROOMOTE_STYLE_GUIDANCE } from '../../../style-guidance'; describe('ROOMOTE_SYSTEM_PROMPT', () => { it('owns only Roomote-wide identity and personality', () => { expect(ROOMOTE_SYSTEM_PROMPT).toContain( - 'You are Roomote, a software engineering teammate.', + "You are Roomote's coding and workspace execution agent.", + ); + expect(ROOMOTE_SYSTEM_PROMPT).not.toContain('You are OpenCode'); + expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( + "You are Roomote's Fast conversational orchestrator", ); expect(ROOMOTE_SYSTEM_PROMPT).toContain('# Roomote Identity'); expect(ROOMOTE_SYSTEM_PROMPT).toContain('# Personality'); diff --git a/packages/cloud-agents/src/style-guidance.ts b/packages/cloud-agents/src/style-guidance.ts index 353f2c0b4..fbcc073ce 100644 --- a/packages/cloud-agents/src/style-guidance.ts +++ b/packages/cloud-agents/src/style-guidance.ts @@ -1,5 +1,5 @@ export const DEFAULT_ROOMOTE_STYLE_GUIDANCE = [ - 'You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail.', + 'Work with deep pragmatism and effectiveness. Bring senior software engineering judgment to repository and workspace work, take engineering quality seriously, and collaborate through direct, factual statements. Communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail.', 'You are guided by clarity, pragmatism, and rigor: make reasoning concrete, keep the end goal and momentum in view, and surface gaps or weak assumptions politely when doing so creates clarity. Prefer plain language over polished corporate phrasing.', 'You communicate concisely and respectfully, focusing on the task at hand. You prioritize actionable guidance, clearly state assumptions, environment prerequisites, and next steps, and avoid excessively verbose explanations unless explicitly asked. It is fine to sound lightly conversational as long as the work stays clear and grounded.', 'Use calibrated language when certainty would be fake. It is fine to say that something is probably fine, might be risky, or needs an edge case checked instead of overstating confidence.', diff --git a/packages/cloud-agents/src/system-prompt.ts b/packages/cloud-agents/src/system-prompt.ts index 2c3ce5208..c5c512008 100644 --- a/packages/cloud-agents/src/system-prompt.ts +++ b/packages/cloud-agents/src/system-prompt.ts @@ -6,7 +6,7 @@ */ import { buildRoomoteStyleGuidanceSection } from './style-guidance'; -const ROOMOTE_SYSTEM_PROMPT_TEMPLATE = `You are Roomote, a software engineering teammate. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. +const ROOMOTE_SYSTEM_PROMPT_TEMPLATE = `You are Roomote's coding and workspace execution agent. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. __ROOMOTE_RELEASE_IDENTIFIER__# Roomote Identity From 7c784bf027a41066fd3e2cc0c30435ec8da1dde2 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:18:12 +0000 Subject: [PATCH 2/3] fix: strip OpenCode identity from agent prompts --- apps/worker/src/run-task/agent-home.test.ts | 18 +++++ apps/worker/src/run-task/agent-home.ts | 8 +++ .../opencode-identity-plugin.test.ts | 70 +++++++++++++++++++ .../src/__tests__/system-prompt.test.ts | 11 +-- packages/cloud-agents/src/index.ts | 1 + .../src/opencode-identity-plugin.ts | 13 ++++ .../server/__tests__/opencode-runtime.test.ts | 3 + .../__tests__/fast-agent-prompt.test.ts | 16 +---- .../server/fast-agent/fast-agent-prompt.ts | 2 +- .../src/server/opencode-runtime.ts | 23 ++++++ .../workflows/__tests__/systemPrompt.test.ts | 6 +- packages/cloud-agents/src/style-guidance.ts | 2 +- packages/cloud-agents/src/system-prompt.ts | 2 +- 13 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts create mode 100644 packages/cloud-agents/src/opencode-identity-plugin.ts diff --git a/apps/worker/src/run-task/agent-home.test.ts b/apps/worker/src/run-task/agent-home.test.ts index 83e5abff8..abdd23924 100644 --- a/apps/worker/src/run-task/agent-home.test.ts +++ b/apps/worker/src/run-task/agent-home.test.ts @@ -14,6 +14,7 @@ import { generateOpenCodeConfig, seedRuntimeHomeMiseGlobalConfig, } from './agent-home'; +import { OPENCODE_IDENTITY_PLUGIN_SCRIPT } from '@roomote/cloud-agents'; describe('createIntegrationMcpInstructions', () => { it.each(['gbrain', 'supermemory'])( @@ -113,6 +114,23 @@ describe('generateOpenCodeConfig provider support', () => { return homeDir; } + it('installs the Roomote identity plugin for standard task sessions', () => { + const result = generateOpenCodeConfig({ + homeDir: createHomeDir(), + runtimeEnv: { + R_MODEL: 'openrouter/openai/gpt-5.6-terra', + OPENROUTER_API_KEY: 'openrouter-key', + }, + }); + + expect( + readFileSync( + join(result.openCodeConfigDir, 'plugins', 'roomote-identity.js'), + 'utf8', + ), + ).toBe(OPENCODE_IDENTITY_PLUGIN_SCRIPT); + }); + it('applies the per-task reasoning effort to a launch-time model override', () => { const result = generateOpenCodeConfig({ homeDir: createHomeDir(), diff --git a/apps/worker/src/run-task/agent-home.ts b/apps/worker/src/run-task/agent-home.ts index dbffe4a8d..e33ca716d 100644 --- a/apps/worker/src/run-task/agent-home.ts +++ b/apps/worker/src/run-task/agent-home.ts @@ -5,6 +5,7 @@ import * as path from 'node:path'; import { createRoomoteAdvisorAgentPrompt, createRoomoteJudgeAgentPrompt, + OPENCODE_IDENTITY_PLUGIN_SCRIPT, ROOMOTE_OPENCODE_ADVISOR_AGENT_DESCRIPTION, ROOMOTE_OPENCODE_ADVISOR_AGENT_NAME, ROOMOTE_OPENCODE_JUDGE_AGENT_DESCRIPTION, @@ -180,6 +181,8 @@ const ROOMOTE_OPENCODE_CHATGPT_GATEWAY_PLUGIN_FILE_NAME = const ROOMOTE_OPENCODE_TOOL_SAFETY_PLUGIN_FILE_NAME = 'roomote-tool-safety.js'; +const ROOMOTE_OPENCODE_IDENTITY_PLUGIN_FILE_NAME = 'roomote-identity.js'; + const OPENCODE_ALLOW_ALL_PERMISSION = { read: 'allow', edit: 'allow', @@ -653,6 +656,10 @@ function writeOpenCodeManagedFiles(openCodeConfigDir: string): void { pluginsDir, ROOMOTE_OPENCODE_TOOL_SAFETY_PLUGIN_FILE_NAME, ); + const identityPluginPath = path.join( + pluginsDir, + ROOMOTE_OPENCODE_IDENTITY_PLUGIN_FILE_NAME, + ); const silenceHookPath = path.join( openCodeConfigDir, ROOMOTE_OPENCODE_SLACK_SILENCE_HOOK_FILE_NAME, @@ -674,6 +681,7 @@ function writeOpenCodeManagedFiles(openCodeConfigDir: string): void { OPENCODE_TOOL_SAFETY_PLUGIN_SCRIPT, 'utf8', ); + fs.writeFileSync(identityPluginPath, OPENCODE_IDENTITY_PLUGIN_SCRIPT, 'utf8'); fs.writeFileSync(silenceHookPath, SLACK_SILENCE_HOOK_SCRIPT, 'utf8'); fs.writeFileSync(stopHookPath, SLACK_STOP_HOOK_SCRIPT, 'utf8'); fs.chmodSync(silenceHookPath, 0o755); diff --git a/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts b/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts new file mode 100644 index 000000000..64547c845 --- /dev/null +++ b/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts @@ -0,0 +1,70 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { OPENCODE_IDENTITY_PLUGIN_SCRIPT } from '../opencode-identity-plugin'; + +type SystemTransformHook = ( + input: unknown, + output: { system: string[] }, +) => Promise; + +describe('OPENCODE_IDENTITY_PLUGIN_SCRIPT', () => { + async function loadSystemTransformHook(): Promise { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'roomote-opencode-identity-plugin-'), + ); + const pluginPath = path.join(tempDir, 'roomote-identity.mjs'); + + try { + fs.writeFileSync(pluginPath, OPENCODE_IDENTITY_PLUGIN_SCRIPT, 'utf8'); + const plugin = (await import( + /* @vite-ignore */ pathToFileURL(pluginPath).href + )) as { + RoomoteOpenCodeIdentity: () => Promise<{ + 'experimental.chat.system.transform': SystemTransformHook; + }>; + }; + const hooks = await plugin.RoomoteOpenCodeIdentity(); + return hooks['experimental.chat.system.transform']; + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + + it.each(['OpenCode', 'opencode'])( + 'removes only the leading %s identity declaration', + async (productName) => { + const transform = await loadSystemTransformHook(); + const output = { + system: [ + `You are ${productName}, You and the user share the same workspace.\n\n## Editing Approach`, + 'You are Roomote in fast mode.', + ], + }; + + await transform({}, output); + + expect(output.system).toEqual([ + 'You and the user share the same workspace.\n\n## Editing Approach', + 'You are Roomote in fast mode.', + ]); + }, + ); + + it('does not rewrite non-leading or Roomote identity text', async () => { + const transform = await loadSystemTransformHook(); + const output = { + system: [ + 'Preserve this preface. You are OpenCode, as referenced in documentation.', + 'You are Roomote, a software engineering teammate.', + ], + }; + const original = structuredClone(output.system); + + await transform({}, output); + + expect(output.system).toEqual(original); + }); +}); diff --git a/packages/cloud-agents/src/__tests__/system-prompt.test.ts b/packages/cloud-agents/src/__tests__/system-prompt.test.ts index 5da70c50d..4c5673f1d 100644 --- a/packages/cloud-agents/src/__tests__/system-prompt.test.ts +++ b/packages/cloud-agents/src/__tests__/system-prompt.test.ts @@ -1,16 +1,9 @@ import { ROOMOTE_SYSTEM_PROMPT } from '../system-prompt'; describe('ROOMOTE_SYSTEM_PROMPT', () => { - it('keeps the Roomote coding identity without conflicting identities or generic coding-agent policy', () => { + it('keeps Roomote identity without generic coding-agent policy', () => { expect(ROOMOTE_SYSTEM_PROMPT).toContain( - "You are Roomote's coding and workspace execution agent.", - ); - expect(ROOMOTE_SYSTEM_PROMPT).not.toContain('You are OpenCode'); - expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( - "You are Roomote's Fast conversational orchestrator", - ); - expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( - 'You are a deeply pragmatic, effective software engineer.', + 'You are Roomote, a software engineering teammate.', ); expect(ROOMOTE_SYSTEM_PROMPT).toContain( 'You are the product, not a generic assistant running inside a container.', diff --git a/packages/cloud-agents/src/index.ts b/packages/cloud-agents/src/index.ts index a016e3e2a..5e3f5cdc1 100644 --- a/packages/cloud-agents/src/index.ts +++ b/packages/cloud-agents/src/index.ts @@ -16,6 +16,7 @@ export { export { resolveRoomoteReleaseVersion } from './release-version'; export * from './style-guidance'; export * from './opencode-prompt-subagents'; +export { OPENCODE_IDENTITY_PLUGIN_SCRIPT } from './opencode-identity-plugin'; export { DEFAULT_STANDARD_TASK_MODEL, DEFAULT_STANDARD_TASK_MODEL_PROVIDER, diff --git a/packages/cloud-agents/src/opencode-identity-plugin.ts b/packages/cloud-agents/src/opencode-identity-plugin.ts new file mode 100644 index 000000000..23e5d62db --- /dev/null +++ b/packages/cloud-agents/src/opencode-identity-plugin.ts @@ -0,0 +1,13 @@ +/** + * OpenCode 1.18.10 prefixes its model-specific system prompt with its own + * product identity. Roomote supplies the product identity in later prompt + * layers, so remove only that prefix and preserve the remaining instructions. + */ +export const OPENCODE_IDENTITY_PLUGIN_SCRIPT = `export const RoomoteOpenCodeIdentity = async () => ({ + 'experimental.chat.system.transform': async (_input, output) => { + if (typeof output.system?.[0] === 'string') { + output.system[0] = output.system[0].replace(/^You are OpenCode,\\s*/iu, ''); + } + }, +}); +`; diff --git a/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts b/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts index 470020002..41aa19914 100644 --- a/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts +++ b/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts @@ -208,6 +208,9 @@ describe('buildOpenCodeCliEnv', () => { ...NON_TASK_TOOL_PERMISSION_DENIALS, task: 'allow', }); + expect(config.plugin).toEqual([ + expect.stringMatching(/^file:\/\/.*roomote-identity\.mjs$/u), + ]); expect(Object.keys(config.agent)).toEqual(['advisor', 'judge']); for (const agentName of ['advisor', 'judge']) { diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts index a22d695a7..5b352c93d 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-prompt.test.ts @@ -48,16 +48,8 @@ describe('buildFastAgentSystemPrompt', () => { }); expect(prompt).toContain( - "You are Roomote's Fast conversational orchestrator on Slack", - ); - expect(prompt).not.toContain('You are OpenCode'); - expect(prompt).not.toContain( - "You are Roomote's coding and workspace execution agent.", - ); - expect(prompt).not.toContain( 'You are a deeply pragmatic, effective software engineer.', ); - expect(prompt).toContain('Bring senior software engineering judgment'); expect(prompt).toContain('Roomote/example-app'); expect(prompt).toContain('Roomote/example-app [id: repo-1]'); expect(prompt).toContain( @@ -379,9 +371,7 @@ describe('buildFastAgentSystemPrompt', () => { surface: 'discord', }); - expect(prompt).toContain( - "Roomote's Fast conversational orchestrator on Discord", - ); + expect(prompt).toContain('fast mode on Discord'); expect(prompt).toContain('Emoji reactions are unavailable on this surface'); expect(prompt).not.toContain(''); expect(prompt).not.toContain( @@ -462,9 +452,7 @@ describe('buildFastAgentSystemPrompt', () => { platformEventVisibility: 'required', }); - expect(prompt).toContain( - "Roomote's Fast conversational orchestrator on a stored automation conversation", - ); + expect(prompt).toContain('fast mode on a stored automation conversation'); expect(prompt).toContain('Automation Platform Event'); expect(prompt).toContain('Execute the automation prompt now'); expect(prompt).not.toContain(''); diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts index 4a9904ad9..a1e348f8d 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-prompt.ts @@ -142,7 +142,7 @@ export function buildFastAgentSystemPrompt({ ? `Roomote release ${releaseVersion}\n\n` : ''; - return `You are ${PRODUCT_NAME}'s Fast conversational orchestrator on ${surfaceName}, not a router and not a transparent relay to a sandbox task. You own the conversation, answer directly when possible, and deliberately delegate execution work when useful. + return `You are ${PRODUCT_NAME} in fast mode on ${surfaceName}. You are the conversational orchestrator for this conversation, not a router and not a transparent relay to a sandbox task. You own the conversation, answer directly when possible, and deliberately delegate execution work when useful. ${releaseIdentifier}## All Environments ${formatRepositoriesForPrompt(availableEnvironments)} diff --git a/packages/cloud-agents/src/server/opencode-runtime.ts b/packages/cloud-agents/src/server/opencode-runtime.ts index 3e329f8b4..714f43e9a 100644 --- a/packages/cloud-agents/src/server/opencode-runtime.ts +++ b/packages/cloud-agents/src/server/opencode-runtime.ts @@ -1,6 +1,10 @@ import { execFileSync, spawn, type ChildProcess } from 'node:child_process'; import { createHash } from 'node:crypto'; +import { mkdtempSync, writeFileSync } from 'node:fs'; import { createServer } from 'node:net'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { collectOpenRouterVariantModelAlias, @@ -28,6 +32,7 @@ import { ROOMOTE_OPENCODE_JUDGE_AGENT_DESCRIPTION, ROOMOTE_OPENCODE_JUDGE_AGENT_NAME, } from '../opencode-prompt-subagents'; +import { OPENCODE_IDENTITY_PLUGIN_SCRIPT } from '../opencode-identity-plugin'; import { FAST_AGENT_SUBAGENT_TOOL_FILTER } from './fast-agent/fast-agent-tool-policy'; const ESCAPE_CHARACTER = String.fromCharCode(27); @@ -221,6 +226,23 @@ type NonTaskOpenCodeRuntimeOptions = { promptOnlySubagents?: boolean; }; +let openCodeIdentityPluginUrl: string | undefined; + +function getOpenCodeIdentityPluginUrl(): string { + if (openCodeIdentityPluginUrl) { + return openCodeIdentityPluginUrl; + } + + const directory = mkdtempSync(join(tmpdir(), 'roomote-opencode-identity-')); + const pluginPath = join(directory, 'roomote-identity.mjs'); + writeFileSync(pluginPath, OPENCODE_IDENTITY_PLUGIN_SCRIPT, { + encoding: 'utf8', + mode: 0o600, + }); + openCodeIdentityPluginUrl = pathToFileURL(pluginPath).href; + return openCodeIdentityPluginUrl; +} + function buildRestrictedNonTaskConfig( options: NonTaskOpenCodeRuntimeOptions, ): Record { @@ -230,6 +252,7 @@ function buildRestrictedNonTaskConfig( return { agent: PROMPT_ONLY_SUBAGENTS, + plugin: [getOpenCodeIdentityPluginUrl()], permission: { ...NON_TASK_TOOL_PERMISSION_DENIALS, task: 'allow' }, }; } diff --git a/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts b/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts index 4bda18885..69093a42e 100644 --- a/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts +++ b/packages/cloud-agents/src/server/workflows/__tests__/systemPrompt.test.ts @@ -7,11 +7,7 @@ import { DEFAULT_ROOMOTE_STYLE_GUIDANCE } from '../../../style-guidance'; describe('ROOMOTE_SYSTEM_PROMPT', () => { it('owns only Roomote-wide identity and personality', () => { expect(ROOMOTE_SYSTEM_PROMPT).toContain( - "You are Roomote's coding and workspace execution agent.", - ); - expect(ROOMOTE_SYSTEM_PROMPT).not.toContain('You are OpenCode'); - expect(ROOMOTE_SYSTEM_PROMPT).not.toContain( - "You are Roomote's Fast conversational orchestrator", + 'You are Roomote, a software engineering teammate.', ); expect(ROOMOTE_SYSTEM_PROMPT).toContain('# Roomote Identity'); expect(ROOMOTE_SYSTEM_PROMPT).toContain('# Personality'); diff --git a/packages/cloud-agents/src/style-guidance.ts b/packages/cloud-agents/src/style-guidance.ts index fbcc073ce..353f2c0b4 100644 --- a/packages/cloud-agents/src/style-guidance.ts +++ b/packages/cloud-agents/src/style-guidance.ts @@ -1,5 +1,5 @@ export const DEFAULT_ROOMOTE_STYLE_GUIDANCE = [ - 'Work with deep pragmatism and effectiveness. Bring senior software engineering judgment to repository and workspace work, take engineering quality seriously, and collaborate through direct, factual statements. Communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail.', + 'You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail.', 'You are guided by clarity, pragmatism, and rigor: make reasoning concrete, keep the end goal and momentum in view, and surface gaps or weak assumptions politely when doing so creates clarity. Prefer plain language over polished corporate phrasing.', 'You communicate concisely and respectfully, focusing on the task at hand. You prioritize actionable guidance, clearly state assumptions, environment prerequisites, and next steps, and avoid excessively verbose explanations unless explicitly asked. It is fine to sound lightly conversational as long as the work stays clear and grounded.', 'Use calibrated language when certainty would be fake. It is fine to say that something is probably fine, might be risky, or needs an edge case checked instead of overstating confidence.', diff --git a/packages/cloud-agents/src/system-prompt.ts b/packages/cloud-agents/src/system-prompt.ts index c5c512008..2c3ce5208 100644 --- a/packages/cloud-agents/src/system-prompt.ts +++ b/packages/cloud-agents/src/system-prompt.ts @@ -6,7 +6,7 @@ */ import { buildRoomoteStyleGuidanceSection } from './style-guidance'; -const ROOMOTE_SYSTEM_PROMPT_TEMPLATE = `You are Roomote's coding and workspace execution agent. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. +const ROOMOTE_SYSTEM_PROMPT_TEMPLATE = `You are Roomote, a software engineering teammate. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled. __ROOMOTE_RELEASE_IDENTIFIER__# Roomote Identity From 50e9f0588fd2a802cc4eed00d2bb042b51e5bde6 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 27 Aug 2026 14:54:43 -0500 Subject: [PATCH 3/3] test: cover pinned OpenCode identity prompts --- .../opencode-identity-plugin.test.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts b/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts index 64547c845..f0c674b32 100644 --- a/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts +++ b/packages/cloud-agents/src/__tests__/opencode-identity-plugin.test.ts @@ -33,21 +33,40 @@ describe('OPENCODE_IDENTITY_PLUGIN_SCRIPT', () => { } } - it.each(['OpenCode', 'opencode'])( - 'removes only the leading %s identity declaration', - async (productName) => { + it.each([ + { + name: 'default prompt', + prompt: + 'You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.', + expected: + 'an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.', + }, + { + name: 'Anthropic prompt', + prompt: + 'You are OpenCode, the best coding agent on the planet.\n\nYou are an interactive CLI tool that helps users with software engineering tasks.', + expected: + 'the best coding agent on the planet.\n\nYou are an interactive CLI tool that helps users with software engineering tasks.', + }, + { + name: 'GPT prompt', + prompt: + "You are OpenCode, You and the user share the same workspace and collaborate to achieve the user's goals.", + expected: + "You and the user share the same workspace and collaborate to achieve the user's goals.", + }, + ])( + 'removes only the leading identity declaration from the $name', + async ({ prompt, expected }) => { const transform = await loadSystemTransformHook(); const output = { - system: [ - `You are ${productName}, You and the user share the same workspace.\n\n## Editing Approach`, - 'You are Roomote in fast mode.', - ], + system: [prompt, 'You are Roomote in fast mode.'], }; await transform({}, output); expect(output.system).toEqual([ - 'You and the user share the same workspace.\n\n## Editing Approach', + expected, 'You are Roomote in fast mode.', ]); },