From 2eb800a7c42fe712e5d03d47e4c452d5a3361fc2 Mon Sep 17 00:00:00 2001 From: Raghav Chari Date: Tue, 4 Aug 2026 20:56:17 -0400 Subject: [PATCH 01/10] cloud: route the HP tier to Altissimo-in-cloud automatically, and say so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting Piccolissimo + Altissimo is meant to mean "every solve runs Altissimo, in Harmoniqs Cloud". Four gaps between that promise and the code: 1. The solve template defaulted to `SOLVER = :ipopt`, so nothing actually selected the Altissimo backend — a paid HP run could quietly solve on IPOPT and look identical in the artifacts. The template is now STAGED at session prep with SOLVER substituted from the solver mode, so the backend follows the selected tier instead of being an authoring decision the agent can get wrong. The placeholder lives inside a string, so an unstaged template still parses and degrades to a local IPOPT solve rather than a syntax error. 2. Altissimo produced NO telemetry on the shipped stack. Piccolissimo 0.2.0 forwards a hardcoded kwarg whitelist to Altissimo.optimize! and drops `callback`, and `AltissimoOptions.verbose` defaults false — so neither the callback nor the iteration table existed, and a cloud solve reported iterations = 0 with an empty Run Inspector. Now: verbose = true, plus a stdout bridge that translates Altissimo's own table rows into AMICODE_ITER. Verified locally at 154 streamed iterations (objective 48 -> 7.8e-3) with the callback never firing. Where a newer Piccolissimo DOES forward the callback it supersedes the bridge, so there is one numbering scheme per run. 3. A defaulted `amico-run script.jl` under the HP tier exited 64 and relied on the agent reading the refusal and retrying with --executor remote — users saw that round-trip as a failed run. It is now PROMOTED to remote. An explicit --executor local is still refused, and a promotion with no cloud connection refuses with the Connections panel instead of failing deep in RemoteExecutor with a cloud.json path. 4. Nothing in the UI said where a run executed: a cloud run and a local run produced an identical Inspector pane. The topbar now reads " · Harmoniqs Cloud" for remote runs, keyed on remote.json (written only by RemoteExecutor) rather than a new run.toml field — run.schema.json is additionalProperties:false. Also corrects four agent-guidance claims that no longer matched the code (the local-launch refusal, the "AMICODE_ITER not available on cloud" note, and the quoted SOLVER line), and states the two real remaining limits: cooperative Stop needs the callback the cloud bundle does not forward, and re-rollout verification is skipped for cloud runs. 941 amico-run tests and the extension suite pass; the two live-model e2e tests are unrelated (they need a working provider). Co-Authored-By: Claude Opus 5 (1M context) --- packages/amico-run/src/launch.ts | 58 +++++-- packages/amico-run/test/solver_mode.test.ts | 80 +++++++-- .../scores/pulse-designer/templates/solve.jl | 156 +++++++++++++++++- packages/extension/src/opencode_config.ts | 74 ++++++--- packages/extension/src/routing.ts | 5 +- packages/extension/src/run_location.ts | 27 +++ packages/extension/src/runs_manager.ts | 5 +- packages/extension/test/agents_md.test.ts | 10 +- .../extension/test/opencode_config.test.ts | 88 +++++++++- .../test/remote_statemachine.test.ts | 5 +- packages/extension/test/run_location.test.ts | 48 ++++++ 11 files changed, 489 insertions(+), 67 deletions(-) create mode 100644 packages/extension/src/run_location.ts create mode 100644 packages/extension/test/run_location.test.ts diff --git a/packages/amico-run/src/launch.ts b/packages/amico-run/src/launch.ts index dd118c86..e7d18797 100644 --- a/packages/amico-run/src/launch.ts +++ b/packages/amico-run/src/launch.ts @@ -14,6 +14,7 @@ import { ConfigError, type Executor, type Finished, type SubmitOpts } from "./ty import { readAuthoring } from "./authoring.js"; import { runGate } from "./gate.js"; import { readSolverMode } from "./solver_mode.js"; +import { hasCloudConfig } from "./remote_config.js"; import { assembleWarrantContext } from "./warrant_context.js"; import { runVerification } from "./verify.js"; import { trySubcommand } from "./subcommands.js"; @@ -43,6 +44,7 @@ export async function launch(argv: string[]): Promise { let specPath: string | undefined; const opts: SubmitOpts = { julia: {} }; let projectExplicit = false; + let executorExplicit = false; for (let i = 0; i < argv.length; i++) { const a = argv[i]; @@ -59,6 +61,7 @@ export async function launch(argv: string[]): Promise { return 0; case "--executor": executor = next(); + executorExplicit = true; break; case "--lab": opts.lab = next(); @@ -126,25 +129,52 @@ export async function launch(argv: string[]): Promise { console.error(`amico-run: unknown --executor ${executor} (supported: local, remote)`); return 64; } - // Piccolissimo + Altissimo is a CLOUD-ONLY tier, so a LOCAL launch is refused - // while it is the selected solver. This lives here, not in runGate, because - // the gate only sees --spec runs (see `if (specPath)` below) — this line is - // the one choke point EVERY run passes through, spec or not. + // Piccolissimo + Altissimo is a CLOUD-ONLY tier. This lives here, not in + // runGate, because the gate only sees --spec runs (see `if (specPath)` below) — + // this is the one choke point EVERY run passes through, spec or not. // - // Why it is needed at all: selecting HP grants the `issimo` entitlement, so - // the import scan would happily admit a local `using Piccolissimo` and the - // solve would precompile the HP stack (and IPOPT) on the laptop — the exact - // failure this tier exists to avoid, and what amico-run's process-group - // timeout used to SIGTERM mid-precompile. + // Why it exists at all: selecting HP grants the `issimo` entitlement, so the + // import scan would happily admit a local `using Piccolissimo` and the solve + // would precompile the HP stack (and IPOPT) on the laptop — the exact failure + // this tier exists to avoid, and what amico-run's process-group timeout used to + // SIGTERM mid-precompile. // - // Fails SAFE: an absent or corrupt solver-mode.json reads as piccolo, so - // ordinary local runs behave exactly as before. + // Two different situations, deliberately handled differently: + // - executor defaulted (no --executor at all): PROMOTE to remote. Selecting + // the cloud tier IS the routing decision; making the caller restate it as a + // flag only creates a way to get it wrong. Previously this returned 64 and + // relied on the agent reading the message and retrying — a round-trip that + // surfaced to users as a failed run. + // - --executor local passed EXPLICITLY: refuse. That is a direct + // contradiction of the selected tier, and silently inverting an explicit + // flag is worse than an error. + // + // Fails SAFE either way: an absent or corrupt solver-mode.json reads as + // piccolo, so ordinary local runs behave exactly as before. if (executor === "local" && readSolverMode() === "hp") { + if (executorExplicit) { + console.error( + `amico-run: Piccolissimo + Altissimo runs in Harmoniqs Cloud and never solves locally — this launch is --executor local. ` + + `Drop the flag (cloud is automatic for this tier), or switch the solver to Piccolo (the model · solver control) for local solves.`, + ); + return 64; + } + // Do not promote into a broken remote. Without a connection the submit would + // fail deep in RemoteExecutor with `cloud config not found: ~/.amico/cloud.json` + // — accurate, but it names a file the user has never heard of instead of the + // control they need. Promotion made this path reachable by DEFAULT, so it has + // to say the human thing. + if (!hasCloudConfig()) { + console.error( + `amico-run: Piccolissimo + Altissimo runs in Harmoniqs Cloud, but no cloud connection is configured. ` + + `Connect Harmoniqs Cloud in the Connections panel (paste your API key), or switch the solver to Piccolo for local solves.`, + ); + return 64; + } + executor = "remote"; console.error( - `amico-run: Piccolissimo + Altissimo runs in Harmoniqs Cloud and never solves locally — this launch is --executor local. ` + - `Run it with --executor remote, or switch the solver to Piccolo (the model · solver control) for local solves.`, + `amico-run: solver is Piccolissimo + Altissimo → running in Harmoniqs Cloud (--executor remote, automatic for this tier)`, ); - return 64; } // --spec + --executor remote is SUPPORTED (High-Performance + Cloud, tier=hpc): // the launch gate is a static local check and runs identically for both diff --git a/packages/amico-run/test/solver_mode.test.ts b/packages/amico-run/test/solver_mode.test.ts index b8e3c11f..11edb198 100644 --- a/packages/amico-run/test/solver_mode.test.ts +++ b/packages/amico-run/test/solver_mode.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll } from "vitest"; import { execFile, execFileSync } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpRoot, fakeJulia } from "./helpers.js"; import { FakeCloud } from "./fake_cloud.js"; @@ -8,7 +8,10 @@ import { readSolverMode, solverModeFile } from "../src/solver_mode.js"; // Piccolissimo + Altissimo is a cloud-only tier. These tests pin the two halves // of that guarantee: the reader that decides which solver is selected, and the -// launch refusal that makes "cloud-only" true rather than merely advertised. +// launch behaviour that makes "cloud-only" true rather than merely advertised — +// a defaulted executor is PROMOTED to the cloud, an explicit --executor local is +// REFUSED. Both halves matter: promotion is what makes the tier automatic, and +// the refusal is what keeps it from being silently overridden. const BUNDLE = join(__dirname, "..", "dist", "amico-run.js"); beforeAll(() => { @@ -63,32 +66,83 @@ describe("readSolverMode", () => { }); describe("Piccolissimo + Altissimo never solves locally", () => { - // The bug: selecting HP grants the `issimo` entitlement, so the import scan - // admits a local `using Piccolissimo` and the laptop precompiles the whole HP - // stack (IPOPT included) until amico-run's process-group timeout SIGTERMs it - // mid-precompile. Refusing the launch is the durable fix. - it("refuses a local launch and exits 64", () => { + // The bug this guards: selecting HP grants the `issimo` entitlement, so the + // import scan admits a local `using Piccolissimo` and the laptop precompiles the + // whole HP stack (IPOPT included) until amico-run's process-group timeout + // SIGTERMs it mid-precompile. + // + // A DEFAULTED executor is promoted to remote rather than refused. Refusing it + // (the original behaviour) meant every plain `amico-run script.jl` under the + // cloud tier exited 64 and depended on the agent reading the message and + // retrying — users saw that round-trip as a failed run. + it("promotes a defaulted launch to the cloud instead of refusing it", async () => { + const fake = new FakeCloud(); + await fake.start(); + fake.state = { + task_status: "Running", + liveness: "alive", + iters: [{ iter: 1, f: "1.0e-2", inf_pr: "1e-8", inf_du: "1e-6" }], // served as {stats} on the wire + finished: { status: "completed" }, + }; + try { + const root = tmpRoot(); + const ops = opsDirWith(root, JSON.stringify({ mode: "hp", status: "ready" })); + // a --julia that would SHOUT if it ever ran: promotion means it never does + const julia = fakeJulia(root, "j", `require('fs').writeFileSync(${JSON.stringify(join(root, "ran-locally"))}, 'x')`); + const script = fakeJulia(root, "s.jl", ""); + const r = await new Promise<{ code: number; stdout: string; stderr: string }>((resolveP) => { + let stdout = ""; + let stderr = ""; + // NO --executor flag: that is the whole point of this case + const child = execFile("node", [BUNDLE, script, "--runs-root", join(root, "runs"), "--julia", julia], { + env: { ...process.env, AMICODE_OPS_DIR: ops, AMICO_CLOUD_URL: fake.base, AMICO_CLOUD_TOKEN: fake.token }, + }); + child.stdout!.on("data", (d: string) => { + stdout += d; + }); + child.stderr!.on("data", (d: string) => { + stderr += d; + }); + child.on("exit", (c) => resolveP({ code: c ?? -1, stdout, stderr })); + }); + expect(r.code).toBe(0); + expect(r.stderr).toMatch(/running in Harmoniqs Cloud/); + // it went to the cloud, not to the laptop + expect(r.stdout).toMatch(/AMICODE_FINISHED status=completed exitCode=0/); + expect(existsSync(join(root, "ran-locally"))).toBe(false); + } finally { + await fake.stop(); + } + }, 15000); + + // An EXPLICIT --executor local contradicts the selected tier. Silently inverting + // a flag the caller typed is worse than an error, so this one still exits 64. + it("refuses an explicit --executor local and exits 64", () => { const root = tmpRoot(); const ops = opsDirWith(root, JSON.stringify({ mode: "hp", status: "ready" })); const julia = fakeJulia(root, "j", `console.log('DONE f=0.99')`); const script = fakeJulia(root, "s.jl", ""); - const r = run([script, "--runs-root", join(root, "runs"), "--julia", julia], { AMICODE_OPS_DIR: ops }); + const r = run([script, "--executor", "local", "--runs-root", join(root, "runs"), "--julia", julia], { + AMICODE_OPS_DIR: ops, + }); expect(r.code).toBe(64); expect(r.stderr).toContain("Harmoniqs Cloud"); expect(r.stderr).toMatch(/never solves locally/); // it must name the way out, in both directions - expect(r.stderr).toMatch(/--executor remote/); + expect(r.stderr).toMatch(/cloud is automatic for this tier/); expect(r.stderr).toMatch(/switch the solver to Piccolo/); }); - // The refusal has to cover a bare `amico-run script.jl` too: runGate only sees - // --spec runs, so a gate-only check would leave the commonest path open. - it("refuses even with no --spec (the gate never runs on that path)", () => { + // Both behaviours have to cover a bare `amico-run script.jl` too: runGate only + // sees --spec runs, so a gate-only check would leave the commonest path open. + it("covers the no --spec path (the gate never runs there)", () => { const root = tmpRoot(); const ops = opsDirWith(root, JSON.stringify({ mode: "hp", status: "ready" })); const julia = fakeJulia(root, "j", `console.log('DONE f=0.99')`); const script = fakeJulia(root, "s.jl", ""); - const r = run([script, "--runs-root", join(root, "runs"), "--julia", julia], { AMICODE_OPS_DIR: ops }); + const r = run([script, "--executor", "local", "--runs-root", join(root, "runs"), "--julia", julia], { + AMICODE_OPS_DIR: ops, + }); expect(r.code).toBe(64); // and it dies BEFORE any run dir exists — no half-run to clean up expect(r.stdout).not.toContain("AMICODE_FINISHED"); diff --git a/packages/extension/scores/pulse-designer/templates/solve.jl b/packages/extension/scores/pulse-designer/templates/solve.jl index f0dee59a..2d1cbec2 100644 --- a/packages/extension/scores/pulse-designer/templates/solve.jl +++ b/packages/extension/scores/pulse-designer/templates/solve.jl @@ -18,12 +18,59 @@ T = 10.0 # gate time (ns) N = 50 # timesteps drive_max = 0.2 # per-quadrature drive bound (GHz) max_iter = 60 -SOLVER = :ipopt # :ipopt (default) or :altissimo (High-Performance + Cloud) # ───────────────────────────────────────────────────────────────────────── +# The solver follows the SELECTED SOLVER MODE, substituted at session prep — it is +# NOT an authoring decision. Piccolo mode → :ipopt; Piccolissimo + Altissimo (the +# paid cloud tier) → :altissimo, every run, automatically. Do not hand-edit this: +# the two backends need different callback wiring (below), and a mismatch between +# the selected tier and the solver is how a "High-Performance" run quietly ends up +# on IPOPT. +# +# Written as a substituted STRING rather than a bare `{{SOLVER}}` symbol so that an +# unsubstituted template is still valid Julia: if session prep could not stage a +# copy, this reads as :ipopt (a working local solve) instead of raising a syntax +# error on the placeholder itself. +SOLVER = let s = "{{SOLVER}}" + Symbol(startswith(s, "{{") ? "ipopt" : s) +end + SOLVER in (:ipopt, :altissimo) || error("SOLVER must be :ipopt or :altissimo, got $SOLVER") if SOLVER === :altissimo @eval using Piccolissimo # AltissimoOptions lives here, not in Piccolo + @eval using DirectTrajOpt + + # ── dispatch bridge: DirectTrajOpt 0.9.7 moved the backend extension point ── + # DTO 0.9.7 renamed it from `Solvers.solve!` to `_solve`, and its fallback only + # @error-LOGS and returns nothing. Piccolissimo (through 0.2.0) still defines + # the OLD name, so under 0.9.7 nothing matches and every Altissimo solve is a + # silent NO-OP that still reports success: `iterations = 0`, fidelity left at + # the random initial guess, FINISHED completed/exit 0. Verified on this machine + # (run r20260729-103718Z-12e3: 0 iters, fidelity 0.048, reported "converged"). + # + # Altissimo is not the bug — its host interface moved out from under it. Until + # Piccolissimo migrates upstream, bridge it here: the solve script ships per + # submission, so this reaches the cloud runner with no image rebake. + # + # Guard on WHICH method would be called, not on `methods(...)` being empty: + # DTO has two fallbacks (one typed `Any`, one `AbstractSolverOptions`) and + # AltissimoOptions matches both, so an emptiness test never installs the bridge. + _alt_dispatch = try + m = which(DirectTrajOpt._solve, (DirectTrajOpt.DirectTrajOptProblem, Piccolissimo.AltissimoOptions)) + string(m.sig.parameters[3]) + catch + "none" + end + if _alt_dispatch in ("Any", "AbstractSolverOptions", "DirectTrajOpt.AbstractSolverOptions", "none") + @eval DirectTrajOpt._solve( + prob::DirectTrajOpt.DirectTrajOptProblem, + options::Piccolissimo.AltissimoOptions; + kwargs..., + ) = DirectTrajOpt.Solvers.solve!(prob, options; kwargs...) + println("AMICODE_NOTE bridged AltissimoOptions onto DirectTrajOpt._solve " * + "(DTO $(pkgversion(DirectTrajOpt)) moved the extension point; was resolving to $_alt_dispatch)") + flush(stdout) + end end # ── telemetry sink ─────────────────────────────────────────────────────────── @@ -158,6 +205,7 @@ end # derive from those rather than emitting NaN — a real number the client can plot # beats a placeholder it has to special-case. function alt_cb(x, info) + alt_cb_fired[] = true # tells the stdout bridge below to stand down (one numbering scheme per run) k = Int(info.outer_iter); iters[] = k ok = pulse_emit(x, k) # frames + AMICODE_PULSE + cooperative STOP inf_pr = haskey(info, :inf_pr) ? info.inf_pr : max(info.eq_viol, info.ineq_viol) @@ -166,13 +214,105 @@ function alt_cb(x, info) return ok end +# Altissimo telemetry needs TWO independent sources, because neither one is +# reliable across the Piccolissimo versions in the wild: +# 1. alt_cb above — the good path: it carries frames as well as numbers. But it +# only fires where Piccolissimo forwards a caller `callback` into +# Altissimo.optimize!. Piccolissimo 0.2.0 declares +# `solve!(prob, ::AltissimoOptions; kwargs...)` and forwards a HARDCODED +# whitelist (tol, polish*, …) — `callback` is not on it, so on 0.2.0 and the +# current cloud image alt_cb never runs at all. +# 2. the stdout bridge below — the floor: Altissimo's own iteration table always +# prints under `verbose`, on old builds too, so translating those rows into +# AMICODE_ITER gives the Run Inspector a live curve no matter what the +# installed Piccolissimo forwards. +# Belt and braces on purpose. With only (1), an Altissimo run on the shipped image +# reports `iterations = 0` and the Inspector stays dark — that is exactly the +# 2026-07-29 failure (fidelity 0.048 reported as a converged result). +# +# Row shapes, both from Altissimo/src/Optimizer.jl: +# inner step " %5s %13.6e %10.3e %10.3e …" iter column is "·" +# final outer " %5d %13.6e %10.3e %10.3e …" iter column is the outer index +# Columns 2-4 are objective, inf_pr, and the dual measure (‖∇L‖ inner / +# stationarity outer) — the same three the IPOPT path plots. +# +# Numbering is SEQUENTIAL over rows, not read out of the iter column: that column +# is "·" for every inner step and only becomes an integer on the last outer +# iteration, so trusting it yields a single point at the end (verified: a 5-outer +# run printed 1 integer row and ~200 "·" rows). Each inner row is one optimizer +# step, so counting rows gives the dense curve IPOPT streams locally. +const ALT_ROW = r"^\s*(?:·|\d+)\s+([-+0-9.eE]+)\s+([-+0-9.eE]+)\s+([-+0-9.eE]+)\s" +alt_cb_fired = Ref(false) + +"""Run the Altissimo solve, mirroring its verbose table into AMICODE_ITER lines. + +Writes through to the ORIGINAL stdout and appends run.log directly instead of +calling `emit()`: stdout is redirected for the duration of the solve, so emit() +would feed the very pipe this reader is draining.""" +function solve_altissimo_streaming(qcp, opts, cb) + real_out = stdout + pipe = Pipe() + Base.link_pipe!(pipe; reader_supports_async = true, writer_supports_async = true) + seq = Ref(0) + reader = @async begin + for line in eachline(pipe) + println(real_out, line) # the raw table still reaches the log + flush(real_out) + # Where the callback IS forwarded it supersedes this bridge: it numbers + # by outer iteration and carries frames, and two numbering schemes + # interleaved in one run.log would plot as a sawtooth. + alt_cb_fired[] && continue + m = match(ALT_ROW, line) + m === nothing && continue + seq[] += 1 + iters[] = seq[] + out = @sprintf("AMICODE_ITER iter=%d f=%s inf_pr=%s inf_du=%s", + seq[], m.captures[1], m.captures[2], m.captures[3]) + println(real_out, out) + flush(real_out) + if CLOUD_RUN + try + open("run.log", "a") do io + println(io, out) + end + catch # telemetry must never take down a solve + end + end + end + end + try + redirect_stdout(pipe) do + solve!(qcp; options = opts, callback = cb) + end + finally + close(pipe.in) + try + wait(reader) + catch + end + close(pipe) + end + if !alt_cb_fired[] + emit("AMICODE_NOTE Piccolissimo $(pkgversion(Piccolissimo)) does not forward `callback` to " * + "Altissimo, so iterations were read from the solver's own table ($(seq[]) rows) and " * + "per-iteration pulse frames are unavailable; the final pulse is still written") + end +end + t0 = time() if SOLVER === :altissimo # The budget goes on the OPTIONS, not as a solve! kwarg. solve!(::AltissimoOptions) # forwards a hardcoded list to Altissimo.optimize! and swallows the rest, so a # `max_iter =` here is silently dropped and the solve quietly runs Altissimo's # default 20 outer iterations instead of the FILL-IN value. - solve!(qcp; options = Piccolissimo.AltissimoOptions(max_outer_iter = max_iter), callback = alt_cb) + # + # verbose = true is load-bearing, not chatter: it is what makes the iteration + # table — and therefore the stdout telemetry bridge above — exist at all. + solve_altissimo_streaming( + qcp, + Piccolissimo.AltissimoOptions(max_outer_iter = max_iter, verbose = true), + alt_cb, + ) else solve!(qcp; max_iter = max_iter, print_level = 1, options = IpoptOptions(intermediate_callback = pulse_emit), @@ -215,4 +355,16 @@ open("result.toml.tmp", "w") do io )) end mv("result.toml.tmp", "result.toml"; force = true) + +# A solve that recorded ZERO iterations did not optimize anything — the fidelity +# above is the random initial guess. Say so instead of letting FINISHED's +# completed/exit-0 read as a converged result downstream. This is the failure that +# hid a silently no-op'd Altissimo backend behind a "converged" badge +# (iterations = 0, fidelity = 0.048, 2026-07-29): the run LOOKED successful, so +# nobody checked. Partial artifacts are still written — the run is recorded, it +# just stops claiming a result it does not have. +if iters[] == 0 + emit("AMICODE_WARN no iterations were recorded — the optimizer never reported progress, so " * + "fidelity=$(fid) is the INITIAL guess, NOT a converged result (solver=$(SOLVER))") +end emit("DONE fidelity=$(fid)") diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 5eace34c..49eb66ea 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -302,15 +302,18 @@ export function solverModeSection(): string { "mode, so never ask the user where a solve should run. Author it as: " + '`tier="hpc"`, `executor="remote"`, `env.kind="provisioned"` (via `amico-run --spec ' + " --executor remote`). The runner image has Piccolissimo/Altissimo pre-baked, so there " + - "is NO local precompile and NO sandbox — never author a sandbox env for HP. A local launch is " + - "REFUSED by amico-run while this solver is selected (exit 64), so attempting one only wastes a turn. " + - "Live iteration frames stream to the Inspector; note that per-iteration AMICODE_ITER stats + the " + - "cooperative Stop are not yet available on the cloud bundle, and re-rollout verification is skipped " + - "for cloud runs (say so). Only claim cloud execution when the launch actually used `--executor remote`." + "is NO local precompile and NO sandbox — never author a sandbox env for HP. amico-run routes this " + + "tier to the cloud on its own (a launch with no `--executor` is promoted to remote) and REFUSES an " + + "explicit `--executor local` (exit 64), so attempting a local run only wastes a turn. " + + "Per-iteration `AMICODE_ITER` stats and live frames both stream to the Inspector. Two real limits to " + + "state plainly if they come up: the cooperative Stop needs the solver callback, which the cloud " + + "bundle's Piccolissimo does not forward to Altissimo (so Stop may not interrupt a cloud solve), and " + + "re-rollout verification is skipped for cloud runs. " + + "Only claim cloud execution when the launch actually ran remotely." : "Harmoniqs Cloud is NOT connected (no API key). Piccolissimo + Altissimo is a PAID cloud tier and " + "CANNOT run locally — do NOT attempt a local Piccolissimo solve (it will fail three ways: amico-run " + - "refuses a local launch in this mode, the private package can't be instantiated in a sandbox, and " + - "the gate rejects a local hpc run). Instead, STOP and tell the user: " + + "refuses the launch in this mode and tells the user to connect, the private package can't be " + + "instantiated in a sandbox, and the gate rejects a local hpc run). Instead, STOP and tell the user: " + '"Piccolissimo + Altissimo needs a Harmoniqs Cloud connection — click **Piccolissimo + Altissimo** ' + "in the model · solver control on the dashboard and connect your API key there (or run **Amico: " + 'Connect Cloud**, which opens the same flow)." Offer to switch back to the free local Piccolo solver ' + @@ -326,23 +329,19 @@ export function solverModeSection(): string { "`EmbeddedOperator`, `UnitaryTrajectory` — every problem-setup name comes from Piccolo. The failure is " + "an UndefVarError at load time, before any solve starts, and on a cloud run you pay the full queue and " + "instance-boot wait before seeing it. " + - "**Solver backend:** the default remains IPOPT (`IpoptOptions`), which is what streams per-iteration " + - "telemetry — its `intermediate_callback` produces the Inspector's frames and the `AMICODE_ITER` lines. " + - "If the researcher asks for the **Altissimo** backend (the augmented-Lagrangian GPU solver, " + - "`AltissimoOptions`), switch it by setting **`SOLVER = :altissimo`** in the template's FILL-IN block — that " + - "one line is the whole change. Do NOT hand-roll the solve call: the template already re-hangs BOTH telemetry " + - "channels onto Altissimo's `(x, info)` hook (the frames come off `IpoptOptions.intermediate_callback`, which " + - "`AltissimoOptions` does not have, so a hand-written call loses the Inspector's frames as well as its " + - "numbers), passes the budget as `AltissimoOptions(max_outer_iter = max_iter)` (a `max_iter` given to " + - "`solve!` is silently DROPPED on that path — the solve would quietly run 20 outer iterations), and derives " + - "`inf_pr`/`inf_du` on older Altissimo builds. " + - "Also TELL THEM that live iterations depend on the INSTALLED version. " + - "Current Piccolissimo main accepts a `callback` on `solve!(::AltissimoOptions)` and forwards it to " + - "`Altissimo.optimize!`, which fires it every outer iteration; older builds swallow `kwargs...` and forward " + - "nothing, so an Altissimo run there emits NO AMICODE_ITER lines and the Run Inspector stays dark until the " + - "solve finishes. Do not promise live iterations you have not seen: run it, and if no AMICODE_ITER line " + - "appears in the first iterations, say so plainly rather than implying the solve is stuck. Never switch to " + - "Altissimo silently. " + + "**Solver backend — already decided, do not touch it.** The template you copy arrives with its " + + "`SOLVER` line ALREADY SET to Altissimo, because Piccolissimo + Altissimo is the selected solver. " + + "Do NOT edit that line, and do NOT hand-write the `solve!` call. The template wires Altissimo's " + + "`(x, info)` callback for both telemetry channels (frames AND `AMICODE_ITER` — the frames come off " + + "`IpoptOptions.intermediate_callback`, which `AltissimoOptions` has no equivalent of, so a hand-rolled " + + "call loses the Inspector's plots as well as its numbers), passes the budget as " + + "`AltissimoOptions(max_outer_iter = max_iter)` (a `max_iter` given to `solve!` is silently DROPPED on " + + "that path and the solve quietly runs 20 outer iterations), and bridges the DirectTrajOpt 0.9.7 " + + "extension-point rename so the backend actually dispatches. " + + "**Report what the artifacts say, never what the exit code says.** A `completed` / exit-0 run can still " + + "have optimized NOTHING: read `result.toml` and if `iterations` is 0 — or the template emitted " + + "`AMICODE_WARN` — the fidelity is the random initial guess, so say the run did not converge. Do not call " + + "a 0-iteration run successful. " + routing + "\n" ); @@ -523,9 +522,32 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro const raw = fs.existsSync(opts.agentsSrc) ? fs.readFileSync(opts.agentsSrc, "utf8") : "# Amicode\nRead the template at {{TEMPLATE_PATH}}, fill params, run `amico-run