Skip to content

snake-eyes: project scaffold, JSON-RPC server, and protocol lifecycle #2

Description

@jflowers

How to use this issue with /opsx-propose

/opsx-propose scaffold-and-protocol

Use this entire issue body as the change description. Do not ask clarifying questions. Every decision is already made below. If something is still unspecified, apply these defaults in order:

  1. Match Gaze protocol v1.1.0 exactly (docs/protocol.md in unbound-force/gaze).
  2. Do not invent CLI flags, config files, or scoring formulas.
  3. Prefer stdlib (json, dataclasses, sys) over new dependencies.

OpenSpec change name: scaffold-and-protocol

Do not implement later issues in this change. Scope is scaffold + JSON-RPC loop + initialize + shutdown + CI only.


Context

snake-eyes is the Python Gaze external analyzer backend. Gaze (Go) spawns it as a subprocess and talks JSON-RPC 2.0 over stdin/stdout (protocol v1.1.0). There is currently no Python source, no pyproject.toml, no tests, no CI. This issue creates that foundation.

Matt Peter (mpeter) has given permission to later lift detection logic from mpeter/gaze-py (Apache 2.0). This issue does not lift any gaze-py analysis code. It only adds a NOTICE file so later lifts have a place to record attribution.

Parent protocol spec: Gaze docs/protocol.md v1.1.0. Authoritative Go types: internal/protocol/types.go in unbound-force/gaze.

Snake-eyes constitution (.specify/memory/constitution.md) applies:

  • I Protocol Fidelity — every JSON-RPC response must match the schema.
  • IV Testability — coverage strategy must be specified and implemented, not deferred.

The existing Speckit spec specs/001-jsonrpc-prototype/ is stale (wrong method set, wrong effect names, discover treated as required). Do not implement that spec. This issue supersedes its handshake/lifecycle stories only.


What to build

1. Python package scaffold

Create:

  • pyproject.toml
  • src/snake_eyes/__init__.py
  • src/snake_eyes/__main__.py
  • NOTICE
  • .github/workflows/ci.yml

pyproject.toml decisions (do not ask):

Field Value
name snake-eyes
version 0.1.0
requires-python >=3.11
license Apache-2.0
runtime deps none yet (stdlib only for this issue)
optional/dev deps pytest, pytest-cov, mypy, ruff
package layout src/
script entry snake-eyes = "snake_eyes.__main__:main"
ruff line length 88
mypy strict = true on src/
pytest testpaths = ["tests"]
coverage fail-under 85

Do not add astroid, radon, or coverage as runtime deps in this issue. Those belong to later analysis issues.

src/snake_eyes/__init__.py:

__version__ = "0.1.0"

src/snake_eyes/__main__.py:

  • Parse argv. The only supported flag is --stdio.
  • If --stdio is present: start the JSON-RPC server on stdin/stdout and block.
  • If --stdio is absent: print a one-line usage message to stderr (snake-eyes --stdio) and exit 2.
  • No other CLI. snake-eyes is not a user-facing tool.

NOTICE (exact content):

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.

(No gaze-py files are lifted in this issue; the NOTICE exists so later issues can add files under that attribution.)

2. Protocol types

src/snake_eyes/protocol.py — dataclasses (stdlib dataclasses, no pydantic):

JSON-RPC envelope

  • JsonRpcRequest: jsonrpc: str, id: int | str | None, method: str, params: dict | None
  • JsonRpcSuccess: jsonrpc: str = "2.0", id: int | str | None, result: object
  • JsonRpcErrorBody: code: int, message: str, data: object | None = None
  • JsonRpcError: jsonrpc: str = "2.0", id: int | str | None, error: JsonRpcErrorBody

Standard error codes (constants, not magic numbers):

Name Code
PARSE_ERROR -32700
INVALID_REQUEST -32600
METHOD_NOT_FOUND -32601
INVALID_PARAMS -32602
INTERNAL_ERROR -32603

initialize

Request params:

{"root_path": "/absolute/path/to/project", "config": {}}
  • root_path (str, required): absolute project root
  • config (object, optional): opaque analyzer config from .gaze.yaml; ignore contents in this issue, accept missing/{}

Result (exact field names):

{
  "analyzer_name": "snake-eyes",
  "language": "python",
  "language_version": "<sys.version_info major.minor.micro>",
  "protocol_version": "1.1.0",
  "capabilities": {
    "discover": false,
    "test_mapping": false,
    "classify_signals": false,
    "streaming": false
  }
}

protocol_version MUST be the string "1.1.0". All four capability flags MUST be present and false in this issue. Later issues flip them to true when those methods land.

language_version is the running interpreter, e.g. "3.12.1" from sys.version_info.

shutdown

  • Request params: null or omitted
  • Result: {}
  • After writing the success response, the process exits 0

Serialization: dataclasses.asdict + json.dumps. Do not emit null for omitted optional error data. Omit the key if data is None.

3. JSON-RPC server loop

src/snake_eyes/server.py

Behavior (do not ask):

  • Transport: line-delimited JSON over stdin/stdout (one JSON object per line). Not LSP Content-Length headers.
  • Read from sys.stdin, write to sys.stdout, flush after every response.
  • Process requests sequentially. Do not pipeline.
  • Empty line: ignore.
  • stdin EOF (no shutdown): exit 0 without writing a response.
  • Malformed JSON: write parse error (-32700), id is null, do not crash.
  • JSON that is not an object, or missing jsonrpc/method: -32600, use request id if present else null.
  • Unknown method: -32601 with message Method not found: <method>.
  • Handler raises: -32603 with the exception message. Do not print tracebacks to stdout (stdout is the protocol). Tracebacks may go to stderr.
  • initialize may be called once. A second initialize is still answered with a valid result (idempotent); do not error.
  • Methods other than initialize/shutdown in this issue: -32601.

Dispatch table starts with initialize and shutdown only.

Do not implement analyze, complexity, coverage, discover, test_mapping, classify_signals, or analyze/stream here.

4. CI

.github/workflows/ci.yml:

  • Trigger: push and pull_request to main
  • Runner: ubuntu-latest
  • Python: 3.11 and 3.12 (matrix)
  • Install: astral-sh/setup-uv then uv sync --all-extras
  • Steps (all must pass):
    1. uv run ruff check src/ tests/
    2. uv run ruff format --check src/ tests/
    3. uv run mypy src/
    4. uv run pytest --cov=snake_eyes --cov-report=term-missing --cov-fail-under=85

Do not add other CI jobs. Do not lower the 85% gate.

5. Tests

tests/test_protocol.py and tests/test_server.py.

Required cases (names may vary; behavior must exist):

  1. initialize roundtrip: write one request line, read one response line, assert jsonrpc, matching id, protocol_version == "1.1.0", analyzer_name == "snake-eyes", language == "python", all four capability keys present and false.
  2. Unknown method → error code -32601.
  3. Malformed JSON ({not json) → error code -32700, process still alive for a subsequent valid initialize.
  4. Missing method field → -32600.
  5. shutdown → success result {} and main/server loop terminates with exit code 0.
  6. stdin EOF with no shutdown → exit 0.
  7. --stdio absent → exit 2, nothing written to stdout.
  8. Request id of 0 and of "abc" both round-trip unchanged.

Tests MUST drive the server through stdin/stdout (pipe or io.StringIO injected into Server), not by calling private helpers only. Assert JSON field values, not merely "no exception".


Out of scope

  • File discovery, analysis, complexity, coverage
  • Any gaze-py source lift
  • astroid / radon / coverage.py dependencies
  • Streaming (analyze/stream)
  • Logging library, config files, .gaze.yaml parsing
  • Updating or deleting specs/001-jsonrpc-prototype/

Coverage strategy (Constitution IV)

Layer Strategy Target
protocol.py Unit: serialize/deserialize each envelope and initialize result 100%
server.py Unit: injected stdin/stdout, cases listed above 100%
__main__.py Unit: --stdio vs missing flag 100%
Project gate pytest --cov=snake_eyes --cov-fail-under=85 85%

Done when

  • uv sync works from a fresh clone
  • uv run snake-eyes --stdio waits on stdin
  • Full initialize → shutdown lifecycle works as JSON-RPC 2.0
  • CI workflow file exists and the four checks pass locally
  • No analysis modules exist yet

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions