From c8c2695895521bab524cb7d580dee13234405fed Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 28 May 2026 18:54:56 -0400 Subject: [PATCH] fix(kimaki): stop injecting Data Machine agent prompts --- bridges/kimaki/plugins/dm-agent-sync.ts | 173 ++---------------------- lib/repair-opencode-json.py | 4 +- runtimes/opencode.sh | 2 + tests/dm-agent-sync.mjs | 33 ++--- upgrade.sh | 2 +- 5 files changed, 26 insertions(+), 188 deletions(-) diff --git a/bridges/kimaki/plugins/dm-agent-sync.ts b/bridges/kimaki/plugins/dm-agent-sync.ts index 0735682..d3e45d2 100644 --- a/bridges/kimaki/plugins/dm-agent-sync.ts +++ b/bridges/kimaki/plugins/dm-agent-sync.ts @@ -1,12 +1,13 @@ -// dm-agent-sync.ts — OpenCode plugin that syncs Data Machine agents into -// OpenCode's agent switcher. +// dm-agent-sync.ts — OpenCode plugin that refreshes Data Machine memory. // -// On session start, queries Data Machine for all registered agents and their -// file paths, then registers each as an OpenCode agent with the correct -// identity files (SOUL.md, MEMORY.md, USER.md, SITE.md) and AGENTS.md. +// On session start, asks Data Machine to recompose its memory files so +// OpenCode's top-level `instructions` and AGENTS.md auto-discovery read fresh +// content. Identity stays owned by Data Machine/channel routing; this plugin +// must not set OpenCode `agent.*.prompt` fields or register Data Machine agents +// into OpenCode's agent switcher. // -// This gives every Data Machine agent its own identity in the agent switcher -// without manual opencode.json maintenance. +// The filename is kept for upgrade compatibility with existing opencode.json +// plugin arrays that reference dm-agent-sync.ts. // // How to use: // Add to opencode.json: "plugin": ["path/to/dm-agent-sync.ts"] @@ -17,32 +18,9 @@ */ import type { Plugin } from "@opencode-ai/plugin"; -interface DmAgent { - agent_id: number; - agent_slug: string; - agent_name: string; - owner_id: number; - status?: string; - agent_config?: { - default_model?: string; - tool_policy?: Record; - model?: { - default?: { - provider?: string; - model?: string; - }; - }; - }; -} - -interface DmPaths { - agent_slug: string; - relative_files: string[]; -} - const dmAgentSync: Plugin = async ({ $ }) => { return { - config: async (config) => { + config: async () => { const sitePath = getSitePath(); const wpAvailable = await $`command -v wp`.quiet().nothrow(); if (wpAvailable.exitCode !== 0) { @@ -62,109 +40,6 @@ const dmAgentSync: Plugin = async ({ $ }) => { if (composeResult.exitCode !== 0) { console.warn(`[dm-agent-sync] memory compose failed (exit ${composeResult.exitCode}): ${await shellOutputText(composeResult)}`); } - - // Query all agents from Data Machine. - const agentsResult = sitePath - ? await $`wp --path=${sitePath} datamachine agents list --format=json --allow-root`.quiet().nothrow() - : await $`wp datamachine agents list --format=json --allow-root`.quiet().nothrow(); - if (agentsResult.exitCode !== 0) { - console.warn(`[dm-agent-sync] agents list failed (exit ${agentsResult.exitCode}): ${await shellOutputText(agentsResult)}`); - return; - } - - const agentsRaw = await shellOutputText(agentsResult); - const jsonMatch = agentsRaw.match(/\[[\s\S]*\]/); - if (!jsonMatch) { - console.warn("[dm-agent-sync] agents list did not contain a JSON array"); - return; - } - - let agents: DmAgent[]; - try { - agents = JSON.parse(jsonMatch[0]); - } catch (error) { - console.warn(`[dm-agent-sync] agents list returned invalid JSON: ${String(error)}`); - return; - } - - const entries = []; - for (const agent of agents) { - const status = agent.status || "active"; - if (status !== "active") { - continue; - } - - const pathsResult = sitePath - ? await $`wp --path=${sitePath} datamachine memory paths --agent=${agent.agent_slug} --format=json --allow-root`.quiet().nothrow() - : await $`wp datamachine memory paths --agent=${agent.agent_slug} --format=json --allow-root`.quiet().nothrow(); - if (pathsResult.exitCode !== 0) { - console.warn(`[dm-agent-sync] memory paths failed for ${agent.agent_slug} (exit ${pathsResult.exitCode}): ${await shellOutputText(pathsResult)}`); - continue; - } - - let paths: DmPaths; - try { - paths = JSON.parse(await shellOutputText(pathsResult)); - } catch (error) { - console.warn(`[dm-agent-sync] memory paths returned invalid JSON for ${agent.agent_slug}: ${String(error)}`); - continue; - } - - if (!paths?.relative_files?.length) { - console.warn(`[dm-agent-sync] memory paths returned no files for ${agent.agent_slug}`); - continue; - } - - const promptRoot = sitePath || "."; - const prompt = [ - `{file:${promptRoot}/AGENTS.md}`, - ...paths.relative_files.map((f: string) => `{file:${promptRoot}/${f}}`), - ].join("\n"); - - const agentModel = - agent.agent_config?.default_model || - (agent.agent_config?.model?.default - ? `${agent.agent_config.model.default.provider}/${agent.agent_config.model.default.model}` - : undefined); - const tools = agent.agent_config?.tool_policy; - const entry: Record = { - prompt, - mode: "primary" as const, - }; - if (agentModel) { - entry.model = agentModel; - } - if (tools) { - entry.tools = tools; - } - - entries.push({ agent, entry, prompt }); - } - - if (!entries.length) { - console.warn(`[dm-agent-sync] no active Data Machine agents with usable memory paths found (${agents.length} listed)`); - return; - } - - if (!config.agent) { - config.agent = {}; - } - - const primary = entries[0]; - syncDefaultSlot(config.agent, "build", primary.entry); - syncDefaultSlot(config.agent, "plan", primary.entry); - - for (const { agent, entry } of entries) { - const agentSlug = agent.agent_slug; - if (!config.agent[agentSlug]) { - config.agent[agentSlug] = { - ...entry, - description: `Data Machine agent: ${agent.agent_name}`, - }; - } - } - - console.warn(`[dm-agent-sync] registered ${entries.length} Data Machine agent(s); build/plan prompt uses ${primary.agent.agent_slug}`); }, }; }; @@ -173,36 +48,6 @@ function getSitePath(): string { return process.env.DATAMACHINE_SITE_PATH || process.env.SITE_PATH || process.env.PWD || ""; } -/** - * Populate build/plan defaults without clobbering user-authored fields. - * - * @param {Record} agentConfig - OpenCode agent config object. - * @param {"build"|"plan"} slot - Default slot to synchronize. - * @param {Record} managedEntry - Data Machine-managed entry. - */ -function syncDefaultSlot( - agentConfig: Record, - slot: "build" | "plan", - managedEntry: Record -): void { - const existing = agentConfig[slot]; - if (!existing || typeof existing !== "object" || Array.isArray(existing)) { - agentConfig[slot] = { ...managedEntry }; - return; - } - - const existingEntry = existing as Record; - if (typeof existingEntry.prompt === "string" && existingEntry.prompt.length > 0) { - return; - } - - agentConfig[slot] = { - ...managedEntry, - ...existingEntry, - prompt: managedEntry.prompt, - }; -} - async function shellOutputText(output: any): Promise { if (typeof output.text === "function") { return output.text(); diff --git a/lib/repair-opencode-json.py b/lib/repair-opencode-json.py index 52a1bfc..6f83241 100755 --- a/lib/repair-opencode-json.py +++ b/lib/repair-opencode-json.py @@ -87,8 +87,8 @@ def expected_plugins( # "drift" comparisons on those runtimes are no-ops. return plugins - # DM context filter + agent sync: only when the bridge is Kimaki, since - # these plugins rewrite Kimaki-specific prompts. wp-coding-agents does + # DM context filter + memory compose: only when the bridge is Kimaki, since + # these plugins manage Kimaki/OpenCode prompt hygiene. wp-coding-agents does # not manage opencode-claude-auth on any bridge — Kimaki ships its own # AnthropicAuthPlugin, and non-kimaki bridges use opencode's native auth # flow. See wp-coding-agents#117. diff --git a/runtimes/opencode.sh b/runtimes/opencode.sh index 3df6e55..8661669 100644 --- a/runtimes/opencode.sh +++ b/runtimes/opencode.sh @@ -176,6 +176,8 @@ runtime_generate_config() { # OpenCode plugins. wp-coding-agents only manages plugins it owns end to # end: dm-context-filter.ts and dm-agent-sync.ts on Kimaki bridges. The + # sync plugin is intentionally compose-only; Data Machine/channel routing + # owns identity, not OpenCode agent slots. # opencode-claude-auth plugin is intentionally NOT installed on any bridge: # Kimaki ships a built-in AnthropicAuthPlugin and non-kimaki bridges use # opencode's native auth flow (`opencode auth login anthropic`). See diff --git a/tests/dm-agent-sync.mjs b/tests/dm-agent-sync.mjs index c87f807..a413e1c 100644 --- a/tests/dm-agent-sync.mjs +++ b/tests/dm-agent-sync.mjs @@ -1,4 +1,4 @@ -// tests/dm-agent-sync.mjs — unit smoke tests for the Kimaki DM agent sync plugin. +// tests/dm-agent-sync.mjs — unit smoke tests for the Kimaki DM memory plugin. import assert from "node:assert/strict" import dmAgentSync from "../bridges/kimaki/plugins/dm-agent-sync.ts" @@ -49,17 +49,9 @@ async function runConfig(config, responses) { return warnings } -const agentsJson = JSON.stringify([ - { agent_id: 1, agent_slug: "franklin", agent_name: "Franklin", owner_id: 1, status: "active" }, - { agent_id: 2, agent_slug: "julia", agent_name: "Julia", owner_id: 1, status: "active" }, -]) - const commonResponses = [ [/^command -v wp$/, output("/usr/local/bin/wp")], [/^wp --path=\/tmp\/datamachine-site datamachine memory compose/, output("composed")], - [/^wp --path=\/tmp\/datamachine-site datamachine agents list/, output(`${agentsJson}\nTotal: 2 agent(s).`)], - [/--agent=franklin /, output(JSON.stringify({ agent_slug: "franklin", relative_files: ["SITE.md", "SOUL.md"] }))], - [/--agent=julia /, output(JSON.stringify({ agent_slug: "julia", relative_files: ["SITE.md", "MEMORY.md"] }))], ] { @@ -71,12 +63,12 @@ const commonResponses = [ }, } const warnings = await runConfig(config, commonResponses) - assert.match(config.agent.build.prompt, /\{file:\/tmp\/datamachine-site\/AGENTS\.md\}/) - assert.match(config.agent.plan.prompt, /\{file:\/tmp\/datamachine-site\/SOUL\.md\}/) + assert.equal(config.agent.build.prompt, undefined) + assert.equal(config.agent.plan.prompt, undefined) assert.equal(config.agent.build.model, "anthropic/claude-opus-4-7") - assert.match(config.agent.franklin.prompt, /\{file:\/tmp\/datamachine-site\/SITE\.md\}/) - assert.match(config.agent.julia.description, /Data Machine agent: Julia/) - assert.ok(warnings.some((line) => line.includes("registered 2 Data Machine agent(s)"))) + assert.equal(config.agent.franklin, undefined) + assert.equal(config.agent.julia, undefined) + assert.deepEqual(warnings, []) } { @@ -88,8 +80,8 @@ const commonResponses = [ } await runConfig(config, commonResponses) assert.deepEqual(config.agent.build.tools, { bash: true }) - assert.match(config.agent.build.prompt, /\{file:\/tmp\/datamachine-site\/SOUL\.md\}/) - assert.match(config.agent.plan.prompt, /\{file:\/tmp\/datamachine-site\/SOUL\.md\}/) + assert.equal(config.agent.build.prompt, undefined) + assert.equal(config.agent.plan.prompt, undefined) } { @@ -100,18 +92,17 @@ const commonResponses = [ } await runConfig(config, commonResponses) assert.equal(config.agent.build.prompt, "custom prompt") - assert.match(config.agent.plan.prompt, /\{file:\/tmp\/datamachine-site\/SOUL\.md\}/) + assert.equal(config.agent.plan, undefined) } { const config = {} const warnings = await runConfig(config, [ [/^command -v wp$/, output("/usr/local/bin/wp")], - [/^wp --path=\/tmp\/datamachine-site datamachine memory compose/, output("composed")], - [/^wp --path=\/tmp\/datamachine-site datamachine agents list/, output("", 1, "db down")], + [/^wp --path=\/tmp\/datamachine-site datamachine memory compose/, output("", 1, "db down")], ]) - assert.ok(warnings.some((line) => line.includes("agents list failed"))) + assert.ok(warnings.some((line) => line.includes("memory compose failed"))) assert.equal(config.agent, undefined) } -console.log("OK: dm-agent-sync injects DM prompts and logs failures") +console.log("OK: dm-agent-sync refreshes memory without injecting agent prompts") diff --git a/upgrade.sh b/upgrade.sh index 3d20681..95cd0f8 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -379,7 +379,7 @@ check_opencode_json_drift() { # Runs whenever opencode.json exists on disk. Default behaviour is # additive repair: managed plugin entries the user is missing get added - # (dm-context-filter.ts and dm-agent-sync.ts on Kimaki bridges), and + # (dm-context-filter.ts and compose-only dm-agent-sync.ts on Kimaki bridges), and # legacy agent.build.prompt / agent.plan.prompt get migrated to a # top-level `instructions` array (fixes Anthropic Claude Max OAuth, # wp-coding-agents#60).