Skip to content

Hexagonal remake: src/poe2_rpc/ package + Typer CLI + 143 tests#10

Open
PrEvIeS wants to merge 17 commits intoezbooz:mainfrom
PrEvIeS:feature/background-launcher
Open

Hexagonal remake: src/poe2_rpc/ package + Typer CLI + 143 tests#10
PrEvIeS wants to merge 17 commits intoezbooz:mainfrom
PrEvIeS:feature/background-launcher

Conversation

@PrEvIeS
Copy link
Copy Markdown

@PrEvIeS PrEvIeS commented May 5, 2026

Summary

Full architectural remake of Path-Of-Exile-2-RPC from a single-file main.py runtime into a hexagonal Python package (src/poe2_rpc/) with explicit domain / application / infrastructure / cli layers, a Typer CLI entrypoint (poe2-rpc), pydantic v2 frozen value objects, structlog structured logging, tenacity split-retry, watchdog event-driven log tailing, and a 143-test pytest suite.

The current main.py is preserved verbatim as the upstream-compatible runtime, so this PR is purely additive to the existing user-visible surface — the legacy python main.py invocation keeps working unchanged.

Relationship to PRs #6#9

This PR is a companion to the four small, focused upstream PRs already open against main.py:

PRs #6#9 carry the same four features re-encoded for the single-file form. This PR is not a substitute for them — it offers the architectural target if you want to migrate the runtime to a package layout. Merge order is fully your call:

What's included

Hexagonal package (src/poe2_rpc/)

Layer Modules
domain/ Frozen pydantic VOs (LevelInfo, InstanceInfo), domain events (CharacterLevelChanged, AreaEntered, AFKStatusChanged, LocalAreaEntered, PartyJoined), runtime-checkable Protocols (GameDetector, LogStream, LogParser, PresencePublisher, EventBus, LocationCatalogPort, Settings), OwnerTracker state machine, CharacterClass / ClassAscendency enums, LocationCatalog.
application/ Orchestrator (composes pipeline + sync close-stream stop signal), InMemoryEventBus, PresenceThrottle, level/area/AFK handlers with structlog bind_contextvars.
infrastructure/ PsutilGameDetector (Steam + official client), WatchdogLogStream (event-driven, asyncio-bridged), RegexLogParser (regexes verbatim from main.py:273-274), PypresencePublisher (tenacity split-retry + AFK small-image override), AppSettings (pydantic-settings), bundled-catalog loader (importlib.resources), structlog config, optional pystray tray + pylnk3 Windows Startup shortcut.
cli.py Typer app — run, once, validate-config, tray, install-autostart, uninstall-autostart. Composition root + _SyncLineIterator adapter bridging async watchdog → sync LogStream Protocol.

Architectural enforcement

  • import-linter config in pyproject.toml enforces domain ← application ← infrastructure ← cli.
  • tests/unit/test_no_mutable_state.py AST guard fails CI if any non-frozen pydantic model or dataclass lands in domain/.
  • tests/unit/test_layering.py AST guard fails CI on unauthorized cross-layer imports.
  • mypy --strict clean across src/poe2_rpc.

Tests (143 passing)

tests/unit/         — per-module unit + AST guards
tests/integration/  — Typer CLI surface, real regex against captured Client.txt, bundled-catalog roundtrip, cold-start benchmark

Packaging

  • pyproject.toml exposes poe2-rpc console script and [tray] extra (pystray, Pillow, pylnk3).
  • PathOfExile2DiscordRPC.spec updated to bundle src/poe2_rpc/locations.json via PyInstaller --onefile so the existing release artifact keeps working.
  • .github/workflows/build.yml runs the full lint+test gate on Ubuntu, then PyInstaller build + deep-smoke + cold-start benchmark on Windows.

Documentation

  • Hierarchical AGENTS.md files at every directory (root + src/poe2_rpc/ + per-layer + tests/ + .github/) with <!-- Parent: --> references for navigability.
  • CLAUDE.md project instructions.
  • README.md / README.ru.md / README.ua.md updated to document the new CLI commands and tray/autostart workflow; legacy python main.py instructions preserved.

Why this is a non-trivial proposal

The hexagonal layout is much more code than the single-file runtime — about 7.8k lines added. The trade-off:

  • Costs: more files to navigate, larger surface to maintain, stricter typing/lint requirements, and a build job that takes longer because of the test gate.
  • Benefits: every adapter is swappable behind a Protocol (huge for testing — the orchestrator E2E test runs without psutil/watchdog/pypresence), retries/throttle/AFK-state are isolated and unit-tested, structured logs carry pipeline context through bind_contextvars, and adding a feature like the four in Support official PoE2 client (PathOfExile.exe) alongside Steam build #6Run as background service: --tray, --install-autostart #9 becomes "implement the Protocol, wire it in cli.py, write tests" instead of editing main.py carefully.

If you'd rather keep main.py as the canonical runtime and merge only #6#9, this PR can be closed without prejudice — the four small PRs stand on their own. If you'd like the package as the canonical runtime going forward, this PR is the migration.

Happy to split this into stacked PRs (one per layer) if that's easier to review.

Test plan

  • Read through the AGENTS.md hierarchy starting at the repo root for an overview of the layout.
  • Run the gate locally: pip install -e ".[dev]" && pytest tests -ra && mypy --strict src/poe2_rpc && lint-imports && ruff check src tests && ruff format --check src tests
  • Smoke the Typer CLI: poe2-rpc validate-config --no-discord
  • Confirm the legacy entrypoint still works: python main.py
  • (Windows) Live-smoke poe2-rpc run and the tray service: pip install ".[tray]" && poe2-rpc tray

d.shuvalov and others added 17 commits May 4, 2026 22:16
Phase A — scaffolding:
- pyproject.toml (src-layout, py3.11+, ruff/mypy/pytest/import-linter/pydantic.mypy)
- src/poe2_rpc/{domain,application,infrastructure,cli}/ skeleton + py.typed
- tests/ skeleton with conftest fixtures for verbatim regex match lines
- CI split into changes / lint-and-test (ubuntu) / build (windows) / release
  with dorny/paths-filter for build_relevant gating

Phase B — domain layer (TDD, all 33/33 unit tests green, mypy strict clean,
import-linter "Hexagonal layered architecture KEPT"):
- domain/classes.py — CharacterClass + ClassAscendency (verbatim from main.py)
- domain/models.py — frozen pydantic LevelInfo (ascension_class: str | None
  per ADR Behavior Change ezbooz#1) + InstanceInfo
- domain/locations.py — Location VO + LocationCatalog.resolve() ported from
  main.py:163-176
- domain/events.py — frozen DomainEvent hierarchy (GameStarted, GameStopped,
  CharacterLevelChanged, AreaEntered)
- domain/ports.py — 6 runtime_checkable Protocols (GameDetector, LogStream,
  LogParser, PresencePublisher, EventBus, LocationCatalogPort)
- tests/unit/test_layering.py + test_no_mutable_state.py — AST guards over
  src/poe2_rpc/domain/**/*.py

main.py remains the runtime entrypoint; CI build job still gated on
build-relevant paths until Phase E (CLI) and Phase F (PyInstaller) close.

Consensus artifacts (.omc/specs, .omc/plans) checked in for traceability.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 8 infrastructure adapters + 1 domain exception, all gated behind
70/70 unit tests, mypy --strict, and import-linter "Hexagonal layered
architecture KEPT" (1/1 contract).

Adapters:
- C-1 settings.py     -- pydantic-settings AppSettings, env+TOML+init precedence
- C-2 logging.py      -- structlog with ConsoleRenderer/JSONRenderer + bind_contextvars
- C-3 detection.py    -- PsutilGameDetector (DI'd process_iter for testability)
- C-4 log_stream.py   -- WatchdogLogStream honoring the C-4 thread-safety contract:
                        observer thread never touches asyncio.Queue; all enqueues via
                        loop.call_soon_threadsafe; exponential backoff (0.05->0.5s, 2s
                        deadline) for domain lines on QueueFull; non-domain lines drop
                        with rate-limited warn; cursor reset on file rotation.
- C-4b exceptions.py  -- PoE2RPCError + LogStreamStalled in domain layer
- C-5 parsing.py      -- regex byte-identical to main.py:273-274 (Principle 5)
- C-6 catalog.py      -- BundledLocationCatalog via importlib.resources;
                        opt-in URL override path, no implicit recovery layer
- C-7a presence.connect -- tenacity 5 x wait_exponential(2,32)
- C-7b presence.publish -- separate 3 x wait_exponential(1,8) -- split policies
                          prevent 3x5=15 amplification (ADR Behavior Change ezbooz#3)

Bundled assets:
- src/poe2_rpc/locations.json copied from repo root (Phase G handles root cleanup)

Beads: panvex-{tqq,ahn,rl4,6jy,562,0d8,mvo,qkl,ne4} closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase D of the DDD/hexagonal migration (epic panvex-enp). Adds the
application layer that wires domain events to infrastructure adapters
via dependency injection (Principle 4: factory injection at composition
root only).

Modules added under src/poe2_rpc/application/:
- bus.py — AsyncioEventBus implementing EventBus Protocol with
  type-keyed subscribe(event_type, handler) dispatch. Handler
  exceptions are isolated via asyncio.gather(return_exceptions=True)
  so one failing handler does not break others.
- throttle.py — PresenceThrottle gating presence updates to one per
  Settings.throttle_window_seconds (default 15s). random_status()
  helper preserves the verbatim status list from main.py:119-132
  (Principle 5: byte-pattern preservation).
- handlers.py — on_level_changed + on_area_entered async handlers.
  Both call structlog.contextvars.bind_contextvars(username=,
  character_class=, area=) BEFORE logging, satisfying AC#7 (every
  log line carries those keys). MutableState threads the latest
  level_info / instance_info between handlers so each can publish
  with the other's last value. Behavior Change ezbooz#1 honored:
  ascension_class None yields a one-segment "(BaseClass - Lvl N)"
  format with no "| Unknown" sentinel.
- orchestrator.py — Orchestrator wiring the runtime: subscribes
  handlers to the bus, drives the LogStream consumer loop, emits
  CharacterLevelChanged / AreaEntered, and connects/closes the
  publisher. Constructor takes only Protocols (EventBus,
  PresencePublisher, Settings) plus a log_stream_factory callable —
  zero infrastructure imports (verified by AST guard in
  test_orchestrator_layering.py).

Protocol drift fixes in domain/ports.py (caught during D-1/D-3 worker
review):
- EventBus.subscribe widened from subscribe(handler) to
  subscribe(event_type, handler) to express type-keyed dispatch.
- PresencePublisher.publish made async to match
  AioPresence-backed implementation.
- Added Handler type alias and Settings Protocol so application code
  can depend on settings shape without importing
  infrastructure.settings.

Tests (15 new, 87/87 total passing):
- tests/unit/test_bus.py — dispatch + exception isolation.
- tests/unit/test_throttle.py — interval gating + random_status.
- tests/unit/test_handlers.py — bind_contextvars assertion via
  structlog.testing.capture_logs(); ascension None formatting.
- tests/integration/test_orchestrator.py — fake log_stream_factory
  end-to-end through bus + handlers + fake publisher.
- tests/unit/test_orchestrator_layering.py — AST walk asserting no
  poe2_rpc.infrastructure imports anywhere in application/.

Exit gates:
- pytest: 87 passed (15 new for Phase D)
- mypy --strict src/poe2_rpc: success, 24 files
- lint-imports: Hexagonal layered architecture KEPT, 1/1 contract

bd: panvex-enp.{1,2,3,4} closed.
Phase E (Typer CLI + composition root) is next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase E of the DDD/hexagonal migration (epic panvex-enp). Wires the
runtime via a Typer-driven composition root that owns adapter
construction; everything below the cli layer continues to see only
domain Protocols (Principle 4).

E-1 — `src/poe2_rpc/cli.py`:
- `app` (Typer) with `--version` callback and three commands.
- `run`: continuous monitor loop until cancelled.
- `once`: single log-stream pass and exit.
- `validate-config --no-discord`: load settings + bundled
  locations.json + configure structlog without contacting Discord IPC.
  Exits 0 with the resolved settings as JSON. Used as the deep-smoke
  step planned for F-3 (CI build job).
- `build_orchestrator(settings) -> Orchestrator` factory extracted
  for testability — patched in unit tests via CliRunner.
- `_SyncLineIterator` adapter in the composition root bridges
  `WatchdogLogStream`'s async queue to the sync `LogStream.lines()`
  Protocol. Owns observer start/stop lifecycle.
- Replaces the empty `cli/__init__.py` stub from Phase B-6 with a
  real module; the directory layer name `poe2_rpc.cli` in the
  import-linter contract continues to resolve correctly.

E-1 also reconciles four Phase D Protocol drifts in the same change
(per the "integrating worker fixes drifts" pattern documented after
Phase D):
- `PresencePublisher.publish` Protocol grew an `async def connect()`
  method to match `PypresencePublisher.connect()`.
- `Orchestrator.run_once()` now calls
  `loop.run_until_complete(self._publisher.connect())` on its
  freshly-created loop before iterating the stream — fixes a latent
  defect where the publisher was never connected before
  `publish()`.
- `infrastructure.parsing` adds a `RegexLogParser` class that
  satisfies the `LogParser` Protocol (the existing module-level
  functions remain).
- `infrastructure.detection.PsutilGameDetector` adds `is_running()`
  and `log_path()` methods matching the `GameDetector` Protocol.
  `log_path()` blocks with a 5s poll loop until the game process is
  found (preserves main.py:88-94 wait-for-game semantics).
- `infrastructure.catalog` adds a `load_bundled_catalog()` helper
  that returns the existing domain `LocationCatalog` populated from
  the bundled `locations.json` via `importlib.resources`.

E-2 — `src/poe2_rpc/__main__.py`:
- Replaces the Phase A stub with `from poe2_rpc.cli import app`
  and `app()` invocation under `if __name__ == "__main__":`.
- Allows `python -m poe2_rpc` as an alternate entry alongside the
  `poe2-rpc` console script declared in `pyproject.toml`.

Tests (7 new, 94/94 total passing):
- tests/integration/test_cli.py — `--version`, `--help`,
  `validate-config --no-discord` (exit 0 + asserts no
  `PypresencePublisher` instantiated), `once`, `run`.
- tests/integration/test_main_module.py — subprocess-driven
  `python -m poe2_rpc --version`.
- tests/unit/test_ports.py + tests/integration/test_orchestrator.py
  — fakes extended with `async def connect` to match the new
  `PresencePublisher` shape.

Exit gates:
- pytest: 94 passed (7 new)
- mypy --strict src/poe2_rpc: success, 24 files
- lint-imports: Hexagonal layered architecture KEPT, 1/1 contract

bd: panvex-{8tv,lsk} closed.
Phase F (PyInstaller spec + bundled assets + CI smoke + cold-start
benchmark) is next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase F of the DDD/hexagonal migration (epic panvex-enp). Wires the
release artifact path: PyInstaller spec is the build entrypoint; the
spec is shipped alongside the bundled `locations.json`, and two new
integration tests guard the asset-resolution and cold-start budgets.

F-1 — `PathOfExile2DiscordRPC.spec`:
- `Analysis(['src/poe2_rpc/__main__.py'], pathex=['src'], ...)`,
  `datas=[('src/poe2_rpc/locations.json', 'poe2_rpc')]` so the
  bundled JSON resolves via `importlib.resources.files('poe2_rpc')`
  inside the .exe (matches `load_bundled_catalog()` from Phase E).
- Expanded `hiddenimports` for the watchdog Windows backend and the
  pydantic-rust-core / pydantic-settings TOML provider / structlog /
  tenacity entrypoints — the seven import paths PyInstaller can't
  discover statically through pydantic v2's plugin chain.
- `collect_submodules()` over pydantic, pydantic_settings, structlog,
  watchdog, tenacity, pypresence — the broad sweep complements the
  explicit hiddenimports list.
- `name='PathOfExile2DiscordRPC'`, `onefile=True`, `console=True` —
  preserves the existing release-asset URL stability constraint.

F-2 — `tests/integration/test_bundled_catalog.py`:
- `test_bundled_locations_json_is_reachable` proves the JSON is
  discoverable through `importlib.resources` after `pip install -e .`
  (uses the existing `[tool.setuptools.package-data]` entry from
  Phase A's pyproject scaffold).
- `test_load_bundled_catalog_resolves_known_area` exercises the
  Phase E `load_bundled_catalog()` helper end-to-end and asserts
  `G1_1 -> "The Riverbank"` (anchors the canonical zone set so a
  silently corrupted bundle is caught at test time, not in CI smoke).

F-3 — already wired in `.github/workflows/build.yml` lines 99-100
(`dist\PathOfExile2DiscordRPC.exe validate-config --no-discord`).
The spec from F-1 unblocks this step; the workflow side requires no
edits. Final acceptance — green CI run on Windows — stays open in
bd until the next push triggers `workflow_dispatch` evidence.

F-4 — `tests/integration/test_cold_start.py`:
- Resolves the binary via `POE2_RPC_EXE` env (CI sets it to
  `dist\PathOfExile2DiscordRPC.exe`); skipped in local dev when the
  exe is absent so `pytest` stays clean without a build.
- Runs `validate-config --no-discord` 5 times, asserts each exits 0
  and computes p95 against the 8s budget. Budget breach fails the
  test; per the plan a breach files a follow-up bd issue and
  annotates release notes — does NOT block the first DDD release.
- The CI step already exists at `build.yml:102-104` with
  `continue-on-error: true` matching the non-blocking policy.

Tests (2 new + 1 skipped, 96/96 + 1 skip total):
- pytest: 96 passed, 1 skipped (cold-start needs the exe)
- mypy --strict src/poe2_rpc: success, 24 files
- lint-imports: contract unchanged (no new domain/application/
  infrastructure code in this commit)

bd: panvex-{98r,6uc} closed. panvex-{73a,9bu} stay open pending the
first CI workflow_dispatch run that produces evidence:
- panvex-73a (F-3): smoke step exits 0 on Windows runner
- panvex-9bu (F-4): p95 cold-start <= 8s recorded in CI logs

Phase G (regex sample test, README/CLAUDE.md updates, delete
main.py blocked-by live smoke, 10-step Windows VM checklist) is
next, but G-2 (delete main.py) cannot close until G-4 records the
live smoke and panvex-00o (catalog.resolve wiring in orchestrator)
ships — both are pre-cutover blockers tracked in bd.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
panvex-00o: Orchestrator now calls LocationCatalog.resolve() between
parser.parse_instance() and bus.emit(AreaEntered(...)), copying the
resolved display_name onto the InstanceInfo via model_copy(). Closes
the pre-cutover blocker — raw internal codes (G1_4_BrambleghastSlain)
are no longer pushed to Discord; the bundled locations.json is the
single source of truth as in main.py:determine_location().

panvex-enn (G-1): tests/integration/test_regex_real_sample.py asserts
both regex_level and regex_instance match >=1 entry in a real
Client.txt sample. Skips with reason when the fixture is absent —
fixture is captured manually during the G-4 Windows live smoke and
locks in regex compatibility once present.

panvex-8kz (G-3): README.md and CLAUDE.md rewritten for the new
src/poe2_rpc/ package. README points at `pip install -e ".[dev]"` +
`poe2-rpc run` and lists CLI subcommands (run/once/validate-config).
CLAUDE.md now describes hexagonal layering, gate suite (pytest,
mypy --strict, ruff, lint-imports), the .spec build, and updated
ascendancy/zone/regex onboarding pointers. BEADS INTEGRATION block
preserved verbatim.

Phase G remaining:
  - panvex-a17 (G-2 delete main.py) — blocked by G-4
  - panvex-12l (G-4 Windows live smoke) — manual VM checklist

Gates: 97 pytest passed + 3 skipped (cold-start without exe,
test_regex_real_sample without fixture); mypy --strict clean across
24 source files; ruff check + ruff format clean on touched files.
Replace lazy imports with module-level imports across tests, rename
LogStreamStalled→LogStreamStalledError per N818, hoist tomllib import
in settings, dedup unused-arg via `_event` and `del` (no rule ignores).

Bump actions/checkout v4→v5 and actions/setup-python v5→v6 (Node 20
deprecation), set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 for paths-filter@v3,
and replace archived create-release@v1 + upload-release-asset@v1 with
softprops/action-gh-release@v2.

Run ruff format on the whole tree to bring previously untouched files
in line with the autoformatter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
infrastructure/catalog.py imports httpx at module level for the
locations_url override path. CI mypy strict failed with
import-not-found because httpx was not in [project.dependencies] —
it only resolved locally via transient install. Declare it
explicitly so CI installs it and the strict typecheck passes.
Previous commit set the env only on the dorny/paths-filter step,
leaving actions/upload-artifact@v4, actions/download-artifact@v4,
and softprops/action-gh-release@v2 still emitting Node 20
deprecation warnings. Hoisting the env to workflow level applies
the Node 24 force to every step in every job in one place.
- actions/checkout@v5 → @v6 (Node 24 native)
- dorny/paths-filter@v3 → @v4 (new ref input, list-files csv)
- actions/download-artifact@v4 → @v5 (Node 24 native)
- softprops/action-gh-release@v2 → @V3 (explicitly Node 24)

actions/upload-artifact stays at @v4 — no v5 yet upstream;
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 retained for that one action.
…ide Steam

`AppSettings.process_name` becomes `list[str]` defaulting to both the Steam
build (`PathOfExileSteam.exe`) and the standalone client (`PathOfExile.exe`).
A `field_validator` + `NoDecode` annotation keeps legacy
`POE2RPC_PROCESS_NAME=Foo.exe` env vars working by promoting the raw string
to a single-element list before validation. `PsutilGameDetector` now matches
the running process by membership in the candidate list.

Refs upstream PR-1 from .omc/plans/ralplan-upstream-prs.md (bd panvex-4gx).
…te machine

Adds an `OwnerTracker` frozen pydantic VO (UNKNOWN -> AREA_ENTERED -> PINNED,
or INVALIDATED if a party member joins inside an unpinned window). The
orchestrator emits two new domain events from the parse loop:

  * `LocalAreaEntered`   <- `: You have entered <area>.`
  * `PartyMemberJoined`  <- `<name> has joined the area.`

Their handlers drive the state machine. `on_level_changed` consults
`owner_tracker.should_emit(li.username)` and drops level events that don't
belong to the local player. The `LogParser` Protocol grows two new
`parse_local_area_entered` / `parse_party_joined` methods (regexes verbatim
from klayveR/poe-log-monitor `resource/events.json`). An optional
`POE2RPC_CHARACTER_NAME` env override short-circuits to PINNED on first
area entry.

Tests cover the full state-machine table (15 cases incl. parametrized party
scenarios + re-entry-resets-INVALIDATED) and add a runtime-checkable
Protocol conformance test for `RegexLogParser`.

Refs upstream PR-2 from .omc/plans/ralplan-upstream-prs.md (bd panvex-7w5).
…verride

On the AFK ON line, the handler snapshots the current small_image (derived
from the current LevelInfo) so a level-up that happens during the AFK window
cannot leak its new icon when AFK turns OFF. Restore semantics use a single
small_image_override path through PypresencePublisher so the snapshot wins
over the recomputed default.

- domain/models: AFKStatus frozen VO (mode: Literal["AFK","DND"], on, autoreply)
- domain/events: AFKStatusChanged
- domain/ports: LogParser.parse_afk_event; PresencePublisher.publish gains
  afk_on + small_image_override kwargs (defaults preserve back-compat)
- infrastructure/parsing: regex_afk + parse_afk_event_line +
  RegexLogParser.parse_afk_event
- infrastructure/presence: [AFK] suffix on state, override replaces small_image
- application/handlers: MutableState gains afk_on/prior_small_image; new
  on_afk_changed handler captures snapshot on ON, restores on OFF
- application/orchestrator: subscribe AFKStatusChanged + parse-loop branch
- tests/unit/test_afk.py: 9 tests (4 parametrized parse + 1 none + 2 kwargs +
  restore-after-level-during-AFK + AFK-without-prior-level)
PR-4 of the upstream campaign. Adds an opt-in tray icon and a Startup-
folder shortcut so the orchestrator can run as a background service.

Hexagonal changes:

- pyproject.toml: new [tray] extras (pystray>=0.19, Pillow>=10, pylnk3>=0.4);
  matching mypy ignore_missing_imports overrides; project-wide PLC0415 ruff
  ignore (deferred imports are the architectural extras-gate, not a smell).
- domain/ports.py: LogStream Protocol gains close() + is_closed() so the
  tray Quit thread can unblock the sync for-line loop.
- infrastructure/log_stream.py: thread-safe idempotent close() that stops
  the watchdog observer and posts an empty sentinel via call_soon_threadsafe
  to wake the queue-bound consumer.
- infrastructure/tray.py: TrayController wrapping pystray.Icon with
  Status/Open log/Restart/Quit menu and a default 64x64 PIL icon when no
  icon file is supplied. Module-level import gate raises a clear hint to
  install the [tray] extras.
- infrastructure/autostart.py: pylnk3-backed Startup-folder shortcut at
  %APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup. Idempotent
  install + boolean uninstall.
- application/orchestrator.py: tracks _current_stream, wraps the line loop
  in try/finally so close() always runs, filters the empty sentinel, and
  exposes a thread-safe stop() that closes the active stream.
- cli.py: tray / install-autostart / uninstall-autostart commands; tray
  spawns the orchestrator on a background thread and wires Quit to
  orch.stop() THEN icon.stop() (drain order matters); _SyncLineIterator
  now forwards close()/is_closed() so cli adapter respects the new
  Protocol.

Tests (+15):

- test_orchestrator_stop.py: threaded run + main-thread stop joins within
  1s; double-close is idempotent; stop with no active stream is a noop.
- test_tray.py: menu wiring, status propagation, quit-callback ordering.
- test_autostart.py: install/uninstall idempotency, frozen exe path
  resolution; uses sys.modules injection so pylnk3 is not required in dev.
- test_extras_missing.py: tray + install-autostart commands exit 1 with
  the pip install hint when extras are absent.

Docs:

- README.md: new "Run as a background service" section with the explicit
  pip install poe2-rpc[tray] prereq before poe2-rpc tray, plus the
  install-autostart / uninstall-autostart pair; flips the matching to-do.

Gates: 143 passed (+15), mypy --strict clean, ruff clean, ruff format
clean, lint-imports kept (1 contract).

Refs: epic panvex-b6p, plan .omc/plans/ralplan-upstream-prs.md
…in EN/RU/UA

The 4-PR upstream campaign (panvex-b6p) is feature-complete on this stacked
branch. Updates the user-facing READMEs and CLAUDE.md to reflect the four
shipped capabilities:

- Official PoE2 client support (Steam + PathOfExile.exe)
- Owner auto-pin (party-mate disambiguation)
- AFK / DND status with small-image override + restore
- Background tray service + Windows Startup shortcut

README.md / README.ru.md / README.ua.md
- Add explicit Features bullets for all four capabilities.
- Flip the To-Do checklist (now 5/5 complete).
- RU/UA translations get the new "Run as a background service" section
  to match the English README that was added during PR-4.

CLAUDE.md
- Replace the four open-work items with checked-off equivalents and a
  pointer to the upstream draft PRs (ezbooz#6, ezbooz#7, ezbooz#8, ezbooz#9). Notes that the
  remaining work is the end-of-campaign Windows live-smoke pass.
Root AGENTS.md was stamped 2026-05-04 and still described the single-file
main.py runtime — completely stale after the G-phase migration to
src/poe2_rpc/. Refreshed via the deepinit skill:

- Root: dual-form (hexagonal + upstream-form main.py for backports), full
  layering rules, build/test gate, upstream PR campaign pointer; preserves
  the BEGIN BEADS INTEGRATION block verbatim.
- src/poe2_rpc/AGENTS.md (new): hexagonal package overview + composition
  root, _SyncLineIterator bridge, optional-extras gate.
- src/poe2_rpc/domain/AGENTS.md (new): frozen pydantic VOs, Protocol ports,
  ascendancy-add workflow.
- src/poe2_rpc/application/AGENTS.md (new): orchestrator + bus + throttle
  + handlers; sync close-stream design.
- src/poe2_rpc/infrastructure/AGENTS.md (new): all 11 adapter modules,
  optional-deps idiom (module-top vs lazy-inside), tenacity split-retry.
- tests/AGENTS.md, tests/unit/AGENTS.md, tests/integration/AGENTS.md
  (new): test layout + AST-guard inventory + Typer CLI surface.

All non-root files carry <!-- Parent: ../AGENTS.md --> (../../AGENTS.md
for src/poe2_rpc/) and a <!-- MANUAL: --> preservation marker.
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