Merge fork master: land chat ops, workspace FS, MolRec metrics UI - #12
Merged
Merged
Conversation
…e_to guard (hardening P2-6)
- Delete _slugify_name_to_id (folder.py): zero callers; _validate_name_to_id is
the one live name→id helper (used by add path + move_to).
- Experiment.__init__: replace __import__('molexp.workspace.fs_local', ...) with
a function-local 'from .fs_local import LocalFileSystem' (matches Project/Run).
- Folder.move_to: it uses OS-level shutil.move (local-only) — guard against
remote-backed folders with a clear NotImplementedError instead of silently
doing local I/O on a remote path.
…(hardening P2-5) (1) TaskSnapshot.from_task_body AST-parsed + normalized + hashed each body's source on every call; codec.ir_to_spec re-snapshots all tasks on every IR deserialization. The source hash depends only on the code object, so it is now memoized via a code-object-keyed lru_cache (_normalized_source_hash) — a body is AST-parsed once, not once per snapshot. Hash values are unchanged (golden + cache + snapshot suites green). (2) _build_deps rebuilt registration_by_name / parallel_decls / loop_max_iters from the frozen CompiledWorkflow on every execution. These are now @cached_property on CompiledWorkflow (registration_by_name / parallel_decls_by_body / loop_max_iters), derived once and reused; only the anyio CapacityLimiters stay fresh per run.
…us poke (hardening P2-1) The workflow runtime recorded a non-propagating task failure by reaching into the workspace-private run_context._context.status dict with a 'failed' string convention (_record_run_failure). Replace that cross-layer private-attribute coupling with a typed contract: - RunContext.mark_failed(error) on the public surface sets status[run]=FAILED and records the error message (lifecycle.exit already consults status[run]). - RunContextLike protocol declares mark_failed so the workflow layer calls it typed; _record_run_failure now just invokes it (duck-typed/defensive). - Drop the now-unused _context private alias (it existed solely for this poke). Behavior unchanged (parallel-failure + clean-exit-failed paths green).
…dening P2-4) models.py documents that RunMetadata.target is 'validated against WorkspaceMetadata.targets at write time', but nothing enforced it. Add _validate_target_registered (folder.py): add_run(target=) and add_experiment(default_target=) now reject an unregistered target name when the workspace has a non-empty target registry. A registry-less workspace keeps accepting free-form target strings (back-compat), so existing local-only usage is unaffected. Note: the second half of P2-4 (reregister_artifact on-disk content-hash integrity) is intentionally left to rebuild(): a per-call disk check would require the pure derived-index catalog to resolve AssetScope -> absolute filesystem path (replicating the Folder layout), a coupling the index avoids; rebuild() is the sanctioned stale-row reaper (rescans manifests).
…g P2-2) compiler.py was at 834 lines (touching the file-size cap). Move the three pure graph-structure helpers — iter_targets, compute_back_edges, compute_indegree — to a new _graph_analysis.py (module-level functions, no pydantic_graph, no compiler state). compiler.py drops to 789 lines and the back-edge/indegree logic is independently testable. Behavior unchanged (full workflow suite green).
…flowLike (hardening P2-2) run.py defined the _WorkflowLike Protocol mid-file (between import groups), forcing every subsequent import to carry '# noqa: E402'. _WorkflowLike is annotation-only (used solely as the bind_workflow_version param type under 'from __future__ import annotations'), so move it — and the Protocol import it needs — into the TYPE_CHECKING block alongside Workspace/Experiment. All runtime imports now sit together at the top with zero E402 suppressions. Behavior unchanged (full suite green).
…dening P2-2) node.py mixed three concerns (603 lines). Split into: - node.py (346): sentinels, _classify_return dispatch, run_task_body + _invoke_body_with_ctx body dispatcher, _collect_upstream_outputs. - node_cache.py (172): content-addressed caching — run_task_body_cached + its _is_json_safe / _cache_inputs / _artifact_manifest / _reregister_artifacts. - node_params.py (123): dependent-params resolution — _resolve_dependent_params + the _UpstreamView / _UpstreamAssetsView proxies. Import DAG is acyclic: node_cache -> node -> node_params. compiler.py imports run_task_body_cached from node_cache. No new pydantic_graph import sites. Behavior unchanged (full suite + import-guard green).
…2-2) run.py held both the Run entity and the large RunContext facade (677 lines). Move RunContext (+ its _WorkflowLike Protocol) to runcontext.py. The two mutually reference each other (Run.start constructs RunContext; RunContext.open reconstructs a Run), so the cycle is broken the same way RunContext.open already handles Workspace: run.py imports RunContext at module level, and runcontext.py imports Run only under TYPE_CHECKING + via a function-local import inside open(). RunContext is re-exported from run.py so 'from molexp.workspace.run import RunContext' keeps working. run_lifecycle's TYPE_CHECKING import repointed. Behavior unchanged (full suite + no import cycle).
…sy-poll (hardening P0-3) The deadlock barrier previously slept asyncio.sleep(20ms) between checks, so a coalescing-Join Step waiting on a slow upstream paid up to 20ms latency per wide fan-in. Replace the poll's wake mechanism with an asyncio.Event: WorkflowState.signal_progress() sets it whenever a result is recorded or a task body finishes, so the barrier wakes the instant its dep can be satisfied. The frontier-exhaustion deadlock guard is unchanged in spirit and MUST stay: asyncio.Event alone would await forever on a never-recorded dep (the original P0-3 hang). The barrier now does wait_for(event.wait(), timeout=_DEP_BARRIER_POLL_S) — event for the fast path, timeout as the periodic re-check that drives the quiescence counter; signalling on body-finish ensures the last body completing wakes the waiter to detect the deadlock. Tests unchanged: bounded deadlock + slow-upstream-not-tripped both green; full suite 1884 passed.
Replace the single run-level --resume with two distinct verbs, both acting
on the same run_id (no clone, no new Run):
- resume: reopen the run's last non-succeeded execution and continue at
workflow-node granularity — seed already-completed node outputs from
disk (read_node_outputs) and recompute only the rest.
- rerun: a fresh execution (exec-{run_id}-N) on the same run, no seed.
Missing precondition is always an error, never a silent fallback:
resume with no resumable execution → CLI error / server 409 pointing at rerun.
Layers:
- workflow: read_node_outputs reader + to_ir(strict=False) observability
serialization (tolerates slug-less tasks); make_execution_id reused.
- workspace: RunLifecycle.enter reopens an existing ExecutionRecord in place
(status→running, finished_at cleared) when given a matching execution_id.
- cli: molexp run --resume / --rerun (mutually exclusive), threaded through
the local handler and the --bg worker.
- server: POST /{run_id}/resume + /{run_id}/rerun → RunContinueResponse
(runId + executionId); the clone-to-new-run rerun route and
RunRerunResponse are removed (breaking API change). SubmitHandler accepts
an explicit execution_id. UI OpenAPI client regenerated; api.ts exposes
resumeRun/rerunRun.
Also brings the working tree to green: ruff/ty clean across the repo and
the full suite (1928 passed) green, including in-progress work that was
previously failing type/lint/test checks.
…roring A run that has never executed (pending, empty execution_history) was being selected by --resume and then rejected with "no resumable execution; use --rerun" — which broke the common batch-matrix flow where build scripts declare many runs and --resume is used to advance the whole set. Running a pending run's first execution is not a fallback (there is no failed attempt to fall back from), so resume now runs it fresh. A failed run is still reopened+seeded (true node-granular resume); succeeded runs are still skipped. Unified across CLI (--resume) and the server /resume route (was 409, now runs the first execution fresh).
Make the three execution verbs unambiguous and collision-free: - plain `run` now starts only work that has not run: create missing + run `pending`. It no longer silently re-runs `failed`/`cancelled` runs — retrying a failure is an explicit `--resume` (continue) or `--rerun` (from scratch). - `--resume` / `--rerun` retry an existing, profile-matching, non-succeeded run; they differ only in handling a finished non-succeeded execution (resume reopens+seeds, rerun starts fresh). - All three now skip a live `running` run (a Run has one ownership stamp + one status, so a second concurrent execution must never start). A dead `running` run is still reaped to `failed` first, so crashed runs remain retryable; a stuck-but-live run must be killed before retrying. Documents the state machine as an invariant in CLAUDE.md.
Each verb now owns a disjoint slice of run status; everything outside a
verb's domain is skipped (CLI) / 409 (server):
- run → start what has not run: create missing + run `pending`.
- resume→ failed/cancelled ONLY: reopen last execution + seed completed
nodes (continue from where it stopped).
- rerun → failed/cancelled ONLY: fresh new execution (re-execute from top).
resume/rerun no longer touch `pending` (that is run's job) — they skip it
instead of running it, so the three verbs never overlap. They still skip
`succeeded` and a live `running` run; a dead `running` run is reaped to
`failed` first and remains retryable. Server resume/rerun return 409 outside
the failed/cancelled domain, matching the CLI skip. Documented as an
invariant in CLAUDE.md.
…ontent-addressed runs (02/06)
…ession-lock parallel edges (03/06)
…never-ran dep (05/06)
…y capable) (06/06)
…pe=) The serialization slug is a property of the task type (registered via TaskTypeRegistry.register), resolved at compile time via slug_for(), so WorkflowCompiler.add no longer takes a task_type argument. Codec and tests updated to match.
…rialization layer pure-task-context-02-materialize-and-collapse. Public TaskContext drops run_context and deps (raise AttributeError); inputs/config plus a retained read-only state (loop/branch data-flow, full removal deferred). New engine materialization layer (content-addressed workdir + return-value persistence, activated). Capabilities-as-inputs: SubWorkflow gets an injected sub_runner, promote_callable target is now fn(inputs, config), SweepMap returns per-cell records. Sweep params + a non-navigable Path workdir injected as root inputs.
…tries Batch-lands the 2026-06-10 tree: - harness: RunMode pipeline (test-gated workflow execution), molexp plan --execute, run fingerprinting, audit/final-report stages + validators - agent: loop compaction wiring, MCP builder, operator-config bridge - workflow: values-on-edges ExecutionPlan engine, persistence coalescing, resume seed integrity, subworkflow persistence - workspace: run heartbeat, run.json lock, params unification, execution results - cli: plan command, TUI tree monitor, zombie reaping, info command - ui: WorkspaceApp split (Rules of Hooks), regenerated API client, agent viewer & entity pages - ui-creation-entries: workspace create-on-open (404 -> confirm -> create_if_missing; open route materializes new dirs) + WorkflowsPage "New workflow" (POST experiment -> seed empty IR -> graph editor) + optional workflow field with tooltip in CreateExperimentDialog; MSW mocks mirror both flows (Python 2275 green, UI 278 green)
…ctLineageStore The provenance→lineage rename left this bench importing the deleted molexp.harness.store.sqlite_provenance_store module. CI's cold ruff resolved the dead import as third-party and failed the lint job on import grouping; the bench itself would ImportError at runtime.
…for FAIR research"; refresh architecture blueprint - Sync the one-line description across README, pyproject, PRODUCT.md, CLAUDE.md, docs/index.md, zensical.toml - README: add molexp.harness capabilities row + agent/orchestration intro - Refresh .claude/notes/architecture.md via /mol:map (agent mode->loop rename, harness Mode/PlanMode/RunMode realized, server/CLI restructure)
…B/C) Aggressive orthogonality cleanup across all layers (workflow-driven, per the tests/README.md contract) plus a function-level-only filter: - per-file prune to the contract: drop trivial/redundant/cross-layer/cosmetic tests, reorganize into class->TestClass mirroring src classes, clear names - delete ALL end-to-end / integration tests: FastAPI TestClient route tests and click CliRunner command tests (test_server/test_cli gutted to units), subprocess/example-script runners, doc-code-block runners, soak/perf/ write-amplification tests, and the full 9-stage PlanMode e2e drive - keep architectural invariant locks (import-guard AST scans, public-surface locks, engine-boundary, layout law) and one-per-verb core-contract tests Result: ~2555 -> ~1670 tests; full parallel run 13+ min -> ~33s. Remaining reds are pre-existing plan-step-audit WIP (sequential_task_build returns YAML not JSON) — 2 function-level test_plan_preflight cases. Note: the sys.modules-based import-guard tests are xdist-isolation sensitive (pass in isolation; can flake under -n when a worker preloaded pydantic_ai).
…-03-plan-tools) New in-process plan tool subpackage molexp.harness.plan_tools (not re-exported at the harness top level): PlanTool descriptor + PlanToolResult, TaskBoardHandle Protocol, seven immutable board-state tools (side_effects=[]), run_capability / run_acceptance_test routing through the new module-level dispatch_capability (extracted from InvokeCapability.run, behavior unchanged), and the as_loop_tool harness->agent adapter with before-tool side-effect gate + after-tool audit hook. Function-level tests only; harness.__all__ still 21; regression exit 0.
…n-emergent-04-task-board-state) New molexp.harness.plan subpackage (not re-exported at harness top level): frozen TaskBoard/BoardTask/FeasibilityAnnotation + TaskStatus/Difficulty enums, pure version-bumping transitions (place_task/set_task_status/annotate_feasibility/ remove_task, TaskNotFoundError), full-rewrite board_store with optimistic-version BoardVersionConflict, and ExperimentPlan(extra=forbid) + freeze_spec/ freeze_experiment_plan content-addressed via FileArtifactStore.put_json. Function-level tests only; harness.__all__ still 21; regression exit 0.
…_report_renderer (plan-emergent-05a-gateway) Generalize RouterBackedAgentGateway.call to dispatch on AgentCallSpec.call_mode (default "structured"): _call_structured (verbatim complete_structured path) vs _call_agentic (drives stream_agentic, serializes the full ReAct trace as raw "log" before parsing FinalChunk.text; identical lineage; parse failure raises with raw already persisted). Register create_experiment_plan (agentic+molmcp) and plan_report_renderer (structured) across the dense maps; append experiment_plan/plan_report to WELL_KNOWN_ARTIFACT_KINDS. Also removed retired files (agent/modes/*, workflow/_pydantic_graph/*, tests/test_agent/modes/*) resurrected by a stray stash-pop conflict. Function-level tests; harness.__all__ still 21; regression exit 0.
Three independent, unit-tested pre-approval-gate components (subpackage-path, not in harness.__all__): EmergentPlanFormValidator (pure should_stop form check over ExperimentPlan → PlanValidationReport, one code per defect class, never raises), PlanReachabilityProbe (read-only registry.search grounding → per-task FeasibilityAnnotation, new board, zero artifacts), and build_experiment_plan_review_pack (hard-policy three-action ReviewPack over the pre-approval experiment_plan artifact). Adds "experiment_plan" to _TARGET_KINDS. Function-level tests; harness.__all__ still 21; regression exit 0.
…emergent-05c-orchestrator) EmergentPlanOrchestrator (harness.modes.emergent_plan, not in harness.__all__; coexists with the old nine-step PlanMode) composes the prior phases into async run(*, run, user_input, gateway, capability_registry) -> ModeResult: store bundle, plan-tool loop with an EmergentPlanFormValidator should_stop guard (malformed board never surfaced), PlanReachabilityProbe annotation, deterministic guard, StepAuditLoop hard gate (store-first suspend via ApprovalPendingError / stored-grant replay), freeze_experiment_plan, plan_report_renderer render. PlanLoopRunner seam + private lazy-import InteractiveLoopPlanRunner. Integration seams: public router accessor on RouterBackedAgentGateway; ApprovalIntent += approve_experiment_plan; a backward-compatible tools-injection seam on InteractiveLoop (empty default = byte-identical); session-storage re-export via agent.session. Import guard widened to a harness->agent allowlist (runtime no-SDK/no-workflow invariant preserved via lazy import). harness.__all__ still 21; spec-02 loop tests green; regression exit 0.
…ization-phase) Second-phase realizer for the frozen task board: RealizeBoard(Stage) maps a codegen self-repair worker (realize_one_task) over EVERY BoundTask in parallel (full coverage by construction), reduces greens into a single workflow_source + test_source, then MaterializeExecution + CompileWorkflow --compile-only. A task that never greens by the attempt ceiling returns blocked (never raises); on any block RealizeBoard persists a durable intervention_request then raises TaskRealizationBlockedError BEFORE compile — never auto-reverts to phase 1. Generalizes SequentialTaskBuild's per-task codegen+pytest into shared task_codegen.py (behavior-preserving; per-task realize/<slug>/ isolation for the parallel map). New InterventionRequest/BlockedTask schemas, TaskRealizationBlockedError, input_set_to_param_space bridge. harness.__all__ still 21; existing codegen tests green; regression exit 0.
…-suspend-resume) Cross-process suspend/resume covering both suspend kinds (phase-1 approval gate and phase-2 intervention request) through the one shared services path (drive_plan_mode / PlanTask / decide_plan_review) — Python == UI. - ApprovalRequest gains typed scope + target_agent_id; ApprovalIntent adds task_intervention; SQLiteApprovalStore round-trips both columns with an idempotent additive migration (legacy rows -> approval_gate); replay laws intact - resume_scope.py: resolve_resume_scope + ResumeDriver seam + RouterBackedResumeDriver + propose_plan_patch fallback + seed factory - PlanTask.resume dispatches by the answered request's scope (phase-1 unchanged); resume_intervention re-runs the named subagent; decide_plan_review threads the human guidance payload into the scope-correct resume - approvals inbox surfaces scope/targetAgentId (function-level tested via _items_for) Function-level tests; harness.__all__ still 21; plan_runtime SDK-free; regression exit 0.
…hestrator (plan-emergent-08-cutover) Greenfield cutover finishing the emergent->deterministic PlanMode rewrite. Delete the replaced orchestration (Mode ABC + .mode_ledger + _evict_rejected_producers, nine-step PlanMode/PlanStep, RepairLoop, SequentialTaskBuild) and repoint the shared Python==UI plan driver onto the two-phase EmergentPlanOrchestrator (built + tested in 01-07): - services/plan_runtime/task.py (PlanTask._drive) + resume_scope.py + cli/plan_cmd.py now construct EmergentPlanOrchestrator; drive_plan_mode / _ModeLike / CLI flags / exit codes / routes unchanged; CLI banner reshaped to the two-phase flow; --execute is an honest notice (realization is a separate phase, no fabrication) - harness.__all__ 21 -> 20 (drop Mode/PlanMode, add EmergentPlanOrchestrator, keep ModeResult); modes/__init__ exports the orchestrator; StepAuditLoop retained - delete orphaned tests (test_mode/test_ledger_eviction/test_repair_loop); retarget test_evidence_repair/test_step_audit_loop/test_assemble_knowledge_context/ test_cli/test_plan_cmd; update public-surface lock; import guards stay green - CLAUDE.md harness charter + .claude/notes/architecture.md rewritten to the two-phase architecture with the corrected 20-symbol surface Full suite: 1806 passed / 2 pre-existing plan-step-audit WIP reds; import molexp.harness pulls no SDK/workflow; regression exit 0.
… root residue Sync /mol:map inventory notes, mark plan-emergent 01–08 closed in INDEX, and ignore accidental root workspace/e2e artifacts.
Apply ruff isort fixes on harness plan-tool tests and resolve UI biome format/lint findings so the suite is greener under local hooks.
Lint stays ruff-pre-commit until the tree is full-tree ruff-clean. Tests use uv run --extra dev tox -e py.
Tox wheel isolation diverged from proven runners and broke Actions. Hooks and CI now run the same `uv sync --extra dev` + ruff/pytest commands.
Adds the Chat-side agent operations surface under `agent/ops/`: - `builtins.py` / `tools.py` — the chat tool set (inspect + scratch code + discover); the full/archive surface adds `workspace_ensure` / `run_land`. - `land.py` — land ad-hoc chat products onto a real Run (workspace storage only: copy sources + products as assets, settle the lifecycle). - `embed.py` — `embed_plot` / `embed_structure` return a JSON envelope the router peels into ToolResultChunk artifacts, so molplot charts and molvis structures render in the conversation instead of markdown PNG dumps. - `chat_policy.py` — BeforeToolHook denying authoritative workspace mutators so MCP tools cannot bypass the chat surface. Also makes the router preflight real: pydantic-ai 2.x defers `infer_model` to the first run, so `Agent(...)` construction validates neither the model id nor its credential. `PydanticAIRouter.preflight()` now resolves each tier's model (memoized back into the tier map) — restoring the fail-before-any-write guarantee `molexp plan` is built on.
Renames the plan pipeline to its steady-state name and adds Chat as a peer mode: - `EmergentPlanOrchestrator` -> `PlanOrchestrator`, module `modes/emergent_plan.py` -> `modes/plan_orchestrator.py`, and `EmergentPlanFormValidator` -> `PlanFormValidator`. "Emergent" described the rewrite, not the thing. - `modes/chat.py` — ChatMode, a peer of Plan on the same InteractiveLoop but with a different tool surface and land policy: no authoritative project/run creation, no `run_land`, code confined to `agent/.scratch/`. - `plan/disk_board.py` — the production TaskBoardHandle the plan tools drive; every write returns a new instance so the immutable-handle contract holds. - `plan/bind_board.py` — the single board -> bound_workflow + experiment_spec conversion seam, so realization never re-implements the mapping. - `plan/document.py` — the 12-section experiment plan book projection, shared by the review UI and prompt as one source of truth for the outline.
No backward compatibility: `modes/emergent_plan.py`, `validators/emergent_plan_form.py` and the `EmergentPlanFormValidator` alias existed only to bridge the rename. Every call site is on the new spelling, so the shims go rather than accumulate as fossils.
- `plan_runtime/loop_events.py` projects the planning InteractiveLoop's AgentEvents into the agent-task `events.json`, so the Agents UI animates thinking/tool steps instead of sitting on "Drafting…" until the whole LLM pass returns. Lives in services (harness stays free of services imports via the injected `on_loop_event` observer). - `preflight_plan_router` no longer loses the install hint when the agent stack goes missing *after* the top-level import — the lazy `molexp.agent` re-export memoizes the router class, so a missing pydantic-ai submodule surfaced as a generic "model failed its preflight check". - tests: one root autouse fixture isolates the whole suite from the developer's `~/.molexp/config.json` and snapshots/restores the process-global `molexp.config`. A single bridged `agent.models` map used to re-tier every later test in the process, which is why the unknown-model and missing-credential preflight tests passed alone and failed in a full run.
- `routes/agent_admin.py` is now the Settings page's real backend: provider config read/write through the one shared `services.operator_config` loader (the same file `molexp config set` writes), a no-disk/no-network preflight test route, and MCP server management. Key values are never echoed — only `apiKeySet` plus a masked preview. Replaces the 503 catch-all stub that forced every pure-UI user back to the CLI before their first AI call. - `server/shutdown.py` — cooperative stop flag long-lived SSE generators poll, so uvicorn exits in seconds instead of hanging on an open browser tab. - `app.py` no longer builds an app at import time. `app = create_app(...)` at module scope meant *importing* the module read `~/.molexp/config.json` and bridged it into the process-global `molexp.config`. Dev/serve now use the factory form (`uvicorn --factory molexp.server.app:create_app`), with a guard test pinning the import-purity property. - `mcp_capabilities.py` accepts a `sources=` allowlist so discovery pins to the operator's packages and unrelated catalogs never enter the plan binder.
- Workbench layout (`components/workbench/`, `panels/BottomPanelContent`) and a de-carded visual language: `components/ui/card.tsx` is gone, tokens and surfaces move into `styles/tailwind.css`. - Chat rendering for the new agent embed tools: `AgentPlotChart` (molplot), `inlineStructure` (molvis), plus `LandDecisionBar` — the Yes/No archive prompt the chat preamble is written against. - Plan surfaces: `PlanDocumentCard` / `experimentPlanDocument` render the 12-section plan book, `PlanDecisionBar` drives the review gate. - Settings: `ModelPicker` and `KnowledgeSourcesPanel` against the real provider/MCP admin routes, ending the pure-UI user's detour to the CLI. - `scripts/patch-generated-api.mjs` keeps the generated client in step with the OpenAPI surface. typecheck clean; 438 UI tests pass.
- Architecture + guide docs describe the two-phase `PlanOrchestrator` flow; `tests/test_docs/` pins that contract (docs name PlanOrchestrator, never the retired EmergentPlanOrchestrator, and describe two phases). - CLAUDE.md: harness public surface is 22 symbols, not 20 — `ChatMode` and `chat_loop_config` joined it when Chat became a peer mode. - `.claude/notes/ui-guidelines.md` records the UI ladder the workbench rework follows; `zensical.toml` gains the published site_url.
BREAKING CHANGE: molexp now requires Python 3.14. molpy is a real dependency of molexp (the preview path needs `Frame` and the io readers), and current molpy — like molrs under it — requires Python >=3.14. Keeping a 3.12 floor would have meant pinning the siblings to an old line, so the floor moves instead: `requires-python`, ruff `target-version`, both tox envs and the CI + publish workflows are all on 3.14. - `molcrafts-molpy` joins `[project] dependencies`; `[tool.uv.sources]` binds molpy (and, for local dev only, molrs) to the sibling checkouts as editable. molrs appears in the dev extra purely so uv's path override applies to it — uv sources do not reach transitive requirements — never as an API molexp may import. - molrs is now reached ONLY through molpy: `server/preview.py` and the preview fixtures import `Frame` from molpy. `tests/test_server/test_preview.py` also drops its `importorskip`, which was wrong twice over — molpy is not optional, and since pytest 8.2 a non-ModuleNotFoundError from it aborted collection of the whole suite whenever the sibling checkouts drifted apart. - `uv lock --upgrade` refreshes every dependency (pydantic-ai 2.3 -> 2.22, fastapi 0.139 -> 0.141, uvicorn 0.49 -> 0.52, …). - `promote.py` drops `asyncio.iscoroutinefunction` (deprecated in 3.14) for `inspect.iscoroutinefunction`. 1890 tests pass on 3.14.
Mechanical only, produced by `ruff format` / `ruff check --fix` after `target-version` moved to py314: PEP 758 unparenthesized `except A, B:` groups, and UP043 dropping redundant `None` type arguments. No behavior change; separated from the version bump so that commit stays readable.
- New `docs/en/architecture/harness.md` — the harness layer had no architecture page at all: what it owns vs what workspace owns, the 22-symbol public surface, the two shipped modes (`PlanOrchestrator`'s two phases, `ChatMode`'s scratch-only contract), and the boundaries that keep the agent edge to one Protocol and the workflow engine out of process. - `.claude/notes/architecture.md` re-synced to the code: `Emergent*` spellings gone, `ChatMode` + the 22-symbol surface, the new `agent/ops/`, `harness/plan/`, `loop_events` and `shutdown` modules, the factory-only server app, and the accurate `preflight()` description. - README carried three factual errors: it required Python >= 3.12, listed `pydantic-graph` as a core dependency (removed when the engine became self-owned), and described the harness as the retired one-pipeline `PlanMode`. Also adds the missing `services` and `knowledge` rows. - The three plan-emergent regression scripts imported the deleted `Emergent*` shims. Renamed, and pinned to `realize=False` — each one's own contract says "offline, no subprocess", which default-on phase-2 realization broke. All three pass again.
- **UI checks were CI-only.** `ci.yml` runs biome + tsc + vitest; nothing ran them locally. Added a pre-push `ui-checks` hook invoking the same command the job does. That job is `continue-on-error` because the UI reaches @molcrafts/molplot and molvis through `file:../../` paths a CI clone does not have — so until those are published this hook is the only place UI regressions are actually caught, and the config says so. - **The CI-parity test hook was overwriting the dev environment.** It syncs with `UV_NO_SOURCES=1` to reproduce the published-dependency resolution; with molpy now a real dependency that also replaced the editable sibling checkouts in `.venv` on every push. It runs in `.venv-ci` instead (gitignored), so the parity check is unchanged and the dev env survives. Verified: `prek run --all-files` and the pre-push stage both pass, including the no-sources resolution on Python 3.14 — i.e. what CI will do. Not closed, needs a decision: `mol_project.build.check` claims `ty check src/`, which runs in neither CI nor pre-commit and currently reports 145 diagnostics. Adding it as a gate would block every commit; loosening it would make it a fake gate. Left as-is and reported rather than silently dropped.
145 diagnostics → 0, fixed at the root rather than suppressed. `ty` was declared in `mol_project.build.check` but ran in neither CI nor pre-commit; both now run it. Three were live bugs, not typing noise: - `routes/agent_admin.py` read `entry.spec.type` off an `McpServerEntry` that has no `spec` — `hasattr(entry.spec, ...)` raises on its own argument, so the MCP health probe crashed whenever it was reached. The field is `transport`. - `InteractiveLoop._chained_before` called the injected before-tool hook **positionally** while `BeforeToolHook.__call__` is keyword-only, so any Protocol-conforming hook raised TypeError. It now goes through the canonical `invoke_before_tool`. - `harvest_run` / `harvest_session` took an LLM- or CLI-supplied `kind: str` straight into a `Literal`-typed API behind a `type: ignore`, deferring an invalid kind to a mid-turn pydantic error. New `workspace.parse_knowledge_kind` validates at the boundary and names the whole vocabulary in one error. The rest were types that lied about the code: - `Target = ComputeTarget` claimed the persisted base, but resolution only ever yields `LocalTarget | RemoteTarget` — so every CLI `isinstance` narrow dead-ended in a type with no `path`. Aliasing the union fixed ~16 sites at once with no API change. - `Asset` had no `kind`, though every subclass declares it as the union discriminator; declared `AssetKind` on the base. - `AgentLoop.run(sink=…)` demanded the concrete `AsyncIteratorEventSink` while the body only ever awaits `sink(event)` — the contract is `EventSink`. - `_HasSideEffects` declared *mutable* attributes, which frozen pydantic conformers cannot satisfy; made the Protocol read-only. - Typed-away `object` in `task_codegen`, `runset`, `bind_board`, `knowledge_write`, `_workspace_root`, `PersistedAgentTask.active_mode`, and the mcp-tools resolver; added `require_agent_gateway` so four stages stop dereferencing an optional gateway. Suppressions kept: the guarded optional `molmcp` imports (not a dependency) and one deliberate uvicorn `handle_exit` monkeypatch. 14 stale `ty: ignore` directives and a dead assignment + duplicated asserts in `ops/tools.py` were deleted. 1890 tests pass; prek (incl. the new ty hook) passes.
Land/chat ops and workbench shell updates, remote/cached workspace FS parity, and molplot Metrics tab for molrec metrics/metrics.jsonl (same JSONL binding as molnex MolRecMetricsHook).
Align requires-python with molpy and CI resolution; biome-format the metrics.jsonl contribution patterns.
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.
Summary
masterinto MolCrafts (includes land/chat ops, plan orchestrator, remote/cached FS, MolRec metrics JSONL UI via molplot).MolRecMetricsHook/ molrec JSONL metrics binding.Test plan