Adopt the four markdown namespaces and gate them

Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
This commit is contained in:
Krzysztof Rudnicki 2026-08-24 15:54:34 +02:00
parent 737b49246c
commit 965abcaeaa
11 changed files with 275 additions and 77 deletions

38
.github/workflows/md-naming.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: md-naming
# The four markdown namespaces, enforced on push as well as at commit time.
# The local pre-commit hook can be skipped (a fresh clone that never ran
# `pre-commit install` has no hooks at all), so the convention needs a second
# gate that no one can forget to install.
#
# Deliberately a SEPARATE workflow from any release job: a badly named doc
# should fail lint, not block publishing an artifact.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
md-naming:
name: markdown naming convention
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The gate and its exemption list live in the shared utils repo so every
# repo enforces the same namespaces. Checking it out beats vendoring a
# copy here, which is exactly how a gate and a survey start disagreeing.
- name: Check out the shared gate
uses: actions/checkout@v4
with:
repository: kuhyx/utils
path: .utils
- name: Enforce the naming convention
run: bash .utils/scripts/check_md_naming.sh --all
# Warns only: no machine can prove a task is finished, so an old TODO
# gets a nudge, never a failure.
- name: Warn about stale TODOs
run: PYTHONPATH=.utils python3 .utils/md_naming/stale.py

View File

@ -192,3 +192,13 @@ repos:
entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck --severity=warning' -- entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck --severity=warning' --
language: system language: system
types: [shell] types: [shell]
# Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the
# rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may.
# Shared gate in ~/utils owns the namespaces and the exemption list, so
# one repo's idea of a valid name cannot drift from every other repo's.
- id: md-naming
name: markdown naming convention
entry: scripts/check_md_naming.sh
language: system
files: \.(md|markdown)$

16
AGENTS.md Normal file
View File

@ -0,0 +1,16 @@
do NOT run tests unless specifically instructed to do so or before committing
If tests fail on the same issue twice in a row, STOP and ask the user how to proceed instead of continuing to fix and retry.
ALWAYS confirm that the feature you add / bug you fixed behaves as it should by running the program after your changes (not tests!) and inspecting output comparing it with what user wanted, after confirming by yourself ask user if the program behaves as they intended
After running tests fix all coverage gaps and issues, do not ignore unless specifically instructed to do so
@/home/kuhy/.claude/rules/typescript-5-es2022.instructions.md
# Steam Backlog Enforcer Notes
- Fixed in commit 8b7bdb6: conftest.py safety net redirects all filesystem paths (STEAMAPPS_PATH, CONFIG_DIR, STATE_FILE, etc.) to tmp_path. Tests are safe to run without asking about state.json first.
- The pre-commit `pytest-coverage` hook is currently broken (measures all of python_pkg at 100%, not just the changed subpackage). There's an in-progress fix via `scripts/pytest_changed_packages.py` + `.pre-commit-config.yaml` change that still needs lint fixes.
- Clearing `hltb_cache.json` alone is not enough for `run.sh`/`done` reassignment: `snapshot.json` also stores `completionist_hours`, and stale snapshot values can still drive reassignment decisions unless refreshed.
- After fixing Steam Backlog Enforcer logic, always run a live verification pass with `python_pkg/steam_backlog_enforcer/run.sh` (or equivalent command) before declaring the fix done.
- cmd_done completion path can pick next game from snapshot-only hours; keep it aligned with HLTB cache/refresh before pick_next_game to avoid prologue-derived stale times (e.g., A Space 0.56h while cache has ~20h).
- HLTB renames games (e.g., "Needy Streamer Overload" → "NEEDY GIRL OVERDOSE"). The old name lives in `game_alias`. Both `game_name` and `game_alias` must be checked when matching — fixed in `_pick_best_hltb_entry`.
- **ALWAYS clear HLTB cache and re-run `run.sh` after changing the HLTB picking/matching algorithm.** Delete `~/.config/steam_backlog_enforcer/hltb_cache.json` (entire file, not just one entry) so all games get re-matched with the new logic. Then run `./run.sh` to verify correct results. Stale cache entries from the old algorithm will persist and hide bugs otherwise.

View File

@ -1,16 +0,0 @@
do NOT run tests unless specifically instructed to do so or before committing
If tests fail on the same issue twice in a row, STOP and ask the user how to proceed instead of continuing to fix and retry.
ALWAYS confirm that the feature you add / bug you fixed behaves as it should by running the program after your changes (not tests!) and inspecting output comparing it with what user wanted, after confirming by yourself ask user if the program behaves as they intended
After running tests fix all coverage gaps and issues, do not ignore unless specifically instructed to do so
@/home/kuhy/.claude/rules/typescript-5-es2022.instructions.md
# Steam Backlog Enforcer Notes
- Fixed in commit 8b7bdb6: conftest.py safety net redirects all filesystem paths (STEAMAPPS_PATH, CONFIG_DIR, STATE_FILE, etc.) to tmp_path. Tests are safe to run without asking about state.json first.
- The pre-commit `pytest-coverage` hook is currently broken (measures all of python_pkg at 100%, not just the changed subpackage). There's an in-progress fix via `scripts/pytest_changed_packages.py` + `.pre-commit-config.yaml` change that still needs lint fixes.
- Clearing `hltb_cache.json` alone is not enough for `run.sh`/`done` reassignment: `snapshot.json` also stores `completionist_hours`, and stale snapshot values can still drive reassignment decisions unless refreshed.
- After fixing Steam Backlog Enforcer logic, always run a live verification pass with `python_pkg/steam_backlog_enforcer/run.sh` (or equivalent command) before declaring the fix done.
- cmd_done completion path can pick next game from snapshot-only hours; keep it aligned with HLTB cache/refresh before pick_next_game to avoid prologue-derived stale times (e.g., A Space 0.56h while cache has ~20h).
- HLTB renames games (e.g., "Needy Streamer Overload" → "NEEDY GIRL OVERDOSE"). The old name lives in `game_alias`. Both `game_name` and `game_alias` must be checked when matching — fixed in `_pick_best_hltb_entry`.
- **ALWAYS clear HLTB cache and re-run `run.sh` after changing the HLTB picking/matching algorithm.** Delete `~/.config/steam_backlog_enforcer/hltb_cache.json` (entire file, not just one entry) so all games get re-matched with the new logic. Then run `./run.sh` to verify correct results. Stale cache entries from the old algorithm will persist and hide bugs otherwise.

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md

View File

@ -1,54 +0,0 @@
# Design audit — steam-backlog-enforcer
Generated against safe-design-rules (anthonyhobday.com/sideprojects/saferules).
Report only — nothing in this repo was changed by the audit itself.
Scope: `web/` (Vite/TS, 22 files). Token entry point: `web/src/index.css`
`:root` (lines 1-18). Reviewed fully alongside `App.tsx`, `FilterPanel.tsx`,
`GameTable.tsx`, `SummaryCards.tsx`, `TimelineChart.tsx`, `index.html`.
## web/ (Vite/TS frontend)
### Violations
- **Rule 1 (near-black/near-white)** — `web/src/index.css:9` — `--heading: #ffffff` is pure white while every other neutral (`--bg:#0f1117`, `--panel:#161922`, `--border:#2a2f3a`) is deliberately muted → give `--heading` a near-white value with the same cool tint, e.g. `#eef2f6`.
- **Rule 2 (saturate neutrals)** — `web/src/index.css:9` — `--heading:#ffffff` has zero saturation, breaking the otherwise-consistent cool-tinted neutral family (`--bg`, `--panel`, `--panel-2`, `--card`, `--border`, `--muted` all carry a slight blue hue) → tint it to match (see Rule 1 fix).
- **Rule 4 (everything deliberate)** — `web/src/index.css:203` and `:489` — `.seg.active { color: #06121c }` and `.badge { color: #1a1205 }` are one-off hardcoded "ink" colors with no token, unrelated to any other value in the file → promote to `--on-accent`/`--on-warn` custom properties so the choice is traceable and reusable.
- **Rule 6 (letter-spacing/line-height by size)** — `web/src/index.css:37-44` — only `h1` (26px) gets a letter-spacing adjustment (`-0.4px`, line 39); none of the 13 smaller sizes in the file (10px badge line 487, 11px axis-label line 404, 12px `.seg`/`.ghost` lines 199/214, 13px `.hint`/table/`.field label` lines 83/127/440) get any compensating letter-spacing or line-height increase → add a small positive letter-spacing (~0.1-0.2px) to the ≤13px tier.
- **Rule 9 (distinct brightness values)** — `web/src/index.css:10,12` — using the HSB-brightness metric the rule's thresholds are defined in (max(R,G,B)/255, matching Rule 18), `--accent` (#66c0f4) is 95.7% and `--warn` (#f0a23b) is 94.1% — only 1.6 points apart; `--accent-2` (81.2%) and `--danger` (89.0%) are well clear of both, so this is narrowly an accent-vs-warn issue, not a whole-palette one → nudge `--warn` a few points darker (e.g. `#e8952e`, ≈89% brightness) so it doesn't sit this close to `--accent`. Low priority: the two colors aren't used adjacently in the current UI (accent = numeric totals, warn = the "slower than leisure" insight label and the "no data" badge).
- **Rule 11 (mathematically related measurements)** — `web/src/index.css` throughout — spacing/radius values are 24, 20, 18, 14, 12, 10, 9, 8, 7, 6, 5, 4, 2, 1px (e.g. panel padding 18px line 111, card padding 14px line 275, th padding 9px line 448, td padding 7px line 462, ghost padding 5px line 210) with no shared base unit; radii alone use four unrelated hardcoded values (4px line 56, 6px lines 168/196/211, 8px lines 188/256/274/321/435, plus the `--radius:10px` token line 14) → consolidate onto one scale (e.g. 4px base: 4/8/12/16/20/24) and derive radii from `--radius` and `--radius-sm`.
- **Rule 13 (12-column grid)** — `web/src/index.css:90` and `:263` — `.layout` uses a fixed `320px 1fr` split and `.cards` uses `repeat(4, 1fr)` / `repeat(2, 1fr)` (line 268), neither derived from a shared 12-column track → define one grid scale (e.g. a `--grid-12` template) both layouts pull from, for future 1/2/3/4-up flexibility.
- **Rule 20 (body text ≥16px)** — `web/src/index.css:28` — `body { font: 15px/1.5 var(--sans); }` is below the 16px floor, and most UI text sits well under that: `.hint`/`.field label`/table/`.parity` at 13px (lines 83, 127, 247, 440), `.seg`/`.ghost`/`.card-blurb` at 12px (lines 199, 214, 286), `.preset`/`.axis-label` at 11px (lines 310, 404), `.badge` at 10px (line 487) → raise body copy to 16px minimum; for the dense data-table cells where 13px is a deliberate density trade-off, note that explicitly rather than leaving it as the default scale.
- **Rule 22 (button padding: horizontal = 2× vertical)** — `web/src/index.css:195` — `.seg { padding: 6px 4px; }` has horizontal padding *less* than vertical (should be ~2× more, e.g. `6px 12px`) → `.ghost` at `web/src/index.css:210` (`padding: 5px 10px`) already gets this right and can be the reference.
- **Rule 24 (nest corners properly)** — `web/src/index.css:188-196` — `.segmented { border-radius: 8px; padding: 4px; }` containing `.seg { border-radius: 6px; }`; per the rule the nested radius should be outer − gap = 8 − 4 = 4px, not 6px → change `.seg` to `border-radius: 4px`.
- **Rule 26 (no shadows in dark interfaces)** — `web/src/index.css:17` declares `color-scheme: dark` (bg `#0f1117`); `web/src/index.css:279` — `.card.active { box-shadow: 0 0 0 1px var(--accent); }` is technically a shadow in a dark UI → replace with a non-shadow selection indicator, e.g. `outline: 1px solid var(--accent)` or a border-color swap, which reads identically without invoking box-shadow. (Low severity: this is a 0-blur selection ring, not an elevation shadow, but it's the literal case the rule flags.)
### Not applicable
- Rule 5 (optical alignment) — no icons or asymmetric shapes in the codebase whose bounding-box center would differ from its visual center.
- Rule 8 (everything aligns with something else) — no misalignment identifiable from CSS/JSX alone; needs a rendered pass to verify.
- Rule 12 (order by visual weight) — layout-level judgment call (sidebar vs. content ordering); no clear violation visible in source.
- Rule 14 (space between high-contrast points) — requires visual inspection of rendered edges, not determinable from source.
- Rule 16 (drop shadow blur ≈ 2× distance) — the only `box-shadow` in the file (line 279) has 0 offset/0 blur, so the ratio rule doesn't meaningfully apply.
- Rule 21 (line length ~70 chars) — no long-form paragraph content; all text is short UI labels/hints.
- Rule 28 (lower icon contrast paired with text) — no dedicated icon glyphs; the only icon-like characters are the sort arrows (`▲`/`▼` in `web/src/components/GameTable.tsx:115`), which inherit the same color as the adjacent header text rather than being a separate heavier element.
### Passing
- Rule 3 (high contrast for important elements) — `.seg.active` (`web/src/index.css:202-204`) pairs `background: var(--accent)` with `color: #06121c`, a very high-contrast combination for the primary interactive state.
- Rule 7 (border contrast with both surfaces) — in HSB brightness, `--border:#2a2f3a` (22.7%) sits clearly above `--bg` (9%) and `--panel` (13.3%) wherever it's used as a divider. Caveat: against `--card:#1b2838` (22.0%) the gap is only 0.7 points — e.g. `.card { border: 1px solid var(--border) }` at `web/src/index.css:273` — border is technically still lighter but not "clearly" so; if card borders read as blurry in practice, lighten `--border` or darken `--card` by a few points.
- Rule 10 (warm OR cool neutrals) — all neutrals (`--bg`, `--panel`, `--panel-2`, `--card`, `--border`, `--muted`) carry a consistent cool/blue tint.
- Rule 15 (closer elements lighter) — HSB brightness (max(R,G,B)/255) increases monotonically through the stack: `--bg`(9.0%) < `--panel`(13.3%) < `--panel-2`(18.8%) < `--card`(22.0%) < `--border`(22.7%) < `--muted`(63.1%) < `--text`(87.5%) < `--heading`(100%).
- Rule 17 (simple on complex / complex on simple) — every surface in `index.css` is a single flat color; no competing textures or gradients anywhere.
- Rule 18 (container brightness limits) — HSB brightness deltas between stacked containers (`--bg`9.0%→`--panel`13.3%→`--panel-2`18.8%→`--card`22.0%) are all under 6 points, well within the ~12% dark-mode ceiling.
- Rule 19 (outer padding ≥ inner padding) — `.panel` outer padding 18px (line 111) ≥ `.field` internal spacing 12px (line 122); `.summary`/`.table-wrap` outer padding 18px ≥ `.cards`/`.table-head` gaps of 12px (lines 264, 425).
- Rule 23 (two typefaces max) — exactly two font families defined, `--sans` and `--mono` (`web/src/index.css:15-16`), used consistently.
- Rule 25 (avoid adjacent hard divides) — no cases of two borders/background-transitions touching directly; dividers (`.field` border-top, `.presets` border-top) are always separated by padding from any other border.
- Rule 27 (don't mix depth techniques) — the app uses exactly one depth technique (flat 1px borders, plus the single Rule-26-flagged ring shadow) — no mixing of soft/hard/glow styles.
### Notes
- The whole surface has exactly one token file (`web/src/index.css` `:root`, lines 1-18) — good centralization, no per-component duplicate palettes found in `App.tsx`, `FilterPanel.tsx`, `GameTable.tsx`, `SummaryCards.tsx`, or `TimelineChart.tsx` (no inline `style=` usage in any `.tsx` file).
- `--accent-bg` is referenced with a fallback (`web/src/index.css:254`, `var(--accent-bg, rgba(102, 192, 244, 0.12))`) but never defined in `:root` — it always resolves to the fallback. Not one of the 28 rules, but worth fixing alongside Rule 11 token consolidation since it's effectively a phantom token.
- Several violations (Rule 11 spacing scale, Rule 24 corner nesting, Rule 22 button padding) share a root cause: values were chosen per-component rather than pulled from a shared scale. Fixing Rule 11 first (defining a spacing/radius scale) would make the Rule 22/24 fixes mechanical rather than case-by-case.
- `web/coverage/` and `web/dist/` contain generated/build CSS (`base.css`, `prettify.css`, `assets/index-*.css`) — excluded from this audit since they're build artifacts, not source.

165
DOCS-next-session.md Normal file
View File

@ -0,0 +1,165 @@
# Next session: manual-pick rules, then finish the 250-line cap
Paste this whole file as the first prompt of a fresh session. Everything needed
is below; you should not need the previous session's transcript.
Repo: `~/steam-backlog-enforcer`. Work on `main`. HEAD is `0f25fba`.
---
## Task 1 (do this first): change the manual-pick rules
kuhy hit this and wants it changed:
```
./run.sh pick-manual 3164500 -> Error: you already have 3 manual pick(s).
./run.sh abandon-pick 495890 -> Grace period EXPIRED ... closed 3.7 day(s) ago.
./run.sh abandon-pick 618510 -> Grace period EXPIRED ... closed 3.3 day(s) ago.
```
He is stuck: three picks locked in, and the 4-day grace window closed on two of
them, so nothing can be abandoned and no new pick can be added.
**The change he asked for, verbatim:**
"allow to abandon pick at any time but reduce number of manual picks to max of 2"
So:
1. **Abandon at any time** — drop the grace-window restriction entirely.
- `_manual_pick_lifecycle.py:73` `can_abandon_manual_pick()` currently returns
`remaining is not None and remaining > 0`. It should return whether the pick
exists at all, regardless of elapsed time.
- `_manual_pick_lifecycle.py:87` `abandon_manual_pick()` has the matching
guard; the docstring says "if still inside its grace window" — update it.
- `MANUAL_GRACE_DAYS = 4` is declared in **two** places:
`_actions.py:33` and `_manual_pick_lifecycle.py:40`. Decide whether the
constant still has a purpose after this change (see "open question" below)
and do not leave two divergent copies.
- `manual_pick_grace_remaining()` (`_manual_pick_lifecycle.py:47`) also feeds
`status_payload()` at line 162 as `grace_days_left`, and `run.sh status`
prints "undoable for N more day(s)". If grace no longer gates anything,
that display is now a lie — either drop it or restate it.
2. **Max picks 3 → 2** — `config.py:63`, `max_manual_picks: int = 3`.
- kuhy's `~/.config/steam_backlog_enforcer/config.json` does **not** set this
key, so it takes the default; changing the default is sufficient and no
config migration is needed. (Verified.)
**Open question — ask kuhy before implementing, it changes the design:**
He currently has **3** picks locked and the new max is **2**. What should happen
to the existing over-cap state? Options: leave the 3 in place and simply refuse
new picks until he abandons down to 2 (least surprising); or force him to
abandon one now. `manual_pick_slots_left()` (`_actions.py:89`) returns
`max_picks - len(picks)` and will go negative — check every caller handles that,
particularly `main/picks.py:109`.
**Verify by running, not just tests** (repo CLAUDE.md requires this):
```
./run.sh status # read-only, safe
./run.sh abandon-pick 495890 # MUTATES STATE - confirm with kuhy first
```
`./run.sh` with **no args defaults to `done`**, which completes the current game
and reassigns. Never run it bare. `status` and `list` are the read-only ones.
---
## Task 2: finish the 250-line cap campaign (user-approved, in progress)
Three files remain. The gate is `bash scripts/check_file_length.sh --all`
(**trust the exit code only** — `| tail` and `grep -c` both report misleadingly).
```
_stats.py: 523 lines (over by 273)
scanning.py: 521 lines (over by 271)
_web_dataset.py: 436 lines (over by 186)
```
Everything else in `pre-commit run --all-files` already passes, so these three
are the only thing between the repo and a working `git push`.
### The method that works: leaf-set extraction ONLY
Extract functions that call **nothing else in their own module**. That keeps
every new import one-way, so no cycle is possible. Splitting on any other
boundary produced circular imports three separate times, and one of them broke
`./run.sh` while kuhy was using it.
Helper scripts from the last session (outside the repo, so they don't dirty git):
`/tmp/claude-1000/-home-kuhy/0f40b913-131e-43b9-b353-23de8c80a8c8/scratchpad/`
- `leaf_split.py` — run with just a module name to LIST its leaves; add
`--new <mod> --doc "..." --names ... --apply` to perform the split.
- `missing_names.py` — reports names a module uses but never binds. Run it on
`steam_backlog_enforcer/*.py` after every split; this catches the fallout
immediately instead of via 30 confusing test failures.
- `retarget.py`, `add_getattr.py` — of limited value, see the warning below.
If `/tmp` has been cleared, `leaf_split.py` is ~90 lines and easy to rewrite:
parse the module, find `FunctionDef`s whose bodies call no sibling function,
move those plus a copy of the module header into a new file, and add a
top-level `from ... import (...)` back in the original.
### Traps that cost the last session hours
- **`_web_dataset` is the hard one.** Its dataclasses (`WebGame`,
`WebStateInfo`, `WebDataset`, `PaceVsHLTB`, `DefaultSummary`, `WebDefaults`)
are used by both the parent and the helpers, so they must go to their own
**leaf model module** that both import — not stay in the parent. Getting this
wrong is what broke `run.sh`.
- **`ruff-format` silently deletes bottom-of-file re-export imports** (the
comment above them survives, so it looks intact). Put re-exports at the top,
or serve them through a module `__getattr__`.
- **A module `__getattr__` re-export** needs: `importlib.import_module` (a call,
so no `PLC0415`), and a `_Reexport: TypeAlias = Any` return annotation —
`object` fails mypy at the call sites, bare `Any` fails ruff `ANN401`.
Working examples already in the tree: `_actions.py`, `_whitelist.py`,
`hltb.py`, `_cmd_done.py`.
- **Patch-target rule**: the target is the module whose **source file contains
the function under test** — not where a helper is defined, not where the test
imports from. Fix these **per file from the pytest failure list**. Six
attempts at bulk regex retargeting had a net effect of −11 tests; per-file
diagnosis worked every single time.
- **`@dataclass` decorators**: when copying a module header, include decorator
lines in the boundary or you strand a bare `@dataclass` (fails with a
confusing `'function' object has no attribute '__mro__'`).
- **Stale `.mypy_cache`** after moving symbols → `AssertionError: Cannot find
component`. `rm -rf .mypy_cache`; it is not a real error.
### Non-negotiables
- **Use `.ci-mirror-venv/bin/python` for pytest**, not system python3 — the
latter has mcp 2.0 against a pinned mcp<2, so `test_mcp*.py` cannot collect
and coverage reads ~97% instead of 100%.
- Baseline to preserve: **1347 tests, 100.00% coverage**.
- **Verify with the coverage run before committing**, not `--no-cov`. Last
session committed 3 failures after misreading a `--no-cov` run as green.
- Never `--no-verify`. Never add `# noqa` without asking.
- **The service auto-restarts from this tree** (`Restart=always`,
`PYTHONPATH` pointed here). After any edit that could break imports, run
`PYTHONPATH=. python3 -c "import steam_backlog_enforcer.main"` before moving
on, and check `systemctl show steam-backlog-enforcer -p NRestarts --value`.
- **Test runs can write to real config.** Two leaks were found and sealed last
session (`exception_audit.log`, `owned_app_ids_cache.json`), both caused by a
path constant moving to a new module while `tests/conftest.py` kept patching
the old one. After any split that moves a module-level path constant, md5sum
`~/.config/steam_backlog_enforcer` before and after a full suite run.
### When the cap is clear
`git push` (13 commits are waiting, including another session's `2ad8752` for
`install.sh`). The pre-push ci-mirror runs `pre-commit --all-files` over the
whole tree, which is why the cap has to be fully clear first.
---
## Context: what the last session actually fixed (already committed, verified)
`21fe2f7` + `919ecdf` — the enforcer was launching Steam **as root**, so Steam
answered with a `zenity` modal reading "Cannot run as root user" on kuhy's
screen. Root cause: `game_install._get_real_user()` read only `SUDO_USER`/`USER`,
neither of which systemd sets, so it returned `None` and every
`geteuid() == 0 and real_user` guard fell through to running as root. It also
caused a retry storm — 1656 attempts in 30 minutes at 1.5 GB RSS. Both fixed and
verified in production (`sudo -u kuhy env DISPLAY=:0 ... steam`, every Steam
process at uid 1000, 0 retries, ~244 MB).
Do not re-litigate that fix; it is done. Tasks 1 and 2 above are what remains.

View File

@ -94,3 +94,5 @@ A refactor without a gate silently regrows. Before this task is done:
## Verify ## Verify
Run the suite, then run the enforcer entry point. Do NOT restart the systemd unit or Steam while a game is running — ask first. Run the suite, then run the enforcer entry point. Do NOT restart the systemd unit or Steam while a game is running — ask first.
REMOVE ME AFTER FINISH

View File

@ -133,3 +133,5 @@ something the name does not.
4. Every suppression you added is listed in the final report with its 4. Every suppression you added is listed in the final report with its
justification. If you could not reach 10.00 without a suppression the user justification. If you could not reach 10.00 without a suppression the user
would not accept, say so and report the real number instead. would not accept, say so and report the real number instead.
REMOVE ME AFTER FINISH

View File

@ -180,3 +180,5 @@ Long jobs (`scripts/ci_mirror.sh`, full-suite runs) go in the background with
Run the enforcer entry point, show the output, and confirm with the user. Run the enforcer entry point, show the output, and confirm with the user.
**Do NOT restart the systemd unit or Steam** — a real total-block/manual-pick **Do NOT restart the systemd unit or Steam** — a real total-block/manual-pick
lock may be active on this machine; ask first. lock may be active on this machine; ask first.
REMOVE ME AFTER FINISH

32
scripts/check_md_naming.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# ============================================================================
# Fail if any markdown file breaks the shared naming convention.
#
# Thin delegate to the shared gate in ~/utils, which owns the four namespaces
# (README / CLAUDE* / DOCS* / TODO*), the "REMOVE ME AFTER FINISH" marker rule
# and the exemption list. Copying that logic here is what lets one repo's idea
# of a valid name drift from every other repo's -- so this script only locates
# the shared checker and forwards its arguments.
#
# Usage:
# scripts/check_md_naming.sh <file> [<file> ...] # pre-commit passes these
# scripts/check_md_naming.sh --all # whole tree, from cwd
# ============================================================================
set -euo pipefail
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_md_naming.sh"
main() {
if [[ ! -x "$SHARED_GATE" ]]; then
echo "Error: shared markdown-naming gate not found at $SHARED_GATE" >&2
echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2
echo " UTILS_ROOT to where it lives." >&2
exit 1
fi
exec bash "$SHARED_GATE" "$@"
}
main "$@"

View File

@ -18,13 +18,13 @@ Logic should be as follows:
test the functionality with 489830 (The Elder Scrolls V: Skyrim Special Edition) test the functionality with 489830 (The Elder Scrolls V: Skyrim Special Edition)
as always first write full functionality confirm that it works alone and with the user and only AFTER that write tests and coverage and fix issues as always first write full functionality confirm that it works alone and with the user and only AFTER that write tests and coverage and fix issues
## Grace period (added 2026-07-19) ## Abandoning a pick (added 2026-07-19, grace window removed 2026-08-24)
A manual pick can be a mistake, and with no way out the user is stuck for the A manual pick can be a mistake, and with no way out the user is stuck for the
full 2 weeks. There is now a short mistake-correction window: full 2 weeks. A pick is never irreversible:
- `MANUAL_GRACE_DAYS = 4` — for 4 days after the pick, `abandon-pick <app_id>` - `abandon-pick <app_id>` backs out of a pick at any time, however long ago
backs out of it. Outside that window the command refuses and exits 1. it was made. There is no grace window.
- The app_id must be passed explicitly and must match the active pick, so an - The app_id must be passed explicitly and must match the active pick, so an
abandon cannot be triggered by muscle memory. abandon cannot be triggered by muscle memory.
- `abandon-pick` is in `_MANUAL_LOCK_EXEMPT_COMMANDS` — otherwise the - `abandon-pick` is in `_MANUAL_LOCK_EXEMPT_COMMANDS` — otherwise the
@ -36,12 +36,12 @@ full 2 weeks. There is now a short mistake-correction window:
`apply_manual_pick` rule, so the MCP `abandon_pick` tool can reuse it. Both `apply_manual_pick` rule, so the MCP `abandon_pick` tool can reuse it. Both
MCP tools stay gated behind `confirm=True`. MCP tools stay gated behind `confirm=True`.
The lock-active message advertises `abandon-pick` only while the window is The lock-active message advertises `abandon-pick` for every active pick, and
still open, and the `pick-manual` warning mentions it up front. the `pick-manual` warning mentions it up front.
## Concurrent manual picks (added 2026-07-19) ## Concurrent manual picks (added 2026-07-19)
`Config.max_manual_picks` (default 3) is how many games may be locked in at `Config.max_manual_picks` (default 2) is how many games may be locked in at
once. All active picks stay installed, visible and un-killed; the lock once. All active picks stay installed, visible and un-killed; the lock
releases only when every pick is finished or past its own 14-day deadline. releases only when every pick is finished or past its own 14-day deadline.