Per the owner ruling on issue 40, linting and testing are phases of the main Dockerfile rather than a separate lint file. script/lint and script/test build one phase each by name with caching disabled, and the final stage copies a harmless file from each so the image cannot be built unless both passed. A stage that is not the last is built only when something depends on it or --target names it, so the gates are invoked by name and the edges kept. script/check runs the gates and builds no image of its own; script/cibuild bootstraps first, because CI runs it alone and fmt-check is native. fmt and fmt-check source nvm for the pinned node before calling yarn, which bootstrap installs but leaves off its caller's PATH. Every build in script/ is tagged and uncached. Issue 30 closes too: a container has its own lint cache and lock. Model: opus-5
33 KiB
title, last_modified
| title | last_modified |
|---|---|
| Repository Policies | 2026-09-08 |
This document covers repository structure, tooling, and workflow standards. Code style conventions are in separate documents:
- Code Styleguide (general, bash, Docker)
- Go
- JavaScript
- Python
- Go HTTP Server Conventions
-
Cross-project documentation (such as this file) must include
last_modified: YYYY-MM-DDin the YAML front matter so it can be kept in sync with the authoritative source as policies evolve. -
ALL external references must be pinned by cryptographic hash. This includes Docker base images, Go modules, npm packages, GitHub Actions, and anything else fetched from a remote source. Version tags (
@v4,@latest,:3.21, etc.) are server-mutable and therefore remote code execution vulnerabilities. The ONLY acceptable way to reference an external dependency is by its content hash (Docker@sha256:..., Go module hash ingo.sum, npm integrity hash in lockfile, GitHub Actions@<commit-sha>). No exceptions. This also means nevercurl | bashto install tools like pyenv, nvm, rustup, etc. Instead, download a specific release archive from GitHub, verify its hash (hardcoded in the Dockerfile or script), and only then install. Unverified install scripts are arbitrary remote code execution. This is the single most important rule in this document. Double-check every external reference in every file before committing. There are zero exceptions to this rule. -
Every repo with software must have a root
Makefilewith these targets:make bootstrap,make setup,make test,make lint,make fmt(writes),make fmt-check(read-only),make check(runstest,lint,fmt-check),make docker, andmake hooks(installs pre-commit hook). A model Makefile is athttps://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile. -
Repos follow the Scripts to Rule Them All pattern: the implementation of each Makefile target lives in an executable script in
script/(script/bootstrap,script/setup,script/test,script/lint,script/fmt,script/fmt-check,script/check,script/docker), and the Makefile targets are thin shims that call them. The scripts must be POSIX sh (#!/bin/sh,set -eu, no bashisms) so they run in minimal containers (e.g. alpine images have no bash); locate the repo root with$(cd "$(dirname "$0")/.." && pwd -P)andcdthere before acting. From the standard's canonical set we usebootstrap,setup(make the repo ready for development after a fresh clone: runsbootstrap, theninstall-precommit, plus any repo-specific initialization),test, andcibuild.script/bootstrapinstalls all dependencies idempotently and assumes nothing is present: base tools come from nix, apt, brew, or apk (detected in that order; apt runs noninteractive). For node it uses the installed node if present; otherwise it installs a PINNED node version via nvm, first installing nvm itself if missing — from a hash-verified GitHub release archive (nevercurl | sh), with bash installed as an explicit prerequisite since nvm requires bash. yarn is then pinned viacorepack prepare yarn@<version> --activate. Never install "latest" or "lts"; always exact versions.script/cibuildruns the CI build: it changes to the repo root, runsscript/bootstrap, runsscript/check, and builds the image with the version; the Gitea workflow calls it.script/cibuildrunsscript/bootstrapfirst, because the workflow checks out the repo and runs nothing else, whilescript/fmt-checkruns the formatter on the host: on a pristine checkout with nothing installed the run dies there, after the containerised gates have passed. The bootstrap alone is not enough:script/bootstrapinstalls node and yarn under nvm and leaves neither on thePATHof the shell that called it, so a bareyarnstill exits 127. The host entrypoints that need yarn —script/fmtandscript/fmt-check— therefore source nvm for the pinned node version before invoking it, exactly asscript/bootstrap's own install step does. A runner carrying nothing but docker and git then gets throughscript/check. Four further scripts are our own extensions to the standard:script/checkrunsscript/test,script/lintandscript/fmt-check;script/precommitis what the git pre-commit hook runs, and it callsscript/check;script/install-precommitinstalls the git pre-commit hook (themake hookstarget shims to it); andscript/projectname(literally that filename) simply outputs the project's name. Scripts that need the name callscript/projectname— e.g.script/dockerassembles its image tag from it — so those scripts stay byte-identical across all repos. Repo-type-specific pre-commit extras (e.g.go mod tidyverification in Go repos) belong inscript/precommit, not in the hook itself. Model scripts are athttps://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>. The README must document the provided scripts in an Entrypoints section (see the README requirements below). -
Always use Makefile targets (
make fmt,make test,make lint, etc.) instead of invoking the underlying tools directly. The Makefile is the single source of truth for how these operations are run. -
The Makefile is authoritative documentation for how the repo is used. Beyond the required targets above, it should have targets for every common operation: running a local development server (
make run,make dev), re-initializing or migrating the database (make db-reset,make migrate), building artifacts (make build), generating code, seeding data, or anything else a developer would do regularly. If someone checks out the repo and typesmake<tab>, they should see every meaningful operation available. A new contributor should be able to understand the entire development workflow by reading the Makefile. -
Every repo should have a
Dockerfile, and it carries the repo's gates: alintphase and atestphase, with the final stage depending on both so the image cannot be built unless they pass. For non-server repos the final stage brings up a development environment; for server repos it is the runtime image. Dockerfiles install development prerequisites by runningscript/bootstraprather than duplicating installs inline; COPYscript/and the dependency manifests (package.json+yarn.lock,go.mod+go.sum, etc.) before running it. -
Linting and testing run in Docker, as phases of the
Dockerfile. There is no separate lint file.script/lintandscript/testeach build one phase and nothing else:docker build --no-cache --target lint -t "$(script/projectname)-lint" . docker build --no-cache --target test -t "$(script/projectname)-test" .A stage that is not the last one in the file is built only when the final stage's chain depends on it, or when
--targetnames it. That is why the two gates are always invoked by name here, and why the final stage carries aCOPY --from=of a harmless file from each of them: without that edge a plaindocker build .builds the last stage alone and exits 0 having linted and tested nothing.Every
docker buildinscript/is tagged, here and inscript/cibuildandscript/docker. An untagged build leaves a dangling image behind on every invocation, on every developer host and every CI runner; a tagged one replaces the previous image.Inside a phase the tool is invoked directly —
golangci-lint,go test,eslint,prettier— never throughmake lintorscript/test, which are themselves adocker buildand would recurse into a daemon that does not exist in a build step. Formatting is the exception and stays on the host:script/fmtwrites the working tree, andscript/fmt-checkis its read-only twin.No lint verdict may come from a host invocation of the linter. On a shared host golangci-lint reads a result cache keyed on file content rather than location, so a second checkout of the same content is served the first one's findings, and a host-global lock in
$TMPDIRmakes concurrent runs exit non-zero withparallel golangci-lint is running— a status a caller cannot tell from real findings. Both have produced wrong verdicts in this org, in both directions. A container has its own cache, its ownTMPDIRand a digest-pinned binary, so neither is reachable. -
Any build that runs checks is built with
--no-cache. Docker invalidates aCOPYlayer only when the copied content changes, so on an unchanged tree the checkRUNis served from cache, nothing executes, and the build still exits 0. Everydocker buildinscript/therefore passes--no-cache:script/lint,script/test,script/cibuildandscript/dockerare the four, and there is no fifth —script/checkruns the two gate phases andscript/fmt-check, and builds no image of its own. A baredocker build .is not evidence that anything ran: a sub-second build reporting success is a cache hit, not a result. Never invalidate by pruning —docker builder pruneand friends destroy a build cache shared with every other build on the host. -
The gate phases are separate stages, and the build stage depends on both. The lint phase is based on the
golangci/golangci-lintimage (pinned by hash), so lint failures surface in seconds rather than after a full compile, and the test phase is based on the Go image. The canonical Go repoDockerfile:# Lint phase # golangci/golangci-lint:v2.x.x, YYYY-MM-DD FROM golangci/golangci-lint@sha256:... AS lint WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN golangci-lint run --config .golangci.yml ./... # Test phase # golang:1.x-alpine, YYYY-MM-DD FROM golang@sha256:... AS test WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go test -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -timeout 90s -race -v ./...; exit 1; } # Build stage. Nothing is wanted from either phase above; the copies # are what make BuildKit build them first, so this stage cannot run # unless lint and test passed. # golang:1.x-alpine, YYYY-MM-DD FROM golang@sha256:... AS builder COPY --from=lint /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath \ -ldflags="-s -w -X main.Version=${VERSION}" \ -o /app ./cmd/app/ # Runtime stage, and the last one FROM alpine@sha256:... COPY --from=builder /app /usr/local/bin/app ENTRYPOINT ["app"]Key points:
- The lint phase uses the
golangci/golangci-lintimage directly (it has both Go and the linter), so nothing needs installing. COPY --from=<phase> /src/go.sum /dev/nullis a no-op copy whose only purpose is the ordering edge. BuildKit runs stages in parallel by default, and a stage nothing depends on is not built at all, so without these two lines a red gate would not fail the build.- Keep the runtime stage last, and if you add a stage after it, give it the
same two copies. A plain
docker build .builds the last stage's chain and nothing else. - If the project uses
//go:embeddirectives that reference build artifacts (e.g. a web frontend compiled in a separate stage), the lint phase must create placeholder files so the embed directives resolve. Example:RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css. - If the project requires CGO or system libraries for linting (e.g.
vips-dev), install them in the lint phase withapk add. ARG VERSION=devis declared in the stage that compiles and supplied byscript/dockerandscript/cibuild; no stage may callgit describe.
- The lint phase uses the
-
Every repo should have a Gitea Actions workflow (
.gitea/workflows/) that runsscript/cibuildon push, and checks out the repo as its only other step. That script bootstraps, runs the gate phases, and then builds the image, so a successful run means every check passed; a baredocker build .does not carry the same guarantee, because its gate phases may come from the cache. The image build is uncached and so runs the gate phases a second time. That is the price of the rule above, and it is worth paying: the image that ships is built from a run of its own gates rather than from a cache entry. -
Use platform-standard formatters:
blackfor Python,prettierfor JS/CSS/Markdown/HTML,go fmtfor Go. Always use default configuration with two exceptions: four-space indents (except Go), andproseWrap: alwaysfor Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown, HTML, CSS) should also have.prettierrcand.prettierignore. -
Pre-commit hook: runs
script/precommit, which callsscript/check. If local testing is not possible in the repo,script/precommitmay skipscript/testand run onlyscript/lintandscript/fmt-check. The hook is installed byscript/install-precommit; the Makefile must provide amake hookstarget that shims to it. -
All repos with software must have tests that run via the platform-standard test framework (
go test,pytest,jest/vitest, etc.). If no meaningful tests exist yet, add the most minimal test possible — e.g. importing the module under test to verify it compiles/parses. There is no excuse formake testto be a no-op. -
make testmust complete in under 60 seconds. That is the hard cap, and a suite that exceeds it fails. Under 20 seconds is the target. A suite between 20 and 60 seconds is still green, but the overage must be filed as an improvement bug against that repo. Add a 90-second timeout to the test invocation (go test -timeout 90s). The backstop deliberately sits above the hard cap so that it catches a genuinely hung test rather than a merely slow one. -
The test command should use the conditional verbose rerun pattern. Run tests without
-v(verbose) first. If tests fail, automatically rerun with-vto show full output. This keeps CI logs anddocker buildoutput clean on success (just package/suite summaries) while providing full diagnostic detail on failure (every test case, every assertion). The command lives in thetestphase of theDockerfile, sincescript/testbuilds that phase; the Makefile form below is the same pattern for any repo-local invocation:test: @<test-command> || \ { echo "--- Rerunning with -v for details ---"; \ <test-command-with-v>; exit 1; }Go example:
test: @go test -count=1 -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -count=1 -timeout 90s -race -v ./...; exit 1; }-count=1is required on both invocations: it defeats Go's test result cache, so the target cannot report a pass it did not earn, and the rerun reproduces a failure instead of replaying it. It leaves the build cache alone, so it costs the runtime of the suite and no recompilation.Note that this is a second, independent cache, stacked below the Docker layer cache that issue #26 addresses.
CHECK_EPOCHguarantees theRUN make teststep re-executes; it does not guaranteego testinside that step does any work, because theGOCACHEbaked into earlier image layers survives into the re-executed step. They are two separate defects requiring two separate fixes, and a fix for one must not be recorded as covering the other.Python example:
test: @python -m pytest || \ { echo "--- Rerunning with -v for details ---"; \ python -m pytest -v; exit 1; }The
exit 1ensures the target always fails after a rerun — the first run already proved the tests are broken, so the build must not pass even if a flaky test happens to succeed on the second attempt. The rerun exists solely for diagnostic output. -
Docker builds must complete in under 5 minutes.
-
make checkmust not modify any files in the repo. Tests may use temporary directories. -
mainmust always passmake check, no exceptions. -
Never commit secrets.
.envfiles, credentials, API keys, and private keys must be in.gitignore. No exceptions. -
.gitignoreshould be comprehensive from the start: OS files (.DS_Store), editor files (.swp,*~), in-repo agent scratch directories (.claude/), language build artifacts, andnode_modules/. Fetch the standard.gitignorefromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignorewhen setting up a new repo. These patterns are written to.gitignore's own semantics, in which an unanchored pattern already matches at every depth; they are not a.dockerignoreand must not be transplanted into one unmodified. -
.dockerignoredoes not use.gitignoresemantics, and copying patterns across unmodified leaves secrets in the build context. Docker matches withmoby/patternmatcher:filepath.Matchsemantics plus a**extension, so*does not cross/and a pattern without a leading**/is anchored at the build-context root. A.dockerignorelisting.env,*.pemand*.keytherefore excludes only the copies at the repository root, whileconfig/.envandcerts/server.keystill reach the context and can land in an image layer — which is more dangerous than a short file with no secret patterns at all, because it reads as solved and stops anyone looking. Give every depth-independent pattern the**/prefix and leave only genuinely root-anchored entries unprefixed:.git, and the repo's own host-built binary, written/myappand never**/myapp, which would also matchcmd/myapp/and delete the package directory from the context. Matching is case-sensitive, and an ALL-CAPS twin per pattern still missesServer.Key, so secret names use character ranges —**/*.[kK][eE][yY],**/*.[pP][eE][mM], and likewise for.envrcand the extensionless SSH keys. Where such a pattern also catches something the build needs, re-include it with a negation (!docs/example.env); deleting the pattern reopens the exposure for every other file it covers. Fetch the standard.dockerignorefromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignoreand extend it with the repo's own artifacts. -
In-repo agent scratch belongs in both files, written to each file's own semantics.
.claude/holds one worktree per in-flight agent — an entire additional checkout of the repo — so underCOPY . .the build context inflates by a multiple of the repo and another session's unreviewed work can be copied into an image layer. In.gitignorethe entry is.claude/, unanchored. In.dockerignoreit is.claude, anchored and with no**/prefix, because the prefixed form would also delete any nested directory of that name from the build. Anchoring carries a known gap that the canonical.dockerignorestates in its own comment, since consuming repos receive the file and not the tracker: the directory is created in the agent's working directory, so a repo running agents in subdirectories still shipsservices/api/.claude/and must add its own anchored entry there. -
Excluding
.gitmeansgit describecannot run inside any build stage, and it fails quietly there. In a build stage there is no repository, sogit describewrites nothing to stdout,-X main.Version=comes out empty, the binary reports no version at all, and the build still exits 0. Compute the version on the host and thread it in as a build arg.script/dockerandscript/cibuilddo this, byte-identically across repos:# Own line: a failing command substitution inside an argument does not # trip `set -e`, so the inline form degrades to an empty constant. version="$(git describe --tags --always --dirty 2>/dev/null || true)" [ -n "$version" ] || version="unknown" docker build --no-cache \ --build-arg VERSION="$version" \ -t "$(script/projectname)" .--alwaysmakes an untagged repo yield an abbreviated commit hash rather than failing, and the[ -n "$version" ]line is the single place the fallback is applied — a live check that fires on a build from an export with no.gitand on a repository with no commits yet. Do not fold it into the substitution as|| echo unknown, which makes the guard unreachable. The Dockerfile's side isARG VERSION=devin the stage that compiles, declared there becauseARGis stage-scoped; passingVERSIONto a repo whose Dockerfile declares no suchARGis ignored and costs nothing, which is why the scripts stay byte-identical. One consequence for CI: the standard checkout action clones shallow and fetches no tags, so a repo that embeds a tag-derived version must setfetch-depth: 0on its checkout step. -
Verify
.dockerignoreby enumerating the image, not by reading the patterns. Plant files at the root and at least two directories deep, build a probe image that doesCOPY . ., and list what actually landed (docker run --rm --entrypoint find IMAGE /app). Thetransferring contextsize is not a substitute: a nested secret is a few bytes, and BuildKit transfers only the delta from the previous build. -
No build artifacts in version control. Code-derived data (compiled bundles, minified output, generated assets) must never be committed to the repository if it can be avoided. The build process (e.g. Dockerfile, Makefile) should generate these at build time. Notable exception: Go protobuf generated files (
.pb.go) ARE committed because repos need to work withgo get, which downloads code but does not execute code generation. -
Never use
git add -Aorgit add .. Always stage files explicitly by name. -
Never force-push to
main. -
Make all changes on a feature branch. You can do whatever you want on a feature branch.
-
.golangci.ymlis standardized. The vendored copy in a consuming repo must NEVER be modified by an agent: fetch it fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.ymland keep it byte-identical, so that no repo can quietly loosen its own linting. Linter configuration changes are made to the canonical copy in thepromptsrepo and reach consuming repos by re-vendoring; an agent may open a PR against canonical, which only the user merges. One list is exempt from byte-identity, because it cannot be written once for every repo: thedenylist of thetest-supportdepguard rule, where a repo names its own test-support packages by full import path. A repo adds entries there and changes nothing else, and a re-vendor carries its entries forward. The canonical golangci-lint version is v2.12.2 (released 2026-05-06), pinned as the digest of the lint phase's base image (golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which reports2.12.2 built with go1.26.2 from c0d3ddc9). That digest is the only pin, since no repo installs golangci-lint on the host: bumping the version means changing it and nothing else. -
script/bootstrapinstalls a pinned tool by comparing versions, never by testing presence. Anif ! command -v <tool>; then install; figuard testsPATHonly, so on an already-provisioned machine the pin is inert and a version bump is a silent no-op — while the Dockerfile, installing into a clean image, gets the pinned version, so a localmake checkandmake dockercan disagree about what the tool even is. The canonical form:- compares the installed version against the pin over the whole version
token; a parser that stops at the first
-reports2.12.2for a host running2.12.2-rc1and skips the install; - treats absent, non-zero, empty or unrecognised
--versionoutput as a mismatch, so the failure direction is a redundant install and never a skipped one; - after installing, re-resolves the binary the way callers do —
hash -r, then throughPATH, not through the directory the installer wrote to — and fails naming the resolved path, since an install that a shadowing binary hides succeeds while changing nothing any caller sees; - is actually called, and prints the version on both success paths: a function defined and never invoked has the same exit status and the same empty output as one that worked.
Keep it POSIX sh: no arrays, no
[[, nogrep -P. - compares the installed version against the pin over the whole version
token; a parser that stops at the first
-
When pinning images or packages by hash, add a comment above the reference with the version and date (YYYY-MM-DD).
-
Use
yarn, notnpm. -
Write all dates as YYYY-MM-DD (ISO 8601).
-
Simple projects should be configured with environment variables.
-
Dockerized web services listen on port 8080 by default, overridable with
PORT. -
HTTP/web services must be hardened for production internet exposure before tagging 1.0. This means full compliance with security best practices including, without limitation, all of the following:
- Security headers on every response:
Strict-Transport-Security(HSTS) withmax-ageof at least one year andincludeSubDomains.Content-Security-Policy(CSP) with a restrictive default policy (default-src 'self'as a baseline, tightened per-resource as needed). Never useunsafe-inlineorunsafe-evalunless unavoidable, and document the reason.X-Frame-Options: DENY(orSAMEORIGINif framing is required). Prefer theframe-ancestorsCSP directive as the primary control.X-Content-Type-Options: nosniff.Referrer-Policy: strict-origin-when-cross-origin(or stricter).Permissions-Policyrestricting access to browser features the application does not use (camera, microphone, geolocation, etc.).
- Request and response limits:
- Maximum request body size enforced on all endpoints (e.g. Go
http.MaxBytesReader). Choose a sane default per-route; never accept unbounded input. - Maximum response body size where applicable (e.g. paginated APIs).
ReadTimeoutandReadHeaderTimeouton thehttp.Serverto defend against slowloris attacks.WriteTimeouton thehttp.Server.IdleTimeouton thehttp.Server.- Per-handler execution time limits via
context.WithTimeoutor chi/stdlibmiddleware.Timeout.
- Maximum request body size enforced on all endpoints (e.g. Go
- Authentication and session security:
- Rate limiting on password-based authentication endpoints. API keys are high-entropy and not susceptible to brute force, so they are exempt.
- CSRF tokens on all state-mutating HTML forms. API endpoints
authenticated via
Authorizationheader (Bearer token, API key) are exempt because the browser does not attach these automatically. - Passwords stored using bcrypt, scrypt, or argon2 — never plain-text, MD5, or SHA.
- Session cookies set with
HttpOnly,Secure, andSameSite=Lax(orStrict) attributes.
- Reverse proxy awareness:
- True client IP detection when behind a reverse proxy
(
X-Forwarded-For,X-Real-IP). The application must accept forwarded headers only from a configured set of trusted proxy addresses — never trustX-Forwarded-Forunconditionally.
- True client IP detection when behind a reverse proxy
(
- CORS:
- Authenticated endpoints must restrict
Access-Control-Allow-Originto an explicit allowlist of known origins. Wildcard (*) is acceptable only for public, unauthenticated read-only APIs.
- Authenticated endpoints must restrict
- Error handling:
- Internal errors must never leak stack traces, SQL queries, file paths,
or other implementation details to the client. Return generic error
messages in production; detailed errors only when
DEBUGis enabled.
- Internal errors must never leak stack traces, SQL queries, file paths,
or other implementation details to the client. Return generic error
messages in production; detailed errors only when
- TLS:
- Services never terminate TLS directly. They are always deployed behind
a TLS-terminating reverse proxy. The service itself listens on plain
HTTP. However, HSTS headers and
Securecookie flags must still be set by the application so that the browser enforces HTTPS end-to-end.
- Services never terminate TLS directly. They are always deployed behind
a TLS-terminating reverse proxy. The service itself listens on plain
HTTP. However, HSTS headers and
This list is non-exhaustive. Apply defense-in-depth: if a standard security hardening measure exists for HTTP services and is not listed here, it is still expected. When in doubt, harden.
- Security headers on every response:
-
README.mdis the primary documentation. Required sections:- Description: First line must include the project name, purpose, category (web server, SPA, CLI tool, etc.), license, and author. Example: "µPaaS is an MIT-licensed Go web application by @sneak that receives git-frontend webhooks and deploys applications via Docker in realtime."
- Getting Started: Copy-pasteable install/usage code block.
- Entrypoints: Opens by stating that the repo adheres to the
Scripts to Rule Them All
standard (with that link), then documents each provided
script/entrypoint and its purpose. - Rationale: Why does this exist?
- Design: How is the program structured?
- TODO: Update meticulously, even between commits. When planning, put the todo list in the README so a new agent can pick up where the last one left off.
- License: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
LICENSEfile in the repo root and a License section in the README. - Author: @sneak.
-
First commit of a new repo should contain only
README.md. -
Go module root:
sneak.berlin/go/<name>. Always rungo mod tidybefore committing. -
Use SemVer.
-
Database migrations live in
internal/db/migrations/and must be embedded in the binary.000_migration.sql— contains ONLY the creation of the migrations tracking table itself. Nothing else.001_schema.sql— the full application schema.- Pre-1.0.0: never add additional migration files (002, 003, etc.).
There is no installed base to migrate. Edit
001_schema.sqldirectly. - Post-1.0.0: add new numbered migration files for each schema change. Never edit existing migrations after release.
-
All repos should have an
.editorconfigenforcing the project's indentation settings. -
Avoid putting files in the repo root unless necessary. Root should contain only project-level config files (
README.md,Makefile,Dockerfile,LICENSE,.gitignore,.editorconfig,REPO_POLICIES.md, and language-specific config). Everything else goes in a subdirectory. Canonical subdirectory names:bin/— executable scripts and toolscmd/— Go command entrypoints; thin only: onemain.goper binary whose body is a single call intointernal/orpkg/, no project logic incmd/configs/— configuration templates and examplesdeploy/— deployment manifests (k8s, compose, terraform)docs/— documentation and markdown (README.md stays in root)internal/— Go internal packagesinternal/db/migrations/— database migrationspkg/— Go library packagesshare/— systemd units, data filesstatic/— static assets (images, fonts, etc.)web/— web frontend source
-
When setting up a new repo, files from the
promptsrepo may be used as templates. Fetch them fromhttps://git.eeqj.de/sneak/prompts/raw/branch/main/<path>. -
New repos must contain at minimum:
README.md,.git,.gitignore,.editorconfigLICENSE,REPO_POLICIES.md(copy from thepromptsrepo)Makefilescript/entrypoints (bootstrap,setup,projectname,test,lint,fmt,fmt-check,check,docker,cibuild,precommit,install-precommit)Dockerfile,.dockerignore.gitea/workflows/check.yml- Go:
go.mod,go.sum,.golangci.yml - JS:
package.json,yarn.lock,.prettierrc,.prettierignore - Python:
pyproject.toml