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:
- Match Gaze protocol v1.1.0 exactly (
docs/protocol.md in unbound-force/gaze).
- Do not invent CLI flags, config files, or scoring formulas.
- 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:
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):
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
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):
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.
- Unknown method → error code
-32601.
- Malformed JSON (
{not json) → error code -32700, process still alive for a subsequent valid initialize.
- Missing
method field → -32600.
shutdown → success result {} and main/server loop terminates with exit code 0.
- stdin EOF with no shutdown → exit 0.
--stdio absent → exit 2, nothing written to stdout.
- 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
How to use this issue with
/opsx-proposeUse 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:
docs/protocol.mdin unbound-force/gaze).json,dataclasses,sys) over new dependencies.OpenSpec change name:
scaffold-and-protocolDo 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 aNOTICEfile so later lifts have a place to record attribution.Parent protocol spec: Gaze
docs/protocol.mdv1.1.0. Authoritative Go types:internal/protocol/types.goin unbound-force/gaze.Snake-eyes constitution (
.specify/memory/constitution.md) applies:The existing Speckit spec
specs/001-jsonrpc-prototype/is stale (wrong method set, wrong effect names,discovertreated as required). Do not implement that spec. This issue supersedes its handshake/lifecycle stories only.What to build
1. Python package scaffold
Create:
pyproject.tomlsrc/snake_eyes/__init__.pysrc/snake_eyes/__main__.pyNOTICE.github/workflows/ci.ymlpyproject.tomldecisions (do not ask):snake-eyes0.1.0>=3.11pytest,pytest-cov,mypy,ruffsrc/snake-eyes = "snake_eyes.__main__:main"strict = trueonsrc/testpaths = ["tests"]Do not add
astroid,radon, orcoverageas runtime deps in this issue. Those belong to later analysis issues.src/snake_eyes/__init__.py:src/snake_eyes/__main__.py:--stdio.--stdiois present: start the JSON-RPC server on stdin/stdout and block.--stdiois absent: print a one-line usage message to stderr (snake-eyes --stdio) and exit 2.NOTICE(exact content):(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 (stdlibdataclasses, no pydantic):JSON-RPC envelope
JsonRpcRequest:jsonrpc: str,id: int | str | None,method: str,params: dict | NoneJsonRpcSuccess:jsonrpc: str = "2.0",id: int | str | None,result: objectJsonRpcErrorBody:code: int,message: str,data: object | None = NoneJsonRpcError:jsonrpc: str = "2.0",id: int | str | None,error: JsonRpcErrorBodyStandard error codes (constants, not magic numbers):
initialize
Request params:
{"root_path": "/absolute/path/to/project", "config": {}}root_path(str, required): absolute project rootconfig(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_versionMUST be the string"1.1.0". All four capability flags MUST be present andfalsein this issue. Later issues flip them totruewhen those methods land.language_versionis the running interpreter, e.g."3.12.1"fromsys.version_info.shutdown
nullor omitted{}Serialization:
dataclasses.asdict+json.dumps. Do not emitnullfor omitted optional errordata. Omit the key ifdatais None.3. JSON-RPC server loop
src/snake_eyes/server.pyBehavior (do not ask):
sys.stdin, write tosys.stdout, flush after every response.-32700),idisnull, do not crash.jsonrpc/method:-32600, use requestidif present elsenull.-32601with messageMethod not found: <method>.-32603with the exception message. Do not print tracebacks to stdout (stdout is the protocol). Tracebacks may go to stderr.initializemay be called once. A secondinitializeis still answered with a valid result (idempotent); do not error.initialize/shutdownin this issue:-32601.Dispatch table starts with
initializeandshutdownonly.Do not implement
analyze,complexity,coverage,discover,test_mapping,classify_signals, oranalyze/streamhere.4. CI
.github/workflows/ci.yml:mainubuntu-latestastral-sh/setup-uvthenuv sync --all-extrasuv run ruff check src/ tests/uv run ruff format --check src/ tests/uv run mypy src/uv run pytest --cov=snake_eyes --cov-report=term-missing --cov-fail-under=85Do not add other CI jobs. Do not lower the 85% gate.
5. Tests
tests/test_protocol.pyandtests/test_server.py.Required cases (names may vary; behavior must exist):
initializeroundtrip: write one request line, read one response line, assertjsonrpc, matchingid,protocol_version == "1.1.0",analyzer_name == "snake-eyes",language == "python", all four capability keys present andfalse.-32601.{not json) → error code-32700, process still alive for a subsequent validinitialize.methodfield →-32600.shutdown→ success result{}andmain/server loop terminates with exit code 0.--stdioabsent → exit 2, nothing written to stdout.idof0and of"abc"both round-trip unchanged.Tests MUST drive the server through stdin/stdout (pipe or
io.StringIOinjected intoServer), not by calling private helpers only. Assert JSON field values, not merely "no exception".Out of scope
astroid/radon/coverage.pydependenciesanalyze/stream).gaze.yamlparsingspecs/001-jsonrpc-prototype/Coverage strategy (Constitution IV)
protocol.pyserver.py__main__.py--stdiovs missing flagpytest --cov=snake_eyes --cov-fail-under=85Done when
uv syncworks from a fresh cloneuv run snake-eyes --stdiowaits on stdin