-
Notifications
You must be signed in to change notification settings - Fork 1
feat: scaffold snake-eyes JSON-RPC server with initialize/shutdown lifecycle #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # This workflow runs lint, formatting, type checking, and tests with | ||
| # coverage on every push and pull request to main. | ||
| name: ci | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| strategy: | ||
| matrix: | ||
| python-version: ["3.11", "3.12"] | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| enable-cache: true | ||
| - run: uv sync --locked | ||
| - run: uv run ruff check src/ tests/ | ||
| - run: uv run ruff format --check src/ tests/ | ||
| - run: uv run mypy src/ | ||
| - run: uv run pytest --cov=snake_eyes --cov-report=term-missing --cov-fail-under=85 |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| snake-eyes | ||
| Copyright 2026 zero-dot-force | ||
|
|
||
| This product includes software originally developed in gaze-py | ||
| (https://github.com/mpeter/gaze-py) by Matt Peter, licensed under | ||
| Apache License 2.0. Copyright headers on lifted files are preserved. | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-08-25 |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| ## Context | ||
|
|
||
| snake-eyes is the Python backend for Gaze's external analyzer protocol (JSON-RPC 2.0 over stdin/stdout, protocol v1.1.0). The project currently has no Python source, no packaging, no tests, and no CI — only a stale Speckit spec (`specs/001-jsonrpc-prototype/`) that predates protocol v1.1.0 and is explicitly superseded for handshake/lifecycle. This change lays the foundation: package scaffold, protocol types, a working stdio server loop, CI, and tests. Authoritative protocol source: unbound-force/gaze `docs/protocol.md` (v1.1.0) and `internal/protocol/types.go`. Build input: issue zero-dot-force/snake-eyes#2. | ||
|
|
||
| Constitution I (Protocol Fidelity) requires every JSON-RPC response to match the v1.1.0 schema exactly. Constitution IV (Testability) requires the coverage strategy to be implemented now, not deferred. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
| - A minimal `snake-eyes` Python package (stdlib only at runtime) installable via `uv sync`. | ||
| - A JSON-RPC 2.0 stdio server implementing `initialize` and `shutdown` correctly per protocol v1.1.0. | ||
| - A CI pipeline (ruff, mypy, pytest with 85% coverage fail-under) on Python 3.11 and 3.12. | ||
| - Tests that drive the server through stdin/stdout and assert JSON field values. | ||
|
|
||
| **Non-Goals:** | ||
| - `analyze`, `complexity`, `coverage`, `discover`, `test_mapping`, `classify_signals`, or `analyze/stream` — these are later issues. | ||
| - Any gaze-py source lift (only a `NOTICE` placeholder is added). | ||
| - `astroid`, `radon`, `coverage.py` as dependencies. | ||
| - Logging library, config-file parsing, or modification of the stale Speckit spec. | ||
|
|
||
| ## Decisions | ||
|
|
||
| 1. **Transport is line-delimited JSON, not LSP Content-Length headers.** | ||
| One JSON object per line over stdin/stdout. This matches Gaze protocol v1.1.0 and keeps the server trivially streamable. Alternative (LSP framing) rejected: the protocol spec mandates line-delimited JSON. | ||
|
|
||
| 2. **Stdlib `dataclasses` for protocol types, serialized via a recursive `to_dict` helper + `json.dumps`.** | ||
| No pydantic. The envelope is small and fixed; a serialization dependency buys nothing at this scope and violates the "prefer stdlib" default. `to_dict` is equivalent to `dataclasses.asdict` plus a `None`-`data` omission filter (plain `asdict` cannot omit a key). The protocol forbids emitting `null` for omitted fields. | ||
|
|
||
| 3. **Server reads `sys.stdin`/writes `sys.stdout` and accepts injected streams plus an injectable dispatch table.** | ||
| A `Server` object takes `stdin`/`stdout`/`stderr` as constructor args so tests can inject `io.StringIO` and drive the loop deterministically. It also accepts an injectable dispatch table (method name → handler callable), defaulting to the built-in `initialize`/`shutdown` table, so a test can register a raising handler to exercise the `-32603` internal-error path through stdin/stdout. This satisfies the "drive through stdin/stdout, not private helpers" test mandate. The broken-pipe test injects a minimal stub whose `write` raises `BrokenPipeError` (not `io.StringIO`, which cannot raise). | ||
|
|
||
| 4. **Sequential request processing.** | ||
| Read a line, handle it, flush, repeat. No pipelining or concurrency — Gaze issues requests sequentially, and ordering matters for `shutdown`. | ||
|
|
||
| 5. **Error taxonomy is the standard JSON-RPC 2.0 set.** | ||
| `-32700` parse error (id `null`), `-32600` invalid request, `-32601` method not found, `-32602` invalid params (fired when `initialize` `params` is absent, non-object, or lacks a string `root_path`), `-32603` internal error. On handler exception, write `-32603` with the message and log tracebacks only to stderr (stdout is the protocol channel). | ||
|
|
||
| 6. **`initialize` is idempotent; `shutdown` exits 0 after ack.** | ||
| A second `initialize` returns a valid result. `shutdown` writes `{}` then the process exits 0. Empty and whitespace-only lines are ignored; stdin EOF without `shutdown` exits 0 silently. Exit codes are surfaced as `SystemExit` raised in-process (the server loop raises `SystemExit(0)` after `shutdown`/EOF and `main()` propagates it); tests primarily catch `SystemExit` from injected streams, with two subprocess smoke tests pinning the real process-boundary behavior. | ||
|
|
||
| 7. **`--stdio` is the only CLI flag.** | ||
| With `--stdio`, start the server and block. Without it, print `snake-eyes --stdio` to stderr and exit 2. No config files, no subcommands. `main(argv=None, *, stdin=None, stdout=None, stderr=None)` defaults each to `sys.*`, mirroring the `Server` seam so tests drive the entry point in-process. | ||
|
|
||
| 8. **CI: `astral-sh/setup-uv` + `uv sync --locked`, matrix 3.11/3.12, four gates.** | ||
| `ruff check`, `ruff format --check`, `mypy src/` (strict), and `pytest --cov=snake_eyes --cov-fail-under=85`. The 85% gate is a governance value and is not lowered. The `astral-sh/setup-uv` action SHALL be pinned to a full commit SHA (tag recorded as a trailing comment) for supply-chain integrity, and the workflow SHALL declare an explicit `permissions: contents: read` block (least privilege). | ||
|
|
||
| 9. **Transport hardening bounds: 16 MiB line cap, 64-char method echo, UTF-8 streams.** | ||
| Request lines longer than 16 MiB (`MAX_LINE_CHARS`) are rejected `-32600` before parsing; deeply nested JSON (`RecursionError`) and undecodable bytes (`UnicodeDecodeError`) yield `-32700` while the loop stays alive; the `-32601` method echo is truncated to 64 characters (`MAX_METHOD_ECHO`). Real stdio text streams are reconfigured to UTF-8 (stdout with `\n` newlines) so the protocol channel is locale-independent. On an output-pipe failure (`OSError`, e.g. EPIPE), the real process stdout is redirected to `os.devnull` before `SystemExit(0)` so interpreter finalization cannot re-flush the broken pipe — a finalization error can exit 120 on Python 3.12+. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| - [Protocol drift vs. Gaze v1.1.0] → Pin `protocol_version` to the literal `"1.1.0"` and assert it in tests; keep the envelope field names verbatim from the issue. | ||
| - [stdlib-only serialization omits `data` incorrectly] → Centralize in one `to_dict` helper with a `None`-omission rule and unit-test the exact JSON output. | ||
| - [stdout contamination from tracebacks] → Route all diagnostics to stderr; tests assert stdout contains only protocol JSON. | ||
| - [Coverage gate vs. small surface area] → `__main__.py`, `protocol.py`, and `server.py` are covered at effectively 100% (statements and branches); the enforced governance gate is the aggregate `--cov-fail-under=85`. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| ## Why | ||
|
|
||
| snake-eyes is the Python Gaze external analyzer backend, but it has no Python source, no `pyproject.toml`, no tests, and no CI. Gaze (Go) spawns it as a subprocess and speaks JSON-RPC 2.0 over stdin/stdout (protocol v1.1.0), so the project cannot even boot without a scaffold and a working `initialize`/`shutdown` lifecycle. This change establishes that foundation. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Add a Python package scaffold: `pyproject.toml`, `src/snake_eyes/__init__.py`, `src/snake_eyes/__main__.py`, and a `NOTICE` file for future gaze-py attribution. | ||
| - Add `src/snake_eyes/protocol.py` with stdlib `dataclasses` for the JSON-RPC 2.0 envelope and the `initialize`/`shutdown` method contracts, plus standard error codes. | ||
| - Add `src/snake_eyes/server.py`: a line-delimited JSON-RPC server loop over stdin/stdout with sequential dispatch and graceful error handling. | ||
| - Add `.github/workflows/ci.yml` running ruff, mypy, and pytest-with-coverage on Python 3.11 and 3.12. | ||
| - Add `tests/test_protocol.py`, `tests/test_server.py`, and `tests/test_cli.py` driving the server through stdin/stdout. | ||
|
|
||
| Out of scope (later issues): analysis, complexity, coverage, discovery, streaming, gaze-py source lift, `astroid`/`radon`/`coverage.py` runtime deps, and any change to the stale `specs/001-jsonrpc-prototype/` Speckit spec. A follow-up will retire or mark the stale spec as superseded; this change leaves it untouched. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
| - `protocol`: JSON-RPC 2.0 envelope types and the `initialize`/`shutdown` method contracts (request params, result schemas, error codes, serialization). | ||
| - `server`: the line-delimited JSON-RPC stdio server loop — request parsing, sequential dispatch, and error handling. | ||
| - `cli`: package scaffold and the `snake-eyes --stdio` entry point (usage-on-error behavior). | ||
|
|
||
| ### Modified Capabilities | ||
|
|
||
| None — this is the first implementation; no existing OpenSpec specs exist yet. | ||
|
|
||
| ### Removed Capabilities | ||
|
|
||
| None. | ||
|
|
||
| ## Impact | ||
|
|
||
| - New files under `src/snake_eyes/`, `tests/`, and `.github/workflows/`. | ||
| - No changes to existing code (there is none) and no runtime third-party dependencies — stdlib (`json`, `dataclasses`, `sys`) only. | ||
| - Dev-only dependencies added: `pytest`, `pytest-cov`, `mypy`, `ruff`. | ||
| - Establishes the project CI gate: ruff lint/format, mypy strict, pytest with 85% coverage fail-under. | ||
|
|
||
| ## Constitution Alignment | ||
|
|
||
| - **I. Protocol Fidelity** — Implemented directly: exact v1.1.0 handshake (`protocol_version = "1.1.0"`), exact envelope/result field names, the standard JSON-RPC error taxonomy, and `data`-omission serialization, all pinned by tests. | ||
| - **II. Detection Accuracy** — N/A: no analysis/detection code is introduced in this change (scaffold + `initialize`/`shutdown` lifecycle only). | ||
| - **III. Python-Native Analysis** — N/A: no analysis/detection code is introduced in this change. | ||
| - **IV. Testability** — The coverage strategy is implemented now, not deferred: effectively 100% statement-and-branch coverage of `protocol.py`, `server.py`, and `__main__.py`; the enforced governance gate is the 85% aggregate (`--cov-fail-under=85`, branch coverage enabled). |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM] Present-tense "includes" asserts gaze-py code ships in this change; the lift is a scheduled follow-up (design.md Non-Goal). Council deferred to human: (a) reword to future intent (amend NOTICE + spec + test together), or (b) keep and disclose in 0.1.0 release notes. (Herald/Scribe)