Merge development into main#104
Open
pskeshu wants to merge 423 commits into
Open
Conversation
Removes all 9 hardware modules from [[tool.mypy.overrides]]. Latent bugs fixed (flagged — no test env here): - devices/camera.py: BottomCamera.__init__ annotated led_control: "DiSPIMLED" but never imported the type (mypy name-defined) -> added TYPE_CHECKING import from .optical. - devices/test_temperature_controller.py: __main__ demo re-imports AcuityNanoPrecisionThermalizerAPI, shadowing the in-file test double (no-redef) -> targeted ignore. Type-only changes: annotate Optional instance attrs (client _session, device_layer system/devices/plans/history, camera _last_image(_time), acquisition num_slices/exposure/laser_config, sam _predictor/_mask_generator); dict[str, Any] for accumulator dicts inferred too narrowly (device_factory devices, device_layer extract results, sam result/changes/verification/ review_r2/results, plans results, switchbot status, client plan_kwargs/ result); assert _session not-None at use sites after connect(); `or 0` guard on a piezo-position read; 4-tuple/float coercions. mypy . clean (300 files); ruff clean.
…, top-level) (#49) Removes 19 modules from [[tool.mypy.overrides]] (ui.web routes + server + strategy_snapshot, ml.data_loader/federated, eval.decision_log/event_capture, analysis.core/steps, organisms.celegans.developmental_tracker, detection, log_config, gently top-level __init__/gently.py/agent, launch_gently). Latent bugs fixed (flagged — no test env here): - ml/federated.py: results from gather(return_exceptions=True) were filtered with isinstance(result, Exception), which misses non-Exception BaseExceptions -> use isinstance(..., BaseException). - ui/web/server.py: VisualizationServer.mesh_service / agent_bridge were set from launch_gently but never declared on the class -> declared in __init__. Type-only changes: dict[str, Any]/list[Any] container annotations; Optional attr annotations (eval file handles, viz/predictor None-inits) with TYPE_CHECKING imports; frozenset annotation on EventCapture.DEFAULT_SKIP; coerce strategy_snapshot embryo_id to str key; legacy optional-import-to-None ignores (__init__ FileContextStore, agent create_devices, data_loader Dataset stub); targeted arg-type ignores where float(None) is caught by except; list[str|None] for the chrome-path candidates; str(log_dir/...) consistency. mypy . clean (300 files); ruff clean.
Device dependency group
Persistence (session-scoped temperature.jsonl + per-acquisition meta stamp) + live SVG graph (water trace, stepped setpoint) on a Devices-tab card. First slice of the temp-strain experiment prep roadmap (A before B/manual mode). Brainstormed and approved 2026-06-27.
- BurstAcquisition accepts temperature_provider (zero-arg callable); _persist_burst_to_disk injects temperature_stamp() into per-frame metadata["metadata"]["temperature"] and burst.yaml top-level "temperature" - TimelapseOrchestrator accepts temperature_provider and threads it through queue_burst() and _apply_runtime_state() → BurstAcquisition - agent.py passes lambda: temperature_sampler.latest when constructing the orchestrator so live sessions stamp real readings - acquisition_tools: folds temperature_stamp into acq_metadata on every put_volume / register_volume call (volume path) - FileStore.get_volume_meta() accessor added (reads tNNNN.meta.yaml) - tests/test_temperature_stamp.py: TDD — None-guard + volume round-trip
…stale latest, tolerant log read
The water trace, gridlines, and tick labels used .devices-container-map-scoped --map-* vars, so the graph rendered blank outside that container. Promote them to :root-level --temp-* vars (mirroring --temp-setpoint-color) so the component is reusable when manual mode (sub-project B) re-mounts it. Same colors in-app.
…sequence acquisition)
… illumination + acquire
…presets
Replace set_laser_power(488, 0) with set_laser_config("ALL OFF") in the
laser/off proxy route so the PLogic gate turns off every laser line, not
just the 488 nm setpoint (spec §2.7 brightfield safety).
Add device-layer handlers handle_set_laser_config / handle_get_laser_configs
(POST /api/laser/config, GET /api/laser/configs) calling
DiSPIMLightSource.set() / ._get_available_configs() on the laser_control
device (group_name="Laser"). Wire matching client methods set_laser_config /
get_laser_configs. Add unguarded GET /api/devices/laser/configs proxy route.
Clears the remaining mypy errors in the last exempted modules (scripts, dataset, benchmarks/perception, diagnostics, core/store) and removes the entire [[tool.mypy.overrides]] block from pyproject.toml. `mypy .` is now clean across all 297 source files with no per-module exemptions. Notable fixes among the final 11 errors: - explorer_server: key session_embryo_counts by (session_id, embryo_id) tuple instead of str - launch_viz_server: pass the Path half of the (timestamp, Path) tuple returned by _discover_volumes to project_volume - projection_explorer: declare ExplorerHandler.session_manager as a required class var (assigned before serving) instead of `= None` - metrics: skip None ground-truth-stage keys in tool_use_by_stage - auto_label_examples: annotate save_data as dict[str, Any] Deletes three dead files that imported removed/never-existent symbols and could not run: - examples/example_dispim_workflows.py (AutofocusConfig/CalibrationConfig and dispim_* plans removed in an earlier refactor) - tests/test_text_tool_call_extraction.py (_extract_text_tool_calls never existed in committed history) - tests/validate_recursive_autofocus.py (recursive_focus_single_round never existed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…re, illumination, acquire, temp
Commit 2be3587 ("mypy: clear gently library modules") silently deleted every entry from [project.dependencies], all [project.optional-dependencies] extras (sam, torch-gpu, torch-cpu, device), the entire [dependency-groups] dev group (pytest, ruff, pre-commit, mypy), and the project classifiers — leaving only the explanatory comments. The commit message mentions none of this; it was an unintended edit. With the dependency lists empty, `uv sync` installs nothing, so the project cannot be installed or run and mypy can only ever see Any-typed third-party imports. Restores all stripped entries verbatim from 2be3587^ (the only intended pyproject change on this branch remains the removal of the [[tool.mypy.overrides]] block). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing mypy step runs deps-less (pip install mypy only), so with ignore_missing_imports every third-party import is Any and real type mismatches stay hidden — `mypy .` reports clean against the weaker check. Adds a separate mypy-strict job that installs the project via uv (cloning the gently-perception sibling first, as [tool.uv.sources] requires) and runs `uv run mypy .` with real stubs present. This is the check contributors get locally per CONTRIBUTING.md and catches genuine mismatches the deps-less run cannot. Marked continue-on-error for now: the deps-installed run still surfaces errors the deps-less baseline hid, and isolating it in its own job keeps the heavy uv/sibling setup from breaking the required lint gate. Flip to a required gate once it is clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six call sites carried `if width > height * 2: take the left half`, a guess at splitting a stitched dual-camera frame. This rig is single-camera and its native SPIM frames are 2048x512 (4:1), so the heuristic fired on every real frame and silently threw away half the image. An embryo right of centre produced an empty or garbage projection, one straddling the midline got sliced, and the same path feeds the perceiver. A genuine dual-view volume is 4D (Views, Z, Y, X) and every one of these sites already handles that case explicitly on the preceding line. So the aspect-ratio branch was never selecting a view — only truncating one. Removed at all six. tests/test_view_splitting_regression.py puts a marker in the right half of a synthetic 2048x512 volume and asserts it survives. Five of its six cases fail against the previous code and pass now; the images.py case passes either way and is kept as a guard rather than a proof. Claude-Session: https://claude.ai/code/session_01DNw6x8E3Kbd3yNXsgZxxrF
…tream
Ryan tests on the rig tomorrow. Three things stood between him and the workflow.
He never would have seen it. The devices tab opened on Map and the finished
guided flow sat behind an unlabelled fifth button. Operate is now the landing
view.
It could not be left or reversed. Progressive disclosure had been implemented as
an exclusive CSS selector list (one .op-group at a time) with every control
inside a group, so no control could outlive a step and setStep was only ever
called forward; the header stepper was styled as a breadcrumb but answered
clicks on one of its eight nodes. Every node is now a real button and the cursor
moves in both directions. This is safe because the gates that matter are
evaluated against live hardware state, not step position — revisiting Center
while the head is down still refuses to move. Progress (_states) stays monotonic
and is never rewound by navigation. Escape and an always-visible Cancel leave
any step.
Z was text, not an instrument. The device layer already returned {position, min,
max, distance_to_floor} on every axis call and the frontend dropped min/max on
the floor; a 1 Hz fdrive/bottom_z stream existed with a comment saying it was
for this view and no subscribers. Both axes now render as travel gauges — real
range, current position marked on it, hard-limit line on the F-drive — living on
a shelf outside the step system, so raising the head is one click from anywhere.
The F-drive gains a fine 1 um step.
Also:
- The head-down interlock survives a reload. It was a client-only flag starting
false, so refreshing mid-embryo re-enabled Center and would have commanded an
absolute XY move with the objective down while the strip read "up". Retract is
a relative +100, so there is no absolute safe height to derive it from — it is
persisted instead. A retract that fails no longer reports the head as up.
- One surface per camera stream. Deleted the Map's embedded bottom-cam panel and
guarded the frame handlers and renderStep on visibility, so a hidden Operate
stops decoding frames and racing the visible view for stream ownership.
- The Run chooser defaults to Manual. It defaulted to Adaptive, so the default
path launched a timelapse and never reached Center.
- The grid responds to its container, not the window. With the agent panel open
the container is ~560px on a 1792px display, which the fixed 264/248px side
columns overflowed — collapsing the viewport to a 24px strip and letting the
rail overlap it, while the stacking breakpoint never fired because it keyed off
viewport width.
Deliberately untouched: the stream encoder. Image quality was never reduced —
only frame rate (commit 6797e0a) — and the 15fps cap is what stands between a
lowered head and a repeat of the 2026-06-29 Video-TDR freeze.
Verified in-browser: landing view, forward and backward jumps, Escape from three
targets, Cancel, gauge geometry against known positions, the reload interlock,
and both layouts in light and dark.
Spec: docs/superpowers/specs/2026-07-18-operate-navigable-instruments-design.md
Claude-Session: https://claude.ai/code/session_01DNw6x8E3Kbd3yNXsgZxxrF
…t strip Follow-up to review on the rig-facing surface. Only the axis in play is shown. Offering SPIM-head controls during the bottom-camera steps invited moving the wrong drive; the bottom objective is equally irrelevant once the SPIM camera is live. Gauge visibility now follows the active camera. Awareness of the other axis is not lost — the safety strip carries the sample stage at every step. F-drive steps follow the height. The drive travels ~25000 µm down to a sample sitting around 50-60, and operators work it in bands: a jump to ~5000, then thousands, then hundreds, then tens. A fixed ±10/±100 was 2500 clicks at the top and a crash risk at the bottom. Steps are now generated per band and captioned with which band you are in, so the change is legible rather than surprising. Engagement is derived from telemetry, not just from clicks. The drive can be moved at the controller box, so a latched UI flag read "clear" while someone hand-drove the sample up to the objective — with XY centering still enabled. Either the latch or telemetry within 1000 µm of the limit now locks XY, and neither alone can report "clear". The threshold is a RIG-NOTE, unconfirmed against real geometry. Fixed a stale-telemetry bug this surfaced: the safety strip only repainted from renderStep, so under uncommanded motion TRAVEL LEFT sat frozen at its last commanded value while the sample closed in. It repaints with the gauges now. Also: - Removed the viewport floor bar. It hardcoded a 500 µm full scale and a literal "30 µm hard floor" caption, so it disagreed with the real axis limits and was showing 24800 while the drive was at 30. - The left spine says what it holds: "Where they are" over the map, "Marked embryos" over the worklist, "Mark more embryos" on the button, and an empty state that names the next action instead of "No embryos yet". - The safety strip labels what each reading means (SAMPLE STAGE / TRAVEL LEFT / LED / LASER / CAMERA) with a tooltip on each, rather than bare abbreviations. - Copy is direction-neutral about the hardware: the F-drive number falls as the sample closes on the objective, but which way it physically travels is not asserted here. "Lower the SPIM head" became "Approach the objective". Verified in-browser by replaying an approach as 1 Hz device-state ticks with no UI interaction: readout, marker, gauge foot and strip all follow, the band walks coarse→approach→close→fine, steps that would cross the limit grey out, and XY locks at 400 µm having never been clicked. Claude-Session: https://claude.ai/code/session_01DNw6x8E3Kbd3yNXsgZxxrF
The deps-installed mypy job (`mypy-strict`) has been passing since the remaining ~200 deps-only errors were cleared (#83 and follow-ups). Remove `continue-on-error: true` so a regression in the real-dependency type check now fails the workflow like the deps-less run, and refresh the now-stale comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cv4znxcdvcX4HpXUbMsRF7
The desktop shell spawns the backend with CREATE_NO_WINDOW in release, so when
something misbehaves mid-session there is no console to look at. The device
layer already had a captured-stdout tail buried in a card on the Devices tab;
the agent side had nothing reachable from the app at all.
A console button in the header opens a drawer with two tabs. Device layer reuses
the supervisor's live captured stdout and falls back to the layer's own log file
when it runs externally and the supervisor captured nothing. Agent tails the
newest {storage}/logs/gently_*.log via a new /api/logs/{source}, reading only
the last 512 KB so a multi-MB session log stays cheap.
Level filter, follow-tail, copy, Esc and Ctrl+` to toggle. Scrolling up turns
following off, because yanking the view back to the end while someone is reading
a traceback is the whole reason log viewers get abandoned. A dot on the header
button marks errors in the tail so a failure that happens while the drawer is
closed still gets noticed.
Polling runs ONLY while the drawer is open — a console that keeps fetching
behind a closed panel is the same class of hidden work that had the camera
streams fighting each other.
Two bugs found by driving it:
- The body used --img-bg, which stays dark under the LIGHT theme while --text is
also dark: black on black, every line invisible. --bg-dark/--text are a
matched pair in both themes.
- A refresh requested while a poll was in flight was dropped, so changing the
tab or the level filter appeared to do nothing for up to two seconds. Pending
refreshes are now remembered and re-run.
Also relabelled a misleading log line: "TUI client disconnected" fired on EVERY
agent-websocket disconnect, including ordinary browser tabs, which reads as the
TUI running when it is not. It now says what it is.
Claude-Session: https://claude.ai/code/session_01DNw6x8E3Kbd3yNXsgZxxrF
ci: make mypy-strict a required gate (closes #63)
Delete the step/workflow layer rather than simplify it. Operate was an 8-node phase stepper (focus/mark/choose/center/approach/focus/calibrate/acquire) with per-embryo progress states, a five-mode run chooser and CSS-exclusive disclosure. It is now three independent surfaces: Bottom cam feed + bottom-Z + detect/mark, click an embryo to centre SPIM head feed + F-drive + sheet alignment + calibrate Acquisition embryo roster + what to run The invariant: no control reads the current pane. `_pane` is consulted only by render dispatch and camera-stream ownership. Gating comes from live hardware state — the XY interlock and the F-drive floor — never from position in a sequence. Selection is a cursor: it parameterises requests and disables nothing. Frontend only; every endpoint already existed. Safety: - One chokepoint for absolute XY moves. The server does not interlock XY against the F-drive, so this predicate is the whole guard. - The interlock is now visible, not merely enforced: a banner on both camera surfaces, markers drawn locked, the canvas cursor withdrawing its affordance, and an always-reachable back-off. Previously the latch could only be cleared on a step you might never reach, stranding XY locked until sessionStorage was cleared by hand. - Telemetry may now clear a stale latch (2x hysteresis), so raising the head at the controller box is noticed. It could previously only add lock. - Camera ownership keys on pane visibility, so at most one decoder ever runs — the condition the 15fps cap exists to protect against. Fixes found on the way: - replay-recorder blocked #op-cam-img by name; the split into two viewports would have silently started recording ~100KB/frame on the main thread. - The F-drive gauge was linear over 30-25000um, making the last 200um — the entire approach-and-crash region — sub-pixel. Now log-scaled with ticks. - The marking overlay sized its backing store once during layout and never re-synced, drawing every marker at the wrong place on a stretched canvas. Now tracks the CSS box, is dpr-aware, and observes resizes. - Markers live in stage coordinates and re-project onto each frame, so they track the sample instead of the viewport. The freeze-the-frame marking mode is gone. - Hidden elements could be defeated by a class setting `display`. Operate subtabs deliberately have no number-key shortcuts: 1-6 are global main-tab navigation, so binding them either steals a key or never fires. Extracted the risky pure logic to operate-math.js with 26 node --test cases (no JS toolchain added): the pixel<->stage transform, F-drive banding, the floor gate, gauge fractions, and the interlock truth table. Stories US-10..US-15 and US-18 rewritten for the new structure; US-19 (surfaces always live) and US-20 (interlock visible) added as the acceptance tests. US-16/17 untouched — they target the Operations tab. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
It describes a design that no longer exists. Kept rather than deleted as the record of why the step model was abandoned instead of repaired. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
A blockSelector that matches nothing does not error — it silently stops blocking. The failure is severe and invisible: rename the camera <img> and rrweb starts capturing a base64 data-URI src swap at frame rate on the main thread. That is exactly what the Operate rewrite would have caused, since BLOCK_SELECTOR still named the old single-viewport id. Audit the composed selector string actually handed to rrweb (BLOCK_SELECTOR, plus HIGH_CHURN in balanced fidelity) once at boot. Misses go to one console warning and ride along on the page-load action, so a postmortem explains an unexpectedly huge or janky recording instead of leaving it a mystery. Self-calibrating rather than gated on a page sentinel: this recorder also runs on launch/login/settings where none of these elements exist, so it only reports when SOME selector in the list matched — that is what identifies the page as the one the list belongs to. Blind spot: every selector rotting at once reads as "wrong page" and stays silent. They live in different subsystems, so that is far less likely than the false positives a sentinel would produce. Verified in-browser on all three paths: silent on launch (no matches), silent on a healthy index (6/6 match), and on a simulated rot it warns naming the selector and persists blockSelectorMisses into the session's ui-replay/actions.jsonl. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
A trailing comment pushed the dom_count call over the line limit. Lifted the comment above the call rather than taking ruff's split-the-arguments fix, which reads worse. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
Written for any contributor, not just those with write access to the org repo. Development workflow: branch off `development`, PR against `development` in gently-project/gently. Covers both paths — pushing branches straight to the org repo with write access, and the fork + `upstream` remote route without it. Before every commit: the three commands CI actually gates on (ruff check, ruff format --check, mypy), plus `pre-commit install`. .pre-commit-config.yaml already wires exactly those hooks at matching pins, but the git hook is not installed in a fresh clone, so commits bypass it — which is how an unformatted file reached CI on PR #100. Also records the traps found while fixing that: the lint job stops at the first failing step, so green ruff is not evidence mypy ran; mypy-strict is continue-on-error and must not be read as the required gate; no CI runs on a feature branch until a PR exists; JS has no CI coverage at all; and gh defaults to `main` and to whichever remote it resolves unless --base/--repo are passed. Corrects the gently-perception repo reference to gently-project/gently-perception, which is what CI clones and what pyproject.toml expects beside this repo. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
…ents CLAUDE.md is the file an agent reads before touching this repo, so it should carry what an agent cannot infer and defer everything else. The first draft duplicated most of CONTRIBUTING.md's toolchain section, which is how docs drift apart. Now it points at README.md for setup/run and CONTRIBUTING.md for the lint/type toolchain, and keeps only what neither says: - Where work lands: branch off `development`, PR against `development` in gently-project/gently, with and without write access to the org repo. - The pre-commit hook is not installed in a fresh clone, so commits silently bypass ruff and mypy — how an unformatted file reached CI on #100. - Non-obvious CI behaviour: the lint job stops at its first failing step (so green ruff is not evidence mypy ran), mypy-strict is continue-on-error and does not gate, no CI runs until a PR exists, and JS has no CI coverage at all. - Running off-Windows: the `D:/Gently3` default is not an absolute path on Linux/macOS and silently creates a junk `D:` directory (issue #56), so GENTLY_STORAGE_PATH must be set. Records the --no-api/--no-auth/--no-browser combination and the 8080 UI port. Also corrects the gently-perception reference to gently-project/gently-perception, matching what CI clones and what pyproject.toml expects beside this repo. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
The instruction stands on its own. Which PR happened to expose the missing hook is trivia a future reader has no use for. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
Pass over the whole file for content that does not help someone working here. Removed: - "Debugging Data Sources" — all eight bullets restated paths already annotated in the directory-layout tree directly above, and re-hardcoded the Windows storage root eight more times. - "Drop-in API replacement" and the "replaces GentlyStore/ContextStore" framing in Key store classes — migration-era wording, and duplicated by the section below it. - The issue reference in the off-Windows note. The behaviour is described on its own terms; the issue now carries the reminder to update this file when fixed. Reframed the legacy-store section around what actually matters: GentlyStore and ContextStore are still in the package and still have passing tests, so they look live, but nothing outside tests/ instantiates them. Stated as "do not wire new code to these" rather than as data-migration history. Fixed the log-tailing commands, which never worked: `ls -t <dir>/gently_*.log` prints a full path, and the documented form prefixed the directory again, yielding `D:/Gently3/logs/D:/Gently3/logs/gently_*.log`. They now resolve the storage root through GENTLY_STORAGE_PATH so they also work off-Windows. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
Review catch from @subindevs: the mypy-strict bullet said the job was non-blocking, but #99 made it a required gate and removed continue-on-error. The bullet went stale in the week this PR sat open, and told an agent to ignore the exact job that would block their merge. Rather than correct the fact and leave the same trap, apply this PR's own defer-don't-duplicate principle to CI as well. lint.yml is now named as the source of truth for what gates, and the bullets keep only what does not depend on current config: that two mypy runs exist and can disagree (deps-less resolves third-party imports as Any, deps-installed sees real types), that a job stops at its first failing step, that CI is Python-only, and that a branch with no PR gets no signal. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
…st frame - Stop now freezes the last camera frame instead of clearing it, so the operator can keep reading the sample surface after ending live view; the "LIVE" badge dropping is the cue the frame is static. Falls back to the placeholder only when no frame was ever shown. - The viewport box (.op-cam-fit) is sized to the frame's own aspect ratio, set from the image, so the border hugs the image instead of letterboxing it inside an oversized box. - "Detect automatically" runs on the frame already on screen when one exists: the device layer caches the last full-res stream frame and the stage XY it was taken at, and detect_embryos reuses it (skipping capture and the LED/room-light dance). Falls back to a fresh capture when the viewport is empty. - A spinner overlay is shown over the image while detection runs. - Log the traceback in the detect_embryos capture/SAM except blocks (device layer + client) instead of only returning it in the response body, so environment failures (missing deps, checkpoint, CUDA) are diagnosable from the device log rather than leaving a clean-looking log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
uv sync (--extra torch-gpu --extra sam) caught the lock up to the already committed pyproject: gently version bump to 1.0.0.dev0 and the playwright dev dependency (plus its transitive greenlet/pyee). No runtime dep changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shared embryo rail (left of the camera, side-by-side across Bottom/SPIM):
- A persistent embryo list left of every instrument surface, rendering the
canonical set. Click a row to select (highlights everywhere); an × removes
a false positive. Bootstraps from /api/embryos/current on load so it
survives a browser refresh instead of vanishing until the next mutation.
- Hidden on Acquisition (its own roster is richer); stacks above the pane
only at the smallest width.
Count bootstrap:
- The global "N embryos" header/footer read /api/embryos (disk store), which
is empty until a volume is acquired, so they showed 0 on load. Bootstrap
from the in-memory /api/embryos/current (mapped to ids) and fan out an
EMBRYOS_UPDATE so the header strip refreshes too.
Detect UX:
- Return and display the frame SAM ran on, so a fresh-capture detect shows
the image the candidates came from instead of a blank viewport.
- Re-running Detect replaces the previous auto-detected set instead of
stacking onto it (kept: manual marks).
Persist embryos to session files:
- Register writes each embryo to embryos/{id}/embryo.yaml (best-effort,
never blocks registration); delete removes it from disk via a new
FileStore.delete_embryo so a removed false positive doesn't resurrect.
Reload on restart is via the existing --resume path (list_embryos).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detect on an empty viewport was a single capture+SAM round-trip, so the
image only appeared at the very end, together with the overlays. Split it
into two phases so the operator sees the image first:
- device: detect_embryos now caches the frame it captures, and honours a
capture_only flag that returns the (illuminated) frame WITHOUT running SAM.
- client/route: thread capture_only through.
- operate.js: when the viewport is empty, runDetect first calls capture_only
and displays the frame ("Capturing…"), then calls use_last_frame to run SAM
on that exact image ("Detecting…") and overlays the results. When a frame is
already on screen it skips straight to detection on it.
Sequence is now: image → detecting spinner → overlaid candidates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review catch from @subindevs: `node --test tests/js/` fails on this branch — `tests/js/` exists only on the operate branch (#100), not on development. The old wording also implied JS tests already exist on development, which they don't. Reworded to the durable fact — CI runs no JavaScript, so verify UI changes by running the app — which holds whether or not an in-tree JS suite exists, so it does not go stale when #100 lands one. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
#102 deletes CLAUDE.md's duplicated toolchain text and defers to CONTRIBUTING.md as canonical — which only holds if CONTRIBUTING.md is right. It wasn't; the incremental-typing migration finished and the doc never followed. CONTRIBUTING.md: - "Type checking" described a `[[tool.mypy.overrides]]` / `ignore_errors = true` ignore-list and a policy for managing it. That block no longer exists in pyproject.toml (ignore_errors: 0 occurrences), so the whole mechanism and its policy were describing something gone. Referenced issue #46, which is closed. Replaced with the durable fact: two mypy runs (deps-less `mypy .` with ignore_missing_imports, and deps-installed `uv run mypy .`) that can disagree, and how to reproduce each. New code must pass both. - "CI" said one mypy runs and `pre-commit run --all-files` fixes any failure. Since #99 there are two mypy jobs and pre-commit reproduces only the deps-less one. Now points at lint.yml as the source of truth for gating and names what pre-commit does and does not reproduce. Deliberately cites no issue numbers: #49 is OPEN but its body describes 369 mypy errors that have since been cleared (mypy . passes clean today), so the tracker is not a reliable source — only pyproject.toml and lint.yml are. CLAUDE.md: its one-line pointer described CONTRIBUTING.md as covering "the incremental-typing policy", the section just removed — updated to match. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
`lint.yml` installed ruff unpinned (`pip install ruff`), so CI floated onto ruff 0.16.0 the moment it released and `ruff format --check .` began failing every PR — while the pre-commit hook and local dev stayed on the pinned v0.15.17. The pre-commit config's own comment promised "local runs, the pre-commit hook, and CI all use the same" ruff, but only mypy was actually pinned everywhere; ruff had an exact pin in pre-commit and a floor (`>=0.4.0`) in pyproject with nothing pinning CI at all. Adopt 0.16.0 deliberately and pin it in all three places, matching how mypy is already handled: - `.github/workflows/lint.yml`: `pip install ruff==0.16.0` - `.pre-commit-config.yaml`: ruff-pre-commit rev `v0.16.0` - `pyproject.toml` dev deps: `ruff==0.16.0` (was `ruff>=0.4.0`) 0.16.0 formats Python code blocks inside Markdown by default, so `ruff format .` restyled the examples in 19 docs (quote normalisation, comment spacing, blank lines). No source files changed — all `.py` was already clean under 0.16.0. Verified: `ruff check .` and `ruff format --check .` both pass, and code-fence counts are unchanged in every doc (no block merged or dropped). Future ruff bumps are now a deliberate `pre-commit autoupdate` plus the two matching pins, not a silent CI drift. Claude-Session: https://claude.ai/code/session_01XMWRh8uasxiH9jKiDxYtrZ
chore: pin ruff to 0.16.0 across CI, pre-commit, and pyproject
docs: make agents and contributors effective — CLAUDE.md + CONTRIBUTING.md
Bind the capture error to a local before formatting it into the response so the line fits the 100-column limit, and let ruff format rejoin a log message that fits on one line. No behaviour change.
…-instruments Operate: navigable steps, position-aware instruments, and the half-frame fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mainhas not moved since 1 June whiledevelopmenthas taken 397 commits. Anyone landing on the repository sees a snapshot that is two months behind the work.This brings to
main:developmentCONTRIBUTING.md— dev setup, pre-commit hooks, how to run the checks, and the incremental typing policydevelopmentis 397 ahead and 0 behind, so this is a fast-forward with no conflicts.Requires one approving review —
mainis protected with admin enforcement on.