Skip to content

Merge fork master: land chat ops, workspace FS, MolRec metrics UI - #12

Merged
Roy-Kid merged 320 commits into
MolCrafts:masterfrom
Roy-Kid:master
Aug 4, 2026
Merged

Roy-Kid merged 320 commits into
MolCrafts:masterfrom
Roy-Kid:master

Conversation

@Roy-Kid

@Roy-Kid Roy-Kid commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fast-forward fork master into MolCrafts (includes land/chat ops, plan orchestrator, remote/cached FS, MolRec metrics JSONL UI via molplot).
  • Aligns with molnex MolRecMetricsHook / molrec JSONL metrics binding.

Test plan

  • origin pre-push: ruff, ty, pytest, biome/tsc/vitest

Roy-Kid added 30 commits June 6, 2026 20:37
…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.
…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)
Roy-Kid added 29 commits July 22, 2026 17:03
…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.
@Roy-Kid
Roy-Kid merged commit e0ebdec into MolCrafts:master Aug 4, 2026
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