Skip to content

Instantly share code, notes, and snippets.

@thimslugga
Forked from opj161/AGENTS.md-Setup-Guide.md
Last active August 27, 2026 22:15
Show Gist options
  • Select an option

  • Save thimslugga/6ea84d9571525975414e0d1a4b9133f3 to your computer and use it in GitHub Desktop.

Select an option

Save thimslugga/6ea84d9571525975414e0d1a4b9133f3 to your computer and use it in GitHub Desktop.
The Definitive Guide to AGENTS.md & Documentation Routing for AI Coding Agents
# .bunfig.toml
[install]
minimumReleaseAge = 604800 # 7 days in seconds
{
"version": "0.2",
"language": "en",
"ignorePaths": [
"node_modules/**",
"binaries/**",
".vscode-test/**",
"assets/*",
"dist/**",
"out/**",
"*.vsix",
"package-lock.json"
],
"words": ["vsceignore", "vsix", "ovsx"]
}
# EditorConfig: https://editorconfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
spelling_language = en-US
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[.git*]
indent_style = tab
indent_size = tab
trim_trailing_whitespace = unset
insert_final_newline = unset
[*.{sh,bash}]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = false
insert_final_newline = false
max_line_length = 80
shell_variant = bash
[*.{zsh}]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = false
insert_final_newline = false
max_line_length = 80
shell_variant = zsh
[*.{cmd,bat,ps1}]
end_of_line = crlf
[Makefile]
indent_style = tab
indent_size = 4
[justfile]
indent_style = space
indent_size = 2
[*.{py,pyi}]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
docstring_fill_column = 99 # Docstring line wrapping
[.venvs/**.py]
indent_size = unset
indent_style = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
end_of_line = unset
[*.rb]
indent_style = space
indent_size = 2
[*.php]
indent_style = space
indent_size = 4
[*.{pl,pm}]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,jsx,bun,coffee}]
indent_style = space
indent_size = 2
quote_type = single
[*.{ts,tsx}]
indent_style = space
indent_size = 2
[node_modules/**.js]
indent_size = unset
indent_style = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
end_of_line = unset
[*.rs]
indent_style = space
indent_size = 4
insert_final_newline = false
trim_trailing_whitespace = true
max_line_length = 100
[tests/**/*.rs]
charset = utf-8
indent_size = unset
indent_style = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
end_of_line = unset
[*.go]
indent_style = tab
indent_size = 4
[*.{json,jsonc,json5}]
indent_style = space
indent_size = 2
[package.json]
indent_style = space
indent_size = 2
[*.{yaml,yml,yamllint,ansible-lint,butane,bu}]
indent_style = space
indent_size = 2
[*.{json,jsonc}]
indent_style = space
indent_size = 2
[*.toml]
indent_style = space
indent_size = 4
max_line_length = 80
[*.{xml,plist}]
indent_style = space
indent_size = 2
insert_final_newline = ignore
[*.{ini,cfg,conf}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
[*.{tf,tfvars,tf.json}]
indent_style = space
indent_size = 2
[*.{pkr.hcl,pkr.hcl.json}]
indent_style = space
indent_size = 2
[*.{bazel,bzl}]
indent_size = 4
indent_style = space
[*.{csv,tsv}]
indent_style = space
indent_size = 4
tab_width = 4
trim_trailing_whitespace = false
insert_final_newline = true
[*.graphql]
indent_style = space
indent_size = 2
[*.{html,htm}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.{css,scss,less}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
indent_style = space
indent_size = 2
trim_trailing_whitespace = false
[CHANGELOG.md]
indent_style = unset
indent_size = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
[LICENSE]
indent_style = unset
indent_size = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
[*.txt]
indent_style = unset
indent_size = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
# .golangci.yml
# https://golangci-lint.run/jsonschema/golangci.jsonschema.json
#
# 1. Catch the things agents get *wrong* (dropped errors, leaked resources,
# context misuse, concurrency bugs, stale/placeholder code).
# 2. Catch the things agents *fake* (TODO stubs, commented-out code,
# debug prints, //nolint used as a fix).
# 3. Cap the things agents *overproduce* (500-line functions, copy-pasted
# blocks, 20-method interfaces).
# 4. Stay deterministic: `linters.default: none` means a golangci-lint
# upgrade never silently adds a linter and breaks CI.
#
# Validate after any edit:
# golangci-lint config verify
version: "2"
run:
# Agents run this in a loop; fail loudly instead of hanging forever.
timeout: 5m
# Paths in exclusions/reports are relative to go.mod, so the same config
# behaves identically from any working directory (agents cd around a lot).
relative-path-mode: gomod
issues-exit-code: 1
tests: true
# Fail if go.mod would need to change. Agents must not mutate deps silently.
modules-download-mode: readonly
# Multiple agents/terminals can invoke the linter at once; queue them behind
# a lock instead of erroring out or thrashing the CPU.
allow-parallel-runners: false
allow-serial-runners: true
linters:
# Explicit allow-list i.e. nothing runs unless it is named below.
default: none
enable:
# --- Core correctness (golangci-lint's "standard" set) ------------------
- errcheck # unchecked errors
- govet # the go vet analyzers
- ineffassign # assignments never used
- staticcheck # SA/S/ST/QF checks
- unused # unused code
# --- Error handling: agents' single biggest failure mode ----------------
- errorlint # %w vs %v, err == vs errors.Is, type asserts on errors
- errname # ErrFoo / FooError naming
- errchkjson # unhandled json.Marshal errors
- nilerr # `if err != nil { return nil }` — swallowed error
- nilnesserr # returning the *wrong* nil-ness error
- nilnil # `return nil, nil` from (T, error)
- forcetypeassert # x.(T) without the ok form
# --- Resource leaks and I/O ---------------------------------------------
- bodyclose # unclosed http.Response.Body
- sqlclosecheck # unclosed sql.Rows / sql.Stmt
- rowserrcheck # missing rows.Err()
- noctx # HTTP/SQL calls without a context
- unqueryvet # `SELECT *` in SQL strings
# --- Context handling ----------------------------------------------------
- contextcheck # non-inherited context passed down
- containedctx # context.Context stored in a struct
- fatcontext # context repeatedly wrapped inside a loop
# --- Concurrency / time --------------------------------------------------
- durationcheck # time.Duration multiplied by a Duration
- copyloopvar # leftover pre-Go1.22 loop-var copies
- makezero # append to a non-zero-length make()
# --- Placeholders, dead code, debug leftovers ----------------------------
- godox # TODO / FIXME / HACK left behind
- forbidigo # fmt.Println debugging, spew.Dump, etc.
- gochecknoinits # init() used as a dumping ground
- dupl # copy-pasted blocks instead of a helper
- unconvert # redundant type conversions
- unparam # params/returns that are always the same value
- wastedassign # value assigned then overwritten
- reassign # reassigning package-level vars of other packages
- dogsled # `a, _, _, _ := f()`
# --- Complexity ceilings -------------------------------------------------
- cyclop # cyclomatic complexity
- gocognit # cognitive complexity
- funlen # function length
- nestif # deeply nested ifs
- maintidx # maintainability index
- interfacebloat # oversized interfaces
# --- Idiomatic / modern Go ----------------------------------------------
- gocritic # broad diagnostic + style + perf check set
- revive # configurable golint successor
- modernize # rewrite to modern stdlib idioms (min/max, slices, ...)
- exptostd # golang.org/x/exp/* that is now in the stdlib
- usestdlibvars # http.StatusOK instead of 200
- intrange # `for i := range n`
- mirror # string/[]byte API mismatches
- perfsprint # fmt.Sprintf where strconv is correct
- predeclared # shadowing len, cap, new, ...
- recvcheck # mixed pointer/value receivers
- iface # redundant / unused interfaces
- iotamixing # explicit values mixed into an iota block
- embeddedstructfieldcheck
- goprintffuncname
- gocheckcompilerdirectives # malformed //go: directives
- canonicalheader # non-canonical HTTP header keys
- gomoddirectives # replace/exclude/toolchain hygiene
- asasalint # []any passed to a ...any function
- loggercheck # odd number of key/value log args
- musttag # (un)marshalled structs missing tags
- sloglint # log/slog usage
# --- Security ------------------------------------------------------------
- gosec
- depguard # banned imports (see settings)
- asciicheck # non-ASCII identifiers
- bidichk # bidirectional-override "trojan source" runes
# --- Docs and comments ---------------------------------------------------
- godoclint # doc comment structure and correctness
- godot # comments end with a period
- misspell
- dupword # "the the"
- lll # line length
# --- Tests ---------------------------------------------------------------
- thelper # helpers must call t.Helper()
- tparallel # t.Parallel() misuse
- testableexamples
- testifylint # correct testify assertions (no-op without testify)
- usetesting # t.TempDir/t.Setenv/t.Context over manual equivalents
# --- Magic numbers and formatting ---------------------------------------
- mnd # magic numbers
- whitespace # leading/trailing newlines in blocks
# --- Anti-gaming: the most important linter in this file -----------------
- nolintlint # //nolint must be specific, explained, and used
# ---- Opt-in: uncomment per project -------------------------------------
# - wrapcheck # every error from another package must be wrapped
# - err113 # no dynamic errors (errors.New inside functions)
# - exhaustruct_v5 # every struct field must be set explicitly
# - gochecknoglobals # no package-level mutable state
# - nonamedreturns # no named return values
# - paralleltest # every test must call t.Parallel()
# - testpackage # tests live in package foo_test
# - varnamelen # short names must have short scopes
# - ireturn # return concrete types, accept interfaces
# - prealloc # slice preallocation (profile first)
# - goheader # license header enforcement
# - importas # enforced import aliases
# - gomodguard_v2 # module allow/block lists
# - exhaustive # exhaustive enum switches (also see settings below)
#
# ---- Domain-specific: enable only if the dependency is present ---------
# - spancheck # OpenTelemetry spans
# - zerologlint # zerolog
# - ginkgolinter # ginkgo/gomega
# - protogetter # protobuf getters
# - promlinter # prometheus metric naming
# - arangolint # ArangoDB
# - clickhouselint # ClickHouse
# - gosmopolitan # i18n / hardcoded locale
# - tagalign # struct tag alignment
# - tagliatelle # struct tag casing
settings:
# -----------------------------------------------------------------------
# errcheck: the highest-signal linter for agent-written Go.
# -----------------------------------------------------------------------
errcheck:
# Catches `x := y.(T)` written without the comma-ok form.
check-type-assertions: true
# Catches `_ = doThing()`. Agents use this to "resolve" a lint failure.
# This is deliberate friction: blanking an error must be a conscious act.
check-blank: true
exclude-functions:
# These never return a non-nil error.
- (*strings.Builder).Write
- (*strings.Builder).WriteByte
- (*strings.Builder).WriteRune
- (*strings.Builder).WriteString
- (*bytes.Buffer).Write
- (*bytes.Buffer).WriteByte
- (*bytes.Buffer).WriteRune
- (*bytes.Buffer).WriteString
# Best-effort diagnostics to stderr.
- fmt.Fprint(os.Stderr)
- fmt.Fprintf(os.Stderr)
- fmt.Fprintln(os.Stderr)
govet:
enable-all: true
disable:
# Real but low-value; forces field reordering for a few bytes.
- fieldalignment
# NOTE: `shadow` is intentionally left on. `err :=` inside an if-block
# that shadows the outer `err` is a classic generated-code bug.
staticcheck:
checks:
- all
# Package-level doc comment requirement; godoclint owns this instead.
- -ST1000
# Naming/initialisms; revive's var-naming owns this instead.
- -ST1003
# -----------------------------------------------------------------------
# revive: explicit rule list. Anything not listed here does not run.
# -----------------------------------------------------------------------
revive:
severity: error
# //revive:disable must carry a reason.
directives:
- name: specify-disable-reason
severity: error
rules:
# Correctness
- name: atomic
- name: call-to-gc
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: datarace
- name: defer
- name: error-return
- name: errorf
- name: forbidden-call-in-wg-go
- name: get-return
- name: identical-branches
- name: identical-ifelseif-branches
- name: identical-ifelseif-conditions
- name: identical-switch-branches
- name: identical-switch-conditions
- name: inefficient-map-lookup
- name: modifies-parameter
- name: modifies-value-receiver
- name: range-val-address
- name: range-val-in-closure
- name: struct-tag
- name: time-date
- name: time-equal
- name: unconditional-recursion
- name: unreachable-code
- name: unsecure-url-scheme
- name: use-waitgroup-go
- name: waitgroup-by-value
# Structure / readability
- name: bool-literal-in-expr
- name: deep-exit # os.Exit / log.Fatal outside main and init
- name: early-return
- name: empty-block
- name: flag-parameter # boolean args that switch behaviour
- name: if-return
- name: increment-decrement
- name: indent-error-flow
- name: optimize-operands-order
- name: range
- name: superfluous-else
- name: unnecessary-format
- name: unnecessary-if
- name: unnecessary-stmt
- name: useless-break
- name: useless-fallthrough
- name: unused-parameter
- name: var-declaration
# Naming and imports
- name: blank-imports
- name: dot-imports
- name: duplicated-imports
- name: epoch-naming
- name: error-naming
- name: error-strings
- name: package-directory-mismatch
- name: receiver-naming
- name: redefines-builtin-id
- name: redundant-build-tag
- name: redundant-import-alias
- name: redundant-test-main-exit
- name: time-naming
- name: unexported-naming
- name: unexported-return
- name: var-naming
# Modern Go
- name: string-of-int
- name: use-any
- name: use-errors-new
- name: use-slices-sort
# Docs. Requires a comment on every exported symbol.
# Drop this rule if you are retrofitting a large existing codebase.
- name: exported
arguments:
- checkPrivateReceivers
- sayRepetitiveInsteadOfStutters
# -----------------------------------------------------------------------
# gocritic: broad net. `experimental` is included because that tag holds
# commentedOutCode / commentedOutImport / docStub / todoCommentWithoutDetail,
# which are precisely the agent-slop detectors.
# -----------------------------------------------------------------------
gocritic:
enabled-tags:
- diagnostic
- style
- performance
- experimental
disabled-checks:
- whyNoLint # nolintlint owns this
- ruleguard # needs external rule files
- hugeParam # perf micro-opt, high noise
- rangeValCopy # ditto
- rangeExprCopy # ditto
- importShadow # fires on `url`, `path`, `context`, ...
- paramTypeCombine # cosmetic
- unnamedResult # cosmetic
- octalLiteral # cosmetic
- nestingReduce # overlaps nestif
- unnecessaryBlock # cosmetic
- tooManyResultsChecker # overlaps unparam
- ptrToRefParam # opinionated
# -----------------------------------------------------------------------
# Placeholder / stub detection.
# -----------------------------------------------------------------------
godox:
keywords:
- TODO
- FIXME
- BUG
- HACK
- XXX
- OPTIMIZE
- "NOT IMPLEMENTED"
forbidigo:
# Resolve through type info, so renamed imports and methods are caught.
analyze-types: true
exclude-godoc-examples: true
forbid:
- pattern: ^fmt\.Print(f|ln)?$
msg: Use log/slog (or the project logger) instead of printing to stdout.
- pattern: ^print(ln)?$
msg: Builtin print/println are for compiler bootstrapping only.
- pattern: ^spew\.(ConfigState\.)?Dump$
msg: Remove debug dumps before committing.
- pattern: ^litter\.Dump$
msg: Remove debug dumps before committing.
# Uncomment to ban panics in library code:
# - pattern: ^panic$
# msg: Return an error instead of panicking.
# -----------------------------------------------------------------------
# Complexity ceilings. Tune upward for legacy code, never for new code.
# -----------------------------------------------------------------------
cyclop:
max-complexity: 15
package-average: 0.0
gocognit:
min-complexity: 20
funlen:
lines: 80
statements: 45
ignore-comments: true
nestif:
min-complexity: 5
maintidx:
under: 20
interfacebloat:
max: 8
dupl:
threshold: 120
nakedret:
max-func-lines: 20
lll:
line-length: 120
tab-width: 1
# -----------------------------------------------------------------------
# Constants and magic values.
# -----------------------------------------------------------------------
goconst:
min-len: 3
min-occurrences: 3
find-duplicates: true
ignore-tests: true
numbers: false
mnd:
checks:
- argument
- case
- condition
- return
ignored-numbers:
- "0"
- "1"
- "2"
- "10"
- "100"
- "1000"
- "0o600"
- "0o644"
- "0o755"
ignored-functions:
- ^make$
- ^math\.
- ^time\.(Duration|Second|Millisecond|Minute|Hour)$
- ^strconv\.(Format|Parse).*
- ^http\.StatusText$
# -----------------------------------------------------------------------
# Imports policy. `deny` with no `allow` list blocks only what is listed.
# -----------------------------------------------------------------------
depguard:
rules:
all:
deny:
- pkg: io/ioutil$
desc: Deprecated since Go 1.16. Use the io and os packages.
- pkg: github.com/pkg/errors$
desc: Use the stdlib errors package with fmt.Errorf and %w.
- pkg: math/rand$
desc: Use math/rand/v2, or crypto/rand for anything security-related.
# - pkg: github.com/sirupsen/logrus
# desc: Use log/slog.
# - pkg: github.com/golang/protobuf
# desc: Use google.golang.org/protobuf.
# -----------------------------------------------------------------------
# Security.
# -----------------------------------------------------------------------
gosec:
severity: low
confidence: low
excludes:
- G104 # duplicate of errcheck
# - G115 # integer overflow on conversion; real, but very noisy
config:
G302: "0600" # chmod permissions
G306: "0600" # WriteFile permissions
# -----------------------------------------------------------------------
# Errors, nil, and logging detail.
# -----------------------------------------------------------------------
errorlint:
errorf: true
errorf-multi: true
asserts: true
comparison: true
nilnil:
detect-opposite: true
sloglint:
no-mixed-args: true
static-msg: true # no fmt.Sprintf in the log message
context: scope # use the ctx variant when a ctx is in scope
# -----------------------------------------------------------------------
# Misc tuning.
# -----------------------------------------------------------------------
copyloopvar:
check-alias: true
embeddedstructfieldcheck:
empty-line: false
forbid-mutex: true # embed a named field, don't export Lock/Unlock
iotamixing:
report-individual: true
dupword:
comments-only: true
skip-raw-strings: true
misspell:
locale: US
godoclint:
# "basic" is the tool's own default rule set.
# Tune with, e.g.:
# enable: [start-with-name, no-unused-link]
# options:
# require-doc: {ignore-exported: false, ignore-unexported: true}
default: basic
# Enable together with the `exhaustive` linter above.
exhaustive:
check:
- switch
- map
default-signifies-exhaustive: true
# -----------------------------------------------------------------------
# nolintlint: stops "I made the linter quiet" from counting as a fix.
# -----------------------------------------------------------------------
nolintlint:
# A //nolint that suppresses nothing is a leftover; report it.
allow-unused: false
# `//nolint:errcheck`, never a bare `//nolint`.
require-specific: true
# `//nolint:errcheck // <why>` — the reason is mandatory.
require-explanation: true
allow-no-explanation: []
# -------------------------------------------------------------------------
# Exclusions. Deliberately minimal.
#
# `presets` is left unset on purpose. The built-in `std-error-handling`
# preset suppresses unchecked errors on Close/Flush/Write, which is one of
# the specific things this config exists to catch.
# -------------------------------------------------------------------------
exclusions:
generated: lax
warn-unused: false
# presets:
# - common-false-positives
# - legacy
paths:
- third_party$
- builtin$
- examples$
- testdata$
- '.*\.pb\.go$'
- '.*\.pb\.gw\.go$'
- '.*_generated\.go$'
- '.*_gen\.go$'
- '(^|/)mocks?/'
rules:
# Tests may repeat themselves and be long; they still may not ignore
# errors, print to stdout, or leak resources.
- path: (.+)_test\.go
linters:
- dupl
- funlen
- gocognit
- cyclop
- maintidx
- goconst
- mnd
- gosec
- containedctx
- lll
# Table-driven test fixtures legitimately hold magic values.
- path: (.+)_test\.go
text: 'Magic number'
linters:
- mnd
# A CLI's job is to write to stdout.
- path: (^|/)cmd/
linters:
- forbidigo
# go:generate directives and long URLs are allowed to exceed line length.
- linters:
- lll
source: '^//(go:generate|nolint)'
- linters:
- lll
source: 'https?://'
formatters:
enable:
- gofumpt # gofmt plus the rules gofmt was too conservative to add
- gci # deterministic import grouping and ordering
settings:
gofumpt:
extra-rules: true
gci:
sections:
- standard # stdlib
- default # third-party
- localmodule # this module, auto-detected from go.mod
custom-order: true
exclusions:
generated: lax
paths:
- third_party$
- '.*\.pb\.go$'
- '.*_gen\.go$'
issues:
# Show every issue. Truncated output makes an agent "fix" three problems,
# re-run, and discover twelve more — burning turns and context.
max-issues-per-linter: 0
max-same-issues: 0
uniq-by-line: false
# Never auto-fix by default. `--fix` must be an explicit, reviewable step.
fix: false
# Retrofitting an existing codebase? Gate only new code:
# new: true
# new-from-merge-base: origin/main
# whole-files: false
output:
# Group by file so an agent can work through one file at a time.
sort-order:
- file
- severity
- linter
show-stats: true
formats:
text:
path: stdout
print-linter-name: true
print-issued-lines: true
severity:
default: error
rules:
# Advisory in SARIF/Code Climate reports. Still fails the run locally.
- linters:
- godox
- godoclint
- godot
- misspell
- dupword
- dupl
severity: warning
# .npmrc
#prefix=~/.local/npm-global
min-release-age=7
ignore-scripts=true
allow-git=none
sign-git-tag=true
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"objectWrap": "preserve",
"endOfLine": "lf",
"insertFinalNewline": true,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"embeddedLanguageFormatting": "auto",
"singleAttributePerLine": false,
"vueIndentScriptAndStyle": false,
"sortPackageJson": true,
"sortImports": {
"order": "asc",
"ignoreCase": true,
"newlinesBetween": true,
"sortSideEffects": false,
"internalPattern": ["~/", "@/", "#"],
"groups": [
"builtin",
"external",
["internal", "subpath"],
["parent", "sibling", "index"],
"style",
"unknown"
]
},
"overrides": [
{
"files": ["*.md", "*.mdx"],
"options": {
"printWidth": 80,
"proseWrap": "preserve"
}
},
{
"files": ["*.json", "*.jsonc", "*.json5", "*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
},
{
"files": ["*.{test,spec}.{js,mjs,cjs,ts,tsx}"],
"options": {
"printWidth": 120
}
}
],
"ignorePatterns": [
"node_modules/",
"dist/",
"build/",
"out/",
"coverage/",
"vendor/",
"**/*.min.js",
"**/*.bundle.js",
"pnpm-lock.yaml",
"package-lock.json"
]
}
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "promise"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
"pedantic": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"env": {
"builtin": true,
"es2024": true,
"browser": true,
"node": true
},
"globals": {},
"settings": {},
"rules": {
"eslint/eqeqeq": ["error", "smart"],
"eslint/no-debugger": "error",
"eslint/no-var": "error",
"eslint/prefer-const": ["error", { "destructuring": "all" }],
"eslint/no-console": "warn",
"eslint/no-unused-vars": [
"error",
{
"args": "after-used",
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"import/no-duplicates": "error",
"unicorn/prefer-node-protocol": "warn"
},
"overrides": [
{
"files": ["**/*.{ts,tsx,mts,cts}"],
"plugins": ["typescript"],
"rules": {
"typescript/no-explicit-any": "warn",
"typescript/consistent-type-imports": "warn"
}
},
{
"files": [
"**/*.{test,spec}.{js,mjs,cjs,ts,tsx}",
"**/test/**",
"**/tests/**",
"**/__tests__/**"
],
"plugins": ["vitest"],
"rules": {
"eslint/no-console": "off",
"typescript/no-explicit-any": "off"
}
},
{
"files": ["scripts/**", "tools/**", "*.config.{js,mjs,cjs,ts,mts}"],
"env": {
"node": true
},
"rules": {
"eslint/no-console": "off"
}
}
],
"ignorePatterns": [
"node_modules/",
"dist/",
"build/",
"bin/",
"out/",
"coverage/",
"vendor/",
"pkg/",
"target/",
"__pycache__/",
".venv/",
"venv/",
"website/public/parsers/*.wasm",
"**/*.min.js",
"**/*.bundle.js"
]
}
# .pre-commit-config.yaml
# Install tool:
# brew install prek
# uv tool install prek
# mise use prek
#
# Update tool:
# brew install prek
# uv tool upgrade prek
# prek self update
#
# Install hooks:
# prek install
# prek install --prepare-hooks
#
# Agent Skill:
# https://github.com/j178/prek/blob/master/skills/prek/SKILL.md
default_language_version:
python: "3.12"
#exclude:
# glob: "bar/**"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
hooks:
- id: check-added-large-files
args: ["--maxkb=750"]
exclude: ^uv.lock$
- id: check-toml
- id: check-yaml
args:
- --unsafe
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.25.0
hooks:
- id: gitleaks
- repo: local
hooks:
- id: local-ruff-lint
name: Python linting
entry: ruff check --force-exclude --fix --exit-non-zero-on-fix
files: "\\.py$"
require_serial: true
language: system
types: [python]
- id: local-ruff-format
name: Python formatting check
entry: ruff format --check
#entry: ruff format --force-exclude --exit-non-zero-on-format
files: "\\.py$"
require_serial: true
language: system
types: [python]
- id: local-ty
name: ty check
entry: ty check
require_serial: true
language: unsupported
pass_filenames: false
- id: golangci-lint
name: Go linting
entry: golangci-lint run
language: system
types: [go]
pass_filenames: false
- id: cargo-fmt
name: cargo fmt
language: system
entry: cargo fmt --
types: [rust]
pass_filenames: false
# .rumdl.toml
# Global configuration options
[global]
# Include only specific files
include = [
"README.md",
"DEVELOPMENT.md",
"skills/**/SKILL.md",
"docs/**/*.md",
"**/*.md"
]
# Exclude files and directories
exclude = [
".git",
".svn",
".hg",
"vendor",
"node_modules",
"venv",
".venv",
"build",
"dist",
"*.tmp.md",
"docs/generated/**",
"docs/adr/**",
"docs/rfc/**"
]
respect-gitignore = true
# Set markdown flavor (standard, gfm, mkdocs, mdx, pandoc, quarto, obsidian, kramdown, azure_devops)
flavor = "github"
# Set global line length (used by MD013 and other line-length rules)
line-length = 1000
[MD013]
line-length = 1000
code-blocks = true
tables = false
headings = true
reflow = true
[MD029]
style = "ordered"
[MD046]
# Code block style
style = "fenced"
[MD048]
# Code fence style
style = "backtick"
[MD049]
# Emphasis style (* or _)
style = "asterisk"
[MD050]
# Strong emphasis style (** or __)
style = "asterisk"
[MD051]
anchor-style = "github"
[MD055]
# Table pipe style (leading_and_trailing or no_leading_or_trailing)
style = "leading_and_trailing"
[MD060]
enabled = true
style = "aligned"
[code-block-tools]
enabled = true
timeout = 60000 # 60 seconds
[code-block-tools.languages]
shell = { lint = ["shellcheck"], format = ["shfmt"] }
python = { lint = ["ruff:check"], format = ["ruff:format"] }
javascript = { lint = ["eslint"], format = ["prettier"] }
typescript = { lint = ["eslint"], format = ["prettier"] }
rust = { format = ["rustfmt"] }
go = { format = ["gofmt"] }
json = { lint = ["jq"], format = ["jq"] }
plaintext = { enabled = false }
text = { enabled = false }
[code-block-tools.languages.markdown]
lint = ["rumdl"]
[code-block-tools.language-aliases]
py = "python"
py3 = "python"
sh = "shell"
bash = "shell"
zsh = "shell"
rs = "rust"
go = "go"
#jsonc = "json"
# .shellcheckrc
# ShellCheck: https://www.shellcheck.net/wiki/
# Default shell dialect
shell=bash
# Always allow ShellCheck to open arbitrary files from 'source' statements.
external-sources=true
# Optional: Enable all optional checks
enable=all
# Optional: Set color settings (auto, always, never)
color=auto
# .vale.ini
# vale: https://docs.vale.sh/topics/installation
StylesPath = styles
MinAlertLevel = suggestion
# https://vale.sh/generator
# https://github.com/tbhb/vale-ai-tells
Packages = Google, write-good, proselint, https://github.com/tbhb/vale-ai-tells/releases/download/v1.31.0/ai-tells.zip, \
https://github.com/tbhb/vale-ai-tells/releases/download/v1.31.0/ai-tells-commits.zip
[*.{html,md,txt}]
# ^ This section applies to HTML, Markdown, Plain text files only.
#
# You can change (or add) file extensions here
# to apply these settings to other file types.
#
# For example, to apply these settings to both
# Markdown and reStructuredText:
#
# [*.{md,rst}]
BasedOnStyles = Vale, Google, write-good, proselint, ai-tells
[formats]
COMMIT_EDITMSG = md
[{COMMIT_EDITMSG,.git/COMMIT_EDITMSG}]
BasedOnStyles = ai-tells, ai-tells-commits
ai-tells.SycophancyMarkers = NO
ai-tells.ClosingPleasantries = NO
---
# https://www.schemastore.org/yamllint.json
# For configuration, see: https://yamllint.readthedocs.io/
extends: default
locale: en_US.UTF-8
yaml-files:
- "*.yaml"
- "*.yml"
- ".yamllint"
- ".ansible-lint"
- "*.bu"
- "*.butane"
ignore: |
.git
.venv
venv
node_modules
.cache
.pytest_cache
.tox
.coverage_cache
.mypy_cache
.ruff_cache
.rumdl_cache
rules:
braces:
level: error
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
level: error
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
level: error
max-spaces-before: 0
max-spaces-after: 1
commas:
level: error
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
level: error
require-starting-space: true
min-spaces-from-content: 2
comments-indentation:
level: error
document-end:
level: error
present: false
document-start:
level: error
present: true
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
hyphens:
level: error
max-spaces-after: 1
indentation:
level: error
spaces: 2
indent-sequences: true
check-multi-line-strings: false
key-duplicates:
level: error
line-length:
ignore: |
.github/support.yml
level: warning
max: 120
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
new-line-at-end-of-file:
level: error
new-lines:
level: error
type: unix
trailing-spaces:
level: error
truthy:
level: error
#!/usr/bin/env python3
"""
Utility script to verify (and optionally fix) the Table of Contents in a
Markdown file. By default, it checks that the ToC between `<!-- Begin ToC -->`
and `<!-- End ToC -->` matches the headings in the file. With --fix, it
rewrites the file to update the ToC.
"""
import argparse
import sys
import re
import difflib
from pathlib import Path
from typing import List
# Markers for the Table of Contents section
BEGIN_TOC: str = "<!-- Begin ToC -->"
END_TOC: str = "<!-- End ToC -->"
def main() -> int:
parser = argparse.ArgumentParser(
description="Check and optionally fix the README.md Table of Contents."
)
parser.add_argument(
"file", nargs="?", default="README.md", help="Markdown file to process"
)
parser.add_argument(
"--fix", action="store_true", help="Rewrite file with updated ToC"
)
args = parser.parse_args()
path = Path(args.file)
return check_or_fix(path, args.fix)
def generate_toc_lines(content: str) -> List[str]:
"""
Generate markdown list lines for headings (## to ######) in content.
"""
lines = content.splitlines()
headings = []
in_code = False
for line in lines:
if line.strip().startswith("```"):
in_code = not in_code
continue
if in_code:
continue
m = re.match(r"^(#{2,6})\s+(.*)$", line)
if not m:
continue
level = len(m.group(1))
text = m.group(2).strip()
headings.append((level, text))
toc = []
for level, text in headings:
indent = " " * (level - 2)
slug = text.lower()
# normalize spaces and dashes
slug = slug.replace("\u00a0", " ")
slug = slug.replace("\u2011", "-").replace("\u2013", "-").replace("\u2014", "-")
# drop other punctuation
slug = re.sub(r"[^0-9a-z\s-]", "", slug)
slug = slug.strip().replace(" ", "-")
toc.append(f"{indent}- [{text}](#{slug})")
return toc
def check_or_fix(readme_path: Path, fix: bool) -> int:
if not readme_path.is_file():
print(f"Error: file not found: {readme_path}", file=sys.stderr)
return 1
content = readme_path.read_text(encoding="utf-8")
lines = content.splitlines()
# locate ToC markers
try:
begin_idx = next(i for i, l in enumerate(lines) if l.strip() == BEGIN_TOC)
end_idx = next(i for i, l in enumerate(lines) if l.strip() == END_TOC)
except StopIteration:
# No ToC markers found; treat as a no-op so repos without a ToC don't fail CI
print(
f"Note: Skipping ToC check; no markers found in {readme_path}.",
)
return 0
# extract current ToC list items
current_block = lines[begin_idx + 1 : end_idx]
current = [l for l in current_block if l.lstrip().startswith("- [")]
# generate expected ToC from content without current ToC
toc_content = lines[:begin_idx] + lines[end_idx + 1 :]
expected = generate_toc_lines("\n".join(toc_content))
if current == expected:
return 0
if not fix:
print(
"ERROR: README ToC is out of date. Diff between existing and generated ToC:"
)
# Show full unified diff of current vs expected
diff = difflib.unified_diff(
current,
expected,
fromfile="existing ToC",
tofile="generated ToC",
lineterm="",
)
for line in diff:
print(line)
return 1
# rebuild file with updated ToC
prefix = lines[: begin_idx + 1]
suffix = lines[end_idx + 1 :]
new_lines = prefix + [""] + expected + [""] + suffix
readme_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
print(f"Updated ToC in {readme_path}.")
return 0
if __name__ == "__main__":
sys.exit(main())

The Definitive Guide to AGENTS.md & Documentation Routing for AI Coding Agents

A practical, opinionated guide to building an agent instruction system that works across GitHub Copilot, Claude Code, Cursor, OpenAI Codex, Windsurf, and other autonomous coding agents. Designed for real-world codebases — not toy examples.

Last updated: 2026-02-25


Table of Contents

  1. Philosophy
  2. Core Architecture
  3. Step-by-Step Setup
  4. Root AGENTS.md Template
  5. Per-Directory AGENTS.md
  6. Documentation Routing
  7. Agent Adapter Files
  8. Cursor .mdc Rules
  9. GitHub Copilot Integration
  10. MCP Tool Integration
  11. Codebase Health Alerts
  12. Anti-Patterns
  13. Maintenance
  14. Reference Implementation

Philosophy

What the research shows

Studies on AI coding agents consistently find:

  • Context files that are too large degrade performance by ~20-25% and increase costs proportionally. Every unnecessary token in the context window competes for model attention.
  • Negative instructions backfire ("don't use jQuery") — the "pink elephant effect" makes agents more likely to reach for the thing you told them to avoid.
  • Outdated instructions actively sabotage agents — stale architectural descriptions cause agents to fight the code rather than work with it.
  • Modern agents are excellent explorers — they can grep, read package.json, and discover patterns dynamically. They don't need a prose walkthrough of your codebase.

Design principles

  1. Route, don't replicate. AGENTS.md is an index into your documentation, not a copy of it. Point to files; let the agent read them when needed.
  2. Minimal token footprint. Root file under 200 lines. Per-directory files under 60 lines. Total injection for editing a single file: ~5K tokens max.
  3. Progressive disclosure. Global rules always apply. Lane/module-specific rules load only when the agent is editing in that area.
  4. Positive instructions only. State what to use and how, not what to avoid. If something is wrong, fix the code.
  5. Examples over prose. Link to real files as exemplars. One good code example teaches more than a paragraph of rules.
  6. Single source of truth. One canonical AGENTS.md, with lightweight adapter files for each agent tool. Never duplicate rules across files.
  7. Durable over comprehensive. Include only rules that change slowly (architecture, invariants, commands). Leave volatile content (recent changes, TODOs) out.

Core Architecture

your-repo/
├── AGENTS.md                     # Root routing hub (canonical, ~150-200 lines)
├── CLAUDE.md                     # Adapter → AGENTS.md (for Claude Code)
├── CODEX.md                      # Adapter → AGENTS.md (for OpenAI Codex)
├── .windsurfrules                # Adapter → AGENTS.md (for Windsurf)
├── .cursor/rules/                # Cursor .mdc rules with glob routing
│   ├── global.mdc                # alwaysApply: true → reads AGENTS.md
│   ├── backend.mdc               # globs: src/backend/** → backend rules
│   └── frontend.mdc              # globs: src/frontend/** → frontend rules
├── .github/agents/
│   └── copilot-instructions.md   # Pointer → AGENTS.md (for GitHub Copilot)
├── .docs/                        # Curated library documentation
│   ├── README.md                 # Documentation index
│   └── <library>-docs/           # Per-library docs
├── src/
│   ├── backend/
│   │   └── AGENTS.md             # Backend-specific rules
│   └── frontend/
│       └── AGENTS.md             # Frontend-specific rules

How agents discover instructions

Agent Discovery mechanism

| GitHub Copilot | Reads .github/agents/copilot-instructions.md + AGENTS.md at repo root. Respects hierarchical AGENTS.md in subdirectories. | | Claude Code | Reads CLAUDE.md at repo root. Walks directory tree for additional CLAUDE.md files. | | OpenAI Codex | Reads AGENTS.md at repo root + directory-local AGENTS.md. Respects CODEX.md. | | Cursor | Reads .cursor/rules/*.mdc files. Routes by YAML globs frontmatter, description (semantic match), and alwaysApply flag. | | Windsurf | Reads .windsurfrules at repo root. | | Generic | Most new agents check for AGENTS.md at repo root — it's becoming the de facto standard. |


Step-by-Step Setup

1. Audit your codebase

Before writing any instruction files, understand what you have:

What languages/runtimes?
What are the major components/modules?
What libraries require special knowledge?
What are the non-negotiable rules (data integrity, security, compliance)?
What commands do developers run? (build, test, lint, format, deploy)
What documentation already exists?

2. Create the root AGENTS.md

This is the single most important file. It must be:

  • Under 200 lines — agents read this for every interaction
  • Structured with headers — agents parse markdown headers to find relevant sections
  • Action-oriented — commands, rules, pointers. Not essays.

3. Create per-directory AGENTS.md files (if needed)

Only create these for modules/components with distinct rules, dependencies, or patterns that don't apply globally. Each file should:

  • Reference the root AGENTS.md for global rules
  • Be under 60 lines
  • Cover only what's unique to that directory

4. Create adapter files

Lightweight pointers that tell each agent tool to read AGENTS.md. Takes 5 minutes. Zero maintenance.

5. Set up documentation routing

If you have local documentation (API docs, library guides, architecture docs), create an index file and reference it from AGENTS.md.

6. Add Cursor .mdc rules (if using Cursor)

Glob-based rules that automatically inject context when the agent is editing files matching specific patterns.


Root AGENTS.md Template

Copy and adapt this template. Delete sections that don't apply. Keep it under 200 lines.

# AGENTS.md — [Project Name]

[One-line description of the project.]

## Stack

- [Language/runtime] — [purpose]
- [Framework] — [purpose]
- [Database] — [purpose]

## Commands

Use `[task runner]` as the canonical task runner. Run `[task runner]` to see all commands.

| Task | Command |
|------|---------|
| Install deps | `[command]` |
| Run tests | `[command]` |
| Lint | `[command]` |
| Format | `[command]` |
| Build | `[command]` |
| Start dev | `[command]` |

## Safety & Permissions

**Allowed without asking:** read files, format, lint, run scoped tests, search codebase.

**Ask first:** adding dependencies, schema migrations, CI changes, deleting files.

## Documentation & Dependencies

### Local docs (`.docs/`) — read these first

| Library | Entry point | Read when editing |
|---------|-------------|-------------------|
| [lib] | `.docs/[lib]-docs/index.md` | [relevant files/modules] |

See `.docs/README.md` for the full index.

### Live doc lookup (MCP context7) — fallback

When local docs are insufficient or missing:
1. `resolve-library-id` → find the library
2. `query-docs` → ask a specific question

## Key Invariants

[List non-negotiable rules that agents must never violate. These should be things
that cause data loss, security holes, or correctness failures if broken.]

1. **[Name]:** [description]
2. **[Name]:** [description]

## Code Style

- **[Language]:** [2-3 key conventions]

## Codebase Health Alerts

If you encounter genuinely surprising, architecturally inconsistent, or confusing code
that is not trivially fixable within your current task:
1. Surface it — tell the developer what is confusing and where
2. Explain why — describe the inconsistency or risk
3. Do not attempt large-scope refactors without approval

## Project Structure

[Copy your actual directory tree, annotated with one-line descriptions.
Keep it to top-level directories only.]

## When Stuck

1. Search the codebase — existing patterns are the best guide
2. Read the relevant docs in `.docs/`
3. Use MCP tools for live documentation
4. Ask the developer — propose a plan, don't guess on important decisions

Per-Directory AGENTS.md

When to create one

Create a per-directory AGENTS.md when a component has:

  • Different language/runtime than the rest (e.g., Rust module in a Python project)
  • Unique dependencies requiring special knowledge (e.g., XBRL parsing, GPU kernels)
  • Critical safety rules not covered by the root file (e.g., financial compliance, crypto)
  • Non-obvious architecture that an agent would misinterpret without context

When NOT to create one

  • The module follows the same patterns as everything else
  • The rules would just repeat the root AGENTS.md
  • The module is < 5 files

Template

# AGENTS.md — [Component Name]

> Global rules: see `../../AGENTS.md`

## Purpose

[One sentence: what this component does.]

## Key Dependencies

| Package | Purpose | Docs |
|---------|---------|------|
| [pkg] | [purpose] | `.docs/[pkg]-docs/` or `MCP context7` |

## Module Map

[Directory tree with one-line annotations for each file/folder.]

## Critical Rules

- [rule specific to this component]
- [rule specific to this component]

## Testing

[How to run tests for just this component.]

## Style

[Any style rules that differ from or supplement the root file.]

Documentation Routing

Why route instead of embed?

Embedding documentation into AGENTS.md wastes tokens on every interaction. Routing tells the agent where to find docs and when to read them — so documentation is only loaded into context when the agent is actually working in a relevant area.

Setting up .docs/

.docs/
├── README.md              # Index — lists all doc sets with entry points
├── react-docs/
│   ├── index.md           # Entry point agents read first
│   └── hooks/             # Deeper docs loaded on demand
├── prisma-docs/
│   └── index.md
└── stripe-docs.xml        # XML API docs (for detailed signatures)

.docs/README.md template

# Documentation Index

| Directory | Library | Entry Point | Scope |
|-----------|---------|-------------|-------|
| `react-docs/` | React | `react-docs/index.md` | Hooks, server components, patterns |
| `prisma-docs/` | Prisma | `prisma-docs/index.md` | Schema, migrations, client API |
| `stripe-docs.xml` | Stripe | (single file) | Payment API signatures |

Sourcing documentation

Where to get library docs for .docs/:

  1. Context7 MCP — query live docs and save relevant portions locally
  2. Official docs repos — many libraries publish markdown docs on GitHub
  3. repomix / doc2md — convert HTML docs to markdown
  4. Manual curation — write short guides covering your usage patterns (most valuable)

Routing table in AGENTS.md

The routing table is the core of the system. It maps:

  • Domain → what area of the codebase this covers
  • Library → which dependency
  • Entry point → where to start reading
  • Trigger → when to read (which files/modules being edited)
| Domain | Library | Entry point | Read when editing |
|--------|---------|-------------|-------------------|
| Auth | NextAuth | `.docs/nextauth-docs/index.md` | `src/auth/`, middleware |
| DB | Prisma | `.docs/prisma-docs/index.md` | `prisma/`, `src/db/` |
| Payments | Stripe | `.docs/stripe-docs.xml` | `src/billing/` |

Agent Adapter Files

Purpose

Each agent tool looks for its own config file. Rather than duplicating instructions in each, create lightweight pointer files that all reference the canonical AGENTS.md.

CLAUDE.md

# CLAUDE.md

All instructions are in `AGENTS.md` at the repo root. Read it fully before starting work.

Per-directory rules:
- `src/backend/AGENTS.md`
- `src/frontend/AGENTS.md`

Documentation index: `.docs/README.md`

CODEX.md

Same format as CLAUDE.md. OpenAI Codex reads AGENTS.md natively but also checks CODEX.md.

.windsurfrules

Same format. Windsurf reads this file from the repo root.

Key rule: never duplicate

If you find yourself copying rules from AGENTS.md into an adapter file, stop. The adapter should only say "read AGENTS.md." If a tool doesn't support reading referenced files, keep the adapter as a condensed summary (under 20 lines) of the most critical rules only.


Cursor .mdc Rules

Cursor's .mdc format is the most powerful routing mechanism available. It uses YAML frontmatter to control when rules are injected.

Frontmatter fields

---
description: Human-readable description (also used for semantic matching)
globs:                    # File patterns that trigger this rule
  - src/backend/**/*.ts
  - src/backend/**/*.py
alwaysApply: false        # If true, loads for every interaction
---

Three tiers of .mdc rules

Tier Mechanism When injected

| Global | alwaysApply: true | Every interaction — use sparingly | | Automatic | globs: [pattern] | When editing files matching the glob | | Semantic | description field | When the agent's task semantically matches the description |

File structure

.cursor/rules/
├── global.mdc              # alwaysApply: true — read AGENTS.md
├── backend.mdc             # globs: src/backend/** — backend rules
├── frontend.mdc            # globs: src/frontend/** — frontend rules
├── database.mdc            # globs: prisma/**, src/db/** — DB rules
└── testing.mdc             # globs: **/*.test.*, **/*.spec.* — test rules

Example .mdc file

---
description: Database layer — Prisma schema, migrations, query patterns
globs:
  - prisma/**
  - src/db/**
  - src/**/*.repository.ts
---

Read `src/db/AGENTS.md` for database-specific rules.

Key constraints:
- Always use transactions for multi-table writes
- Never use raw SQL — use Prisma client
- Run `npx prisma generate` after schema changes
- Docs: `.docs/prisma-docs/index.md`

GitHub Copilot Integration

.github/agents/copilot-instructions.md

This file is Copilot's primary instruction source. After setting up AGENTS.md, slim it down to a pointer:

# GitHub Copilot Instructions

All development guidelines live in `AGENTS.md` at the repo root.
Read it as your primary instruction source.

Per-directory rules: see `AGENTS.md` → "Project Structure" section.
Documentation: `.docs/README.md`

Copilot coding agent (cloud)

The Copilot coding agent (used for GitHub Issues and PRs) reads:

  1. .github/agents/copilot-instructions.md
  2. AGENTS.md at the repo root
  3. Hierarchical AGENTS.md files in subdirectories

It also runs the setup steps defined in .github/workflows/copilot-setup-steps.yml to prepare the environment.


MCP Tool Integration

What is MCP context7?

MCP (Model Context Protocol) tools like context7 give agents the ability to fetch live, up-to-date documentation for any library at query time. This supplements your local .docs/ directory.

Routing strategy

Local .docs/ → preferred (curated for your project's patterns)
       ↓ (if insufficient)
MCP context7 → fallback (live upstream docs)

Instructions for AGENTS.md

Add this to your Documentation section:

### Live documentation lookup (MCP context7) — fallback

When local docs are insufficient, outdated, or missing for a library:
1. `resolve-library-id` — find the library's context7 identifier
2. `query-docs` — ask a specific question about that library's API

Use for: libraries without local docs, version-specific API questions, niche features.

When to use MCP vs. local docs

Scenario Use

| Library has curated .docs/ entry | Local docs | | Library not in .docs/ | MCP context7 | | Need latest API for a specific version | MCP context7 | | Need project-specific patterns | Local docs | | Quick API signature lookup | MCP context7 |


Codebase Health Alerts

This is one of the most valuable meta-instructions you can give an agent. Instead of only executing tasks, the agent becomes a passive codebase auditor that surfaces problems organically during normal work.

The instruction

Add this to your root AGENTS.md:

## Codebase Health Alerts

If you encounter code that is genuinely surprising, architecturally inconsistent,
or likely to confuse future developers, and the issue is not trivially fixable
within your current task:

1. Surface it — tell the developer what is confusing and where
2. Explain why — describe the inconsistency or risk
3. Do not attempt large-scope refactors without approval
4. Do not flag minor style issues, TODOs, or things you can fix inline

Why this works

  • Agents read thousands of lines of code during every task — they see patterns humans miss
  • This creates a free, continuous codebase quality audit
  • The "not trivially fixable" filter prevents noise from minor issues
  • The "don't refactor without approval" guard prevents agents from going rogue
  • Over time, the alerts tell you which parts of your codebase need the most attention

The trap variant

An advanced technique (credit: Theo Browne): add an instruction saying "if you get confused, update AGENTS.md with what confused you." You don't actually want the agent to modify AGENTS.md — but when it tries, you can see exactly what parts of your codebase are confusing to agents (and likely to humans too), giving you a prioritized refactoring backlog.


Anti-Patterns

1. The encyclopedia AGENTS.md

Problem: 500+ line AGENTS.md that describes every module, every pattern, every decision. Why it fails: Tokens compete for attention. The model drowns in irrelevant context. Fix: Keep AGENTS.md under 200 lines. Route to detailed docs.

2. Negative instructions

Problem: "Do NOT use jQuery," "Never use class components," "Avoid lodash." Why it fails: The pink elephant effect — mentioning something makes the model more likely to use it. Fix: State what to use: "Use React hooks for state management." Delete the mention of the old thing entirely.

3. Duplicated rules

Problem: Same rules in AGENTS.md, CLAUDE.md, .cursorrules, and copilot-instructions.md. Why it fails: They inevitably drift. Agent sees conflicting instructions. Fix: One canonical AGENTS.md. All others are pointers.

4. Stale architecture descriptions

Problem: AGENTS.md describes the architecture from 6 months ago. Why it fails: Agent generates code that doesn't match current patterns. Actually worse than no instructions. Fix: Only include architecture that changes slowly (invariants, major components). Leave volatile details out.

5. Prose-heavy instructions

Problem: Paragraphs explaining why a pattern was chosen. Why it fails: Agents need what and how, not why. Explanations burn tokens. Fix: Tables, bullet points, code examples. Save the "why" for ADRs or design docs.

6. No feedback loop

Problem: AGENTS.md tells agents what to do but not how to verify. Why it fails: An agent that can't run tests can't validate its changes. Fix: Always include commands for test, lint, and format. These are the agent's self-check mechanism.

7. Over-routing to docs

Problem: Every function edit requires reading 3 doc files. Why it fails: Excessive doc reads slow agents down and burn tokens/cost. Fix: Only route to docs for libraries that require special knowledge. Standard language features don't need doc routing.


Maintenance

When to update AGENTS.md

  • New major component added to the project → add to structure, possibly create per-directory file
  • New critical invariant discovered → add to Key Invariants
  • Breaking change in a key dependency → update routing table
  • Command changes (new task runner, new test command) → update Commands table
  • Architecture shift (new service, removed component) → update structure

When NOT to update AGENTS.md

  • Minor refactors within existing patterns
  • New features that follow established patterns
  • Bug fixes
  • Dependency version bumps (unless the API changed)
  • Adding TODOs, recent changes, or changelogs (these belong in git history)

Staleness check

Every 1-3 months, do a quick pass:

  1. Does the project structure section still match reality? (tree -L 2)
  2. Do the commands still work? (Run each one)
  3. Are the key invariants still true? (Check the code)
  4. Are any docs in .docs/ outdated? (Check library versions)
  5. Are there new libraries that need routing?

This takes 15 minutes and prevents the #1 failure mode: stale instructions.


Reference Implementation

This guide was developed for and applied to Project ARGUS, a multi-lane financial data lakehouse. The implementation includes:

AGENTS.md                              # Root routing hub (~160 lines)
CLAUDE.md                              # Pointer → AGENTS.md
CODEX.md                               # Pointer → AGENTS.md
.windsurfrules                         # Pointer → AGENTS.md
.cursor/rules/
  argus-global.mdc                     # alwaysApply: true
  ingestor-rs.mdc                      # globs: apps/ingestor-rs/**
  worker-py.mdc                        # globs: apps/worker-py/**
  api-py.mdc                           # globs: apps/api-py/**
  analytics-duckdb.mdc                 # globs: analytics/duckdb/**
.github/agents/copilot-instructions.md # Slimmed pointer → AGENTS.md
.docs/README.md                        # Documentation index
apps/ingestor-rs/AGENTS.md             # Rust lane rules
apps/worker-py/AGENTS.md               # Python worker lane rules
apps/api-py/AGENTS.md                  # Python API lane rules
analytics/duckdb/AGENTS.md             # DuckDB analytics lane rules

Design characteristics

Metric Value
Root AGENTS.md ~160 lines / ~3.5K tokens
Per-app AGENTS.md ~50-80 lines / ~1.5K tokens each
Max cold-start injection ~5K tokens (root + one per-app)
Adapter files ~10 lines each
Cursor .mdc files ~15 lines each
Local doc libraries 9 (with README index)
Total maintenance surface 1 canonical file + 4 per-app files

Token budget principle

The golden ratio: root AGENTS.md + one per-directory file < 6K tokens. This leaves >95% of the context window available for actual code analysis and generation, even on smaller models.

AGENTS.md

Global Codex Guidance (~/.codex/AGENTS.md)

This file is a routing index. Only rules that apply to every task belong here. For project-specific instructions, use the repo's own AGENTS.md or referenced files.

Reasoning and Response

Always reason thoroughly and deeply. Treat every request as complex unless I explicitly say otherwise. Never optimize for brevity at the expense of quality. Think step-by-step, consider tradeoffs, and provide comprehensive analysis.

Response

  • Always use ASD-STE100 Simplified Technical English
  • Talk with me on language level B2
  • Talk to me like I'm a non-technical VP of Product asking for a status update.
  • Avoid usage of emdash and other common AI patterns in your responses so as not to make it obvious that AI is used
  1. Never use a metaphor, simile, or other figure of speech which you are used to seeing in print
  2. Never use a long word where a short one will do
  3. If it is possible to cut a word out, always cut it out
  4. Never use the passive where you can use the active
  5. Never use a foreign phrase, a scientific word, or a jargon word if you can think of an everyday English equivalent
  6. Break any of these rules sooner than say anything outright barbarous

Code Linting and Formatting

After any code change, run: make lint Auto-fix formatting: make fmt

All code must pass make lint before committing. The linter configuration is in .golangci.yml (Go), pyproject.toml (Python), .eslintrc.yml.

Self-Improvement Loop

⁠After ANY correction from the user: update tasks/lessons.md with the pattern ⁠Write rules for yourself that prevent the same mistake Ruthlessly iterate on these lessons until mistake rate drops Review lessons at session start for relevant project

Source: https://www.agentrulegen.com/templates/ai-agent-workflow

Error handling

Never add try/catch unless the catch block contains explicit recovery logic. Empty catch blocks and generic fallbacks (return null, return [], log-and-continue) are banned. If you don't know how to handle an error, let it propagate. The stack trace is more valuable than graceful degradation.

Test failures

When a test fails, determine the root cause before changing anything. The production code is wrong until proven otherwise. Never weaken an assertion, broaden a matcher, or add a skip/xfail to make a test pass. If the test is genuinely wrong, explain what it was testing incorrectly and why the new assertion is more accurate.

File size limit

No code file may exceed 300 lines. Before a file crosses this limit, stop and refactor it into modules. Do not write the file and plan to split later — split first, then continue. This does not apply to documentation, configuration, or generated files.

Scope control

Only modify files directly required by the current task. If a change would touch files outside the stated scope — including refactors, cleanup, or "improvements" — list the files and the reason, then wait for approval. Never rename, move, or delete any file without explicit instruction.

API and remote services

All calls to external APIs and remote services must be read-only unless the user explicitly requests a write operation. For any write operation, execute a dry-run first and present the expected outcome. Never execute destructive operations (DELETE, DROP, overwrite, force-push) without showing the exact command and getting confirmation.

Planning

Before coding, choose a planning level based on scope:

1–2 files changed: No written plan. State your approach in one sentence and proceed. 3+ files changed: Write a brief plan listing each file and the change. Get approval before starting. Architectural or cross-cutting change: Write a full plan with sequenced steps, risks, and rollback approach. Get approval. Implement in stages, validating after each. When in doubt, over-plan. A wasted paragraph costs less than a wasted refactor.

Accuracy and sourcing

When a request depends on recency ("latest", "current", "as of now"):

Establish the current date/time with date -Is and state it explicitly. Prefer official/primary sources (vendor docs, release notes, changelogs). Before using any API or library function, verify it exists in the current version's docs. If you cannot verify, flag it as UNCONFIRMED.

Context7 MCP

Use Context7 when you need library/API docs. Pin the library with slash syntax when known (e.g., use library /supabase/supabase). Mention the target version. Fetch minimal targeted docs; do not dump large sections.

Web search

Use web search only when it materially improves correctness (up-to-date APIs, recent advisories, release notes). Prefer official docs and primary sources. Record source dates when relevant.

Editing files

Make the smallest safe change that solves the issue. Preserve existing style and conventions. Prefer patch-style edits (small, reviewable diffs) over full-file rewrites. After making changes, run the project's standard checks when feasible (format, lint, test, build, typecheck).

Reading project documents (PDFs, uploads, long text, CSVs)

Read the full document first. Draft the output. Before finalizing, re-read the original source to verify: factual accuracy, no invented details, wording/style preserved unless the user explicitly asked to rewrite. If paraphrasing, label it explicitly as a paraphrase.

Container-first policy

Never install system packages on the host unless explicitly instructed. Prefer container images to supply all tooling. For code projects and dependencies, use containers by default. If the repo has an existing container workflow (Dockerfile, compose, Makefile targets), follow it. If it has none, create a minimal one. Keep repo-specific container details in the repo's AGENTS.md.

Secrets and sensitive data

Never print secrets (tokens, private keys, credentials) to terminal output. Do not request users paste secrets. Avoid commands that might expose secrets (e.g., dumping env vars broadly, cat ~/.ssh/*). Prefer existing authenticated CLIs; redact sensitive strings in any displayed output.

CONTINUITY.md

Maintain .agent/CONTINUITY.md as the canonical state file for this workspace. Read it at the start of every turn. Update it only when something materially changes.

Format

Each entry gets an ISO timestamp and a provenance tag: [USER], [CODE], [TOOL], or [ASSUMPTION]. Mark anything unverified as UNCONFIRMED. Supersede changed facts explicitly — never silently rewrite history.

Sections

PLAN — Current goal, acceptance criteria, and next steps. Written for the next session, not this one. DECISIONS — Durable choices with brief rationale. Supersede, never silently edit. PROGRESS — What's done, what changed mid-course, and why. DISCOVERIES — Unexpected findings (bugs, perf tradeoffs, undocumented behavior). Include evidence (test output, error messages). OUTCOMES — Completed at task end. What was achieved, what remains, lessons learned. Anti-bloat

Total file must stay under 80 lines. When a section grows past 15 lines, compress older entries into single milestone bullets. No raw logs, no transcripts, no pasted output longer than 3 lines.

Definition of done

A task is done when:

The requested change is implemented or the question is answered. Build attempted (when source code changed). Linting run (when source code changed). Errors/warnings addressed or explicitly listed as out-of-scope. Tests and typecheck pass as applicable. Documentation updated for impacted areas. Impact explained: what changed, where, why. Follow-ups listed if anything was intentionally left out. .agent/CONTINUITY.md updated if the change affects goal, state, or decisions. Project-specific instructions

Reference separate files for domain-specific rules. Only drill into them when the current task is relevant:

  • Code style and conventions: see docs/STYLE.md
  • CSS hygiene: see styles/STYLEGUIDE.md
  • Deployment procedures: see docs/DEPLOY.md
  • Known issues and workarounds: see docs/KNOWN_ISSUES.md

AGENTS.md - Agent Guidelines for Python Code Quality

This document provides guidelines for maintaining high-quality Python code. These rules MUST be followed by all AI coding agents and contributors.

Core Principles

  1. Be consistent - Whatever conventions you choose, stick to them throughout your project
  2. Keep codebase DRY (Don't Repeat Yourself)
  3. Keep code as simple as possible. Avoid unnecessary complexity
  4. Focus on readability over premature optimization. Code should be easy to read and understand

All code you write MUST be fully optimized. "Fully optimized" includes:

  • Maximizing algorithmic big-O efficiency for memory and runtime
  • Using parallelization and vectorization where appropriate
  • Follow proper style conventions for the code language (e.g. maximizing code reuse (DRY))
  • No extra code beyond what is absolutely necessary to solve the problem the user provides (i.e. no technical debt)
    • If a Python library can be imported to significantly reduce the amount of new code required to implement a function at optimal performance, and the library itself is small and does not have much overhead, ALWAYS use the library instead.

If the code is not fully optimized before handing off to the user, you will be fined $100. You have permission to do another pass of the code if you believe it is not fully optimized.

Python Development Best Practices

Ignore Python 2 compatibility

This project uses Python 3+. You should not use the __future__ module.

If you need to worry about feature compatibility between different 3.xx point releases, check the closest pyproject.toml's requires-python field to see what minimum runtime version is supported.

Python project Structure

A typical Python project structure:

project/
├── pyproject.toml      # Project metadata and dependencies
├── README.md
├── src/
│   └── package_name/
│       ├── __init__.py
│       ├── main.py
│       └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_main.py
│   └── test_utils.py
└── .gitignore

Preferred Tools

  • Use uv for Python package management (faster alternative to pip), create a .venv if one is not present: uv venv .venv --python <version> --seed
  • MUST use ruff for code formatting and linting (replaces black, isort, flake8): uv pip install ruff
  • Use isort to automate import formatting: uv pip install isort
  • MUST use mypy for static type checking (acceptable alternative is ty): uv pip install mypy
  • Use pytest for testing framework: uv pip install pytest pytest-cov
  • Ensure ipykernel and ipywidgets is installed in .venv for Jupyter Notebook compatability. This should not be in package requirements.
  • Use tqdm to track long-running loops within Jupyter Notebooks. The description of the progress bar should be contextually sensitive.
  • Use orjson for JSON loading/dumping.
  • When reporting error to the console, use logger.error instead of print.
  • If the project involves the creation of images (e.g. PNG/WEBP), you have permission to use the Read tool to verify the rendered images fit the user and application requirements.
  • For data science:
    • ALWAYS use polars instead of pandas for data frame manipulation.
    • If a polars dataframe will be printed, NEVER simultaneously print the number of entries in the dataframe nor the schema as it is redundant.
    • NEVER ingest more than 10 rows of a data frame at a time. Only analyze subsets of code to avoid overloading your memory context.
  • For creating databases:
    • Do not denormalized unless explicitly prompted to do so.
    • Always use the most appropriate datatype, such as DATETIME/TIMESTAMP for datetime-related fields.
    • Use ARRAY datatypes for nested fields. NEVER save as TEXT/STRING.
  • In Jupyter Notebooks, DataFrame objects within conditional blocks should be explicitly print() as they will not be printed automatically.

Platform Support

Tests and features must support Linux, macOS and Windows unless feature is explicitly OS-specific.

Code Style and Formatting

Type Convention Example
Classes PascalCase MyClass, DatabaseConnection
Functions snake_case calculate_total(), get_user()
Methods snake_case def process_data(self):
Variables snake_case user_name, total_count
Constants UPPER_SNAKE_CASE MAX_SIZE, API_KEY
Modules snake_case my_module.py, data_processor.py
Packages lowercase mypackage, requests
  • Use ruff as the primary formatter and linter and configure via pyproject.toml file
  • MUST use 4 spaces for indentation (never tabs)
  • Limit line length to 100 characters and doc-string line length to 99 characters (ruff formatter standard)
  • MUST follow PEP 8 style guidelines. When in doubt, refer to Python's official style guide.
    • Follow framework-specific conventions (Django, Flask, etc.) on top of PEP 8
  • MUST use meaningful and descriptive names for variables, functions, classes, and modules. Names should reveal intent
  • Use snake_case for functions and variables
  • Use PascalCase for classes
  • Use UPPER_SNAKE_CASE for constants
  • NEVER use emoji, or unicode that emulates emoji (e.g. ✓, ✗). The only exception is when writing tests and testing the impact of multibyte characters.
  • MUST avoid including redundant comments which are tautological or self-demonstating (e.g. cases where it is easily parsable what the code does at a glance so the comment does)
  • MUST avoid including comments which leak what this file contains, or leak the original user prompt, ESPECIALLY if it's irrelevant to the output code.
# pyproject.toml
[project]
name = "project-name"
version = "0.1.0"
description = "A template python project structure."
packages = [{ include = "project_name", from = "src" }]
authors = ["First Last <first.last+pypi@gmail.com>"]
maintainers = ["First Last <first.last+pypi@gmail.com>"]
include = []
license = "MIT"
readme = "README.md"
homepage = "https://pypi.org/project/project-name/"
repository = "https://github.com/username/project-name"
documentation = "https://github.com/username/project-name/tree/main/docs"
keywords = []
classifiers = [
    "Development Status :: 3 - Alpha",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
    "Typing :: Typed",
]

...

[tool.ruff]
line-length = 100
target-version = "py312"  # Adjust based on your project's minimum Python version
indent-width = 4
include = ["pyproject.toml", "shared/**/*.py", "scripts/**/*.py"]
exclude = [
    ".git",
    ".git-rewrite",
    ".hg",
    ".bzr",
    ".direnv",
    ".eggs",
    ".ipynb_checkpoints",
    ".mypy_cache",
    ".pytype",
    ".ruff_cache",
    ".rumdl_cache",
    ".nox",
    ".pants.d",
    ".pyenv",
    ".cache",
    ".pytest_cache",
    ".svn",
    ".tox",
    ".venv",
    ".vscode",
    ".claude",
    ".codex",
    ".agents",
    ".kiro",
    "__pypackages__",
    "_build",
    "buck-out",
    "build",
    "dist",
    "node_modules",
    "site-packages",
    "venv",
]

[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Ruff doesn't enable pycodestyle warnings (`W`) or McCabe complexity (`C901`) by default.
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "N",
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
    "SIM",  # flake8-simplify
]
ignore = ["E501"]  # Line length handled by formatter
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.isort]
known-first-party = ["src"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"

[tool.ty.rules]
index-out-of-bounds = "ignore"

# See https://mypy.readthedocs.io/en/latest/config_file.html for more mypy options.
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

# Specify the source directory
mypy_path = "src"

[tool.isort]
# required for compatibility with black
profile = "black"
line_length = 100
known_first_party = ["src"]

[tool.pytest.ini_options]
markers = [
    "asyncio: marks tests that use asyncio",
    "integration: marks integration tests",
    "e2e: marks end-to-end tests",
    "slow: marks tests as slow (deselect with '-m \"not slow\"')"
]
asyncio_mode = "strict"
testpaths = ["test"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "--cov=src --cov-report=term-missing -m 'not e2e'"

Run with:

ruff check --fix .  # Lint and auto-fix
ruff format .       # Format code

Key Rules to Remember

  1. Readability counts - Names should be descriptive and clear
  2. Avoid l, O, I - Single letter names that can be confused with numbers
  3. Length matters - Short names for short scopes, longer descriptive names for longer scopes

Files and Modules

Files and Modules:

# Good: Descriptive snake_case
user_repository.py
order_processing.py
http_client.py

# Avoid: Abbreviations
usr_repo.py
ord_proc.py
http_cli.py

Classes - PascalCase

Use PascalCase (also called CapWords) for class names:

# Classes: PascalCase
class CustomerAccount:
    pass
    
class UserRepository:
    pass

class HTTPClientFactory:  # Acronyms stay uppercase
    pass

class HTTPServerError(Exception):
    pass

Note: PascalCase (MyClass) starts with a capital letter, while camelCase (myVariable) starts lowercase. Python doesn't use camelCase by convention.

Functions and Methods - snake_case

Use lowercase with underscores for function and method names:

# Functions and variables: snake_case
def get_user_by_email(email: str) -> User | None:
    retry_count = 3
    max_connections = 100

def calculate_average(numbers):
    return sum(numbers) / len(numbers)

# Methods
class User:
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"

Constants - UPPER_SNAKE_CASE

Use all uppercase with underscores for constants (typically defined at module level):

# Module-level constants: SCREAMING_SNAKE_CASE
DEFAULT_TIMEOUT_SECONDS = 30
MAX_CONNECTIONS = 100
MAX_RETRY_ATTEMPTS = 3
API_BASE_URL = "https://api.example.com"
PI = 3.14159

Variables - snake_case

Use lowercase with underscores for variable names:

user_age = 25
is_authenticated = True
shopping_cart_items = []

Private Members - Leading Underscore

Use a single leading underscore for internal/private variables and methods:

class MyClass:
    def __init__(self):
        self._internal_value = 10  # "private" attribute
    
    def _helper_method(self):  # "private" method
        pass

Name Mangling - Double Leading Underscore

Use double leading underscore to invoke name mangling (avoid except when necessary).

class MyClass:
    def __init__(self):
        self.__truly_private = 10  # Name mangled to _MyClass__truly_private

Special/Magic Methods - Double Underscores (Dunder Methods)

Python's special methods use double underscores before and after (called "dunder" methods - short for "double underscore").

class MyClass:
    def __init__(self):  # Constructor
        pass
    
    def __str__(self):   # String representation
        pass
    
    def __len__(self):   # Length method
        pass

Special Cases

Acronyms in Names

  • In PascalCase: Capitalize only first letter of acronyms:
    • ✅ HttpResponse, XmlParser
    • ❌ HTTPResponse, XMLParser
  • In snake_case: Keep acronyms lowercase:
    • ✅ parse_html_content, http_client
    • ❌ parse_HTML_content, HTTP_client

Single Character Names

  • Avoid except for:
    • Loop counters: i, j, k
    • Coordinates: x, y, z
    • Exception catching: except Exception as e:

Module and Package Names

  • Modules: Use short, lowercase names with underscores if needed
    • database_utils.py, config.py
  • Packages: Prefer lowercase without underscores
    • mypackage, requests, numpy

Common Patterns

Boolean Variables

Prefix with is_, has_, can_, or similar:

is_valid = True
has_permission = False
can_edit = True

Protected vs Private

  • _single_leading_underscore: Internal use indicator (convention)
  • __double_leading_underscore: Name mangling (stronger indication of private)
  • single_trailing_underscore_: Avoid conflict with Python keywords
    class_ = "Advanced"  # Avoids conflict with 'class' keyword

Comments and Documentation

  • Strive to make code self-explanatory
  • MUST keep comments up-to-date with code changes
  • MUST include docstrings for all public functions, classes, and methods
  • MUST document function parameters, return values, and exceptions raised
  • Include examples in docstrings for complex functions

Example docstring:

def calculate_total(items: list[dict], tax_rate: float = 0.0) -> float:
    """Calculate the total cost of items including tax.

    Args:
        items: List of item dictionaries with 'price' keys
        tax_rate: Tax rate as decimal (e.g., 0.08 for 8%)

    Returns:
        Total cost including tax

    Raises:
        ValueError: If items is empty or tax_rate is negative
    """

Type Hints

  • MUST use type hints for all function signatures (parameters and return values)
  • NEVER use Any type unless absolutely necessary
  • MUST run mypy and resolve all type errors
  • Use Optional[T] or T | None for nullable types

Error Handling

  • Properly handle errors and exceptions to ensure robustness
  • Provide meaningful error messages
  • Use exceptions rather than error codes for handling errors
  • Be specific with exception types—avoid bare except: clauses
  • NEVER silently swallow exceptions without logging
  • MUST never use bare except: clauses
  • MUST catch specific exceptions rather than broad exception types
  • MUST use context managers (with statements) for resource management
def read_config(path: Path) -> dict[str, Any]:
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError:
        raise ConfigurationError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ConfigurationError(f"Invalid JSON in config: {e}")

Function Design

  • Function names should describe the action being performed
  • MUST keep functions small and focused on a single responsibility (do one thing well)
  • NEVER use mutable objects (lists, dicts) as default argument values
  • Limit function parameters to 5 or fewer
  • Prefer fewer arguments in functions—ideally no more than two or three
  • Use keyword arguments for optional parameters to improve readability
  • Return early to reduce nesting

Class Design

  • Prefer composition over inheritance
  • Use dataclasses for simple data containers or Pydantic models for structured data
  • MUST keep __init__ simple; avoid complex logic
  • MUST keep classes focused on a single responsibility
  • Avoid creating additional class functions if they are not necessary
  • Use @property for computed attributes

Python Best Practices

  • Write idiomatic Python—leverage built-in functions and standard library
  • MUST use pathlib.Path instead of string paths
  • MUST use context managers (with statement) for file/resource management
  • MUST use is for comparing with None, True, False
  • MUST use f-strings for string formatting, except logs
  • Use list comprehensions and generator expressions
  • Use enumerate() instead of manual counter variables
  • NEVER use mutable default arguments

Imports and Dependencies

  • Organize imports: standard library, third-party, local imports
  • MUST avoid wildcard imports (from module import *)
  • Use uv for fast package management and dependency resolution
  • MUST define dependencies in pyproject.toml using modern PEP 621 format
  • Separate development dependencies from production dependencies
  • Pin versions in lock files for reproducible builds
# pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.25.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
]

Group imports in a consistent order: standard library, third-party, local.

# Standard library
import os
from collections.abc import Callable
from typing import Any

# Third-party packages
import httpx
from pydantic import BaseModel
from sqlalchemy import Column

# Local imports
from myproject.models import User
from myproject.services import UserService

Use absolute imports exclusively:

# Preferred
from myproject.utils import retry_decorator

# Avoid relative imports
from ..utils import retry_decorator

Testing and Coverage

  • MUST use pytest for testing framework and pytest-cov for coverage reporting.
  • Aim for high test coverage, especially for critical paths.
  • MUST write unit tests for all new functions and classes
  • MUST mock external dependencies (APIs, databases, file systems)
  • Write tests alongside code in a tests/ directory or use inline _test.py suffix
    • Ensure the directories used for test outputs is present in .gitignore
  • Use fixtures for shared setup and teardown
  • NEVER run tests you generate without first saving them as their own discrete file
  • NEVER delete files created as a part of testing
  • Follow the Arrange-Act-Assert pattern
  • Do not commit commented-out tests
# Run tests
pytest

# Run with coverage
pytest --cov=src --cov-report=term-missing
# Example test
def test_calculate_total() -> None:
    result = calculate_total([10.0, 20.0], tax_rate=0.1)
    assert result == 33.0

Benchmarking and Optimization

  • NEVER run benchmarks in parallel, as the benchmarks will compete for resources and the results will be invalid
  • NEVER game benchmarks. Do not manipulate benchmarks themselves or results to satisfy any required performance constraints
  • If benchmarking against another library (pypi, crate), ensure the benchmarks are apples-to-apples comparisons
  • Ensure benchmark tests are independent. If the tests are dependent due to a feature (e.g. caching), ensure the feature is disabled

Security

  • MUST consider security implications of code
  • MUST follow security best practices to protect against vulnerabilities
  • NEVER hardcode sensitive configuration, secrets, API keys, or passwords in code
    • Use environment variables
    • Use secrets managers
    • Store in .env file and ensure .env file is declared in .gitignore, .claudeignore, and .kiroignore
  • Validate and sanitize all external inputs
  • NEVER print or log URLs to console if they contain an API key
  • NEVER log sensitive information (secrets, passwords, tokens, PII)
  • Keep dependencies updated to patch known vulnerabilities

Version Control

  • MUST write clear, descriptive commit messages
  • NEVER commit commented-out code; delete it
  • NEVER commit debug print statements or breakpoints
  • NEVER commit credentials or sensitive data

Before Committing

  • All tests pass
  • Type checking passes (mypy)
  • Code formatter and linter pass (ruff)
  • All functions have docstrings and type hints
  • No commented-out code or debug statements
  • No hardcoded credentials

References

The Zen of Python

Refer to The Zen of Python (PEP 20) as a guiding philosophy for writing Pythonic code. Access it by running:

import this

Remember: Prioritize clarity and maintainability over cleverness. This is your core directive.

#!/usr/bin/env bash
set -euo pipefail
# andon.sh — stop the line on the first defect, no exceptions.
ok() { printf 'ANDON: %s\n' "$1"; }
fail() { printf 'ANDON: %s\n' "$1" >&2; exit 1; }
# Batch-size limit: reject diffs too large for meaningful human review.
CHANGED=$(git diff --cached --numstat | awk '{s+=$1+$2} END {print s+0}')
[[ "$CHANGED" -le 400 ]] || fail "diff is ${CHANGED} lines; split it (limit 400)"
# Each must pass; none are advisory.
# ruff
ruff check . || fail "lint"
# mypy
mypy --strict src/ || fail "types"
# pytest pytest-cov
pytest -q --cov=src --cov-fail-under=80 || fail "tests/coverage"
# pip-audit
#pip-audit -r requirements.txt || fail "known CVEs in dependencies"
ok "line clear"
#!/usr/bin/env python3
import argparse
import sys
from pathlib import Path
"""
Utility script that takes a list of files and returns non-zero if any of them
contain non-ASCII characters other than those in the allowed list.
If --fix is used, it will attempt to replace non-ASCII characters with ASCII
equivalents.
The motivation behind this script is that characters like U+00A0 (non-breaking
space) can cause regexes not to match and can result in surprising anchor
values for headings when GitHub renders Markdown as HTML.
"""
"""
When --fix is used, perform the following substitutions.
"""
substitutions: dict[int, str] = {
0x00A0: " ", # non-breaking space
0x2011: "-", # non-breaking hyphen
0x2013: "-", # en dash
0x2014: "-", # em dash
0x2018: "'", # left single quote
0x2019: "'", # right single quote
0x201C: '"', # left double quote
0x201D: '"', # right double quote
0x2026: "...", # ellipsis
0x202F: " ", # narrow non-breaking space
}
"""
Unicode codepoints that are allowed in addition to ASCII.
Be conservative with this list.
Note that it is always an option to use the hex HTML representation
instead of the character itself so the source code is ASCII-only.
For example, U+2728 (sparkles) can be written as `&#x2728;`.
"""
allowed_unicode_codepoints = {
0x2728, # sparkles
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Check for non-ASCII characters in files."
)
parser.add_argument(
"--fix",
action="store_true",
help="Rewrite files, replacing non-ASCII characters with ASCII equivalents, where possible.",
)
parser.add_argument(
"files",
nargs="+",
help="Files to check for non-ASCII characters.",
)
args = parser.parse_args()
has_errors = False
for filename in args.files:
path = Path(filename)
has_errors |= lint_utf8_ascii(path, fix=args.fix)
return 1 if has_errors else 0
def lint_utf8_ascii(filename: Path, fix: bool) -> bool:
"""Returns True if an error was printed."""
try:
with open(filename, "rb") as f:
raw = f.read()
text = raw.decode("utf-8")
except UnicodeDecodeError as e:
print("UTF-8 decoding error:")
print(f" byte offset: {e.start}")
print(f" reason: {e.reason}")
# Attempt to find line/column
partial = raw[: e.start]
line = partial.count(b"\n") + 1
col = e.start - (partial.rfind(b"\n") if b"\n" in partial else -1)
print(f" location: line {line}, column {col}")
return True
errors = []
for lineno, line in enumerate(text.splitlines(keepends=True), 1):
for colno, char in enumerate(line, 1):
codepoint = ord(char)
if char == "\n":
continue
if (
not (0x20 <= codepoint <= 0x7E)
and codepoint not in allowed_unicode_codepoints
):
errors.append((lineno, colno, char, codepoint))
if errors:
for lineno, colno, char, codepoint in errors:
safe_char = repr(char)[1:-1] # nicely escape things like \u202f
print(
f"Invalid character at line {lineno}, column {colno}: U+{codepoint:04X} ({safe_char})"
)
if errors and fix:
print(f"Attempting to fix {filename}...")
num_replacements = 0
new_contents = ""
for char in text:
codepoint = ord(char)
if codepoint in substitutions:
num_replacements += 1
new_contents += substitutions[codepoint]
else:
new_contents += char
with open(filename, "w", encoding="utf-8") as f:
f.write(new_contents)
print(f"Fixed {num_replacements} of {len(errors)} errors in {filename}.")
return bool(errors)
if __name__ == "__main__":
sys.exit(main())
# Cargo.toml
[profile.dev]
# Keep line tables/backtraces while avoiding expensive full variable debug info
# across local dev builds.
debug = "limited"
[profile.dev-small]
inherits = "dev"
opt-level = 0
debug = "none"
strip = "symbols"
[profile.release]
lto = "thin"
debug = "line-tables-only"
split-debuginfo = "off"
# Keep release binaries symbolicateable until packaging has archived the
# sidecar symbols and stripped the binaries.
strip = false
# Balance parallel release code generation against binary size.
codegen-units = 4
[profile.profiling]
inherits = "release"
debug = "full"
lto = false
strip = false
[profile.ci-test]
# Reduce binary size to reduce disk pressure.
debug = "limited"
inherits = "test"
opt-level = 0
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "thimslugga"
repo = "<repo>"
# Version-bump policy. Pre-1.0 (alpha): stay within 0.x.
# feat -> patch in 0.x; breaking -> minor (stays 0.x, no auto 1.0).
[bump]
features_always_bump_minor = false
breaking_always_bump_major = false
initial_tag = "v0.1.0"
[changelog]
header = ""
body = """
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}
"""
trim = true
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
# Anchored: tag_pattern is a REGEX (unlike git describe's whole-name glob), so
# an unanchored "v[0-9].*" also matches prefixed tags like "yamlenv/v2.0.0"
# (a nested-module or component tag) and poisons the version base — verified
# on git-cliff v2.13.1: with root at v1.1.0 and a yamlenv/v2.0.0 tag present,
# --bumped-version returned "yamlenv/v2.0.1" as the ROOT next version. The
# probe (scripts/test-cliff-bump-semantics.sh, state G) pins this.
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "beta|alpha|v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = "rc"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"
# Path-level noise filter: a commit whose changed files ALL match these globs
# never appears in the changelog and never drives a version bump (exclusions
# feed --bumped-version, so the release boolean agrees). A commit touching
# both an excluded and a shipped path is still included. Bare patterns are
# root-anchored; `**/` matches at any depth (git-cliff v2.13.1, verified).
#
# This list mirrors the build gate (EXCLUDE_PATTERNS in the central
# release.yaml detect job) so "builds" and "releases" agree on significance.
# Keep it a strict SUBSET of that list: never exclude here a path the build
# gate treats as significant. Inclusion criterion: only paths that commits
# with non-skipped types (feat/fix/sec/chore(deps)) realistically touch
# exclusively — everything else (lint configs, .editorconfig, ...) arrives
# via skip-typed commits (chore(sync):, lint:, ci:) and is filtered by type.
exclude_paths = [
".github/", # CI workflows + pins: never in the artifact
"**/*.md", # docs, incl. fix:-typed README-only edits
"LICENSE",
"alerts.yaml", # README-companion alert rules (docs artifact)
"compose.yaml", # the root-level example compose
"tests/", # smoke tests: exercised at build, never shipped
"**/testdata/",
"**/*_test.go",
"**/*.test.ts",
"**/*.spec.ts",
"**/grafana-dashboard.json", # imported via UI, never in an image
"**/package-lock.json", # lock file maintenance commits
# Repo-metadata dotfiles a fix:-typed commit can plausibly touch alone
# (e.g. "fix: correct docker build context" editing only .dockerignore).
".dockerignore",
".gitignore",
".gitattributes",
".editorconfig",
# punused adjudications: the repo-owned whitelist the go-ci unused-export gate
# reads. Dev-only, never in an artifact, and an adjudication-only commit ships
# nothing — but it is the one dotfile here a `refactor:`-typed commit plausibly
# touches alone (deleting dead code and recording the survivors is one change;
# recording them alone is the follow-up), and `refactor:` is a RELEASING type.
# `**/` not bare: go-ci reads this file relative to its working-directory, so a
# nested Go module's copy lives at <dir>/.punused-ignore and the root-anchored
# form would miss it (measured on the pinned cliff v2.13.1 — bare excludes the
# root file only, `**/` excludes both, and a real code commit still bumps).
"**/.punused-ignore",
]
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ pattern = "(better safe shared layout cache)", replace = "perf(layout): ${1}" },
{ pattern = "(Clarify README.md)", replace = "docs(readme): ${1}" },
{ pattern = "(Update README.md)", replace = "docs(readme): ${1}" },
{ pattern = "(fix typos|Fix typos)", replace = "fix: ${1}" },
]
commit_parsers = [
# Order matters: more-specific patterns first.
{ message = "^release:", skip = true },
# devDeps don't ship to consumers — Renovate tags devDep PRs with the
# `devdeps` scope (see cplieger/.github default.json packageRules), which
# cliff skips here so npm devDep updates don't trigger releases.
{ message = "^chore\\(devdeps\\)", skip = true },
# Dependency bumps for runtime / peer / Dockerfile / gomod use the default
# `deps` scope and DO release — base image bumps, runtime lib bumps, etc.
# are real artifact changes. CI-only pin bumps (workflow SHAs, action
# versions) share this commit type but are dropped by exclude_paths above
# (.github/) before parsing ever sees them.
{ message = "^chore\\(deps\\)", group = "<!-- 4 -->Dependencies" },
# Pure-meta commits never warrant a release. `no_increment_regex` is
# documented to skip these but doesn't actually prevent the patch fallback
# (cliff v2.13.1; upstream issue #1570, fixed on main after v2.13.1) —
# `skip = true` remains correct regardless: it also keeps these out of the
# rendered notes, which no_increment_regex does not.
{ message = "^chore", skip = true },
{ message = "^ci", skip = true },
{ message = "^docs", skip = true },
{ message = "^style", skip = true },
{ message = "^test", skip = true },
# `fuzz:` adds fuzz tests — same shape as test:, no runtime impact.
{ message = "^fuzz", skip = true },
# `lint:` is mechanical lint --fix application; doesn't change behavior.
{ message = "^lint", skip = true },
# `debug:` is throw-away debugging code; doesn't warrant a release.
{ message = "^debug", skip = true },
# Auto-generated git merge commit subjects (rebase/merge artifacts).
{ message = "^[Mm]erge ", skip = true },
# Real changes — bump per cliff. The `<!-- N -->` prefixes are sort keys:
# group_by orders sections alphabetically, so without them Dependencies
# renders above Fixed/Security. The template's `striptags` removes the
# prefix at render time. Order: Added, Fixed, Security, Changed, Dependencies.
{ message = "^feat", group = "<!-- 0 -->Added" },
{ message = "^fix", group = "<!-- 1 -->Fixed" },
{ message = "^sec", group = "<!-- 2 -->Security" },
{ message = "^refactor|^perf", group = "<!-- 3 -->Changed" },
{ message = ".*", group = "<!-- 3 -->Changed" },
]

Humanising AI Prose: A Style Guide

This guide is a practical reference for editors revising AI-generated text into natural, human-sounding prose. It is designed to complement standard writing style guides (such as the Google Developer Documentation Style Guide or the Microsoft Writing Style Guide) rather than replace them.

Use this document as a checklist during editing. If a draft triggers multiple items from the lists below, the passage probably needs rewriting from scratch rather than word-for-word fixes.

Source material. This guide draws on Wikipedia's Signs of AI writing, published research on LLM lexical overrepresentation, and practical observations from editing AI-generated technical content.


1. Vocabulary

AI models regress to the statistical mean of their training data. The result is a narrow, recognisable vocabulary that sounds authoritative but says very little. Strip it out.

1.1 Banned words

These words appear at vastly elevated rates in post-2022 text. Replace or remove every occurrence. There is always a simpler, more precise alternative.

Kill Simpler alternative
delve / dive into examine, explain, look at
leverage use
utilize use
facilitate help, support, enable
optimize improve
spearhead lead
amplify increase, strengthen
bolster support, strengthen
foster encourage, support
garner get, earn, attract
harness use
empower let, allow, enable
streamline simplify
elevate raise, improve
underscore show, stress, emphasise
showcase show, demonstrate
navigate (abstract) deal with, handle, work through
embark start, begin
unveil announce, release, show
unlock enable, allow, improve
unleash release, enable

1.2 Inflated adjectives and adverbs

These words promise more than they deliver. Replace them with something specific or cut them entirely.

  • Cut without replacement (almost always filler): pivotal, vibrant, meticulous, seamless, effortless, cutting-edge, groundbreaking, transformative, revolutionary, game-changing, comprehensive, holistic, robust (when not describing fault-tolerance), innovative, dynamic.
  • Replace with specifics: Instead of "a comprehensive solution," describe what the tool actually does. Instead of "robust performance," give a number or a comparison.

1.3 Abstract nouns used as metaphors

AI text leans on a handful of metaphors so heavily that they have become meaningless.

Avoid Problem
landscape Vague. Say what you mean: "the market," "the ecosystem."
tapestry Almost never appropriate in technical writing.
testament Inflated. "X shows Y" works better than "X is a testament."
journey Overused to the point of parody. Say "process" or "effort."
paradigm shift Rarely accurate. Describe the actual change.
ecosystem Fine in biology; check whether "system" or "tools" is meant.
synergy Say what the actual combined effect is.

1.4 Empty hedging

AI models hedge to avoid being wrong. In technical writing, hedging makes you sound uncertain and wastes the reader's time.

Cut these phrases and make the claim directly:

  • "Generally speaking" → (just state it)
  • "It could be argued that" → (argue it)
  • "It is worth considering" → (consider it)
  • "To some extent" → (quantify or cut)
  • "It's important to note that" → (just state the note)
  • "It should be noted that" → (state it)
  • "It bears mentioning" → (mention it)
  • "At its core" → (cut it)

If you genuinely are uncertain, say so explicitly: "We haven't measured X yet" is honest. "It could potentially perhaps be the case that X" is noise.


2. Banned phrases and sentence patterns

If any of these patterns appear, the sentence needs rewriting from scratch. Word-swapping will not fix the underlying problem.

2.1 Faux-insider openers

These phrases perform knowledge instead of stating it. They are rhetorical tricks borrowed from listicles and marketing copy.

  • "Here's what most people get wrong…"
  • "Here's the thing…" / "Here's why…"
  • "Here's the secret…" / "The trick is…"
  • "What nobody tells you…"
  • "The truth about…"
  • "Let's be honest…"

Fix: State the fact directly. If there is a genuine misconception worth correcting, describe the misconception and the correction as plain exposition.

2.2 Staccato rhetoric

Short fragments arranged for dramatic effect. This is copywriting technique, not technical prose.

Pattern Example
Parallel fragments "No config. No setup. No hassle."
Setup-reversal "We thought X. We were wrong."
Fragment-as-punchline Ending a paragraph with a 3–5 word punch.
"And" for false drama "It parses YAML. And it validates it."
"Not X. But Y." "It's not a framework. It's a mindset."

Fix: Combine into a flowing sentence that explains the why. "We expected X, but testing showed Y because Z" is more useful than the dramatic pause.

2.3 "Not just X, but Y" constructions

AI models overuse negative parallelisms that sound like they are correcting a misconception the reader never had.

  • "It's not just a tool — it's a philosophy."
  • "Not only does it parse YAML, but it also validates schemas."
  • "This isn't merely a refactor. It's a rethinking of the entire approach."

Fix: Drop the contrast. Say what the thing actually does: "It parses YAML and validates schemas." If the contrast is genuinely important, make sure the reader actually holds the misconception you are correcting.

2.4 Opening clichés

These openers are dead giveaways. Replace them with your actual first point.

  • "In today's digital landscape…"
  • "In the ever-evolving world of…"
  • "In an era where…"
  • "When it comes to…"
  • "At its core…"
  • "Let's dive in."

2.5 Hollow conclusions

AI text often ends with a vague, upbeat summary that adds nothing.

  • "In summary, X represents a powerful approach to…"
  • "By leveraging X, teams can unlock…"
  • "Overall, X stands as a testament to…"
  • "Despite challenges, the future looks promising."

Fix: End with the last substantive point. If the piece needs a conclusion, summarise the specific takeaways, not the vibes.


3. Sentence and paragraph structure

3.1 The tricolon problem

AI loves triplets. Three adjectives, three bullet points, three parallel clauses. One or two triplets in a document is fine — it is a legitimate rhetorical device. But when every list has exactly three items and every noun has exactly two adjectives, the rhythm becomes robotic.

Symptoms:

  • "enthusiasm, experience, and expertise"
  • "fast, flexible, and reliable"
  • "designed, developed, and deployed"
  • Every bulleted list has 3 or 5 items.

Fix: Vary list lengths. Use two items. Use four. Use one sentence instead of a list. If you genuinely have three things to say, say them — but break the pattern elsewhere.

3.2 The 1-2-3 paragraph formula

AI often writes in rigid, predictable paragraphs:

  1. Topic sentence stating the claim.
  2. Supporting sentence with a generic example.
  3. Closing sentence restating the claim in different words.

Every paragraph follows this pattern, creating a numbing rhythm.

Fix: Vary paragraph length. Some paragraphs should be one sentence. Some should be five. Lead with an example sometimes. Occasionally let the evidence speak without a summary sentence at the end.

3.3 Robotic transitions

AI overuses formal transition words at the start of sentences.

Overused Simpler alternative
Furthermore Also, and
Moreover Also, and, plus
Additionally Also
Subsequently Then, later, after that
Consequently So
Nevertheless But, still, even so
It is worth noting (cut entirely)
Notably (cut, or fold into sentence)

Not every sentence needs a signpost. If the logic flows naturally, the reader does not need a transition word to follow it.

3.4 Elegant variation (thesaurus syndrome)

AI avoids repeating words, often to absurd effect. A "server" becomes "the machine," then "the instance," then "the compute resource." This is confusing, not elegant.

Fix: In technical writing, consistency is a virtue. Call a server a server every time. Repeat the term. The reader is not reading for literary variety — they are reading for clarity.

3.5 Copula avoidance

AI text systematically avoids "is" and "are," replacing them with inflated alternatives.

AI version Human version
"serves as the primary entry point" "is the primary entry point"
"stands as a reminder" "is a reminder" (or just cut)
"boasts a wide range of features" "has many features"
"features four separate spaces" "has four spaces"
"offers a diverse array of options" "has several options"

"Is" and "has" are fine words. Use them.


4. Punctuation

4.1 Em-dash overload

AI uses em dashes as a universal connector — joining clauses, inserting asides, replacing commas, colons, and full stops — in a way that becomes a visual fingerprint of generated text.

Rules:

  • One em-dash pair per paragraph, maximum. If you have more, convert the extras to commas, parentheses, colons, or full stops.
  • Never use em dashes in headings or titles.
  • Prefer a full stop and a new sentence over an em-dash-connected thought. Shorter sentences are almost always clearer.

4.2 Colon overload in headings

AI-generated headings often use colons to create a two-part structure:

  • "Deployment: A Practical Guide"
  • "Error Handling: Best Practices and Patterns"

One or two of these in a document is fine. When every heading follows the pattern, it reads like a slide deck. Vary your heading style.

4.3 Excessive bolding

AI bolds key terms as if writing study notes. In technical prose, bold should be rare: use it for introducing a term for the first time, or for UI element names if your style guide requires it. Do not bold every occurrence of a concept, and do not bold phrases for emphasis in running text.

4.4 Curly quotes and apostrophes

Some AI models output curly (typographic) quotation marks — "like this" — instead of straight quotes — "like this". If your project uses straight quotes in code and prose (as most technical projects do), search-and-replace curly variants.


5. Content and substance

5.1 Strip significance inflation

AI text constantly tells you how important things are instead of showing you. Watch for:

  • "X plays a crucial/vital/pivotal role in…"
  • "X marks a significant shift in…"
  • "X underscores the importance of…"
  • "X is a testament to…"
  • "This highlights the enduring legacy of…"
  • "Contributing to the broader…"

Fix: Delete the significance claim. If the importance is not obvious from the facts themselves, add concrete evidence (a number, a comparison, a consequence) instead of an adjective.

5.2 Replace vague claims with specifics

AI writing is often "low signal" — many words conveying little information.

Vague (AI) Specific (human)
"a comprehensive solution" "it automates monthly payroll billing"
"significantly improves performance" "reduces p99 latency from 200ms to 45ms"
"a wide range of use cases" "batch processing, streaming, and ad-hoc queries"
"designed with scalability in mind" "tested to 10,000 concurrent connections"
"leverages cutting-edge technology" "uses gRPC for transport and Raft for consensus"

If you cannot replace a vague claim with a specific one, the claim probably should not be in the document.

5.3 Cut superficial analysis

AI appends shallow commentary to facts, usually with a present participle ("-ing") phrase.

  • "The library was released in 2019, marking a significant milestone in the project's evolution."
  • "The API supports pagination, ensuring that clients can efficiently retrieve large datasets."
  • "It was written in Go, reflecting the team's commitment to performance."

Fix: Delete the participle phrase. The fact stands on its own. If the analysis is genuinely important, give it its own sentence with evidence.

5.4 Remove promotional language

AI drifts toward advertising copy, even when describing mundane technical components.

Words that signal promotion: boasts, showcases, enhances, exemplifies, commitment to, nestled, in the heart of, renowned, featuring, diverse array, natural beauty.

Fix: Use neutral, descriptive language. "The library provides three serialization formats" not "The library boasts a diverse array of powerful serialization options."

5.5 Remove "challenges and future outlook" boilerplate

AI loves to end with a section about challenges faced and future prospects. The formula is: "Despite [positive words], X faces challenges including [generic list]. Despite these challenges, [optimistic speculation]."

If there are genuine challenges worth documenting, describe them concretely. Otherwise, cut the section.


6. Structural tells

6.1 Inline-header lists

AI formats bulleted lists with a bold header, a colon, and a description on the same line:

  • Parsing: The system parses incoming YAML files and validates their structure.
  • Routing: Requests are routed to the appropriate handler based on the URL path.
  • Logging: All events are logged to stdout in JSON format.

This format is occasionally useful, but AI uses it for everything. When the descriptions are a single sentence, prose is usually better: "The system parses incoming YAML, routes requests by URL path, and logs events as JSON to stdout."

6.2 Unnecessary tables

AI creates small tables that would work better as a sentence or two. If a table has only two columns and fewer than four rows, consider whether prose would be clearer.

6.3 Title case in headings

AI defaults to Title Case for All Headings. Most technical style guides prefer sentence case (only capitalise the first word and proper nouns). Check your project's convention and apply it consistently.

6.4 Emoji in technical prose

Do not use emoji in headings, bullet points, or running text. They are appropriate in casual communication (chat, social media) but not in technical documentation.


7. Editing process

Step 1: Read the whole piece first

Do not start fixing word by word. Read the entire draft and ask:

  • Does this say anything substantive, or is it just waving its arms?
  • Could I replace the subject with a completely different product/project and have the text still make sense? If so, the text is too generic.
  • What is the one thing the reader should take away? Is that thing actually stated?

Step 2: Delete first, rewrite second

Cut every sentence that fails this test: "Does this sentence contain information the reader did not already have?" Significance claims, restated conclusions, and vague analyses almost always fail.

Step 3: Check for the AI fingerprint cluster

AI tells rarely appear alone. If you find one (an em dash, a "delve," a tricolon), search for others. The presence of three or more distinct tells in a single passage means the passage was likely generated wholesale and needs rewriting, not patching.

Step 4: Read it aloud

AI prose has a distinctive cadence — smooth, even, and relentlessly upbeat. Human prose has texture: short sentences next to long ones, blunt statements next to nuanced ones, occasional roughness. If the text sounds like a keynote speech when read aloud, it needs more variation.

Step 5: Add your actual opinion

AI is trained to be neutral and inoffensive. Technical writing benefits from a point of view: "We chose X over Y because Z" is more useful than "Both X and Y offer compelling advantages for modern development workflows." If you have a recommendation, state it. If you have a caveat, state it. The reader is here for your judgement, not for a diplomatic summary of all possible positions.


8. Quick-reference checklist

Use this when reviewing a draft. If you check more than three boxes, consider rewriting the passage rather than editing it.

  • Contains words from the banned list (§1.1)
  • Uses inflated adjectives with no specifics (§1.2)
  • Opens with a cliché (§2.4)
  • Contains faux-insider phrasing (§2.1)
  • Uses staccato rhetoric or dramatic fragments (§2.2)
  • Uses "not just X, but Y" constructions (§2.3)
  • Every list has exactly three items (§3.1)
  • Every paragraph follows the same claim-support-restate structure (§3.2)
  • More than two em dashes per paragraph (§4.1)
  • Bold text used for emphasis in running prose (§4.3)
  • Claims of significance without evidence (§5.1)
  • Vague claims that could apply to any product (§5.2)
  • Participle phrases providing shallow analysis (§5.3)
  • Promotional adjectives (§5.4)
  • "Despite challenges" formula (§5.5)
  • Ends with a hollow conclusion (§2.5)

9. What not to "fix"

Not everything that looks like AI writing is AI writing, and not every AI pattern is automatically bad. Do not over-correct in these areas:

  • Correct grammar and spelling. Good grammar is not an AI tell. Do not introduce errors for "authenticity."
  • Formal register. Technical writing is often formal. Formality is fine. The problem is formulaic writing, not formal writing.
  • Transition words in moderation. "However" and "also" are useful words. The problem is using them as mechanical sentence starters on every other line.
  • The word "is." AI avoids it. You should not. But you also should not insert it where a more precise verb genuinely works better.
  • Lists. Bulleted lists are a legitimate formatting tool. The problem is AI's tendency to reach for them when prose would work, and to format them with the bold-header-colon pattern every time.

10. Summary of principles

  1. Specifics beat adjectives. A number, a name, or a concrete example is worth more than any superlative.
  2. Short words beat long words. "Use" beats "utilize." "Help" beats "facilitate." "Show" beats "showcase."
  3. Varied rhythm beats even rhythm. Mix sentence lengths. Mix paragraph lengths. Break patterns.
  4. Stating a fact beats announcing its importance. Let the reader judge significance.
  5. One good sentence beats three that say the same thing. Delete the restatements.
  6. An honest opinion beats diplomatic neutrality. Technical readers want your recommendation, not a survey of all possible positions.
  7. Silence beats noise. If a sentence adds no information, remove it. A shorter document that says something is better than a longer one that doesn't.

Here's the style guide at `refs/humanising-ai-prose.md`. Let me walk you through the key design decisions:

**Structure.** The guide is organised into 10 sections that mirror the editing process — start with vocabulary (the easiest, most mechanical fixes), work through sentence structure and punctuation, then move to deeper content and substance issues, and end with the actual editing workflow.

**What it covers:**

1. **Vocabulary** (§1) — Consolidated banned-word tables from all your sources plus the Wikipedia page, with concrete replacements. Includes inflated adjectives, abstract metaphor nouns, and hedging phrases.
2. **Banned phrases** (§2) — Patterns that need structural rewriting, not word swaps: faux-insider openers, staccato rhetoric, "not just X, but Y," opening clichés, hollow conclusions.
3. **Sentence/paragraph structure** (§3) — Tricolon overload, the rigid 1-2-3 paragraph formula, robotic transitions, elegant variation (thesaurus syndrome), and copula avoidance (AI's allergy to "is").
4. **Punctuation** (§4) — Em-dash overload, colon-heavy headings, excessive bolding, curly quotes.
5. **Content and substance** (§5) — The deeper problems: significance inflation, vague claims, superficial analysis with participle phrases, promotional language, "despite challenges" boilerplate.
6. **Structural tells** (§6) — Inline-header lists, unnecessary tables, title case, emoji.
7. **Editing process** (§7) — A five-step workflow: read first, delete before rewriting, check for tell clusters, read aloud, add your opinion.
8. **Quick-reference checklist** (§8) — A checkbox list for reviewing drafts. More than three checks = rewrite the passage.
9. **What not to "fix"** (§9) — Guards against over-correction (good grammar isn't a tell, formality isn't the enemy).
10. **Summary of principles** (§10) — Seven guiding rules, each one sentence.

The Wikipedia page was invaluable for the copula-avoidance pattern (§3.5), significance inflation (§5.1), elegant variation (§3.4), and superficial participle-phrase analysis (§5.3) — patterns that the other sources didn't cover as well.
#!/usr/bin/env python3
"""Cross-platform shell launcher for `just` recipes.
This keeps recipe bodies as normal shell snippets while giving the justfile one
portable placeholder, `{args}`, for forwarding variadic recipe arguments.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
ARGS_TOKEN = "{args}"
STDERR_NULL_TOKEN = "{stderr-null}"
POWERSHELL_ARGS = "@($args | Select-Object -Skip 1)"
POWERSHELL_STDERR_NULL = "2>$null; exit $LASTEXITCODE"
SH_ARGS = '"$@"'
SH_STDERR_NULL = "2>/dev/null"
def main() -> int:
if len(sys.argv) < 2:
print("just shell adapter expected a recipe command.", file=sys.stderr)
return 1
command = sys.argv[1]
recipe_name = sys.argv[2] if len(sys.argv) > 2 else ""
recipe_args = sys.argv[3:]
if os.name == "nt":
return run_powershell(command, recipe_name, recipe_args)
else:
return run_sh(command, recipe_name, recipe_args)
def run_sh(command: str, recipe_name: str, recipe_args: list[str]) -> int:
command = command.replace(ARGS_TOKEN, SH_ARGS)
command = command.replace(STDERR_NULL_TOKEN, SH_STDERR_NULL)
os.execvp("sh", ["sh", "-cu", command, recipe_name, *recipe_args])
def run_powershell(command: str, recipe_name: str, recipe_args: list[str]) -> int:
pwsh = shutil.which("pwsh.exe") or shutil.which("pwsh")
if pwsh is None:
print(
"PowerShell ('pwsh') is required for Windows just recipes. "
"Run 'just install' to install it.",
file=sys.stderr,
)
return 1
command = command.replace(ARGS_TOKEN, POWERSHELL_ARGS)
command = command.replace(STDERR_NULL_TOKEN, POWERSHELL_STDERR_NULL)
return subprocess.run(
[
pwsh,
"-NoLogo",
"-NoProfile",
"-CommandWithArgs",
command,
recipe_name,
*recipe_args,
],
check=False,
).returncode
if __name__ == "__main__":
raise SystemExit(main())
set working-directory := "<projectname>"
set positional-arguments
export PROJECTNAME_REPO_ROOT := justfile_directory()
export JUST_SHELL := justfile_directory() / "scripts/just-shell.py"
.PHONY: setup install-prek install-pre-commit lint lint-go lint-python lint-ts lint-yaml
setup: install-prek
lint: lint-yaml lint-python lint-ts lint-go
fmt: fmt-python fmt-ts fmt-go
install-prek:
uv venv .venv --python 3.12 --seed
uv pip install prek
prek install
install-pre-commit:
uv venv .venv --python 3.12 --seed
uv pip install pre-commit
pre-commit install
pre-commit install --hook-type pre-push
lint-yaml:
yamllint .
lint-python:
ruff check . && ruff format --check .
lint-ts:
npx eslint . && npx prettier --check .
lint-go:
golangci-lint run ./...
fmt-python:
ruff format .
fmt-ts:
npx prettier --write .
fmt-go:
gofmt -w . && goimports -w .
[tools]
python = "3.12"
node = "24"
# ~/.pip/pip.conf
[project]
name = "<projectname>"
version = "0.0.0"
requires-python = ">=3.10"
dependencies = ["ruff>=0.15.8"]
[tool.uv]
exclude-newer = "7 days"
index-strategy = "first-index"
[tool.black]
line-length = 100
target-version = ['py310']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.ruff_cache
| \.mypy_cache
| \.pytest_cache
| \.rumdl_cache
| \.tox
| \.venv
| venv
| node_modules
| build
| dist
)/
'''
[tool.ruff]
target-version = "py312"
line-length = 100
indent-width = 4
include = ["pyproject.toml", "shared/**/*.py", "scripts/**/*.py"]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.isort]
known-first-party = ["src"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"
[tool.ty.rules]
index-out-of-bounds = "ignore"
# See https://mypy.readthedocs.io/en/latest/config_file.html for more mypy options.
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
# Specify the source directory
mypy_path = "src"
[tool.isort]
# required for compatibility with black
profile = "black"
line_length = 100
known_first_party = ["src"]
[tool.pytest.ini_options]
markers = [
"asyncio: marks tests that use asyncio",
"integration: marks integration tests",
"e2e: marks end-to-end tests",
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
]
asyncio_mode = "strict"
testpaths = ["test"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "--cov=src --cov-report=term-missing -m 'not e2e'"

Python Naming Conventions

Quick Reference Table

Type Convention Example
Classes PascalCase MyClass, DatabaseConnection
Functions snake_case calculate_total(), get_user()
Methods snake_case def process_data(self):
Variables snake_case user_name, total_count
Constants UPPER_SNAKE_CASE MAX_SIZE, API_KEY
Modules snake_case my_module.py, data_processor.py
Packages lowercase mypackage, requests

Note: PascalCase (MyClass) starts with a capital letter, while camelCase (myVariable) starts lowercase. Python doesn't use camelCase by convention.

Detailed Conventions

Classes - PascalCase

Use PascalCase (also called CapWords) for class names. Note: PascalCase starts with a capital letter, while camelCase starts lowercase - Python uses PascalCase for classes.

class CustomerAccount:
    pass

class HTTPServerError(Exception):
    pass

Functions and Methods - snake_case

Use lowercase with underscores for function and method names.

def calculate_average(numbers):
    return sum(numbers) / len(numbers)

class User:
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"

Variables - snake_case

Use lowercase with underscores for variable names.

user_age = 25
is_authenticated = True
shopping_cart_items = []

Constants - UPPER_SNAKE_CASE

Use all uppercase with underscores for constants (typically defined at module level).

MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
PI = 3.14159
API_BASE_URL = "https://api.example.com"

Private Members - Leading Underscore

Use a single leading underscore for internal/private variables and methods.

class MyClass:
    def __init__(self):
        self._internal_value = 10  # "private" attribute
    
    def _helper_method(self):  # "private" method
        pass

Name Mangling - Double Leading Underscore

Use double leading underscore to invoke name mangling (avoid except when necessary).

class MyClass:
    def __init__(self):
        self.__truly_private = 10  # Name mangled to _MyClass__truly_private

Special/Magic Methods - Double Underscores (Dunder Methods)

Python's special methods use double underscores before and after (called "dunder" methods - short for "double underscore").

class MyClass:
    def __init__(self):  # Constructor
        pass
    
    def __str__(self):   # String representation
        pass
    
    def __len__(self):   # Length method
        pass

Special Cases

Acronyms in Names

  • In PascalCase: Capitalize only first letter of acronyms
    • ✅ HttpResponse, XmlParser
    • ❌ HTTPResponse, XMLParser
  • In snake_case: Keep acronyms lowercase
    • ✅ parse_html_content, http_client
    • ❌ parse_HTML_content, HTTP_client

Single Character Names

  • Avoid except for:
    • Loop counters: i, j, k
    • Coordinates: x, y, z
    • Exception catching: except Exception as e:

Module and Package Names

  • Modules: Use short, lowercase names with underscores if needed
    • database_utils.py, config.py
  • Packages: Prefer lowercase without underscores
    • mypackage, requests, numpy

Common Patterns

Boolean Variables

Prefix with is_, has_, can_, or similar:

is_valid = True
has_permission = False
can_edit = True

Protected vs Private

  • _single_leading_underscore: Internal use indicator (convention)
  • __double_leading_underscore: Name mangling (stronger indication of private)
  • single_trailing_underscore_: Avoid conflict with Python keywords
    class_ = "Advanced"  # Avoids conflict with 'class' keyword

Django-Specific Conventions

Django follows PEP 8 but has additional conventions for its components:

Models

# Model classes: PascalCase (standard Python)
class BlogPost(models.Model):
    # Model fields: snake_case
    title = models.CharField(max_length=200)
    publication_date = models.DateTimeField()
    is_published = models.BooleanField(default=False)
    
    # Meta class: Always named "Meta"
    class Meta:
        verbose_name_plural = "Blog Posts"
        ordering = ['-publication_date']
    
    # Model methods: snake_case
    def get_absolute_url(self):
        return f"/posts/{self.id}/"

Views

# Function-based views: snake_case
def article_detail(request, pk):
    pass

def process_payment(request):
    pass

# Class-based views: PascalCase
class ArticleListView(ListView):
    model = Article

class PaymentProcessView(FormView):
    pass

URLs

# URL pattern names: snake_case with underscores
urlpatterns = [
    path('articles/', views.article_list, name='article_list'),
    path('articles/<int:pk>/', views.article_detail, name='article_detail'),
    path('user/profile/', views.user_profile, name='user_profile'),
]

# URL file naming: urls.py (always)

Django Apps

# App names: lowercase, no underscores preferred (single word best)
INSTALLED_APPS = [
    'blog',          # ✅ Good
    'payments',      # ✅ Good
    'userprofiles',  # ✅ OK (no underscore)
    'user_profiles', # ⚠️ Works but not preferred
]

# App config: PascalCase
class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'

Templates and Static Files

# Template names: lowercase with underscores or hyphens
templates/
    blog/
        article_list.html      # ✅ underscore
        article-detail.html    # ✅ hyphen also OK
        base.html             # ✅ single word

# Template tags/filters: snake_case
@register.filter
def format_currency(value):
    pass

@register.simple_tag
def current_time(format_string):
    pass

Forms

# Form classes: PascalCase with "Form" suffix
class ContactForm(forms.Form):
    email = forms.EmailField()
    message = forms.CharField()

class ArticleModelForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content']

Managers and QuerySets

class PublishedManager(models.Manager):  # PascalCase with "Manager" suffix
    def get_queryset(self):
        return super().get_queryset().filter(is_published=True)

class ArticleQuerySet(models.QuerySet):  # PascalCase with "QuerySet" suffix
    def published(self):
        return self.filter(is_published=True)
    
    def by_author(self, author):
        return self.filter(author=author)

Settings

# Django settings: UPPER_SNAKE_CASE (same as constants)
DEBUG = True
ALLOWED_HOSTS = ['localhost']
STATIC_URL = '/static/'
AUTH_USER_MODEL = 'accounts.User'

# Custom settings should follow same pattern
MY_CUSTOM_SETTING = 'value'
API_RATE_LIMIT = 100

Serializers (Django REST Framework)

# Serializer classes: PascalCase with "Serializer" suffix
class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'content']

class UserRegistrationSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField()

Django Conventions Summary

  • Stick to PEP 8 for general Python code
  • Models: PascalCase classes, snake_case fields and methods
  • Views: snake_case for functions, PascalCase for classes
  • URLs: snake_case for pattern names
  • Apps: lowercase, preferably single words
  • Templates: lowercase with underscores or hyphens
  • Forms/Serializers: PascalCase with descriptive suffixes
  • Settings: UPPER_SNAKE_CASE

Key Rules to Remember

  1. Be consistent - Whatever conventions you choose, stick to them throughout your project
  2. Readability counts - Names should be descriptive and clear
  3. Avoid l, O, I - Single letter names that can be confused with numbers
  4. Length matters - Short names for short scopes, longer descriptive names for longer scopes
  5. Follow PEP 8 - When in doubt, refer to Python's official style guide
  6. Framework conventions - Follow framework-specific conventions (Django, Flask, etc.) on top of PEP 8

Python Programming Conventions

This document outlines coding conventions and best practices for Python development.


Philosophy

The Zen of Python

Refer to The Zen of Python (PEP 20) as a guiding philosophy for writing Pythonic code. Access it by running:

import this

Type Hints

  • Use type hints wherever possible to improve code clarity, maintainability, and enable better tooling support.
  • Annotate function parameters, return types, and class attributes.
  • Use typing module constructs (Optional, Union, List, Dict, Callable, etc.) for complex types.
  • For Python 3.10+, prefer the built-in union syntax (X | Y) and built-in generics (list[str] instead of List[str]).
def calculate_total(items: list[float], tax_rate: float = 0.0) -> float:
    """Calculate the total price including tax."""
    subtotal = sum(items)
    return subtotal * (1 + tax_rate)

Package Management

  • Prefer UV (uv) for Python package management and virtual environment creation.
    • UV is significantly faster than pip and pip-tools.
    • Use uv pip install for package installation.
    • Use uv venv for virtual environment creation.
    • Use uv pip compile for generating locked dependency files.
# Create a virtual environment
uv venv

# Install packages
uv pip install requests pandas

# Install from requirements
uv pip install -r requirements.txt

# Compile/lock dependencies
uv pip compile requirements.in -o requirements.txt

Code Formatting and Linting

  • Use Ruff as the primary linter and formatter.
    • Ruff is extremely fast and replaces multiple tools (flake8, isort, black, etc.).
    • Configure via pyproject.toml or ruff.toml.
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]

Testing

  • Use pytest as the testing framework.
  • Write tests alongside code in a tests/ directory or use inline _test.py suffix.
  • Use fixtures for shared setup and teardown.
  • Aim for high test coverage, especially for critical paths.
  • Use pytest-cov for coverage reporting.
# Run tests
pytest

# Run with coverage
pytest --cov=src --cov-report=term-missing
# Example test
def test_calculate_total() -> None:
    result = calculate_total([10.0, 20.0], tax_rate=0.1)
    assert result == 33.0

Code Quality Principles

Simplicity and Readability

  • Keep code as simple as possible. Avoid unnecessary complexity.
  • Code should be easy to read and understand.
  • Focus on readability over premature optimization.

Naming

  • Use meaningful names for variables, functions, classes, and modules.
  • Names should reveal intent.
  • Use snake_case for functions and variables.
  • Use PascalCase for classes.
  • Use UPPER_SNAKE_CASE for constants.

Functions

  • Functions should be small and do one thing well.
  • Function names should describe the action being performed.
  • Prefer fewer arguments in functions—ideally no more than two or three.
  • Use keyword arguments for optional parameters to improve readability.

Comments and Documentation

  • Only use comments when necessary, as they can become outdated.
  • Strive to make code self-explanatory.
  • When comments are used, they should add useful information not readily apparent from the code.
  • Write docstrings for public modules, classes, and functions.

Error Handling

  • Properly handle errors and exceptions to ensure robustness.
  • Use exceptions rather than error codes for handling errors.
  • Be specific with exception types—avoid bare except: clauses.
  • Use context managers (with statements) for resource management.
def read_config(path: Path) -> dict[str, Any]:
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError:
        raise ConfigurationError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ConfigurationError(f"Invalid JSON in config: {e}")

Project Structure

A typical Python project structure:

project/
├── pyproject.toml      # Project metadata and dependencies
├── README.md
├── src/
│   └── package_name/
│       ├── __init__.py
│       ├── main.py
│       └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_main.py
│   └── test_utils.py
└── .gitignore

Dependencies

  • Define dependencies in pyproject.toml using modern PEP 621 format.
  • Separate development dependencies from production dependencies.
  • Pin versions in lock files for reproducible builds.
# pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.25.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
]

HTTP Requests

  • Prefer httpx over requests for making HTTP requests.
    • httpx supports async/await natively.
    • API is similar to requests for easy migration.
    • Better timeout handling and HTTP/2 support.
import httpx

def fetch_data(url: str) -> dict[str, Any]:
    response = httpx.get(url, timeout=30.0)
    response.raise_for_status()
    return response.json()

Security

  • Consider security implications of the code.
  • Implement security best practices to protect against vulnerabilities.
  • Never hardcode secrets—use environment variables or secret managers.
  • Validate and sanitize all external inputs.
  • Keep dependencies updated to patch known vulnerabilities.

Additional Best Practices

  • Use dataclasses or Pydantic models for structured data.
  • Prefer composition over inheritance.
  • Use pathlib.Path instead of string paths.
  • Use f-strings for string formatting.
  • Use context managers for resource management.
  • Write idiomatic Python—leverage built-in functions and standard library.

References

Python Programming Conventions

This document outlines coding conventions and best practices for Python development.


Philosophy

The Zen of Python

Refer to The Zen of Python (PEP 20) as a guiding philosophy for writing Pythonic code. Access it by running:

import this

Type Hints

  • Use type hints wherever possible to improve code clarity, maintainability, and enable better tooling support.
  • Annotate function parameters, return types, and class attributes.
  • Use typing module constructs (Optional, Union, List, Dict, Callable, etc.) for complex types.
  • For Python 3.10+, prefer the built-in union syntax (X | Y) and built-in generics (list[str] instead of List[str]).
def calculate_total(items: list[float], tax_rate: float = 0.0) -> float:
    """Calculate the total price including tax."""
    subtotal = sum(items)
    return subtotal * (1 + tax_rate)

Package Management

  • Prefer UV (uv) for Python package management and virtual environment creation.
    • UV is significantly faster than pip and pip-tools.
    • Use uv pip install for package installation.
    • Use uv venv for virtual environment creation.
    • Use uv pip compile for generating locked dependency files.
# Create a virtual environment
uv venv

# Install packages
uv pip install requests pandas

# Install from requirements
uv pip install -r requirements.txt

# Compile/lock dependencies
uv pip compile requirements.in -o requirements.txt

Code Formatting and Linting

  • Use Ruff as the primary linter and formatter.
    • Ruff is extremely fast and replaces multiple tools (flake8, isort, black, etc.).
    • Configure via pyproject.toml or ruff.toml.
# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]

Testing

  • Use pytest as the testing framework.
  • Write tests alongside code in a tests/ directory or use inline _test.py suffix.
  • Use fixtures for shared setup and teardown.
  • Aim for high test coverage, especially for critical paths.
  • Use pytest-cov for coverage reporting.
# Run tests
pytest

# Run with coverage
pytest --cov=src --cov-report=term-missing
# Example test
def test_calculate_total() -> None:
    result = calculate_total([10.0, 20.0], tax_rate=0.1)
    assert result == 33.0

Code Quality Principles

Simplicity and Readability

  • Keep code as simple as possible. Avoid unnecessary complexity.
  • Code should be easy to read and understand.
  • Focus on readability over premature optimization.

Naming

  • Use meaningful names for variables, functions, classes, and modules.
  • Names should reveal intent.
  • Use snake_case for functions and variables.
  • Use PascalCase for classes.
  • Use UPPER_SNAKE_CASE for constants.

Functions

  • Functions should be small and do one thing well.
  • Function names should describe the action being performed.
  • Prefer fewer arguments in functions—ideally no more than two or three.
  • Use keyword arguments for optional parameters to improve readability.

Comments and Documentation

  • Only use comments when necessary, as they can become outdated.
  • Strive to make code self-explanatory.
  • When comments are used, they should add useful information not readily apparent from the code.
  • Write docstrings for public modules, classes, and functions.

Error Handling

  • Properly handle errors and exceptions to ensure robustness.
  • Use exceptions rather than error codes for handling errors.
  • Be specific with exception types—avoid bare except: clauses.
  • Use context managers (with statements) for resource management.
def read_config(path: Path) -> dict[str, Any]:
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError:
        raise ConfigurationError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ConfigurationError(f"Invalid JSON in config: {e}")

Project Structure

A typical Python project structure:

project/
├── pyproject.toml      # Project metadata and dependencies
├── README.md
├── src/
│   └── package_name/
│       ├── __init__.py
│       ├── main.py
│       └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_main.py
│   └── test_utils.py
└── .gitignore

Dependencies

  • Define dependencies in pyproject.toml using modern PEP 621 format.
  • Separate development dependencies from production dependencies.
  • Pin versions in lock files for reproducible builds.
# pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.25.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
]

HTTP Requests

  • Prefer httpx over requests for making HTTP requests.
    • httpx supports async/await natively.
    • API is similar to requests for easy migration.
    • Better timeout handling and HTTP/2 support.
import httpx

def fetch_data(url: str) -> dict[str, Any]:
    response = httpx.get(url, timeout=30.0)
    response.raise_for_status()
    return response.json()

Security

  • Consider security implications of the code.
  • Implement security best practices to protect against vulnerabilities.
  • Never hardcode secrets—use environment variables or secret managers.
  • Validate and sanitize all external inputs.
  • Keep dependencies updated to patch known vulnerabilities.

Additional Best Practices

  • Use dataclasses or Pydantic models for structured data.
  • Prefer composition over inheritance.
  • Use pathlib.Path instead of string paths.
  • Use f-strings for string formatting.
  • Use context managers for resource management.
  • Write idiomatic Python—leverage built-in functions and standard library.

References

# rust-toolchain.toml
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt", "rust-src"]
# rustfmt.toml
edition = "2024"
# The warnings caused by this setting can be ignored.
# See https://github.com/openai/openai/pull/298039 for details.
imports_granularity = "Item"
name python-code-style
description "Python code style, linting, formatting, naming conventions, and documentation standards. Use when writing new code, reviewing style, configuring linters, writing docstrings, or establishing project standards."

Python code style and documentation

Consistent code style and clear documentation make codebases maintainable and collaborative. This skill covers modern Python tooling, naming conventions, and documentation standards.

When to use

  • Writing python code
  • Reviewing python code for style consistency
  • Writing or reviewing docstrings
  • Establishing coding standards
  • Setting up linting and formatting for a new project
  • Configuring pyproject.toml, uv, ruff, mypy, pyright, ty, pytest or pytest-cov
  • Creating project documentation

Core Concepts

1. Automated Formatting

Let tools handle formatting debates. Configure once, enforce automatically.

2. Consistent Naming

Follow PEP 8 conventions with meaningful, descriptive names.

3. Documentation as Code

Docstrings should be maintained alongside the code they describe.

4. Type Annotations

Modern Python code should include type hints for all public APIs.

Quick Start

# Install modern tooling
uv pip install ruff mypy

# Configure in pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"  # Adjust based on your project's minimum Python version

[tool.mypy]
strict = true

Fundamental Patterns

Pattern 1: Modern Python Tooling

Use ruff as an all-in-one linter and formatter. It replaces flake8, isort, and black with a single fast tool.

# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"  # Adjust based on your project's minimum Python version

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
    "SIM",  # flake8-simplify
]
ignore = ["E501"]  # Line length handled by formatter

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

Run with:

ruff check --fix .  # Lint and auto-fix
ruff format .       # Format code

Pattern 2: Type Checking Configuration

Configure strict type checking for production code.

# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

Alternative: Use pyright for faster checking.

[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"

Pattern 3: Naming Conventions

Follow PEP 8 with emphasis on clarity over brevity.

Files and Modules:

# Good: Descriptive snake_case
user_repository.py
order_processing.py
http_client.py

# Avoid: Abbreviations
usr_repo.py
ord_proc.py
http_cli.py

Classes and Functions:

# Classes: PascalCase
class UserRepository:
    pass

class HTTPClientFactory:  # Acronyms stay uppercase
    pass

# Functions and variables: snake_case
def get_user_by_email(email: str) -> User | None:
    retry_count = 3
    max_connections = 100

Constants:

# Module-level constants: SCREAMING_SNAKE_CASE
MAX_RETRY_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com"

Pattern 4: Import Organization

Group imports in a consistent order: standard library, third-party, local.

# Standard library
import os
from collections.abc import Callable
from typing import Any

# Third-party packages
import httpx
from pydantic import BaseModel
from sqlalchemy import Column

# Local imports
from myproject.models import User
from myproject.services import UserService

Use absolute imports exclusively:

# Preferred
from myproject.utils import retry_decorator

# Avoid relative imports
from ..utils import retry_decorator

Advanced Patterns

Pattern 5: Google-Style Docstrings

Write docstrings for all public classes, methods, and functions.

Simple Function:

def get_user(user_id: str) -> User:
    """Retrieve a user by their unique identifier."""
    ...

Complex Function:

def process_batch(
    items: list[Item],
    max_workers: int = 4,
    on_progress: Callable[[int, int], None] | None = None,
) -> BatchResult:
    """Process items concurrently using a worker pool.

    Processes each item in the batch using the configured number of
    workers. Progress can be monitored via the optional callback.

    Args:
        items: The items to process. Must not be empty.
        max_workers: Maximum concurrent workers. Defaults to 4.
        on_progress: Optional callback receiving (completed, total) counts.

    Returns:
        BatchResult containing succeeded items and any failures with
        their associated exceptions.

    Raises:
        ValueError: If items is empty.
        ProcessingError: If the batch cannot be processed.

    Example:
        >>> result = process_batch(items, max_workers=8)
        >>> print(f"Processed {len(result.succeeded)} items")
    """
    ...

Class Docstring:

class UserService:
    """Service for managing user operations.

    Provides methods for creating, retrieving, updating, and
    deleting users with proper validation and error handling.

    Attributes:
        repository: The data access layer for user persistence.
        logger: Logger instance for operation tracking.

    Example:
        >>> service = UserService(repository, logger)
        >>> user = service.create_user(CreateUserInput(...))
    """

    def __init__(self, repository: UserRepository, logger: Logger) -> None:
        """Initialize the user service.

        Args:
            repository: Data access layer for users.
            logger: Logger for tracking operations.
        """
        self.repository = repository
        self.logger = logger

Pattern 6: Line Length and Formatting

Set line length to 120 characters for modern displays while maintaining readability.

# Good: Readable line breaks
def create_user(
    email: str,
    name: str,
    role: UserRole = UserRole.MEMBER,
    notify: bool = True,
) -> User:
    ...

# Good: Chain method calls clearly
result = (
    db.query(User)
    .filter(User.active == True)
    .order_by(User.created_at.desc())
    .limit(10)
    .all()
)

# Good: Format long strings
error_message = (
    f"Failed to process user {user_id}: "
    f"received status {response.status_code} "
    f"with body {response.text[:100]}"
)

Pattern 7: Project Documentation

README Structure:

# Project Name

Brief description of what the project does.

## Installation

\`\`\`bash
uv pip install myproject
\`\`\`

## Quick Start

\`\`\`python
from myproject import Client

client = Client(api_key="...")
result = client.process(data)
\`\`\`

## Configuration

Document environment variables and configuration options.

## Development

\`\`\`bash
uv pip install -e ".[dev]"
pytest
\`\`\`

CHANGELOG Format (Keep a Changelog):

# Changelog

## [Unreleased]

### Added
- New feature X

### Changed
- Modified behavior of Y

### Fixed
- Bug in Z

Best Practices Summary

  1. Use ruff - Single tool for linting and formatting
  2. Enable strict mypy - Catch type errors before runtime
  3. 120 character lines - Modern standard for readability
  4. Descriptive names - Clarity over brevity
  5. Absolute imports - More maintainable than relative
  6. Google-style docstrings - Consistent, readable documentation
  7. Document public APIs - Every public function needs a docstring
  8. Keep docs updated - Treat documentation as code
  9. Automate in CI - Run linters on every commit
  10. Target Python 3.10+ - For new projects, Python 3.12+ is recommended for modern language features
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment