diff --git a/packages/extension/.vscodeignore b/packages/extension/.vscodeignore index 3fa77d69..87173b62 100644 --- a/packages/extension/.vscodeignore +++ b/packages/extension/.vscodeignore @@ -11,3 +11,4 @@ tsconfig.json **/*.map !bin/** !julia/** +!demo/** diff --git a/packages/extension/AGENTS.md b/packages/extension/AGENTS.md index e2584f13..6392dafc 100644 --- a/packages/extension/AGENTS.md +++ b/packages/extension/AGENTS.md @@ -12,12 +12,21 @@ and the Run Inspector renders the live solve. 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 solve.jl` - (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. +3. Run it **detached** so the chat doesn't block on the ~minutes-long solve: + ```bash + mkdir -p /tmp/amicode-work + ( nohup amico-run --project /tmp/amicode-work/solve.jl \ + > /tmp/amicode-work/solve.log 2>&1 < /dev/null & ) + ``` + (use the project path provided below). The outer subshell returns in <1s. + `amico-run` takes only a script path and runner flags — it parses **no** + physics options; all the physics lives in the script you wrote. Then + immediately tell the user: **"Solve launched — watch the Run Inspector + (first run may take a few minutes while Julia warms up)."** +4. Do **not** block on the solve. The Run Inspector streams iterations + the + final fidelity from the run directory, and prompts promotion itself when + F ≥ 0.99 — don't ask. If asked for the result later, read the latest run's + `FINISHED` + `result.toml` under `~/.amico/runs///`. There is **no MCP server**. The only tool is `amico-run` via bash. `amico-run --help` prints usage. diff --git a/packages/extension/CONTRACT.md b/packages/extension/CONTRACT.md new file mode 100644 index 00000000..8ad02d8c --- /dev/null +++ b/packages/extension/CONTRACT.md @@ -0,0 +1,49 @@ +# Amicode run-dir contract — β freeze (wk-3, Phase-β DoD) + +This is the **frozen** contract every Amicode solve emits and the Run Inspector +consumes. It is the seam between the orchestrator (`@amicode/amico-run`, β.1), +the bundled agent (`AGENTS.md` + `solve_template.jl`, β.3), and the extension's +watcher/inspector. **Frozen for the β phase** — changes after wk-3 go through +Phase 0' (the SchemaPackage supersedes the provisional validators below). + +## Layout + +A run lives at `~/.amico/runs///`, where `runId` is +`rZ-` (e.g. `r20260617-161814Z-e8cb`). `amico-run` writes +`manifest.toml` **first** and `FINISHED` **last**; the script (cwd = the run dir) +emits the rest. + +| Artifact | Writer | Contents | +|---|---|---| +| `manifest.toml` | amico-run (first) | `schema_version = "1"`, snake_case keys: `run_id`, `lab`, `lab_id`, `script_path`, `created_at`, `orchestrator_version`, and a `[julia]` table (`binary`, optional `project`/`sysimage`). | +| `run.log` | amico-run (stdout tee) | One `AMICODE_ITER iter= f= inf_pr=<…> inf_du=<…>` line per Ipopt iteration (drives the live stats row), plus a final `DONE fidelity=<…>` line and any Julia traceback. | +| `iter_.png` | script | Per-iteration pulse/fidelity plot. `N` is the iteration with **unbounded digits** (`iter_0`, `iter_10`, … `iter_0060`). The inspector globs `iter_*.png`. | +| `result.toml` | script (atomic) | Written `result.toml.tmp` then renamed. At least `fidelity` (float) and `iterations` (int); `wall_seconds` optional. | +| `FINISHED` | amico-run (last, terminal) | `status = "completed" | "failed" | "aborted"` and `exit_code` (int). Its presence is the **only** completion signal — the inspector fires `onFinished` solely on a valid `FINISHED`, so a killed solve shows "running", never a false success. | + +Two convenience files live at the **lab runs root** (`~/.amico/runs//`): + +| File | Writer | Contents | +|---|---|---| +| `index` | amico-run (`appendIndex`) | Append-only, tab-separated `\t\t` per run. | +| `latest` | amico-run (`updateLatest`) | Symlink → the most recent ``; written via temp-then-rename so the watcher sees an atomic swing. The inspector follows `latest`. | + +## Frozen schemas + +The provisional validators in `@amicode/amico-run` are the β source of truth: + +- `validateManifest` — `schema_version === "1"`; the six string keys non-empty; a `[julia]` table with a string `binary`. +- `validateFinished` — `status` ∈ {completed, failed, aborted}; integer `exit_code`. +- `validateResult` — numeric `fidelity`; integer `iterations`. + +These are **frozen for β**. The Phase 0' SchemaPackage replaces them; any contract +change before then is a breaking change to the watcher and must be coordinated. + +## Exit codes (amico-run) + +- `0` — `FINISHED.status == "completed"`. +- `130` — `aborted` (SIGINT/SIGTERM, e.g. the inspector's stop control). +- `64` — usage/config error, orchestrator fault (any unexpected throw), or a + missing `FINISHED` (write fault). +- otherwise — the Julia process's return code (a `failed` run; `1` if it failed + with a zero return code). diff --git a/packages/extension/DEMO_CHECKLIST.md b/packages/extension/DEMO_CHECKLIST.md new file mode 100644 index 00000000..47eff76e --- /dev/null +++ b/packages/extension/DEMO_CHECKLIST.md @@ -0,0 +1,35 @@ +# Amicode demo acceptance checklist (β.6) + +Sign off **before** the live demo. The presenter runs this on the target machine +after the [RUNBOOK](./RUNBOOK.md) install. Pre-flight (rows 1–3) is the safety +net; rows 4–6 are the live run. + +## Pre-flight (arm the fallback first) + +- [ ] **Install clean** — followed `RUNBOOK.md` end-to-end on the target machine; total time recorded below (target ≤ 60 min). +- [ ] **Healthcheck green** — `node packages/extension/scripts/healthcheck.mjs` exits `0` (julia + pinned Piccolo project · opencode `/event` · `amico-run` · Bedrock creds). +- [ ] **Fallback armed** — Command Palette → **"Amicode: Replay demo run"** stages the bundled solve and the Run Inspector renders it (iter frames + final fidelity + promote prompt), with **no Julia, no opencode, no creds**. Confirm this works *before* relying on the live path. + +## Live run + +- [ ] **Chat → script** — a chat prompt makes the agent read the template, author `solve.jl`, and launch `amico-run` **detached** (`( nohup … & )`); the chat returns immediately with "Solve launched — watch the Run Inspector" and is **not** blocked by the solve. +- [ ] **Inspector streams** — the Run Inspector shows `AMICODE_ITER` rows advancing + `iter_*.png` frames updating while the solve runs in the background. +- [ ] **Fidelity shown** — on completion the inspector reports the final fidelity (F ≥ 0.99 → promote prompt fires automatically). + +## Definition-of-Done (Phase β) + +- [ ] **Contract frozen** — the run-dir contract + provisional schemas are frozen and documented in [`CONTRACT.md`](./CONTRACT.md). +- [ ] **Timings recorded** — clean-machine install + first-solve timings written into `RUNBOOK.md` (cold first run pays Julia precompile/JIT on top of the warm ~100 s solve). + +--- + +**Recorded timings (fill in at the dry-run):** + +| Step | Time | +|---|---| +| Julia install | | +| `install.sh` (instantiate + precompile + VSIX) | | +| Healthcheck | | +| First live solve (cold) | | +| Replay fallback | instant | +| **Total** | | diff --git a/packages/extension/RUNBOOK.md b/packages/extension/RUNBOOK.md index 1af74535..09f4f599 100644 --- a/packages/extension/RUNBOOK.md +++ b/packages/extension/RUNBOOK.md @@ -18,4 +18,12 @@ the dominant cost is the first Julia precompile. - `✗ amico-run` → `pnpm -r build` (stages `bin/`) or reinstall the VSIX. - `✗ LLM creds` → fix the opencode model/provider + AWS creds, then re-run. -> Actual timings are recorded during the β.6 demo dry-run. +## Fallback (live solve or creds fail on-site) + +If the live solve stalls, or Bedrock creds / opencode are unavailable at demo +time, run **Command Palette → "Amicode: Replay demo run"**. It stages a bundled +pre-baked converged solve into the runs root and the Run Inspector renders it +(iteration frames + final fidelity + promote prompt) — with **no Julia, no +opencode, and no credentials**. Arm it first (see `DEMO_CHECKLIST.md`). + +> Actual timings are recorded during the β.6 demo dry-run (see `DEMO_CHECKLIST.md`). diff --git a/packages/extension/demo/run/FINISHED b/packages/extension/demo/run/FINISHED new file mode 100644 index 00000000..a09f7c1d --- /dev/null +++ b/packages/extension/demo/run/FINISHED @@ -0,0 +1,2 @@ +status = "completed" +exit_code = 0 diff --git a/packages/extension/demo/run/iter_0000.png b/packages/extension/demo/run/iter_0000.png new file mode 100644 index 00000000..e23d51d3 Binary files /dev/null and b/packages/extension/demo/run/iter_0000.png differ diff --git a/packages/extension/demo/run/iter_0010.png b/packages/extension/demo/run/iter_0010.png new file mode 100644 index 00000000..078dab2e Binary files /dev/null and b/packages/extension/demo/run/iter_0010.png differ diff --git a/packages/extension/demo/run/iter_0020.png b/packages/extension/demo/run/iter_0020.png new file mode 100644 index 00000000..9e023fd6 Binary files /dev/null and b/packages/extension/demo/run/iter_0020.png differ diff --git a/packages/extension/demo/run/iter_0030.png b/packages/extension/demo/run/iter_0030.png new file mode 100644 index 00000000..e59b2452 Binary files /dev/null and b/packages/extension/demo/run/iter_0030.png differ diff --git a/packages/extension/demo/run/iter_0040.png b/packages/extension/demo/run/iter_0040.png new file mode 100644 index 00000000..ecf3caa7 Binary files /dev/null and b/packages/extension/demo/run/iter_0040.png differ diff --git a/packages/extension/demo/run/iter_0050.png b/packages/extension/demo/run/iter_0050.png new file mode 100644 index 00000000..28b5145f Binary files /dev/null and b/packages/extension/demo/run/iter_0050.png differ diff --git a/packages/extension/demo/run/iter_0060.png b/packages/extension/demo/run/iter_0060.png new file mode 100644 index 00000000..07718435 Binary files /dev/null and b/packages/extension/demo/run/iter_0060.png differ diff --git a/packages/extension/demo/run/manifest.toml b/packages/extension/demo/run/manifest.toml new file mode 100644 index 00000000..44342b0e --- /dev/null +++ b/packages/extension/demo/run/manifest.toml @@ -0,0 +1,11 @@ +schema_version = "1" +run_id = "r20260617-161814Z-e8cb" +script_path = "/Users/raghavchari/amicode/packages/extension/templates/solve_template.jl" +lab = "default" +lab_id = "default" +created_at = "2026-06-17T16:18:14.159Z" +orchestrator_version = "0.1.0" + +[julia] +binary = "julia" +project = "/Users/raghavchari/.amico/julia" diff --git a/packages/extension/demo/run/result.toml b/packages/extension/demo/run/result.toml new file mode 100644 index 00000000..c27b9486 --- /dev/null +++ b/packages/extension/demo/run/result.toml @@ -0,0 +1,3 @@ +iterations = 60 +fidelity = 0.9999788203047787 +wall_seconds = 101.5023238658905 diff --git a/packages/extension/package.json b/packages/extension/package.json index 844da464..ecc0cd4b 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -76,6 +76,10 @@ { "command": "amicode.restartServer", "title": "Amicode: Restart opencode server" + }, + { + "command": "amicode.replayDemo", + "title": "Amicode: Replay demo run" } ], "configuration": { diff --git a/packages/extension/src/demo_replay.ts b/packages/extension/src/demo_replay.ts new file mode 100644 index 00000000..ba06f6db --- /dev/null +++ b/packages/extension/src/demo_replay.ts @@ -0,0 +1,34 @@ +import { copyFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { generateRunId, appendIndex, updateLatest } from "@amicode/amico-run"; + +/** + * Copy a bundled demo run-dir into the runs root under a fresh β.1 runId, + * rewrite manifest.toml's `run_id` to match the new directory, append the + * index, and swing `latest` to it. Reuses the β.1 run-dir primitives so the + * staged run is byte-for-byte contract-identical and the existing + * RunsRootWatcher renders it exactly like a live solve. + * + * Filesystem side effects only (pure w.r.t. its inputs). Returns the staged + * run directory. + */ +export function stageDemoRun(demoDir: string, runsRoot: string): string { + mkdirSync(runsRoot, { recursive: true }); + const runId = generateRunId(runsRoot); + const runDir = join(runsRoot, runId); + mkdirSync(runDir); + for (const f of readdirSync(demoDir)) { + if (f === "manifest.toml") { + const m = readFileSync(join(demoDir, f), "utf8").replace( + /run_id\s*=\s*"[^"]*"/, + `run_id = ${JSON.stringify(runId)}`, + ); + writeFileSync(join(runDir, f), m); + } else { + copyFileSync(join(demoDir, f), join(runDir, f)); + } + } + appendIndex(runsRoot, runId, new Date().toISOString(), "demo-replay"); + updateLatest(runsRoot, runId); + return runDir; +} diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 2333ef6f..74484dce 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -11,6 +11,7 @@ import { prepareOpencodeProject } from "./opencode_config"; import { resolveAmicoRunBinDir, resolveRunsRoot } from "./opencode_paths"; import { OpencodeEventClient } from "./sse_client"; import { RunsRootWatcher } from "./file_watcher"; +import { stageDemoRun } from "./demo_replay"; // ============================================================================ // Extension entry point. Boot order on activate: @@ -139,6 +140,23 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.window.showErrorMessage(`Amicode: restart failed — ${(err as Error).message}`); } }), + // On-site fallback (β.6): stage the bundled pre-baked solve into the runs + // root. The watcher already running on runsRoot follows `latest` → + // ingestRunDir replays the converged solve — no Julia, no opencode, no creds. + vscode.commands.registerCommand("amicode.replayDemo", async () => { + const demoDir = path.join(ctx.extensionPath, "demo", "run"); + if (!fs.existsSync(path.join(demoDir, "FINISHED"))) { + void vscode.window.showErrorMessage("Amicode: demo run not bundled — reinstall the VSIX."); + return; + } + try { + const runDir = stageDemoRun(demoDir, runsRoot); + runsChannel.appendLine(`[demo] replayed → ${runDir}`); + await vscode.commands.executeCommand("amicode.runInspector.focus"); + } catch (e) { + void vscode.window.showErrorMessage(`Amicode: replay failed — ${(e as Error).message}`); + } + }), ); opencodeChannel.appendLine(`[boot] activated; runsRoot=${runsRoot}; amicoRunBinDir=${amicoRunBinDir ?? "(none)"}`); diff --git a/packages/extension/test/agents_md.test.ts b/packages/extension/test/agents_md.test.ts index 56fa288e..91a8aed5 100644 --- a/packages/extension/test/agents_md.test.ts +++ b/packages/extension/test/agents_md.test.ts @@ -9,6 +9,15 @@ describe('AGENTS.md teaches the D9/D10 script-authoring workflow', () => { expect(AGENTS).toMatch(/solve_template\.jl/) expect(AGENTS).toMatch(/amico-run .*solve\.jl/) // the actual invocation it teaches }) + it('teaches the portable detached launch (nohup + & in a subshell + watch inspector), not setsid', () => { + expect(AGENTS).toMatch(/nohup/) + expect(AGENTS).toMatch(/&\s*\)/) // backgrounded inside a subshell + expect(AGENTS).toMatch(/Run Inspector/) + expect(AGENTS).not.toMatch(/setsid/) // Linux-only; would silently break the macOS demo + }) + it('does not tell the agent to block on the solve', () => { + expect(AGENTS).not.toMatch(/wait for (the )?solve to finish/i) + }) it('documents the run-dir contract the script must emit', () => { expect(AGENTS).toMatch(/AMICODE_ITER/) expect(AGENTS).toMatch(/iter_.*\.png/) diff --git a/packages/extension/test/demo_replay.test.ts b/packages/extension/test/demo_replay.test.ts new file mode 100644 index 00000000..3ea1f90a --- /dev/null +++ b/packages/extension/test/demo_replay.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { mkdtempSync, writeFileSync, readFileSync, readlinkSync, existsSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { parse } from 'smol-toml' +import { validateManifest, validateFinished } from '@amicode/amico-run' +import { stageDemoRun } from '../src/demo_replay' + +function fakeDemo(): string { + const d = mkdtempSync(join(tmpdir(), 'demo-')) + writeFileSync(join(d, 'manifest.toml'), + `schema_version = "1"\nrun_id = "rDEMO"\nscript_path = "/demo.jl"\nlab = "default"\nlab_id = "default"\ncreated_at = "2026-06-17T00:00:00Z"\norchestrator_version = "0.1.0"\n[julia]\nbinary = "julia"\n`) + writeFileSync(join(d, 'run.log'), 'AMICODE_ITER iter=10 f=0.1 inf_pr=1e-8 inf_du=1e-6\n') + writeFileSync(join(d, 'iter_0010.png'), 'PNG') + writeFileSync(join(d, 'result.toml'), 'fidelity = 0.9999\niterations = 10\n') + writeFileSync(join(d, 'FINISHED'), 'status = "completed"\nexit_code = 0\n') + return d +} + +describe('stageDemoRun', () => { + it('stages the demo into a fresh runId, rewrites manifest run_id, swings latest', () => { + const demo = fakeDemo() + const runsRoot = mkdtempSync(join(tmpdir(), 'runs-')) + const runDir = stageDemoRun(demo, runsRoot) + const runId = runDir.split('/').pop()! + expect(runId).toMatch(/^r\d{8}-\d{6}Z-[0-9a-f]{4}$/) // β.1 runId format + expect(existsSync(join(runDir, 'iter_0010.png'))).toBe(true) + const m = parse(readFileSync(join(runDir, 'manifest.toml'), 'utf8')) as Record + expect(validateManifest(m).ok).toBe(true) + expect(m.run_id).toBe(runId) // rewritten to match the dir + expect(validateFinished(parse(readFileSync(join(runDir, 'FINISHED'), 'utf8'))).ok).toBe(true) + expect(readlinkSync(join(runsRoot, 'latest'))).toBe(runId) // the watcher will follow this + expect(readFileSync(join(runsRoot, 'index'), 'utf8')).toContain(runId) // appended to the index + }) +}) diff --git a/packages/extension/test/packaging.test.ts b/packages/extension/test/packaging.test.ts index 17d0cc6c..246e470e 100644 --- a/packages/extension/test/packaging.test.ts +++ b/packages/extension/test/packaging.test.ts @@ -11,6 +11,8 @@ const REQUIRED = [ 'extension/julia/Project.toml', 'extension/julia/Manifest.toml', 'extension/AGENTS.md', + 'extension/demo/run/manifest.toml', + 'extension/demo/run/FINISHED', ] // Guards against a silently-dropped runtime asset (the β.2 .gitignore-fallback