-
-
Notifications
You must be signed in to change notification settings - Fork 0
Improve concurrency handling, fix part of CI pipeline and more - #39
Open
Aksem wants to merge 45 commits into
Open
Improve concurrency handling, fix part of CI pipeline and more #39Aksem wants to merge 45 commits into
Aksem wants to merge 45 commits into
Conversation
...tartup fan-out (ADR-0063) Three related gaps in how the WM manages Extension Runner processes: 1. A start attempt that spawns the OS process but then fails or times out before the port handshake/RPC connection completes (debugger port wait, connect_to_server) left that process orphaned — nothing ever revisited a runner once it was marked FAILED. JsonRpcClient gains force_kill() (SIGKILL via os.killpg, since the process is started with start_new_session=True so it and anything it spawned, e.g. a package-manager invocation, can be reached as one group); every failed-start path in _start_extension_runner_process now calls it. client.pid is now captured as soon as the process is spawned (before the handshake), so force_kill has a target even when start() never returns successfully. 2. shutdown_service.on_shutdown stopped RUNNING/REPAIRING runners one at a time via the old *_sync helpers, so total shutdown time scaled with runner coun bounded to _STOP_TIMEOUT_SEC, but the total no longer multiplies. A runner still INITIALIZING at shutdown was never sent a graceful exit (that only happens once RUNNING) and has no way to learn the WM is going away, so those are force-killed directly instead. The now-unused stop_extension_runner_sync/shutdown_sync/exit_sync helpers are removed. Deliberately no force-kill fallback was added to stop_extension_runner's own timeout path: an ER that already received exit may still be tearing down its own spawned subprocesses, and killing it mid-cleanup risks orphaning exactly the children a slower-but-graceful exit would have reaped itself. 3. Nothing bounded how many ERs could be starting at once — process spawn + interpreter init + imports is CPU/memory-bursty, and every trigger (workspace init, a matrixed run, prepare-envs' runner-start step) funnels through the same _start_extension_runner_process chokepoint. It's now held under ws_context.er_startup_semaphore (sized via FINECODE_WM_MAX_CONCURRENT_ER_STARTS or the shared machine-budget formula, ADR-0063) from just before spawn until the RPC channel connects — not around whatever the triggering action does afterward in the ER's own process, a separate and far more variable cost this cap deliberately leaves unconstrained. Also switches the `@typing.override` decorator to a version-gated `typing`/`typing_extensions` import in the two lowest-level protocol files touched here (finecode_jsonrpc/client.py, wm_server/runner/_internal_client_types.py), since `typing.override` requires 3.12+ and these run under the workspace's >=3.11 floor; pulls in typing-extensions as an explicit dependency where needed.
...ncing (PRD-0004-AC6/AC7) An otlp_endpoint that's merely malformed previously surfaced as a confusing failure deep inside OTel SDK setup. telemetry._validate_endpoint now parses host+port up front (with or without a scheme) and raises a clear, actionable error at the three init_* call sites (logging, tracer, meter provider) before touching the SDK. A configured-but-unreachable collector — the common case when FineCode starts before `scripts/observability.sh up`, or before a Compose otel profile comes up alongside the container — no longer needs to be reachable at startup: exporters already buffer and retry, so this doesn't change behavior, but a stream of gRPC export-retry warnings for the process lifetime is now silenced (opentelemetry.exporter logger raised to ERROR) in favor of a single one-time reachability heads-up per endpoint, logged once and cached in _probed_endpoints so it can't repeat. Documents the resulting contract in configuration.md: malformed values fail fast, unreachable ones don't block startup, and points to the Observability guide for running a backend locally.
Ships the core release-automation loop: release_workspace_packages (fine_release) discovers every workspace package whose declared version is absent from its registry, computes a dependency-respecting publish order via fine_dep_graph, and sweeps them in order. A package whose transitive same-run dependency failed is BLOCKED rather than attempted (AC7, AC11); a successfully published package gets a best-effort git tag recording the publish (ADR-0060), via a new minimal fine_git preset (create_git_tag, push_git_refs) that fine_release depends on without pulling in release semantics. Getting there required fixing a registry-endpoint bug the old flow never exercised: Repository carried a single `url`, but PyPI splits reads and writes across two hosts (pypi.org vs upload.pypi.org). Reusing one URL for both silently misroutes — an index lookup against the upload host 404s, which is indistinguishable from "nothing published" and would make a package look unpublished when it isn't. Repository now carries index_url/upload_url explicitly (matching pip's/twine's own naming), and fine_python_package_info.registry_endpoints validates each is used for its own role before any request goes out. is_artifact_published_to_registry is replaced by list_published_artifacts, which reports the registry's actual filenames for a version instead of a bool per caller-supplied dist path. This lets a caller (the release sweep, a dry-run preview) ask "what's published" before it has built anything locally, and derive what still needs uploading by filename membership rather than needing to already know the dist paths up front. publish_artifact_to_registry and publish_artifact now report registry failures in their results (`error`/`failed_registries`) instead of raising, and publish_artifact_handler dispatches to registries with asyncio.gather instead of a TaskGroup — one registry's failure no longer cancels uploads already in flight to the others (ADR-0062). publish_and_verify_artifact's result gains a matching `publish_errors` map alongside its existing `verification_errors`. get_src_artifact_version's src_artifact_def_path is now optional, defaulting to the current project, so release handlers fanning the same payload out to every candidate project don't each need to resolve their own path first. Adds finecode_extension_runner/testing: an in-memory handler test harness (run_handler/handler_test_session) that boots a real ER in-process against stub services (IFileEditor, ICommandRunner, WAL writer, progress/partial-result senders) instead of stubbing each handler's dependencies by hand per test. Used throughout the new fine_git and fine_python_package_info tests, and covers its own progress-collection behavior. CI workflow env vars are updated to the new index_url/upload_url shape for both TestPyPI and PyPI publish steps; also drops a stale "test with all supported python versions" TODO that predates the interpreter-matrix work already handling that.
... (ADR-0067) Two independent gaps closed together: Converting an env to an interpreter matrix (ADR-0047) silently orphans its old venv: config only sees the expanded children afterward, so neither prepare-envs nor --recreate ever revisits the base name's .venvs/ directory again. Same fate for a renamed env or a preset that stops contributing one — the venv just sits there, easily hundreds of MB per project. list_envs reports declared/on-disk/orphaned status per env from the filesystem alone (cheap across a whole workspace, no interpreter execution); remove_envs deletes them, defaulting to the orphaned set. Two guards apply to whatever was asked for before existence is even checked, so a rejection can't depend on disk state: a declared env needs force (an ER may be running in it), and the current env is never removable regardless. IFileManager.remove_dir gains tolerant=True for this: read-only contents, dangling symlinks, and already-absent paths are all treated as "make it gone" rather than errors, since a half-created or permission-damaged venv is exactly what removal should clear. Separately, `run` fan-out across projects had no concurrency bound, unlike prepare-envs (ADR-0055) — a workspace-wide run put every project's ER to work at once, each free to spawn subprocesses up to its own ICommandRunner cap, so the two layers compose multiplicatively exactly as ADR-0055 warned about. max_project_fanout couldn't be reused directly to fix this: it's a runaway-recursion guard that refuses, meant for nested orchestration (ADR-0016), and applying it at depth 0 would make every workspace-wide action unusable past an arbitrary workspace size someone chose on purpose. Depth 0 now gets a per-call semaphore instead (FINECODE_WM_RUN_MAX_CONCURRENT_PROJECTS, same sqrt-split default formula as ADR-0055) that throttles without ever refusing; the recursion guard still applies, and only applies, once orchestration_depth > 0. The semaphore is built fresh per fan-out call rather than shared, since a shared one would let an outer fan-out hold every permit while waiting on an inner one that can never acquire any.
fine_release's release_workspace_packages action previously owned both the cross-package concerns (candidate discovery, dependency order, blocking, tagging, pushing) and the single-package concerns (build, publish, verify) in one handler chain, so a package could not customize its own release steps without touching the workspace-wide action. ReleasePackageAction is now a package's own chain (build/publish/record_tag), and release_workspace_packages delegates to it once per candidate via run_action_in_projects, keeping only what is inherently cross-package: ordering, dependent-blocking, and publishing every git ref the run produced in a single push (fine_git's push_git_refs) rather than once per package. Extends ADR-0054's canonical-source resolution (previously actions only) to handlers: resolve_action_meta now reports canonicalSource per handler alongside fileLoc, since config almost always declares a handler via its package's __init__.py re-export rather than the module the class is actually defined in. ActionHandler gains a canonical_source field (None until the hosting ER resolves it, or permanently if the class can't be imported), and parse_workspace_actions carries it through from the wire payload. finecode_extension_runner/testing's handler_test_session needed a way to observe progress calls directly instead of only through the token/global-function forwarding meant for production RPC, so run_action now accepts an explicit progress_sender that bypasses that path. Also fixes a bug the new fine_release tests exposed: handler config structuring only caught cattrs.ClassValidationError, so a malformed entry in a list- or dict-typed config field raised the sibling IterableValidationError uncaught instead of surfacing as a readable ActionFailedException. create_envs now reports whether it actually built a virtualenv or found a valid one already there (CreateEnvsRunResult.created), and create/install env progress messages use a new env_label() helper (<project>/<env>) instead of the bare env name, which was ambiguous whenever one dispatch call spans multiple same-named envs across projects (e.g. prepare-envs' dev_workspace bootstrap step). remove_dir's --recreate path now passes tolerant=True, since a half-created or permission-damaged venv being removed before recreation is exactly the case tolerant removal exists for. Clarifies IProjectInfoProvider.get_project_raw_config's docstring: the config it serves is resolved (presets merged, interpreter matrices already expanded per ADR-0047), not the file's own raw content, which was easy to misread as unprocessed. Adds a regression test pinning that the expansion happens before the config is stored for serving, not just that resolve_interpreter_matrices itself produces the right shape, plus a comment in prepare_envs_service.py on why step 3 keeps installing into dev_workspace even though step 5 excludes it.
...070) A service declaration's identity is its `interface` dotted path, but that can't be written readably in an environment variable (dots collapse to underscores and collide with the `__` nesting separator already used for handler config). FINECODE_SERVICE_CONFIG_<NAME>__<PARAM_PATH> now addresses a service by a short alias instead: derived from the interface's class name (leading `I` + uppercase stripped, snake-cased), never declared, so a declaration and an environment can never disagree about it. ServiceConfigResolver (finecode_extension_runner/service_config.py) merges raw_config (activator) < declared config < env override by deep merge, and reports ambiguous aliases (two interfaces deriving the same name) or unmatched override names once all bindings have registered. Resolution happens in the Extension Runner, not the WM, because only the ER can see activator-registered bindings alongside declared ones. IServiceRegistry.register_impl drops its `singleton` flag: every binding is already cached for the runner's life, and the flag never controlled that -- it only masked the case where a `[[tool.finecode.service]]` entry had no way to pass it, silently handing a concrete-injecting handler a second instance. register_impl now always alias-binds the concrete type to the interface's instance. Config-only `[[tool.finecode.service]]` entries (no `source`) are now legal: they carry config for a binding an activator already owns instead of restating and pinning its implementation. IRepositoryCredentialsProvider loses its push-seed methods (set_credentials/add_repository) since they only existed to fake provisioning that belongs to the concrete impl (ADR-0068); ConfigRepositoryCredentialsProvider is provisioned entirely through service config now, and the old PublishAndVerifyArtifactInitRepositoryProviderHandler -- an init action that only carried config -- is deleted per S-203. The one remaining init_repository_provider_handler is documented as dynamic-runtime-seeding only, injecting the concrete provider type rather than the interface. Adds docs/guides/designing-services.md: the S-1xx (contract) / S-2xx (provisioning) / S-3xx (registration/lifecycle) rule set this change implements, distilled from ADR-0038/0056/0068/0070. docs/configuration.md documents the env-var format and service-name derivation; docs/reference/services.md, docs/wm-er-protocol.md and docs/guides/designing-actions-rules.md are updated to match. HandlerInfo gains a `canonical_source` field (module a handler class is actually defined in, vs. the config-facing `source` alias), following the same canonical-source resolution already used for actions. Also: - Registers IKnowledgeStore (finecode_extension_api) with an ER-side KnowledgeStoreImpl, and wires runner_manager/wm_server to route two knowledge methods to it. - Adds typing-extensions as an explicit dependency for fine_python_pyrefly, fine_python_ruff, fine_toml_tombi and finecode_extension_runner, since their `override` compat shim for Python <3.12 depends on it directly rather than transitively. - Adds the fine_python_envs preset (derives an env's interpreter axis from requires-python via fine_envs' sync_toolchains/check_toolchains contracts) and a fine_git README. - RuffFormatFileHandler now returns an empty `code` when the file did not change, instead of the unchanged content.
check_toolchains (ADR-0053) only surfaced axis drift through its own run result; nothing routed it into audit_code, the umbrella CI/editor diagnostics run through. CheckToolchainsAuditCodeBridgeHandler adds that path: one ERROR diagnostic per stale env, anchored at the project's definition file (position 0,0, matching how import-linter anchors whole-project violations) since the drift belongs to the file, not a line in it. CheckToolchainsRunResult gains project_def_path so the bridge has something to key diagnostics on; CheckToolchainsHandler fills it from IProjectInfoProvider since it runs in the project's own ER and is the only place that knows the path without a second lookup. The dependency on fine_audit_code/fine_inspect_code stays optional (fine_envs[audit]) since fine_envs is the mandatory base preset every project loads and only this one module needs the audit umbrella. While building the bridge's tests, found the same bug already live in check_toolchains_precommit_bridge_handler: it merged every project's CheckToolchainsRunResult with update(), but that merge is scoped within one project (R-302) and EnvToolchainAxis is keyed by env name, unique inside a project but not across them. Two projects sharing the `testing` env name (every matrix project does) collapsed into one entry showing only the first project's versions, silently hiding the second project's drift. Cross-project aggregation now happens in the bridge itself, keeping one action_results entry per drifted project under its own label. Also: - fine_python_test's run_tests moves from the single-interpreter `dev` env to a new `testing` matrix env whose interpreter axis is derived from requires-python via fine_python_envs' sync_python_interpreters (ADR-0053/0057), instead of running tests against only `dev`'s interpreter. - Adds regression tests for _collect_services_in_config covering config-only entries (source/env omitted) and same-process alias collisions, which ADR-0070 left unmatched by test coverage.
read_file previously took a block=True flag shared with the same _blocked_files map that a modifying caller used, so a read nested anywhere inside an in-flight modification (e.g. formatting pyproject.toml triggers a language-detection lookup that reads pyproject.toml through a second session) waited on a release only its own caller could perform, deadlocking the ER. Reads are now always non-blocking and modification is its own operation, modify_file, which claims exclusion on the file path (not the session) for as long as the claim is held. Session identity is kept only for teardown: ending a session releases whatever claims it still holds. save_file gains an optional if_version to make a write conditional on the version it was based on: if the file changed underneath the caller, FileVersionConflict is raised and nothing is written, rather than silently discarding whatever produced the newer version. SaveFormatFileHandler uses this so a formatting pipeline whose input went stale mid-run refuses the write and logs, instead of last-writer- wins clobbering a concurrent edit. InMemoryFileEditor's test double derives versions from content hash instead of a constant "1", so if_version checks in tests mean something, and gains matching modify_file locking.
LSP leaves parallel request execution to the server's discretion, so a client that pipelines without bound relies on unspecified behaviour. tombi guards its document, reference and schema stores with locks it reports contention on, and with many documents in flight on one session individual requests stall past any usable timeout while the process sits idle rather than busy. Add an opt-in max_concurrent_requests to LspService, defaulting to unbounded so pyrefly and ruff-lsp are unaffected, and set it to 1 for tombi. The limit bounds whole interactions rather than single messages: the document synchronization around a request mutates the same server state the request reads, so letting another document's notifications interleave defeats the point. Forwarded file events take the limit too, before the per-uri lock — the reverse order deadlocks against format_file, which already holds them that way round. Measured over 194 workspace TOML files against one tombi process at a 30s timeout: unbounded fails 88 of 194 requests; every bounded value from 1 to 8 completes cleanly in 41-51s. Alongside it: - Drop didOpen for a change to a document the server does not hold open. didClose is only sent for documents an editor session opened, so nothing would ever close it again and every file written through the editor accumulated in the server for the lifetime of the session. - Send didClose even when a format request fails, for the same reason. - Answer workspace/inlayHint/refresh, which servers send regardless of the declared client capability — for some, once per document sync. - Run tombi with --offline. No action here consumes live dependency data, so its per-dependency network lookups are latency for results nothing reads. Claude-Session: https://claude.ai/code/session_019ZMjy2zw1jTjkfXCWNfEe6
Whitespace/line-wrap/quote-style normalization from running the project formatter on every file with no pending unrelated changes. Files that already had unstaged edits before formatting are excluded from this commit so their formatting and content changes aren't mixed together.
format and check_formatting disagreed persistently: format reported a set of files as reformatted, check_formatting run straight afterwards reported the same files as needing formatting, and neither run changed what was on disk. Since check_formatting is format with save=False, that can only mean the pipeline was not deterministic across runs. IsortFormatFileHandler built isort.settings.Config without anchoring it to the project, so isort inferred which packages are first-party from the cwd of the process-executor worker -- a value the handler does not control. With the project directory current, a project's own package is first-party and its imports get their own trailing section; with any other directory current, nothing is first-party and the same imports collapse into the third-party block. Consecutive runs therefore produced two different layouts for the same file and each run reported the other one's output as needing formatting. settings_path alone does not fix this: isort derives src_paths, which is what first-party placement actually consults, from `directory`, which independently defaults to os.getcwd(). Both are now set to the project directory, so the same file sorts identically regardless of where the runner was started. The cwd was unstable because JsonRpcClient.start established it by mutating the workspace manager's own cwd around the spawn and restoring it afterwards. Runners start up to er_startup_semaphore at a time (7 on this machine) and the spawn itself is dispatched to the io thread, so an ER could be forked while another client held the cwd -- inheriting that project's directory, or the restored original. The VIRTUAL_ENV pop/restore had the same shape. Both are now passed to create_subprocess_shell explicitly, which is what StdioTransport already did. Verified with a whole-workspace format followed immediately by check_formatting: 43 files reformatted, then 0 files needing formatting. The loguru import move in client.py is that fix applied to this file -- it was one of the files whose import layout had been flipping.
Second pass of formatter normalization (import ordering, line wrapping) over files not covered by the previous formatting commit. Files with pre-existing unstaged edits are still excluded so their formatting and content changes aren't mixed together.
The wm-layered import-linter contract was recently repaired to point at real modules (it previously referenced ones that didn't exist, so it never ran). Running it for the first time surfaced 7 direction violations against the documented layer stack (docs/guides/wm-server- internals.md). All are implementation debt, not a contract bug — fixed in four groups: - domain.py imported ErLoggingConfig from config.config_models (a stranded pure-data type). Moved it into domain.py; config_models.py and runner_client.py now import it from there. - context.py importing runner.runner_client for ExtensionRunnerInfo can't be fixed the same way — that type wraps a live JsonRpcClient, so it can't move down to domain. Documented as an intentional exception in the wm-layered contract, mirroring the existing wm-domain-purity carve-out for context.py. - Five call sites (_api_handlers/_workspace.py, services/ runner_start_service.py, runner/runner_manager.py x3) reached back into wm_server.py/services via deferred imports inside function bodies to broadcast client notifications, forward ER logs, and dispatch ER-initiated action runs. Replaced with two new bridge modules (runner/wm_bridge.py, runner/run_dispatch_bridge.py), mirroring the existing runner/knowledge_bridge.py pattern: a Protocol + install/handlers slot living in the lower layer, filled by the higher layer at import time. The ER-dispatch handler bodies moved out of runner_manager.py into services/run_service/ er_dispatch.py. - config/read_configs.py called runner.runner_client directly to resolve py-preset install paths via an already-running dev_workspace runner. Split read_project_config into a pure read/merge pair (read_project_config_sources / finish_project_config) and moved the RPC-dependent preset resolution into a new runner/preset_resolution.py, which runner_manager.py (the only caller that ever needed it) now calls directly. No opaque object/Any typing needed anywhere in this path. Verified with lint-imports (5/5 contracts kept, was 1 broken) and the unit suite (87 failed/239 passed before and after — pre-existing pytest-asyncio gap in this venv, confirmed via git stash comparison).
Ruff, black and isort each guessed a language level on their own -- ruff defaulted to py38, black had none configured at all -- so a project's declared requires-python, its lint target, and its format target could all name different Python versions with nothing to notice the disagreement. Add get_src_artifact_toolchain_range as the one place that range is read (from requires-python for Python, via a new handler in fine_python_package_info), shared with the interpreter axis derivation in sync_python_interpreters so both read requires-python the same way. Black and ruff now derive their target version from it when not explicitly configured; an explicit value still wins. Ruff's target version and format/lint settings are pushed to its shared LSP server only during initialize, and deriving the version now requires an async action call, so a handler can no longer just build its settings dict in the constructor and call update_settings. RuffLspService instead collects a settings provider from each handler and resolves them all -- deep-merged so a linter's and a formatter's contributions to the same table don't clobber each other -- right before the server actually starts, regardless of which handler gets there first. Also: - Add extend_ignore to the ruff lint handler, mirroring extend_select, so a preset can turn rules off without discarding a project's own [tool.ruff.lint] ignore list. - Expand fine_python_lint's default rule selection (PLE, ASYNC, UP, DTZ, C4, PIE, PERF, FURB, SIM, RUF, FLY, INT, ICN, TID, PGH, LOG) now that a wrong target-version can no longer silently skew what these rules report. - Bump ruff to 0.16.* and pyrefly to 1.2.*. - Fix wm_server code using PEP 695 syntax (type X = Y, class Foo[T]) that requires 3.12, which the presets' own requires-python (>=3.11) does not guarantee -- caught once ruff started targeting the range it actually declares instead of its py38 default.
Editing FineCode-managed code required restarting the whole workspace
server by hand to see the effect take hold, and there was no way for a
client to survive that restart without reconnecting manually.
Adds a four-rung recovery ladder, cheapest first, each covering what the
one before it doesn't (ADR-0073, ADR-0075):
- reload_action: re-imports the packages owning an action and its
handlers, in every environment of every target project. Starts no
process.
- restart_runner: replaces a project's extension runner processes, for
edits to shared code no single action owns.
- reload_config: re-reads pyproject.toml/finecode.toml/presets and
replaces the runners they configure -- the rung to reach for when
unsure which kind of edit was made. ADR-0078 makes the target
explicit: one project, or the whole workspace via allProjects, never
defaulted into.
- restart_wm: replaces the workspace server process itself, for edits
to FineCode's own code.
Recovery is a WM capability the client only projects or refuses
(ADR-0077): each command needs --shared-server, since recovering a
dedicated server the command itself started would report success while
changing nothing that outlives the command. `server/reset` is gone
(ADR-0076) -- it logged one line and returned {}, indistinguishable
from a reset that worked; `workspace/reloadConfig` with
allProjects/rescan replaces it.
A reload or restart is refused while a run is in flight in its target
project, since replacing runners kills whatever they're executing with
no way to tell the caller whether side effects happened;
killInFlightRuns overrides it by name (ADR-0079). Runs the WM started on
its own behalf are exempt -- re-derivable and awaited by nobody, they're
cancelled instead of blocking (ADR-0080). Both are tracked in the new
in_flight_runs registry.
Restarting a shared WM disconnects every other client, so ApiClient
grows reconnect-with-backoff and session re-attachment (ADR-0074): a
dropped connection retries with jittered exponential backoff inside the
WM's 30s disconnect timeout, then replays whatever session state the
surface needs -- add_dir, MCP tool-list invalidation, the LSP's
server_initialized gate -- through a single on_reattach hook shared by
first-connect and reconnect.
MCP exposes the four rungs as tools rather than actions, since an
action executing inside the runner it would replace can't complete.
Each tool's description states what it covers and what to reach for
next: staleness is never auto-detected (ADR-0075), so the description
is the only signal a caller has for which rung applies.
Also:
- schema_utils: describe nested dataclasses (e.g. Range, Position) as
JSON object schemas instead of {}, needed to expose the new tool
parameters correctly.
- next_step.for_runner_failure derives the `prepare-envs` command a
failed recovery should be retried with from the runner's own failure
state (PRD-0008 R11), instead of surfacing a raw import error.
- New docs/guides/wm-server-internals.md describes the WM layer stack
and the recovery flow end to end.
Code actions were offered but had no way to be executed: the LSP
codeAction/resolve request had nothing behind it, and there was no
action to turn a get_lint_fixes/get_code_actions result into a write.
This adds the missing writing half of the code action pipeline.
- resolve_code_action: recovers a code action's edits from the
(provider, action_id) pair round-tripped through the LSP `data`
field, since the client only ever holds an opaque handle, not the
edits themselves. Concurrent handlers each claim only the actions
their own provider minted (lint_fixes_resolve_bridge_handler checks
`provider == PROVIDER_ID`) and pass through untouched otherwise, so
one resolve request fans out safely to every registered provider.
- apply_code_actions: applies a batch of selections (each pinned to
the file version it was computed against) via a new text-edit
algebra (_text_edit_algebra) that applies same-file edits
back-to-front so ranges stay valid, detects overlaps, and reports a
per-selection ApplyOutcome (applied / deferred / version_conflict /
invalid_range / unresolved / unsupported_operation / write_failed /
partially_applied) rather than failing the whole batch on one bad
selection. Only TextEditOperation is executed; Create/Rename/Delete
are refused wholesale until IFileEditor grows support for them.
- apply_lint_fixes / apply_lint_fixes_files: the fix-oriented entry
points, layered on top of apply_code_actions so lint fixes and
editor-offered code actions converge through the same apply and
version-conflict handling instead of two parallel write paths.
The LSP code_actions endpoint now carries {provider, action_id,
file_path} in each CodeAction's `data` instead of a bare action_id,
and adds a cattrs structure hook to disambiguate the
CodeActionOperation union on the way back in (cattrs' default
disambiguator can't tell CreateFileOperation and DeleteFileOperation
apart from a shared file_path with only defaulted fields of their
own).
Also catches docs/reference/lsp-protocol.md up to the reloadConfig/
restartWm rename that shipped in fbdf049 but never touched this file.
fbdf049 (PRD-0008) described ADR-0080 in its commit message -- runs the WM starts on its own behalf, re-derivable and awaited by nobody, should be cancelled by a config reload instead of blocking it -- but never wired the plumbing: InFlightRun had no cancellable field and config_reload_service always refused on any run regardless of who started it. This finishes that: - InFlightRun.cancellable (domain.py) and in_flight_runs.track/ blocking_runs now drop cancellable runs before checking whether a project is idle, only falling back to naming a blocking run when a user-started one is present alongside them. - run_action, run_actions_in_running_project/_in_projects and WorkspaceExecutor.run_actions thread a cancellable flag through to the track() call, so a caller can opt a whole dispatch in; it is a property of the dispatch, not the action. - shutdown_service flushes knowledge_service's pending fact writes before stopping runners, since a graceful shutdown has no reason to accept the throttled writer's usual crash-window loss. - New integration tests exercise both halves: a lone cancellable run lets reloadConfig proceed, and a user run sharing the project with one still refuses and names only the run that matters (on-demand-extraction-plan D-8 AC7). Also catches docs/wm-protocol.md and docs/cli.md up to the PRD-0008 rungs that fbdf049 shipped without documenting: reload_action and restarts_runner's MCP exposure, allProjects/rescan addressing, server/getInfo's pid and clients fields, and a recovery-ladder quick-reference table in cli.md. tests/integration/conftest.py exposes InProcClient.ws_context so tests can seed in-flight runs and read workspace state back directly.
...project boundaries Two independent fixes that landed together: Ruff's CLI (`ruff check`) and LSP paths reported the same violation at different positions: dropping the 1-based-to-0-based column shift put every CLI-path range one character to the right of what the LSP path and an editor agree on, so highlights covered the wrong span and `payload.range` filtering missed matches across the two paths. `_position_from_ruff` now does both the row and column shift in one place. Also `_run_cli_fixes` was calling `ICommandRunner.run` with an argv list where it takes a single shell string -- the CLI path only ever worked against a stub; `shlex.join` fixes the real call. Ruff reports fixability inline with each violation (`fix` non-null) but the LSP protocol has no field for it, so `Diagnostic.fixable` is threaded through from ruff's own `data` on the LSP path and from the CLI's `fix` field on the other, with `None` kept distinct from `False` so "no fix" isn't confused with "tool doesn't say." The CLI text report marks fixable diagnostics with `[fixable]`. LSP code actions carry no applicability, and ruff offers unsafe fixes as quickfixes regardless of configuration, so a fix arriving over LSP could not be told apart from a safe one. `_label_applicability` now cross-references each LSP fix against a same-content `ruff check` call (by code + position) to recover its real applicability, and flags noqa-suppression fixes as `DISPLAY_ONLY` since they suppress rather than fix. Separately, `list_src_artifact_files_by_lang`'s Python and TOML handlers walked their project directory with a plain `rglob`, which does not stop at a nested project's root -- a workspace operation scoped to the outer project silently pulled in the inner project's files too, and an unscoped one processed them twice. New `workspace_utils.nested_project_dirs` / `walk_project_files` prune the walk at any nested project's root (using workspace project info that was already available) and skip hidden directories/files (venvs, `.git`, dotfile configs) by default.
The knowledge model engine (entity/fact model, query IR, interpreter, memoization DAG) previously lived alongside FineCode's own schema and rules. R20 requires the core to carry no language- or tool-specific logic, but that was only a lint-enforced convention -- nothing stopped rule code from ending up in the same distribution the WM imports to run the DAG. Splitting it into its own package makes R20 a packaging fact instead: finecode_knowledge has zero dependencies and no schema of its own: schema, providers and rules are declared by a separate distribution (fine_knowledge) and handed in as a SchemaRegistry via set_default_registry. The WM can import the engine without any rule code entering its process, because the package holding rules is never installed in its environment.
resource_uri.py already flagged the bug in a NOTE: a relative
file:// URI reaching an ER resolves against that ER's own project
directory, not the user's terminal directory, so the same payload
silently names a different file per project a run fans out to. The
CLI is the last process that still knows the user's directory, so
that is where expansion has to happen.
- resource_uri.py gains absolutize_resource_uri, resource_location_to_uri
and is_relative_file_uri, splitting out _parse_file_uri_path so the
netloc/path reassembly ("file://relative/path" splits in two under
urlparse) is shared instead of duplicated.
- New cli_app/payload_uris.py walks an action payload against the
fields its own payload schema (actions/getPayloadSchemas) marks
format: "uri", and rewrites only those to absolute file:// URIs —
a plain path or a relative URI is accepted, but only on a field a
schema vouches for as a resource.
- run_cmd.run_actions calls it before dispatch and now fails the run
if a relative file:// URI survives, naming the field and whether a
schema was even available, rather than sending something an ER
would resolve wrong.
- mcp_server: stop advertising a "project" tool argument for
workspace-scoped actions, since the WM always resolves those to
the workspace root itself and rejects an explicit one; and replace
the top-level try/except KeyboardInterrupt with
contextlib.suppress.
Project paths on finecode/runActionInWorkspace come from a handler's payload, typically a caller-supplied URI some ER turned into a path, so the WM has never vetted them. An unknown path used to reach a bare dict lookup deep in the fan-out (proxy_utils.run_actions_in_projects or the actions_by_project dict in er_dispatch), and the caller was handed a KeyError whose entire message was the repr of a PosixPath: it named the path but never said what it had failed to match. - er_dispatch and proxy_utils now check requested paths against ws_context.ws_projects up front and raise errors.ProjectError naming the path, the requesting runner, and the closest known projects by shared path segments (capped at 3, "and N more") so a hundred-project workspace doesn't bury the answer. - partial_results_service rejects an explicit project path passed alongside a workspace-scoped action, mirroring the non-streaming actions/run guard in _helpers.py, instead of silently narrowing a workspace-wide action to one runner. - run_dispatch_bridge documents the new ProjectError case.
AsyncProcess only ever buffered a child's stdout/stderr and handed it over on wait_for_end(), so any handler wanting live output had no way to get it -- get_output()/get_error_output() carried "TODO: live output?" comments marking exactly this gap. stdout_lines()/ stderr_lines() close it without disturbing the ~20 existing handlers that still read output the old way. - ICommandRunner gains stdout_lines()/stderr_lines() on IAsyncProcess only -- the sync flavour has no way to interleave reads with anything else. - command_runner.py: _LineStream drains a stream unconditionally from spawn (an unread pipe fills its OS buffer and blocks the child forever), accumulates until a subscriber shows up, then switches to a queue and stops accumulating. A stream that hits an unrecoverable error (a line past the reader's size limit, or non-UTF-8 output) keeps draining and discarding rather than stopping, so one bad stream cannot hang the whole process. The per-line limit is raised well past asyncio's 64 KiB default, since an overrun there discards the buffered bytes before raising rather than just reporting it. - CommandRunner now keeps strong references to its release-when-done tasks. The event loop only holds weak ones, so a task with nothing referencing it could be collected before the child exits -- permanently losing a semaphore slot and eventually wedging the runner at its concurrency cap. - Test doubles across the ruff, uv and git presets/extensions gained stdout_lines()/stderr_lines() to keep satisfying the IAsyncProcess protocol.
A shared server started by run --shared-server still exits 30s after the last client disconnects — the flag amortizes nothing if no process holds the server up between calls, so config loading and runner startup were still paid on every command in the devcontainer. - start-wm-server gains --keep-alive (disables both the no-client and disconnect auto-stop timers; server/shutdown still stops it) and --detach (start it in the background, no-op if one is already listening). Keep-alive is passed explicitly and never read from the environment, so it can't leak onto the dedicated per-command servers, which must keep auto-stopping. - wm_lifecycle.ensure_running forwards keep_alive/disconnect_timeout/ wal_enabled to the spawned server and starts it in its own session, so it survives the signals of whichever short-lived client or script happened to start it. - .devcontainer/start-wm-server.sh runs start-wm-server --detach --keep-alive from postStartCommand, gated on FINECODE_WM_AUTOSTART (on by default in .env.example) and skipped when the dev_workspace venv doesn't exist yet.
A run_agent_task action lets FineCode delegate a task to an AI coding agent without a caller committing to a specific backend: the fine_agent preset registers the action slot alone, and a backend extension supplies the one handler. fine_system_claude_code is renamed to fine_agent_claude_code to sit alongside the new fine_agent_pi extension under that naming, and ICommandRunner gains process-group teardown so a handler can actually stop a hung agent run -- signalling the shell alone leaves the tools it spawned running. - presets/fine_agent: RunAgentTaskAction, RunAgentTaskRunPayload/Result, and AgentRunUsage carrying only what a backend actually reported, never a derived total or a converted currency. - extensions/fine_agent_claude_code (renamed from fine_system_claude_code): ClaudeCodeAgentHandler drives the Claude Code CLI over its stream-json protocol. - extensions/fine_agent_pi (new): PiAgentHandler drives pi.dev over its own RPC-like wire format (pi_rpc.py), which is not JSON-RPC and has several surprises documented there (agent_settled vs agent_end, doubled usage figures across message_end/turn_end). - finecode_extension_runner/impls/command_runner.py: run(..., new_process_group=True) starts the child in its own session; is_alive()/terminate()/kill() signal the whole group so a handler's escalation ladder (SIGTERM, wait, SIGKILL) reaches an agent's own tool-call subprocesses, not just the wrapping shell. - docs/reference/services.md documents the new stop-a-process pattern.
fine_git could only push tags, so nothing in the workspace could read a project's git state or discard local changes through an action -- a caller had to shell out itself. get_git_status/get_git_diff report both porcelain columns and both diff projections (patch and parsed lines) in one answer, since callers want different slices of the same state rather than each parsing it themselves; restore_git_files requires an explicit path list, since it destroys uncommitted work and "restore everything" has no place being one flag away. - get_git_status_action.py / git_get_git_status_handler.py: reports repo_root=None for a project outside a git repo as a result, not an error. - get_git_diff_action.py / git_get_git_diff_handler.py: source picks worktree/staged/worktree_and_head; context_lines=0 yields hunks with only changed lines. - restore_git_files_action.py / git_restore_git_files_handler.py: a path outside the project directory is refused and reported in skipped rather than restored; deleting an untracked path needs remove_untracked=True since it is unrecoverable. - tests/conftest.py: FakeCommandResult/FakeCommandRunner moved out of test_create_git_tag_handler.py so the three new handlers' tests can share them.
The shared cache file (cache/finecode/results/<action>.json) is read-modify-written on every run, so a reader can't tell entries this run produced from ones left by earlier runs against other projects. An AI caller or script scripting a single `run` invocation needs a record of exactly what that run did and nothing else. - cli.py: _write_run_results_file writes a JSON document scoped to the single run (request, per-action per-project return codes and results) via a temp-file-plus-rename so a concurrent reader never sees a half-written document. Written on every exit path, including failures before any action executed, so a failed run can't leave a stale previous file in place. Implies save_results so the structured data exists even under --no-save-results, which only suppresses the shared cache. - RunActionsResult gains scope_by_action_source and project_paths_requested so the results file can record each action's declared scope -- a workspace-scoped action files its result under the project that hosted it, not the projects it was pointed at, and nothing else in the result says which case applies. - run_cmd.py: _build_streaming_result threads scope and resolved project paths through from the WM's batch result.
A handler sometimes needs a decision only a person can make mid-run, and IUserMessenger can't provide it: those three methods broadcast a string to everyone connected and cannot fail, they don't address one person or wait for an answer. This adds a proper ask/answer channel end to end - ER handler, WM, and both client surfaces - addressed to the specific connection that started the run rather than broadcast, since two clients can be running the same project at once. - finecode_extension_api/interfaces/iuserprompt.py: IUserPrompt. ask_choice() returns an ElicitationResult with three outcomes (answered/declined/unavailable) rather than two, so a handler can tell "a person refused" from "no person was there" and react differently - abort vs. take the pipeline's default. - finecode_extension_runner: run_context.py is a contextvar naming the run each task belongs to, set once at dispatch in er_server.py and inherited by everything the run starts; impls/user_prompt.py calls the WM's new finecode/elicit back-channel through it and turns every failure mode (transport error, WM error response, no WM at all) into UNAVAILABLE, except run cancellation, which propagates instead of looking like an unattended run. - wm_client.py: ApiClient can now receive server-to-client *requests*, not just notifications - the first traffic on this connection that isn't ER-work-driven. Fixes a latent bug in the read loop, which tested "id present" to mean "response" and would have silently mis-routed any inbound request as a response to an unrelated id. - wm_server.py: negotiates the capability at client/initialize (a pipeline or non-interactive client declares nothing, so its runs get "unavailable" immediately instead of waiting out a deadline), addresses a request to one connection with its own deadline, and resolves every pending question at once when that connection drops. - elicitation_bridge.py: the runner-layer slot finecode/elicit comes in through, plus the run-id -> connection registry it's resolved against. Bound in in_flight_runs.track, since a run's addressability and its in-flight lifetime are the same thing; nested dispatch (er_dispatch.py) inherits the calling run's connection so a question asked several hops into a fan-out still reaches the right person. - walRunId is renamed to runId throughout the ER protocol: it was already the WM's per-dispatch identifier and ADR-0079 already keys in-flight runs by it, so reusing it to address elicitation avoids minting a second id with the same lifetime. - run_cmd.py: the CLI's side of client/elicit prints to stderr (so piped stdout is unaffected), serializes concurrent ERs onto one lock so two questions can't race the same stdin, and runs the blocking prompt on a daemon thread so a torn-down run doesn't wait on an unanswered prompt to exit. Declares the capability only when stdin is a tty. - mcp_server/server.py: forwards to the AI client via MCP's elicitation/create, mapping its three outcomes (accept/decline/ cancel) onto ours, and bumps the negotiated protocol version to 2025年06月18日 where that request exists. - tests/unit/test_run_results_file.py: test coverage for the --results-file feature that was missing from its own commit.
The ContextVar-based originating_client() let the connection an ER should ask questions of leak into ambient state, which is invisible at call sites and silently defaults to "nobody" if a hop forgets to set it — exactly the failure the two progressToken handlers in _streaming.py had (a connected client whose run could never be asked). Passing RunDispatchOrigin explicitly, with no default at any hop, turns a dropped connection into a signature the type checker can catch instead of a question nobody receives (ADR-0082). - elicitation_bridge.py: RunDispatchOrigin(connection) replaces the originating_client() context manager; bind_run() now takes it as a required argument instead of reading the ContextVar. - er_dispatch.py, in_flight_runs.py, proxy_utils.py, matrix_streaming.py, project_executor.py, execution_scopes.py, workspace_executor.py, partial_results_service.py: thread `origin` explicitly from each request handler down through every dispatch seam to the choke point (in_flight_runs.track) that binds it. - _actions.py, _streaming.py, cli_app/utils.py, prepare_envs_service.py: each call site states its origin explicitly, including the plain request/response and internal dispatch paths that have none. - developing-finecode.md: documents the "ambient state" rule this refactor follows (when a ContextVar is justified vs. when a parameter is required with no default), plus the boolean-flags, comments, knowledge-package-split and local-observability-stack sections accumulated in this file since the last commit touching it.
A lint fix or refactor that needs to create, rename, or delete a file had no way to express that: CodeAction only ever carried a flat map of text edits. This extends the operation model end to end so a handler can emit an ordered list of text-edit and resource operations, and the LSP endpoint negotiates what the connected editor can actually apply before offering or resolving an action built from them. - ifileeditor.py / file_editor.py: IFileEditorSession gains create_file, delete_file, claim_file and file_exists so a handler can build these operations without reaching around the interface. claim_file covers the not-yet-existing case that modify_file's claim can't; claims are now reentrant within one task rather than deadlocking on renesting. - code_action_types.py / apply_code_actions_action.py: CodeAction.edits (an unordered dict) becomes CodeAction.operations, an ordered list of CreateFileOperation/RenameFileOperation/DeleteFileOperation/ TextEditOperation — the same shape both resolve and apply speak. - code_actions.py (LSP endpoint): negotiates documentChanges support and declared resourceOperations from the client's initialize capabilities (lsp_server.py), and converts an operation list into a WorkspaceEdit only when the client can express it — falling back to the `changes` map when documentChanges isn't supported, and dropping (offer) or refusing (resolve) an action the client can't represent rather than flattening it into something it didn't ask for. codeActionProvider now advertises resolveProvider so resolve is actually reachable. - apply_workspace_edit_bridge.py: replaces the bare module-level `apply_workspace_edit` callable in runner_manager.py with a slot matching elicitation_bridge/run_dispatch_bridge's pattern — an unfilled slot fails the waiting caller explicitly instead of crashing on a None call. runner_manager forwards documentChanges as-is (order-preserving) and checks each resource operation's kind against what the bridge reports the editor supports before sending it. - developing-finecode.md: records the --shared-server workflow (why it matters, when to drop it, how to narrow a check to one project or file) worked out while developing this change, plus the enum and NamedTuple conventions this change's own types follow.
An unknown --field=value name or a scalar where an action declares a list used to be accepted silently: the ER ignored the unknown field and cattrs' default union/list handling coerced a string into characters or passed a mismatched type straight through, so mistakes surfaced later as wrong behavior instead of an error. - payload_uris.coerce_raw_value parses each raw CLI string guided by the field's schema (string/boolean/integer/number/array/object, plus enum membership), falling back to the existing blind JSON parse where the schema vouches for nothing. run_cmd._resolve_payload (was _absolutize_payload_resources) refuses unknown field names with a close-match suggestion, unless any requested action's schema is unavailable, in which case the name check is skipped entirely. Fields routed through --map-payload-fields keep their placeholder value untouched. - finecode_extension_runner._converter gains a second `payload_converter` used only for action payloads: strict bool/int/float/str hooks and a strict list/union structuring so a wrong-typed value raises instead of being silently coerced or passed through. The shared `converter` (results, run state, handler/service config) is left untouched, and extra keys are still tolerated there since cross-env subaction dispatch depends on it. - schema_utils gains the shared FieldSchema/PayloadSchema/JsonValue vocabulary these two sides speak, marked as plumbing to move into a common package once one exists. - RunActionsResult.resolved_payload records what actually ran, so --results-file reflects the resolved payload rather than the raw blind parse. - developing-finecode.md: documents the new CLI validation behavior, folds the now-superseded run_tests relative-path pitfall into the same "payload field" account, and adds the Any/sentinel/bare-generic typing conventions worked out while writing the strict converter.
- docs/reference/actions.md: add reference sections for apply_code_actions and run_agent_task, which landed in prior commits (ADR-0083, ADR-0082) without ever getting a reference entry; document group_src_artifact_files_by_lang's per-project pruning behavior and why it matters for correctness and performance; correct file_paths fields to ResourceUri and add the fixability field to lint/audit_code results, so the reference matches what the schemas actually declare. - docs/guides/package-naming.md, creating-preset.md: rewrite the naming convention for ADR-0081 — the segment before the role word is now a domain (language or preset family), not just a language, and the role-word vocabulary is open rather than a closed table to keep in sync. - _converter.py: raise TypeError instead of ValueError from the strict structuring hooks (ruff TRY004), and read the list item type via typing.get_args instead of cls.__args__, which raises AttributeError on a bare `list` annotation before the Any fallback can apply. - run_context.py: document why the run id is ambient (ContextVar) instead of a threaded parameter — the consumer outlives the run and the alternative is a public API change that pushes the responsibility onto every handler author — and contrast it with RunActionMeta.wal_run_id, which carries the same identifier but under handler control. - wm_client.py: type get_payload_schemas' return as schema_utils.PayloadSchema | None instead of a bare dict, matching what the WM actually returns; drop the redundant BLE001 noqa on the request-handler catch-all now that the comment above it fully documents the justification.
The three independent concurrency layers (prepare-envs' sqrt-split
projects/envs caps, run fan-out's per-project semaphore, and each ER's
CommandRunner cap) each guessed a slice of the machine's real capacity
without knowing what the others were doing, and composed multiplicatively
in the worst case. A precommit hook made it concrete: each file-based
bridge handler (lint, format, type_check, inspect_code, audit_code) fanned
out one concurrent workspace call per staged project, each free to spawn
up to its own ER's subprocess cap, so a modest multi-project commit could
launch far more subprocesses than any single layer's math accounted for.
- services/process_budget.py: the WM now owns one machine-wide budget of
subprocess work slots, sized from FINECODE_MAX_CONCURRENT_PROCESSES or
the machine's usable CPU count. ERs lease slots per action run over
finecode/leaseProcessBudget and release them over
finecode/releaseProcessBudget; runner_manager reclaims a runner's
outstanding leases on stop, crash, or a failed start so slots never
leak. A nested lease always gets at least one slot so a run started by
another run's fan-out can never deadlock waiting on the same budget.
- process_slots.py (new, ER side): CommandRunner and ProcessExecutor now
draw from one shared, resizable gate per ER instead of each keeping its
own semaphore — a condition variable rather than an asyncio.Semaphore,
since a shrinking target must block new grants without revoking slots
already held. The WM pushes resize targets down via
finecodeRunner/updateProcessBudget, a process-level tweak that does not
rebuild RunnerContext.
- CommandRunner's config.max_concurrent_processes and prepare-envs'
--max-concurrent-projects/FINECODE_WM_*_MAX_CONCURRENT_PROJECTS are
removed as independent layers; the service-config cap survives as an
optional additional ceiling on top of the shared budget.
- IWorkspaceActionRunner gains run_action_per_project, letting a caller
send one workspace call with a complete payload per project instead of
opening one TaskGroup-fanned-out call per project. The wire protocol
carries this as payloadOverridesByProject, shallow-merged by the WM
against an empty base payload. Every file-based precommit bridge
handler switches to it, cutting a 3-project staged commit from three
concurrent workspace calls to one.
See ADR-0090 for the full rationale and docs/guides/wm-server-internals.md
("Process budget") / docs/guides/preparing-environments.md ("Bounding
concurrency") for the updated operator-facing picture.
...-0072, ADR-0028) goals.md §4.10 puts the WM in charge of the logical fact store and the memo table, but nothing before this owned either: an ER had no path to send `knowledge/query` or `knowledge/registerSchema` requests, and the DAG needed to memoize a query and refresh a stale bucket on demand did not exist anywhere in the WM. - services/knowledge_service.py: loads the fact store, runs R11's confirmation walk, holds the schema an ER registers, executes queries over the interpreter, and memoizes them keyed to the fact DAG's revision. Mode.VERIFIED re-checks tracked inputs before answering; Mode.CACHED skips that walk for a live WM's latency-sensitive callers (ADR-0014 D6). A stale bucket a walk discovers is refreshed by dispatching a scoped `extract_knowledge` run into an ER and ingesting the facts it returns, deduped per bucket and capped in flight so concurrent queries needing the same bucket cost one round trip. - runner/knowledge_bridge.py: the installable slot (ADR-0072) an ER's JSON-RPC client calls into, so the runner layer never imports the service that sits above it in the WM's layer stack. - finecode_knowledge/query/validate.py: adds `_warn_lookup_literals`, the mirror of `_warn_unconstrained_key_bindings` -- it flags a FIELD literal that exists only to bind a head `Prov`, which in a conjunctive language is an existence test the author never asked for. ADR-0028 is the case this closes: `expected_in` was bound by adding a `def_path` literal to two rules, so a project missing that fact produced no finding at all even though the real violation held.
start_own_server() (used by prepare-envs, run, dump-config, bootstrap) sent the subprocess's stderr to DEVNULL, so a crash before the server's own logger initializes left no trace anywhere. The resulting timeout error told the caller to "check logs for errors" that never existed. Capture stderr to a file instead (creating its parent dir, which ensure_running gets for free but this path doesn't), and inline its content into the raised error so it survives even when the disk that held it is gone by the time anyone looks (e.g. a CI runner). Claude-Session: https://claude.ai/code/session_01XaMRTq9Sk67d8x8zEm7wBJ
is_valid_venv() ran `finecode version` (not a real command, always fails) then fell back to `finecode --help`, which click resolves without ever invoking a command body. Neither check imports anything the WM server actually needs, so a broken/stale cached venv -- e.g. one where finecode_knowledge top-level imports fine but a submodule prepare-envs's dedicated WM subprocess needs (`fact_file`) does not -- passes validation and CI skips reinstalling. The failure only surfaces later, as prepare-envs's dedicated server crashing on import and burning its full 30s startup timeout before the (now-captured, see previous commit) traceback explains why. Import the actual module prepare-envs starts instead, so this is caught immediately and the venv gets rebuilt rather than silently reused broken. Claude-Session: https://claude.ai/code/session_01XaMRTq9Sk67d8x8zEm7wBJ
...imports" This reverts commit 10a13ec.
is_valid_venv() called `finecode version` -- not a real command, always failed -- then fell back to `finecode --help`, which click resolves without invoking any command body. Neither check exercises anything the WM server actually needs, so a broken/stale venv (e.g. one where finecode_knowledge imports fine at the top level but its `fact_file` submodule doesn't -- the exact crash the previous commit's stderr capture surfaced from CI) passes validation and CI skips reinstalling. Add `finecode version` as a real command: it starts (or attaches to) the WM server and asks it for its version via server/getInfo, rather than reading local package metadata -- proving the server's full startup path (spawn, import, bind, respond) actually completes, which is what every other client depends on and what actually breaks. This checks the server end-to-end rather than reaching into its internals the way an `import finecode.wm_server.wm_server` probe would. is_valid_venv() now calls this one real command instead of the dead duplicate-and-fallback chain. Verified it correctly reports the venv invalid (and prints the real crash) when fact_file.py is removed, and valid on a healthy install. Claude-Session: https://claude.ai/code/session_01XaMRTq9Sk67d8x8zEm7wBJ
The extension renames and additions from recent commits never got their config-side registration: - Depend on finecode_knowledge/fine_knowledge and add the fine_knowledge preset, wiring in the WM-side knowledge service from 3ba945a. - Point install_claude_code at fine_agent_claude_code (the package was already renamed from fine_system_claude_code) and register the new install_pi handler for fine_agent_pi. - Add the wm_server.context -> services.process_budget ignore_imports exception for the ADR-0090 process budget (c0f9fc4): context.py has imported it since that commit, but the contract was never updated, so audit_code would have flagged it. Also re-run the TOML formatter, which reflows arrays to 2-space indent and alphabetizes dependency lists.
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.
No description provided.