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 packages/extension/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ tsconfig.json
**/*.map
!bin/**
!julia/**
!demo/**
21 changes: 15 additions & 6 deletions packages/extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <JULIA_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 <JULIA_PROJECT> /tmp/amicode-work/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.

Step 2 copies to solve.jl but you run /tmp/amicode-work/solve.jl here — have step 2 write to that path so they match.

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.

Fixed in e21539c (on #23, stacked above) — step 2 now writes /tmp/amicode-work/solve.jl, the exact path step 3 runs (mkdir -p /tmp/amicode-work && cp {{TEMPLATE_PATH}} /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/<lab>/<runId>/`.

There is **no MCP server**. The only tool is `amico-run` via bash.
`amico-run --help` prints usage.
Expand Down
49 changes: 49 additions & 0 deletions packages/extension/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -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/<lab-id>/<runId>/`, where `runId` is
`r<UTC-timestamp>Z-<hex>` (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=<n> f=<obj> inf_pr=<…> inf_du=<…>` line per Ipopt iteration (drives the live stats row), plus a final `DONE fidelity=<…>` line and any Julia traceback. |
| `iter_<N>.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/<lab-id>/`):

| File | Writer | Contents |
|---|---|---|
| `index` | amico-run (`appendIndex`) | Append-only, tab-separated `<runId>\t<createdAt>\t<scriptPath>` per run. |
| `latest` | amico-run (`updateLatest`) | Symlink → the most recent `<runId>`; 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).
35 changes: 35 additions & 0 deletions packages/extension/DEMO_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -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** | |
10 changes: 9 additions & 1 deletion packages/extension/RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
2 changes: 2 additions & 0 deletions packages/extension/demo/run/FINISHED
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
status = "completed"
exit_code = 0
Binary file added packages/extension/demo/run/iter_0000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0010.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0020.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0030.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0040.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0050.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/extension/demo/run/iter_0060.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions packages/extension/demo/run/manifest.toml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions packages/extension/demo/run/result.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
iterations = 60
fidelity = 0.9999788203047787
wall_seconds = 101.5023238658905
4 changes: 4 additions & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
{
"command": "amicode.restartServer",
"title": "Amicode: Restart opencode server"
},
{
"command": "amicode.replayDemo",
"title": "Amicode: Replay demo run"
}
],
"configuration": {
Expand Down
34 changes: 34 additions & 0 deletions packages/extension/src/demo_replay.ts
Original file line number Diff line number Diff line change
@@ -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;
}
18 changes: 18 additions & 0 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -139,6 +140,23 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
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)"}`);
Expand Down
9 changes: 9 additions & 0 deletions packages/extension/test/agents_md.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
35 changes: 35 additions & 0 deletions packages/extension/test/demo_replay.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
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
})
})
2 changes: 2 additions & 0 deletions packages/extension/test/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading