Skip to content

Prepare for public MIT release - #4

Closed
AuroraLHT wants to merge 340 commits into
mainfrom
public-release-cleanup
Closed

AuroraLHT wants to merge 340 commits into
mainfrom
public-release-cleanup

Conversation

@AuroraLHT

Copy link
Copy Markdown
Owner

Prep for open-sourcing under MIT, referencing arXiv:2602.20432.

What's here

  • LICENSE — MIT, "Copyright (c) 2026 Haotong Liang and the Lumi-Lab contributors". Declared in pyproject.toml (license = "MIT", license-files = ["LICENSE"]).
  • README.md
    • Lead paragraph tying the stack to the paper; new Citation (BibTeX) and License sections.
    • Replaced the transient "the lab broker is not ready for this yet" note with a general note about pointing at a broker only this stack uses.
    • Dropped "ported from the v1.0 ones" from the Notebooks section.
  • notebooks/ (both _*.py sources + rebuilt .ipynb, and notebooks/README.md)
    • Removed the "ported from UMD*Experiment.ipynb" framing and the v1.0-vs-now comparison tables — there's no public v1.0 to compare against.
    • Removed scattered "the v1.0 notebook did X" / "the production campaign" asides; kept the technical substance.
    • Config cells: ACTOR = "hliang16""operator", PROJECT = "UMD_AI_HZO_BO""hzo_bo_demo".
    • _bo_deposition.py: dropped the from src.analysis import analyze_rheed_video try/except (that module isn't in this repo) for an explicit HAVE_ANALYSIS = False placeholder.

No code or contract changes — docs and notebook prose only. Notebooks rebuilt with uv run notebooks/build_notebooks.py.

Not done (out of scope, flagging)

  • old/ (61 tracked v1.0 files) still ships. Decide whether to keep or drop before publishing.
  • docs/TODO.md / SAMPLE_TRACKING.md / FRONTEND_MIGRATION.md scanned for secrets/IPs/emails — clean — but not otherwise edited for a public audience.

🤖 Generated with Claude Code

AuroraLHT added 30 commits July 23, 2024 00:03
AuroraLHT and others added 27 commits August 12, 2026 16:44
Adds a streamable-HTTP transport alongside stdio, gated by the same
JWT/user store the browser bridge uses (lumi.api.db/security) so a
remote agent authenticates with an operator-or-admin token instead of
a second auth mechanism. Bearer tokens are verified via a TokenVerifier
(lumi.mcp.auth.LumiTokenVerifier) and every tool call is re-checked
against lumi.contracts.policy.permits for its specific op, independent
of settings.auth.enabled since this transport is meant to reach real
equipment control from off-box. TLS itself is left to a reverse proxy
in front of the plain-HTTP transport (tracked in docs/TODO.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folds a standalone root TODO.md into the existing known-gaps doc
instead of keeping two TODO files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both default to is_streaming=False until a client sends the `start`
control verb, same as any stream capability. Detection already forces
the RHEED *input* stream on unconditionally (it consumes continuously,
not on demand from a viewer), but left its own two output streams off
until a browser happened to click something -- so a growth recorded
before that first click got no AI frames even though the detection
node reported itself up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously --chamber-speed defaulted to 1 regardless of
--experiment-speed, so passing --experiment-speed alone left the
chamber itself at real time while only the experiment node's
wall-clock warm-up pacing sped up -- and the script's own printed
demo-script hint then (correctly, but confusingly) suggested --speed
1. Passing either flag alone now moves both together, matching what
"run the sim faster" usually means; pass both explicitly to run them
at different rates on purpose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`rabbitmq.host` had been the lab machine's IP since 9ef767c, so anything
reading the setting directly reached for real equipment. Nothing did in
practice: start_simulation.sh hardcodes RABBITMQ_HOST=localhost and passes
--host to every node it launches, which shadowed the setting on every path
we actually used. The MCP server was the first thing started by hand, and
it went straight at the lab broker -- where it died on

    PRECONDITION_FAILED - inequivalent arg 'type' for exchange 'RHEED':
    received 'topic' but current is 'direct'

That broker still holds the pre-refactor stack's CHAMBER/RHEED/STORAGE as
non-durable `direct` exchanges, with live bindings from the old nodes. Any
refactored node pointed there fails the same way; start_simulation.sh's
reset_stale_exchanges() already fixes exactly this, but only on localhost,
since deleting exchanges on a shared broker is destructive. Nothing on the
lab broker was touched.

So localhost becomes the tracked default -- a broker address is a
machine-local fact, and the chamber host should set it in cfg/.secrets.toml
or pass --host. Consequence worth knowing: a real-hardware run now needs
--host explicitly, or it quietly talks to a local broker and finds nothing.

Also documents the MCP server in the README, which had no mention of it:
how to start both transports, registering it with `claude mcp add` (and why
--project is needed), the operator-token requirement on HTTP, the state of
the lab broker, and the caveat that long ops return a task_id whose
completion does not imply success -- an agent that skips the poll will
deposit at whatever temperature the substrate actually reached.

The --host default expression is unchanged; only its resolved value and the
help text moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWGcmJxk3qs5tPtGJKHQ8S
`perform_single_deposition` printed "[Manual] Set Pressure to ..." and waited for
a human, because the experiment contract had no way to say it. It has one now:
set_pressure / set_pressure_control for the closed loop, and set_mfc_control
alongside the existing set_mfc_flow.

The gates are separate ops rather than arguments to the setpoints. PASCAL's
`MFC Control` is a single master enable with no channel argument, so folding it
into set_mfc_flow would mean set_mfc_flow(2, 0) silently shuts MFC1 as well.
Nothing in the chamber log reports either gate back -- there is no bit for them
in PARSE_DICT -- so the ordering requirement is written into the op docs, which
is where an LLM agent actually reads it, instead of being left to be discovered
by watching the pressure not move.

Also gives to_temperature a doc saying the heating laser has to be on first, and
fixes a crash in the chamber simulator: only MFC1/MFC2 are plumbed, but the MI
script grammar accepts `MFC(\d) Flow Set=`, so a script asking for MFC3 added a
key that `_tick_gas` then hit a KeyError on -- killing the log-writer thread and
taking the whole simulated chamber quiet with it.

Contract hash moves to 7e7d5e6d72531bde; generated clients regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWGcmJxk3qs5tPtGJKHQ8S
All three token paths -- the REST dependency, the websocket handshake, and the
MCP bearer verifier -- checked `sub is not None` and then called `int(subject)`
unguarded. A correctly-signed token carrying anything else (a username, an older
token format, a hand-rolled JWT) got past the None check and raised ValueError
out of the verifier: a 500 with a stack trace from the REST side, and an
exception through the auth middleware on the MCP side, for what is simply an
invalid credential.

Each now converts inside a try and rejects the way every other bad token is
rejected, logging what it saw at debug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWGcmJxk3qs5tPtGJKHQ8S
The tool handed the agent `{"dtype": "uint16", "shape": [540, 720]}` and nothing
else. A binary-codec op answers `(headers, body)` -- the response model describes
the frame, the pixels travel beside it -- and `_on_call_tool` kept `result[0]`
and dropped the body, with nothing in the reply to suggest one had existed. The
agent reported, correctly, that the camera tool returns no image.

`lumi.mcp.frames` now renders the payload as MCP ImageContent, which is the wire
form a host turns into a real image block for the model. Anything that is not a
renderable image degrades to a sentence saying what came back instead -- the one
option not on the table is silently returning the headers alone, which is
indistinguishable from an op that genuinely had nothing to return.

The one judgement call is contrast. These cameras emit 12-bit counts inside a
uint16, so scaling against the dtype gives a black rectangle and casting to uint8
gives a posterised mess; neither is a picture of anything. Each frame is scaled
by its own min and max, and the real numbers go back in the text beside it, so
"the spot is brighter now" is a claim the agent makes from the readings rather
than from the rendering. A uniform frame is left black rather than stretched --
a shuttered camera has to look shuttered, not like a vivid texture made out of
sensor noise. Frames are downscaled past 1024px, since every pixel becomes base64
in a JSON-RPC message and then tokens in a context window, and an already-encoded
JPEG or PNG body is passed through rather than re-encoded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWGcmJxk3qs5tPtGJKHQ8S
The HTTP transport verified bearer tokens but had no way to issue one, so using
it meant minting a JWT out of band and pasting it into a client config --
scripts/start_mcp_http.sh existed largely to do that. That is not how anyone
actually connects an MCP client, and a pasted 12-hour token is a credential
sitting in a config file with no way to revoke it.

The server is now its own OAuth 2.1 authorization server, so

    claude mcp add --transport http lumi-experiment http://127.0.0.1:8100/mcp

is the whole setup: the client registers itself, gets sent to /login, the person
types their lumi password, and the client holds a refresh token from then on.

Reusing the SDK's authorization-server support rather than writing the protocol
means metadata discovery, PKCE verification, dynamic registration and the RFC
error shapes come from `mcp.server.auth`; what is ours is ten provider methods
and one login page. It also keeps the transport usable without the api node
running, which putting the flow in the api node would not have.

Both routes end at the same JWT `POST /auth/login` already mints, so
LumiTokenVerifier is untouched and a hand-minted token keeps working.
Authorization codes and pending logins stay in memory (60s and 15min lives), but
registered clients and refresh tokens go to SQLite -- a forgotten client_id
becomes `invalid_client` on the next refresh, and a lost refresh token turns
"log in once" into "log in on every restart". Its own database file rather than
tables in users.db, so the MCP server and the api node never contend for
SQLite's database-wide write lock.

Two limits, stated in the module docstring rather than left to be found: over
plain HTTP the password crosses the wire in the clear, so put a TLS proxy in
front and give it --public-url; and access tokens are stateless JWTs, so
revocation drops the refresh token and stops renewal but cannot recall a token
already issued. --no-oauth restores the verify-only behaviour.

A viewer is refused at the login page with a sentence saying why, rather than
being given a token that authenticates fine and then 401s every tool call --
the least debuggable outcome available.

Alongside: start_simulation.sh writes run/simulation/env.sh so a second terminal
can adopt the stack's broker and user database instead of silently talking to
the settings.toml defaults, and stop_nodes.sh learns about the experiment node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWGcmJxk3qs5tPtGJKHQ8S
The agent had rheed.camera but no way to reach the chamber's own webcam.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Production had no start script at all -- the README just listed the nodes and
left the operator to run them by hand, in order, with the right flags. Two
launchers now cover the split: start_server_host.sh brings up monitor, storage,
detection and api on the machine that holds the broker, and
start_instrument_host.sh brings up pascal and rheed on the machine wired to the
hardware.

Both follow start_simulation.sh: preflight, ordered start, reverse-order TERM
shutdown, and die if any node dies. `--check` runs only the preflight.

The preflights cover what actually bites on real hardware rather than in the
sim. The instrument launcher requires --host and refuses to start as `guest`
against a non-loopback broker, since RabbitMQ rejects that account off
loopback. The server launcher probes the users table directly for the `role`
column -- a database from before the contract refactor survives
CREATE TABLE IF NOT EXISTS and then fails one request at a time -- and refuses
to run with auth disabled or the placeholder signing key unless
--allow-insecure-auth says otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migrate cfg/settings.toml to a tracked template plus a machine-local copy.

- Rename tracked cfg/settings.toml -> cfg/settings.example.toml (the template
  carrying safe defaults). The real cfg/settings.toml is now git-ignored; each
  checkout copies it from the template.
- lumi.config raises FileNotFoundError with the cp command when settings.toml
  is missing, instead of letting dynaconf hand back a config full of None.
- Add cfg/.secrets.example.toml showing where the generated JWT signing key and
  other machine-local overrides go; keep it tracked via a .gitignore negation
  while .secrets.* stays ignored.
- merge_enabled=True so .secrets.toml overlays settings.toml key by key -- a
  bare [section] there would otherwise replace the whole table and crash every
  node on settings.rabbitmq.host.
- CI copies the template into place; conftest seeds it for fresh clones.
- README and in-tree comments point at settings.example.toml / .secrets.toml.
scripts/start_instrument_host.ps1 mirrors start_instrument_host.sh for a
Windows instrument machine: same flags, same preflight checks, same
start pascal + rheed / monitor-and-shut-down flow.

Windows-specific differences (documented inline):
- hard Stop-Process on Ctrl-C / node death (no SIGTERM)
- stdout and stderr go to separate log files (Start-Process can't merge)
- args hand-quoted so -Log/-Mi paths with spaces survive on PS 5.1

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WebCameraConfig gained a required `device` field, but neither build_camera
call site was updated, so `--src path` (pascal) and `--src webcam` (rheed)
crashed at construction with a missing-argument TypeError. The value is
already in settings (`[pascal.webcam] device`, `[rheed.webcam] device`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…host

Three fixes to the newly-ported launcher:

- `$Src:` / `$RheedSrc:` inside double-quoted Warn strings parsed as a
  scope/drive qualifier and failed at script load. Wrapped in `${}`.

- Windows PowerShell 5.1 has no $PSNativeCommandUseErrorActionPreference,
  so under `$ErrorActionPreference = 'Stop'` any bytes a child writes to
  stderr become a terminating NativeCommandError and `2>$null` does not
  reliably stop it -- an `import watchdog` traceback aborted the preflight.
  All `python -c` probes now go through an Invoke-Py helper that drops
  stderr and returns { Out; Code }.

- -BrokerHost is no longer mandatory: when omitted it falls back to
  settings.rabbitmq.host (which cfg/.secrets.toml overrides), matching how
  the nodes themselves default --host. A loopback or empty settings value
  is still a hard stop, since the broker is not on this machine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
apply_broker_permissions.py wrote the same restrictive resource
permissions for every role: configure limited to ^(amq[._].*)$, write/read
to the anonymous queues plus the lumi exchanges. That is right for a
browser (viewer/operator), which only ever declares anonymous reply/sub
queues -- and a viewer must not be able to read q.<cap>.req or it can
steal RPCs off the bus.

It is wrong for a node. On startup an equipment node redeclares its
exchange, declares its durable work queue q.<cap>.req, then binds and
consumes it -- all four operations need that name in scope, so a
node-role account failed queue.declare with ACCESS_REFUSED. policy.py
already grants node (and admin) ".*" topic permissions on the grounds
that nodes are trusted lab code, not the threat model; the resource
permissions now match.

node/admin get configure/write/read = ".*"; viewer/operator keep the
restrictive set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add PowerShell instrument host launcher; fix node broker permissions
The growth database recorded what was deposited but not what exists, and
nothing recorded the process itself. Three changes, in dependency order.

**Substrate resume no longer rewinds.** `resume_substrate` rebuilt a
Substrate with `current_position_id = 0` and an empty accessed set, so a
campaign resumed after a node restart handed back an already-grown pixel
and deposited on top of it. The history was always in
`experiment.pixel_location`; `get_used_pixel_indices` reads it back and
`Substrate.restore_progress` re-applies it. The pre-refactor notebook
driver had the same omission, which is why it carried a `collector`
pickle and a hand-managed counter beside the database.

**`sample`, as a tree.** One row per physical specimen: a bare substrate
is the root, each growable position a child, a cleaved piece a child of
that. Materialised at `register_substrate` rather than at first growth,
so "which positions are spent" is a query and a step has something to
attach to before any deposition. The layer stack is deliberately *not* a
table -- `get_layer_stack` derives it from successful deposition steps,
so it cannot drift from what was actually run.

**The step journal.** `_start_task` already minted an id, stamped a start
time, held the typed request as `detail` and built the `{"ok": ...}`
outcome -- then pushed it to subscribers and dropped it. That is a step
record, discarded. Two hooks now persist it:

  - `MqServer._handle_request`, for ops that finish inside one RPC
    (heater on, gas in, alignment, a human answering a gate).
  - `ExperimentHandler._start_task`, for the long-running ones, so a
    ramp's row spans the ramp and not the ack.

Which ops are journaled is declared on the contract (`Op.journal`), never
inferred: `get_current_log` is polled about once a second and would bury
the rows that describe a growth. `Op.journal` is kept out of `_op_json`,
so it is server-side policy and the contract hash -- and the generated
frontend client -- are untouched.

`actor` is new on the request envelope, carried to long-running ops
through a contextvar because a handler method never sees AMQP headers.
It is self-asserted, which is right for a session you started yourself
and why the bridge and the MCP HTTP transport should set it from the
verified JWT rather than forward a caller's claim.

Only the experiment node gets a journal: growth.db is on the server host
while pascal and rheed run on the instrument PC. The hook is generic, so
shipping step events to a collector later needs no change here.

Verified against the simulator: a full dryrun growth recorded itself as
ten chained steps with real durations, a refused ramp left a row saying
why, a 16.4s ramp carried its actor, and 40 temperature polls produced
zero rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWYALcF31zhQib9RS948pY
growth.db lives with the experiment node on the server host, so a notebook
on any other machine has no way to it except through the bus. Five ops,
which together replace the CSV and the pickled `collector` the
pre-refactor notebooks kept beside the database:

  list_samples       which positions exist and which are spent
  get_sample         one sample plus its layer stack, derived
  sample_history     the step journal, filtered by sample or session
  add_measurement    attach a result -- RHEED metric, XRD, AFM, transport
  list_measurements  the GP training set, in one call

`list_measurements` resolves each sample's growth conditions server-side
so assembling a training set is one round trip rather than one per point.
The contract hash moves to 983c936b3e71f4fb; web/src/generated/lumi.ts and
schemas/contract.json are regenerated with it.

Three defects a live run against the simulator caught, none of which the
unit tests would have:

  - A deposition step recorded `target_id: "C"` and no material. The
    carousel's slot-to-material map is chamber config that changes when
    targets are swapped, so read back after the next swap that step names
    the wrong material. The name is now resolved when the step is
    journaled, which freezes it.

  - A **dryrun** deposition counted as a layer. It fires no laser: the
    step happened, the film did not. `get_layer_stack` filters it out,
    so a rehearsal no longer puts material on a bare sample.

  - `growth_conditions` returned the step's params *or* the experiment
    row. Neither is sufficient: the row holds the measured pressure and
    laser power a GP actually regresses on, which the deposition request
    never carried, while the step holds the material and rate. The row is
    now the base and the step overlays it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWYALcF31zhQib9RS948pY
…th them

Completes the "infrastructure first, then the notebooks" sequence. Adds
`lumi.opt`, the two notebooks, and the fixes that driving the simulator
turned up.

**lumi.opt** -- ported from ~/HZO_PLD/OpCode/src/{gp,preprocess}.py, the
code that ran the real autonomous campaigns. The maths is unchanged;
altering it while porting would have invalidated the campaign history it
produced. `GrowthCampaign` replaces `GPManager` with one structural
change: observations come from `list_measurements` over the contract, not
from `gp_db/<project>.csv`. A campaign can now be resumed on a different
machine, and the GP is fitted on what the lab actually recorded. New
`opt` extra, split out because torch and gpytorch are a heavy install
that only the machine running the BO notebook needs.

**Notebooks** are generated from plain Python sources by
`build_notebooks.py` and carry no outputs. Notebooks are JSON: a one-line
cell change otherwise shows up in review as a rewritten blob.

Five defects a live run caught, none of which the unit tests would have:

  - A deposition step recorded no temperature or pressure. Those are
    chamber state, not request fields, so in a dryrun -- where
    finish_experiment_record never runs -- the GP had no conditions at
    all and skipped every point. The BO loop silently never left its
    random-seed phase. Read from the chamber when the step is journaled,
    so the step stands on its own.

  - `to_current_pixel` failed nearly every time it ran unattended. It
    issues a mask move then a RHEED move; the mask move waits for MI
    completion, but `Motor free` comes from the chamber log, which PASCAL
    rewrites about once a second. For up to a log tick after the
    controller says the move finished, the log still says busy, and the
    second move was refused. `_await_motor_free` waits, bounded, instead
    of refusing -- the safety property is unchanged, and a caller that
    got the old error could only retry, which is this loop written at the
    client with a round trip per poll.

  - A deposition step recorded `target_id` and no material. The
    slot-to-material map is chamber config that changes when targets are
    swapped, so that step names the wrong material after the next swap.
    Resolved at journal time, which freezes it.

  - A dryrun deposition counted as a layer. It fires no laser: the step
    happened, the film did not.

  - `ExperimentSession`'s docstring had `image()`'s return backwards --
    it is `(meta, frame)`. A pre-existing bug in the example written for
    notebook authors, which is exactly who it misled.

Verified against the simulator: SingleDeposition.ipynb runs end to end
(three layers, every step journaled with its actor, the derived stack
read back), and six consecutive pixel moves now succeed in ~0.2s each
where the first used to fail. BODeposition.ipynb was seen to get past
its random-seed phase into GP-proposed conditions but was NOT observed to
completion -- the host VM crashed during the run, twice. That, and the
rest of the open work, is written up in docs/SAMPLE_TRACKING.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWYALcF31zhQib9RS948pY
Ran BODeposition.ipynb headlessly against the simulator: 2 random-seed growths
then 5 GP-proposed ones, all 7 substrate positions used, journal intact,
session closed cleanly -- the item docs/SAMPLE_TRACKING.md left unconfirmed.

Along the way: neither notebook's matplotlib import was covered by any
dependency group, so a clean `uv sync --all-extras` couldn't run either one.
Added a `notebooks` extra.

The completion run also surfaces a caveat worth recording rather than just
checking the box: under DRYRUN = True, to_temperature is skipped by design and
pressure is only ever a printed manual instruction with nothing in the
simulator to act on it, so growth_conditions() (what list_measurements feeds
the GP) records the chamber's undisturbed idle state for every point instead
of what was actually proposed. This run confirms the loop's mechanics --
bookkeeping, gates, journaling, GP wiring -- not its optimisation behaviour.
Documented in both docs/SAMPLE_TRACKING.md and notebooks/README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hgijsb7VRHKujNQUv4HwhJ
…it for laser-on

- get_layer_stack no longer drops dryrun depositions; each layer carries
  is_dryrun so a rehearsed run stays visible instead of looking like the
  growth went missing. Contract, handler and notebook display updated to match.
- start_storage prefixes the recording name with the loaded sample (which
  already carries its position), falling back to the project name when no
  substrate is registered, so a stray file in storage is traceable.
- initiate_heating_laser now polls is_heating_laser_on() until the chamber
  log reflects the state, bounded like _await_motor_free -- a to_temperature
  call right after it no longer races the ~1 Hz log writer in simulation.
- SingleDeposition notebook selects the real terminal-input providers the
  moment DRYRUN is off, rather than leaving a commented-out hint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyxfY7uJetWk4KxVSEDTpZ
Samples, steps and measurements as first-class rows: a step journal that
records who did what, samples materialised per growable position, and the
whole stack reachable over the contract. Operator notebooks and the BO
layer ported onto it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyxfY7uJetWk4KxVSEDTpZ
lumi-codegen --check flagged the generated clients, lumi.ts and
contract.json as stale after is_dryrun was added to LayerInfo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyxfY7uJetWk4KxVSEDTpZ
Add an MIT LICENSE (Haotong Liang and the Lumi-Lab contributors) and
declare it in pyproject. The README gains a lead paragraph tying the
stack to arXiv:2602.20432, plus Citation and License sections.

Strip lab-internal framing from the two operator notebooks and their
README: the "ported from v1.0" narrative and v1.0-vs-now comparison
tables (there is no public v1.0 to compare against), the transient
"lab broker not ready" note in the main README, and the hard-coded
hliang16 actor / UMD project names in the notebook config cells.
Rebuilt both .ipynb from source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyxfY7uJetWk4KxVSEDTpZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant