Use this skill when building, reviewing, or refactoring Python code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, strict typing, architecture review, and clean project structure. Complements the project's Python AGENTS.md with process rigor.
This skill complements AGENTS.md, .codex/rules/python-patterns.md, and .codex/rules/python-idioms.md. It must stay aligned with all three and applies as an execution discipline layer on top of the project's Python standards.
This skill adds execution rigor:
ATDD/TDD workflow
SRP, DRY, and OCP decision rules
explicit dependency injection discipline with a pragmatic carve-out for CLI composition roots
strict type discipline
comment quality standards
structured implementation and review checks
hermesd-specific invariants (cache-preservation, read-only access, panels as OCP seam)
If AGENTS.md is stricter on any point, follow AGENTS.md.
implementing a new feature or behavior increment
refactoring Python code for clearer ownership or testability
reviewing module boundaries or dependency flow
replacing hidden collaborator construction with explicit injection
tightening tests around user-visible or integration behavior
removing Any types or tightening weak types in touched code
cleaning up hardcoded values or global mutable state
Inspect the project first.
Read pyproject.toml, project layout, local AGENTS.md, CONTRIBUTING.md, CHANGELOG.md, tool configs ([tool.ruff], [tool.mypy], [tool.pytest.ini_options]), and existing tests.
Define acceptance behavior first.
Express the user-visible outcome before writing implementation details.
Add or update an acceptance-level test when the project has that layer.
For hermesd, the acceptance seam is Collector → DashboardState → panel renderer. Use rich.console.Console(record=True) to capture output for end-to-end render assertions.
Add the next smallest failing test.
Prefer a focused unit or module test for the next behavior increment.
Implement the minimum change that makes the test pass.
Keep the diff tight. Do not rewrite unrelated code.
Refactor while green.
Improve naming, cohesion, dependency flow, and readability without changing behavior.
Keep standards and user-facing docs in sync.
If the task materially changes project conventions, architecture, or workflow expectations, update AGENTS.md or the relevant rule/skill in the same change. Update CHANGELOG.md for user-visible changes and README.md when install/usage instructions change.
Verify locally before opening a PR.
Run uv run ruff check ., uv run ruff format --check ., uv run mypy hermesd, uv run pytest tests/ -q -ra --tb=short -W error::ResourceWarning --cov=hermesd --cov-report=term-missing, uv run pip-audit, uv lock --check, uv build, wheel smoke installs, and uvx twine check dist/*. CI runs the same gate commands across Python 3.11/3.12/3.13/3.14.
Keep project structure clean and predictable
SRP: each module, class, and function should have one clear reason to change
DRY: remove repeated validation, mapping, branching, and policy logic when the abstraction improves clarity
OCP: extend behavior through composition, Protocols, configuration, and strategy injection instead of invasive branching or copy-paste forks
Prefer domain-oriented module boundaries over technical dumping grounds
Keep domain logic separate from transport, persistence, configuration, and presentation concerns
Prefer the smallest coherent abstraction that solves the real duplication or extension point
Do not introduce Protocol-first abstractions without real consumer pressure
Prefer composition over inheritance; keep inheritance hierarchies shallow
OCP example in hermesd.hermesd/panels/__init__.py is the canonical OCP seam: add a new panel by writing hermesd/panels/your_panel.py, adding a _render_your_panel(ctx: PanelRenderContext) wrapper, then registering that wrapper in _RENDERERS and the label in PANEL_NAMES. This pattern is documented in CONTRIBUTING.md#Adding a New Panel.
General rules:
Use constructor injection (__init__ parameters) for long-lived collaborators
Use function parameters for short-lived collaborators and pure logic inputs
Pass dependencies through typed config, constructors, or arguments
Do not instantiate external clients, repositories, clocks, or runtime collaborators inside core domain logic
Do not hardcode dependency selection, URLs, ports, credentials, or feature switches
Avoid globals, module-level mutable state, and hidden singletons
Composition-root carve-out. The composition root — typically the main() entry point or a single top-level CLI class — is allowed to construct collaborators directly. In hermesd, __main__.main() and DashboardApp.__init__ form this root: they may call Collector(hermes_home) and load_theme(hermes_home) without those being injected parameters. Below the composition root, follow the general DI rules strictly — collectors, renderers, and panel functions must not construct their own DB readers, file readers, clocks, or theme loaders.
When testing the composition root itself, prefer constructing it with a fake hermes_home directory (see tests/conftest.py::hermes_home) rather than mocking the inner collaborators.
Python-specific guidance:
Prefer Protocols at dependency boundaries for structural typing
Use dataclasses or Pydantic models for typed configuration
pytest fixtures are the natural DI mechanism in tests — prefer fixtures over monkeypatch
Use functools.partial or closures for lightweight function-level injection
Avoid DI frameworks; explicit wiring is preferred
# hermesd-shaped constructor injection — domain logic stays testable
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
class DatabaseReader(Protocol):
def read_sessions(self) -> list[dict[str, object]]: ...
def read_tool_stats(self) -> list[dict[str, object]]: ...
class Clock(Protocol):
def now(self) -> float: ...
@dataclass(frozen=True, slots=True)
class SessionSummarizer:
db: DatabaseReader # injected, not constructed inside
clock: Clock # no time.time() inside this class
def summarize(self) -> dict[str, object]:
rows = self.db.read_sessions()
return {"count": len(rows), "as_of": self.clock.now()}
# function parameter for short-lived/pure logic
def within_budget(cost: float, now: float, budget_reset_at: float) -> bool:
return now >= budget_reset_at or cost < 10.0
Use threading.Lock/RLock for shared mutable state; name the attribute _*_lock and document what it guards.
Use threading.Event for cross-thread signaling (e.g., _force_refresh in DashboardApp). Prefer Event.wait(timeout=...) over sleep loops.
Daemon threads (daemon=True) must not hold open resources through shutdown. Close explicitly in a finally: block on the main thread.
Signals only deliver to the main thread. Register signal.signal(SIGINT, handler) on the main thread; daemon threads cannot receive signals. Signal handlers must be fast and set flags only — never call into Rich or SQLite from a handler.
Any SQLite connection shared across threads must be created with check_same_thread=False. See hermesd/db.py.
Never raise across thread boundaries without propagation — the collector thread in app.py::_collector_loop swallows exceptions into is_stale=True on purpose. Mirror that pattern for any new background thread.
When a new background task is added, require an explicit cancellation path (an Event or a self._running flag the thread checks every iteration).
Flag shared mutable state immediately; push toward immutable data copies or message passing through a queue.
For any future async code (not applicable to current hermesd):
Prefer asyncio.TaskGroup (3.11+) for structured concurrency over bare create_task.
Use asyncio.gather(return_exceptions=True) when all tasks must complete regardless of failures.
Never nest asyncio.run() calls; one event loop per thread; prefer asyncio.run() or asyncio.Runner as the single entry point.
Never mix sync and async I/O in the same code path without run_in_executor.
Cache last-good data. Every reader (SQLite, JSON, YAML, log file) caches the last successful read and returns it on transient errors. See hermesd/db.py::HermesDB._cached_* and hermesd/collector.py::_read_json_cached.
Count consecutive errors. After N (currently 3) consecutive failures, reconnect/reload rather than retrying blindly. See HermesDB._consecutive_errors.
Never blank the display. If a panel has no data, show "no data" text styled for the theme — do not raise or return None for the renderable.
NULL tolerance. SQLite column reads use or 0 / or "" coalescence, not dict.get(key, default). This is a AGENTS.md critical rule.
Read-only always. Never open ~/.hermes/*.db without ?mode=ro. Never write files, create directories, or mutate data under ~/.hermes/.
Do not import hermes-agent. Read the on-disk formats directly.
Keep the code easy to read and maintain.
Write modern Python and prefer 3.11-safe idioms; see .codex/rules/python-idioms.md for version-tagged patterns
Put from __future__ import annotations at the top of every module (project convention)
Keep functions short, explicit, and focused on one job
Use consistent formatting and indentation; ruff format defines layout (4 spaces, 100-char lines per [tool.ruff])
Use whitespace to separate concepts, not decorate code
Use meaningful whitespace: blank lines between logical sections within functions, two blank lines between top-level definitions
Keep naming concrete and intention-revealing; snake_case for functions/variables, PascalCase for classes
Prefer straightforward control flow over clever compression
Early returns over deep nesting; guard clauses at the top
Separate logic clearly so domain rules, I/O, transport, persistence, and orchestration are easy to trace
Avoid hardcoded values; move runtime values and environment behavior into typed config, constants, or inputs
Explicit imports; no import * outside __init__.py re-exports
Inline function-level imports are allowed when they break an import cycle or defer heavy startup cost (see hermesd/__main__.py::main deferring DashboardApp import). Prefer top-of-module imports otherwise.
[tool.mypy] in pyproject.toml is the source of truth for enforced strictness
Every public function has explicit parameter and return type annotations
Minimize Any in domain code; use object, Protocol, generics, Union, or narrower types
Boundary-Any policy (convention, not mypy-enforced).[tool.mypy] never sets disallow_any_explicit, so explicit Any is currently legal in every module; the hermesd.db per-module override (disallow_any_explicit = false) is a no-op today and only becomes meaningful if the base flag is ever enabled. By convention, Any stays at the untyped boundary: raw SQLite rows (hermesd/db.py, hermesd/collect/sqlite_util.py) and parsed JSON/YAML/state data (hermesd/collect/*, hermesd/collector.py). Translate into typed Pydantic models at the earliest reasonable point — hermesd/models.py and hermesd/panels/* contain no Any, and new Any should not be introduced past the collector. Enabling disallow_any_explicit = true repo-wide currently reports 150+ violations across the boundary modules; tightening to enforcement is aspirational and would require narrowing the boundary types first.
NewType for domain IDs and values that should not mix: UserId = NewType("UserId", int) — plain assignment, not the type statement
Protocol for dependency boundaries; enables structural typing without inheritance coupling
Literal for constrained value sets; Final for immutable module-level bindings
# type: ignore[code] requires a specific error code and inline justification — never bare # type: ignore
Avoid cast() unless unavoidable; prefer narrowing with isinstance or restructuring
Make invalid states unrepresentable with enums, dataclasses with validation, and constrained constructors
Keep weakly typed data at the boundary and translate into strict internal types immediately
Version-specific type syntax (see .codex/rules/python-idioms.md for full details):
@override (3.12+) — requires typing_extensions import on 3.11; do not use unconditionally while the project supports 3.11
TypeIs (3.13) — requires typing_extensions on 3.11/3.12; prefer TypeGuard in library code supporting 3.11
Inline generic syntax def fn[T](...) (3.12+) — SyntaxError on 3.11; use the old TypeVar("T") form in library code
ATDD first for user-visible changes: define the acceptance scenario before writing implementation
Express the boundary behavior (Given/When/Then or equivalent scenario)
Add or update the acceptance-level test
Write the next smallest failing unit test
Implement the minimum change that makes it pass
Refactor while green
Repeat for the next behavior increment
TDD for the next increment: write the smallest failing unit test, implement, then refactor
Use ATDD extensively for user-visible behavior, acceptance flows, and cross-boundary scenarios
Every meaningful change should cover:
expected behavior (happy path)
invalid input and validation failures
edge cases and boundary values
error and failure paths
Use pytest with fixtures; prefer fixtures over monkeypatch for dependency injection
@pytest.mark.parametrize for table-driven tests; direct narrative tests when variation is not the point
Fakes and stubs over mocks; test behavior not implementation
Use tmp_path for filesystem tests; time_machine or freezegun for time-dependent tests
Keep public API doctests accurate when behavior changes
hermesd test taxonomy. Tests live in a flat tests/ directory with file-prefix categories:
Prefix
Scope
test_models.py
Pydantic model construction, field defaults
test_db*.py
SQLite reader, caching, resilience
test_collector*.py
data collection from ~/.hermes/
test_*_panel.py
panel rendering (compact + detail)
test_app*.py
TUI key handling, layout, lifecycle
test_*_resilience.py
error handling, cache preservation
Add new tests to the matching prefix. Use tests/conftest.py fixtures (hermes_home, sample_db, populated_hermes_home) rather than hand-rolling setup.
Acceptance seam. For user-visible changes, assert against a full Collector → DashboardState → render_panel flow using rich.console.Console(record=True) to capture output. See existing test_*_resilience.py files for the pattern.
# hermesd-shaped fixture + sync test (no async — project is threading-based)
@pytest.fixture
def collector(hermes_home: Path) -> Collector:
return Collector(hermes_home)
def test_collector_returns_empty_state_when_home_is_bare(collector: Collector) -> None:
state = collector.collect()
assert state.gateway.running is False
assert state.sessions == []
def test_collector_preserves_cache_on_db_corruption(
collector: Collector,
sample_db: Path,
) -> None:
first = collector.collect()
sample_db.write_bytes(b"corrupt")
second = collector.collect()
assert second.sessions == first.sessions # cache-preservation invariant
assert second.is_stale is False # collector thread sets is_stale, not collect()
Good comments explain:
intent and rationale for non-obvious design choices
invariants and constraints
ownership or concurrency rules
non-obvious tradeoffs or performance considerations
why a particular approach was chosen over alternatives
Do not write comments that:
restate the code
narrate simple assignments or obvious operations
explain standard Python syntax
leave vague TODOs without context or ticket reference
duplicate the docstring with less precision
Docstrings (hermesd convention):
Use short, single-line docstrings ("""Description.""") for public functions and fixtures
Omit docstrings on private helpers, test functions, and obvious one-liners
When the function has a complex contract, a short multi-line docstring is acceptable — match the neighbouring style
Do not adopt Google/NumPy-style blocks; the project has not historically used them
Update docstrings when exported behavior, config, or API semantics change
Raise specific exceptions with context; never bare except: — it catches SystemExit and KeyboardInterrupt
except Exception: without re-raise is acceptable only at these boundary handlers:
CLI entry point (hermesd/__main__.py::main)
Collector and input threads (hermesd/app.py::_collector_loop, _input_loop)
Exception-message sanitizer (hermesd/collect/redaction.py::_safe_exception_text) — must remain non-throwing while handling another error; return only the exception type if sanitization fails
Signal handlers
Everywhere else, re-raise or handle specifically.
raise NewError("context") from original_err to preserve cause chains
Custom exception hierarchies for domain errors; stdlib exceptions for programming errors
contextlib.suppress(SpecificError) only for known-safe cases with clear justification
Keep error messages actionable and specific enough to debug
Never log secrets, tokens, credentials, or sensitive payloads
Use ExceptionGroup and except* (3.11+) when multiple independent errors should be reported together
Return None or Optional only when absence is a valid, documented part of the contract
Do not use sentinel values when an exception or Optional return would be clearer
Run uv run ruff format --check . (or uv run ruff format . to fix)
Run uv run ruff check . (with --fix for auto-fixable issues)
Run uv run mypy hermesd (configured via [tool.mypy] in pyproject.toml)
Run uv run pytest tests/ -v -W error::ResourceWarning (with relevant paths for tighter affected-scope checks)
Run uv run pip-audit; CI runs it on every push
Run uv lock --check; if dependencies changed, run uv lock and verify the diff
Run uv build, smoke install dist/hermesd-*.whl, and run uvx twine check dist/* before release or packaging-affecting changes
If public API changed, verify docstrings and update README/CHANGELOG if user-facing
Treat linting and static analysis as normal development tools, not release-only checks
Fix root causes instead of scattering # noqa or # type: ignore comments
If a lint rule is intentionally suppressed, use the specific code and add a justification comment
For local enforcement, pre-commit is a reasonable option — add a .pre-commit-config.yaml running ruff + mypy on staged files if the team wants it
AGENTS.md#Critical Rules project invariants hold (read-only, no hermes-agent imports, cache preservation, NULL tolerance, escape-sequence bulk reads)
module boundaries are coherent — no cross-domain leaks
responsibilities are not mixed across domain, transport, persistence, and config
structure is clean, predictable, and free of dumping-ground modules
dependencies are injected explicitly below the composition root — no hidden construction or global state
no hardcoded runtime values (URLs, ports, credentials, paths, timeouts)
types follow <type_discipline> — Any stays at the documented SQLite and parsed-data boundaries, no bare # type: ignore
every public function has explicit parameter and return type annotations
functions are short, focused, and readable in one pass
formatting, indentation, and whitespace follow ruff format defaults
comments explain intent or invariants instead of restating the code
error handling is clear, concise, contextual, and uses cause chains
no bare except:; except Exception: only at documented boundary handlers; no swallowed errors; no mutable default arguments
tests cover acceptance behavior and unit behavior; both new tests were written first
TDD/ATDD flow was followed
threading: locks, events, daemon-thread shutdown, and signal handlers follow <threading_rules>
resilience: cache-preservation, consecutive-error counting, and "never blank the display" hold per <resilience_rules>
public API docstrings are accurate and updated when behavior changed
CHANGELOG.md updated for user-visible changes; README.md updated for install/usage changes
the full local gate from <tooling_rules> passes locally
version/tooling guidance from AGENTS.md, .codex/rules/python-idioms.md, and .codex/rules/python-patterns.md has been followed
giant functions mixing validation, orchestration, and persistence
Protocol-per-class abstraction without consumer need
hardcoded configuration or collaborator construction below the composition root
Any added or left in touched code without explicit justification or the documented boundary carve-out
bare except: anywhere; except Exception: without re-raise outside the handlers listed in <error_and_type_rules>
mutable default arguments (def fn(items=[]))
import * in non-__init__.py files
module-level mutable global state used as hidden dependency
comments that restate code
hidden singletons or global registries
brittle mock-only tests when a fake or boundary test would be clearer
transport or storage concerns embedded in core domain logic
# type: ignore without specific error code and justification
production design distorted to satisfy a mock framework
large speculative refactors when a smaller coherent change would solve the task
cast() used to silence type errors instead of fixing the type
deep inheritance hierarchies when composition would be clearer
writing to ~/.hermes/ or importing from hermes-agent
returning blank/None renderables from panels on error (violates "never blank the display")
3.12+ syntax (inline generics, type statement, @override), 3.13+ syntax (TypeIs, warnings.deprecated), or 3.14+ syntax (template strings) in library code while requires-python still includes 3.11
changes are small, test-backed, and easy to review
dependency flow is explicit from the composition root downward
module responsibilities are cleaner after the change, not blurrier
types in touched code are more precise after the change, not less
the implementation matches the Python standards in AGENTS.md and .codex/rules/
tests speak in behavior terms, not implementation vocabulary
the resulting code reads clearly without comments explaining the control flow
the resulting code is easier to extend without rewriting stable behavior
cache-preservation and read-only invariants are preserved
uv run ruff check, uv run mypy hermesd, uv run pytest, and uv run pip-audit all pass
1---2name: py-rig-23description: Use this skill when building, reviewing, or refactoring Python code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, strict typing, architecture review, and clean project structure. Complements the project's Python AGENTS.md with process rigor.4---56<objective>7Apply strict design and testing discipline for Python projects.89This skill complements `AGENTS.md`, `.codex/rules/python-patterns.md`, and `.codex/rules/python-idioms.md`. It must stay aligned with all three and applies as an execution discipline layer on top of the project's Python standards.1011This skill adds execution rigor:1213- ATDD/TDD workflow14- SRP, DRY, and OCP decision rules15- explicit dependency injection discipline with a pragmatic carve-out for CLI composition roots16- strict type discipline17- comment quality standards18- structured implementation and review checks19- hermesd-specific invariants (cache-preservation, read-only access, panels as OCP seam)2021If `AGENTS.md` is stricter on any point, follow `AGENTS.md`.22</objective>2324<when_to_use>25Use this skill when:2627- implementing a new feature or behavior increment28- refactoring Python code for clearer ownership or testability29- reviewing module boundaries or dependency flow30- replacing hidden collaborator construction with explicit injection31- tightening tests around user-visible or integration behavior32- removing `Any` types or tightening weak types in touched code33- cleaning up hardcoded values or global mutable state34</when_to_use>3536<process>37Follow this workflow:38391. Inspect the project first.40 Read `pyproject.toml`, project layout, local `AGENTS.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, tool configs (`[tool.ruff]`, `[tool.mypy]`, `[tool.pytest.ini_options]`), and existing tests.41422. Define acceptance behavior first.43 Express the user-visible outcome before writing implementation details.44453. Add or update an acceptance-level test when the project has that layer.46 For hermesd, the acceptance seam is `Collector → DashboardState → panel renderer`. Use `rich.console.Console(record=True)` to capture output for end-to-end render assertions.47484. Add the next smallest failing test.49 Prefer a focused unit or module test for the next behavior increment.50515. Implement the minimum change that makes the test pass.52 Keep the diff tight. Do not rewrite unrelated code.53546. Refactor while green.55 Improve naming, cohesion, dependency flow, and readability without changing behavior.56577. Keep standards and user-facing docs in sync.58 If the task materially changes project conventions, architecture, or workflow expectations, update `AGENTS.md` or the relevant rule/skill in the same change. Update `CHANGELOG.md` for user-visible changes and `README.md` when install/usage instructions change.59608. Verify locally before opening a PR.61 Run `uv run ruff check .`, `uv run ruff format --check .`, `uv run mypy hermesd`, `uv run pytest tests/ -q -ra --tb=short -W error::ResourceWarning --cov=hermesd --cov-report=term-missing`, `uv run pip-audit`, `uv lock --check`, `uv build`, wheel smoke installs, and `uvx twine check dist/*`. CI runs the same gate commands across Python 3.11/3.12/3.13/3.14.62</process>6364<design_rules>65Apply these rules during implementation:6667- Keep project structure clean and predictable68- **SRP**: each module, class, and function should have one clear reason to change69- **DRY**: remove repeated validation, mapping, branching, and policy logic when the abstraction improves clarity70- **OCP**: extend behavior through composition, Protocols, configuration, and strategy injection instead of invasive branching or copy-paste forks71- Prefer domain-oriented module boundaries over technical dumping grounds72- Keep domain logic separate from transport, persistence, configuration, and presentation concerns73- Prefer the smallest coherent abstraction that solves the real duplication or extension point74- Do not introduce Protocol-first abstractions without real consumer pressure75- Prefer composition over inheritance; keep inheritance hierarchies shallow7677**OCP example in hermesd.** `hermesd/panels/__init__.py` is the canonical OCP seam: add a new panel by writing `hermesd/panels/your_panel.py`, adding a `_render_your_panel(ctx: PanelRenderContext)` wrapper, then registering that wrapper in `_RENDERERS` and the label in `PANEL_NAMES`. This pattern is documented in `CONTRIBUTING.md#Adding a New Panel`.78</design_rules>7980<dependency_injection>81Manage code relationships explicitly.8283General rules:8485- Use constructor injection (`__init__` parameters) for long-lived collaborators86- Use function parameters for short-lived collaborators and pure logic inputs87- Pass dependencies through typed config, constructors, or arguments88- Do not instantiate external clients, repositories, clocks, or runtime collaborators inside **core domain logic**89- Do not hardcode dependency selection, URLs, ports, credentials, or feature switches90- Avoid globals, module-level mutable state, and hidden singletons9192**Composition-root carve-out.** The *composition root* — typically the `main()` entry point or a single top-level CLI class — is allowed to construct collaborators directly. In hermesd, `__main__.main()` and `DashboardApp.__init__` form this root: they may call `Collector(hermes_home)` and `load_theme(hermes_home)` without those being injected parameters. Below the composition root, follow the general DI rules strictly — collectors, renderers, and panel functions must not construct their own DB readers, file readers, clocks, or theme loaders.9394When testing the composition root itself, prefer constructing it with a fake `hermes_home` directory (see `tests/conftest.py::hermes_home`) rather than mocking the inner collaborators.9596Python-specific guidance:9798- Prefer Protocols at dependency boundaries for structural typing99- Use `dataclasses` or Pydantic models for typed configuration100- pytest fixtures are the natural DI mechanism in tests — prefer fixtures over monkeypatch101- Use `functools.partial` or closures for lightweight function-level injection102- Avoid DI frameworks; explicit wiring is preferred103104```python105# hermesd-shaped constructor injection — domain logic stays testable106from dataclasses import dataclass107from pathlib import Path108from typing import Protocol109110111class DatabaseReader(Protocol):112 def read_sessions(self) -> list[dict[str, object]]: ...113 def read_tool_stats(self) -> list[dict[str, object]]: ...114115116class Clock(Protocol):117 def now(self) -> float: ...118119120@dataclass(frozen=True, slots=True)121class SessionSummarizer:122 db: DatabaseReader # injected, not constructed inside123 clock: Clock # no time.time() inside this class124125 def summarize(self) -> dict[str, object]:126 rows = self.db.read_sessions()127 return {"count": len(rows), "as_of": self.clock.now()}128```129130```python131# function parameter for short-lived/pure logic132def within_budget(cost: float, now: float, budget_reset_at: float) -> bool:133 return now >= budget_reset_at or cost < 10.0134```135</dependency_injection>136137<threading_rules>138hermesd is threading-based, not async. Apply these rules to any code that runs across threads:139140- Use `threading.Lock`/`RLock` for shared mutable state; name the attribute `_*_lock` and document what it guards.141- Use `threading.Event` for cross-thread signaling (e.g., `_force_refresh` in `DashboardApp`). Prefer `Event.wait(timeout=...)` over sleep loops.142- Daemon threads (`daemon=True`) must not hold open resources through shutdown. Close explicitly in a `finally:` block on the main thread.143- **Signals only deliver to the main thread.** Register `signal.signal(SIGINT, handler)` on the main thread; daemon threads cannot receive signals. Signal handlers must be fast and set flags only — never call into Rich or SQLite from a handler.144- Any SQLite connection shared across threads must be created with `check_same_thread=False`. See `hermesd/db.py`.145- Never raise across thread boundaries without propagation — the collector thread in `app.py::_collector_loop` swallows exceptions into `is_stale=True` on purpose. Mirror that pattern for any new background thread.146- When a new background task is added, require an explicit cancellation path (an Event or a `self._running` flag the thread checks every iteration).147- Flag shared mutable state immediately; push toward immutable data copies or message passing through a queue.148149For any future async code (not applicable to current hermesd):150151- Prefer `asyncio.TaskGroup` (3.11+) for structured concurrency over bare `create_task`.152- Use `asyncio.gather(return_exceptions=True)` when all tasks must complete regardless of failures.153- Never nest `asyncio.run()` calls; one event loop per thread; prefer `asyncio.run()` or `asyncio.Runner` as the single entry point.154- Never mix sync and async I/O in the same code path without `run_in_executor`.155</threading_rules>156157<resilience_rules>158hermesd is a **read-only** observer of `~/.hermes/`. Data sources can disappear, corrupt, or lag. Readers MUST preserve continuity of display.159160- **Cache last-good data.** Every reader (SQLite, JSON, YAML, log file) caches the last successful read and returns it on transient errors. See `hermesd/db.py::HermesDB._cached_*` and `hermesd/collector.py::_read_json_cached`.161- **Count consecutive errors.** After N (currently 3) consecutive failures, reconnect/reload rather than retrying blindly. See `HermesDB._consecutive_errors`.162- **Never blank the display.** If a panel has no data, show "no data" text styled for the theme — do not raise or return `None` for the renderable.163- **NULL tolerance.** SQLite column reads use `or 0` / `or ""` coalescence, not `dict.get(key, default)`. This is a `AGENTS.md` critical rule.164- **Read-only always.** Never open `~/.hermes/*.db` without `?mode=ro`. Never write files, create directories, or mutate data under `~/.hermes/`.165- **Do not import hermes-agent.** Read the on-disk formats directly.166</resilience_rules>167168<style_and_readability>169Keep the code easy to read and maintain.170171- Write modern Python and prefer 3.11-safe idioms; see `.codex/rules/python-idioms.md` for version-tagged patterns172- Put `from __future__ import annotations` at the top of every module (project convention)173- Keep functions short, explicit, and focused on one job174- Use consistent formatting and indentation; `ruff format` defines layout (4 spaces, 100-char lines per `[tool.ruff]`)175- Use whitespace to separate concepts, not decorate code176- Use meaningful whitespace: blank lines between logical sections within functions, two blank lines between top-level definitions177- Keep naming concrete and intention-revealing; `snake_case` for functions/variables, `PascalCase` for classes178- Prefer straightforward control flow over clever compression179- Early returns over deep nesting; guard clauses at the top180- Separate logic clearly so domain rules, I/O, transport, persistence, and orchestration are easy to trace181- Avoid hardcoded values; move runtime values and environment behavior into typed config, constants, or inputs182- Explicit imports; no `import *` outside `__init__.py` re-exports183- Inline function-level imports are allowed when they break an import cycle or defer heavy startup cost (see `hermesd/__main__.py::main` deferring `DashboardApp` import). Prefer top-of-module imports otherwise.184</style_and_readability>185186<type_discipline>187Strict types are enforced via `mypy` in CI.188189- `[tool.mypy]` in `pyproject.toml` is the source of truth for enforced strictness190- Every public function has explicit parameter and return type annotations191- Minimize `Any` in domain code; use `object`, `Protocol`, generics, `Union`, or narrower types192- **Boundary-Any policy (convention, not mypy-enforced).** `[tool.mypy]` never sets `disallow_any_explicit`, so explicit `Any` is currently legal in every module; the `hermesd.db` per-module override (`disallow_any_explicit = false`) is a no-op today and only becomes meaningful if the base flag is ever enabled. By convention, `Any` stays at the untyped boundary: raw SQLite rows (`hermesd/db.py`, `hermesd/collect/sqlite_util.py`) and parsed JSON/YAML/state data (`hermesd/collect/*`, `hermesd/collector.py`). Translate into typed Pydantic models at the earliest reasonable point — `hermesd/models.py` and `hermesd/panels/*` contain no `Any`, and new `Any` should not be introduced past the collector. Enabling `disallow_any_explicit = true` repo-wide currently reports 150+ violations across the boundary modules; tightening to enforcement is aspirational and would require narrowing the boundary types first.193- `NewType` for domain IDs and values that should not mix: `UserId = NewType("UserId", int)` — plain assignment, not the `type` statement194- `Protocol` for dependency boundaries; enables structural typing without inheritance coupling195- `Literal` for constrained value sets; `Final` for immutable module-level bindings196- `# type: ignore[code]` requires a specific error code and inline justification — never bare `# type: ignore`197- Avoid `cast()` unless unavoidable; prefer narrowing with `isinstance` or restructuring198- Make invalid states unrepresentable with enums, dataclasses with validation, and constrained constructors199- Keep weakly typed data at the boundary and translate into strict internal types immediately200201Version-specific type syntax (see `.codex/rules/python-idioms.md` for full details):202203- `@override` (3.12+) — requires `typing_extensions` import on 3.11; do not use unconditionally while the project supports 3.11204- `TypeIs` (3.13) — requires `typing_extensions` on 3.11/3.12; prefer `TypeGuard` in library code supporting 3.11205- Inline generic syntax `def fn[T](...)` (3.12+) — SyntaxError on 3.11; use the old `TypeVar("T")` form in library code206</type_discipline>207208<testing_discipline>209Testing is mandatory. TDD is the default workflow.210211- **ATDD first** for user-visible changes: define the acceptance scenario before writing implementation212 1. Express the boundary behavior (Given/When/Then or equivalent scenario)213 2. Add or update the acceptance-level test214 3. Write the next smallest failing unit test215 4. Implement the minimum change that makes it pass216 5. Refactor while green217 6. Repeat for the next behavior increment218- **TDD** for the next increment: write the smallest failing unit test, implement, then refactor219- Use ATDD extensively for user-visible behavior, acceptance flows, and cross-boundary scenarios220- Every meaningful change should cover:221 - expected behavior (happy path)222 - invalid input and validation failures223 - edge cases and boundary values224 - error and failure paths225- Use `pytest` with fixtures; prefer fixtures over monkeypatch for dependency injection226- `@pytest.mark.parametrize` for table-driven tests; direct narrative tests when variation is not the point227- Fakes and stubs over mocks; test behavior not implementation228- Keep tests deterministic; avoid sleep-based flakiness229- Use `tmp_path` for filesystem tests; `time_machine` or `freezegun` for time-dependent tests230- Keep public API doctests accurate when behavior changes231232**hermesd test taxonomy.** Tests live in a flat `tests/` directory with file-prefix categories:233234| Prefix | Scope |235|---|---|236| `test_models.py` | Pydantic model construction, field defaults |237| `test_db*.py` | SQLite reader, caching, resilience |238| `test_collector*.py` | data collection from `~/.hermes/` |239| `test_*_panel.py` | panel rendering (compact + detail) |240| `test_app*.py` | TUI key handling, layout, lifecycle |241| `test_*_resilience.py` | error handling, cache preservation |242243Add new tests to the matching prefix. Use `tests/conftest.py` fixtures (`hermes_home`, `sample_db`, `populated_hermes_home`) rather than hand-rolling setup.244245**Acceptance seam.** For user-visible changes, assert against a full `Collector → DashboardState → render_panel` flow using `rich.console.Console(record=True)` to capture output. See existing `test_*_resilience.py` files for the pattern.246247```python248# hermesd-shaped fixture + sync test (no async — project is threading-based)249@pytest.fixture250def collector(hermes_home: Path) -> Collector:251 return Collector(hermes_home)252253254def test_collector_returns_empty_state_when_home_is_bare(collector: Collector) -> None:255 state = collector.collect()256 assert state.gateway.running is False257 assert state.sessions == []258259260def test_collector_preserves_cache_on_db_corruption(261 collector: Collector,262 sample_db: Path,263) -> None:264 first = collector.collect()265 sample_db.write_bytes(b"corrupt")266 second = collector.collect()267 assert second.sessions == first.sessions # cache-preservation invariant268 assert second.is_stale is False # collector thread sets is_stale, not collect()269```270</testing_discipline>271272<comment_rules>273Write comments only when they add information the code cannot carry cleanly on its own.274275Good comments explain:276277- intent and rationale for non-obvious design choices278- invariants and constraints279- ownership or concurrency rules280- non-obvious tradeoffs or performance considerations281- why a particular approach was chosen over alternatives282283Do not write comments that:284285- restate the code286- narrate simple assignments or obvious operations287- explain standard Python syntax288- leave vague TODOs without context or ticket reference289- duplicate the docstring with less precision290291Docstrings (hermesd convention):292293- Use short, single-line docstrings ("""Description.""") for public functions and fixtures294- Omit docstrings on private helpers, test functions, and obvious one-liners295- When the function has a complex contract, a short multi-line docstring is acceptable — match the neighbouring style296- Do not adopt Google/NumPy-style blocks; the project has not historically used them297- Update docstrings when exported behavior, config, or API semantics change298</comment_rules>299300<error_and_type_rules>301Keep errors and types strict and readable.302303- Raise specific exceptions with context; never bare `except:` — it catches `SystemExit` and `KeyboardInterrupt`304- `except Exception:` without re-raise is acceptable only at these boundary handlers:305 - CLI entry point (`hermesd/__main__.py::main`)306 - Collector and input threads (`hermesd/app.py::_collector_loop`, `_input_loop`)307 - Per-source collection boundary (`hermesd/collector.py::_CollectionHealth.collect`)308 - Message-search worker thread (`hermesd/app.py::_search_session_messages_worker`)309 - Exception-message sanitizer (`hermesd/collect/redaction.py::_safe_exception_text`) — must remain non-throwing while handling another error; return only the exception type if sanitization fails310 - Signal handlers311 Everywhere else, re-raise or handle specifically.312- `raise NewError("context") from original_err` to preserve cause chains313- Custom exception hierarchies for domain errors; stdlib exceptions for programming errors314- `contextlib.suppress(SpecificError)` only for known-safe cases with clear justification315- Keep error messages actionable and specific enough to debug316- Never log secrets, tokens, credentials, or sensitive payloads317- Use `ExceptionGroup` and `except*` (3.11+) when multiple independent errors should be reported together318- Return `None` or `Optional` only when absence is a valid, documented part of the contract319- Do not use sentinel values when an exception or `Optional` return would be clearer320</error_and_type_rules>321322<tooling_rules>323Verification is part of the implementation, not an optional cleanup step.324325- Run `uv run ruff format --check .` (or `uv run ruff format .` to fix)326- Run `uv run ruff check .` (with `--fix` for auto-fixable issues)327- Run `uv run mypy hermesd` (configured via `[tool.mypy]` in `pyproject.toml`)328- Run `uv run pytest tests/ -v -W error::ResourceWarning` (with relevant paths for tighter affected-scope checks)329- Run `uv run pip-audit`; CI runs it on every push330- Run `uv lock --check`; if dependencies changed, run `uv lock` and verify the diff331- Run `uv build`, smoke install `dist/hermesd-*.whl`, and run `uvx twine check dist/*` before release or packaging-affecting changes332- If public API changed, verify docstrings and update README/CHANGELOG if user-facing333- Treat linting and static analysis as normal development tools, not release-only checks334- Fix root causes instead of scattering `# noqa` or `# type: ignore` comments335- If a lint rule is intentionally suppressed, use the specific code and add a justification comment336- For local enforcement, `pre-commit` is a reasonable option — add a `.pre-commit-config.yaml` running ruff + mypy on staged files if the team wants it337</tooling_rules>338339<review_checklist>340Before finishing, verify:341342- [ ] `AGENTS.md#Critical Rules` project invariants hold (read-only, no hermes-agent imports, cache preservation, NULL tolerance, escape-sequence bulk reads)343- [ ] module boundaries are coherent — no cross-domain leaks344- [ ] responsibilities are not mixed across domain, transport, persistence, and config345- [ ] structure is clean, predictable, and free of dumping-ground modules346- [ ] dependencies are injected explicitly below the composition root — no hidden construction or global state347- [ ] no hardcoded runtime values (URLs, ports, credentials, paths, timeouts)348- [ ] types follow `<type_discipline>` — `Any` stays at the documented SQLite and parsed-data boundaries, no bare `# type: ignore`349- [ ] every public function has explicit parameter and return type annotations350- [ ] functions are short, focused, and readable in one pass351- [ ] formatting, indentation, and whitespace follow `ruff format` defaults352- [ ] comments explain intent or invariants instead of restating the code353- [ ] error handling is clear, concise, contextual, and uses cause chains354- [ ] no bare `except:`; `except Exception:` only at documented boundary handlers; no swallowed errors; no mutable default arguments355- [ ] tests cover acceptance behavior and unit behavior; both new tests were written **first**356- [ ] TDD/ATDD flow was followed357- [ ] threading: locks, events, daemon-thread shutdown, and signal handlers follow `<threading_rules>`358- [ ] resilience: cache-preservation, consecutive-error counting, and "never blank the display" hold per `<resilience_rules>`359- [ ] public API docstrings are accurate and updated when behavior changed360- [ ] `CHANGELOG.md` updated for user-visible changes; `README.md` updated for install/usage changes361- [ ] the full local gate from `<tooling_rules>` passes locally362- [ ] version/tooling guidance from `AGENTS.md`, `.codex/rules/python-idioms.md`, and `.codex/rules/python-patterns.md` has been followed363</review_checklist>364365<reject_patterns>366Reject these patterns:367368- giant functions mixing validation, orchestration, and persistence369- Protocol-per-class abstraction without consumer need370- hardcoded configuration or collaborator construction **below the composition root**371- `Any` added or left in touched code without explicit justification or the documented boundary carve-out372- bare `except:` anywhere; `except Exception:` without re-raise outside the handlers listed in `<error_and_type_rules>`373- mutable default arguments (`def fn(items=[])`)374- `import *` in non-`__init__.py` files375- module-level mutable global state used as hidden dependency376- comments that restate code377- hidden singletons or global registries378- brittle mock-only tests when a fake or boundary test would be clearer379- transport or storage concerns embedded in core domain logic380- `# type: ignore` without specific error code and justification381- production design distorted to satisfy a mock framework382- large speculative refactors when a smaller coherent change would solve the task383- `cast()` used to silence type errors instead of fixing the type384- deep inheritance hierarchies when composition would be clearer385- writing to `~/.hermes/` or importing from `hermes-agent`386- returning blank/None renderables from panels on error (violates "never blank the display")387- 3.12+ syntax (inline generics, `type` statement, `@override`), 3.13+ syntax (`TypeIs`, `warnings.deprecated`), or 3.14+ syntax (template strings) in library code while `requires-python` still includes 3.11388</reject_patterns>389390<success_criteria>391This skill is being followed correctly when:392393- changes are small, test-backed, and easy to review394- dependency flow is explicit from the composition root downward395- module responsibilities are cleaner after the change, not blurrier396- types in touched code are more precise after the change, not less397- the implementation matches the Python standards in `AGENTS.md` and `.codex/rules/`398- tests speak in behavior terms, not implementation vocabulary399- the resulting code reads clearly without comments explaining the control flow400- the resulting code is easier to extend without rewriting stable behavior401- cache-preservation and read-only invariants are preserved402- `uv run ruff check`, `uv run mypy hermesd`, `uv run pytest`, and `uv run pip-audit` all pass403</success_criteria>
Run npx skillmds@latest add mudrii/py-rig-2 in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Use this skill when building, reviewing, or refactoring Python code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, strict typing, architecture review, and clean project structure. Complements the project's Python AGENTS.md with process rigor. It is listed under Productivity on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
mudrii (@mudrii) published this skill. Their other Agent Skills are listed on their SkillMD profile.