From c7dd17b6d4ac7a8e60cec12d660b9ca7e7dffcad Mon Sep 17 00:00:00 2001 From: Raghav Chari Date: Wed, 17 Jun 2026 16:07:41 -0400 Subject: [PATCH] fix(extension): inject AGENTS.md via opencode instructions so chat actually runs solves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chat behaved like vanilla opencode — 'exploring the codebase', never authoring a script or running amico-run, so the Run Inspector was never fed. Root cause: the extension wrote AGENTS.md into a temp project dir, but opencode runs the chat session in the VS Code workspace folder (e.g. ~/amico), which has no AGENTS.md — so the amico solve workflow never reached the agent. Fix (directory-independent): inject the amico AGENTS.md via opencode's `instructions` config, loaded for every session regardless of its cwd. - buildOpencodeConfigContent(agentsPath) → OPENCODE_CONFIG_CONTENT spawn env; merges over the user's global config (Bedrock model/provider preserved). - AGENTS.md references the template by absolute {{TEMPLATE_PATH}} (session cwd is the workspace, not the temp dir); solve script already goes to /tmp. - resolveJuliaProject defaults to ~/.amico/julia when unset (the VS Code config default '' slipped past the old `?? "UNSET"`, rendering a blank --project). - Drop the now-vestigial temp-dir template copy + .opencode/opencode.json. Verified: /config shows instructions merged with the global model; and a live probe in an empty workspace (no AGENTS.md) had the agent answer 'help a quantum-control researcher synthesize optimal-control pulses with Piccolo...' — the injected persona, confirming directory-independent delivery. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/extension/AGENTS.md | 6 +- packages/extension/package.json | 2 +- packages/extension/src/extension.ts | 12 ++- packages/extension/src/opencode_config.ts | 74 +++++++++++++------ packages/extension/test/agents_md.test.ts | 4 + .../extension/test/opencode_config.test.ts | 46 ++++++++---- 6 files changed, 100 insertions(+), 44 deletions(-) diff --git a/packages/extension/AGENTS.md b/packages/extension/AGENTS.md index 6392dafc..1b7519a2 100644 --- a/packages/extension/AGENTS.md +++ b/packages/extension/AGENTS.md @@ -6,7 +6,8 @@ and the Run Inspector renders the live solve. ## Workflow (this is the whole job) -1. Read the bundled template `solve_template.jl` in this project dir. +1. Read the bundled template `solve_template.jl` at its absolute path: + `{{TEMPLATE_PATH}}`. 2. Copy it to a working file (e.g. `solve.jl`) and fill in the `# FILL IN` parameter block from the user's request: transmon frequency `ω` (GHz), anharmonicity `δ` (GHz), `levels`, the target gate, gate time `T` (ns), @@ -55,8 +56,7 @@ correct loader in this Piccolo. ## Julia project The Julia project to pass as `--project` is: -**{{JULIA_PROJECT}}**. Always pass it. If it reads `UNSET`, omit `--project` -and tell the user `amicode.juliaProject` is not configured. +**{{JULIA_PROJECT}}**. Always pass it. ## Style diff --git a/packages/extension/package.json b/packages/extension/package.json index ecc0cd4b..40568403 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -93,7 +93,7 @@ "amicode.juliaProject": { "type": "string", "default": "", - "description": "Julia project (--project) the agent passes to amico-run. Empty = agent omits --project." + "description": "Julia project (--project) the agent passes to amico-run. Empty = defaults to ~/.amico/julia (the provisioned project)." }, "amicode.runsRoot": { "type": "string", diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 74484dce..b5e785c2 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -7,7 +7,7 @@ import { ChatPanel } from "./chat_panel"; import { registerRunInspector } from "./run_inspector"; import { registerTrees } from "./trees"; import { StatusBarManager } from "./status_bar"; -import { prepareOpencodeProject } from "./opencode_config"; +import { prepareOpencodeProject, resolveJuliaProject, buildOpencodeConfigContent } from "./opencode_config"; import { resolveAmicoRunBinDir, resolveRunsRoot } from "./opencode_paths"; import { OpencodeEventClient } from "./sse_client"; import { RunsRootWatcher } from "./file_watcher"; @@ -55,7 +55,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const opencodeProject = prepareOpencodeProject({ agentsSrc: path.resolve(ctx.extensionPath, "AGENTS.md"), templateSrc: path.resolve(ctx.extensionPath, "templates", "solve_template.jl"), - juliaProject: vscode.workspace.getConfiguration("amicode").get("juliaProject", ""), + juliaProject: resolveJuliaProject( + vscode.workspace.getConfiguration("amicode").get("juliaProject", ""), + ), }); opencodeChannel.appendLine(`[boot] opencode project dir: ${opencodeProject.projectDir}`); opencodeChannel.appendLine(`[boot] AGENTS.md: ${opencodeProject.agentsPath}`); @@ -96,6 +98,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { cwd: opencodeProject.projectDir, env: { PATH: `${amicoRunBinDir ? amicoRunBinDir + ":" : ""}${process.env.PATH ?? ""}`, + // Inject the amico solve workflow as opencode `instructions` (loaded for + // every session regardless of its cwd) — merges over the user's global + // config, so the model/provider are preserved. This is what makes the + // chat actually author + run solves instead of behaving like vanilla + // opencode (the session cwd is the workspace, not opencodeProject.projectDir). + OPENCODE_CONFIG_CONTENT: buildOpencodeConfigContent(opencodeProject.agentsPath), }, channel: opencodeChannel, }); diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 40b08f90..ec22c3a1 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -6,49 +6,75 @@ import * as os from "node:os"; // Prepare a per-session opencode project directory. // // opencode invokes amico-run via its built-in `bash` tool — no MCP, no -// callback HTTP. We deliver into the session: (a) AGENTS.md (auto-loaded LLM -// context, with the Julia project path substituted in), and (b) the vetted -// solve_template.jl the agent copies + fills in. PATH augmentation (so -// `amico-run` resolves) happens at spawn time in extension.ts. +// callback HTTP. The amico solve workflow (AGENTS.md) reaches the agent via +// opencode's `instructions` config (see buildOpencodeConfigContent + the +// OPENCODE_CONFIG_CONTENT spawn env in extension.ts), which is loaded for +// every session regardless of the session's working directory. opencode's web +// UI runs the session in the VS Code workspace folder, NOT this temp dir, so +// the temp dir exists only to hold the substituted AGENTS.md — the absolute +// path `instructions` points at. PATH augmentation (so `amico-run` resolves) +// happens at spawn time in extension.ts. +// +// LIFETIME INVARIANT: opencode reads the `instructions` file lazily, per +// message — and a missing file fails *silently* (empty instruction set → +// regression to vanilla opencode). The temp dir is created at activate() and +// is never cleaned by the extension, so it persists for the server's lifetime. +// Do NOT add temp-dir cleanup without moving AGENTS.md somewhere equally durable. // ============================================================================ +/** Resolve the Julia project (--project) the agent should pass. A configured, + * non-empty value wins (trimmed); otherwise default to the β.4-provisioned + * project at ~/.amico/julia. (The VS Code config default is "", which `??` + * does NOT catch — hence an explicit empty check rather than a nullish one.) */ +export function resolveJuliaProject(configValue: string): string { + const v = configValue.trim(); + return v === "" ? path.join(os.homedir(), ".amico", "julia") : v; +} + +/** Build the OPENCODE_CONFIG_CONTENT value: a config object that injects the + * amico AGENTS.md as a top-level `instructions` entry. opencode MERGES this + * over the user's global config (model/provider preserved) for every session, + * independent of the session's working directory. */ +export function buildOpencodeConfigContent(agentsPath: string): string { + return JSON.stringify({ + $schema: "https://opencode.ai/config.json", + instructions: [agentsPath], + }); +} + export interface OpencodeConfigOptions { - /** Absolute path to packages/extension/AGENTS.md to copy into the project dir. */ + /** Absolute path to packages/extension/AGENTS.md to substitute + write into the project dir. */ agentsSrc: string; - /** Absolute path to the vetted solve_template.jl to copy into the project dir. */ + /** Absolute path to the vetted solve_template.jl. Substituted into AGENTS.md + * as {{TEMPLATE_PATH}} (the agent reads it there; it is not copied). */ templateSrc: string; - /** Julia project (--project) the agent should use; substituted into AGENTS.md. - * undefined → "UNSET" (AGENTS.md tells the agent to omit --project). */ + /** Julia project (--project) the agent should use; already resolved (see + * resolveJuliaProject). Substituted into AGENTS.md as {{JULIA_PROJECT}}. */ juliaProject: string | undefined; } export interface OpencodeProject { projectDir: string; agentsPath: string; + /** The vetted template the agent reads — the bundled source (absolute), not a copy. */ templatePath: string; } export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodeProject { const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "amicode-v2-")); - fs.mkdirSync(path.join(projectDir, ".opencode"), { recursive: true }); - // AGENTS.md: read → substitute {{JULIA_PROJECT}} → write (auto-loaded by opencode). + // AGENTS.md: read → substitute {{JULIA_PROJECT}} + {{TEMPLATE_PATH}} → write. + // This file is the target of opencode's `instructions` config (absolute path). const agentsPath = path.join(projectDir, "AGENTS.md"); const raw = fs.existsSync(opts.agentsSrc) ? fs.readFileSync(opts.agentsSrc, "utf8") - : "# Amicode\nRead solve_template.jl, fill params, run `amico-run