ExLab-Wizard remaining work: T1–T13 (README generator, session-control epic, settings UI, cleanups) - #16
Merged
Conversation
Production built the creation controller without a readme_generator, so GUI-created projects/runs ran on NoOpReadmeGenerator and shipped a 3-line stub README with no YAML front matter and no readme_fields.json cache. - Inject ReadmeGenerator() in tray.dependencies._build_controller (the sole production constructor; covers the lifespan build and the apply_live_config fresh-build branch). - Replace the controller's flat placeholder ReadmeContext with the canonical layered type from exlab_wizard.readme; the controller's ReadmeGeneratorProtocol and NoOpReadmeGenerator now use the tuple[Path, Path] contract. - Add CreationController._build_readme_context: partitions readme_extra across the template/config/custom layers by id, maps template + config field declarations, and fills the §10.6 system block (created_by = OS user, project = folder name, run = run dir / null). - Presence is already gated by _validate_inputs and the GUI submits no typed extra fields yet, so the generator's strict validation adds no new failures for current creations. - Add end-to-end test asserting four-layer front matter + readme_fields.json. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Code review flagged that the §10.6 system block's `project` field must be the machine-safe LIMS short id (e.g. PROJ-0042) recorded in README metadata (§3.1), not the human-readable <project>/ folder segment. Use _short_id_for (matching the original flat ReadmeContext) and correct the acceptance test. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Confirm & Create progress bar was static (active_phase=None): nothing consumed CreationController.subscribe(), and the session_progress component keyed two of its six phases on the wrong strings. - Fix phase-name mismatch: session_progress PHASES/PHASE_LABELS used 'post_validation'/'queueing_sync' while the controller emits the wire-format 'validating_post_creation'/'queueing_nas_sync' (state_machine.Phase). Align the component to the wire-format so a live phase frame maps onto a row without translation. - Add SessionProgressState + apply_frame(state, frame): folds phase/ progress/done frames into render args; ignores input_required (T5) and unknown phases. - Render the confirm-step bar through a @ui.refreshable bound to state.progress, exposing state.progress_refresh. - mount._run_creation consumes controller.subscribe() via _consume_session_progress while the pipeline runs; race-free because _launch creates the event queue before the pipeline starts and the queue buffers early phases. The controller emits no 'progress' frame yet, so the §9.3 per-plugin sub-row stays dormant until the plugin host emits one (handled defensively). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The operations_modal component was built and exported but rendered nowhere; there was no way to inspect in-flight sessions from the GUI. - Add SessionStore.iter_sorted() (public, sorted accessor) and a shared project_identifier(request) helper; refactor the /operations route onto them so neither the route nor the UI reaches into store._sessions, and rows are labelled identically. - Add OperationRow.from_session(): maps the §4.7 state machine onto the panel's running/suspended/completed buckets. - main.py: [Operations…] toolbar button (visible when >0 in flight, warning-colored when any need input) and a footer Sync segment that flips to 'N operations need input' and opens the same modal (§3.5.5); MainPageState gains operations_count / operations_input_required. - mount.py: _operation_counts, _build_operation_rows, _open_operations_modal (fresh snapshot per open), and an _open_operation_details 'view log' dialog. Resume/cancel are baseline here (cancel keeps files); the §9.4 dialog and §9.6 disable rule land in T4, the input dialog in T5. - Fix a latent circular import: operations.py now imports SessionState / project_identifier from the controller submodules, not the package. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- _cancel_operation opens a Discard/Keep confirm dialog mapped to controller.cancel(id, discard_files=...): Discard removes the partial directory (shutil.rmtree), Keep leaves it as an orphan. Errors are toasted; cancel is a no-op on an already-terminal session. - §9.6 single-equipment concurrency: _operation_counts now also reports an 'active' (strictly non-terminal) count; _build_main_state sets MainPageState.creation_in_flight, and the New Project / Run / Test Run buttons disable (with a tooltip) while any creation is in flight. - Tests for _operation_counts (panel vs active vs input_required). Resume row action still routes through the INPUT_REQUIRED dialog landing in T5. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
When a plugin suspended mid-creation the wizard hung indefinitely with no way to answer; only the HTTP /resume route could. - New component ui/components/input_required_dialog.py: the §9.1 'Additional input required' dialog -- plugin pill, reason line, one widget per pending_input field (string/text/choice/boolean) two-way bound to a values dict; persistent so it must resolve before the next frame (§9.2). - The T2 progress consumer opens the dialog on an input_required frame and force-closes it on a terminal done/failed frame (plugin timeout). Submit -> controller.resume(sid, values) (errors surfaced as a toast; plugin re-rejection re-emits input_required and re-opens the dialog), Cancel -> the §9.4 cancel dialog. - Split _cancel_operation into a controller-driven _cancel_session core (reused by the dialog) plus a deps resolver; the Operations modal's Resume reads the parked pending_input and re-opens the same dialog. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Settings had no editor for OperatorsConfig.allowlist (the controller's allowlist gate was a permanent no-op for GUI users) and rendered content_scan_extensions read-only. - Add a reusable _render_chip_editor(values, ...) in settings.py (add / per-chip delete / optional reset; mutates the draft list in place so persistence rides the existing draft -> finalize -> Save path). - T7: add 'operators' to SETTINGS_SECTIONS (between nas_cleanup and validator) + SECTION_TITLES and an operators section with §7.9 helper text + a chip editor bound to draft.operators.allowlist. Stored verbatim (case-sensitive; trimmed on add). Non-gating. - T10: replace the read-only extensions label with the same chip editor bound to draft.validator.content_scan_extensions, with Reset to defaults and an on-add '.'-prefix validator. - Update the settings section-count test (8 -> 9, operators present). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- Consumer leak (Major): the abandoned-session GC closed INPUT_REQUIRED
sessions directly on the store, bypassing the controller's _publish, so
the in-process subscribe() consumer parked forever on queue.get(). The
GC now pushes a terminal {kind: failed} frame onto the session's event
queue so the wizard progress loop wakes and exits.
- FAILED mislabeled (Major): OperationRow.from_session mapped FAILED to
the running bucket (play glyph, no action). Add a STATE_FAILED bucket
(error glyph) so a failed op is labelled distinctly; sort places it
after running, before completed.
- Footer Sync segment is only clickable when operations_count > 0, so a
click never opens an empty Operations panel.
- Fix an inaccurate _open_input_required_dialog docstring (resume does
not reject an empty payload).
Tests for the FAILED bucket and the GC terminal-frame wake-up.
https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Application section's three controls were inert: the autostart checkbox was unbound, the tray-status label was a static literal, and the Quit button had no handler. - T8: tray/dependencies seeds deps.autostart_is_registered; _apply_autostart returns the toggle's real is_registered(); the checkbox seeds from it and applies immediately on change (exempt from the draft, §7.13), reverting to the actual post-op state on mismatch. - T9: the tray builder attaches deps.request_quit = tray_app.request_quit; the Quit button is gated behind a confirm and scheduled non-blocking via ui.timer so the HTTP response flushes before shutdown. - T11: the tray builder sets deps.tray_available from a pystray-import probe; the label reflects available / unavailable (window-only) and the window-on-close behavior copy is added. - AppDependencies gains typed autostart_is_registered / request_quit / tray_available fields. Controls disable cleanly when their hook is absent (headless / tests). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Problems tab badge and right-pane summary were hardcoded to 0 and the /problems footer showed 'Last audit: --'; nothing read the audit. - The 30s background _audit_loop now caches tier counts on deps.last_audit_hard / last_audit_soft (single source; avoids a per-render O(tree) re-audit). _build_main_state reads them into MainPageState.problems_count_hard/soft, so the tab badge and the right-pane summary are real (right-pane no longer hardcodes 'Showing 0'). - render_problems_page takes last_audit_at and renders 'Last audit: HH:MM:SS · Next refresh in Ns' with a 1s ui.timer countdown. start_audit_task=True confirmed in the tray build. - AppDependencies gains typed last_audit_hard/soft fields. Deferred (documented in the tracker): wiring the §11.5 override action and the full live WS-delta stream -- render_problems_page expects a view-model shape the raw Validator Finding doesn't provide (a pre-existing, e2e-only mismatch); reconciling that + the in-process override-write path is a follow-up. The counts/last-audit core lands here. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
…ents (T12, T13) T12 (§D1): resolve the offline-catalogue schema-version policy as treat-as-absent / WARN (§7.2.9.3, user-confirmed). read_catalogue now returns OfflineCatalogue | None -- a schema_version mismatch logs a WARN and returns None (the consumer falls through) instead of raising ConfigError; endpoint mismatch and missing/parse errors stay hard errors. The decision is recorded in the docstring; TODO(spec) removed. The caller treats None as []. Test flipped to assert None + WARN. T13 (§D2): rewrite the three stale 'plugins/registry.py owned by Agent A / not yet committed' comments in plugins/host.py to state that plugins.registry.PluginRegistry is the committed, wired production registry and _ListBackedRegistry is test-only. Pure cleanup. https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- T9 Quit deadlock (Critical): ui.timer ran the quit callback on the server's running event loop, where request_quit's asyncio.run(...) raised 'loop already running' and was swallowed -- the app never shut down. Run the hook on a fresh daemon thread instead (no running loop, so asyncio.run works; the click handler still returns immediately so the HTTP response flushes). - Problems footer countdown (Minor): clamp 'Next refresh in Ns' at 0 once the interval elapses instead of re-cycling via modulo (which misreported the wait when an audit pass slipped past 30s). - Chip editor (Nit): a duplicate add now notifies 'already in the list' and keeps the typed text, matching the validator branch (was silently cleared). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The class docstring still said a schema_version mismatch surfaces as ConfigError; under T12 it is treated as catalogue-absent (WARN + None). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Quality-only cleanups from the /simplify review (no behavior change): - Extract the §9.5 Operations-panel membership rule into one shared on_operations_panel(session) (controller/session_store.py); use it from the /operations route and the in-process _panel_sessions, replacing three copies of the DONE/ABORTED filter. - _operation_counts: derive panel from the shared _panel_sessions and use Session.is_terminal() for the active count instead of a re-derived terminal-state tuple. - Drop the box_holder dict in the settings autostart handler (the closure reads autostart_box directly; the re-entrancy guard stays). - Factor apply_frame's duplicated 'mark predecessors complete' loop into a local _complete_through helper. - Count audit findings against Tier.HARD/SOFT.value instead of bare string literals, matching the rest of the codebase. Full unit+integration suite green (2215 passed). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Run the project formatter over the changed files (line-wrapping only, no behavior change) and remove the now-inaccurate 'operators is deferred' note from the settings module docstring (operators is wired in T7). https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
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.
Implements the full remaining-work backlog in
docs/REMAINING_WORK.md/docs/REMAINING_WORK_TASKS.md(all 13 tasks, 13/13 complete). Each phase was followed by a code-review agent and the findings fixed; a final/simplifypass applied reuse/dedup cleanups. Full unit+integration suite green (2215 passed, 3 skipped);ruff check,ruff format --check, andmypyall clean across the package.Phase 1 — release-blocking correctness
ReadmeGeneratorintray.dependencies._build_controller(sole production constructor). Replaced the controller's flat placeholderReadmeContextwith the canonical layered type; added_build_readme_context(partitionsreadme_extraacross the template/config/custom layers, fills the §10.6 system block —created_by= OS user,project= LIMS short id,run= run dir). GUI-created projects/runs now ship a §10README.md+readme_fields.jsoninstead of a 3-line stub. (Review fix:system.projectis the short id per §3.1, not the folder name.)Phase 2 — session-control epic
controller.subscribe()into a@ui.refreshablebar. Also fixed a real bug —session_progress.PHASESusedpost_validation/queueing_syncwhile the controller emitsvalidating_post_creation/queueing_nas_sync(two phases silently no-op'd).[Operations…]toolbar button + footer Sync segment, populated from a sharedSessionStore.iter_sorted()/project_identifier()(the/operationsroute was refactored onto them, dropping its privatestore._sessionsreach-in).discard_files); §9.6 single-equipment lock disables the creation buttons while a session is in flight.INPUT_REQUIREDescalation dialog (Frontend §9.1): renders the plugin's fields, Submit →controller.resume, Cancel → §9.4. The consumer opens it on aninput_requiredframe and force-closes on terminal frames. (Review fix: the abandoned-session GC now publishes a terminal frame so the consumer can't park forever; FAILED ops get a distinct panel bucket.)Phase 3 — visibility & usability
/problemsfooter shows "Last audit: HH:MM:SS · Next refresh in Ns". (See "Deferred" below.)content_scan_extensionswith reset-to-defaults.request_quit'sasyncio.rundoesn't deadlock the server loop — review fix); real tray-availability + window-on-close labels.Phase 4 — cleanups
None) per §7.2.9.3 (decision confirmed); endpoint/parse errors stay hard errors;TODO(spec)removed.plugins/host.py.Deferred (documented in the tracker)
T6's §11.5 override-and-allow-sync wiring and the full live WS-delta stream are not included:
render_problems_pagerenders a view-model shape (finding.severity/.path/.state/.finding_id) that the rawValidatorFinding(tier/rule/run_path/…) doesn't provide — a pre-existing,# pragma: no cover(e2e-only) mismatch. Reconciling that view-model plus the in-process override-write path (mirroringPOST /problems/{run_path}/override) is a follow-up; the counts + last-audit core lands here.Notes
api/__init__eager-importsapi.app→ routers → the controller package mid-init) was left out of scope;operations.pywas made cycle-robust via submodule imports, butsessions.pyshares the latent pattern (it only bites when acontrollersubmodule is the very first import in a process — never in the full test run / app startup).Per-task detail and impl notes:
docs/REMAINING_WORK_TASKS.md.https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Generated by Claude Code