diff --git a/.gitignore b/.gitignore index 1e09804a..6668b41b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ node_modules/ .opencode/USER/* !.opencode/USER/README.md !.opencode/USER/.gitkeep +.opencode/PAI/USER/* +!.opencode/PAI/USER/README.md # Security patterns (user customizes) .opencode/PAISECURITYSYSTEM/patterns.yaml diff --git a/.opencode/PAI/Tools/pai.ts b/.opencode/PAI/Tools/pai.ts index a2d975f5..68c396b5 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 @@ -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 5a96ae78..47fd4455 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,8 +259,13 @@ 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/ + // 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) @@ -370,18 +382,30 @@ 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 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; + } + + 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();