diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d49e6d9..5b26650 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,5 +1,14 @@ # Project Instructions +## Core Rules + +- When asked to do ONE thing, do exactly that. Do not proactively migrate dependencies, refactor adjacent code, or expand scope. You may suggest further edits, but wait for confirmation before any scope expansion. +- Prefer the simplest, most localized solution. Changes should target the most-relevant section of code — for example, catch errors in the scope that best handles them rather than injecting data up or down the stack. Take time to think about the best approach rather than quickly jumping to an implementation. + +## Tool Usage Preferences + +- For simple factual lookups (package versions, release dates), use targeted, purpose-built commands and local CLI tools first before attempting web searches — e.g. `pip index versions ` for Python, `npm view versions` for Node. Prefer fast local approaches over web research. + ## Container Environment (Podman) This environment runs inside a container with access to a Podman socket shared from the host. There is no `docker` or `podman` CLI available, but you can interact with containers via the Docker-compatible API. @@ -31,6 +40,10 @@ Style preferences (when not conflicting with existing patterns): - Avoid mutation of inputs - Pure functions where practical +**Fun is welcome in moderation.** Clarity and readability come first, but the occasional reference, joke, or creative naming makes code more enjoyable to read and write. The key constraint: it must be apropos to the actual code — no random remarks. A comment that winks at a known falsehood the code knowingly embraces, or a function name that doubles as a cultural reference while accurately describing its behavior, are both fair game. Keep it sparse; if every function has a quip, none of them land. + +**Naming in test code** has different rules than implementation code. Implementation names must always be meaningful and reflective of purpose. Test code, however, may use metasyntactic variables when a value is arbitrary and meaningfulness would be misleading. Preferred metasyntactic names: `foo`, `bar`, `baz`, `frob`, `xyzzy`, and conjugations of `frobnicate`. For arbitrary magic numbers, prefer values with clean ternary representations (e.g., 72 or 243 over 128 or 255 when a test needs a fixed byte value). + **Style changes should be separate from implementation.** If you notice style inconsistencies or want to improve patterns, do so in dedicated refactor commits or branches rather than mixing with feature work. ## Test Coverage @@ -46,6 +59,18 @@ Why this matters: When adding or modifying code, verify that tests cover the new logic. If coverage drops, add tests before merging. +### Coverage Exclusions and Test Quality + +**Pure I/O code is excluded from coverage requirements.** Code whose sole purpose is performing I/O (reading files, making network calls, rendering output) cannot be effectively tested without manual interaction. However, this has a direct design implication: keep the I/O layer as thin and trivial as possible. All business logic, validation, transformation, and decision-making must live in testable modules that the I/O layer merely calls into. A fat I/O layer is a design smell, not an excuse for missing tests. + +**The value of integration testing for I/O is context-dependent** — it depends on whether I/O is incidental to the component or central to its purpose. + +When I/O is incidental (e.g., an application that loads configuration from a file), there is no value in testing the file-reading call itself — trust the language's I/O primitives. Instead, feed raw data to a pure function that handles parsing and validation. In some cases even parsing tests may be unnecessary, such as a JSON config file loaded via a standard-library routine that directly constructs application-defined structs. Structure such code to confine I/O in a short routine that can be excluded from coverage. + +When I/O *is* the core business logic (e.g., a database engine or FUSE filesystem), it must be thoroughly integration-tested against a functioning backend. The I/O layer cannot be excluded here because it is the component's reason for existing. Provision appropriate test infrastructure: a tmpfs filesystem for storage-centric tests, an Alpine testcontainer for cases that need to exercise interactions between different user permissions, or an emulated service with reliable compatibility to the real target (e.g., MinIO via testcontainers for S3-dependent code). This is preferable to either mocking away the I/O (which hides real failure modes) or leaving the logic untested. + +**Tests must exercise actual code paths, not reproduce them.** In rare cases, code is so trivial that the only apparent way to test it is to restate it in the test. Such tests verify nothing — they pass by construction and remain passing even when the code changes, which demonstrates that they provide no actual validation. Do not write these. Instead, explicitly exclude the code from coverage. Note that this situation is rare and usually signals a design gap (logic that should be extracted or combined with something more substantive) rather than inherent untestability. + ## CLI Style **Prefer long option names over short ones** in command-line applications and examples. @@ -59,3 +84,37 @@ command -v -o file.txt ``` Long options are self-documenting and make scripts and examples easier to understand without consulting help text. Short options are acceptable for interactive use but should not appear in committed code, documentation, or examples. + +## Git Workflow + +Assume you are working in a git repository. Partition changes into small, self-contained commits and commit each before proceeding to the next change. When enacting a plan, a single action item will often span several such commits — that is expected and preferred over bundling unrelated changes together. + +Leverage the git history during development as well. Git enables efficient and reliable rollbacks of recent changes or research dead ends, and clean reverts of specific diffs from earlier in the history. Prefer these over manual cleanup. + +## Green-Field Project Setup + +When setting up a new project, code-quality and developer-experience tooling must be included from the start and integrated into the development workflow. The principles below use Python as a concrete example, but apply generally to any language ecosystem. + +### Python Tooling + +Use **uv** to manage dependencies and create the project virtual environment. All work must be performed inside the venv. Additionally, install and configure the **pre-commit** hook manager with a baseline DevEx toolset: + +- **ruff** — linting and formatting +- **mypy** — static type checking +- **tach** — structural/dependency boundary checks + +Configure all tools for their strictest check levels by default. Include a `py.typed` marker file in every package to signal PEP 561 compliance. + +### Line Length + +Do not manually break lines to conform to a line-length limit. Automated code formatters (ruff, gofmt, etc.) handle this for source code. Write unbroken lines in text and Markdown files (e.g., README.md) as well. This also applies to one-off files outside of a project context. + +### Licensing (REUSE) + +In all projects, install a **pre-commit hook for the REUSE tool** to lint licensing information and ensure every file has correct SPDX headers. + +Default license assignments: + +- **GPL-3.0-or-later** — source code files in coding projects +- **CC-BY-SA-4.0** — documentation files (README, user guides, etc.); also the default project license for non-coding projects +- **CC0-1.0** — project configuration files (e.g., `pyproject.toml`, `tach.toml`) and small utility scripts or Makefiles that are not core to the implemented logic diff --git a/.config/containers/systemd/ollama.container b/.config/containers/systemd/ollama.container index 98929ac..500baad 100644 --- a/.config/containers/systemd/ollama.container +++ b/.config/containers/systemd/ollama.container @@ -5,11 +5,16 @@ Description=A local LLM server # keep-sorted start AutoUpdate=registry ContainerName=ollama -Environment=OLLAMA_KEEP_ALIVE=10m +DropCapability=ALL +Environment=OLLAMA_KEEP_ALIVE=30m +HealthCmd=ollama list +# HealthInterval=30s +# HealthStartPeriod=15s Image=docker.io/ollama/ollama:latest Network=ollama.network -PodmanArgs=--transient-store -PublishPort=11434:11434 +NoNewPrivileges=true +PodmanArgs=--pull=newer --transient-store +PublishPort=127.0.0.1:11434:11434 ReadOnly=true Volume=%h/.local/share/ollama:/root/.ollama:ro,z # keep-sorted end diff --git a/.config/containers/systemd/plantuml.container b/.config/containers/systemd/plantuml.container index aa8057d..8fa35df 100644 --- a/.config/containers/systemd/plantuml.container +++ b/.config/containers/systemd/plantuml.container @@ -5,10 +5,12 @@ Description=A local PlantUML server # keep-sorted start AutoUpdate=registry ContainerName=plantuml +DropCapability=ALL Image=docker.io/plantuml/plantuml-server:jetty Network=private -PodmanArgs=--transient-store -PublishPort=8080:8080 +NoNewPrivileges=true +PodmanArgs=--cpus 1 --memory 1g --pull=newer --transient-store +PublishPort=127.0.0.1:8080:8080 ReadOnly=true # keep-sorted end @@ -16,4 +18,8 @@ ReadOnly=true WantedBy=default.target [Service] +# keep-sorted start +CPUQuota=100% +MemoryMax=1G Restart=always +# keep-sorted end diff --git a/.config/containers/systemd/transmission.container b/.config/containers/systemd/transmission.container index 6d83357..daeee81 100644 --- a/.config/containers/systemd/transmission.container +++ b/.config/containers/systemd/transmission.container @@ -7,12 +7,15 @@ AutoUpdate=registry ContainerName=transmission Environment=PGID=1000 Environment=PUID=1000 +HealthCmd=curl --fail --silent http://localhost:9091/ +# HealthInterval=30s +# HealthStartPeriod=30s Image=lscr.io/linuxserver/transmission:latest Network=private -PodmanArgs=--transient-store +PodmanArgs=--cpus 2 --memory 512m --pull=newer --transient-store +PublishPort=127.0.0.1:9091:9091 PublishPort=51413:51413 PublishPort=51413:51413/udp -PublishPort=9091:9091 ReadOnly=true UserNS=keep-id Volume=%h/.config/transmission:/config:Z @@ -25,10 +28,12 @@ WantedBy=default.target [Service] # keep-sorted start +CPUQuota=200% ExecStartPre=mkdir --parents %h/.config/transmission ExecStartPre=mkdir --parents %h/Downloads/transmission ExecStartPre=mkdir --parents %h/Downloads/transmission/complete ExecStartPre=mkdir --parents %h/Downloads/transmission/incomplete ExecStartPre=mkdir --parents %h/Downloads/transmission/watch +MemoryMax=512M Restart=always # keep-sorted end diff --git a/.config/emacs/init.el b/.config/emacs/init.el index 3da0d64..c8748eb 100644 --- a/.config/emacs/init.el +++ b/.config/emacs/init.el @@ -30,10 +30,14 @@ (use-package emacs :ensure nil - :bind (("C-z" . nil) - ("C-z i" . find-init-file) + :bind ( + ("C-z" . nil) + ;; keep-sorted start ("C-z f" . ffap) - ("C-z u" . insert-uuid4-at-point)) + ("C-z i" . find-init-file) + ("C-z u" . insert-uuid4-at-point) + ;; keep-sorted end + ) :hook ( ;; keep-sorted start (after-save . executable-make-buffer-file-executable-if-script-p) diff --git a/.config/emacs/site-lisp/local-models.el b/.config/emacs/site-lisp/local-models.el index ac695eb..bc9402d 100644 --- a/.config/emacs/site-lisp/local-models.el +++ b/.config/emacs/site-lisp/local-models.el @@ -11,6 +11,14 @@ :context-window 128 :cutoff-date "2024-08" ) + ( + gemma4:latest + :description "A model from Google built on Gemini technology" + :capabilities (media tool-use cache) + :mime-types ("image/bmp" "image/gif" "image/jpeg" "image/png" "image/tiff" "image/webp") + :context-window 128 + :cutoff-date "2025-01" + ) ( hf.co/Orenguteng/Llama-3.1-8B-Lexi-Uncensored-V2-GGUF:latest :description "Uncensored model based on Llama-3.1-8b-Instruct" diff --git a/.config/setup/04-install-deb-packages.sh b/.config/setup/04-install-deb-packages.sh index b742e59..6ee1d44 100755 --- a/.config/setup/04-install-deb-packages.sh +++ b/.config/setup/04-install-deb-packages.sh @@ -15,6 +15,7 @@ DEB_PKGS=( borgbackup build-essential catatonit + command-not-found curl default-jdk direnv @@ -34,6 +35,7 @@ DEB_PKGS=( graphviz grim guile-3.0 + hcloud-cli htop imagemagick inkscape diff --git a/.config/setup/14-install-cron-jobs.sh b/.config/setup/14-install-cron-jobs.sh index 22851b8..df7b93d 100755 --- a/.config/setup/14-install-cron-jobs.sh +++ b/.config/setup/14-install-cron-jobs.sh @@ -5,6 +5,8 @@ IFS=$'\n\t' # keep-sorted start systemctl --user enable --now backup.timer +systemctl --user enable --now podman-healthcheck@ollama.timer +systemctl --user enable --now podman-healthcheck@transmission.timer systemctl --user enable --now sync-backup.timer systemctl --user enable --now sync-git-repos.timer # keep-sorted end diff --git a/.config/systemd/user/podman-healthcheck@.service b/.config/systemd/user/podman-healthcheck@.service new file mode 100644 index 0000000..b521d85 --- /dev/null +++ b/.config/systemd/user/podman-healthcheck@.service @@ -0,0 +1,6 @@ +[Unit] +Description=Podman health check for %i + +[Service] +Type=oneshot +ExecStart=podman --transient-store healthcheck run %i diff --git a/.config/systemd/user/podman-healthcheck@.timer b/.config/systemd/user/podman-healthcheck@.timer new file mode 100644 index 0000000..255104d --- /dev/null +++ b/.config/systemd/user/podman-healthcheck@.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Podman health check timer for %i +BindsTo=%i.service +After=%i.service + +[Timer] +OnActiveSec=30s +OnUnitActiveSec=30s + +[Install] +WantedBy=%i.service diff --git a/.gitconfig b/.gitconfig index d269033..92ad45b 100644 --- a/.gitconfig +++ b/.gitconfig @@ -20,3 +20,5 @@ # keep-sorted end [include] path = .hostgitconfig +[core] + excludesfile = ~/.gitignore_global diff --git a/.gitignore_global b/.gitignore_global new file mode 100644 index 0000000..1a88ff4 --- /dev/null +++ b/.gitignore_global @@ -0,0 +1,3 @@ +/conversation-id.txt +/conversation-id-*.txt +/.claude/settings.local.json diff --git a/.local/bin/sync-backup b/.local/bin/sync-backup index 97edd7a..dc3c432 100755 --- a/.local/bin/sync-backup +++ b/.local/bin/sync-backup @@ -5,8 +5,11 @@ IFS=$'\n\t' export BORG_REPO="/media/backup/" -if [ "$1" = "service" ]; then - rclone sync "${BORG_REPO}" gdrive-backup:hot-repo/ +if [ "${1:-}" = "service" ]; then + extra_args=() else - rclone sync --progress "${BORG_REPO}" gdrive-backup:hot-repo/ + extra_args=(--progress) fi + +rclone sync "${extra_args[@]}" "${BORG_REPO}" gdrive-backup:hot-repo/ +rclone copy "${extra_args[@]}" ~/.keys/ --include '*.kdbx' gdrive-backup:keys/ diff --git a/.local/share/github-versions/dolt b/.local/share/github-versions/dolt index 24eab50..9254013 100755 --- a/.local/share/github-versions/dolt +++ b/.local/share/github-versions/dolt @@ -11,8 +11,8 @@ dolt_resource() { } install_dolt() { - tar xz --directory="$(systemd-path user-binaries)" --strip-components=2 dolt-linux-amd64/bin/dolt - chmod 550 "$(systemd-path user-binaries)"/dolt + tar xz --directory="$(systemd-path user-binaries)" --strip-components=2 dolt-linux-amd64/bin/dolt && \ + chmod 550 "$(systemd-path user-binaries)"/dolt } github_update "${package}" "${repo}" dolt_resource install_dolt diff --git a/.local/share/github-versions/fstar b/.local/share/github-versions/fstar index 26499f3..b56a055 100755 --- a/.local/share/github-versions/fstar +++ b/.local/share/github-versions/fstar @@ -20,7 +20,7 @@ install_fstar() { rm --force --recursive "${INSTALL_DIR}" && \ mv "${tempdir}"/fstar "$(dirname "${INSTALL_DIR}")" && \ rm --force --recursive "${tempdir}" && \ - ln --symbolic "${INSTALL_DIR}"/bin/fstar.exe "$(systemd-path user-binaries)"/fstar.exe + ln --force --symbolic "${INSTALL_DIR}"/bin/fstar.exe "$(systemd-path user-binaries)"/fstar.exe } github_update "${package}" "${repo}" fstar_resource install_fstar diff --git a/.local/share/github-versions/kingfisher b/.local/share/github-versions/kingfisher index 10c7f17..f6903dc 100755 --- a/.local/share/github-versions/kingfisher +++ b/.local/share/github-versions/kingfisher @@ -11,8 +11,8 @@ kingfisher_resource() { } install_kingfisher() { - tar xz --directory="$(systemd-path user-binaries)" kingfisher - chmod 550 "$(systemd-path user-binaries)"/kingfisher + tar xz --directory="$(systemd-path user-binaries)" kingfisher && \ + chmod 550 "$(systemd-path user-binaries)"/kingfisher } github_update "${package}" "${repo}" kingfisher_resource install_kingfisher diff --git a/.local/share/github-versions/minikube b/.local/share/github-versions/minikube index 004f74c..8012485 100755 --- a/.local/share/github-versions/minikube +++ b/.local/share/github-versions/minikube @@ -11,10 +11,10 @@ minikube_resource() { } install_minikube() { - tempfile="$(mktemp)" - cat - > "${tempfile}" - chmod 550 "${tempfile}" - mv "${tempfile}" "$(systemd-path user-binaries)"/minikube + tempfile="$(mktemp)" && \ + cat - > "${tempfile}" && \ + chmod 550 "${tempfile}" && \ + mv "${tempfile}" "$(systemd-path user-binaries)"/minikube } github_update "${package}" "${repo}" minikube_resource install_minikube diff --git a/.local/share/github-versions/rust-analyzer b/.local/share/github-versions/rust-analyzer index 1add828..bd41614 100755 --- a/.local/share/github-versions/rust-analyzer +++ b/.local/share/github-versions/rust-analyzer @@ -11,10 +11,10 @@ rust_analyzer_resource() { } install_rust_analyzer() { - tempfile="$(mktemp)" - gunzip --to-stdout - > "${tempfile}" - chmod 550 "${tempfile}" - mv "${tempfile}" "$(systemd-path user-binaries)"/rust-analyzer + tempfile="$(mktemp)" && \ + gunzip --to-stdout - > "${tempfile}" && \ + chmod 550 "${tempfile}" && \ + mv "${tempfile}" "$(systemd-path user-binaries)"/rust-analyzer } github_update "${package}" "${repo}" rust_analyzer_resource install_rust_analyzer diff --git a/.local/share/github-versions/simplex-chat b/.local/share/github-versions/simplex-chat new file mode 100755 index 0000000..82e7977 --- /dev/null +++ b/.local/share/github-versions/simplex-chat @@ -0,0 +1,20 @@ +#! /usr/bin/bash + +set -euo pipefail +IFS=$'\n\t' + +package=simplex-chat +repo=simplex-chat/simplex-chat + +sc_resource() { + echo "simplex-chat-ubuntu-24_04-x86_64" +} + +install_sc() { + tempfile="$(mktemp)" && \ + cat - > "${tempfile}" && \ + chmod 550 "${tempfile}" && \ + mv "${tempfile}" "$(systemd-path user-binaries)"/simplex-chat +} + +github_update "${package}" "${repo}" sc_resource install_sc diff --git a/.local/share/github-versions/tlapm b/.local/share/github-versions/tlapm index 2da3899..34e5227 100755 --- a/.local/share/github-versions/tlapm +++ b/.local/share/github-versions/tlapm @@ -19,7 +19,7 @@ install_tlapm() { rm --force --recursive "${INSTALL_DIR}" && \ mv "${tempdir}"/tlapm "$(dirname "${INSTALL_DIR}")" && \ rm --force --recursive "${tempdir}" && \ - ln --symbolic "${INSTALL_DIR}"/bin/tlapm "$(systemd-path user-binaries)"/tlapm + ln --force --symbolic "${INSTALL_DIR}"/bin/tlapm "$(systemd-path user-binaries)"/tlapm } github_update "${package}" "${repo}" tlapm_resource install_tlapm 1.6.0-pre diff --git a/.local/share/github-versions/uv b/.local/share/github-versions/uv index b0c0ad9..389c20d 100755 --- a/.local/share/github-versions/uv +++ b/.local/share/github-versions/uv @@ -11,10 +11,10 @@ uv_resource() { } install_uv() { - tempdir="$(mktemp --directory)" - tar xz --directory="${tempdir}" --strip-components=1 && \ - chmod 550 "${tempdir}"/uv "${tempdir}"/uvx && \ - mv --force "${tempdir}"/uv "${tempdir}"/uvx "$(systemd-path user-binaries)" + tempdir="$(mktemp --directory)" && \ + tar xz --directory="${tempdir}" --strip-components=1 && \ + chmod 550 "${tempdir}"/uv "${tempdir}"/uvx && \ + mv --force "${tempdir}"/uv "${tempdir}"/uvx "$(systemd-path user-binaries)" } github_update "${package}" "${repo}" uv_resource install_uv diff --git a/.ssh/config b/.ssh/config new file mode 100644 index 0000000..51ec533 --- /dev/null +++ b/.ssh/config @@ -0,0 +1 @@ +Include ~/.ssh/config.d/*.conf diff --git a/.ssh/config.d/90-hardened-security.conf b/.ssh/config.d/90-hardened-security.conf new file mode 100644 index 0000000..47856a4 --- /dev/null +++ b/.ssh/config.d/90-hardened-security.conf @@ -0,0 +1,14 @@ +# SSH client algorithm hardening. +# +# Require PQ-hybrid KEX, AEAD ciphers, Ed25519 keys. +# Applied to all outgoing SSH connections from this machine. +# +# Requires OpenSSH 9.9+ for mlkem768x25519-sha256. + +Host * + KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com + Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com + MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com + HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com + PubkeyAcceptedAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com + RekeyLimit 1G 1h diff --git a/.ssh/config.d/90-terminal-emulator.conf b/.ssh/config.d/90-terminal-emulator.conf new file mode 100644 index 0000000..a11d57f --- /dev/null +++ b/.ssh/config.d/90-terminal-emulator.conf @@ -0,0 +1,2 @@ +Host * + SetEnv TERM=xterm-256color