Compare commits

...

10 Commits

Author SHA1 Message Date
78e3c34d8d add README
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:57:45 -04:00
2e631136c6 pin build deps in requirements.txt for native host build
The handbook is served natively (no Docker) on the Gitea box: a python
venv runs mkdocs build, and the reverse proxy serves the static site/ at
docs.rethinkstudios.io. Pin the build deps so the host venv reproduces the
build exactly:

  pip install -r requirements.txt && mkdocs build

Verified the host rebuild flow from scratch: fresh venv + clean
pip install -r requirements.txt + mkdocs build --strict builds all 6 pages
with no errors. Static output uses relative URLs and site_url is the docs
subdomain, so it serves correctly behind the proxy at the subdomain root.
site/ and .venv/ stay gitignored.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:56:50 -04:00
613d8f28ea add Virtual environments page (project isolation + pyenv)
New docs/environments.md, nav 'Virtual environments' (before Deploy):
- the rule: never touch system Python (danger callout)
- project-based isolation as Local .venv / Makefile-driven / Docker tabs,
  each with a runnable snippet; Docker ties back to the deploy standard
- local dev with pyenv: why, official install link, shell init with
  annotated lines, everyday use, per-project .python-version
- shell quality-of-life extras (flake8 alias, .local/bin + npm-global PATH),
  cross-ref'd to Standards and Workflow

workflow.md pyenv bullet now points at the new page; index.md gains a card.

Verified in-browser: tabs switch (Local/Makefile/Docker), annotations and
admonitions render; mkdocs build --strict clean (cross-ref anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:54:31 -04:00
e684ac2853 update deploy guide per spec + add licensing standard
deploy.md:
- fix logs path to /srv/logs/<dev>/<project>
- reframe permissions as a deployer-side heads-up (bind-mount ownership is
  handled at deploy time; 'if your bot won't start or logs vanish, flag us')
  instead of a dev task / heavy footgun
- git in the image only when the container needs it (host always has git)
- NEW: layer caching (requirements before code copy)
- NEW: subprocess/browser workloads — init:true (tini + PID-1 shell-wrapper
  gotcha), shm_size 2gb, mem_limit; with code annotations and a warning
- refresh the compose-needs checklist accordingly

standards.md:
- NEW: licensing — no per-file headers; single top-level LICENSE only when a
  repo is for outside use

Verified: mkdocs build --strict clean; new deploy sections rendered.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:51:07 -04:00
4fd19bf620 rename to 'rethink development', brighten lib table, drop stale note
- Rename site 'a rethink development' -> 'rethink development' (site_name,
  landing H1); copyright -> 'rethink development (handbook)'.
- libraries.md: remove the 'if the live list is empty' admonition now that
  Gitea CORS is fixed.
- extra.css: improve Library-column readability — brighter #cfe6ff code
  chips on a cyan-tinted bg, more weight, roomier cell padding (0.7/1em),
  nowrap lib names, accent-colored links.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:35:16 -04:00
e5ca1c0ada style the live libraries table + add Language column
- libraries.md: render the live lib list as a 3-column table (Library /
  What it does / Language), wrapped in .rt-lib-table so it picks up
  explicit styling (Material only auto-styles markdown tables, not
  innerHTML-injected ones — that's why it rendered bare before).
- extra.css: branded table (navy header, blue borders, hover rows,
  code-styled linked names) and a blue Language badge.

Verified in-browser against real Gitea API data (14 repos fetched,
handbook denylisted -> 13 libs): table renders sorted with language
badges, matches the theme. Live load on prod still needs Gitea CORS
(app.ini [cors] ALLOW_DOMAIN = the docs origin).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:26:11 -04:00
8fc81fc4f4 add Gitea-style admonitions, code annotations, callouts
Make the things that matter stand out, matching the Gitea callout look:
- extra.css: brand-orange warning admonitions (#f57c00, the lambda orange),
  hotter danger (#e8590c), blue note/info and cyan tip/example, plus blue
  code-annotation markers.
- deploy.md: footgun -> danger callout, secrets -> orange warning, eligible
  -> tip; Dockerfile and compose gain numbered code annotations explaining
  each magic line; paths/mounts as a table; a restart snippet for rotation.
- workflow.md: warning on setting per-repo git identity before first commit;
  tip elevating verify-by-executing.

Verified in-browser: computed border colors match (warning #f57c00,
danger #e8590c), 4 annotation markers render in brand blue, admonition
icons + tinted headers match the Gitea style. mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:16:02 -04:00
142a0dbff6 add lambda logo, fold sections, add python/error/log snippets
- Pull the rethink lambda logo (assets/img/logo.svg) into docs/assets and
  wire it as theme.logo + favicon — matches the Gitea brand mark.
- standards.md: fold six thin sections into three fuller ones (Files and
  style / Documentation / Quality and error handling), each illustrated
  with python snippets, a flake8 output block, Do/Don't tabbed examples,
  a traceback, and a log line. Admonitions for the run-it-locally tip and
  the lib-logging note.
- libraries.md: add a collapsible 'using a library' example (pyproject
  pin + import/usage python snippet).

Verified in-browser: logo renders in the header, snippets/tabs/traceback/
log blocks render against the dark theme, libraries example expands.
mkdocs build --strict clean.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:09:30 -04:00
da47923088 theme handbook to match rethink Gitea + rename to 'a rethink development'
Mirror the rethink Gitea theme (data-theme=rethink) on the docs site:
- docs/stylesheets/extra.css maps the exact palette onto Material's slate
  scheme: body #0a0f1f, nav #061541, text #eef1f6, primary #569bcc,
  accent #55bbff, borders #294274; square corners and blue card/table
  borders echoing the Gitea repo panels.
- Force dark-only (drop the light/dark toggle; Gitea is dark-only).
- Rename site to lowercase 'a rethink development' (site_name, copyright,
  landing H1).

Verified: mkdocs build --strict clean; rendered landing + content pages
in-browser against the live Gitea page — colors, header bar, links, and
cards match.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:05:39 -04:00
5e787edbb0 add workflow page — dev-flow centerpiece
New docs/workflow.md covering how we actually work day to day:
- why git / why WSL2 (our reasons, links out for generic install)
- signing up on our Gitea (self-serve register, SSH + deploy-key model)
- our git vs public git: per-repo identity, signed commits, develop->main
- our-flavored git basics (everyday loop, commit small/often, gl graph)
- the dev workflow: plan-in-chat / build-in-Claude-Code, tmux split,
  claudedo voice control, the .claude/ project convention, AI-assist
  habits (git diff | clip.exe)
- recommended setup (WSL + VS Code + Claude Code, pyenv, flake8)
- handy .zshrc/.bashrc snippets with prose on the high-value ones

Claude-first but soft: any AI works, we recommend Claude, and the
.claude/ structure is built on that preference. Sanitized with
placeholders throughout. Nav: Home / Libraries / Standards / Workflow /
Deploy; landing card grid updated.

Verified: mkdocs build --strict clean.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:00:12 -04:00
11 changed files with 956 additions and 97 deletions

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# handbook
The Rethink Studios handbook — our public reference for the shared library suite,
coding standards, the dev workflow, Python environments, and how to deploy on our
network.
Live at **[docs.rethinkstudios.io](https://docs.rethinkstudios.io)**.
## What this is
A static documentation site built with [MkDocs](https://www.mkdocs.org/) +
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). Markdown lives
under `docs/`, builds to a static `site/`, and is served by the reverse proxy at
the subdomain — no app process, just static HTML/CSS/JS.
The **Libraries** page is the one dynamic part: it fetches the `rethink-public`
repo list from the Gitea API client-side at view time, so new libraries appear on
the next page load with no rebuild. Everything else is static markdown.
## Build
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
mkdocs build # -> static site/
mkdocs serve # local preview at http://127.0.0.1:8000
```
## Contributing
- One topic per page, grouped under `docs/`, wired into the nav in `mkdocs.yml`.
- **Public — sanitize:** no real hostnames, internal IPs, secrets, or exact
topology. Use placeholders (`<dev>`, `<project>`, `/srv/...`).
- Markdown: trailing newline, no trailing whitespace, LF line endings.
- Commits signed (`git commit -s`).

10
docs/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -4,76 +4,120 @@
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
> you've built.
**Eligible:** APIs, websites, applets, bots, monitors.
The whole network runs on a few simple, consistent rules. Get your container to
follow them and deploying is mostly handing us a `compose.yaml`.
!!! tip "Eligible"
APIs, websites, applets, bots, monitors. The whole network runs on a few
simple, consistent rules — get your container to follow them and deploying is
mostly handing us a `compose.yaml`.
## Docker — the services account
Every service runs containerized as the shared **`services`** account:
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
runs cleanly as that account:
- `user: "1337:1337"` in compose.
- `chmod -R a+rwX /app` in the Dockerfile (covers non-mounted dirs — see the
bind-mount note below).
- `HOME=/tmp`.
- **No** in-container `user`/`useradd` — don't bake a user into the image.
runs cleanly as that account.
```dockerfile
FROM python:3.12-slim
ENV HOME=/tmp
ENV HOME=/tmp # (1)!
WORKDIR /app
# git in the build if you pip-install from git
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* # (2)!
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # (3)!
COPY . .
RUN pip install --no-cache-dir . \
&& chmod -R a+rwX /app
RUN chmod -R a+rwX /app # (4)!
CMD ["python", "-m", "yourapp"]
```
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
`$HOME` (caches, configs) needs a writable target.
2. Include `git` **only if the container itself needs it** — e.g. you
`pip install` from git, or the app shells out to git at runtime. The build and
host always have git; this line is about what's *inside* the image.
3. Install deps **before** copying the code (see [layer
caching](#layer-caching)).
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
"uid-agnostic" means.
```yaml
services:
yourapp:
build: .
user: "1337:1337"
user: "1337:1337" # (1)!
environment:
HOME: /tmp
volumes:
- /srv/configs/<project>:/app/config:ro # host-managed, read-only
- /srv/<dev>/<project>:/app/logs # live + rolled logs
- yourapp-data:/app/data # named volume — the rest
- /srv/configs/<project>:/app/config:ro # (2)!
- /srv/logs/<dev>/<project>:/app/logs # (3)!
- yourapp-data:/app/data # (4)!
volumes:
yourapp-data:
```
1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
user into the image; set it here.
2. Configs: host-managed bind mount, mounted **read-only**.
3. Logs: bind mount — live and rolled, scraped for monitoring.
4. Everything else: a **named volume**. Docker owns it, so there are no host
permissions to fiddle with.
## Paths and mounts
- **Configs**`/srv/configs/<project>/` — bind mount, host-managed.
- **Logs**`/srv/<dev>/<project>/` — bind mount; live and rolled, scraped for
monitoring.
- **Everything else** (caches, browser profiles, scratch) → **named volumes**.
Docker manages ownership, so there are no host permissions to fiddle with.
| What | Where | How |
| --- | --- | --- |
| Configs | `/srv/configs/<project>/` | bind mount, host-managed, read-only |
| Logs | `/srv/logs/<dev>/<project>/` | bind mount; live + rolled, scraped |
| Caches, profiles, scratch | named volume | Docker manages ownership |
## Permissions — the bind-mount footgun
!!! note "If your service won't start or its logs aren't persisting"
That's usually a host-side bind-mount **ownership** thing — the kind of detail
**we sort out at deploy time**, not something you need to chown or provision.
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix
the mount perms. Stick to a clean `compose.yaml` and let us handle the host.
`docker compose up` does **not** create bind-mount directories as you. If a
bind-mount source is missing, the Docker daemon (**root**) creates it **as
root** — and your container (**1337**) then can't write it. Logs fall back to
console-only, caches re-download every run.
## Layer caching
So:
Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches
layers in order, so deps only reinstall when `requirements.txt` changes — not on
every code edit. Get this backwards and every one-line change triggers a full
dependency reinstall.
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned _before_
`up`.** This is handled at provisioning, not a per-deploy chown hook.
- **Named volumes avoid this entirely** — use them for anything that doesn't need
host visibility.
- The Dockerfile `chmod -R a+rwX /app` only covers **non-mounted** dirs. A bind
mount overrides the image directory with the host directory, so for mounted
paths the **host-side ownership wins**.
```dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
COPY . . # changes every build
```
## Subprocess and browser workloads
Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
knobs in compose:
```yaml
services:
yourbot:
build: .
user: "1337:1337"
init: true # (1)!
shm_size: "2gb" # (2)!
mem_limit: "4g" # (3)!
```
1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals.
Without it, spawned Chrome/Xvfb processes leak as zombies.
2. Chrome and most headless browsers crash on Docker's default **64 MB**
`/dev/shm`. Bump it for any browser workload.
3. Bound memory — especially when each worker spawns a browser. Raise it as
worker count grows.
!!! warning "The PID-1 gotcha with shell-wrapper CMDs"
If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not**
be PID 1, or the real process dies on startup. `init: true` is exactly what
fixes this — tini takes PID 1, and your wrapper runs as a normal child.
## What your compose / Dockerfile needs
@ -83,16 +127,21 @@ So:
- secrets bind-mounted **`:ro`**
- `HOME=/tmp`
- `chmod -R a+rwX /app`
- `git` in the build if you `pip install` from git
- deps installed **before** the code copy (layer caching)
- `git` in the image **if the container needs it**
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
## Secrets
We do **not** commit secrets (usually, lol). The rule:
- Secrets stay **gitignored**.
- They're placed on the host at `/srv/configs/<project>/`.
- They're bind-mounted **read-only** at runtime.
- **Never** baked into the image — add them to `.dockerignore` so `COPY . .`
can't grab them.
!!! warning "Secrets never go in the image"
We do **not** commit secrets (usually, lol). They stay **gitignored**, live on
the host at `/srv/configs/<project>/`, and are bind-mounted **read-only** at
runtime. Add them to `.dockerignore` so a `COPY . .` can't sweep them into a
layer.
Rotating a secret = edit the host file and restart. No rebuild.
```bash
vim /srv/configs/<project>/secrets.env # edit on the host
docker compose restart yourapp # pick up the change — no rebuild
```

145
docs/environments.md Normal file
View File

@ -0,0 +1,145 @@
# Virtual environments
How we keep a project's Python isolated — from the system Python and from every
other project. The rule underneath all of it: **never install into, or upgrade,
the system Python.** The OS depends on it; a project must never touch it.
!!! danger "Leave the system Python alone"
No `sudo pip install`, no upgrading the system interpreter for a project. If a
project needs a different version or a package, that goes in a **virtual
environment** — never the system one. Breaking system Python can break the OS.
## Project-based isolation
Every project runs against its own isolated environment. There are three ways that
happens, depending on where the project runs:
=== "Local `.venv`"
A virtualenv living in the repo. Simplest for day-to-day local work.
```bash
python -m venv .venv # create it (once)
source .venv/bin/activate # activate for this shell
pip install -r requirements.txt
```
Keep `.venv/` **gitignored** — it's per-machine, never committed.
=== "Makefile-driven"
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
no "did you activate it?" drift.
```makefile
VENV := .venv
PY := $(VENV)/bin/python
$(VENV): requirements.txt
python -m venv $(VENV)
$(PY) -m pip install -r requirements.txt
.PHONY: run
run: $(VENV)
$(PY) -m yourapp
```
`make run` creates the venv if missing, installs deps, and runs — all against
the isolated interpreter, no manual activation.
=== "Docker"
The container **is** the isolation — its own filesystem, its own interpreter,
nothing shared with the host. You don't need a `.venv` inside an image; install
straight into the container's Python.
```dockerfile
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
```
This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, mounts, layer caching).
!!! tip "Which one?"
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
setup across the team, **Docker** for anything that ships. They're not
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
deploy.
## Local dev with pyenv
For local work you also need the right **Python version**, not just isolated deps.
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
switches versions per-project without touching the system Python.
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
auto-select that interpreter when you `cd` in — everyone on the project runs the
same one.
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
isolated version *and* isolated deps.
### Install
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
for the installer and build dependencies — no point reproducing it here. Then add
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
### Shell init
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
auto-version-switching won't work:
```bash
# pyenv — Python version management
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)" # (1)!
eval "$(pyenv init -)" # (2)!
eval "$(pyenv virtualenv-init -)" # (3)!
```
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
version instead of the system one.
2. Shell integration — command rehashing and completion.
3. Auto-activates a project's virtualenv on `cd`**only** if you use
`pyenv-virtualenv`. Drop this line if you don't.
### Everyday use
```bash
pyenv install 3.10.14 # install a version (one-time)
pyenv install --list # see available versions
cd <project>
pyenv local 3.10.14 # writes .python-version -> auto-selects here
python --version # confirms the pinned version
```
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
matches.
- **`pyenv global <ver>`** for your default outside any project.
## Shell quality-of-life extras
A couple of optional lines worth having alongside pyenv:
```bash
# flake8 with our shared config (max line 120)
alias flake8='flake8 --config ~/.config/flake8'
# local bins on PATH (pip --user installs, npm globals)
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
- The **flake8 alias** keeps everyone linting with the same config (our 120
max-line, etc. — see [Standards](standards.md#files-and-style)).
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
`pip install --user` or a global npm install.
!!! note "More shell setup"
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
the [Workflow](workflow.md#handy-shell-setup) page.

View File

@ -1,4 +1,4 @@
# Rethink Studios Handbook
# rethink development
The public reference for building and shipping with Rethink Studios: our shared
libraries, our coding standards, and how to deploy a project on our network.
@ -25,6 +25,20 @@ out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
House coding standards — file hygiene, docstrings, type hints, linting, and
how we handle errors.
- :material-sitemap: __[Workflow](workflow.md)__
---
How we actually work day to day — our Gitea, git habits, and the
plan-in-chat / build-in-Claude-Code flow, plus shell setup.
- :material-language-python: __[Virtual environments](environments.md)__
---
Project-based Python isolation — local `.venv`, Makefile, or Docker — and
local version management with pyenv.
- :material-rocket-launch: __[Deploy](deploy.md)__
---

View File

@ -11,10 +11,44 @@ the repo, where the README and tags live.
## Install
Pin a tag in your dependencies — never an unpinned branch:
```
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
```
??? example "Using a library — install, import, go"
Add it to your project's deps (e.g. `pyproject.toml`):
```toml
[project]
dependencies = [
"aioweb @ git+https://git.rethinkstudios.io/rethink-public/aioweb.git@v0.3.1",
]
```
Then use it — the README in each repo has the real surface; this is the shape:
```python
import asyncio
from aioweb import Session
async def main() -> None:
"""fetch a page through the shared async http session"""
async with Session() as web:
resp = await web.get("https://example.test")
print(resp.status, len(resp.content))
asyncio.run(main())
```
Need to bump a lib? Change the `@<tag>` and reinstall — versions live with the
lib, not in these docs.
<div id="lib-list" markdown="0">
<p class="lib-status">Loading libraries from Gitea…</p>
</div>
@ -55,12 +89,17 @@ the repo, where the README and tags live.
var rows = libs.map(function (r) {
var url = r.html_url || (REPO_BASE + "/" + r.name);
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
var lang = r.language
? '<span class="rt-lang">' + escapeHtml(r.language) + "</span>"
: '<span class="rt-lang rt-lang--none"></span>';
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
escapeHtml(r.name) + '</code></a></td><td>' + desc + '</td></tr>';
escapeHtml(r.name) + "</code></a></td><td>" + desc +
"</td><td>" + lang + "</td></tr>";
}).join("");
el.innerHTML =
'<table><thead><tr><th>Library</th><th>What it does</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>';
'<div class="rt-lib-table"><table>' +
"<thead><tr><th>Library</th><th>What it does</th><th>Language</th></tr></thead>" +
"<tbody>" + rows + "</tbody></table></div>";
}
function fail() {
@ -84,11 +123,3 @@ the repo, where the README and tags live.
}
})();
</script>
!!! info "If the live list is empty or stale"
The fetch runs in your browser against the Gitea API and needs the org repos
to be readable unauthenticated (they are — it's a public org) and CORS to
allow the docs domain. If the list won't load, browse the org directly at
[git.rethinkstudios.io/rethink-public](https://git.rethinkstudios.io/rethink-public).
The fallback for a disabled live fetch is a webhook rebuild — a push to any
`rethink-public` repo triggers a site rebuild.

View File

@ -3,40 +3,129 @@
The house standards every Rethink Studios project follows. They keep our code
consistent, readable, and predictable across the whole suite.
## File hygiene
## Files and style
- Files end with a **single trailing newline** (LF / Unix line endings).
- **No trailing whitespace** on any line.
- Files end with a **single trailing newline** (LF / Unix line endings), and
carry **no trailing whitespace** on any line.
- **4 spaces** for indentation by default. Respect language norms — TS/JS use
**2 spaces**, Go uses **tabs** — and when editing an existing file, follow that
file's existing indentation.
- **flake8 clean**, max line length **120**. **Type hints** on public functions.
## Indentation
A well-formed module — module docstring, type hints, lowercase-start docstring:
- **4 spaces** by default.
- Respect language norms: TS/JS use **2 spaces**, Go uses **tabs**.
- When editing an existing file, follow that file's existing indentation.
```python
"""async key/value store backed by a single json file"""
## Docstrings and comments
from pathlib import Path
- Public functions get a **docstring**. Style: **lowercase-start, no trailing
period**.
- Keep inline comments minimal — prefer docstrings. Use an inline comment only
def load_state(path: Path) -> dict[str, str]:
"""read the json state file, returning an empty dict if it's missing"""
if not path.exists():
return {}
return _read_json(path)
```
flake8 tells you the moment you drift — keep the tree clean:
```text
$ flake8
./aiokv/store.py:14:80: E501 line too long (96 > 79 characters)
./aiokv/store.py:22:1: F401 'json' imported but unused
./aiokv/store.py:31:5: E303 too many blank lines (3)
```
!!! tip "Run it locally"
Wire the shared config into an alias so every project lints the same way:
`alias flake8='flake8 --config ~/.config/flake8'` (max line 120). See the
[Workflow](workflow.md#handy-shell-setup) page.
## Documentation
- Public functions get a **docstring****lowercase-start, no trailing period**.
- Keep inline comments minimal; prefer docstrings. Use an inline comment only
where the code is genuinely complex.
- Each module/file states its scope and purpose via a **module docstring**
(header string) — **not** a license or copyright header.
- Every library and project has a **README** with the install line, what it does,
and a usage example.
## READMEs
!!! note "Licensing — no per-file headers"
Don't prepend license/copyright boilerplate to source files. If a repo needs a
license, it's a single top-level **`LICENSE`** file — never repeated per file.
Most internal repos don't carry one; add it only when a repo is meant for
outside use and the terms are decided.
Every library and project has a defined **README** covering:
=== "Do"
- the install line,
- what it does,
- a usage example.
```python
def mask_secret(value: str, keep: int = 4) -> str:
"""mask all but the last ``keep`` characters of a secret"""
if len(value) <= keep:
return "*" * len(value)
return "*" * (len(value) - keep) + value[-keep:]
```
## Linting and types
=== "Don't"
- **flake8 clean**, max line length **120**.
- **Type hints** on public functions.
```python
# Masks a secret. <- license-header-style noise, capitalized, trailing period
def mask_secret(value, keep=4): # no type hints
# loop over the chars and hide them
return "*" * (len(value) - keep) + value[-keep:] # crashes if short
```
## Error handling
## Quality and error handling
- **Fail loud** — never swallow exceptions.
- Catch the **specific** exception, and **log** it.
**Fail loud** — never swallow exceptions. Catch the **specific** exception and
**log** it; don't paper over failures with a bare `except`.
=== "Do"
```python
import logging
log = logging.getLogger(__name__)
def fetch(url: str) -> bytes:
"""fetch ``url``, logging and re-raising on failure"""
try:
return _client.get(url).content
except TimeoutError:
log.warning("fetch timed out: %s", url)
raise
```
=== "Don't"
```python
def fetch(url):
try:
return _client.get(url).content
except Exception:
pass # swallowed — the caller has no idea anything broke
```
When something does break, a loud failure gives you a real traceback to act on:
```python-traceback
Traceback (most recent call last):
File "run.py", line 42, in <module>
data = fetch("https://example.test/feed")
File "aioweb/session.py", line 88, in fetch
return self._client.get(url).content
TimeoutError: request timed out after 30s
```
…and the log line that precedes it tells you where to look:
```text
2026-06-29 14:03:11,204 WARNING aioweb.session fetch timed out: https://example.test/feed
```
!!! note "Logging belongs to the app, not the library"
Libraries **emit only**`log = logging.getLogger(__name__)` and nothing
else. Handlers, levels, and formatting are configured once at the
application entry point, so a lib never dictates how its host logs.

236
docs/stylesheets/extra.css Normal file
View File

@ -0,0 +1,236 @@
/* a rethink development mirror of the rethink Gitea theme
palette pulled from git.rethinkstudios.io (data-theme="rethink") */
:root {
--rt-body: #0a0f1f; /* deep navy page background */
--rt-nav: #061541; /* darker header/nav background */
--rt-surface: #0e1530; /* cards / code blocks, a touch above body */
--rt-text: #eef1f6; /* near-white body text */
--rt-muted: #aab4c5; /* secondary text */
--rt-primary: #569bcc; /* links / primary blue */
--rt-primary-dark: #4a8cbb;
--rt-primary-light: #6ba9d5;
--rt-accent: #55bbff; /* bright cyan-blue accent */
--rt-border: #294274; /* row dividers / borders */
}
/* Map the rethink palette onto Material's slate scheme. */
[data-md-color-scheme="slate"] {
--md-hue: 222;
--md-default-bg-color: var(--rt-body);
--md-default-fg-color: var(--rt-text);
--md-default-fg-color--light: var(--rt-muted);
--md-default-fg-color--lighter: rgba(238, 241, 246, 0.32);
--md-default-fg-color--lightest: rgba(238, 241, 246, 0.12);
--md-primary-fg-color: var(--rt-nav);
--md-primary-fg-color--light: var(--rt-primary-light);
--md-primary-fg-color--dark: var(--rt-nav);
--md-primary-bg-color: var(--rt-text);
--md-primary-bg-color--light: var(--rt-muted);
--md-accent-fg-color: var(--rt-accent);
--md-accent-fg-color--transparent: rgba(85, 187, 255, 0.1);
--md-typeset-color: var(--rt-text);
--md-typeset-a-color: var(--rt-primary);
--md-code-bg-color: var(--rt-surface);
--md-code-fg-color: #d6deeb;
--md-footer-bg-color: var(--rt-nav);
--md-footer-bg-color--dark: var(--rt-body);
}
/* Header / top nav: match Gitea's dark-blue bar. */
.md-header,
.md-tabs {
background-color: var(--rt-nav);
color: var(--rt-text);
}
/* Links hover to the bright accent, like Gitea. */
.md-typeset a:hover {
color: var(--rt-accent);
}
/* Sidebar nav active item picks up the blue. */
.md-nav__link--active,
.md-nav__item .md-nav__link--active {
color: var(--rt-primary);
}
/* Square the corners — Gitea uses radius 0 throughout. */
.md-typeset .admonition,
.md-typeset details,
.md-typeset pre > code,
.md-typeset .highlight,
.md-typeset table:not([class]),
.md-search__form,
.md-typeset .tabbed-set > input:checked + label,
.md-typeset code {
border-radius: 0;
}
/* Subtle blue row dividers in tables, echoing the repo list. */
.md-typeset table:not([class]) {
border: 1px solid var(--rt-border);
}
.md-typeset table:not([class]) th {
background-color: var(--rt-nav);
color: var(--rt-text);
}
.md-typeset table:not([class]) td {
border-top: 1px solid rgba(41, 66, 116, 0.5);
}
/* Search field on the dark bar. */
.md-search__input {
background-color: rgba(255, 255, 255, 0.06);
}
.md-search__input::placeholder {
color: rgba(238, 241, 246, 0.6);
}
/* Admonitions Gitea-style: brand-orange warning, blue note, etc.
The lambda logo's orange (#ffae42 -> #f57c00) drives the warning/danger look. */
.md-typeset .admonition,
.md-typeset details {
border-width: 0 0 0 .2rem;
background-color: var(--rt-surface);
}
/* warning / caution: the orange Gitea callout */
.md-typeset .admonition.warning,
.md-typeset details.warning,
.md-typeset .admonition.caution,
.md-typeset details.caution {
border-color: #f57c00;
}
.md-typeset .warning > .admonition-title,
.md-typeset .warning > summary,
.md-typeset .caution > .admonition-title,
.md-typeset .caution > summary {
background-color: rgba(245, 124, 0, 0.12);
}
.md-typeset .warning > .admonition-title::before,
.md-typeset .caution > .admonition-title::before {
background-color: #f57c00;
}
/* danger: a hotter orange-red for true footguns */
.md-typeset .admonition.danger,
.md-typeset details.danger {
border-color: #e8590c;
}
.md-typeset .danger > .admonition-title,
.md-typeset .danger > summary {
background-color: rgba(232, 89, 12, 0.14);
}
/* note / info / tip: blue + cyan from the rethink palette */
.md-typeset .admonition.note,
.md-typeset details.note,
.md-typeset .admonition.info,
.md-typeset details.info {
border-color: var(--rt-primary);
}
.md-typeset .note > .admonition-title,
.md-typeset .info > .admonition-title {
background-color: rgba(86, 155, 204, 0.12);
}
.md-typeset .admonition.tip,
.md-typeset details.tip,
.md-typeset .admonition.example,
.md-typeset details.example {
border-color: var(--rt-accent);
}
.md-typeset .tip > .admonition-title,
.md-typeset .example > summary {
background-color: rgba(85, 187, 255, 0.1);
}
/* Live libraries table JS-injected, so it needs explicit Material-style rules
(Material only auto-styles markdown tables, not innerHTML ones). */
.rt-lib-table {
overflow-x: auto;
margin: 1em 0;
}
.rt-lib-table table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--rt-border);
font-size: 0.74rem;
}
.rt-lib-table th,
.rt-lib-table td {
text-align: left;
padding: 0.7em 1em;
vertical-align: top;
}
.rt-lib-table td:first-child {
white-space: nowrap;
}
.rt-lib-table thead th {
background-color: var(--rt-nav);
color: var(--rt-text);
font-weight: 700;
border-bottom: 1px solid var(--rt-border);
}
.rt-lib-table tbody tr {
border-top: 1px solid rgba(41, 66, 116, 0.5);
transition: background-color 0.15s;
}
.rt-lib-table tbody tr:hover {
background-color: rgba(86, 155, 204, 0.08);
}
.rt-lib-table td code {
background-color: rgba(85, 187, 255, 0.12);
color: #cfe6ff;
padding: 0.2em 0.55em;
font-size: 0.9em;
font-weight: 600;
}
.rt-lib-table a {
color: var(--rt-accent);
font-weight: 600;
}
.rt-lib-table a:hover code {
background-color: rgba(85, 187, 255, 0.22);
color: #ffffff;
}
/* Language badge in the libs table. */
.rt-lang {
display: inline-block;
padding: 0.1em 0.5em;
border: 1px solid var(--rt-border);
border-radius: 0;
background-color: rgba(86, 155, 204, 0.1);
color: var(--rt-primary-light);
font-size: 0.85em;
white-space: nowrap;
}
.rt-lang--none {
border-color: transparent;
background-color: transparent;
color: var(--rt-muted);
}
/* Code annotation markers (the (1) callouts) in brand blue. */
.md-typeset .md-annotation__index {
background-color: var(--rt-primary);
color: var(--rt-body);
}
/* Landing-page cards: surface tint + blue border like Gitea panels. */
.md-typeset .grid.cards > :is(ul, ol) > li,
.md-typeset .grid > .card {
background-color: var(--rt-surface);
border: 1px solid var(--rt-border);
border-radius: 0;
}
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
border-color: var(--rt-primary);
}

243
docs/workflow.md Normal file
View File

@ -0,0 +1,243 @@
# Workflow
How we actually work day to day at Rethink Studios — where code lives, how we use
git, the AI-assisted dev flow we recommend, and the shell setup that ties it
together. This is the **our-flavored** version: why *we* do it this way and how
*our* setup is wired. For the truly generic parts (installing WSL, learning git),
we link the official docs rather than reteach them.
!!! info "Public, sanitized"
Examples use placeholders — `<you>`, `<key>`, `dev@<you>`, `/mnt/c/<your>/...`.
Swap in your own real values locally; never commit personal paths, key names,
or emails.
## Why git, why WSL
**Git** is the backbone of everything here. Version history and branching are the
obvious part, but our whole deploy model is built on git too: we `pip install`
libraries straight from git by tag, servers pull via per-repo deploy keys, and
changes flow `develop → main` before they ship. If you know git, you already
understand how our code moves from your machine to production.
**WSL2 (Ubuntu)** is where we develop — a real Linux toolchain on a Windows
desktop. You get native Linux tooling (the same environment our servers run) plus
the Windows apps you actually use day to day. Install WSL2 from Microsoft's docs;
this page documents the *our-setup* layer that goes on top.
- [Install WSL (Microsoft)](https://learn.microsoft.com/windows/wsl/install)
- [VS Code Remote — WSL](https://code.visualstudio.com/docs/remote/wsl)
## Signing up on our Gitea
Our code lives on **Gitea** at
[git.rethinkstudios.io](https://git.rethinkstudios.io). Two orgs you'll use:
- **[rethink-public](https://git.rethinkstudios.io/rethink-public)** — public
libraries, resources, and assets.
- **[rethink-software](https://git.rethinkstudios.io/rethink-software)** — our
applications.
**Get an account:**
1. Register at [git.rethinkstudios.io](https://git.rethinkstudios.io) and verify
your email.
2. Add your **SSH public key** under *Settings → SSH / GPG Keys* so you can clone
and push over SSH.
3. For servers, we use a **per-repo deploy-key** model rather than your personal
key — see the [Deploy guide](deploy.md) for how a box gets read access to just
the repos it needs.
## Our git vs. public git (GitHub / GitLab)
It's the same git — just a different host. The thing most devs trip on is
**identity and keys per host**: you may have a GitHub identity *and* a Gitea
identity on one machine, and commits need to be attributed (and signed/pushed)
with the right one per project.
We solve that with **per-repo local git config** — run a small alias inside a repo
to set its local user and the SSH key it pushes with (see
[per-project git identity](#per-project-git-identity) below). No global identity
juggling.
Our conventions, in short:
- **Signed commits**`git commit -s`.
- **No AI co-author trailer** on your own work. (Intern/dev work you're crediting
gets that dev's `Co-Authored-By` — nothing else.)
- **`develop` is staging**, merge to **`main`** via MR when it's ready to ship.
- **Libraries install from git by tag** — pin a version in your deps, bump the
tag when the lib releases.
!!! warning "Set your per-repo identity *before* the first commit"
One machine often carries more than one Gitea identity/key. If you forget to
run `gitsetup` in a fresh clone, your commits attribute to the wrong
user — or push with the wrong key and bounce. Run it right after cloning;
see [per-project git identity](#handy-shell-setup).
## Git basics (our-flavored)
Not a git tutorial — just how the everyday loop looks against our Gitea. For the
generic command reference, keep the
[Git cheat sheet](https://training.github.com/downloads/github-git-cheat-sheet/)
or the [Pro Git book](https://git-scm.com/book) handy.
**Clone** over SSH (the alias maps to a key — see the shell setup):
```bash
git clone git@<alias>:rethink-public/<repo>.git
```
**The everyday loop:**
```bash
git switch -c <feature> # branch off
# ...make a logical change...
git commit -s -m "..." # commit, signed
git push -u origin <feature> # push
# open an MR: develop -> main
```
**Commit small and often** — one commit per logical change, not a giant
end-of-day dump. Small commits are easier to review, revert, and `git bisect`
when something breaks.
**Read history** as a graph with the `gl` alias below:
```bash
gl # git log --graph, oneline, decorated
```
## The dev workflow
This is the part that's distinctly *ours*. AI is a force multiplier, but only with
discipline around it — the two habits that matter most are **plan before you
build** and **verify by executing, not asserting**.
You can use whatever AI you like — but we **recommend Claude** (via
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview), in the
terminal or VS Code). The whole project structure we recommend below — the
`.claude/` folder, `CLAUDE.md` instructions, numbered specs — is built on that
preference: it's designed around how Claude Code reads project context and takes
handoffs. Other tools can read these files too, but the convention assumes
Claude-first.
**Plan here, build in Claude Code.** Do the thinking in chat — plan, write the
spec, make the decisions. Then hand that spec to a Claude Code agent that does the
build: it implements, **verifies by running**, and pushes. The chat plans; the
agent implements.
**tmux + Claude Code split.** In practice that's a Claude Code agent running in a
tmux pane doing the build work while you plan/review in another. One spec in, a
verified change out.
**claudedo (optional, hands-free).** Voice control for Claude Code over tmux: a
wake-word plus local Whisper speech-to-text drives your tmux session without the
keyboard — handy when you're fullscreen or away from the desk. It's available
here:
[git.rethinkstudios.io/rethink-software/claudedo](https://git.rethinkstudios.io/rethink-software/claudedo).
**The `.claude/` project convention.** Every project has a top-level `.claude/`
folder (always gitignored — nothing under it is committed):
- **`CLAUDE.md`** — project-specific instructions for the agent (stack, layout,
conventions). Layered on top of your global instructions.
- **`compact.md`** — a running state log (done / decided / in-flight), updated at
checkpoints so a fresh session catches up fast.
- **`commands.log`** — an append-only record of shell commands run.
- **`spec/`** — numbered, per-change specs named `NN-<type>-<short>.md`
(e.g. `01-feature-logging.md`).
Tell the agent to **"setup project"** to scaffold all of that (and add `.claude/`
to `.gitignore`) in a new repo.
**AI-assist habits.** When you're stuck, give the AI the *exact* context instead
of describing it — the WSL clipboard bridge makes this trivial:
```bash
git diff | clip.exe # then paste the diff straight into the chat
```
Use AI for the plan and the spec, let the agent build, and always **prove it
works by running it** — don't accept "this should work."
!!! tip "Verify by executing, not asserting"
The single habit that separates good AI-assisted work from plausible-looking
nonsense: **run it**. A passing build, a real screenshot, actual output — that
is proof. "It should work" is not.
## Recommended setup
- **WSL2 (Ubuntu) + VS Code + Claude Code.** Develop in WSL, edit in VS Code over
[Remote — WSL](https://code.visualstudio.com/docs/remote/wsl), and run
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as your
agent (terminal or the VS Code extension).
- **[pyenv](https://github.com/pyenv/pyenv)** for per-project Python versions
(we target **3.10+**) and isolated `.venv`s — full setup on the
[Virtual environments](environments.md) page.
- **[flake8](https://flake8.pycqa.org/)** with a shared config — max line length
**120** (see the alias below).
## Handy shell setup
Copy-paste these into your `.zshrc` / `.bashrc`. Replace every placeholder with
your real values. The high-value ones are explained underneath.
```bash
# --- WSL <-> Windows bridges ---
alias explorer="explorer.exe ." # open current dir in Windows Explorer
# pipe to the Windows clipboard (great for pasting context into AI):
# git diff | clip.exe -> paste the diff straight into a chat
# auto-alias Windows .exe tools on PATH (e.g. adb/platform-tools):
export PATH="$PATH:/mnt/c/<your>/platform-tools"
for exe in /mnt/c/<your>/platform-tools/*.exe; do
alias "$(basename "${exe}" .exe)"="${exe}"
done
# --- pyenv (Python version management) ---
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# --- flake8 with a shared config ---
alias flake8='flake8 --config ~/.config/flake8'
# --- per-project git identity (switch identity per repo) ---
# set the LOCAL (per-repo) user + the key to sign/push with:
alias gitsetup='git config --local user.name "<you>"; \
git config --local user.email "dev@<you>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<key>"'
# a second identity for a different account, same pattern:
alias gitea='git config --local user.name "<alt>"; \
git config --local user.email "<alt>@<host>"; \
git config --local core.sshCommand "ssh -i $HOME/.ssh/<alt-key>"'
# --- git log graph ---
alias gl='git log --graph --abbrev-commit --pretty=oneline --decorate'
# --- local bins on PATH ---
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
```
```bash
# cherry-pick a commit onto master/main quickly
gitcs() {
if [ -z "$1" ]; then echo "Usage: gitcs <commit>"; return 1; fi
git checkout master && git cherry-pick "$1"
}
```
**The ones worth understanding:**
- **`git diff | clip.exe`** — the killer WSL trick for AI-assisted dev. Pipe your
working changes straight to the Windows clipboard and paste them into the chat
so the AI sees *exactly* what changed instead of your paraphrase of it.
- **`gitsetup` / `gitea`** — per-repo identity. One machine, multiple Gitea
identities and keys; run the alias inside a repo to set its **local** user and
signing/push key, so commits attribute correctly without touching your global
config.
- **`gl`** — a readable branch graph for understanding history at a glance.
- **`pyenv`** — per-project Python versions, so each repo builds against the
version it targets.

View File

@ -1,26 +1,20 @@
site_name: Rethink Studios Handbook
site_name: rethink development
site_description: Libraries, coding standards, and how to deploy on our network.
site_url: https://docs.rethinkstudios.io/
copyright: Rethink Studios
copyright: rethink development (handbook)
extra_css:
- stylesheets/extra.css
theme:
name: material
language: en
logo: assets/logo.svg
favicon: assets/logo.svg
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/weather-sunny
name: Switch to light mode
scheme: slate
primary: custom
accent: custom
features:
- navigation.instant
- navigation.tracking
@ -59,4 +53,6 @@ nav:
- Home: index.md
- Libraries: libraries.md
- Standards: standards.md
- Workflow: workflow.md
- Virtual environments: environments.md
- Deploy: deploy.md

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# Build dependencies for the handbook docs site.
# Host build: python -m venv .venv && . .venv/bin/activate
# pip install -r requirements.txt && mkdocs build
# Output: static site/ served by the reverse proxy at docs.rethinkstudios.io.
mkdocs-material==9.7.6 # theme — pulls mkdocs, pymdown-extensions, pygments
mkdocs==1.6.1 # pinned explicitly for a reproducible build
mkdocs-material-extensions==1.3.1
pymdown-extensions==11.0 # admonitions, tabbed, superfences, annotations
Pygments==2.20.0 # syntax highlighting in code blocks