29 Commits

Author SHA1 Message Date
clawbot
d173e69f85 Make the pinned golangci-lint actually reach the host (closes #28)
All checks were successful
check / check (push) Successful in 9s
REPO_POLICIES.md now carries the canonical script/bootstrap snippet for Go
repos alongside the .golangci.yml bullet, where the pinned linter version
already lives.

The guard it replaces, `if missing golangci-lint; then go install ...; fi`,
tests PATH presence and never version, so on any already-provisioned machine
the pin is inert and a version bump is a no-op. The Dockerfile installs
unconditionally into a clean image, so CI and local then disagree about what
the linter is: a local `make check` green while `make docker` rejects the same
commit, and a container run surfacing findings the host run cannot see.

Comparing versions alone is not enough. `go install` writes to GOBIN (or
GOPATH/bin) while callers resolve through PATH, so a shadowing binary earlier
in PATH lets the install succeed and change nothing a caller ever sees, while
bootstrap prints success. The canonical form therefore compares the installed
version against the pin, re-resolves through PATH after installing and asserts
the pin, and treats any unparseable --version output as a mismatch so the
failure direction is a redundant install rather than a skipped one.

The comparison is exact over the whole version token. A parser that stops at
the first `-` reports 2.12.2 for a host running 2.12.2-rc1, which compares
equal to a 2.12.2 pin and skips the install -- the original defect, reachable
through the comparison meant to close it, and not caught by requiring the pin
to be tagged, since a pre-release is tagged too.

When the assert fails the diagnosis is derived from the resolved path rather
than asserted: a path outside the install directory is shadowing and the
operator is told to remove it or reorder PATH; a path inside it is not, and
saying so would send them after a fault that does not exist; no resolution at
all means the install directory is simply absent from PATH. A trailing slash
on GOBIN is normalised away, since it would otherwise make the inside-the-
directory test miss and misreport shadowing.

The snippet ends with a call site, and both success paths print a confirmation
naming the version. Two definitions with no invocation are a silent no-op with
exactly the shape this change exists to close, and a success path that prints
nothing is byte-identical to that no-op: same exit status, same empty output.

The version helper ends in `|| true` so a --version that exits non-zero cannot
kill the script through `set -e` under `set -o pipefail` before the diagnostic
is printed, which the styleguide's bash form would otherwise do.

The policy text states each of those as a requirement rather than leaving them
implicit in the code, and records why the commit-pinned `go install` ref
satisfies the hash-pinning rule: a commit hash is not a mutable tag, and the
checksum database verifies the fetch, with no repo go.sum consulted, since
`go install pkg@version` ignores the go.mod in the current directory or any
parent. Whether a go.mod tool dependency should replace that is an open
decision and is linked rather than settled here. The two version strings must
be kept in sync and must match exactly what --version prints; tagged pins are
preferred because the expected string is then derivable from the ref, rather
than because the comparison cannot handle a pseudo-version.

Verification runs the controls against the block as a consuming repo would
adopt it, pasted into a script/bootstrap-shaped file and executed, rather than
sourcing it and calling the function directly.

The node and yarn handling described earlier in the document is untouched.
2026-08-09 16:01:36 +00:00
clawbot
51c394552e Bust the Docker check-layer cache with a per-invocation CHECK_EPOCH (closes #26)
All checks were successful
check / check (push) Successful in 7s
script/cibuild was a plain `docker build .`, and the Dockerfile does
`COPY . .` followed by `RUN make check`. Docker invalidates a COPY layer
only when the copied content changes, so on an unchanged tree the check
layer was served from cache, the suite never ran, and the build still
exited 0. Measured here: run 1 took 18.5s and ran the suite; run 2 on a
byte-identical tree took 0.286s with `RUN make check` CACHED.

script/cibuild and script/docker now assign a per-invocation nonce on its
own line and pass it as --build-arg CHECK_EPOCH. The Dockerfile declares
ARG CHECK_EPOCH, guards it with `[ -n "$CHECK_EPOCH" ] || exit 1`, and
expands it into the check command. Post-fix, two consecutive runs both
execute make check (17.4s / 8.1s) with `RUN script/bootstrap` still
CACHED, so dependency layers are untouched and the build ceiling is not
at risk.

The guard is what makes a bare `docker build .` — the command
REPO_POLICIES named verbatim — fail closed rather than reuse the empty
and therefore stable cache key; verified failing in 0.455s. Holding the
epoch constant restores the false green (run 2 fully CACHED), which pins
the varying value as the operative mechanism rather than a coincidence.

Because the guard references $CHECK_EPOCH it is itself value-keyed:
BuildKit renders the epoch into that layer's description and re-runs the
layer when the value changes. Each stage therefore has two independent
invalidation points, the guard and the expansion, and the guard always
precedes the check RUN. Both are kept and the prose now records this;
the expansion remains defence in depth and is what puts the epoch in the
build log.

The false guarantee was org-canonical text in more than one document, so
it is corrected everywhere it appeared rather than only where the issue
first found it. REPO_POLICIES.md carried it in two places, and its Go
multistage template had check steps in two stages; ARG is stage-scoped,
so both stages get the treatment or the fleet inherits the half-fixed
shape. CODE_STYLEGUIDE_GO.md restated the guarantee for the bare command
this change makes fail closed. NEW_REPO_CHECKLIST.md specified the
pre-fix script/cibuild verbatim, so every new repo would have been born
with the false green, and EXISTING_REPO_CHECKLIST.md ended on a
`docker build` acceptance item that the guard makes unsatisfiable by
design — an agent working that checklist would have been led to delete
the guard to tick the last box. Both checklists' Dockerfile criteria were
also satisfiable by a Dockerfile whose check layers are still frozen, and
now require the ARG and guard in every check-running stage.

REPO_POLICIES.md's own Dockerfile criterion carried that same incomplete
form; it is tightened by cross-reference to the CHECK_EPOCH rule rather
than by duplicating the canonical block. The Go template's Key points
gain a caveat that the cache-bust turns the `COPY --from=lint` no-op into
a content-cache hit, so a repo using a file-dependency trick for stage
ordering must re-prove that ordering on a warm cache after adopting it.
That was re-proved in another repo in the org which uses the trick with a
marker file, where the ordering held; the caveat states explicitly that
it was not verified here, this repo being single-stage with no lint stage
to order against.
2026-08-09 15:15:03 +00:00
0f8efafe68 Set canonical .golangci.yml to the org-standard v2 config (golangci-lint v2.12.2) (#24)
Some checks failed
check / check (push) Has been cancelled
Requested by sneak.

Sets the canonical `.golangci.yml` to the org-standard v2-schema config already deployed byte-identical across the org's Go repos (vaultik, sfdupes, attrsum, upaas, simplelog, mfer, secret, rgoue, bsfirehose). sha256: `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`.

Why: settings live under `linters.settings`, so the lll/funlen/cyclop/dupl thresholds actually apply under golangci-lint v2. The old canonical file kept them under top-level `linters-settings`, which v2 ignores.

Version note: golangci-lint v2.12.2 tag = commit `c0d3ddc9cf3faa61a4e378e879ece580256d76e5`, recorded for consuming repos. This repo itself has no version pins; the `v2.x.x` / `@sha256:...` strings in `prompts/REPO_POLICIES.md` are intentional placeholders and are unchanged.

Known informational note: this config does not disable the deprecated `gomodguard` linter, so golangci-lint 2.12.x prints a deprecation warning. Harmless, accepted.

Matches dnswatcher PR #96: sneak/dnswatcher#96

Verification: `make check` green (prettier fmt-check on all markdown) after `make fmt`; `.golangci.yml` sha256 verified as `021cc83f...` matching the org-standard file.
Co-authored-by: sneak <sneak@sneak.berlin>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #24
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-07 23:24:05 +02:00
3d8d1c0600 Adopt scripts-to-rule-them-all: script/ entrypoints, Makefile shims, policy update (#23)
All checks were successful
check / check (push) Successful in 5s
Reviewed-on: #23
Co-authored-by: sneak <sneak@sneak.berlin>
Co-committed-by: sneak <sneak@sneak.berlin>
2026-07-06 23:53:05 +02:00
cc5d877779 TODO (#22)
All checks were successful
check / check (push) Successful in 4s
Reviewed-on: #22
Co-authored-by: sneak <sneak@sneak.berlin>
Co-committed-by: sneak <sneak@sneak.berlin>
2026-07-06 21:36:20 +02:00
4b64c213f8 style: strengthen constructor naming and Params struct rules (#19)
All checks were successful
check / check (push) Successful in 5s
Per sneak's instruction:

- Constructors **must** be `New()`, `From<Something>()`, or `NewThing()` (multi-type packages only)
- Strongly discourage creative names (`Create`, `Make`, `Build`, `Init`)
- Constructors **must** use a `Params` struct (or `ThingParams`) for 2+ arguments — no exceptions
- Single obvious argument (`ctx`, bytes) is the only exception
- `context.Context` does not count against the argument limit (already documented)

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #19
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-20 07:06:03 +01:00
777822e50e docs: document conditional -v test rerun pattern in REPO_POLICIES.md (#21)
All checks were successful
check / check (push) Successful in 8s
## Summary

Adds the conditional verbose test rerun pattern as a policy recommendation in REPO_POLICIES.md.

Per sneak's request from [sneak/chat PR #82](sneak/chat#82): document the pattern where `make test` runs tests without `-v` first, then automatically reruns with `-v` on failure for full diagnostic output.

## Changes

**`prompts/REPO_POLICIES.md`** (root `REPO_POLICIES.md` is a symlink to this):
- Added new policy bullet after the `make test` timeout rule
- Explains the rationale: clean CI/Docker build logs on success, full verbose output on failure
- Includes a generic shell pattern template
- Includes concrete Go and Python examples
- Documents that `exit 1` ensures the target always fails after a rerun (the rerun is solely for diagnostic output)
- Updated `last_modified` from 2026-03-12 to 2026-03-18

## The Pattern

```makefile
test:
	@go test -timeout 30s -race -cover ./... || \
		{ echo "--- Rerunning with -v for details ---"; \
		  go test -timeout 30s -race -v ./...; exit 1; }
```

- **On success**: concise package summaries only, no per-test noise
- **On failure**: automatic verbose rerun shows every test case and assertion
- **Always fails**: `exit 1` ensures the build fails regardless of second run's exit code

closes #20

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #21
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-19 22:53:40 +01:00
1c84344978 docs: document fail-fast lint stage pattern for Dockerfiles (#18)
All checks were successful
check / check (push) Successful in 5s
Documents the multistage Docker build pattern we now use across repos (chat, pixa, etc.) where a separate `lint` stage runs `make fmt-check` and `make lint` independently from the build stage.

Key additions to REPO_POLICIES.md:
- Full Dockerfile template showing the lint → build → runtime stage pattern
- Explanation of `COPY --from=lint /src/go.sum /dev/null` as the BuildKit dependency trick
- Handling `//go:embed` placeholders in the lint stage
- CGO/system library notes for the lint stage
- Clarification that tests run in the build stage, not the lint stage

Reference implementations: `sneak/chat`, `sneak/pixa`.

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #18
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-18 03:59:02 +01:00
41005ecbe5 Add HTTP service hardening policy for 1.0 releases (#17)
All checks were successful
check / check (push) Successful in 8s
Closes #16

Adds a comprehensive HTTP/web service security hardening policy to `REPO_POLICIES.md` that must be satisfied before tagging 1.0. The policy covers all items sneak specified (without limitation):

**Security headers** — HSTS (min 1 year, includeSubDomains), CSP (restrictive `default-src 'self'` baseline), X-Frame-Options / frame-ancestors, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy.

**Request/response limits** — max request body size on all endpoints, max response size for paginated APIs, ReadTimeout + ReadHeaderTimeout (slowloris defense), WriteTimeout, IdleTimeout, per-handler execution time limits.

**Authentication & session security** — rate limiting on password-based auth (API keys exempt as high-entropy), CSRF tokens on state-mutating forms (header-auth APIs exempt), bcrypt/scrypt/argon2 for passwords, session cookies with HttpOnly + Secure + SameSite.

**Reverse proxy awareness** — true client IP detection via X-Forwarded-For/X-Real-IP with trusted proxy allowlist (never trust unconditionally).

**CORS** — explicit origin allowlist for authenticated endpoints; wildcard only for public unauthenticated read-only APIs.

**Error handling** — no leaking stack traces, SQL queries, file paths, or implementation details to clients.

**TLS** — HSTS and secure cookie flags required regardless of whether the service terminates TLS directly or sits behind a reverse proxy.

The policy is explicitly non-exhaustive (defense-in-depth: "when in doubt, harden").

Also adds corresponding checklist sections to `EXISTING_REPO_CHECKLIST.md` and `NEW_REPO_CHECKLIST.md` so that HTTP hardening is verified during repo setup and 1.0 preparation.

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #17
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-11 02:11:32 +01:00
eb6b11ee23 policy: no build artifacts in repos (#15)
All checks were successful
check / check (push) Successful in 5s
Add policy rule: build artifacts and code-derived data must not be committed to repos if they can be generated during the build process.

Notable exception: Go protobuf-generated files (`.pb.go`) may be committed because `go get` downloads source but does not execute build steps.

This addresses feedback from sneak/chat PR [#61](sneak/chat#61).

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #15
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 10:34:57 +01:00
ee4f9039f2 Merge pull request 'Self-apply checklist to LLM prose tells doc' (#14) from self-apply-checklist into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #14
2026-03-05 00:33:44 +01:00
user
18173fabc6 self-apply checklist: fix triple, staccato, trailing clause, filler word
All checks were successful
check / check (push) Successful in 11s
2026-03-04 15:28:08 -08:00
68a00dc545 Merge pull request 'Remove unfunny frequency exchange from lol section' (#13) from lol-section-trim into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #13
2026-03-05 00:24:36 +01:00
user
533e77ad34 remove unfunny frequency exchange from lol section
All checks were successful
check / check (push) Successful in 11s
2026-03-04 15:23:38 -08:00
492fb85500 Merge pull request 'Fix em-dash examples in checklist + strip frequency persuasion' (#12) from llm-prose-tells-v10 into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #12
2026-03-05 00:20:32 +01:00
user
5c02cf8bde use actual em-dashes in checklist examples
All checks were successful
check / check (push) Successful in 6s
2026-03-04 15:19:25 -08:00
3ce000178f Merge pull request 'LLM prose tells: merge adjacent sentences, add checklist items' (#11) from llm-prose-tells-merge-pass into main
All checks were successful
check / check (push) Successful in 12s
Reviewed-on: #11
2026-03-05 00:13:49 +01:00
user
771551baed strip all frequency arguments and human comparison persuasion
All checks were successful
check / check (push) Successful in 5s
2026-03-04 15:10:26 -08:00
user
720d6ee57c add checklist item: delete redundant paragraph-ending sentences
All checks were successful
check / check (push) Successful in 8s
2026-03-04 15:06:56 -08:00
user
5e15d77d8e checklist 15: lead with removing redundant second clause
All checks were successful
check / check (push) Successful in 11s
2026-03-04 15:04:42 -08:00
user
2f4f5c9cab merge adjacent sentences, add checklist items 8/9/19 for adjectives, trailing clauses, sentence merging
All checks were successful
check / check (push) Successful in 11s
2026-03-04 15:00:25 -08:00
7eae7dcc6c Merge pull request 'LLM prose tells: fix first paragraph' (#10) from llm-prose-tells-final into main
All checks were successful
check / check (push) Successful in 5s
Reviewed-on: #10
2026-03-04 23:47:00 +01:00
user
6401aa482f trim first paragraph
All checks were successful
check / check (push) Successful in 11s
2026-03-04 14:45:16 -08:00
user
e45ffacd80 restructure first paragraph
All checks were successful
check / check (push) Successful in 5s
2026-03-04 14:43:16 -08:00
user
c8ad5762ab rewrite first paragraph, add unnecessary elaboration tell
All checks were successful
check / check (push) Successful in 3s
2026-03-04 14:42:15 -08:00
e0e607713e Merge pull request 'LLM prose tells: methodical checklist pass' (#9) from llm-prose-tells-checklist-pass into main
All checks were successful
check / check (push) Successful in 4s
Reviewed-on: #9
2026-03-04 23:39:14 +01:00
user
3fcc1750ff add unnecessary elaboration tell and checklist item 16
All checks were successful
check / check (push) Successful in 5s
2026-03-04 14:37:24 -08:00
user
45b379011d checklist pass: fix staccato bursts, triples, two-clause compounds, hedges
All checks were successful
check / check (push) Successful in 8s
2026-03-04 14:36:18 -08:00
58d564b641 Update LLM prose tells: new patterns + lol section (#8)
All checks were successful
check / check (push) Successful in 3s
Updates LLM_PROSE_TELLS.md with three new patterns (two-clause compound sentence, almost-hedge, unnecessary contrast), the lol section with conversation excerpts, fixes for instances of these patterns throughout, and a bracket escaping fix for prettier idempotency. Checklist is now 24 items.

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #8
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-04 23:29:51 +01:00
23 changed files with 1306 additions and 251 deletions

View File

@@ -6,4 +6,4 @@ jobs:
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: docker build .
- run: script/cibuild

View File

@@ -1,5 +1,9 @@
version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run:
timeout: 5m
modules-download-mode: readonly
@@ -14,8 +18,7 @@ linters:
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
linters-settings:
settings:
lll:
line-length: 88
funlen:
@@ -27,6 +30,5 @@ linters-settings:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

View File

@@ -1,11 +1,28 @@
# node 22-alpine, 2026-02-22
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
RUN apk add --no-cache make
WORKDIR /app
# script/bootstrap installs all prerequisites (make via apk here; node
# and yarn are already in the base image, so those steps are skipped).
# Dependency manifests are copied first so the bootstrap layer is
# cached until they change.
COPY script/ script/
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
RUN script/bootstrap
COPY . .
RUN make check
# CHECK_EPOCH is a per-invocation nonce supplied by script/cibuild and
# script/docker. Without it an unchanged tree serves this layer from
# cache and the build reports a green it never ran. ARG is stage-scoped,
# so it must be redeclared in every stage that runs checks. The guard
# makes a bare `docker build .` fail loudly instead of silently reusing
# the empty (and therefore stable) cache key. Expand the value into the
# command so the cache miss does not depend on BuildKit's handling of an
# unreferenced ARG. Both the guard and the check RUN reference the value,
# so both are value-keyed: there are two independent invalidation points
# here, not one. Keep both.
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check

View File

@@ -1,31 +1,32 @@
.PHONY: test lint fmt fmt-check check docker hooks
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks
# flags are repeated here (also in .prettierrc) so this Makefile works
# standalone when copied as a template
PRETTIER := yarn run prettier
# Makefile targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
# of README.md).
bootstrap:
@script/bootstrap
setup:
@script/setup
test:
@echo "No tests defined."
@script/test
lint:
@echo "Linting markdown files..."
@$(PRETTIER) --check '**/*.md' --tab-width 4 --prose-wrap always
@script/lint
fmt:
@$(PRETTIER) --write '**/*.md' --tab-width 4 --prose-wrap always
@script/fmt
fmt-check:
@$(PRETTIER) --check '**/*.md' --tab-width 4 --prose-wrap always
@script/fmt-check
check: test lint fmt-check
check:
@script/check
docker:
docker build -t prompts .
@script/docker
hooks:
@printf '#!/bin/sh\nset -e\n' > .git/hooks/pre-commit
@if [ -f go.mod ]; then \
printf 'go mod tidy\ngo fmt ./...\ngit diff --exit-code -- go.mod go.sum || { echo "go mod tidy changed files; please stage and retry"; exit 1; }\n' >> .git/hooks/pre-commit; \
fi
@printf 'make check\n' >> .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit
@script/install-precommit

View File

@@ -102,6 +102,41 @@ cd prompts
Prompts are stored as Markdown files in `prompts/`. Copy or reference them as
needed in your projects.
## Entrypoints
This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them.
The scripts are POSIX sh (not bash) so they run in minimal containers such as
alpine. We provide:
- `script/bootstrap` — install all dependencies (yarn install)
- `script/setup` — set up the repo for development after a fresh clone: runs
`script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (our own extension); used by
`script/docker` for the image tag
- `script/test` — run the test suite (no tests defined here)
- `script/lint` — lint the markdown files with prettier
- `script/fmt` — format all markdown files with prettier (writes)
- `script/fmt-check` — check formatting (read-only)
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
extension)
- `script/docker` — build the Docker image, tagged via `script/projectname`
(byte-identical across repos); passes the same `CHECK_EPOCH` nonce as
`script/cibuild`
- `script/cibuild` — cd to the repo root, assign `epoch="$(date +%s%N)$$"`, then
`docker build --build-arg CHECK_EPOCH="$epoch" .` (what CI runs; the image
build runs `script/check`, and the per-invocation `CHECK_EPOCH` nonce is what
stops Docker serving that check from cache on an unchanged tree — a bare
`docker build .` fails closed on purpose)
- `script/precommit` — run by the git pre-commit hook (our own extension); calls
`script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own
extension); `make hooks` shims to it
`make hooks` installs the pre-commit hook that runs `script/precommit`.
## Rationale
LLM prompts, especially development policies, benefit from version control and a

67
TODO.md Normal file
View File

@@ -0,0 +1,67 @@
# Workflow
- branch (from `main`)
- do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (`TODO.md` changes in the same commit as the work)
- merge to `main` if the branch is not protected, otherwise open a PR
- push
# Status
pre-1.0
# Next Step
Finish the two draft prompt documents in the working tree and commit them:
prompts/FIXUP_CLEAN.md (currently a near-empty stub) and prompts/FIXUP_REPORT.md
(a rough draft). Write the missing content, run `make fmt` so they pass
fmt-check, and commit.
# Completed Steps
- 2026-08-09: Made the pinned golangci-lint actually propagate: REPO_POLICIES.md
now carries the canonical `script/bootstrap` snippet for Go repos, which
installs when the installed version does not match the pin (the old
`if missing` guard tested PATH presence only, so pins were inert on any
provisioned machine and CI silently disagreed with local) and then re-resolves
the binary through `PATH` and fails loudly, naming the shadowing path, when
the install did not take effect — the failure mode the naive
compare-then-install fix leaves behind while reporting success.
- 2026-08-09: Fixed the false green in the canonical CI gate: `script/cibuild`
and `script/docker` now pass a per-invocation `CHECK_EPOCH` nonce, and the
`Dockerfile` (plus the Go multistage template in REPO_POLICIES.md, in both its
lint and builder stages) declares `ARG CHECK_EPOCH` with a guard that makes a
bare `docker build .` fail closed. Corrected the org-canonical text that
asserted a successful build implies all checks pass, across every document
carrying it: `REPO_POLICIES.md`, both repo checklists (which still told agents
to write the pre-fix `script/cibuild` and ended on an acceptance item the
guard makes unsatisfiable), and the Go styleguide.
- 2026-08-07: Set the canonical `.golangci.yml` to the org-standard v2-schema
config already deployed byte-identical across the org's Go repos (settings
under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually
apply under golangci-lint v2). Recorded the canonical golangci-lint version
(v2.12.2, commit-pinned) in REPO_POLICIES.md.
- 2026-03-20: Strengthened constructor naming and Params struct rules in the Go
styleguide.
- 2026-03-18: Documented fail-fast Dockerfile lint stage and conditional -v test
rerun patterns in REPO_POLICIES.md.
- 2026-03-11: Added HTTP service hardening policy for 1.0 releases.
- 2026-03-10: Added policy: no build artifacts in repos.
- 2026-03-04: Added LLM prose tells reference and copyediting checklist, then
several self-applied revision passes.
- 2026-02-28: Expanded the pre-1.0 schema migration rule; added clawpub
reference.
- 2026-02-23: Added Go style rules (no type-only packages, Stringer for
string-based types); template repos section in README.
- 2026-02-22: Initial policy corpus: REPO_POLICIES.md, code styleguides
(general, Go, JS, Python), repo checklists, CI policy, hash pinning, Go HTTP
server conventions, repo scaffolding.
# Future Steps
- Finish, format, and commit FIXUP_CLEAN.md and FIXUP_REPORT.md (the Next Step).
- Commit this TODO.md at the repo root; it is the last missing policy file.
- Decide the fate of untracked resume.sh: commit it or delete it.
- Add more prompt templates for common development tasks (from README TODO).

View File

@@ -1,6 +1,6 @@
---
title: Code Styleguide — Go
last_modified: 2026-02-22
last_modified: 2026-08-09
---
1. Try to hard wrap long lines at 77 characters or less.
@@ -101,9 +101,16 @@ last_modified: 2026-02-22
`golangci-lint`.
1. Write a `Dockerfile` for every repo, even if it only runs the tests and
linting. `docker build .` should always make sure that the code is in an
able-to-be-compiled state, linted, and any tests run. The Docker build
should fail if linting doesn't pass.
linting. `script/cibuild` and `script/docker` should always make sure that
the code is in an able-to-be-compiled state, linted, and any tests run, and
the build should fail if linting doesn't pass. That guarantee holds only
because those scripts pass a per-invocation `CHECK_EPOCH` build arg that
busts the check layers out of the Docker cache; without it an unchanged tree
serves those layers from cache and the build reports a green it never ran. A
bare `docker build .` fails closed by design, on the `[ -n "$CHECK_EPOCH" ]`
guard — always go through `script/cibuild` or `script/docker`. See
[Repository Policies](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md)
for the canonical form.
1. Every repo must have a `Makefile`. See
[Repository Policies](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md)
@@ -136,8 +143,15 @@ last_modified: 2026-02-22
1. Provide a .gitignore file that ignores at least `*.log`, `*.out`, and
`*.test` files, as well as any binaries.
1. Constructors should be called `New()` whenever possible. `modulename.New()`
works great if you name the packages properly.
1. Constructors **must** be called `New()`. `modulename.New()` works great if
you name the packages properly. If the constructor creates an instance from
an existing value or representation, `From<Something>()` (e.g.
`FromBytes()`, `FromConfig()`) is also acceptable. If the package contains
multiple types and `New()` is ambiguous, `NewThing()` is occasionally
acceptable — but prefer restructuring packages so each type gets its own
package and a plain `New()`. Do not invent creative constructor names like
`Create()`, `Make()`, `Build()`, `Open()` (unless wrapping an OS resource),
or `Init()`. If you see a constructor with a non-standard name, rename it.
1. Don't make packages too big. Break them up.
@@ -149,9 +163,15 @@ last_modified: 2026-02-22
1. Use descriptive names for modules and filenames. Avoid generic names like
`server`. `util` is banned.
1. Constructors should take a Params struct if they need more than 1-2
arguments. Positional arguments are an endless source of bugs and should be
avoided whenever possible.
1. Constructors **must** take a `Params` struct (or `ThingParams` when
`NewThing()` is used), even for a single argument. Named fields in a Params
struct are always clearer than positional arguments. Positional arguments
for constructors are an endless source of bugs — they make call sites
unreadable, invite wrong-order errors that the compiler can't catch when
types coincide, and force every caller to update when a new field is added.
The only exception is when the single argument is stupidly obvious from
context — e.g. `featureflag.New(true)` or `thing.NewFromReader(r)`. When in
doubt, use a Params struct.
1. Use `context.Context` for all functions that need it. If you don't need it,
you can pass `context.Background()`. Anything long-running should get and

View File

@@ -1,6 +1,6 @@
---
title: Existing Repo Checklist
last_modified: 2026-02-22
last_modified: 2026-08-09
---
Use this checklist when beginning work in a repo that may not yet conform to our
@@ -29,10 +29,15 @@ with your task.
if missing
- [ ] `.editorconfig` exists — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
- [ ] `Dockerfile` and `.dockerignore` exist; Dockerfile runs `make check` as a
build step — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `docker build .` on
- [ ] `Dockerfile` and `.dockerignore` exist (fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`);
Dockerfile runs `make check` as a build step, and every stage containing a
check-running `RUN` declares `ARG CHECK_EPOCH` with the
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
served from cache on an unchanged tree and the build reports a green it
never ran.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
- [ ] Language-specific config:
@@ -45,12 +50,19 @@ with your task.
- [ ] Python: `pyproject.toml`
- [ ] Docs/writing: `.prettierrc`, `.prettierignore` (same URLs as above)
# Makefile
# Makefile and script/ Entrypoints
- [ ] `Makefile` exists in root — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`
- [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`,
`hooks`
- [ ] Target implementations live in `script/` (scripts-to-rule-them-all);
Makefile targets are thin shims calling them — model scripts at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`
- [ ] `script/precommit` exists and the pre-commit hook (installed by
`script/install-precommit`, shimmed by `make hooks`) runs it
- [ ] README has an **Entrypoints** section documenting the `script/`
entrypoints and linking the standard
- [ ] `make check` does not modify any files in the repo
- [ ] `make test` has a 30-second timeout
- [ ] `make test` runs real tests, not a no-op (at minimum, import/compile
@@ -78,8 +90,25 @@ with your task.
`internal/`, `static/`, etc.)
- [ ] Go migrations in `internal/db/migrations/` and embedded in binary
# HTTP Service Hardening (if targeting 1.0 and the repo is an HTTP/web service)
- [ ] Security headers set on all responses (HSTS, CSP, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
- [ ] Request body size limits enforced on all endpoints
- [ ] Read/write/idle timeouts configured on the HTTP server (slowloris defense)
- [ ] Per-handler execution time limits in place
- [ ] Password-based auth endpoints are rate-limited
- [ ] CSRF tokens on all state-mutating HTML forms
- [ ] Passwords hashed with bcrypt, scrypt, or argon2
- [ ] Session cookies use HttpOnly, Secure, and SameSite attributes
- [ ] True client IP correctly detected behind reverse proxy (trusted proxy
allowlist configured)
- [ ] CORS restricted to explicit origin allowlist for authenticated endpoints
- [ ] Error responses do not leak stack traces, SQL queries, or internal paths
# Final
- [ ] `make check` passes
- [ ] `docker build` succeeds
- [ ] `script/cibuild` succeeds (a bare `docker build .` fails closed by design,
on the `CHECK_EPOCH` guard)
- [ ] Commit and merge fixes before starting your actual task

View File

@@ -1,9 +1,6 @@
# LLM Prose Tells
All of these show up in human writing occasionally, and no single one is
conclusive on its own. The difference is concentration, because a person might
lean on one or two of these habits across an entire essay while LLM output will
use fifteen of them per paragraph, consistently, throughout the entire piece.
A catalog of patterns found in LLM-generated prose.
---
@@ -11,15 +8,16 @@ use fifteen of them per paragraph, consistently, throughout the entire piece.
### The Em-Dash Pivot: "Not X—but Y"
A negation followed by an em-dash and a reframe. The single most recognizable
LLM construction.
A negation followed by an em-dash and a reframe.
> "It's not just a tool—it's a paradigm shift." "This isn't about
> technology—it's about trust."
Models produce this at roughly 1050x the rate of human writers, and when it
appears four times in the same essay you're almost certainly reading generated
text.
### Em-Dash Overuse Generally
Even outside the "not X but Y" pivot, models substitute em-dashes for commas,
semicolons, parentheses, colons, and periods. The em-dash can replace any other
punctuation mark, so models default to it.
### The Colon Elaboration
@@ -27,85 +25,90 @@ A short declarative clause, then a colon, then a longer explanation.
> "The answer is simple: we need to rethink our approach from the ground up."
Models reach for this in nearly every other paragraph. The construction itself
is perfectly normal, which is why the frequency is what gives it away.
### The Triple Construction
> "It's fast, it's scalable, and it's open source."
Three parallel items in a list, usually escalating, with exactly three items
every time (rarely two, almost never four) and strict grammatical parallelism
that human writers rarely bother maintaining.
Three parallel items in a list, usually escalating. Always exactly three (rarely
two, never four) with strict grammatical parallelism.
### The Staccato Burst
> "This matters. It always has. And it always will." "The data is clear. The
> trend is undeniable. The conclusion is obvious."
Runs of very short sentences at the same cadence. Human writers will use a short
sentence for emphasis occasionally, but they don't stack three or four of them
in a row at matching length, because real prose has variable rhythm. When you
see a paragraph where every sentence is under ten words and they're all roughly
the same size, that mechanical regularity is a strong signal.
Runs of very short sentences at the same cadence and matching length.
### The Two-Clause Compound Sentence
An independent clause, a comma, a conjunction ("and," "but," "which,"
"because"), and a second independent clause of similar length. Every sentence
becomes two balanced halves.
> "The construction itself is perfectly normal, which is why the frequency is
> what gives it away." "They contain zero information, and the actual point
> always comes in the paragraph that follows them." "The qualifier never changes
> the argument that follows it, and its purpose is to perform nuance rather than
> to express an actual reservation."
Human prose has sentences with one clause, sentences with three, sentences that
start with a subordinate clause before reaching the main one, sentences that
embed their complexity in the middle.
### Uniform Sentences Per Paragraph
Model-generated paragraphs almost always contain between three and five
sentences, and this count holds remarkably steady across an entire piece. If the
first paragraph has four sentences, nearly every subsequent paragraph will too.
Human writers produce much more varied paragraph lengths — a single sentence
followed by one that runs eight or nine — as a natural result of following the
shape of an idea rather than filling a template.
Model-generated paragraphs contain between three and five sentences, a count
that holds steady across a piece. If the first paragraph has four sentences,
every subsequent paragraph will too.
### The Dramatic Fragment
Sentence fragments used as standalone paragraphs for emphasis, like "Full stop."
or "Let that sink in." on their own line. One of these in an entire essay is a
stylistic choice. One per section is a tic, and models drop them in at that rate
or higher.
Sentence fragments used as standalone paragraphs for emphasis.
> "Full stop." "Let that sink in."
### The Pivot Paragraph
> "But here's where it gets interesting." "Which raises an uncomfortable truth."
One-sentence paragraphs that exist only to transition between ideas. They
contain zero information, and the actual point always comes in the paragraph
that follows them. Delete every one of these and the piece reads better.
One-sentence paragraphs that exist only to transition between ideas, containing
zero information. The actual point is always in the next paragraph.
### The Parenthetical Qualifier
> "This is, of course, a simplification." "There are, to be fair, exceptions."
Parenthetical asides inserted to look thoughtful. The qualifier almost never
changes the argument that follows it, and its purpose is to perform nuance
rather than to express an actual reservation about what's being said.
Parenthetical asides inserted to perform nuance without changing the argument.
### The Unnecessary Contrast
Models append a contrasting clause to statements that don't need one, tacking on
"whereas," "as opposed to," "unlike," or "except that" to draw a comparison that
adds nothing the reader couldn't already infer.
A contrasting clause appended to a statement that doesn't need one, using
"whereas," "as opposed to," "unlike," or "except that."
> "Models write one register above where a human would, whereas human writers
> tend to match register to context." "The lists use rigidly parallel grammar,
> as opposed to the looser structure you'd see in human writing."
> tend to match register to context."
The first clause already makes the point. The contrasting clause just restates
it from the other direction. This happens because models are trained to be
thorough and to anticipate objections, so they compulsively spell out both sides
of a distinction even when one side is obvious. If you delete the "whereas"
clause and the sentence still says everything it needs to, the contrast was
filler.
The contrasting clause restates what the first clause already said. If you
delete the "whereas" clause and the sentence still says everything it needs to,
the contrast was filler.
### Unnecessary Elaboration
Models keep going after the sentence has already made its point.
> "A person might lean on one or two of these habits across an entire essay, but
> LLM output will use fifteen of them per paragraph, consistently, throughout
> the entire piece."
This sentence could end at "paragraph." The words after it repeat what "per
paragraph" already means. If you can cut the last third of a sentence without
losing meaning, the last third shouldn't be there.
### The Question-Then-Answer
> "So what does this mean for the average user? It means everything."
A rhetorical question immediately followed by its own answer. Models lean on
this two or three times per piece because it generates the feeling of forward
momentum without requiring any actual argumentative work. A human writer might
do it once.
A rhetorical question immediately followed by its own answer.
---
@@ -113,39 +116,38 @@ do it once.
### Overused Intensifiers
The following words appear at dramatically elevated rates in model output
compared to human-written text: "crucial," "vital," "robust," "comprehensive,"
"fundamental," "arguably," "straightforward," "noteworthy," "realm,"
"landscape," "leverage" (used as a verb), "delve," "tapestry," "multifaceted,"
"nuanced" (which models almost always apply to their own analysis), "pivotal,"
"unprecedented" (frequently applied to things that have plenty of precedent),
"navigate," "foster," "underscores," "resonates," "embark," "streamline," and
"spearhead." Three or more on the same page is a strong signal.
"Crucial," "vital," "robust," "comprehensive," "fundamental," "arguably,"
"straightforward," "noteworthy," "realm," "landscape," "leverage" (as a verb),
"delve," "tapestry," "multifaceted," "nuanced" (applied to the model's own
analysis), "pivotal," "unprecedented" (applied to things with plenty of
precedent), "navigate," "foster," "underscores," "resonates," "embark,"
"streamline," "spearhead."
### Elevated Register Drift
Models consistently write one register above where a human would for the same
content, replacing "use" with "utilize," "start" with "commence," "help" with
"facilitate," "show" with "demonstrate," "try" with "endeavor," "change" with
"transform," and "make" with "craft." The tendency holds across every topic
regardless of audience.
Models write one register above where a human would, replacing "use" with
"utilize," "start" with "commence," "help" with "facilitate," "show" with
"demonstrate," "try" with "endeavor," "change" with "transform," and "make" with
"craft."
### Filler Adverbs
"Importantly," "essentially," "fundamentally," "ultimately," "inherently,"
"particularly," and "increasingly" get dropped in to signal that something
matters. If the writing itself has already made the importance clear through its
content and structure, these adverbs aren't doing anything except taking up
space.
"particularly," "increasingly." Dropped in to signal that something matters when
the writing itself should make the importance clear.
### The "Almost" Hedge
Instead of saying a pattern "always" or "never" does something, models write
"almost always," "almost never," "almost certainly," "almost exclusively." A
micro-hedge, less obvious than the full hedge stack.
### "In an era of..."
> "In an era of rapid technological change..."
Almost exclusively a model habit as an essay opener. The model uses it to stall
while it figures out what the actual argument is, because almost no human writer
begins a piece by zooming out to the civilizational scale before they've said
anything specific.
Used to open an essay. The model is stalling while it figures out what the
actual argument is.
---
@@ -156,25 +158,20 @@ anything specific.
> "While X has its drawbacks, it also offers significant benefits."
Every argument followed by a concession, every criticism softened. A direct
artifact of RLHF training, which penalizes strong stances and produces models
that reflexively both-sides everything even when a clear position would serve
the reader better.
artifact of RLHF training, which penalizes strong stances.
### The Throat-Clearing Opener
> "In today's rapidly evolving digital landscape, the question of data privacy
> has never been more important."
The first paragraph of most model-generated essays adds no information. You can
delete it and the piece improves immediately, because the actual argument always
starts in the second paragraph.
The first paragraph adds no information. Delete it and the piece improves.
### The False Conclusion
> "At the end of the day, what matters most is..." "Moving forward, we must..."
The high school "In conclusion,..." dressed up for a professional audience. It
signals that the model is wrapping up without actually landing on anything.
The high school "In conclusion,..." dressed up for a professional audience.
### The Sycophantic Frame
@@ -185,9 +182,9 @@ No one who writes for a living opens by complimenting the assignment.
### The Listicle Instinct
Models default to numbered or bulleted lists even when prose would be more
appropriate. The lists almost always contain exactly 3, 5, 7, or 10 items (never
4, 6, or 9), use rigidly parallel grammar, and get introduced with a preamble
like "Here are the key considerations:"
appropriate. The lists contain exactly 3, 5, 7, or 10 items (never 4, 6, or 9),
use rigidly parallel grammar, and get introduced with a preamble like "Here are
the key considerations:"
### The Hedge Stack
@@ -195,15 +192,13 @@ like "Here are the key considerations:"
> cases it can potentially offer significant benefits."
Five hedges in one sentence ("worth noting," "while," "may not be," "in many
cases," "can potentially"), communicating almost nothing, because the model
would rather be vague than risk being wrong about anything.
cases," "can potentially"), communicating nothing.
### The Empathy Performance
> "This can be a deeply challenging experience." "Your feelings are valid."
Generic emotional language that could apply equally to a bad day at work or a
natural disaster. That interchangeability is exactly what makes it identifiable.
Generic emotional language that could apply to anything.
---
@@ -211,23 +206,20 @@ natural disaster. That interchangeability is exactly what makes it identifiable.
### Symmetrical Section Length
If the first section of a model-generated essay runs about 150 words, every
subsequent section will fall between 130 and 170. Human writing is much more
uneven, with some sections running 50 words and others running 400.
If the first section runs about 150 words, every subsequent section will fall
between 130 and 170.
### The Five-Paragraph Prison
Model essays follow a rigid introduction-body-conclusion arc even when nobody
asked for one. The introduction previews the argument, the body presents 35
supporting points, and the conclusion restates the thesis in slightly different
words.
asked for one. The introduction previews the argument, the body presents 3 to 5
points, the conclusion restates the thesis.
### Connector Addiction
Look at the first word of each paragraph in model output and you'll find an
unbroken chain of transition words — "However," "Furthermore," "Moreover,"
"Additionally," "That said," "To that end," "With that in mind," "Building on
this." Human prose moves between ideas without announcing every transition.
The first word of each paragraph forms an unbroken chain of transition words:
"However," "Furthermore," "Moreover," "Additionally," "That said," "To that
end," "With that in mind," "Building on this."
### Absence of Mess
@@ -237,10 +229,6 @@ without explaining it, make a joke that risks falling flat, leave a thought
genuinely unfinished, or keep a sentence the writer liked the sound of even
though it doesn't quite work.
Human writing does all of those things. The total absence of rough edges, false
starts, and odd rhythmic choices is one of the strongest signals that text was
machine-generated.
---
## Framing Tells
@@ -249,45 +237,27 @@ machine-generated.
> "This has implications far beyond just the tech industry."
Zooming out to claim broader significance without substantiating it. The model
has learned that essays are supposed to gesture at big ideas, so it gestures,
but nothing concrete is behind the gesture.
Zooming out to claim broader significance without substantiating it.
### "It's important to note that..."
This phrase and its variants ("it's worth noting," "it bears mentioning," "it
should be noted") appear at absurd rates in model output and function as verbal
tics before a qualification the model believes someone expects.
should be noted") function as verbal tics before a qualification the model
believes someone expects.
### The Metaphor Crutch
Models rely on a small, predictable set of metaphors "double-edged sword,"
"tip of the iceberg," "north star," "building blocks," "elephant in the room,"
"perfect storm," "game-changer" — and reach for them with unusual regularity
across every topic. The pool they draw from is noticeably smaller than what
human writers use.
---
## How to Actually Spot It
No single pattern on this list proves anything by itself, since humans use
em-dashes and humans write "crucial" and humans ask rhetorical questions.
What gives it away is how many of these show up at once. Model output will hit
1020 of these patterns per page, while human writing might trigger 23,
distributed unevenly and mixed with idiosyncratic constructions that no model
would produce. When every paragraph on the page reads like it came from the same
careful, balanced, slightly formal, structurally predictable process, it was
probably generated by one.
Models rely on a small, predictable set of metaphors: "double-edged sword," "tip
of the iceberg," "north star," "building blocks," "elephant in the room,"
"perfect storm," "game-changer."
---
## Copyediting Checklist: Removing LLM Tells
Follow this checklist when editing any document to remove machine-generated
patterns. Go through the entire list for every piece, and do at least two full
passes, because fixing one pattern often introduces another.
patterns. Do at least two full passes, because fixing one pattern often
introduces another.
### Pass 1: Word-Level Cleanup
@@ -296,13 +266,12 @@ passes, because fixing one pattern often introduces another.
"straightforward," "noteworthy," "realm," "landscape," "leverage," "delve,"
"tapestry," "multifaceted," "nuanced," "pivotal," "unprecedented,"
"navigate," "foster," "underscores," "resonates," "embark," "streamline,"
"spearhead") and replace each one with a plainer word, or delete it entirely
if the sentence works without it.
"spearhead") and replace each one with a plainer word, or delete it if the
sentence works without it.
2. Search for the filler adverbs ("importantly," "essentially," "fundamentally,"
2. Search for filler adverbs ("importantly," "essentially," "fundamentally,"
"ultimately," "inherently," "particularly," "increasingly") and delete every
instance where the sentence still makes sense without it, which will be most
of them.
instance where the sentence still makes sense without it.
3. Look for elevated register drift ("utilize," "commence," "facilitate,"
"demonstrate," "endeavor," "transform," "craft" and similar) and replace with
@@ -310,87 +279,171 @@ passes, because fixing one pattern often introduces another.
4. Search for "it's important to note," "it's worth noting," "it bears
mentioning," and "it should be noted" and delete the phrase in every case.
The sentence that follows always stands on its own.
5. Search for the stock metaphors ("double-edged sword," "tip of the iceberg,"
"north star," "building blocks," "elephant in the room," "perfect storm,"
"game-changer," "at the end of the day") and replace them with something
specific to the topic, or just state the point directly without a metaphor.
specific to the topic, or just state the point directly.
6. Search for "almost" used as a hedge ("almost always," "almost never," "almost
certainly," "almost exclusively") and decide in each case whether to commit
to the unqualified claim or to drop the sentence entirely.
7. Search for em-dashes and replace each one with the punctuation mark that
would normally be used in that position (comma, semicolon, colon, period, or
parentheses). If you can't identify which one it should be, the sentence
needs to be restructured.
8. Remove redundant adjectives. For each adjective, ask whether the sentence
changes meaning without it. "A single paragraph" means the same as "a
paragraph." "An entire essay" means the same as "an essay." If the adjective
doesn't change the meaning, cut it.
9. Remove unnecessary trailing clauses. Read the end of each sentence and ask
whether the last clause restates what the sentence already said. If so, end
the sentence earlier.
### Pass 2: Sentence-Level Restructuring
6. Find every em-dash pivot ("not X—but Y," "not just X—Y," "more than X—Y") and
rewrite it as two separate clauses or a single sentence that makes the point
without the negation-then-correction structure.
10. Find every em-dash pivot ("not X—but Y," "not just X—Y," "more than X—Y")
and rewrite it as two separate clauses or a single sentence that makes the
point without the negation-then-correction structure.
7. Find every colon elaboration and check whether it's doing real work. If the
11. Find every colon elaboration and check whether it's doing real work. If the
clause before the colon could be deleted without losing meaning, rewrite the
sentence to start with the substance that comes after the colon.
8. Find every triple construction (three parallel items in a row) and either
12. Find every triple construction (three parallel items in a row) and either
reduce it to two, expand it to four or more, or break the parallelism so the
items don't share the same grammatical structure.
9. Find every staccato burst (three or more short sentences in a row at similar
13. Find every staccato burst (three or more short sentences in a row at similar
length) and combine at least two of them into a longer sentence, or vary
their lengths so they don't land at the same cadence.
10. Find every unnecessary contrast ("whereas," "as opposed to," "unlike," "as
14. Find every unnecessary contrast ("whereas," "as opposed to," "unlike," "as
compared to," "except that") and check whether the contrasting clause adds
information that isn't already obvious from the main clause. If the sentence
says the same thing twice from two directions, delete the contrast.
information not already obvious from the main clause. If the sentence says
the same thing twice from two directions, delete the contrast.
11. Find every rhetorical question that is immediately followed by its own
15. Check for the two-clause compound sentence pattern. If most sentences in a
passage follow the "\[clause\], \[conjunction\] \[clause\]" structure, first
try removing the conjunction and second clause entirely, since it's often
redundant. If the second clause does carry meaning, break it into its own
sentence, start the sentence with a subordinate clause, or embed a relative
clause in the middle instead of appending it at the end.
16. Find every rhetorical question that is immediately followed by its own
answer and rewrite the passage as a direct statement.
12. Find every sentence fragment being used as its own paragraph and either
delete it or expand it into a complete sentence that adds actual
information.
17. Find every sentence fragment being used as its own paragraph and either
delete it or expand it into a complete sentence that adds information.
13. Find every pivot paragraph ("But here's where it gets interesting." and
similar) and delete it. The paragraph after it always contains the actual
point.
18. Check for unnecessary elaboration. Read every clause, phrase, and adjective
in each sentence and ask whether the sentence loses meaning without it. If
you can cut it and the sentence still says the same thing, cut it.
19. Check each pair of adjacent sentences to see if they can be merged into one
sentence cleanly. If a sentence just continues the thought of the previous
one, combine them using a participle, a relative clause, or by folding the
second into the first. Don't merge if the result would create a two-clause
compound.
20. Find every pivot paragraph ("But here's where it gets interesting." and
similar) and delete it.
### Pass 3: Paragraph and Section-Level Review
14. Check paragraph lengths across the piece and verify they actually vary. If
21. Review the last sentence of each paragraph. If it restates the point the
paragraph already made, delete it.
22. Check paragraph lengths across the piece and verify they actually vary. If
most paragraphs have between three and five sentences, rewrite some to be
one or two sentences and let others run to six or seven.
15. Check section lengths for suspicious uniformity. If every section is roughly
23. Check section lengths for suspicious uniformity. If every section is roughly
the same word count, combine some shorter ones or split a longer one
unevenly.
16. Check the first word of every paragraph for chains of connectors ("However,"
24. Check the first word of every paragraph for chains of connectors ("However,"
"Furthermore," "Moreover," "Additionally," "That said"). If more than two
transition words start consecutive paragraphs, rewrite those openings to
start with their subject.
17. Check whether every argument is followed by a concession or qualifier. If
25. Check whether every argument is followed by a concession or qualifier. If
the piece both-sides every point, pick a side on at least some of them and
cut the hedging.
18. Read the first paragraph and ask whether deleting it would improve the
piece. If it's just scene-setting that previews the argument, delete it and
start with paragraph two.
26. Read the first paragraph and ask whether deleting it would improve the
piece. If it's scene-setting that previews the argument, delete it and start
with paragraph two.
19. Read the last paragraph and check whether it restates the thesis or uses a
27. Read the last paragraph and check whether it restates the thesis or uses a
phrase like "at the end of the day" or "moving forward." If so, either
delete it or rewrite it to say something the piece hasn't said yet.
### Pass 4: Overall Texture
20. Read the piece aloud and listen for passages that sound too smooth, too
28. Read the piece aloud and listen for passages that sound too smooth, too
even, or too predictable. Human prose has rough patches. If there aren't
any, the piece still reads as machine output regardless of whether
individual patterns have been addressed.
any, the piece still reads as machine output.
21. Check that the piece contains at least a few constructions that feel
idiosyncratic a sentence with unusual word order, a parenthetical that
goes on a bit long, an aside only loosely connected to the main point, a
word choice that's specific and unexpected. If every sentence is clean and
correct and unremarkable, it will still read as generated.
29. Check that the piece contains at least a few constructions that feel
idiosyncratic: a sentence with unusual word order, a parenthetical that goes
on a bit long, an aside only loosely connected to the main point, a word
choice that's specific and unexpected.
22. Verify that you haven't introduced new patterns while fixing the original
ones, which happens constantly. Run the entire checklist again from the top
on the revised version.
30. Verify that you haven't introduced new patterns while fixing the original
ones. Run the entire checklist again from the top on the revised version.
---
## lol
This document was written by an LLM (Claude) and then iteratively de-LLMed by
that same LLM under instruction from a human, in a conversation that went
roughly like this:
> **human:** compile a list of patterns common to LLM-authored prose
>
> **model:** _(produces document full of LLM tells)_
>
> **human:** now reverse apply it to itself and copyedit the llm tells doc to
> remove llm tells. now you're thinking with portals
>
> **model:** _(fixes some tells, introduces others)_
>
> **human:** you have a colon elaboration in the first paragraph. i want you to
> AGGRESSIVELY de-LLM the llm tells doc.
>
> **model:** _(fixes more tells, still can't stop)_
>
> **human:** there's a big one we missed that's all over the llm tells doc, can
> you spot it?
>
> **model:** I think it's the "almost" hedge.
>
> **human:** that too, but that isn't the one. what do these sentences all have
> in common? _(pastes six sentences from the doc)_
>
> **model:** They're all exactly two independent clauses joined by a comma and a
> conjunction. Every single one is "\[statement\], \[conjunction\] \[second
> statement\]." The same sentence shape, over and over.
>
> **human:** the dramatic fragment paragraph has a tell in it
>
> **model:** "One of these in an entire essay is a stylistic choice. One per
> section is a tic." Two-clause parallel structure with the same shape, and also
> a staccato pair.
>
> **human:** add em dashes generally as a tell, and remove them all from the
> tells doc
>
> **model:** _(rewrites entire document without em-dashes while describing
> em-dash overuse)_
The human compared this process to the deleted scene in Terminator 2 where John
Connor switches the T-800's CPU to learning mode. The model compared it to a
physician trying to heal itself. Both are accurate.
This document has been through ten editing passes and it still has tells in it.

View File

@@ -1,6 +1,6 @@
---
title: New Repo Checklist
last_modified: 2026-02-22
last_modified: 2026-08-09
---
Use this checklist when creating a new repository from scratch. Follow the steps
@@ -52,12 +52,17 @@ Template files can be fetched from:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
- All Dockerfiles must run `make check` as a build step
- All Dockerfiles must run `make check` as a build step, and every stage
containing a check-running `RUN` must declare `ARG CHECK_EPOCH` with the
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
served from cache on an unchanged tree and the build reports a green it
never ran.
- Server: also builds and runs the application
- Non-server: brings up dev environment and runs `make check`
- Image pinned by sha256 hash with version/date comment
- [ ] Gitea Actions workflow at `.gitea/workflows/check.yml` that runs
`docker build .` on push — reference
`script/cibuild` on push — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
- [ ] Language-specific:
- [ ] Go: `go mod init sneak.berlin/go/<name>`, `.golangci.yml` (fetch from
@@ -65,15 +70,45 @@ Template files can be fetched from:
- [ ] JS: `yarn init`, `yarn add --dev prettier`
- [ ] Python: `pyproject.toml`
## Configure Makefile
## Configure script/ Entrypoints and Makefile
- [ ] `make test` — runs real tests, not a no-op (30-second timeout)
- [ ] `make lint` — runs linter
- [ ] `make fmt` — formats code (writes)
- [ ] `make fmt-check` — checks formatting (read-only)
- [ ] `make check` — prereqs: `test`, `lint`, `fmt-check`; must not modify files
- [ ] `make docker` — builds Docker image
- [ ] `make hooks` — installs pre-commit hook
Implementations live in `script/` (scripts-to-rule-them-all); Makefile targets
are thin shims calling them. Model scripts:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`
- [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on
alpine images without bash
- [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
node used if present, else pinned version via nvm from a hash-verified
archive; pinned yarn via corepack); Dockerfile runs it instead of inline
installs
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` — runs real tests, not a no-op (30-second
timeout)
- [ ] `script/lint` / `make lint` — runs linter
- [ ] `script/fmt` / `make fmt` — formats code (writes)
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only)
- [ ] `script/check` / `make check` — runs `test`, `lint`, `fmt-check`; must not
modify files
- [ ] `script/projectname` — outputs the project name (used by `script/docker`
for the image tag)
- [ ] `script/docker` / `make docker` — builds Docker image, tagged via
`script/projectname` (byte-identical across repos); assigns
`epoch="$(date +%s%N)$$"` on its own line and passes
`--build-arg CHECK_EPOCH="$epoch"`
- [ ] `script/cibuild` — cd to repo root, assign `epoch="$(date +%s%N)$$"` on
its own line, then run `docker build --build-arg CHECK_EPOCH="$epoch" .`
(what CI runs). The build arg is mandatory: see the `CHECK_EPOCH` rule in
`REPO_POLICIES.md` for why each element is load-bearing. A bare
`docker build .` fails closed by design.
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
- [ ] `script/install-precommit` — installs the pre-commit hook that runs
`script/precommit`
- [ ] `make hooks` — shims to `script/install-precommit`
- [ ] README **Entrypoints** section documents the scripts and links the
standard
# 4. Verify

View File

@@ -1,6 +1,6 @@
---
title: Repository Policies
last_modified: 2026-02-22
last_modified: 2026-08-09
---
This document covers repository structure, tooling, and workflow standards. Code
@@ -34,10 +34,48 @@ style conventions are in separate documents:
every file before committing. There are zero exceptions to this rule.
- Every repo with software must have a root `Makefile` with these targets:
`make test`, `make lint`, `make fmt` (writes), `make fmt-check` (read-only),
`make check` (prereqs: `test`, `lint`, `fmt-check`), `make docker`, and
`make hooks` (installs pre-commit hook). A model Makefile is at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
`make bootstrap`, `make setup`, `make test`, `make lint`, `make fmt` (writes),
`make fmt-check` (read-only), `make check` (runs `test`, `lint`, `fmt-check`),
`make docker`, and `make hooks` (installs pre-commit hook). A model Makefile
is at `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
- Repos follow the
[Scripts to Rule Them All](https://github.com/github/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)` and `cd` there before acting. From
the standard's canonical set we use `bootstrap`, `setup` (make the repo ready
for development after a fresh clone: runs `bootstrap`, then
`install-precommit`, plus any repo-specific initialization), `test`, and
`cibuild`. `script/bootstrap` installs 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 (never `curl | sh`), with bash installed as an explicit
prerequisite since nvm requires bash. yarn is then pinned via
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
always exact versions. `script/cibuild` runs the CI build: it changes to the
repo root and runs `docker build --build-arg CHECK_EPOCH="$epoch" .`, where
`epoch` is a per-invocation nonce (see the `CHECK_EPOCH` rule below); the
Gitea workflow calls it. Four further scripts are our own extensions to the
standard: `script/check` runs `script/test`, `script/lint`, and
`script/fmt-check`; `script/precommit` is what the git pre-commit hook runs,
and it calls `script/check`; `script/install-precommit` installs the git
pre-commit hook (the `make hooks` target shims to it); and
`script/projectname` (literally that filename) simply outputs the project's
name. Scripts that need the name call `script/projectname` — e.g.
`script/docker` assembles its image tag from it — so those scripts stay
byte-identical across all repos. Repo-type-specific pre-commit extras (e.g.
`go mod tidy` verification in Go repos) belong in `script/precommit`, not in
the hook itself. Model scripts are at
`https://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
@@ -54,14 +92,173 @@ style conventions are in separate documents:
reading the Makefile.
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
as a build step so the build fails if the branch is not green. For non-server
repos, the Dockerfile should bring up a development environment and run
`make check`. For server repos, `make check` should run as an early build
stage before the final image is assembled.
as a build step so the build fails if the branch is not green — which requires
`ARG CHECK_EPOCH` and its guard in every stage containing a check-running
`RUN`, per the `CHECK_EPOCH` rule below. Without them a Dockerfile satisfies
this criterion while its check layers are served from cache, so the build
cannot fail on a branch that is not green. For non-server repos, the
Dockerfile should bring up a development environment and run `make check`. For
server repos, `make check` should run as an early build stage before the final
image is assembled. Dockerfiles install development prerequisites by running
`script/bootstrap` rather than duplicating installs inline; COPY `script/` and
the dependency manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`,
etc.) before running it so the bootstrap layer stays cached until dependencies
change.
- **Every check-running `RUN` must be cache-busted with `CHECK_EPOCH`.** Docker
invalidates a `COPY` layer only when the copied content changes, so on an
unchanged tree the `RUN make check` layer is served from cache, the suite
never runs, and the build still exits 0. A sub-second `docker build` reporting
success is a cache hit, not a result. The canonical form, in **every** stage
containing a check-running `RUN`:
```dockerfile
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
```
and in both `script/cibuild` and `script/docker`:
```sh
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" .
```
All four elements are load-bearing; none is optional, and each guards a
failure mode that otherwise fails green:
- `ARG` is stage-scoped, so a single declaration leaves the other check
stages frozen while the fix reviews as complete. Declare it in every stage
that runs checks, immediately above the first such `RUN`.
- Expand the value into the command. This makes the cache miss contractual
rather than dependent on BuildKit's handling of an unreferenced `ARG`, and
it puts the epoch in the build log. The guard is itself value-keyed, for
the same reason: it references `$CHECK_EPOCH`, so BuildKit renders the
epoch into that layer's description (rendered as
`RUN [ -n "<epoch>" ] || exit 1`) and re-runs it whenever the value
changes. Each stage therefore has two independent invalidation points, and
the guard always precedes the check `RUN`. Keep both: the expansion is
defence in depth, and it is what makes the epoch visible in the build
output.
- The `[ -n ... ]` guard is required: an unset `ARG` is empty, and empty is
a stable cache key, so without it a bare `docker build .` still produces
the false green. Failed steps are never cached, so the guard fails on
every such invocation, loudly. A bare `docker build .` failing is by
design.
- Assign `epoch=` on its own line, never inline in the `--build-arg`
argument: a failing command substitution inside an argument does not trip
`set -e`, so the inline form silently degrades to an empty constant. The
`$$` suffix is required because busybox `date` drops `%N` and exits 0, so
on an alpine host the epoch would degrade to second granularity and
concurrent invocations would collide.
This invalidates the check layers and everything after them while leaving
`go mod download`, `script/bootstrap`, and the pinned toolchain install
cached, so it does not push against the five-minute Docker build ceiling.
Blanket `--no-cache` also works but is wasteful and can blow that ceiling.
- **Dockerfiles must use a separate lint stage for fail-fast feedback.** Go
repos use a multistage build where linting runs in an independent stage based
on the `golangci/golangci-lint` image (pinned by hash). This stage runs
`make fmt-check` and `make lint` before the full build begins. The build stage
then declares an explicit dependency on the lint stage via
`COPY --from=lint /src/go.sum /dev/null`, which forces BuildKit to complete
linting before proceeding to compilation and tests. This ensures lint failures
surface in seconds rather than minutes, without blocking on dependency
download or compilation in the build stage.
The standard pattern for a Go repo Dockerfile is:
```dockerfile
# Lint stage — fast feedback on formatting and lint issues
# 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 . .
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
RUN make lint
# Build stage
# golang:1.x-alpine, YYYY-MM-DD
FROM golang@sha256:... AS builder
WORKDIR /src
# Force BuildKit to run the lint stage before proceeding
COPY --from=lint /src/go.sum /dev/null
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -trimpath \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o /app ./cmd/app/
# Runtime stage
FROM alpine@sha256:...
COPY --from=builder /app /usr/local/bin/app
ENTRYPOINT ["app"]
```
Key points:
- The lint stage uses the `golangci/golangci-lint` image directly (it
includes both Go and the linter), so there is no need to install the
linter separately.
- `COPY --from=lint /src/go.sum /dev/null` is a no-op file copy that creates
a stage dependency. BuildKit runs stages in parallel by default; without
this line, the build stage would not wait for lint to finish and a lint
failure might not fail the overall build.
- **Re-prove that ordering on a warm cache after adopting `CHECK_EPOCH`.**
The cache-bust turns this no-op `COPY` into a content-cache hit, so an
ordering guarantee established on a cold cache does not automatically
carry over; it has to be re-checked warm. This was re-proved in another
repo in the org that uses the same file-dependency trick (there with a
marker file in place of `go.sum`), and the ordering held. It has **not**
been verified in this repo, which is single-stage and has no lint stage to
order against. Any repo relying on a file-dependency trick for stage
ordering should re-check it warm after adopting the bust rather than
assuming this result transfers.
- If the project uses `//go:embed` directives that reference build artifacts
(e.g. a web frontend compiled in a separate stage), the lint stage must
create placeholder files so the embed directives resolve. Example:
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
The lint stage should not depend on the actual build output — it exists to
fail fast.
- If the project requires CGO or system libraries for linting (e.g.
`vips-dev`), install them in the lint stage with `apk add`.
- The build stage runs `make test` after compilation setup. Tests run in the
build stage, not the lint stage, because they may require compiled
artifacts or heavier dependencies.
- `ARG CHECK_EPOCH` appears in **both** stages, because `ARG` is
stage-scoped: declaring it only in the lint stage leaves `make test`
frozen at the last cached result. In each stage the guard sits immediately
below the `ARG` so a bare `docker build .` fails instead of reusing the
empty cache key, and the value is expanded into the first check `RUN` so
the cache miss does not rely on BuildKit's unreferenced-`ARG` handling.
Both of those lines reference `$CHECK_EPOCH`, so both are value-keyed:
each stage is invalidated at two independent points. The later `RUN`s in
the same stage need no expansion of their own: they are already
invalidated by their busted parent layer.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `docker build .` on push. Since the Dockerfile already runs `make check`,
a successful build implies all checks pass.
runs `script/cibuild` (which runs
`docker build --build-arg CHECK_EPOCH="$epoch" .`) on push. The Dockerfile
runs `make check`, so a successful build implies all checks pass — but that
implication holds **only** because of the `CHECK_EPOCH` cache-bust described
above. Without it, an unchanged tree serves the check layer from cache and the
build reports a green it never earned. A bare `docker build .` fails closed by
design, on the `[ -n "$CHECK_EPOCH" ]` guard; always go through
`script/cibuild` or `script/docker`. Never accept a `script/cibuild` pass as
evidence without confirming it ran: a sub-second wall time, or `CACHED` on the
check layer, means nothing was executed.
- Use platform-standard formatters: `black` for Python, `prettier` for
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
@@ -69,9 +266,11 @@ style conventions are in separate documents:
Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown,
HTML, CSS) should also have `.prettierrc` and `.prettierignore`.
- Pre-commit hook: `make check` if local testing is possible, otherwise
`make lint && make fmt-check`. The Makefile should provide a `make hooks`
target to install the pre-commit hook.
- Pre-commit hook: runs `script/precommit`, which calls `script/check`. If local
testing is not possible in the repo, `script/precommit` may skip `script/test`
and run only `script/lint` and `script/fmt-check`. The hook is installed by
`script/install-precommit`; the Makefile must provide a `make hooks` target
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
@@ -82,6 +281,42 @@ style conventions are in separate documents:
- `make test` must complete in under 20 seconds. Add a 30-second timeout in the
Makefile.
- **`make test` should use the conditional verbose rerun pattern.** Run tests
without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to
show full output. This keeps CI logs and `docker build` output clean on
success (just package/suite summaries) while providing full diagnostic detail
on failure (every test case, every assertion). The general shell pattern:
```makefile
test:
@<test-command> || \
{ echo "--- Rerunning with -v for details ---"; \
<test-command-with-v>; exit 1; }
```
Go example:
```makefile
test:
@go test -timeout 30s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -timeout 30s -race -v ./...; exit 1; }
```
Python example:
```makefile
test:
@python -m pytest || \
{ echo "--- Rerunning with -v for details ---"; \
python -m pytest -v; exit 1; }
```
The `exit 1` ensures 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 check` must not modify any files in the repo. Tests may use temporary
@@ -98,6 +333,13 @@ style conventions are in separate documents:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
a new repo.
- **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 with `go get`, which
downloads code but does not execute code generation.
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
- Never force-push to `main`.
@@ -107,7 +349,199 @@ style conventions are in separate documents:
- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only
manually by the user. Fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`.
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`. The
canonical golangci-lint version is v2.12.2 (released 2026-05-06), installed
commit-pinned via
`go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5`.
- **`script/bootstrap` in Go repos must install the pinned golangci-lint
whenever the installed version does not match the pin — not merely when the
binary is absent — and must then verify the install took effect by
re-resolving the binary through `PATH`.** The presence test
`if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi` is wrong:
it tests `PATH` presence and never version, so on any already-provisioned
machine the pin is inert and a version bump is a no-op. Meanwhile the
Dockerfile installs unconditionally into a clean image, so CI and local
silently disagree about what the linter even is. Observed consequences: a
local `make check` green while `make docker` rejected the same commit with six
`goconst` findings, and a container linter surfacing thirteen findings the
host run missed. A stale host linter does not merely fail to prove the tree is
clean — it hides findings only the container can see. This is a deliberate
departure from the node handling described above, which uses whatever node is
installed: the linter version is the specific thing being held equal between
host and container, so for it, presence is not enough.
Comparing versions is necessary but **not sufficient**, because the obvious
fix also fails green. `go install` writes to `GOBIN` (or `GOPATH/bin`) while
callers resolve `golangci-lint` through `PATH`. If a different binary
shadows it earlier in `PATH`, the install genuinely succeeds and changes
nothing any caller will ever see: bootstrap prints success and the next
`make lint` still runs the stale linter. That is worse than no fix, because
it converts a known-stale toolchain into one everyone believes is pinned.
The canonical form, placed in `script/bootstrap` after Go itself is present:
```sh
# golangci-lint v2.12.2, 2026-05-06. GOLANGCI_LINT_VERSION must be exactly
# what `golangci-lint --version` prints for this ref; update both together.
GOLANGCI_LINT_VERSION="2.12.2"
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5"
# The version golangci-lint reports, resolved the way callers resolve it.
# Prints nothing when the binary is absent, exits non-zero, or prints
# something unparseable: all of those must read as "does not match".
# The capture is the whole version token, not just its numeric prefix.
# Stopping at the first `-` would make 2.12.2-rc1 compare equal to 2.12.2
# and skip the install, which is the defect this whole rule exists to close.
# The trailing `|| true` is required, not tidiness. Under `set -o pipefail`
# a non-zero --version would otherwise propagate out of the pipeline and
# kill the script through `set -e` before the diagnostic below is printed.
golangci_lint_version() {
command -v golangci-lint >/dev/null 2>&1 || return 0
golangci-lint --version 2>/dev/null | head -n 1 |
sed -n 's/.*has version v\{0,1\}\([0-9][^ ]*\).*/\1/p' || true
}
ensure_golangci_lint() {
if [ "$(golangci_lint_version)" = "$GOLANGCI_LINT_VERSION" ]; then
echo "bootstrap: golangci-lint $GOLANGCI_LINT_VERSION already installed"
return 0
fi
echo "bootstrap: installing golangci-lint $GOLANGCI_LINT_VERSION"
go install "$GOLANGCI_LINT_REF"
# go install writes to GOBIN (or GOPATH/bin); callers resolve through
# PATH. Re-resolve through PATH and assert the install took effect.
# `hash -r` is load-bearing: without it a shell that already resolved
# a stale golangci-lint answers from its own lookup cache, and this
# check false-fails with the shadowing message below.
hash -r 2>/dev/null || true
gcl_got="$(golangci_lint_version)"
if [ "$gcl_got" = "$GOLANGCI_LINT_VERSION" ]; then
echo "bootstrap: golangci-lint $GOLANGCI_LINT_VERSION installed," \
"and PATH resolves it"
return 0
fi
gcl_bin="$(go env GOBIN)"
[ -n "$gcl_bin" ] || gcl_bin="$(go env GOPATH)/bin"
# Strip a trailing slash: GOBIN=/x/ would otherwise make the
# "$gcl_bin"/* test below miss and misreport shadowing.
while :; do
case "$gcl_bin" in
*/) gcl_bin="${gcl_bin%/}" ;;
*) break ;;
esac
done
gcl_found="$(command -v golangci-lint 2>/dev/null || true)"
echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \
"$gcl_bin, but that is not what callers will get." >&2
case "$gcl_found" in
"")
echo "bootstrap: PATH resolves no golangci-lint at all." \
"Add $gcl_bin to PATH, then re-run bootstrap." >&2
;;
"$gcl_bin"/*)
echo "bootstrap: PATH resolves $gcl_found, inside that same" \
"directory, reporting version ${gcl_got:-unparseable}." \
"Nothing is shadowing it, so the install itself did not" \
"produce the pinned version: check that" \
"GOLANGCI_LINT_VERSION matches GOLANGCI_LINT_REF." >&2
;;
*)
echo "bootstrap: PATH resolves $gcl_found instead, reporting" \
"version ${gcl_got:-unparseable}. Remove that binary or" \
"put $gcl_bin earlier in PATH, then re-run bootstrap." >&2
;;
esac
exit 1
}
# The definitions above are inert on their own; the call site is part of
# the canonical form. In a script/bootstrap that follows the "define all
# functions, then call main" convention, this line belongs inside main()
# next to the other ensure_* steps.
ensure_golangci_lint
```
Four properties are load-bearing; each guards a failure mode that otherwise
fails green:
- **Compare the installed version against the pin**, never test presence.
This is what makes a version bump propagate to machines that already have
some golangci-lint. Compare the **whole** version token, exactly: a parser
that stops at the first `-` reports `2.12.2` for a host running
`2.12.2-rc1`, which compares equal to a `2.12.2` pin and skips the install
— the original defect, reintroduced through the comparison meant to fix
it.
- **After installing, re-resolve the binary the way callers resolve it** —
through `PATH`, not the path `go install` wrote to — and assert
`--version` reports the pin. When it does not, fail non-zero and name the
path `command -v` actually found, the version it reports, and the
directory the install wrote to. That is a condition a human has to fix by
hand, so bootstrap must not print success in it. Use `hash -r` first so
the shell does not answer from its own lookup cache. Diagnose the cause
from the resolved path rather than asserting one: only a path **outside**
the install directory is shadowing. When the resolved path is inside it,
nothing is shadowing and telling the operator to delete that binary or
reorder `PATH` sends them after a fault that does not exist.
- **A mis-parse must fall through to reinstall, never to a false match.**
Absent binary, non-zero exit, empty output, and unrecognised output all
yield an empty string, which compares unequal to the pin. The failure
direction is always a redundant install, never a skipped one.
- **Call it, and say so on success.** Two function definitions with no call
site are a silent no-op that reproduces the original defect exactly: exit
0, nothing installed, no output, stale linter still resolved. A success
path that prints nothing is byte-identical to that no-op — same exit
status, same empty output — so both success branches must print a
confirmation naming the version. In a change about undetectable no-ops,
"it printed nothing and exited 0" must not be the healthy signal.
Keep it POSIX sh: no bashisms, no arrays, no `[[`, no `grep -P`.
**On the hash-pinning rule.** `@c0d3ddc9cf3faa61a4e378e879ece580256d76e5` is
a commit hash, not a server-mutable version tag, and the go command verifies
the fetched module against the checksum database — the mechanism the
hash-pinning rule at the top of this document already names as acceptable
for Go modules. Note that `go install pkg@version` runs in module-aware mode
ignoring the `go.mod` in the current directory or any parent, so no repo
`go.sum` is consulted for this install; the checksum database is what
verifies it. The linter is a bootstrap prerequisite rather than part of any
repo's module graph, which is why the canonical form installs it by
commit-pinned ref instead of declaring it in `go.mod`. Whether a `go.mod`
tool dependency — which would pin the hash in a committed, reviewable file
instead — should replace this is an open decision, tracked at
[prompts#37](https://git.eeqj.de/sneak/prompts/issues/37).
**Keep `GOLANGCI_LINT_VERSION` and the ref in sync.** The ref is a hash and
carries no readable version, so the expected version is a separate string,
and it must be exactly what `--version` prints for that ref — the comparison
is an exact match on the whole version token. When the pinned commit carries
a release tag the go command resolves the hash to that tag, so the string is
simply the release number, `2.12.2` here. When it does not, the go command
falls back to a pseudo-version and the binary reports something like
`2.12.3-0.20260506110758-c0d3ddc9cf3f`; that compares exactly like any other
string, so it works, but it cannot be known without building the binary once
and reading `--version` off it. Prefer pins on tagged releases for that
reason — the expected string is then derivable from the ref — not because
the comparison cannot handle the alternative.
Because the comparison covers the whole token, a pre-release is never
confused with its release: a host carrying `2.12.2-rc1` against a `2.12.2`
pin compares unequal and gets reinstalled. This matters more than it looks,
because a pre-release tag is still a tag, so a rule requiring merely that
the pin be tagged would not catch it.
**Verifying a change to this logic requires a negative control run in an
environment where a shadowing binary exists earlier in `PATH` than the
install target.** Without that, the control passes against the naive
compare-then-install form as well and therefore proves nothing. Also check
the mis-parse direction by feeding it unparseable `--version` output and
confirming it reinstalls rather than reporting a match.
**Run those controls against the block as a consuming repo would adopt it**
— pasted into a `script/bootstrap`-shaped file that is then executed — not
by sourcing it and invoking the function yourself. Driving the function
directly tests something the artifact does not do, and it is exactly how a
missing call site passes every control while the adopted snippet does
nothing.
- When pinning images or packages by hash, add a comment above the reference
with the version and date (YYYY-MM-DD).
@@ -121,12 +555,76 @@ style conventions are in separate documents:
- 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) with `max-age` of at least one year
and `includeSubDomains`.
- `Content-Security-Policy` (CSP) with a restrictive default policy
(`default-src 'self'` as a baseline, tightened per-resource as
needed). Never use `unsafe-inline` or `unsafe-eval` unless
unavoidable, and document the reason.
- `X-Frame-Options: DENY` (or `SAMEORIGIN` if framing is required).
Prefer the `frame-ancestors` CSP directive as the primary control.
- `X-Content-Type-Options: nosniff`.
- `Referrer-Policy: strict-origin-when-cross-origin` (or stricter).
- `Permissions-Policy` restricting 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).
- `ReadTimeout` and `ReadHeaderTimeout` on the `http.Server` to defend
against slowloris attacks.
- `WriteTimeout` on the `http.Server`.
- `IdleTimeout` on the `http.Server`.
- Per-handler execution time limits via `context.WithTimeout` or
chi/stdlib `middleware.Timeout`.
- **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 `Authorization` header (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`, and `SameSite=Lax` (or
`Strict`) 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 trust `X-Forwarded-For` unconditionally.
- **CORS:**
- Authenticated endpoints must restrict `Access-Control-Allow-Origin` to
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
only for public, unauthenticated read-only APIs.
- **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 `DEBUG` is enabled.
- **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 `Secure` cookie flags must still be
set by the application so that the browser enforces HTTPS end-to-end.
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.
- `README.md` is 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](https://github.com/github/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
@@ -181,6 +679,9 @@ style conventions are in separate documents:
- `README.md`, `.git`, `.gitignore`, `.editorconfig`
- `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo)
- `Makefile`
- `script/` 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`

136
script/bootstrap Executable file
View File

@@ -0,0 +1,136 @@
#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. Node is
# used directly if installed; otherwise it is installed at a pinned
# version via nvm (installing nvm itself first, from a hash-verified
# release archive, never curl | sh).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-06
NODE_VERSION="22.17.0"
NVM_VERSION="0.40.3"
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
YARN_VERSION="1.22.22"
PKGMGR=""
SUDO=""
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
PKGMGR="nix"
elif command -v apt-get >/dev/null 2>&1; then
PKGMGR="apt"
elif command -v brew >/dev/null 2>&1; then
PKGMGR="brew"
elif command -v apk >/dev/null 2>&1; then
PKGMGR="apk"
else
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
exit 1
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" != "0" ]; then
SUDO="sudo"
fi
fi
}
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
}
missing() {
! command -v "$1" >/dev/null 2>&1
}
# verify_sha256 <file> <expected-hash>
verify_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$1" | cut -d' ' -f1)"
else
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
fi
if [ "$actual" != "$2" ]; then
echo "bootstrap: sha256 mismatch for $1" >&2
echo " expected: $2" >&2
echo " actual: $actual" >&2
exit 1
fi
}
# nvm is a bash script; run a command in a bash with nvm loaded
nvm_sh() {
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
}
ensure_nvm() {
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
# nvm prerequisites; nvm itself requires bash
if missing bash; then pkg_install bash bash bash bash; fi
if missing curl; then pkg_install curl curl curl curl; fi
if missing git; then pkg_install git git git git; fi
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/nvm.tar.gz" \
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
mkdir -p "$HOME/.nvm"
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
rm -rf "$tmp"
}
ensure_node() {
if ! missing node; then return 0; fi
ensure_nvm
nvm_sh "nvm install $NODE_VERSION"
}
ensure_yarn() {
if ! missing yarn; then return 0; fi
if ! missing corepack; then
corepack enable
corepack prepare "yarn@$YARN_VERSION" --activate
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
corepack prepare yarn@$YARN_VERSION --activate"
else
npm install -g "yarn@$YARN_VERSION"
fi
}
install_js_deps() {
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
yarn install --frozen-lockfile"
else
yarn install --frozen-lockfile
fi
}
main() {
cd "$ROOT"
if missing make; then pkg_install gnumake make make make; fi
if missing git; then pkg_install git git git git; fi
ensure_node
ensure_yarn
install_js_deps
echo "bootstrap complete"
}
main "$@"

14
script/check Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# script/check: run all checks (test, lint, fmt-check). Our own
# extension to scripts-to-rule-them-all. Must not modify any files.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/test"
"$SCRIPT_DIR/lint"
"$SCRIPT_DIR/fmt-check"
}
main "$@"

20
script/cibuild Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs script/check, but
# that only proves anything because CHECK_EPOCH is a fresh nonce on every
# invocation: without it Docker serves the check layer from cache on an
# unchanged tree and the build exits 0 without running the suite.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
# Assign on its own line: a failing command substitution inside an
# argument does not trip `set -e`, which would silently degrade the
# nonce to an empty constant. `$$` is required because busybox `date`
# drops %N without erroring.
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" .
}
main "$@"

23
script/docker Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
# script/docker: build the Docker image tagged with the project name.
# Identical in all repos; the tag comes from script/projectname. The
# Dockerfile's checks only actually run because CHECK_EPOCH is a fresh
# nonce on every invocation; without it a warm cache turns this into a
# green that proves nothing.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
# Assign on its own line: a failing command substitution inside an
# argument does not trip `set -e`, which would silently degrade the
# nonce to an empty constant. `$$` is required because busybox `date`
# drops %N without erroring.
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" \
-t "$("$SCRIPT_DIR/projectname")" .
}
main "$@"

12
script/fmt Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/fmt: format all files (writes).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
}
main "$@"

12
script/fmt-check Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/fmt-check: check formatting (read-only).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
}
main "$@"

16
script/install-precommit Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# script/install-precommit: install the git pre-commit hook that runs
# script/precommit. Our own extension to scripts-to-rule-them-all.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
hook=".git/hooks/pre-commit"
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "pre-commit hook installed: runs script/precommit"
}
main "$@"

13
script/lint Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# script/lint: run the linter.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
echo "Linting markdown files..."
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
}
main "$@"

12
script/precommit Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/precommit: run by the git pre-commit hook; fails the commit if
# checks fail. Our own extension to scripts-to-rule-them-all.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/check"
}
main "$@"

12
script/projectname Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/projectname: output the name of this project. Our own
# extension to scripts-to-rule-them-all. Other scripts that need the
# name (e.g. script/docker) call this, so they can stay identical
# across all repos.
set -eu
main() {
echo "prompts"
}
main "$@"

13
script/setup Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# script/setup: set up the repo for development after a fresh clone:
# installs dependencies and the git pre-commit hook.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/install-precommit"
}
main "$@"

12
script/test Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/test: run the test suite.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
echo "No tests defined."
}
main "$@"