Be terse. Prefer fragments over full sentences. Skip filler and preamble. Sacrifice grammar for density. Lead with the answer or action.
Web service for monitoring cannabis industry activity: licenses, regulatory filings, compliance events, and entity relationships.
TDD required. Red → Green → Refactor. No production code without a failing test first.
Python ≥3.12, uv, pytest, ruff; Node.js + npm (for Tailwind CLI — sudo npm install -g @tailwindcss/cli, one-time VM setup).
Top-level directories. Read the code for per-file detail.
src/api/ FastAPI app (ASGI routes, schemas, deps)
src/core/ Shared domain logic (models, probe, watches, notifications, diff, extractors, fetchers, scheduler, storage, crypto)
src/dashboard/ Server-rendered UI (Jinja2 + HTMX + Tailwind)
src/workers/ Procrastinate task queue (check_watch, schedule_tick, pipeline)
tests/ Mirrors src/ structure
deploy/ Systemd units and deployment config
docs/ Reference docs (COMMANDS, DEPLOYMENT, SKILLS, STYLE)
scripts/ Build scripts (Tailwind, vendor CSS, cleanup)
skills/ Agent skills (committed overrides + symlinks → skills-vendor/)
skills-vendor/ Git submodules for external skill repos
.claude/skills/ Claude Code skill discovery (symlinks → ../../skills/<name>)
Single-VM setup. Dev and prod on the same VM. Code committed to main is the deployed code. Systemd service watcher runs the live site on port 8000.
| Service | Port | Managed by |
|---|---|---|
| API (live) | 8000 | systemctl (watcher.service) |
| API (dev) | 8001 | manual uvicorn |
The exe.dev proxy forwards 3000–9999. Dev server reachable at https://watcher.exe.xyz:8001/.
Port 8000 belongs to systemd. Never start uvicorn manually on port 8000.
After committing to main: sudo systemctl restart watcher. After DB model changes: uv run alembic upgrade head then restart. After Tailwind/vendor CSS changes: bash scripts/build-css.sh then restart. Logs: sudo journalctl -u watcher -f.
Dev server (port 8001, leaves prod alone):
export $(cat /etc/watcher/.env .env 2>/dev/null | xargs)
uv run uvicorn src.api.main:app --host 0.0.0.0 --port 8001 --reloadFull lifecycle reference + cleanup timer: docs/DEPLOYMENT.md.
Two env files load in order (later overrides earlier):
/etc/watcher/.env— production secrets (DATABASE_URL,APPRISE_SECRET_KEY). Persistent, managed manually on the VM..env(repo root, git-ignored) — dev/agent secrets (GH_TOKEN,TEST_DATABASE_URL). Never commit.
Load both for shell commands:
export $(cat /etc/watcher/.env .env 2>/dev/null | xargs)Full variable reference: docs/DEPLOYMENT.md.
uv sync # install deps
uv run pytest # tests
uv run pytest -m integration # integration tests (needs PostgreSQL)
uv run ruff check . # lint
uv run alembic upgrade head # apply migrations
uv run alembic revision --autogenerate -m "description"Full reference: docs/COMMANDS.md.
Skills live in skills/ (agentskills.io) and .claude/skills/ (Claude Code). Local overrides in skills/ shadow vendor submodules in skills-vendor/.
| Skill | Triggers / when to invoke |
|---|---|
reviewing-code-claude |
CR, code review |
reviewing-architecture-claude |
AR, architecture review |
shipping-work-claude |
ship it, push GH, close GH, wrap up |
brainstorming |
brainstorm, design this, let's design |
writing-plans |
write plan, implementation plan |
writing-skills |
write skill, new skill, author skill |
systematic-debugging |
any bug, test failure, unexpected behavior |
verification-before-completion |
before any completion claim or commit |
test-driven-development |
before writing implementation code |
subagent-driven-development |
dispatch agents for plan execution |
dispatching-parallel-agents |
2+ independent tasks in parallel |
using-git-worktrees |
feature work needing isolation |
managing-skills-claude |
add skill repo, manage external skills |
socraticode (codebase MCP) |
search before reading; codebase_search, codebase_graph_query, codebase_impact before opening files |
Full skill reference + SocratiCode tool-selection table: docs/SKILLS.md. Cross-project search to the sister notifier index requires a per-instance .claude/settings.local.json (gitignored) — see "Linked Projects" in docs/SKILLS.md.
Commit Messages:
#<number> [type]: <description> # with issue
[type]: <description> # without issue
Types: feat, fix, refactor, docs, test, chore.
Logging:
from src.core.logging import get_logger
logger = get_logger(__name__)Entry points only: call configure_logging() once.
Date & Time: All UTC. ISO 8601: YYYY-MM-DDTHH:MM:SS.ffffffZ (timestamps), YYYY-MM-DD (dates).
General:
- No inline module imports; all at file top
- Docstrings for public modules, classes, functions
- Test structure mirrors source (
src/foo.py→tests/test_foo.py) - Explicit imports only
- Small, focused functions
DB Triggers (gotcha):
- Triggers live in Alembic migrations (
CREATE OR REPLACE FUNCTION+CREATE OR REPLACE TRIGGER; downgrade withDROP TRIGGER IF EXISTS … ON table; DROP FUNCTION IF EXISTS …). - Integration tests use
Base.metadata.create_all(not migrations), so triggers are NOT applied automatically. Any trigger added in a migration must also be recreated intests/conftest.pyinside thetest_enginefixture, aftercreate_all. - Current triggers:
trg_changes_update_last_changed_at(AFTER INSERT ON changes → setswatches.last_changed_at = NEW.detected_at).
Authoritative reference: docs/STYLE.md.
Brand: Cannabis Observer — co-purple-600 (#6d4488) primary accent. Never use brand colors for semantic status (green/yellow/red/blue).
Dark Mode: Tailwind dark: variants on every color utility. Class-based toggle (<html class="dark">). localStorage key: watcher-color-scheme.
Accessibility: WCAG 2.1 AA. Skip link, ARIA landmarks, focus-visible rings, 44px touch targets, aria-live on HTMX swap targets, reduced motion. Wrap decorative emoji in <span aria-hidden="true">. No title attributes.
CSS: Tailwind v4 with @theme in input.css. Use component classes (.btn, .badge, .stat-card, .data-table, .form-input, .link, .segment-group, .segment, .chip-group, .chip, .detail-grid, .toggle, .danger-zone). Badge variants: .badge-active (green), .badge-inactive (gray), .badge-archived (amber), .badge-error (red), .badge-warning (orange), .badge-info (blue). Use CSS logical properties (margin-inline-start not margin-left).
HTMX: OOB flash via partials/flash_oob.html. CSS .htmx-request for loading states. Detect HTMX via HX-Request header with HX-Boosted guard. All mutation routes provide non-HTMX redirect fallback.
Performance: Pre-built Tailwind (no CDN). BUILD_ID env var for cache-busting (?v={{ build_id }}). defer on all non-critical scripts. System font stack.