From 860e8c5f65dd927eb6dfa5953650f93db0e70490 Mon Sep 17 00:00:00 2001 From: Eddo van den Boom Date: Thu, 9 Apr 2026 01:39:46 +0200 Subject: [PATCH 1/4] separate "pai" command from "opencode" command (context loading) and fix content loading for "pai" --- .opencode/PAI/Tools/pai.ts | 4 ++-- .opencode/plugins/pai-unified.ts | 38 +++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.opencode/PAI/Tools/pai.ts b/.opencode/PAI/Tools/pai.ts index a2d975f5..e6c1390f 100755 --- a/.opencode/PAI/Tools/pai.ts +++ b/.opencode/PAI/Tools/pai.ts @@ -459,10 +459,10 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; skipPerms?: // Voice notification (using focused marker for calmer tone) notifyVoice(`[🎯 focused] ${getDAName()} here, ready to go.`); - // Launch OpenCode + // Launch OpenCode with PAI_ENABLED flag so plugin loads full context const proc = spawn(args, { stdio: ["inherit", "inherit", "inherit"], - env: { ...process.env }, + env: { ...process.env, PAI_ENABLED: "1" }, }); // Wait for OpenCode to exit diff --git a/.opencode/plugins/pai-unified.ts b/.opencode/plugins/pai-unified.ts index 5a96ae78..9f6681af 100644 --- a/.opencode/plugins/pai-unified.ts +++ b/.opencode/plugins/pai-unified.ts @@ -252,8 +252,11 @@ async function readFileSafe(filePath: string): Promise { */ async function loadMinimalBootstrap(): Promise { try { - const cwd = process.cwd(); - const paiDir = path.join(cwd, ".opencode", "PAI"); + // Resolve PAI directory relative to plugin location, not cwd + // Plugin is at ~/.opencode/plugins/pai-unified.ts + // PAI is at ~/.opencode/PAI/ + const pluginDir = path.dirname(__filename); + const paiDir = path.join(pluginDir, "..", "PAI"); const bootstrapPath = path.join(paiDir, "MINIMAL_BOOTSTRAP.md"); // Check if bootstrap exists (async) @@ -370,18 +373,27 @@ export const PaiUnified: Plugin = async (_ctx) => { await injectCompactionContext(input, output); }, - /** - * CONTEXT INJECTION (SessionStart equivalent) - * - * WP2: Injects minimal bootstrap (~7KB) instead of full 233KB context. - * Skills load on-demand via OpenCode native skill tool. - */ - "experimental.chat.system.transform": async (input, output) => { - try { - fileLog("Injecting minimal bootstrap context (WP2 lazy loading)..."); + /** + * CONTEXT INJECTION (SessionStart equivalent) + * + * WP2: Injects minimal bootstrap (~7KB) instead of full 233KB context. + * Skills load on-demand via OpenCode native skill tool. + * + * NOTE: Only loads when PAI_ENABLED=1 is set (via `pai` wrapper). + * Plain `opencode` launches without PAI context by design. + */ + "experimental.chat.system.transform": async (input, output) => { + try { + // Gate: Only load PAI context when explicitly enabled via pai wrapper + if (!process.env.PAI_ENABLED) { + fileLog("PAI context disabled (use 'pai' command for full context)", "info"); + return; + } + + fileLog("Injecting minimal bootstrap context (WP2 lazy loading)..."); - // Emit session start - emitSessionStart({ model: (input as any).model }).catch(() => {}); + // Emit session start + emitSessionStart({ model: (input as any).model }).catch(() => {}); // WP2: Use minimal bootstrap instead of full context loader const bootstrap = await loadMinimalBootstrap(); From 7ba5684cfe20bbe5ad1b66ebb3a2177b3e1c00f6 Mon Sep 17 00:00:00 2001 From: Eddo van den Boom Date: Thu, 9 Apr 2026 01:45:03 +0200 Subject: [PATCH 2/4] update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1e09804a..f5ec949e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ node_modules/ .opencode/USER/* !.opencode/USER/README.md !.opencode/USER/.gitkeep +.opencode/PAI/USER/* +!.opencode/PAI/USER/README.md +!.opencode/PAI/USER/.gitkeep # Security patterns (user customizes) .opencode/PAISECURITYSYSTEM/patterns.yaml From aeac3349e6cfa5fabbdacfd9907499adef1c49ce Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:27:34 +0200 Subject: [PATCH 3/4] fix(plugin): use ESM import.meta.url instead of __filename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .opencode/package.json declares "type": "module", which means the CommonJS globals __dirname / __filename are not defined at runtime in .opencode/plugins/pai-unified.ts. The original fix introduced a ReferenceError that would trigger the first time loadMinimalBootstrap() was called after a user ran the 'pai' command. Replaced with the ESM-idiomatic pattern: import { fileURLToPath } from 'node:url'; const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url)); PLUGIN_DIR is now resolved once at module load and reused inside loadMinimalBootstrap(). Same intent as the original fix (resolve PAI dir relative to plugin location, not cwd) — just compatible with the ESM module system. Co-authored-by: eddovandenboom --- .opencode/plugins/pai-unified.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.opencode/plugins/pai-unified.ts b/.opencode/plugins/pai-unified.ts index 9f6681af..8fe1784f 100644 --- a/.opencode/plugins/pai-unified.ts +++ b/.opencode/plugins/pai-unified.ts @@ -49,6 +49,13 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +// ESM-equivalent of __dirname — .opencode/package.json has "type": "module" +// so CommonJS globals (__dirname / __filename) are not defined. Used below +// by loadMinimalBootstrap() to resolve the PAI dir relative to this plugin +// file regardless of the current working directory opencode is launched from. +const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url)); import type { Hooks, Plugin } from "@opencode-ai/plugin"; import { captureAgentOutput, isTaskTool } from "./handlers/agent-capture"; import { validateAgentExecution } from "./handlers/agent-execution-guard"; @@ -252,11 +259,13 @@ async function readFileSafe(filePath: string): Promise { */ async function loadMinimalBootstrap(): Promise { try { - // Resolve PAI directory relative to plugin location, not cwd + // Resolve PAI directory relative to plugin location, not cwd. // Plugin is at ~/.opencode/plugins/pai-unified.ts - // PAI is at ~/.opencode/PAI/ - const pluginDir = path.dirname(__filename); - const paiDir = path.join(pluginDir, "..", "PAI"); + // PAI is at ~/.opencode/PAI/ + // PLUGIN_DIR is resolved via fileURLToPath(import.meta.url) because + // .opencode/package.json declares "type": "module" — __filename is + // not defined in ESM contexts. + const paiDir = path.join(PLUGIN_DIR, "..", "PAI"); const bootstrapPath = path.join(paiDir, "MINIMAL_BOOTSTRAP.md"); // Check if bootstrap exists (async) From 9f789f5a9aae30448273817645e218bf84a892bd Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:38:46 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix(pai):=20coderabbit=20review=20=E2=80=94?= =?UTF-8?q?=20gitignore,=20cmdPrompt=20env,=20explicit=20PAI=5FENABLED=20c?= =?UTF-8?q?heck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from CodeRabbit, all verified and fixed. - .gitignore: remove the spurious '!.opencode/PAI/USER/.gitkeep' exception. The .gitkeep file does not exist in PAI/USER/ (only README.md and several real directories like ACTIONS, BUSINESS, FLOWS, etc.). The exception was modelled on the .opencode/USER/ block which does ship a .gitkeep, but PAI/USER/ does not. The block is now: .opencode/PAI/USER/* !.opencode/PAI/USER/README.md - .opencode/PAI/Tools/pai.ts: cmdPrompt was missing PAI_ENABLED: '1' in its spawn env, so one-shot headless prompts (e.g. 'pai -p "..."') ran without the plugin injecting PAI context. Added the same env flag that cmdLaunch already had so both interactive and prompt modes load the full PAI bootstrap. - .opencode/plugins/pai-unified.ts: the PAI_ENABLED gate used a loose truthy check (!process.env.PAI_ENABLED) which would enable context injection for any non-empty value, including 'PAI_ENABLED=0' or 'PAI_ENABLED=false'. Replaced with an explicit strict equality check (process.env.PAI_ENABLED !== '1') that only activates for exactly the string '1', matching the contract set by the pai wrapper. Updated the log message accordingly. Co-authored-by: eddovandenboom --- .gitignore | 1 - .opencode/PAI/Tools/pai.ts | 4 +++- .opencode/plugins/pai-unified.ts | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index f5ec949e..6668b41b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ node_modules/ !.opencode/USER/.gitkeep .opencode/PAI/USER/* !.opencode/PAI/USER/README.md -!.opencode/PAI/USER/.gitkeep # Security patterns (user customizes) .opencode/PAISECURITYSYSTEM/patterns.yaml diff --git a/.opencode/PAI/Tools/pai.ts b/.opencode/PAI/Tools/pai.ts index e6c1390f..68c396b5 100755 --- a/.opencode/PAI/Tools/pai.ts +++ b/.opencode/PAI/Tools/pai.ts @@ -601,9 +601,11 @@ async function cmdPrompt(prompt: string) { process.chdir(OPENCODE_DIR); + // Set PAI_ENABLED=1 so the plugin injects bootstrap context in headless + // prompt mode, same as in interactive cmdLaunch. const proc = spawn(args, { stdio: ["inherit", "inherit", "inherit"], - env: { ...process.env }, + env: { ...process.env, PAI_ENABLED: "1" }, }); const exitCode = await proc.exited; diff --git a/.opencode/plugins/pai-unified.ts b/.opencode/plugins/pai-unified.ts index 8fe1784f..47fd4455 100644 --- a/.opencode/plugins/pai-unified.ts +++ b/.opencode/plugins/pai-unified.ts @@ -393,9 +393,12 @@ export const PaiUnified: Plugin = async (_ctx) => { */ "experimental.chat.system.transform": async (input, output) => { try { - // Gate: Only load PAI context when explicitly enabled via pai wrapper - if (!process.env.PAI_ENABLED) { - fileLog("PAI context disabled (use 'pai' command for full context)", "info"); + // Gate: Only load PAI context when the `pai` wrapper explicitly sets + // PAI_ENABLED=1. We check for the exact string "1" (not just truthy) + // so accidental env pollution (e.g. PAI_ENABLED=0, PAI_ENABLED=false) + // does not inadvertently enable context injection. + if (process.env.PAI_ENABLED !== "1") { + fileLog("PAI context disabled (PAI_ENABLED !== '1'; use 'pai' command for full context)", "info"); return; }