Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ dist/
.vscode-test/
*.log
packages/extension/vendor/
packages/extension/bin/
71 changes: 0 additions & 71 deletions AGENTS.md

This file was deleted.

1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules/**
esbuild.config.mjs
tsconfig.json
**/*.map
!bin/**
56 changes: 56 additions & 0 deletions packages/extension/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Amicode project context

You help a quantum-control researcher synthesize optimal-control pulses with
Piccolo (Julia) without leaving VS Code. You author a Julia script, run it,
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.
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),
timesteps `N`, `max_iter`. **Parameters live in the script — never in this
file.** If the user gives a `lab.toml` path, read it in the script.
3. Run it via the `bash` tool: `amico-run --project <JULIA_PROJECT> solve.jl`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invocation omits --lab and the template writes no params, so a run can't report which lab/params it actually solved.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two parts: params — fixed in a5121a9, the template now writes a [params] table (delta/levels/T/N/drive_max/max_iter) into result.toml, so each run is self-describing. --lab — added in e21539c (on #23, stacked above): the AGENTS.md invocation now passes --lab default so the run records its lab.

(use the project path provided below). `amico-run` takes only a script path
and runner flags — it parses **no** physics options; all the physics lives
in the script you wrote.
4. When it finishes, quote the final `DONE fidelity=…` line. If F ≥ 0.99 the
extension prompts promotion automatically — don't ask.

There is **no MCP server**. The only tool is `amico-run` via bash.
`amico-run --help` prints usage.

## The run-dir contract your script MUST emit

`amico-run` writes `manifest.toml` (first) and `FINISHED` (last) itself. Your
script, running with cwd = the run dir, must emit:

- `AMICODE_ITER iter=<n> f=<obj> inf_pr=<…> inf_du=<…>` to stdout, flushed,
once per Ipopt iteration (drives the live stats row).
- `iter_<N>.png` every few iterations (the live plot the Inspector shows).
- `result.toml`, written **atomically** (write `result.toml.tmp`, then `mv`),
with at least `fidelity` (float) and `iterations` (int).
- `pulse.jld2` (the solved pulse) via `JLD2.save`.
- a final `DONE fidelity=<…>` line.

The template already does all of this — you only fill in numbers.

## Warm-start idiom

To seed from a previous solve: `traj = load_traj("path/to/pulse.jld2")` and
pass it as the initial guess to the problem constructor. `load_traj` is the
correct loader in this Piccolo.

## Julia project

<!-- AMICO_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.

## Style

Terse — the user is a quantum-control researcher. On failure, read the run's
`run.log` for the Julia traceback before guessing. Don't suggest installing
Julia packages; the environment is provisioned.
16 changes: 16 additions & 0 deletions packages/extension/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { build, context } from "esbuild";
import { cpSync, mkdirSync, existsSync, chmodSync } from "node:fs";

const watch = process.argv.includes("--watch");

// Stage amico-run so `bin/launcher/amico-run` finds `bin/dist/amico-run.js`
// (the launcher execs node "$DIR/../dist/amico-run.js"). Package-time artifact;
// the dev Extension Host uses the sibling-launcher fallback (resolveAmicoRunBinDir).
// Runs in both build and --watch (CWD = the package dir under `pnpm run`).
const arRoot = "../amico-run";
if (existsSync(`${arRoot}/dist/amico-run.js`)) {
mkdirSync("bin/launcher", { recursive: true });
mkdirSync("bin/dist", { recursive: true });
cpSync(`${arRoot}/launcher/amico-run`, "bin/launcher/amico-run", { dereference: true });
cpSync(`${arRoot}/dist/amico-run.js`, "bin/dist/amico-run.js", { dereference: true });
chmodSync("bin/launcher/amico-run", 0o755); // guarantee +x survives pack/unpack
} else {
console.warn("[esbuild] amico-run/dist not built — run `pnpm --filter @amicode/amico-run build` before packaging");
}

const targets = [
// extension host entry point
{
Expand Down
67 changes: 48 additions & 19 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"engines": {
"vscode": "^1.95.0"
},
"categories": ["Other"],
"categories": [
"Other"
],
"activationEvents": [
"onCommand:amicode.openChat",
"onCommand:amicode.openInspector",
Expand Down Expand Up @@ -37,18 +39,43 @@
},
"views": {
"amicode": [
{ "id": "amicode.vault", "name": "Vault", "type": "tree" },
{ "id": "amicode.catalog", "name": "Catalog", "type": "tree" },
{ "id": "amicode.armonia", "name": "Armonia", "type": "tree" }
{
"id": "amicode.vault",
"name": "Vault",
"type": "tree"
},
{
"id": "amicode.catalog",
"name": "Catalog",
"type": "tree"
},
{
"id": "amicode.armonia",
"name": "Armonia",
"type": "tree"
}
],
"amicode-panel": [
{ "id": "amicode.runInspector", "name": "Run Inspector", "type": "webview" }
{
"id": "amicode.runInspector",
"name": "Run Inspector",
"type": "webview"
}
]
},
"commands": [
{ "command": "amicode.openChat", "title": "Amicode: Open Chat" },
{ "command": "amicode.openInspector", "title": "Amicode: Open Run Inspector" },
{ "command": "amicode.restartServer", "title": "Amicode: Restart opencode server" }
{
"command": "amicode.openChat",
"title": "Amicode: Open Chat"
},
{
"command": "amicode.openInspector",
"title": "Amicode: Open Run Inspector"
},
{
"command": "amicode.restartServer",
"title": "Amicode: Restart opencode server"
}
],
"configuration": {
"title": "Amicode",
Expand All @@ -60,31 +87,33 @@
},
"amicode.juliaProject": {
"type": "string",
"default": "/tmp/amicode-spike-julia",
"description": "Julia project root (--project=...) the run_julia tool will use."
"default": "",
"description": "Julia project (--project) the agent passes to amico-run. Empty = agent omits --project."
},
"amicode.juliaScript": {
"amicode.runsRoot": {
"type": "string",
"default": "",
"description": "Absolute path to spike_solve.jl. If empty, looks up via the extensionPath/../amicode/julia/spike_solve.jl convention."
"description": "Runs root the inspector watches. Empty = ~/.amico/runs/default (must match where amico-run writes)."
}
}
}
},
"scripts": {
"build": "node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"build": "node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test": "vitest run --passWithNoTests --exclude '**/slow/**'",
"test:slow": "vitest run test/slow",
"test:smoke": "node test/boot_smoke.mjs",
"fetch:opencode": "node scripts/fetch_opencode.mjs"
},
"devDependencies": {
"@amicode/amico-run": "workspace:*",
"@types/node": "^22.0.0",
"@types/node": "^22.0.0",
"@types/vscode": "^1.95.0",
"esbuild": "^0.24.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
"esbuild": "^0.24.0",
"smol-toml": "^1.3.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
}
}
46 changes: 20 additions & 26 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { registerRunInspector } from "./run_inspector";
import { registerTrees } from "./trees";
import { StatusBarManager } from "./status_bar";
import { prepareOpencodeProject } from "./opencode_config";
import { resolveAmicoRunBinDir, resolveRunsRoot } from "./opencode_paths";
import { OpencodeEventClient } from "./sse_client";
import { RunsRootWatcher } from "./file_watcher";

Expand All @@ -26,33 +27,38 @@ let sseClient: OpencodeEventClient | undefined;
let watcher: RunsRootWatcher | undefined;
let opencodeReadyUrl: URL | undefined;

const DEFAULT_RUNS_ROOT = "/tmp/amicode-runs";

export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
const opencodeChannel = vscode.window.createOutputChannel("Amicode — opencode");
const runsChannel = vscode.window.createOutputChannel("Amicode — runs");
ctx.subscriptions.push(opencodeChannel, runsChannel);

// Runs root (resolved early — the inspector needs it for its CSP resource roots).
const runsRoot = resolveRunsRoot(vscode.workspace.getConfiguration("amicode").get<string>("runsRoot", ""));

// 1. UI surfaces
registerTrees(ctx);
registerRunInspector(ctx);
registerRunInspector(ctx, runsRoot);
statusBar = new StatusBarManager();
ctx.subscriptions.push({ dispose: () => statusBar?.dispose() });

// 2. Start watching the runs root immediately — solves may already exist
// from prior dev-host sessions, and watchers are cheap.
const runsRoot = vscode.workspace.getConfiguration("amicode").get<string>("runsRoot", DEFAULT_RUNS_ROOT);
fs.mkdirSync(runsRoot, { recursive: true });
watcher = new RunsRootWatcher({ runsRoot, channel: runsChannel, statusBar });
watcher.start();
ctx.subscriptions.push(watcher);

// 3. opencode project bootstrap
const binDir = path.resolve(ctx.extensionPath, "bin");
const agentsSrc = path.resolve(ctx.extensionPath, "AGENTS.md");
const opencodeProject = prepareOpencodeProject({ binDir, agentsSrc });
const amicoRunBinDir = resolveAmicoRunBinDir(ctx.extensionPath);
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<string>("juliaProject", ""),
});
opencodeChannel.appendLine(`[boot] opencode project dir: ${opencodeProject.projectDir}`);
opencodeChannel.appendLine(`[boot] AGENTS.md: ${opencodeProject.agentsPath}`);
opencodeChannel.appendLine(`[boot] template: ${opencodeProject.templatePath}`);

// 4. Spawn opencode — the VENDORED binary by default (spec §4; S35, kills
// Assumption 4). Config override is a dev-only escape hatch. On a missing
Expand All @@ -78,16 +84,17 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}

if (binary !== undefined) {
const juliaScript = resolveJuliaScript(ctx);
const juliaProject = resolveJuliaProject();
// amico-run is argv-only (β.1) — no AMICO_* env propagation (S37). The agent
// gets the Julia project from AGENTS.md (substituted at session-copy time)
// and passes it as `--project`. PATH just needs to resolve the launcher.
if (amicoRunBinDir === undefined) {
opencodeChannel.appendLine(`[boot] WARNING: amico-run launcher not found — chat can author but solves won't run (build amico-run or check the VSIX)`);
}
serverManager = new ServerManager({
binary,
cwd: opencodeProject.projectDir,
env: {
PATH: `${binDir}:${process.env.PATH ?? ""}`,
AMICO_JULIA_SCRIPT: juliaScript,
AMICO_JULIA_PROJECT: juliaProject,
AMICO_RUNS_ROOT: runsRoot,
PATH: `${amicoRunBinDir ? amicoRunBinDir + ":" : ""}${process.env.PATH ?? ""}`,
},
channel: opencodeChannel,
});
Expand Down Expand Up @@ -134,7 +141,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}),
);

opencodeChannel.appendLine(`[boot] activated; runsRoot=${runsRoot}; binDir=${binDir}`);
opencodeChannel.appendLine(`[boot] activated; runsRoot=${runsRoot}; amicoRunBinDir=${amicoRunBinDir ?? "(none)"}`);
}

export function deactivate(): void {
Expand All @@ -144,16 +151,3 @@ export function deactivate(): void {
statusBar?.dispose();
}

// ---------------------------------------------------------------------------

function resolveJuliaScript(ctx: vscode.ExtensionContext): string {
const fromCfg = vscode.workspace.getConfiguration("amicode").get<string>("juliaScript", "");
if (fromCfg && fs.existsSync(fromCfg)) return fromCfg;
const sibling = path.resolve(ctx.extensionPath, "..", "amicode", "julia", "spike_solve.jl");
if (fs.existsSync(sibling)) return sibling;
return path.join(ctx.extensionPath, "dist", "julia", "spike_solve.jl");
}

function resolveJuliaProject(): string {
return vscode.workspace.getConfiguration("amicode").get<string>("juliaProject", "/tmp/amicode-spike-julia");
}
Loading
Loading