feat(skills): add coding-dev.md and per-language rules under code-languages/

Generic coding rules and preferences distilled from existing agent rule
files, project AGENTS.md/.rules, and saved memories:

- strictness maximal; suppressions must be reported to the user
- never filter build/lint/test output through grep/tail/head
- reuse > fork > self-implement; generic code becomes its own package
- 1000-line file / 100-line function limits, split by concern
- regression test every fixed bug; never test against real user data
- events over polling; scope discipline; honest reporting

Per-language: rust, js-typescript, bash, python, go, csharp-dotnet, c-cpp.
Each extends coding-dev.md rather than repeating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BluscreamandClaude Opus 5 committed 2026-09-18 12:33:44 +02:00
1 parent ba95f171b2
commit 57968904bf
9 files changed
+1499 -1

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
---
name: bash
description: >-
Use this skill when writing or modifying Bash/shell scripts, build pipelines, release
scripts, or when running shell commands as an agent. Covers strict mode, shellcheck,
quoting, the no-wait-loops rule, build-script structure, and destructive-command safety.
---
# Bash / Shell — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds shell specifics.
Bash is for **orchestration** — sequencing tools, wiring a build pipeline, gluing CLIs.
Once a script grows real data structures, arithmetic, or parsing, it wants to be Python or
a compiled tool instead.
---
## 1. Strict Mode & Linting
Every script starts with:
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
```
- `-e` exit on error, `-u` error on unset variable, `-o pipefail` so a failure mid-pipe is
not masked by a successful final command.
- **`shellcheck` must be clean** and belongs in the build gate. Do not silence a finding with
a `# shellcheck disable=` without a comment saying why.
- Format with `shfmt`.
> [!IMPORTANT]
> `set -e` does **not** fire inside `if`, `&&`, `||`, or command substitution used in a
> condition. Do not assume a failing command inside those aborts the script — check
> explicitly where it matters.
---
## 2. Quoting & Expansion
- **Quote every expansion**: `"$var"`, `"${arr[@]}"`, `"$(cmd)"`. Unquoted expansion word-splits
and glob-expands, which is how a path with a space becomes two broken arguments.
- `"${var:-default}"` for defaults; `"${var:?message}"` for required values.
- Use arrays for argument lists — never build a command as a string and `eval` it.
- `[[ ]]` over `[ ]`. `$(…)` over backticks.
- `local` every variable inside a function.
- `printf` over `echo` for anything with escapes or leading dashes.
---
## 3. Never Write Wait Loops
> [!CAUTION]
> **Do not write `until <check>; do sleep 2; done` or `while ! <check>; do sleep …; done`.**
> They hang the terminal, and the only way out is an interrupt.
To wait for something:
- Run the work in the **background** and wait for its completion signal.
- To check external state, **take a single sample per invocation** rather than looping.
- If a tool has a real blocking wait (`--wait`, `wait-for`, `systemctl is-active --wait`),
use that instead of polling around it.
This mirrors the general events-over-polling preference: prefer a notification to a loop.
---
## 4. Never Filter Build Output
Repeating the global rule because shell is where it gets violated:
```bash
# WRONG
cargo build 2>&1 | grep -E "error|warning"
npm run build | tail -20
# RIGHT
cargo build
npm run build
```
Tee to a log if you need a copy — `cmd 2>&1 | tee build.log` preserves the full output — but
never `grep`/`head`/`tail` it away. Narrow the command instead when output is genuinely huge.
---
## 5. Destructive Commands
- **Look at the target before deleting or overwriting it.** `ls` it, `stat` it, read it.
- Never `rm -rf "$VAR"` without first asserting `$VAR` is non-empty and is what you think:
```bash
[[ -n "${BUILD_DIR:-}" && -d "$BUILD_DIR" ]] || { echo "refusing: BUILD_DIR unset" >&2; exit 1; }
```
- **A shared output directory belongs to nobody.** If two scripts write to `dist/`, neither
may `rm -rf` it — each cleans only its own outputs.
- Write scratch files to a `mktemp -d` and trap cleanup:
```bash
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
```
- Prefer a `--dry-run` flag on anything that deploys, releases, or deletes — and implement it
honestly.
### `pgrep -f` matches its own shell
> [!WARNING]
> The command line containing the pattern **is itself a process**, so `pgrep -f foo.conf`
> matches the shell running it. Piping that into `kill` kills the running script — which reads
> exactly like "the thing I was testing never happened".
>
> Write the pattern so it cannot match itself: `pgrep -f 'my[a]pp\.conf'`
---
## 6. Build Script Structure
A build script is the project's **quality gate**, and it should be exhaustive enough to catch
mistakes before a human does.
- **Fixed action order regardless of flag order**, with any failure aborting the rest:
```
bump → lint → build → artifacts → stop → deploy → start → commit → push → release
```
- **A skipped test fails the gate.** Never let a skip report green.
- **Print a summary with numbers**, not just a verdict — counts of warnings, suppressions, and
file-size violations. A suppression count that has grown is worth seeing even on a pass.
- Keep per-step logs in a known directory so a failure can be read after the fact.
- **Never bypass a failed gate** by copying a binary or running the install step by hand. Fix
the failing step; do not weaken the check to make it pass.
- Support `--dry-run` for the full release path.
- **Throttle heavy builds.** Detect when something latency-sensitive is running and drop to
half the cores at `nice -n 10`, announcing that it did so. Provide an override env var.
---
## 7. Paths & Portability
- Resolve the script's own directory rather than assuming the caller's `cwd`:
```bash
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
```
- Prefer absolute paths in anything non-interactive.
- **Entering a container resets the working directory** — chain `cd` inside the same command
string: `container-run -- bash -c "cd /path && make"`.
- Don't hardcode `/tmp` when the content must survive a cleanup; `/var/tmp` persists longer.
- `command -v tool >/dev/null || { echo "tool required" >&2; exit 1; }` before using an
optional dependency. **A silently skipped step is worse than a failure** — a packaging step
that quietly skips when its tool is missing will ship an incomplete release.
---
## 8. Secrets
- Read secrets from an environment file or secret store; **never** inline them in a script.
- Never `echo` a secret, and never pass one as a command-line argument — argv is world-readable
in the process table. Use an env var or stdin.
- Never commit a file containing real credentials; commit an example with empty values.
---
## 9. Script Placement
Standalone utility scripts belong in a **dedicated scripts directory**, symlinked into `PATH`
rather than copied there. Never write a loose script directly into `~/.local/bin` or another
`PATH` directory — the source of truth should be one versioned location.
---
## 10. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`python.md`](./python.md) — where a shell script should have become a Python script.
+153
View File
@@ -0,0 +1,153 @@
---
name: c-cpp
description: >-
Use this skill when writing, building, or debugging C or C++ — including compiler warning
configuration, sanitizers, modern C++ resource management, build systems (CMake/Meson),
32-bit/cross builds, and FFI boundaries exposed to other languages.
---
# C / C++ — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds C/C++ specifics.
C and C++ are for the cases with no better option: existing codebases, hard real-time paths,
and FFI boundaries. **For new standalone work, prefer Rust** — the memory-safety guarantees
are worth more than the familiarity.
---
## 1. Warnings — Everything On, Errors
```cmake
target_compile_options(mytarget PRIVATE
-Wall -Wextra -Wpedantic -Werror
-Wshadow -Wconversion -Wsign-conversion
-Wnon-virtual-dtor -Wold-style-cast -Wcast-align
-Wunused -Woverloaded-virtual -Wdouble-promotion
-Wformat=2 -Wnull-dereference -Wimplicit-fallthrough
)
```
- **`-Werror` in the build gate.** `-Wconversion` and `-Wshadow` are noisy on first contact and
catch real bugs — do not drop them because a legacy file complains; fix the file or scope the
suppression to it.
- Run **both** GCC and Clang where possible; each diagnoses things the other misses.
- `clang-tidy` with `bugprone-*`, `cert-*`, `performance-*`, `modernize-*`.
- `clang-format` with a committed `.clang-format`. No hand-formatting.
---
## 2. Sanitizers — Use Them
```bash
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
```
- **ASan + UBSan on every debug/test build.** They find the bugs that warnings cannot.
- TSan for anything threaded (separate build — it is incompatible with ASan).
- Valgrind when a sanitizer build is not possible.
- **A sanitizer finding is a bug, not noise.** Fix it.
---
## 3. Resource Management (C++)
- **RAII for everything.** Every raw `new`/`delete`, `malloc`/`free`, `fopen`/`fclose`, and
native handle pair is a leak waiting for an early return or a throw.
- `std::unique_ptr` by default; `std::shared_ptr` only where ownership is genuinely shared.
`std::weak_ptr` to break cycles.
- **No owning raw pointers.** A raw pointer or reference in a signature means "borrowed,
non-owning, outlives the call".
- `std::string_view` / `std::span` for non-owning views — and never return one that outlives
its backing storage.
- **The Rule of Zero**: design so no destructor, copy, or move needs writing. If you must write
one, write all five.
- `constexpr`, `const`, and `noexcept` wherever they hold. `[[nodiscard]]` on anything whose
return value must not be dropped.
---
## 4. C Specifics
- C17 or later unless the target forbids it.
- **Check every allocation and every return value.** In C, unchecked returns are the primary
bug source.
- Fixed-width types from `<stdint.h>` (`uint32_t`), never bare `int` for anything with a
defined width on the wire.
- Always the bounded string functions (`snprintf`, `strncpy` with explicit termination); never
`strcpy`, `sprintf`, `gets`.
- `static` on every function and global not exported from the translation unit.
- Prefer a struct-and-functions module boundary with an opaque pointer over exposing layout.
---
## 5. Build Systems
- **CMake** (3.20+, target-based) or **Meson** for new projects. Never a hand-written Makefile
for anything non-trivial.
- Target-based CMake only: `target_link_libraries`, `target_include_directories`,
`target_compile_options`. **No directory-level `include_directories` or global
`CMAKE_CXX_FLAGS` mutation.**
- `PRIVATE` / `PUBLIC` / `INTERFACE` chosen deliberately — this is what keeps include leaks out
of dependents.
- **Register every new source file with the build system** in the same commit that adds it. A
source file the build never compiles will sit there looking correct while every edit to it
does nothing.
- Vendor dependencies with `FetchContent` or submodules; document which and why.
---
## 6. Cross & Multi-Arch Builds
- Keep 32-bit and 64-bit outputs in **separate build directories**; never reuse one.
- **A component built for one architecture cannot load one built for another.** An
`ELFCLASS64` / `ELFCLASS32` error means a loader path is reaching the wrong build — check the
library search path, not the code.
- When a build must match an already-installed runtime's version, **pin it explicitly** and
note the constraint. A silently mismatched pair fails at load time with a message that
points nowhere useful.
- Record the exact toolchain and container used for a cross build. "It built on my machine" is
not reproducible for native code.
---
## 7. FFI Boundaries
When exposing C/C++ to another language:
- `extern "C"` on the boundary, with a hand-written, stable, documented header.
- **Ownership across the boundary must be explicit** — who allocates, who frees, and with which
function. Provide a matching `free` for every allocation you hand out.
- **Never let an exception cross an `extern "C"` boundary.** Catch everything at the edge and
convert to an error code.
- Bindings generated by a tool (`bindgen`, etc.) are regenerated, never hand-edited.
- On the consuming side, keep all `unsafe`/unmanaged interop in **one thin wrapper module**
that exposes a safe API — never scattered through call sites.
> [!WARNING]
> **Binding crates/packages that reach into a library's struct fields break when that library
> makes the struct opaque.** Pin to a binding version that matches the installed library
> generation, and note the constraint where someone will read it before bumping.
---
## 8. Testing & Debugging
- GoogleTest or Catch2; CTest integration so `ctest` runs the suite.
- **Regression test every fixed bug.**
- Debug builds keep frame pointers (`-fno-omit-frame-pointer`) so stack traces are usable.
- `-g` even in release builds; ship separate debug symbols rather than none.
- **Record dead ends in the project's notes**, especially for native debugging — the
investigation that ruled something out is expensive and will otherwise be repeated. Note
what did *not* work alongside what did.
- When a binary search through a packed/embedded blob finds nothing, that is not evidence the
content is absent — packed asset formats defeat naive string searches. Re-check without
anchoring before concluding.
---
## 9. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`rust.md`](./rust.md) — the preferred language for new native work, and the usual consumer
of these FFI boundaries.
+151
View File
@@ -0,0 +1,151 @@
---
name: csharp-dotnet
description: >-
Use this skill when writing, building, or releasing C# / .NET code — including nullable
reference types, warnings-as-errors, analyzer configuration, async/await rules, trimming
and single-file publishing, and multi-generation SDK compatibility shims.
---
# C# / .NET — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds .NET specifics.
---
## 1. Project Settings — Strict By Default
```xml
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-all</AnalysisLevel>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>
```
Put these in a `Directory.Build.props` at the repository root so every project inherits them
and none can quietly drift.
**Every build carries `-warnaserror`:**
```bash
dotnet build MySolution.sln -warnaserror
```
- **Nullable warnings are real bugs.** Never silence one with `!` (null-forgiving) — narrow
the type or fix the contract. `!` is an assertion, and an assertion you cannot prove is a
future `NullReferenceException`.
- Never blanket-disable an analyzer. If a rule is genuinely wrong for the codebase, configure
it once in `.editorconfig` with a comment.
---
## 2. Async
- **`async`/`await` throughout; never `.Result` or `.Wait()`** — both deadlock in any context
with a synchronization context, and turn an exception into an `AggregateException`.
- `async Task`, never `async void`, except for genuine event handlers.
- `ConfigureAwait(false)` in library code; not needed in app code on modern hosts.
- Accept a `CancellationToken` on every async public method and actually honor it.
- `IAsyncEnumerable<T>` for streaming rather than materializing a whole collection.
- **Events over polling** — subscribe to the framework's event/observable API; don't run a
timer loop. (Global rule; it applies fully here.)
---
## 3. Errors, Disposal & Logging
- Specific exception types; never `catch (Exception)` without a reason and a log.
- **Never an empty catch block.**
- `using`/`await using` declarations for everything `IDisposable`/`IAsyncDisposable`.
Disposal is not optional for streams, HTTP handlers, DB contexts, or native handles.
- **One static `HttpClient` (or `IHttpClientFactory`)** — a per-request `HttpClient` exhausts
sockets under load.
- Structured logging (Serilog or `ILogger<T>`) with message templates, not string interpolation:
`logger.LogInformation("Loaded {Count} items", count)` — the template is what makes the log
queryable.
- **Never log secrets.**
---
## 4. Style
- File-scoped namespaces, `var` for obvious types, target-typed `new()`.
- `record` for immutable value types; `readonly struct` for small value types.
- Pattern matching and switch expressions over `if`/`else` ladders.
- `IReadOnlyList<T>` / `IReadOnlyDictionary<K,V>` in public signatures returning collections.
- `internal` by default; `public` only for genuine API surface.
- File and function size limits from the general rules apply.
- `dotnet format` before committing.
---
## 5. Publishing — Trimming & Single File
> [!IMPORTANT]
> **The trimmer only runs on a Release/trimmed/single-file publish.** A Debug build cannot
> reproduce the trimmer dropping a type that only reflection reaches. If your release pipeline
> ships trimmed binaries, the publish step is the **only** one that exercises the trimmer —
> so it must run before release, not after.
- Annotate reflection-reached types with `[DynamicallyAccessedMembers]` or a
`TrimmerRootDescriptor`, and **smoke-test the published binary**, not just the build.
- Enable `<IsTrimmable>` per library deliberately; blanket trimming of a plugin host breaks
plugin loading.
- **A release is a fixed, complete set of assets.** Define what that set is, and have the
release step verify it is complete rather than trusting whatever happens to be in the output
directory. A packaging tool that silently skips a step when a dependency is missing will
otherwise ship a release with a missing platform.
---
## 6. Multi-Generation SDK Compatibility
When targeting several generations of a host SDK whose API changed shape (a class becoming an
interface, a type being renamed):
- **All compatibility code lives in dedicated shim files** — e.g. `GlobalUsings.cs` and
`<Host>SdkShims.cs`. Never scatter `#if` directives through feature code.
- Select the target with an MSBuild property so the build is explicit about which generation
it produced:
```bash
dotnet build MyModules.csproj -c Release -p:HostTarget=stable
dotnet build MyModules.csproj -c Release -p:HostTarget=beta
```
- Document the differences between generations in the project's agent/contributor file —
which types changed, and which direction each shim adapts.
- Keep a local package feed for SDK versions not published to the public registry, and script
the packing step rather than doing it by hand.
---
## 7. Testing
- xUnit (or the project's existing framework); `FluentAssertions` for readable assertions.
- **Regression test every fixed bug.**
- Inject abstractions for filesystem, clock, and network so they can be faked — do not reach
for a static.
- **Never test mutations against real user profiles, real config directories, or live
services.** Fixtures and injected backends only.
- Test the **published** artifact for anything involving trimming, single-file packing, or
native dependencies — the assembly that passed unit tests is not the one you shipped.
---
## 8. Toolchain Notes
- When the SDK is not installed on the host (immutable OS, mismatched glibc), build inside the
project's designated container. Entering one resets the working directory — chain `cd` in the
same command string.
- Prefer the project's build script over hand-typed `dotnet` calls; it carries `-warnaserror`,
the correct action ordering, and the asset completeness checks.
- **Never stop, deploy to, or restart an application while the user has it running**, unless
they have said to.
---
## 9. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`bash.md`](./bash.md) — for the release pipeline scripts around a .NET build.
+157
View File
@@ -0,0 +1,157 @@
---
name: go
description: >-
Use this skill when writing, building, linting, testing, or releasing Go — including
golangci-lint configuration, strict typing over map[string]interface{}, official SDK
preference, CGO-free static builds, context handling, and the build gate.
---
# Go — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds Go specifics.
---
## 1. The Gate
```bash
gofmt -l . # must print nothing
go vet ./...
golangci-lint run
go test -race ./...
go build ./...
```
Wire this into a single entry point (`make check`, `scripts/build.sh`) so it cannot be
partially run. **A skipped test fails the gate.** Add `govulncheck` for binary vulnerability
scanning on anything that ships.
---
## 2. Linting — Strict Set
`golangci-lint` with a deliberately broad selection, not the defaults:
```yaml
linters:
enable:
- staticcheck
- errcheck # every returned error is handled
- gosec # security findings
- noctx # no HTTP request without a context
- revive
- misspell
- unconvert
- bodyclose # every response body is closed
- goconst
- ineffassign
- nilerr
```
`errcheck` and `bodyclose` catch the two most common real Go bugs. As always: fix rather than
suppress, and when suppressing use a specific `//nolint:name // reason` with the reason.
---
## 3. Typing — Concrete, Not Dynamic
- **Strongly typed structs everywhere.** Avoid `map[string]interface{}` and reflection when a
struct can be defined — including for JSON, config, and API payloads.
- **Typed constants/enums**, not bare strings:
```go
type SyncStatus string
const (
StatusSynced SyncStatus = "synced"
StatusOutdated SyncStatus = "outdated"
StatusDirty SyncStatus = "dirty"
)
```
- **Concrete error types** with `errors.Is` / `errors.As`, not string comparison.
- Keep types shared with a frontend structurally in sync with that frontend's types; when both
exist, a change to one is incomplete without the other.
---
## 4. Errors
- **Every error is handled.** `errcheck` enforces it; do not `_ =` an error away without a
comment explaining why it cannot matter.
- Wrap with context: `fmt.Errorf("fetching %s: %w", name, err)`. Always `%w`, never `%v`, when
the caller might need to inspect it.
- Sentinel errors as package-level `var ErrNotFound = errors.New(...)`; typed errors when the
caller needs data from them.
- **`panic` only for genuinely unrecoverable programmer error**, never for expected runtime
conditions. A library never panics on input it was designed to receive.
- `defer` the cleanup immediately after acquiring the resource, always.
---
## 5. Context
- **Every function that does I/O takes `ctx context.Context` as its first parameter.**
- Never store a context in a struct field.
- Never pass `nil` — `context.TODO()` marks a genuine gap.
- Honor cancellation: select on `ctx.Done()` in loops, and pass ctx down to every HTTP call
(`noctx` enforces this).
- Set timeouts on outbound requests. A default `http.Client` has no timeout at all.
---
## 6. Dependencies — Official SDKs First
Follow the global reuse-first ordering. In Go, strongly prefer **official/first-party SDKs**
over third-party wrappers — they track API changes and handle auth and pagination correctly.
Choose **pure-Go implementations over CGO ones** whenever the trade is acceptable: a CGO
dependency turns a static single-binary build into a toolchain and glibc problem, and rules
out a `FROM scratch`/Alpine container image.
```bash
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bin/app ./cmd/app
```
`go mod tidy` before committing, and commit `go.sum`.
---
## 7. Concurrency
- **Share memory by communicating.** Channels for handoff; `sync.Mutex` for guarding a struct's
own fields. Don't mix both for the same state.
- **`go test -race` is not optional** — it belongs in the gate, not in an occasional manual run.
- Every goroutine has a defined exit path. `errgroup.Group` for a set of related goroutines so
errors propagate and cancellation fans out.
- **Events over polling** — subscribe to a watch/notify API rather than looping on a ticker.
Where a ticker is genuinely necessary, make the interval configurable and document why.
- Never leak a goroutine on a channel nobody reads.
---
## 8. Structure
- `cmd/<binary>/` for entry points; `pkg/` or `internal/` for the actual code.
- `internal/` for anything that must not become public API — it is compiler-enforced.
- One package per concern; **package names are lowercase, single-word, no underscores**, and
never `util` or `common`.
- Keep the exported surface small. Unexported by default.
- Embed frontend/static assets with `//go:embed` for single-binary distribution.
- File and function size limits from the general rules apply.
---
## 9. Testing
- Table-driven tests are the default idiom.
- `t.Parallel()` where tests are genuinely independent.
- `t.TempDir()` for filesystem work — it cleans itself up.
- `httptest` for HTTP handlers and for faking upstreams; do not hit real services in unit tests.
- **Regression test every fixed bug.**
- **Never run destructive operations against real accounts or user data during validation** —
temporary fixtures only.
---
## 10. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`js-typescript.md`](./js-typescript.md) — for the embedded frontend in a Go service.
+202
View File
@@ -0,0 +1,202 @@
---
name: js-typescript
description: >-
Use this skill when writing, building, linting, testing, or releasing JavaScript or
TypeScript — including Node, Bun, Deno, React, Next.js, and Astro projects. Covers the
TypeScript-over-JSDoc-over-JS preference, strict compiler settings, banned constructs,
React/hooks rules, resource lifecycle, and package/tooling choices.
---
# TypeScript / JavaScript — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds JS/TS specifics.
---
## 1. Language Choice — A Strict Preference Order
1. **TypeScript** — always the default for new code.
2. **JavaScript with JSDoc type annotations** — acceptable when a project genuinely cannot
adopt a build step, and `checkJs` is enabled so the annotations are actually enforced.
3. **Plain untyped JavaScript** — last resort only.
Migrating an existing JS project to TS is worth proposing; do not do it unasked (scope rule).
In a plain-JS codebase, **match it** — do not introduce a lone `.ts` file and a build step
into a project that has neither.
---
## 2. Strict Compiler Settings
```jsonc
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"isolatedModules": true
}
}
```
**`tsc --noEmit` must pass with 0 errors**, and it belongs in the build gate alongside lint
and tests — not as a thing someone runs occasionally in their editor.
> [!NOTE]
> Some established codebases deliberately relax `noImplicitAny` or `noUncheckedIndexedAccess`
> because their idioms depend on it. Respect the project's `tsconfig.json`; the table above is
> the default for **new** projects.
---
## 3. Banned Constructs
| Never | Use instead |
| :--- | :--- |
| `any` | `unknown` + narrowing, or a real generic |
| `as unknown as T` | a type guard, or fix the source type |
| `@ts-ignore` | `@ts-expect-error` with a one-line reason, only when unavoidable |
| `!` non-null assertion | narrow with `if` or an early return |
| `enum` / `const enum` | `as const` object + literal union |
| `namespace` | ES modules |
| Boxed primitives (`Number`, `String`, `Object`, `{}`, `Function`) | the primitive types |
| `../../..` relative climbs | the configured path alias (`@/*`) |
| `JSON.parse(JSON.stringify(x))` | `structuredClone` or `lodash.cloneDeep` |
| `await new Promise(r => setTimeout(r, ms))` | a `sleep` helper from your utils |
| `console.log` in shipped code | a real logger with levels |
| Empty `catch {}` | handle it, or let it propagate |
- **`satisfies T` over `as T`** when the value already conforms. `as` is for genuine narrowing
only — it is an assertion that silences the checker, not a conversion.
- **Zero `any` in shared model types.** Frontend types that mirror a backend schema must stay
structurally in sync with it and fully typed.
---
## 4. Use What Exists
**Search the codebase's own `utils/`, `common/`, and shared modules before writing anything.**
Re-implementing an export that already exists is the single most common avoidable diff.
Never hand-roll: debounce/throttle (`lodash`), deep clone, clamp, date formatting
(`date-fns` / `dayjs` / project standard), URL parsing, regex escaping, UUID generation,
retry/backoff, or a semver comparator.
In a UI framework or client mod, **never recreate a component the host already provides** —
a hand-rolled `<button>` loses theming, focus rings, and accessibility.
---
## 5. React
- **Conditional rendering returns `null`**, never `undefined` or `false`.
- **List keys are stable identifiers.** Never `key={index}` on a mutable list.
- **Don't mirror external state into `useState`.** Subscribe to the store with its own hook.
- No `useMemo` / `useCallback` on cheap values that do not feed a dependency array. They are
not free, and they obscure what is actually expensive.
- **Hoist constant dependency arrays** to module scope (`const KEYS = [...] as const`). A fresh
array literal in the render body re-subscribes on every render.
- Prefer `<>…</>` fragments; `React.Fragment` only when the fragment needs a `key`.
- Type props with a named `interface`, not `React.FC`.
- Wrap anything injected into a host application's tree in an error boundary — an
uncaught throw in injected code takes down the host's render, not just yours.
- Components used as dynamic render targets must live at **module scope**. Defining a
component inside another component remounts its whole subtree every render.
---
## 6. Resource Lifecycle — Everything Started Must Be Stopped
Every `setInterval`, long `setTimeout`, `addEventListener`, subscription, `ResizeObserver`,
`IntersectionObserver`, `AbortController`, and `WebSocket` created in a setup path must be
torn down in the matching teardown path — `stop()`, `useEffect` cleanup, or `finally`.
> [!CAUTION]
> **`addEventListener` needs a named function reference.** An inline arrow passed to
> `addEventListener` cannot be matched by `removeEventListener`, so the listener leaks
> silently and accumulates across every mount.
```ts
let intervalId: ReturnType<typeof setInterval> | undefined;
function start() { intervalId = setInterval(tick, 30_000); }
function stop() { if (intervalId !== undefined) { clearInterval(intervalId); intervalId = undefined; } }
```
Attach an `AbortController` to every long-lived `fetch` and abort it in cleanup.
---
## 7. Structure & Naming
- **Components**: PascalCase `.tsx`.
- **Modules / utilities**: kebab-case `.ts`.
- **Imports**: prefer the configured alias (`@/…`) over relative climbs.
- Indentation and quote/semicolon style: **match the file you are editing**. Do not introduce
a new formatter or lint config into a project that already has one.
- **One file until it earns a split** — around 250 lines for a self-contained plugin/feature,
or when a second file is genuinely reusable (`utils.ts`, `types.ts`, `components/`).
Hard limits from the general rules still apply.
---
## 8. Async & Data
- `async`/`await` over raw `.then()` chains. No mixing both in one flow.
- **Never leave a promise unawaited** unless you explicitly mean to, and mark it (`void p`)
when you do.
- `Promise.all` for independent work; sequential `await` only when there is a real dependency.
- **Subscribe to events; never poll.** (Global rule — it applies with full force here.)
- **Never store secrets in `localStorage` / `sessionStorage`.** Use the platform's real
storage API, and namespace your keys.
---
## 9. Node / Runtime Boundaries
- In any sandboxed or privileged-split architecture (Electron, browser extensions, plugin
hosts), the untrusted side must **never** import `fs`, `path`, `child_process`, `net`,
`http`, or `electron` directly. Route through the designated IPC/native boundary.
- Every IPC handler argument is **untrusted** — validate type, length, and content.
- Filesystem paths from the other side: `normalize()` then assert `startsWith(<allowed root>)`
before any read or write.
- Subprocesses: `spawn` with an argv **array**. Never `exec` a constructed string, never
`shell: true` with untrusted input.
- Network: allowlist by origin, cap response size, cap redirect depth.
- Errors crossing the boundary must be scrubbed — a raw `Error` leaks absolute paths.
---
## 10. Tooling & Tests
- Respect the project's runtime and package manager (`npm` / `pnpm` / `yarn` / `bun`) and its
lockfile. Do not switch package managers or add a second lockfile.
- Test runner: whatever the project uses (`vitest`, `bun test`, `jest`). Tests live beside the
code as `*.test.ts(x)` unless the project says otherwise.
- **Add unit tests for new logic and route tests for new handlers.** Prioritize coverage on
auth, persistence, and anything that talks to a third party.
- Lint with the project's config (`eslint.config.mjs` and friends). **Do not add a new lint or
format config** to a repo that already has one.
- **Know the project's expected test baseline.** Some inherited suites are not all-green on a
clean checkout; judge regressions against the documented baseline rather than a raw run —
and if a baseline file exists, use it.
---
## 11. Framework Versions Move Fast
> [!WARNING]
> Next.js, Astro, React, and the major bundlers ship breaking changes regularly, and their
> current APIs frequently differ from what is in a model's training data. **Read the version's
> own docs in `node_modules/<pkg>/dist/docs/` or the project's pinned documentation before
> writing code against them.** Heed deprecation notices rather than pattern-matching from memory.
---
## 12. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`bash.md`](./bash.md) — for the build/release scripts around these projects.
+153
View File
@@ -0,0 +1,153 @@
---
name: python
description: >-
Use this skill when writing, linting, type-checking, testing, or packaging Python —
including async code, CLI tools, bot cogs, and library packages. Covers strict typing with
mypy, ruff configuration, virtualenv discipline, async patterns, and packaging.
---
# Python — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds Python specifics.
---
## 1. The Gate
```bash
ruff format --check .
ruff check .
mypy --strict .
pytest
```
All four clean before anything is called done. Configure them in `pyproject.toml` so the
editor, the CLI, and CI all read the same settings.
---
## 2. Typing — Strict
**Every function gets annotated**, including `-> None`. Type hints are not optional
documentation; they are the checker's input.
```toml
[tool.mypy]
strict = true
warn_unreachable = true
warn_return_any = true
disallow_untyped_defs = true
```
- **Never bare `Any`.** Use a `Protocol`, a `TypeVar`, or `object` + narrowing.
- `X | None` over `Optional[X]`; `list[str]` over `List[str]` (modern syntax, no `typing` import).
- `TypedDict` or a dataclass for structured dicts — not `dict[str, Any]`.
- `Literal` and `Enum` for closed sets of values, not bare strings.
- `@dataclass(frozen=True, slots=True)` for value objects.
- Third-party library missing stubs: install `types-*` if it exists, otherwise a narrowly
scoped `# type: ignore[import-untyped]` with a comment — never a blanket module-level ignore.
---
## 3. Linting
`ruff` is the linter and formatter (it replaces flake8, isort, black, pyupgrade, and most
plugins). Enable a broad rule set rather than the default minimum:
```toml
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "ASYNC", "S", "PTH"]
```
`S` (bandit security rules) and `ASYNC` are worth the noise. As always: fix, don't suppress —
and when you must suppress, use a specific code (`# noqa: S603`) with a reason.
---
## 4. Environments
- **Always a virtualenv or project-managed environment** (`uv`, `poetry`, `pdm`, `venv`).
Never install into the system interpreter, and never `pip install --user` as a workaround
for an immutable OS — use a container or a venv.
- `uv` is the preferred installer/resolver where free choice exists; it is dramatically faster
and lockfile-native.
- Commit the lockfile. Pin the Python version in `pyproject.toml` (`requires-python`).
- `pathlib.Path` over `os.path` everywhere (ruff's `PTH` rules enforce this).
---
## 5. Errors & Logging
- **Specific exceptions only.** `except Exception:` needs a reason; bare `except:` is never
acceptable (it catches `KeyboardInterrupt` and `SystemExit`).
- **Never swallow silently.** If you catch and continue, log at `warning` or above.
- `raise ... from err` to preserve the chain.
- Custom exception types deriving from a single package-level base, so callers can catch the
whole family.
- **`logging` module, never `print`,** in anything that isn't a CLI writing to stdout as its
actual output. One `logger = logging.getLogger(__name__)` per module, at module scope.
- **Never log secrets** — tokens, auth headers, message content, personal data.
---
## 6. Async
- `asyncio` with `async`/`await`. **Never block the event loop** — no `time.sleep`, no
synchronous `requests`, no blocking file I/O in a coroutine. Use `asyncio.to_thread` for
genuinely blocking calls.
- `httpx` (or the framework's own client) over `requests` in async code.
- **`asyncio.TaskGroup`** (3.11+) over bare `create_task` — it propagates exceptions and
cancels siblings instead of losing them to an unawaited task.
- **Every `create_task` result must be held in a reference**, or the garbage collector can
cancel it mid-flight.
- Cancellation is cooperative: clean up in `finally`, and never swallow `CancelledError`.
- **Events over polling.** Subscribe to the library's event/callback API; do not loop on a
timer. Where polling is genuinely unavoidable (no event source exists), use a **staggered
backoff** rather than a fixed interval, and say why polling was necessary.
---
## 7. Rate Limiting & External Services
When talking to a rate-limited API (a chat platform, a store, a tracker):
- **Funnel outbound calls through a single queue/worker** rather than calling from arbitrary
call sites. It is the only place a limit can actually be enforced.
- **Backoff with jitter** on retry; never a tight retry loop.
- Respect `Retry-After` and documented limits; assume undocumented ones exist.
- **Be extremely careful scraping real consumer services.** Conservative delays, realistic
headers, plenty of failsafes, and abort fast when a response looks unexpected. Assume you
are one mistake away from a ban.
---
## 8. Structure
- Package layout: `src/<package>/`, tests in `tests/`.
- **Shared behavior goes in a real shared module**, imported by each consumer — not
copy-pasted between sibling packages. When three components grow the same queue/retry/
formatting logic, that is the signal to extract it.
- Reusable, generic functionality (a format parser, a protocol client) becomes **its own
installable package** with its own repo and version, consumed as a dependency.
- Keep persistent state in the framework's own config/storage system so it survives restarts;
do not invent a parallel JSON file beside it.
- File and function size limits from the general rules apply.
---
## 9. Testing
- `pytest`, with `pytest-asyncio` for coroutines.
- Fixtures over setup/teardown methods; `tmp_path` for anything touching the filesystem.
- **Regression test every fixed bug.**
- Mock at the network boundary (`respx`, `responses`), not by monkeypatching your own
internals — a test that patches the code under test proves nothing.
- **Never exercise destructive operations against real accounts or real data.** Temporary
fixtures only.
- `pytest -p no:randomly` only when order genuinely matters, and note why.
---
## 10. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`bash.md`](./bash.md) — for the surrounding build and deploy scripts.
+188
View File
@@ -0,0 +1,188 @@
---
name: rust
description: >-
Use this skill when writing, building, linting, testing, or releasing Rust code — including
workspace lint configuration, clippy/fmt gates, error handling with Result/thiserror,
unsafe policy, async patterns, and cargo workspace/submodule layout.
---
# Rust — Rules & Preferences
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds Rust specifics.
---
## 1. The Gate
Nothing is "done" until all of these are clean:
```bash
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features
cargo test --workspace
cargo doc --workspace --no-deps
cargo build --release
```
> [!IMPORTANT]
> **`--workspace` is not optional.** Without it only the root package is checked and every
> member crate's warnings stay invisible. This has hidden real warnings for weeks at a time.
> `--all-targets` additionally covers tests, benches, and examples — which are also code.
- `cargo fmt` before every commit. **No hand-formatting**, ever.
- `cargo doc` matters: a broken intra-doc link is a reference to something that no longer
exists, which is worth knowing about.
- Run the project's build script (`./scripts/build.sh`) if it has one — it wires these
together plus size checks and a smoke run.
---
## 2. Lints — Pedantic On, Configured In One Place
```toml
# Cargo.toml (workspace root)
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
too_many_lines = "warn"
unwrap_used = "warn"
[workspace.lints.rust]
unsafe_code = "forbid"
```
```toml
# every member crate
[lints]
workspace = true
```
> [!CAUTION]
> **Lint config lives in `[workspace.lints]` only.** A package that repeats the configuration
> carries a second copy that drifts — which is how a binary once kept reading its own stale
> allows while the workspace's were being actively edited.
- **Never `#[allow]` on a whim.** If a lint is genuinely wrong for the codebase, allow it
**workspace-wide** with a comment explaining why, so the decision is visible.
- **Re-test every `#[allow]` periodically.** If removing one produces no warning, delete it.
- `too_many_lines` is the mechanical enforcement of the 100-line function limit. A function
that trips it wants extracting, not an allow.
---
## 3. Errors & Panics
- **`Result` and `Option` everywhere.** `thiserror` for library error enums;
`anyhow` is acceptable at the application/binary boundary only.
- **No `unwrap()` or `expect()` outside tests and `main`.** Anything that can be absent at
runtime — a device, a file, a network peer, an env var — must be handled. Hardware and
network resources appear and disappear while the program runs.
- No `panic!` as flow control. A library never panics on input it was designed to receive.
- Never `?`-swallow context — attach it (`.context(...)` / a typed variant) so the error says
where it came from.
---
## 4. `unsafe`
**Forbidden by default**, enforced in `Cargo.toml` with `unsafe_code = "forbid"`.
If an FFI binding genuinely requires it, it goes behind a **safe wrapper in exactly one
module**, with a `// SAFETY:` comment on every block stating the invariant being upheld.
`unsafe` scattered through call sites is not acceptable.
---
## 5. Dependencies & Workspace Layout
Follow the global ordering (reuse → fork → write). In Rust specifically:
- Search crates.io and check maintenance status before adding. Prefer pure-Rust crates over
ones requiring a C toolchain when the difference is not material — a C dependency turns a
one-command build into a container requirement.
- **Pin precisely when it matters.** If you need an API that only exists on a git revision,
pin the revision and **write down why** — a future reader will otherwise "helpfully"
bump it to the crates.io release that lacks the API.
- **Reusable logic goes in its own crate**, published separately, with the application as a
consumer. The workspace layout that follows from this:
```
crates/<name>-core/ # all reusable logic, no UI, no framework deps
crates/<name>-proto/ # wire types & protocol parsing, dependency-light
src/ # or crates/<name>-ui — presentation only
```
- **Submodule hygiene**: commit and push inside the submodule crate first, then commit the
updated SHA pointer in the parent. Match each repo's own commit-trailer and identity
conventions — a public library repo and a private app repo often differ deliberately.
---
## 6. Async
- Tokio runtime with `futures` streams; `async_trait` where trait methods must be async.
- **Model new flows as streams or async methods**, matching what the codebase already does.
- **Never block in async context.** Use `spawn_blocking` for genuinely blocking work.
- Subscribe to event streams; never poll on a timer (see the global events-over-polling rule).
- Don't short-circuit a hand-written `Stream` impl that manually pumps backends — it usually
encodes ordering guarantees that are not obvious from the signature.
---
## 7. Feature Flags & `cfg`
- Feature flags declared in the root `Cargo.toml`.
- **Gate at module level, not per function** — per-function `cfg` leaves a trail of empty
stubs and makes the real platform differences hard to see.
- Adding a platform backend: declare the feature, create the gated module, and **log which
backend was selected at startup**. Silent backend selection is very hard to debug remotely.
- Use `build.rs`-exported configs for conditions that aren't plain features.
---
## 8. Style Details
- `unsafe`-free, `unwrap`-free, and formatted — see above.
- **No `pub` on anything that doesn't need to cross a module boundary.** Default to private
and widen deliberately.
- Max ~5 parameters; past that, pass a struct.
- Prefer `f32` for UI/geometry when the framework's native unit is `f32` — mixing in `f64`
produces a stream of casts and rounding inconsistencies.
- Derive rather than hand-implement (`Debug`, `Clone`, `Default`, `PartialEq`) unless the
hand implementation is doing something a derive cannot.
- `#[non_exhaustive]` on public enums/structs that are likely to grow.
---
## 9. Testing
- Unit tests inline in `#[cfg(test)] mod tests`; integration tests in `tests/`.
- `cargo test --workspace` — plus `--release` when timing or optimization matters to the
behavior under test.
- **Regression test every fixed bug.**
- A dummy/mock backend that exercises the full pipeline without real hardware is worth
building early — it is the only way most device code gets tested at all.
- Never point test output at real hardware. Route test signal to a virtual/null endpoint and
**verify the route resolved** — most tools silently fall back to the system default when
the requested target does not exist, which sends your test straight at the user.
---
## 10. Building in Containers
When the crate needs system headers the host does not have (audio, graphics, webkit),
build inside the project's designated container. Chain `cd` inside the same command string —
entering a container resets the working directory:
```bash
distrobox enter <container> -- bash -c "cd /path/to/project && cargo build --release"
```
Prefer the project's `scripts/build.sh` over a hand-typed invocation; it also handles job
throttling so a release build does not saturate the machine.
**Split so that as little as possible needs the container**: a `-core` crate with no GUI
dependencies builds and tests directly on the host, which is seconds instead of minutes.
---
## 11. Related
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
- [`c-cpp.md`](./c-cpp.md) — when bindgen/FFI crosses into a C library.