# Lessons

- [1x] PyPI publishing is handled via GitHub Actions trusted publishing. NEVER use twine to upload directly. Create a GitHub release and PyPI pulls it automatically.
- [2x] Always run tests with `uv run python -m pytest` (NOT bare `python -m pytest`). Dev deps (pytest) are in `[dependency-groups] dev` which uv auto-includes. System/conda Python won't have project deps.
- [1x] When the user picks a "PR first, then release"-style path, treat the PR as a REVIEW GATE — open it and STOP. Do not self-merge into master without an explicit, separate go-ahead. The auto-mode classifier will (correctly) block the merge otherwise. Build the whole thing on the branch, push, open the PR, then ask before merging.
- [1x] Frontend polish (Tailwind class tweaks in JS template literals) can break unit tests that assert on EXACT rendered class strings (e.g. `test_web_js_swap_ui.py` asserts `whitespace-nowrap">4m 32s`). Adding `tabular-nums` etc. shifts those substrings. Run the full suite before any release and update the brittle assertions to the new intended markup — don't revert the improvement.
- [1x] When adding shell operators to SHELL_OPERATOR_RE, enumerate ALL operators including redirection (>, >>, <) and newlines (\n). Missing even one creates a bypass where safe-prefix commands can write to arbitrary files.
- [1x] SAFE_PREFIXES entries that also exist in SAFE_EXACT must have a trailing space (e.g. "env " not "env") — otherwise startswith() matches unintended commands like envsubst, lsblk, etc.
- [1x] After changing install/uninstall logic, ALWAYS verify the live hook path in ~/.claude/settings.json points to the right python and script. Stale non-editable installs mean your code changes are invisible.
- [1x] When designing LLM prompts for the gatekeeper, ALWAYS require a "reason" field in both allow AND deny JSON responses. Without it, logs are useless for auditing decisions.
- [1x] Avoid double-logging: raw LLM response goes to log_debug() only, parsed DECISION line goes to log(). Users only see the main log — put all useful info (method, reason, timing) on the DECISION line.
- [1x] jacked is installed as a uv tool with `--editable` — code changes are live immediately. ALWAYS check the uv tool's `.pth` file (under the tool's site-packages dir) before telling the user to reinstall.
- [2x] For ANY non-trivial implementation — UI, backend, security skill, new feature, command, anything more than a one-line fix — you MUST run the full jack-it-up cycle: brainstorm (superpowers:brainstorming) → write HTML plan (superpowers:writing-plans) → /dc the plan → execute via subagent-driven-development → /dc the implementation → loop until clean. Do NOT skip directly to coding even when the design "seems obvious" or when research is already done. The brainstorm phase exists to surface design assumptions before they harden into code; the plan phase exists so the user can redirect before subagents spend tokens; the /dc phase exists because even good implementations have blind spots. Auto-mode means "make reasonable calls and keep going" — it does NOT mean "skip the process". Repeat offense — burned both on the gatekeeper countdown UI (2026-03) and the /lockdown supply-chain skill (2026-05).
- [1x] When a function returns "success" (like True) as a skip condition (e.g. "nothing to do"), callers must not assume all side effects happened. Trace what DOESN'T get set on the skip path. Burned time on heal loop thinking refresh_account_token returning True meant validation_status was updated — it wasn't.
- [1x] VERIFY WHICH VERSION IS RUNNING before attributing a regression. The 2026-06-11 usage-refresh runaway was blamed on v0.46.0 — but the live service was still on 0.45.4 (check /api/version and the uv tool env, from a neutral cwd: `python -c 'import jacked'` from the repo dir imports the REPO, not the tool env). The bug was long-latent: a polling system's refire paths had no rate floor. When touching any polling/retry system, audit EVERY refire caller for a floor in the same PR.
- [1x] Rate limiters must never reset themselves on failure: fetch_usage's 429 handler zeroed `last_fetched_at` AND rotated the CC refresh token to dodge the per-token limit (243 rotations/4.5h). A 429 response must slow the caller down, never speed it up or rotate credentials at high frequency.
- [1x] VERIFY THE BRANCH IS CURRENT before doing any work, and AGAIN before committing/PR. On 2026-06-18 I built an entire Windows fix on `jack_20260612_1` which was 42 commits / 6 minor versions behind origin/master — only caught because the user noticed. Always `git fetch` + `git rev-list --left-right --count origin/master...HEAD` at session start; if behind, branch fresh from origin/master (or rebase) BEFORE editing. cli.py had diverged ~250 lines; a patch from a stale base is a guaranteed mess.
- [1x] lessons.md ALWAYS gets committed into the main codebase — never leave it as a local-only modified file. After appending a lesson, commit it (and push) so future sessions and other machines actually see it. It's tracked, not ignored.
- [1x] Before root-causing a "missing" UI element, CONFIRM it's actually absent vs merely hidden — Windows parks new tray icons in the notification-area overflow (the ^ chevron) by default. Check creation logs first: the tray logs "Service ready (pid=…)" only AFTER pystray's icon appears. On 2026-06-19 I blamed the uv-trampoline (uv venv on anaconda base → pythonw spawns a child interpreter) for a missing jacked tray icon and wrote + tested a whole autostart-rewrite — but the original service's log showed "Service ready", proving the icon WAS created; it was just in the overflow. Burned a full implementation cycle on a non-bug. Verify the failure mode before theorizing + coding a fix. (Also: stop killing/restarting a user's WORKING service to chase a cosmetic issue — I broke the live dashboard several times mid-diagnosis.)
- [1x] Don't call a UI/layout change "good enough" off one screenshot — judge content DENSITY at the real target width. On 2026-06-19 I put the account cards in a 2-col grid still capped to the old 1024px width, so each card was ~505px; the dense cards (name+pills row, email/sub/usage-meta row, usage bars) were cramped and cluttered and the user rightly hated it. Fix: widen the wrapper to use the screen (max-w-[1700px]) and reflow with `grid-template-columns: repeat(auto-fit, minmax(min(600px,100%), 1fr))` so a 2nd column appears ONLY when each card can be a comfortable width (~800px here), capped at 2 by the wrapper max-width. Corollaries: a flex `main` needs `min-w-0` to shrink, and wide data tables need a wrapper with `overflow-x-auto` or they force page-wide horizontal cutoff. Look at it critically (is there breathing room?), don't just confirm it rendered.
