Skip to content

Commit a4875aa

Browse files
authored
Merge pull request #25 from finecode-dev/feature/api-server
API Server
2 parents 6358569 + 842e181 commit a4875aa

227 files changed

Lines changed: 9883 additions & 3884 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-cd.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ jobs:
3131
fail-fast: false
3232
max-parallel: 1
3333
matrix:
34-
os: [ubuntu-24.04, macos-13, windows-2022]
34+
os: [ubuntu-24.04, macos-15, windows-2022]
3535
include:
3636
- os: ubuntu-24.04
3737
name: Linux
38-
- os: macos-13
38+
- os: macos-15
3939
name: macOS
4040
- os: windows-2022
4141
name: Windows
@@ -82,12 +82,13 @@ jobs:
8282
python -m finecode run build_artifact
8383
shell: bash
8484

85-
# - name: Run unit tests
86-
# if: ${{ !cancelled() }}
87-
# run: |
88-
# source .venvs/dev_workspace/bin/activate
89-
# python -m finecode run test
90-
# shell: bash
85+
- name: Run unit tests
86+
if: ${{ !cancelled() }}
87+
run: |
88+
source .venvs/dev_workspace/bin/activate
89+
# TODO: test with all supported python versions
90+
python -m finecode run run_tests
91+
shell: bash
9192

9293
- name: Publish to TestPyPI and verify
9394
if: runner.os == 'Linux' && github.event_name == 'workflow_dispatch' && inputs.publish_testpypi

.mcp.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"mcpServers": {
3+
"finecode": {
4+
"type": "stdio",
5+
"command": ".venvs/dev_workspace/bin/python",
6+
"args": ["-m", "finecode", "start-mcp"]
7+
}
8+
}
9+
}

docs/adr/0001-use-adr.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ADR-0001: Use ADRs for architecture decisions
2+
3+
- **Status:** accepted
4+
- **Date:** 2026-03-19
5+
- **Deciders:** @Aksem
6+
- **Tags:** meta
7+
8+
## Context
9+
10+
FineCode has several important architectural decisions that
11+
are currently documented implicitly across code, comments, and CLAUDE.md. When
12+
new contributors or AI agents work on the codebase, they lack visibility into
13+
*why* decisions were made, what alternatives were considered, and what
14+
constraints must be preserved.
15+
16+
As the project grows and automated testing is introduced, we need a lightweight
17+
way to record decisions so they can be referenced, reviewed, and superseded
18+
over time.
19+
20+
## Related ADRs Considered
21+
22+
None — this is the first ADR.
23+
24+
## Decision
25+
26+
We will use Architecture Decision Records stored in `docs/adr/` following a
27+
simplified [MADR](https://adr.github.io/madr/) (Markdown Any Decision Records)
28+
template. The required sections are Context, Related ADRs Considered, Decision,
29+
and Consequences. Each ADR is a sequentially numbered Markdown file.
30+
31+
The template also documents optional sections (Alternatives Considered, Risks,
32+
Related Decisions, References, Implementation Notes, Review Date) that can be
33+
added when they provide value, but are not required.
34+
35+
ADRs are immutable once accepted. Changed decisions produce a new ADR that
36+
supersedes the previous one.
37+
38+
## Consequences
39+
40+
- Every architecturally significant decision gets a permanent, discoverable
41+
record with its rationale.
42+
- New contributors and AI agents can understand *why* the codebase is shaped
43+
the way it is.
44+
- Slightly more process overhead per decision — mitigated by keeping the
45+
template minimal.
46+
- Existing implicit decisions can be backfilled as ADRs when they become
47+
relevant.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ADR-0002: Port-file discovery for the WM server
2+
3+
- **Status:** accepted
4+
- **Date:** 2026-03-19
5+
- **Deciders:** @Aksem
6+
- **Tags:** ipc, wm-server
7+
8+
## Context
9+
10+
The WM (Workspace Manager) server binds to a random available TCP port on
11+
startup to avoid conflicts between multiple instances (e.g. different workspaces,
12+
test runs). Clients such as the LSP server, MCP server, and CLI commands are
13+
started independently and need a way to find the WM server's port without
14+
prior coordination or a hard-coded value.
15+
16+
Two modes of use must be supported:
17+
18+
- **Shared mode**: a single long-lived WM server shared by multiple clients in the
19+
same workspace (the typical IDE session).
20+
- **Dedicated mode**: a private WM server started by one client (e.g. MCP,
21+
CLI `run`) that must not interfere with the shared instance.
22+
23+
## Related ADRs Considered
24+
25+
None — port/discovery mechanism has no overlap with other ADRs at the time of writing.
26+
27+
## Decision
28+
29+
The WM server writes its listening port as a plain text number to a
30+
*discovery file* immediately after binding:
31+
32+
- **Shared discovery file** (default): `{venv}/cache/finecode/wm_port`, where
33+
`{venv}` is venv where finecode WM server is installed.
34+
- **Dedicated discovery file**: a caller-specified path passed via
35+
`--port-file`. Dedicated instances write to this path instead, leaving the
36+
shared file untouched.
37+
38+
Clients discover the server by reading the file and probing the TCP connection. The probe distinguishes a live server
39+
from a stale file left by a crashed process. The file is deleted on any clean or signal-driven shutdown, and the server directory is created
40+
recursively (including parent directories) on first startup.
41+
42+
## Consequences
43+
44+
- **No port conflicts**: random binding means multiple WM instances (different
45+
workspace, concurrent test runs) coexist without configuration.
46+
- **Stale-file resilience**: client verifies the TCP connection, not
47+
just file existence, so a crashed server does not block future starts.
48+
- **Test isolation**: each e2e test can pass its own file path as
49+
the dedicated port file, running a private WM instance without touching the
50+
developer's live shared server or conflicting with other tests.
51+
- **Cross-process discovery**: any process that can read a file can find the
52+
WM, regardless of parent–child relationship (IDE extensions, CLI tools, MCP
53+
hosts).
54+
- **Crash cleanup gap**: if the server process is killed with SIGKILL or
55+
crashes before port file is removed, the discovery file is not removed. Clients
56+
handle this via the TCP probe, but the stale file persists on disk until the
57+
next server start overwrites it.
58+
59+
### Alternatives Considered
60+
61+
- **Fixed/configured port**: eliminates the discovery file but requires port
62+
coordination across concurrent instances and breaks test isolation.
63+
- **Unix domain socket file**: the socket path serves as both identity and
64+
transport endpoint, avoiding the TCP-probe step. Rejected because Unix
65+
sockets are not available on Windows.
66+
- **Environment variable**: works only for direct child processes; IDE
67+
extensions and independently launched CLI commands cannot inherit it.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# ADR-0003: One Extension Runner process per project execution environment
2+
3+
- **Status:** accepted
4+
- **Date:** 2026-03-19
5+
- **Deciders:** @Aksem
6+
- **Tags:** architecture, extension-runner
7+
8+
## Context
9+
10+
FineCode executes action handlers contributed by extensions. Each handler
11+
declares the **execution environment** (`env`) it runs in and its own set of
12+
dependencies. An execution environment is a named, isolated context serving a
13+
specific purpose (e.g. `runtime` for the project's own runtime code,
14+
`dev_workspace` for workspace tooling, `dev_no_runtime` for dev tools without
15+
runtime deps). In Python, each execution environment is materialized as a
16+
project-local virtual environment.
17+
18+
The **Extension Runner (ER)** is an inter-language concept — a process that
19+
executes handler code inside a specific execution environment. The current
20+
implementation, `finecode_extension_runner`, is Python-specific. Future
21+
implementations for other languages (e.g. JavaScript, Rust) would follow the
22+
same one-process-per-execution-environment model.
23+
24+
The primary requirement is to separate dependencies needed by the project's
25+
own runtime from dependencies needed only by tooling. FineCode must be able to
26+
run project code in one execution environment and run development tooling in
27+
other execution environments without forcing them into a single shared
28+
dependency set.
29+
30+
Once execution environments are isolated, they can also be made more
31+
fine-grained by purpose. This allows tooling dependencies to be grouped
32+
according to their role and makes it possible to move tools with incompatible
33+
dependency requirements into separate execution environments when needed.
34+
35+
The Workspace Manager (WM) is a long-running server that must stay stable
36+
across the full user session. A handler bug, crash, or blocking call in one
37+
execution environment must not take down the WM or interfere with other
38+
execution environments.
39+
40+
## Related ADRs Considered
41+
42+
None — process isolation model has no overlap with other ADRs at the time of writing.
43+
44+
## Decision
45+
46+
Each execution environment in a project runs as an independent
47+
**Extension Runner (ER)** subprocess. In the Python implementation, the ER is
48+
launched using the interpreter from the corresponding project-local virtual
49+
environment, so each ER has a fully isolated dependency set.
50+
51+
Key properties of this design:
52+
53+
- **One ER per (project, execution environment) pair.** ERs are keyed by
54+
`(project_dir_path, env_name)` in the WM's workspace context.
55+
- **Lazy startup with bootstrap exception.** An ER is started only when the
56+
first action request requiring its execution environment arrives, then cached
57+
and reused for subsequent requests. The `dev_workspace` execution
58+
environment is the exception because it must be started first to resolve
59+
presets for other execution environments.
60+
- **JSON-RPC over TCP.** Each ER binds to a random loopback port on startup
61+
and advertises it to the WM. The WM connects via TCP and communicates using
62+
JSON-RPC with Content-Length framing (the same wire format as LSP).
63+
- **Independent lifecycle.** An ER can crash and be restarted without
64+
affecting the WM or ERs for other execution environments. Shutdown is
65+
cooperative: the WM sends `shutdown` + `exit` JSON-RPC calls; the ER exits
66+
cleanly.
67+
- **`dev_workspace` bootstrap execution environment.** The `dev_workspace`
68+
execution environment is always started first; it resolves presets for all
69+
other execution environments before they are configured or started.
70+
71+
## Consequences
72+
73+
- **Dependency isolation**: project runtime dependencies and tooling
74+
dependencies are kept separate, and tooling can be split further into
75+
purpose-specific execution environments when conflicts or different
76+
dependency sets require it.
77+
- **Fault isolation**: a crash or hang in one ER does not affect the WM or
78+
other ERs. The WM can restart a failed ER independently.
79+
- **Startup cost**: launching a Python subprocess and importing handler modules
80+
takes time. Mitigated by lazy startup and long-lived reuse.
81+
- **Higher memory usage**: running multiple ER processes per project uses more
82+
RAM than a single shared process. The overhead is expected to be acceptable
83+
relative to the benefits of dependency isolation, fault isolation, and
84+
long-lived per-environment state.
85+
- **One virtual environment per execution environment per project**:
86+
`prepare-envs` must create and populate the project-local virtual
87+
environment for each declared execution environment before the ER can start.
88+
Missing virtual environments result in `RunnerStatus.NO_VENV` rather than a
89+
crash.
90+
- **`dev_workspace` is a prerequisite**: preset resolution depends on the
91+
`dev_workspace` ER being available. Actions in other execution environments
92+
cannot be configured until `dev_workspace` is initialized.
93+
94+
### Alternatives Considered
95+
96+
- **Single shared process for all handlers**: eliminates subprocess overhead
97+
but forces runtime code and tooling into one shared dependency set, makes
98+
fine-grained environment separation impractical, and means one handler crash
99+
can corrupt or kill the entire tool.
100+
- **Thread per handler invocation**: handlers run in the same process and
101+
virtual environment. No dependency isolation; a blocking or crashing handler
102+
affects all others.
103+
- **In-process plugin loading**: simplest architecture but handlers can import
104+
conflicting packages and accidentally mutate shared WM state.
105+
- **New subprocess per handler invocation**: full isolation per call, but
106+
Python startup cost makes interactive use (e.g. format-on-save) too slow.
107+
It also prevents effective in-process caching between calls because each
108+
invocation starts with cold process state. The long-lived ER model amortizes
109+
startup cost across many invocations and allows caches to be retained in
110+
process when appropriate.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# ADR-0004: Auto-shutdown on disconnect timeout
2+
3+
- **Status:** accepted
4+
- **Date:** 2026-03-19
5+
- **Deciders:** @Aksem
6+
- **Tags:** lifecycle, wm-server
7+
8+
## Context
9+
10+
The WM server is a long-running process started on demand by clients (LSP
11+
server, MCP server, CLI). Clients may terminate without sending an explicit
12+
shutdown request — for example, when the IDE is force-closed, crashes, or the
13+
extension is reloaded. Without a self-termination mechanism, the WM would run
14+
indefinitely as a ghost process, holding the discovery file and consuming
15+
resources.
16+
17+
Clients may also intentionally stop or restart the WM through an explicit
18+
shutdown request. This ADR addresses the complementary case where no such
19+
request is sent and the WM must determine on its own when to exit.
20+
21+
Two distinct scenarios require handling:
22+
23+
1. **No client ever connects** — the WM started successfully but the client
24+
failed to connect (e.g. misconfiguration, client crash during startup).
25+
2. **Last client disconnects** — a normal session end or unexpected client
26+
termination.
27+
28+
## Related ADRs Considered
29+
30+
Reviewed [ADR-0002](0002-port-file-discovery-for-wm-server.md) — related topic:
31+
the WM's shutdown flow performs the discovery-file cleanup defined there.
32+
33+
## Decision
34+
35+
The WM server uses two independent timeout-based shutdown mechanisms:
36+
37+
- **No-client timeout** (default 30 s): started immediately after the server
38+
begins listening. If no client connects within this window, the WM performs
39+
its normal shutdown and exits.
40+
- **Disconnect timeout** (default 30 s): started when the last client
41+
disconnects. If no client reconnects within this window, the WM performs its
42+
normal shutdown and exits.
43+
44+
These timeouts complement, rather than replace, explicit shutdown requests used
45+
by clients that intentionally stop or restart the WM.
46+
47+
Both timeout paths use the WM's normal shutdown flow, including discovery-file
48+
cleanup (see [ADR-0002](0002-port-file-discovery-for-wm-server.md)).
49+
50+
The disconnect timeout is configurable so that tests and dedicated instances
51+
can use a shorter grace period when needed.
52+
53+
Using the same 30-second default for both timeouts keeps lifecycle behavior
54+
simple and provides a reasonable reconnection window for IDE extension reloads
55+
and brief transient disconnects without leaving orphaned processes running for
56+
long.
57+
58+
## Consequences
59+
60+
- **Ghost process prevention**: the WM exits automatically after a client
61+
disconnects, without requiring clients to explicitly decide when the shared
62+
WM should stop. This is the primary defense against orphaned processes after
63+
IDE close or crash.
64+
- **Reconnection window**: the grace period allows clients to reconnect within
65+
the timeout — for example, after an IDE extension reload or a brief
66+
disconnection. The WM does not need to be restarted for each reconnection.
67+
- **Warm reuse across brief idle gaps**: the grace period allows a shared WM
68+
to survive short pauses between independent clients, such as sequential CLI
69+
commands, preserving in-process state and caches between commands and
70+
reducing restart overhead.
71+
- **Connection-driven lifecycle**: shutdown depends on client liveness rather
72+
than completion of previously requested work. Once no clients remain past the
73+
grace period, the WM exits through its normal shutdown path.
74+
- **Discovery file cleanup**: normal shutdown removes the discovery file, so a
75+
stale file is never left behind after a timeout-driven shutdown (unlike a
76+
SIGKILL).
77+
78+
### Alternatives Considered
79+
80+
- **Immediate shutdown on last disconnect**: safe but breaks IDE extension
81+
reload scenarios and brief idle gaps between independent clients, such as
82+
sequential CLI commands using a shared WM.
83+
- **Never auto-shutdown (persistent daemon)**: WM runs until explicitly
84+
stopped. Requires external process management and makes
85+
it harder to reason about lifecycle in tests and CI.
86+
- **Client heartbeat / keepalive**: client sends periodic pings; WM shuts down
87+
if pings stop. More precise than a fixed timeout for detecting dead connected
88+
clients, but it still does not answer how long the WM should remain alive
89+
when no clients are connected at all. Shared-WM use cases with brief idle
90+
gaps between clients, such as sequential CLI commands, would still require a
91+
grace-period timeout or a different persistent-daemon policy. It also
92+
requires all clients to implement the heartbeat protocol.
93+
- **Parent PID tracking**: WM monitors its parent process and exits when the
94+
parent dies. Does not work when the WM is started independently of its client
95+
(e.g. shared WM).

0 commit comments

Comments
 (0)