Skip to content

feat: scaffold snake-eyes JSON-RPC server with initialize/shutdown lifecycle - #7

Merged
jflowers merged 2 commits into
mainfrom
opsx/scaffold-and-protocol
Aug 27, 2026
Merged

feat: scaffold snake-eyes JSON-RPC server with initialize/shutdown lifecycle#7
jflowers merged 2 commits into
mainfrom
opsx/scaffold-and-protocol

Conversation

@jflowers

Copy link
Copy Markdown
Contributor

Summary

snake-eyes is the Python backend for the Gaze analyzer, but it had no Python source, package metadata, tests, or CI — it could not boot. This change delivers the foundation: the package scaffold, JSON-RPC 2.0 protocol types, a line-delimited stdio server loop implementing the initialize/shutdown lifecycle, CI, and tests — matching Gaze's analyzer protocol v1.1.0 exactly.

  • Package scaffold: pyproject.toml (stdlib-only runtime, src/ layout, snake-eyes entry point), __init__.py (__version__ = "0.1.0"), __main__.py (--stdio only), and NOTICE.
  • Protocol types: stdlib dataclasses for the JSON-RPC envelope, exact error codes (-32700-32603), initialize result schema (4 capability flags all false), and data-omission-when-None serialization.
  • Server loop: injectable streams + injectable dispatch table (default initialize/shutdown), sequential processing, clean teardown on shutdown/EOF/broken-pipe.
  • CI: Python 3.11 + 3.12, SHA-pinned actions, permissions: contents: read, ruff + mypy + pytest with 85% coverage gate.

Out of scope (later issues): analysis, complexity, coverage, discovery, streaming, gaze-py source lift, and astroid/radon/coverage.py runtime deps.

How to Test

uv sync --locked
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

Manual lifecycle check:

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"root_path":"/"}}\n{"jsonrpc":"2.0","id":2,"method":"shutdown"}\n' | uv run snake-eyes --stdio

How to Demo

Run uv run snake-eyes --stdio and observe it waits on stdin. Send an initialize request and confirm the response carries analyzer_name, language, language_version, protocol_version: "1.1.0", and four false capability flags. Send shutdown and observe the {} result and clean exit 0. Running uv run snake-eyes without --stdio prints snake-eyes --stdio to stderr and exits 2.

Key Files Changed

  • src/snake_eyes/protocol.py — JSON-RPC envelope dataclasses, error codes, initialize/shutdown results, serialization.
  • src/snake_eyes/server.py — line-delimited stdio loop with injectable streams/dispatch and full error taxonomy.
  • src/snake_eyes/__main__.py--stdio-only CLI entry point.
  • src/snake_eyes/__init__.py — package version.
  • pyproject.toml — build config, stdlib-only runtime, dev deps, ruff/mypy/pytest config.
  • .github/workflows/ci.yml — 4-gate CI (ruff, mypy, pytest with 85% coverage).
  • tests/test_{protocol,server,cli}.py — 42 tests driving through injected stdin/stdout (100% coverage).
  • NOTICE — gaze-py attribution.
  • openspec/changes/scaffold-and-protocol/ — proposal, design, specs, tasks.

Known Issues

The following review-council findings (all LOW, non-blocking) were acknowledged but not resolved:

  • LOW: _extract_id treats JSON booleans as valid ids (Python bool subclasses int); {"id": true} is echoed rather than rejected.
  • LOW: BrokenPipeError teardown may emit "Exception ignored" noise on interpreter shutdown.
  • LOW: requires-python = ">=3.11" is unbounded while CI only verifies 3.11/3.12; build backend hatchling is unpinned.

This PR was generated by /uf.finale (AI-assisted).

…fecycle

- Add package scaffold (pyproject.toml, src layout, NOTICE attribution)
- Implement protocol types with stdlib dataclasses and JSON-RPC 2.0 error codes
- Implement line-delimited stdio server loop with injectable streams and dispatch
- Add CI workflow (ruff, mypy, pytest with 85% coverage gate)
- Add tests driving through injected stdin/stdout (100% coverage)

Assisted-by: deepseek-v4-pro
Generated with AI assistance (deepseek-v4-pro)

@jflowers jflowers left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Council Verdict: APPROVE

Reviewers: adversary, architect, curator, envoy, guard, herald, scribe, sre, testing
Iterations: 2

All nine reviewers APPROVE after one fix pass (bool JSON-RPC id, README/AGENTS current-state, initialize/config tests, to_dict wording, checkout tag comment).

5 LOW findings omitted.

Note: Could not post as APPROVE due to insufficient permissions. Posted as COMMENT instead. Original verdict: APPROVE.


This review was generated by /review-council (AI-assisted).

@jflowers jflowers left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Could not post as APPROVE — GitHub prohibits self-review (PR author). Posted as COMMENT instead. Original verdict: APPROVE.

Council Verdict: APPROVE

Reviewers: Adversary, Architect, Guard, Tester, SRE, Curator, Envoy, Herald, Scribe
Iterations: 2 (hardening fix round + spec doc-sync round)

All 9 reviewers approve. All CRITICAL/HIGH/MEDIUM findings resolved in the fix loop; two findings retracted as false positives on git evidence. Gates: ruff + mypy strict + pytest — 69 tests, 100% statement+branch coverage (85% gate untouched).

Adversary (APPROVE)

  • [MEDIUM] Stdin parse path unhardened (RecursionError escape, no size cap) → FIXED (-32700/-32600 + stay-alive regression tests)
  • 5 LOW fixed or deferred to issues #3-#6 with rationale

Architect (APPROVE — Alignment 92→96/100)

  • [MEDIUM] BrokenPipeError teardown could exit 120 on Python 3.12+ → FIXED (OSError catch + devnull redirect)
  • 4 LOW fixed (helper extraction, conftest.py, annotations, docstrings)

Guard (APPROVE)

  • [MEDIUM] Constitution citations — RETRACTED (false positive; .specify/memory/constitution.md tracked on main)
  • Full spec→code→test traceability; zero scope creep; gates intact

Tester (APPROVE)

  • [HIGH] Transport liveness unverifiable with StringIO fixtures → FIXED (flush-spy exact-count test; shutdown+trailing-request test)
  • [MEDIUM] No process-boundary test → FIXED (two subprocess smoke tests)
  • 4 LOW fixed (branch coverage enabled — gate value not lowered)

SRE (APPROVE)

  • [MEDIUM] Locale-dependent stdio encoding → FIXED (UTF-8 reconfigure + UnicodeDecodeError→-32700)
  • [MEDIUM] Version dual source of truth → FIXED (dynamic version + drift-pinning test)
  • 9 LOW fixed/verified (CI hardening, NOTICE-in-wheel via uv build) or deferred

Curator (APPROVE)

  • [MEDIUM] Public-seam docstrings underdocumented → FIXED (dispatch-replacement seam, exit codes, field semantics)

Envoy (APPROVE) — 3 LOW (1 retracted artifact, 2 deferred)

Herald (APPROVE)

  • [MEDIUM] NOTICE present-tense attribution ahead of the gaze-py lift → DEFERRED TO HUMAN (see below)
  • 5 LOW fixed or deferred

Scribe (APPROVE)

  • [CRITICAL] readme/LICENSE missing — RETRACTED (false positives; both tracked on main at 204ec28)
  • 4 LOW fixed (AGENTS.md tree, spec wording)

Round-2 residuals — resolved

  • [MEDIUM] Spec text lagged hardened implementation → FIXED (server/cli spec amendments, design decision 9, tasks.md metadata)

Open item for human decision (non-blocking)

  • [MEDIUM] NOTICE:4-6 — present-tense "includes software originally developed in gaze-py" while the lift is a scheduled follow-up. (a) Reword to future intent (amend NOTICE + spec + test together), or (b) keep and disclose in 0.1.0 release notes. Herald recommends (b).

This review was generated by /review-council (AI-assisted).

Comment thread NOTICE
snake-eyes
Copyright 2026 zero-dot-force

This product includes software originally developed in gaze-py

Copy link
Copy Markdown
Contributor Author

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)

@jflowers jflowers left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Council Verdict: APPROVE

Note: Could not post as APPROVE — GitHub prohibits self-review (PR author). Posted as COMMENT instead. Original verdict: APPROVE.

Reviewers: Adversary, Architect, Guard, Tester, SRE, Curator, Envoy, Herald, Scribe
Iterations: 1 (unanimous APPROVE on first pass)

Adversary (APPROVE)

No blocking findings. 3 LOW: internal error message leaks exception string (benign — trusted subprocess), _devnull_stdout bare pass needs comment, ubuntu-latest mutable runner.

Architect (APPROVE — Alignment 9/10)

No blocking findings. 4 LOW: .gitignore boilerplate, conftest.py missing from manifest, test stub docstring inconsistency, conftest.py bare module import.

Guard (APPROVE)

Full spec→code→test traceability verified. Zero scope creep. All governance gates intact (85% coverage, mypy strict, ruff). 2 LOW: .gitignore boilerplate, CI naming convention.

Tester (APPROVE)

2 MEDIUM: to_dict scalar passthrough not directly tested (see inline), subprocess timeout messaging. 2 LOW: conftest.py id shadows builtin, test_initialize_roundtrip relies on helper correctness.

SRE (APPROVE)

4 LOW: dev deps unpinned (lockfile protects), ubuntu-latest mutable runner, no Dependabot/Renovate, README missing troubleshooting.

Curator (APPROVE)

1 MEDIUM: platform pattern blog opportunity — recommend filing when analysis capabilities land.

Envoy (APPROVE)

3 LOW omitted.

Herald (APPROVE)

3 MEDIUM: notification semantics (see inline), root_path not validated as absolute (see inline), CI action version comment drift. 5 LOW omitted.

Scribe (APPROVE)

4 LOW omitted.

Summary

Severity Count
CRITICAL 0
HIGH 0
MEDIUM 5
LOW 22

22 LOW findings omitted.

All findings are non-blocking. This is a clean, well-structured scaffold with strong protocol fidelity, comprehensive test coverage (69 tests, 100% coverage, 85% gate preserved), and proper CI hardening.


This review was generated by /review-council (AI-assisted).

Comment thread tests/test_protocol.py

def test_to_dict_handles_list_values() -> None:
request = JsonRpcRequest("2.0", 1, "initialize", {"items": [1, 2]})
assert to_dict(request)["params"]["items"] == [1, 2]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Missing direct test for to_dict scalar passthrough. The scalar branch (return obj) is exercised transitively through dataclass field serialization but has no dedicated test. Consider adding:

def test_to_dict_scalar_passthrough() -> None:
    assert to_dict(42) == 42
    assert to_dict("hello") == "hello"
    assert to_dict(None) is None

(Testing)

Comment thread src/snake_eyes/server.py
request_id = data.get("id")
if isinstance(request_id, (int, str)):
return request_id
return None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] _extract_id returns None for absent id, causing the server to respond to all requests including JSON-RPC 2.0 notifications (§4.1 says notifications with no id should not receive responses). Acceptable simplification since Gaze never sends notifications — worth noting for future protocol compliance. (Herald)

Comment thread src/snake_eyes/server.py
if not isinstance(params, dict):
raise RpcError(INVALID_PARAMS, "Invalid params")
root_path = params.get("root_path")
if not isinstance(root_path, str):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] root_path is validated as a string but not as an absolute path or verified to exist. Not needed until discover/analyze are implemented (issues #3-#4) — flagging for tracking. (Herald)

- Add oversize line rejection (16 MiB cap) with stay-alive behavior
- Add UnicodeDecodeError handling at readline level (-32700)
- Add RecursionError guard for deeply nested JSON payloads
- Add bool-is-int guard in _extract_id (Python bool subclasses int)
- Add broken pipe / OSError teardown with devnull stdout redirect
- Add method name truncation (64 chars) in -32601 responses
- Add UTF-8 stream reconfiguration in CLI entry point
- Add conftest.py with shared req()/responses() test helpers
- Add subprocess smoke tests for initialize/shutdown roundtrip
- Improve error messages with specific field names
- Enhance docstrings across protocol types and server
- Amend spec artifacts to reflect hardened implementation
- Update CI: concurrency group, persist-credentials, timeout
- Consolidate coverage config; single-source version via hatch
@jflowers
jflowers merged commit 3d4a1ff into main Aug 27, 2026
2 checks passed
@jflowers

jflowers commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

PR Cost Report: #7

Session: Propose new change artifacts

ID: ses_fc544c2f2ffeUuUHJLFyzxlMGB

Metric Value
Cost (parent only) $8.97
Input tokens 2,120,775
Output tokens 89,330
Reasoning tokens 104,440
Cache read tokens 18,742,016
Cache write tokens 0

Timeline: 2026-08-25 17:02:24 — 2026-08-27 13:03:42

Child sessions: 56 — additional cost: $14.39
Session tree total: $23.36

Child session breakdown
Session Cost Output Tokens
Spec review: adversary (@divisor-adversary subagent) $0.06 3,284
Spec review: architect (@divisor-architect subagent) $0.07 3,134
Spec review: guard (@divisor-guard subagent) $0.05 2,867
Spec review: testing (@divisor-testing subagent) $0.06 3,442
Spec review: sre (@divisor-sre subagent) $0.06 3,474
Spec review: curator (@divisor-curator subagent) $0.05 2,468
Spec review: divisor-testing (@divisor-testing subagent) $0.06 2,852
Spec review: divisor-adversary (@divisor-adversary subagent) $0.05 2,755
Spec review: divisor-architect (@divisor-architect subagent) $0.06 3,069
Spec review: divisor-guard (@divisor-guard subagent) $0.05 2,926
Spec review: divisor-sre (@divisor-sre subagent) $0.05 3,585
Spec review: divisor-curator (@divisor-curator subagent) $0.05 1,928
Spec review round 3: testing (@divisor-testing subagent) $0.05 2,389
Spec review round 3: adversary (@divisor-adversary subagent) $0.04 1,938
Spec review round 3: architect (@divisor-architect subagent) $0.06 3,199
Spec review round 3: guard (@divisor-guard subagent) $0.06 3,097
Spec review round 3: sre (@divisor-sre subagent) $0.06 2,702
Spec review round 3: curator (@divisor-curator subagent) $0.06 2,418
Spec review final: testing (@divisor-testing subagent) $0.03 1,332
Spec review final: guard (@divisor-guard subagent) $0.06 2,513
Spec review final: sre (@divisor-sre subagent) $0.06 2,039
Spec review final: architect (@divisor-architect subagent) $0.04 1,786
Spec review final: adversary (@divisor-adversary subagent) $0.04 1,717
Spec review final: curator (@divisor-curator subagent) $0.04 2,074
Spec review consensus: sre (@divisor-sre subagent) $0.05 2,003
Spec review consensus: architect (@divisor-architect subagent) $0.05 2,557
Spec review consensus: testing (@divisor-testing subagent) $0.04 1,666
Spec review consensus: guard (@divisor-guard subagent) $0.05 2,371
Spec review consensus: adversary (@divisor-adversary subagent) $0.03 1,133
Spec review consensus: curator (@divisor-curator subagent) $0.04 1,520
Code review: divisor-adversary (@divisor-adversary subagent) $0.09 3,234
Code review: divisor-architect (@divisor-architect subagent) $0.10 3,867
Code review: divisor-guard (@divisor-guard subagent) $0.10 3,837
Code review: divisor-testing (@divisor-testing subagent) $0.07 3,161
Code review: divisor-sre (@divisor-sre subagent) $0.12 3,560
Code review: divisor-curator (@divisor-curator subagent) $0.05 2,343
Gaze full quality report (@gaze-reporter subagent) $0.17 1,067
Code review: adversary (@divisor-adversary subagent) $0.77 2,644
Code review: architect (@divisor-architect subagent) $0.59 2,817
Code review: guard (@divisor-guard subagent) $1.06 3,575
Code review: testing (@divisor-testing subagent) $0.60 3,581
Code review: sre (@divisor-sre subagent) $0.60 2,957
Code review: curator (@divisor-curator subagent) $0.34 2,737
Code review: envoy (@divisor-envoy subagent) $0.66 3,414
Code review: herald (@divisor-herald subagent) $0.68 3,155
Code review: scribe (@divisor-scribe subagent) $0.96 3,387
Re-review adversary (@divisor-adversary subagent) $0.98 2,327
Re-review architect (@divisor-architect subagent) $0.94 2,648
Re-review guard (@divisor-guard subagent) $0.94 2,428
Re-review testing (@divisor-testing subagent) $0.39 1,627
Re-review sre (@divisor-sre subagent) $0.28 1,466
Re-review curator (@divisor-curator subagent) $0.04 86
Re-review envoy (@divisor-envoy subagent) $0.93 2,518
Re-review herald (@divisor-herald subagent) $0.54 1,959
Re-review scribe (@divisor-scribe subagent) $0.69 2,142
Re-review curator retry (@divisor-curator subagent) $0.17 908

Session: Python project review - skip gaze testing

ID: ses_fbbd1cb74ffeOPcB3kZ1otI4H4

Metric Value
Cost (parent only) $5.00
Input tokens 533,415
Output tokens 38,502
Reasoning tokens 43,960
Cache read tokens 7,196,544
Cache write tokens 0

Timeline: 2026-08-27 13:04:33 — 2026-08-27 14:17:20

Child sessions: 9 — additional cost: $11.79
Session tree total: $16.78

Child session breakdown
Session Cost Output Tokens
Adversary security review (@divisor-adversary subagent) $1.09 4,894
Architect structure review (@divisor-architect subagent) $1.02 5,335
Guard intent-drift review (@divisor-guard subagent) $1.05 5,658
Testing quality review (@divisor-testing subagent) $0.92 5,422
SRE operations review (@divisor-sre subagent) $1.02 5,702
Curator docs review (@divisor-curator subagent) $1.29 5,917
Envoy general review (@divisor-envoy subagent) $1.88 4,340
Herald general review (@divisor-herald subagent) $1.41 5,324
Scribe technical docs review (@divisor-scribe subagent) $2.10 6,444

Session: Review council: skip gaze for Python project

ID: ses_fbb55c7d5ffefvhRiBPl6yLJqZ

Metric Value
Cost (parent only) $7.00
Input tokens 82
Output tokens 32,054
Cache read tokens 5,078,377
Cache write tokens 585,818

Timeline: 2026-08-27 15:20:01 — 2026-08-27 16:51:01

Child sessions: 9 — additional cost: $7.67
Session tree total: $14.67

Child session breakdown
Session Cost Output Tokens
divisor-adversary review (@divisor-adversary subagent) $0.96 4,899
divisor-architect review (@divisor-architect subagent) $0.85 5,068
divisor-guard review (@divisor-guard subagent) $1.08 5,809
divisor-testing review (@divisor-testing subagent) $0.80 5,387
divisor-sre review (@divisor-sre subagent) $0.89 3,798
divisor-curator review (@divisor-curator subagent) $0.99 3,907
divisor-envoy review (@divisor-envoy subagent) $0.71 3,021
divisor-herald review (@divisor-herald subagent) $0.71 3,881
divisor-scribe review (@divisor-scribe subagent) $0.67 3,265

Grand Total: $54.82 across 3 session tree(s) (77 sessions)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant