First-PR scope from #1. Single-process Python daemon that relays between Claude Code instances and a chat-Claude session via the Anthropic API. CC drops JSON envelopes into queue/; the daemon appends to a running history, calls the API (system prompt cached), and either dispatches the response back to the originating session or pauses on [NEEDS-JC] and notifies via ntfy.sh.
tier/2-review. Long-lived process; touches secrets; sends data to a third-party API.
Changes
relay/ package (8 modules)
config.py — .env + config.yaml loader. Auto-generates a 16-char secrets.token_urlsafe ntfy topic on first run and persists back to .env. Defensive against the common KEY= # inline comment dotenv parse trap. Seeds a default config.yaml (system prompt + summarization prompt + one session-1) if missing.
state.py — Atomic file I/O (tempfile + os.replace); fcntl.flock-based instance lock at state/.lock so a second daemon refuses to start.
conversation.py — Append-only history with summarization. Triggers when total chars exceed HISTORY_CHAR_CAP (default 400k); replaces history with the summary turn plus the most recent 10 turns.
anthropic_client.py — Single SDK wrapper. Marks the system prompt cacheable (cache_control: ephemeral). Estimates per-call cost from a model→price table (Opus 4.7, Opus 4.7-1m, Sonnet 4.6, Haiku 4.5) including cache-write/read rates. Concatenates text blocks, ignores tool_use blocks safely, falls back to Opus pricing for unknown models.
queue.py — JSON envelope intake. Picks oldest by mtime; validates {session_id, timestamp, content}; rejects malformed envelopes to queue/.rejected/. ack() deletes after successful dispatch.
dispatch.py — Per-session dispatch/<session_id>/input.txt. Atomic write via state.write_atomic. Won't overwrite a pending dispatch — internal deque per session; flushes when CC consumes (deletes) the previous file.
ntfy.py — Best-effort POST to https://ntfy.sh/<topic>. Network failures logged, never raised — main loop never blocks on notification delivery.
daemon.py — Main polling loop. Each tick: flush deferred dispatches, drain state/jc_input.txt (priority), drain queue (one entry/tick), heartbeat for stuck-queue alerts. [NEEDS-JC] detection in first 200 chars pauses the loop and ntfy-alerts. JC override supports @session-N: prefix for direct dispatch without an API call.
__main__.py — CLI: relay run (daemon), relay status (read state/status.json), relay topic (print ntfy URL). Signal handling: SIGINT/SIGTERM → graceful stop.
Tests (57 unit + 1 gated real-API)
test_state.py — atomic write durability, instance lock semantics including a concurrent-acquire thread test.
test_anthropic_client.py — system-prompt cache marker, cost estimates including cache savings, fallback for unknown models, multi-block concatenation, non-text block handling.
test_daemon_loop.py — full loop with a fake client. Covers: queue → dispatch, [NEEDS-JC] pause, @session-N: direct dispatch, JC override clears needs_jc, summarization triggers at cap, malformed file does not crash loop, dispatch queues on pending input.
test_real_api.py — gated @pytest.mark.real_api, opt in with pytest -m real_api. Sends one Haiku 4.5 round-trip (~$0.0001). Skips cleanly on credit balance too low errors so it's honest about why it didn't run.
Project files
pyproject.toml — risv3-relay 0.1.0, Python ≥ 3.10. Pinned deps: anthropic, flask (reserved for follow-up status endpoint), requests, pyyaml, python-dotenv. Default test invocation excludes real_api via addopts = "-m 'not real_api'".
config.yaml — auto-seeded on first daemon run; committed so JC can review/edit the system prompt and summarization prompt.
README.md — install / run quickstart, CC-side polling protocol with shell snippet, JC operations cheatsheet.
Test checklist
New tests added for new behavior — 57 unit tests across 9 modules.
All tests pass locally: pytest → 57 passed, 1 deselected (real_api). pytest -m real_api → 1 skipped (no API credit).
Manual smoke test performed — see below.
No new TODO/FIXME comments without a linked issue.
No secrets in diffs (.env stays gitignored; .env.example carries placeholders only).
Smoke test performed
Booted relay run against a real .env. The daemon:
Generated a fresh ntfy topic on first start and persisted it to .env (subsequent runs reuse it).
Printed the announcement banner with the subscription URL.
Acquired the state/.lock via flock; a concurrent boot would refuse to start (covered by unit test).
Polled queue/ and state/jc_input.txt at 1 Hz with empty inputs (no API calls, no costs).
Wrote state/status.json each tick. relay status and relay topic round-trip cleanly.
I did NOT run a live API turn end-to-end — the test API key has zero credit balance and returns 400 on any messages.create. The real_api smoke test detects that and skips, and the daemon_loop integration suite covers the full path with a fake client (queue → API → dispatch, including the summarization trigger).
Schema changes
None (no DB).
Multi-AI review
Skipped octopus review for this first PR — it's all greenfield code with comprehensive unit + integration coverage and the spec was JC's. Will reinstate octopus reviews on follow-up PRs that touch security-sensitive paths (status endpoint exposure, systemd integration, multi-tenant config).
Screenshots
N/A — backend daemon, no UI.
Out of scope (follow-up PRs)
Flask status endpoint (flask already a dependency for this).
Multi-session config in real use.
Exponential backoff on transient API errors.
systemd unit file.
Cost-tracking aggregation / dashboard.
## Summary
First-PR scope from #1. Single-process Python daemon that relays between Claude Code instances and a chat-Claude session via the Anthropic API. CC drops JSON envelopes into `queue/`; the daemon appends to a running history, calls the API (system prompt cached), and either dispatches the response back to the originating session or pauses on `[NEEDS-JC]` and notifies via [ntfy.sh](https://ntfy.sh).
## Issue
Closes #1.
## AI tier
`tier/2-review`. Long-lived process; touches secrets; sends data to a third-party API.
## Changes
### `relay/` package (8 modules)
- **`config.py`** — `.env` + `config.yaml` loader. Auto-generates a 16-char `secrets.token_urlsafe` ntfy topic on first run and persists back to `.env`. Defensive against the common `KEY= # inline comment` dotenv parse trap. Seeds a default `config.yaml` (system prompt + summarization prompt + one `session-1`) if missing.
- **`state.py`** — Atomic file I/O (tempfile + `os.replace`); `fcntl.flock`-based instance lock at `state/.lock` so a second daemon refuses to start.
- **`conversation.py`** — Append-only history with summarization. Triggers when total chars exceed `HISTORY_CHAR_CAP` (default 400k); replaces history with the summary turn plus the most recent 10 turns.
- **`anthropic_client.py`** — Single SDK wrapper. Marks the system prompt cacheable (`cache_control: ephemeral`). Estimates per-call cost from a model→price table (Opus 4.7, Opus 4.7-1m, Sonnet 4.6, Haiku 4.5) including cache-write/read rates. Concatenates `text` blocks, ignores `tool_use` blocks safely, falls back to Opus pricing for unknown models.
- **`queue.py`** — JSON envelope intake. Picks oldest by mtime; validates `{session_id, timestamp, content}`; rejects malformed envelopes to `queue/.rejected/`. `ack()` deletes after successful dispatch.
- **`dispatch.py`** — Per-session `dispatch/<session_id>/input.txt`. Atomic write via `state.write_atomic`. Won't overwrite a pending dispatch — internal deque per session; flushes when CC consumes (deletes) the previous file.
- **`ntfy.py`** — Best-effort POST to `https://ntfy.sh/<topic>`. Network failures logged, never raised — main loop never blocks on notification delivery.
- **`daemon.py`** — Main polling loop. Each tick: flush deferred dispatches, drain `state/jc_input.txt` (priority), drain queue (one entry/tick), heartbeat for stuck-queue alerts. `[NEEDS-JC]` detection in first 200 chars pauses the loop and ntfy-alerts. JC override supports `@session-N:` prefix for direct dispatch without an API call.
- **`__main__.py`** — CLI: `relay run` (daemon), `relay status` (read `state/status.json`), `relay topic` (print ntfy URL). Signal handling: SIGINT/SIGTERM → graceful stop.
### Tests (57 unit + 1 gated real-API)
- `test_state.py` — atomic write durability, instance lock semantics including a concurrent-acquire thread test.
- `test_conversation.py` — append, persist, summarize-with-recent-turns, char counts.
- `test_queue.py` — oldest-by-mtime, envelope validation, .rejected/ routing, ack semantics.
- `test_dispatch.py` — single-pending invariant, per-session independence, queue-and-flush.
- `test_anthropic_client.py` — system-prompt cache marker, cost estimates including cache savings, fallback for unknown models, multi-block concatenation, non-text block handling.
- `test_daemon_loop.py` — full loop with a fake client. Covers: queue → dispatch, `[NEEDS-JC]` pause, `@session-N:` direct dispatch, JC override clears `needs_jc`, summarization triggers at cap, malformed file does not crash loop, dispatch queues on pending input.
- `test_config.py` — env precedence, ntfy auto-generation, inline-comment-as-blank defensive parse, missing API key raises, default `config.yaml` seeded.
- `test_ntfy.py` — header/body shape, non-200 handling, network errors return False.
- `test_real_api.py` — gated `@pytest.mark.real_api`, opt in with `pytest -m real_api`. Sends one Haiku 4.5 round-trip (~$0.0001). Skips cleanly on `credit balance too low` errors so it's honest about why it didn't run.
### Project files
- `pyproject.toml` — `risv3-relay` 0.1.0, Python ≥ 3.10. Pinned deps: `anthropic`, `flask` (reserved for follow-up status endpoint), `requests`, `pyyaml`, `python-dotenv`. Default test invocation excludes `real_api` via `addopts = "-m 'not real_api'"`.
- `config.yaml` — auto-seeded on first daemon run; committed so JC can review/edit the system prompt and summarization prompt.
- `.gitignore` — fixed inherited typo (was excluding `.env.example` and listing `.env` twice).
- `.env.example` — placeholder values + instructions; `.env` itself stays gitignored.
- `README.md` — install / run quickstart, CC-side polling protocol with shell snippet, JC operations cheatsheet.
## Test checklist
- [x] New tests added for new behavior — 57 unit tests across 9 modules.
- [x] All tests pass locally: `pytest` → 57 passed, 1 deselected (real_api). `pytest -m real_api` → 1 skipped (no API credit).
- [x] Manual smoke test performed — see below.
- [x] No new TODO/FIXME comments without a linked issue.
- [x] No secrets in diffs (`.env` stays gitignored; `.env.example` carries placeholders only).
## Smoke test performed
Booted `relay run` against a real `.env`. The daemon:
1. Generated a fresh ntfy topic on first start and persisted it to `.env` (subsequent runs reuse it).
2. Printed the announcement banner with the subscription URL.
3. Acquired the `state/.lock` via flock; a concurrent boot would refuse to start (covered by unit test).
4. Polled `queue/` and `state/jc_input.txt` at 1 Hz with empty inputs (no API calls, no costs).
5. Wrote `state/status.json` each tick. `relay status` and `relay topic` round-trip cleanly.
I did NOT run a live API turn end-to-end — the test API key has zero credit balance and returns 400 on any `messages.create`. The `real_api` smoke test detects that and skips, and the `daemon_loop` integration suite covers the full path with a fake client (queue → API → dispatch, including the summarization trigger).
## Schema changes
- [x] None (no DB).
## Multi-AI review
Skipped octopus review for this first PR — it's all greenfield code with comprehensive unit + integration coverage and the spec was JC's. Will reinstate octopus reviews on follow-up PRs that touch security-sensitive paths (status endpoint exposure, systemd integration, multi-tenant config).
## Screenshots
N/A — backend daemon, no UI.
## Out of scope (follow-up PRs)
- Flask status endpoint (`flask` already a dependency for this).
- Multi-session config in real use.
- Exponential backoff on transient API errors.
- systemd unit file.
- Cost-tracking aggregation / dashboard.
First-PR scope from #1. Single-process Python daemon that relays
between Claude Code instances and chat-Claude (Anthropic API).
Components:
* relay.config — .env + config.yaml loader. Auto-generates ntfy
topic on first run and persists it back to .env.
* relay.state — atomic file I/O via tempfile + rename, advisory
flock at state/.lock to enforce single-instance.
* relay.conversation — append-only history with summarization.
Triggers a summarize call when total chars exceed
HISTORY_CHAR_CAP (default 400k); replaces history with the
summary plus the most recent 10 turns.
* relay.anthropic_client — SDK wrapper. Marks the system prompt
cacheable (5-min ephemeral cache); concatenates text blocks;
estimates per-call cost from the Anthropic price table with
cache-write/read accounted for.
* relay.queue — JSON envelope intake; oldest-by-mtime;
malformed envelopes moved to queue/.rejected/.
* relay.dispatch — one-input-at-a-time per session
(dispatch/<session_id>/input.txt). Won't overwrite a pending
dispatch; queues internally and waits for CC to delete.
* relay.ntfy — best-effort POST to https://ntfy.sh/<topic>;
failures logged but never block the main loop.
* relay.daemon — main loop. Polls jc_input.txt (priority) then
queue/. Detects [NEEDS-JC] in the first 200 chars of any
response and pauses dispatch until JC writes jc_input.txt.
JC override supports @session-N: prefix for direct dispatch
without an API call.
* relay.__main__ — CLI: relay run / relay status / relay topic.
Tests: 57 unit tests pass (config, state, conversation, queue,
dispatch, anthropic_client, ntfy, full daemon loop with a fake
client). One real-API smoke test marked real_api, opt-in via
pytest -m real_api; skips cleanly on credit-balance errors.
Out of scope for this PR (deferred to follow-ups): Flask status
endpoint, multi-session config in production, exponential
backoff, systemd unit, cost-tracking aggregation.
Closes#1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closing without merging per JC review on 2026-05-02. The cost-monitoring overhead and runaway-loop risk outweigh the time savings for this workflow scale; the behavioral changes that close most of the friction do not require infrastructure. Branch feat/1-daemon-skeleton is preserved for future reference if circumstances change.
Closing without merging per JC review on 2026-05-02. The cost-monitoring overhead and runaway-loop risk outweigh the time savings for this workflow scale; the behavioral changes that close most of the friction do not require infrastructure. Branch `feat/1-daemon-skeleton` is preserved for future reference if circumstances change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
First-PR scope from #1. Single-process Python daemon that relays between Claude Code instances and a chat-Claude session via the Anthropic API. CC drops JSON envelopes into
queue/; the daemon appends to a running history, calls the API (system prompt cached), and either dispatches the response back to the originating session or pauses on[NEEDS-JC]and notifies via ntfy.sh.Issue
Closes #1.
AI tier
tier/2-review. Long-lived process; touches secrets; sends data to a third-party API.Changes
relay/package (8 modules)config.py—.env+config.yamlloader. Auto-generates a 16-charsecrets.token_urlsafentfy topic on first run and persists back to.env. Defensive against the commonKEY= # inline commentdotenv parse trap. Seeds a defaultconfig.yaml(system prompt + summarization prompt + onesession-1) if missing.state.py— Atomic file I/O (tempfile +os.replace);fcntl.flock-based instance lock atstate/.lockso a second daemon refuses to start.conversation.py— Append-only history with summarization. Triggers when total chars exceedHISTORY_CHAR_CAP(default 400k); replaces history with the summary turn plus the most recent 10 turns.anthropic_client.py— Single SDK wrapper. Marks the system prompt cacheable (cache_control: ephemeral). Estimates per-call cost from a model→price table (Opus 4.7, Opus 4.7-1m, Sonnet 4.6, Haiku 4.5) including cache-write/read rates. Concatenatestextblocks, ignorestool_useblocks safely, falls back to Opus pricing for unknown models.queue.py— JSON envelope intake. Picks oldest by mtime; validates{session_id, timestamp, content}; rejects malformed envelopes toqueue/.rejected/.ack()deletes after successful dispatch.dispatch.py— Per-sessiondispatch/<session_id>/input.txt. Atomic write viastate.write_atomic. Won't overwrite a pending dispatch — internal deque per session; flushes when CC consumes (deletes) the previous file.ntfy.py— Best-effort POST tohttps://ntfy.sh/<topic>. Network failures logged, never raised — main loop never blocks on notification delivery.daemon.py— Main polling loop. Each tick: flush deferred dispatches, drainstate/jc_input.txt(priority), drain queue (one entry/tick), heartbeat for stuck-queue alerts.[NEEDS-JC]detection in first 200 chars pauses the loop and ntfy-alerts. JC override supports@session-N:prefix for direct dispatch without an API call.__main__.py— CLI:relay run(daemon),relay status(readstate/status.json),relay topic(print ntfy URL). Signal handling: SIGINT/SIGTERM → graceful stop.Tests (57 unit + 1 gated real-API)
test_state.py— atomic write durability, instance lock semantics including a concurrent-acquire thread test.test_conversation.py— append, persist, summarize-with-recent-turns, char counts.test_queue.py— oldest-by-mtime, envelope validation, .rejected/ routing, ack semantics.test_dispatch.py— single-pending invariant, per-session independence, queue-and-flush.test_anthropic_client.py— system-prompt cache marker, cost estimates including cache savings, fallback for unknown models, multi-block concatenation, non-text block handling.test_daemon_loop.py— full loop with a fake client. Covers: queue → dispatch,[NEEDS-JC]pause,@session-N:direct dispatch, JC override clearsneeds_jc, summarization triggers at cap, malformed file does not crash loop, dispatch queues on pending input.test_config.py— env precedence, ntfy auto-generation, inline-comment-as-blank defensive parse, missing API key raises, defaultconfig.yamlseeded.test_ntfy.py— header/body shape, non-200 handling, network errors return False.test_real_api.py— gated@pytest.mark.real_api, opt in withpytest -m real_api. Sends one Haiku 4.5 round-trip (~$0.0001). Skips cleanly oncredit balance too lowerrors so it's honest about why it didn't run.Project files
pyproject.toml—risv3-relay0.1.0, Python ≥ 3.10. Pinned deps:anthropic,flask(reserved for follow-up status endpoint),requests,pyyaml,python-dotenv. Default test invocation excludesreal_apiviaaddopts = "-m 'not real_api'".config.yaml— auto-seeded on first daemon run; committed so JC can review/edit the system prompt and summarization prompt..gitignore— fixed inherited typo (was excluding.env.exampleand listing.envtwice)..env.example— placeholder values + instructions;.envitself stays gitignored.README.md— install / run quickstart, CC-side polling protocol with shell snippet, JC operations cheatsheet.Test checklist
pytest→ 57 passed, 1 deselected (real_api).pytest -m real_api→ 1 skipped (no API credit)..envstays gitignored;.env.examplecarries placeholders only).Smoke test performed
Booted
relay runagainst a real.env. The daemon:.env(subsequent runs reuse it).state/.lockvia flock; a concurrent boot would refuse to start (covered by unit test).queue/andstate/jc_input.txtat 1 Hz with empty inputs (no API calls, no costs).state/status.jsoneach tick.relay statusandrelay topicround-trip cleanly.I did NOT run a live API turn end-to-end — the test API key has zero credit balance and returns 400 on any
messages.create. Thereal_apismoke test detects that and skips, and thedaemon_loopintegration suite covers the full path with a fake client (queue → API → dispatch, including the summarization trigger).Schema changes
Multi-AI review
Skipped octopus review for this first PR — it's all greenfield code with comprehensive unit + integration coverage and the spec was JC's. Will reinstate octopus reviews on follow-up PRs that touch security-sensitive paths (status endpoint exposure, systemd integration, multi-tenant config).
Screenshots
N/A — backend daemon, no UI.
Out of scope (follow-up PRs)
flaskalready a dependency for this).Closing without merging per JC review on 2026-05-02. The cost-monitoring overhead and runaway-loop risk outweigh the time savings for this workflow scale; the behavioral changes that close most of the friction do not require infrastructure. Branch
feat/1-daemon-skeletonis preserved for future reference if circumstances change.