4 Commits

Author SHA1 Message Date
bb19029f53 Migrate canonical .golangci.yml to gomodguard_v2 with a block list
All checks were successful
check / check (push) Successful in 9s
golangci-lint v2.12.0 deprecated gomodguard, and `linters.default: all`
enables it, so every lint run in every consuming Go repo prints a
deprecation warning. Disable the old name, which is what silences the
warning; name gomodguard_v2 in linters.enable so the settings block has
a visible owner; give it a blocked module list.

The block list restates only decisions already recorded in the Go
package defaults: zerolog, the pre-fork go-redis/redis, sergi/go-diff
and hexops/gotextdiff. Recorded rejections that repos vendoring this
file still require are deliberately absent, so a re-vendor cannot
redden a repo that has done nothing wrong.
2026-08-31 01:54:40 +00:00
f77d785bed Scope the .golangci.yml agent prohibition to vendored copies (#49)
All checks were successful
check / check (push) Successful in 9s
SPECULATIVE and ahead of your ruling — nothing here is urgent and closing it costs nothing. One sentence of policy prose changed; no config file is touched.

## The contradiction

`REPO_POLICIES.md` line 266 currently reads:

> `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only manually by the user.

Stated unqualified, that forbids an agent from modifying `.golangci.yml` **anywhere** — including the canonical copy in this repo, which is the only place it can ever be fixed. An agent that wants to remediate a linter problem must either violate the rule or leave the problem standing. A rule that cannot be complied with and satisfied at the same time gets resolved ad hoc, differently by each reader, which is the worst of both outcomes it was trying to produce.

This is not hypothetical. It has already cost real time:

- The `gomodguard` deprecation (#25) has been open since 2026-08-07 and still prints on every lint run in every consuming Go repo.
- One agent read the rule as binding here and **declined to open even a speculative branch**, so the fix was not written at all on that pass.
- #47 exists only because a later request was explicit enough to override the reading, and its lead comment asks for exactly this ruling before the PR itself can be judged on its merits.
- The same warning is refiled downstream as sneak/homoicon#4, where it is correctly marked owner-only and correctly punted upstream.

Each new agent that meets the rule reruns this whole argument.

## The change

Scope the prohibition to the vendored copy, and name the one legitimate path by which the config can change:

```
- `.golangci.yml` is standardized. The vendored copy in a consuming repo must
  _NEVER_ be modified by an agent: fetch it from
  `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it
  byte-identical, so that no repo can quietly loosen its own linting. Linter
  configuration changes are made to the canonical copy in the `prompts` repo and
  reach consuming repos by re-vendoring; an agent may open a PR against
  canonical, which only the user merges.
```

The version pin sentence that followed is unchanged.

This keeps the property the rule exists for — no repo silently weakens its own linting, and divergence from canonical stays detectable — while removing the reading that freezes canonical itself. Your control is not reduced: an agent may open a PR here, and only you merge it.

## Scope of the wording sweep

I grepped every `.md` in the repo for the absolute phrasing. It appears in **exactly one place**, `prompts/REPO_POLICIES.md` lines 266-267.

`EXISTING_REPO_CHECKLIST.md` (line 39) and `NEW_REPO_CHECKLIST.md` (line 63) both mention `.golangci.yml`, but only as "fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`" — an instruction to vendor canonical verbatim, which is exactly what the scoped rule says. Neither carries a prohibition, so neither needs changing and neither is left contradicting the other. `REPO_POLICIES.md` line 414 lists `.golangci.yml` as a required file, also unaffected.

Note that the repo-root `REPO_POLICIES.md` is a symlink to `prompts/REPO_POLICIES.md`, so the single edit covers both paths.

## Deliberately NOT included

This PR does **not** change `.golangci.yml`. The `gomodguard` fix stays in #47 so the two can be judged separately — the policy question is worth settling on its own terms regardless of what you decide about that config change, and merging them would collapse two decisions into one.

## Unrelated observation, for the record

While verifying #47 against a scratch `sneak/homoicon` clone, I found that homoicon's vendored `.golangci.yml` is sha256 `391ea68e637432980f1db0776076f51578fa58193bbd17f4b40ad725975e21f8`, while canonical `main` is `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`. The whole difference is a three-line comment recording a one-time agent edit you authorized on 2026-08-07; the config is functionally identical.

That matters only if you ever want a hash-based drift guard against canonical, which #25 floats as an offline alternative to `golangci-lint config verify`: such a guard would already report homoicon as drifted on day one. Worth knowing before building one. No change proposed here.

## Validation

`make check` passes (`prettier --check '**/*.md' --tab-width 4 --prose-wrap always`: all matched files clean). `make fmt` produced no further changes. `last_modified` in the front matter updated to 2026-08-19 per this file's own rule.

Co-authored-by: sneak <sneak@sneak.berlin>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #49
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-30 06:26:19 +02:00
3f8201d532 Require thin cmd/ entrypoints: all logic in internal/ or pkg/ (#54)
All checks were successful
check / check (push) Successful in 13s
Codifies your ruling (2026-08-30, filed as homoicon issue 555): no project logic outside `internal/` or `pkg/`; each `cmd/<name>/` is a single `main.go` whose body is one call into library code.

- `CODE_STYLEGUIDE_GO.md`: strengthens the "keep `main` small" rule to the single-call form and adds the no-logic-outside-`internal/`-or-`pkg/` rule.
- `REPO_POLICIES.md`: annotates `cmd/` in the canonical subdirectory list accordingly. (Root copy is a symlink; one edit covers both.)

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #54
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-30 04:20:42 +02:00
a8686891ba Raise org-wide make test cap to 60s, backstop timeout to 90s (#42)
All checks were successful
check / check (push) Successful in 4s
## The question this answers

Is the 60-second test-time cap the new **org-wide** ceiling, or an approved
**dnswatcher-only** divergence?

- Question: #41
- Origin: sneak/dnswatcher#93

This PR implements **org-wide**.

## The ruling

On sneak/dnswatcher#93 (comment) (2026-08-09),
verbatim:

&gt; make the cap 60s in both and never use mocking, always use live resolvers and
&gt; assume the build and run environments have full unmodified unrestricted
&gt; internet access. it is ok if they fail due to a bad build environment that
&gt; alters dns packets.

And on #41 (comment), disambiguating
the scope:

&gt; org wide. the hard cap is 60 for ci/green, but over 20s should be filed as an
&gt; improvement bug.

That second comment landed after this work was started, and it confirms the
option implemented here. The two-tier shape it describes (60s hard, 20s target,
overage filed as a bug) is encoded in the policy text.

## What changed, and the number chosen

The backstop moves from `30s` to **`90s`**.

The old pairing was incoherent under the new cap: a 60-second ceiling with a
30-second `-timeout` means the timeout kills the suite long before the ceiling
is reached, so the ceiling would never be the thing that fails. The backstop has
to sit above the cap, where it does its actual job of catching a hung test
rather than a merely slow one. `90s` preserves the 1.5x backstop-to-cap ratio
the old `20s`/`30s` pair already had, so the relationship between the two
numbers is unchanged and only the scale moves.

Every place a number changed:

| File | What |
| --- | --- |
| `prompts/REPO_POLICIES.md` | Prose ceiling: `20 seconds` to `60 seconds`, plus the new 20s target / improvement-bug tier |
| `prompts/REPO_POLICIES.md` | Backstop prose: `30-second timeout` to `90-second timeout`, with the rationale for why it exceeds the cap |
| `prompts/REPO_POLICIES.md` | Go example Makefile snippet, first run: `go test -timeout 30s` to `-timeout 90s` |
| `prompts/REPO_POLICIES.md` | Go example Makefile snippet, verbose rerun: `go test -timeout 30s` to `-timeout 90s` |
| `prompts/EXISTING_REPO_CHECKLIST.md` | `make test` has a `30-second` timeout, to `90-second` timeout plus the 60s hard cap and the 20s filing rule |
| `prompts/NEW_REPO_CHECKLIST.md` | `script/test` / `make test` entrypoint line: `30-second timeout` to `90-second timeout, 60-second hard cap on wall time` |

I swept the whole repo for `20 second`, `30-second`, `20s`, `30s`, `timeout 20`,
`timeout 30`, and `under 20`/`under 30`. After this change the only remaining
occurrences of `20` in a test-timing context are the two intentional references
to the new 20-second target. The other `timeout` hits in the repo are unrelated
(`.golangci.yml` lint timeout, HTTP server `ReadTimeout`/`WriteTimeout` examples,
`middleware.Timeout`) and were left alone.

One deliberate non-change: the Python example Makefile snippet in
`prompts/REPO_POLICIES.md` carries no timeout flag today and still carries none.
`pytest` has no built-in timeout, so adding one would mean mandating the
`pytest-timeout` plugin org-wide, which is a new dependency requirement rather
than a renumbering, and outside what was ruled on. It is a pre-existing gap
between the prose and that snippet, not one this PR introduces. Happy to file it
separately or add `--timeout=90` here if you want it in scope.

## The alternative that was not implemented

**Keep canonical at 20s and let `sneak/dnswatcher` carry a documented per-repo
divergence.** That was the recommendation originally written up in
#41, on the reasoning that the 20s
ceiling is doing real work in repos with fast deterministic suites and only
dnswatcher needs the headroom.

Org-wide was chosen instead for two reasons. First, the pressure is not specific
to DNS: any repo whose tests exercise real infrastructure over the network
inherits the same variance, and there is no principled line that admits
dnswatcher and excludes the next such repo. Second, and more decisively,
`REPO_POLICIES.md` is a vendored file. A sanctioned per-repo divergence in a
vendored file is indistinguishable, on inspection, from a stale vendored copy:
the next re-vendoring silently reverts the divergence, and nobody reading a
consuming repo can tell whether the number they are looking at is an intentional
exception or drift. That is the bidirectional-drift problem already tracked in
#31. The two-tier cap gets the same
outcome without the drift, since a fast repo that regresses from 4s to 45s still
generates an improvement bug.

## Status

This PR was opened speculatively, ahead of a decision, on the standing "open it
rather than wait" instruction. The scope question has since been answered
org-wide in the issue, but the specific backstop value of `90s` and the two-tier
wording are still my proposals rather than anything ruled on, so closing this or
sending it back for a different number is a perfectly fine outcome.

`make check` passes; `make fmt` was run and the result is included (it was a
no-op, the edits were already prettier-conformant).

`sneak/dnswatcher` is landing the matching 60s edit to its vendored copy in
parallel, and will match whichever way this is decided.

Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #42
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-10 16:13:31 +02:00
6 changed files with 69 additions and 20 deletions

View File

@@ -10,15 +10,21 @@ run:
linters: linters:
default: all default: all
enable:
# Successor to the deprecated gomodguard. Named explicitly, rather than
# left to `default: all`, because it carries the module policy below.
- gomodguard_v2
disable: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields - exhaustruct # Requires all struct fields
- depguard # Dependency allow/block lists - depguard # Dependency allow/block lists
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
# Deprecated: the warning is attached to the old name, so it is
# silenced by disabling that name, not by enabling the successor.
- wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2
settings: settings:
lll: lll:
line-length: 88 line-length: 88
@@ -29,6 +35,27 @@ linters:
max-complexity: 15 max-complexity: 15
dupl: dupl:
threshold: 100 threshold: 100
# Only decisions already recorded in the Go package defaults are
# listed here. Entries match the module path exactly (the default
# match-type), so a differently-versioned module path is unaffected.
gomodguard_v2:
blocked:
- module: github.com/rs/zerolog
recommendations:
- log/slog
reason: "Structured logging is stdlib log/slog."
- module: github.com/go-redis/redis
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/sergi/go-diff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "No unified diff output; use go-udiff."
- module: github.com/hexops/gotextdiff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "Unmaintained fork; use go-udiff."
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0

View File

@@ -21,6 +21,11 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-08-31: Migrated the canonical `.golangci.yml` from the deprecated
`gomodguard` to `gomodguard_v2`: the old linter is disabled by name (which is
what silences the deprecation warning), the successor is named explicitly in
`linters.enable`, and it carries a `blocked` module list drawn only from
decisions already recorded in the Go package defaults.
- 2026-08-07: Set the canonical `.golangci.yml` to the org-standard v2-schema - 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 config already deployed byte-identical across the org's Go repos (settings
under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually

View File

@@ -124,10 +124,15 @@ last_modified: 2026-03-18
1. Keep the `main()` function as small as possible. 1. Keep the `main()` function as small as possible.
1. Keep the `main` package as small as possible. Move as much code as is 1. Keep the `main` package as small as possible. Each `cmd/<name>/` directory
feasible to a library package, even if it's an internal one. `main` is just contains a single `main.go` whose body is one call into library code (for
an entrypoint to your code, not a place for implementations. Exception: example `os.Exit(cli.Main())` calling `internal/cli`). All CLI logic — flag
single-file scripts. parsing, subcommand dispatch, argument handling, output formatting — lives
in `internal/` or `pkg/`, not in `cmd/`. `main` is just an entrypoint to
your code, not a place for implementations. Exception: single-file scripts.
1. No project logic outside `internal/` or `pkg/`. Anything in `cmd/` is a thin
entrypoint only.
1. HTTP HandleFuncs should be returned from methods or functions that need to 1. HTTP HandleFuncs should be returned from methods or functions that need to
handle HTTP requests. Don't use methods or your top level functions as handle HTTP requests. Don't use methods or your top level functions as

View File

@@ -59,7 +59,9 @@ with your task.
- [ ] README has an **Entrypoints** section documenting the `script/` - [ ] README has an **Entrypoints** section documenting the `script/`
entrypoints and linking the standard entrypoints and linking the standard
- [ ] `make check` does not modify any files in the repo - [ ] `make check` does not modify any files in the repo
- [ ] `make test` has a 30-second timeout - [ ] `make test` has a 90-second timeout and completes within the 60-second
hard cap (over 20 seconds is green but must be filed as an improvement
bug)
- [ ] `make test` runs real tests, not a no-op (at minimum, import/compile - [ ] `make test` runs real tests, not a no-op (at minimum, import/compile
check) check)
- [ ] `make check` passes on current branch - [ ] `make check` passes on current branch

View File

@@ -80,8 +80,8 @@ are thin shims calling them. Model scripts:
installs installs
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`, - [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` — runs real tests, not a no-op (30-second - [ ] `script/test` / `make test` — runs real tests, not a no-op (90-second
timeout) timeout, 60-second hard cap on wall time)
- [ ] `script/lint` / `make lint` — runs linter - [ ] `script/lint` / `make lint` — runs linter
- [ ] `script/fmt` / `make fmt` — formats code (writes) - [ ] `script/fmt` / `make fmt` — formats code (writes)
- [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only) - [ ] `script/fmt-check` / `make fmt-check` — checks formatting (read-only)

View File

@@ -1,6 +1,6 @@
--- ---
title: Repository Policies title: Repository Policies
last_modified: 2026-08-07 last_modified: 2026-08-19
--- ---
This document covers repository structure, tooling, and workflow standards. Code This document covers repository structure, tooling, and workflow standards. Code
@@ -189,8 +189,13 @@ style conventions are in separate documents:
module under test to verify it compiles/parses. There is no excuse for module under test to verify it compiles/parses. There is no excuse for
`make test` to be a no-op. `make test` to be a no-op.
- `make test` must complete in under 20 seconds. Add a 30-second timeout in the - `make test` must complete in under 60 seconds. That is the hard cap, and a
Makefile. suite that exceeds it fails. Under 20 seconds is the target. A suite between
20 and 60 seconds is still green, but the overage must be filed as an
improvement bug against that repo. Add a 90-second timeout to the test
invocation in the Makefile (`go test -timeout 90s`). The backstop deliberately
sits above the hard cap so that it catches a genuinely hung test rather than a
merely slow one.
- **`make test` should use the conditional verbose rerun pattern.** Run tests - **`make test` should use the conditional verbose rerun pattern.** Run tests
without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to
@@ -209,9 +214,9 @@ style conventions are in separate documents:
```makefile ```makefile
test: test:
@go test -timeout 30s -race -cover ./... || \ @go test -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \ { echo "--- Rerunning with -v for details ---"; \
go test -timeout 30s -race -v ./...; exit 1; } go test -timeout 90s -race -v ./...; exit 1; }
``` ```
Python example: Python example:
@@ -258,11 +263,14 @@ style conventions are in separate documents:
- Make all changes on a feature branch. You can do whatever you want on a - Make all changes on a feature branch. You can do whatever you want on a
feature branch. feature branch.
- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only - `.golangci.yml` is standardized. The vendored copy in a consuming repo must
manually by the user. Fetch from _NEVER_ be modified by an agent: fetch it from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`. The `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it
canonical golangci-lint version is v2.12.2 (released 2026-05-06), installed byte-identical, so that no repo can quietly loosen its own linting. Linter
commit-pinned via configuration changes are made to the canonical copy in the `prompts` repo and
reach consuming repos by re-vendoring; an agent may open a PR against
canonical, which only the user merges. 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`. `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5`.
- When pinning images or packages by hash, add a comment above the reference - When pinning images or packages by hash, add a comment above the reference
@@ -382,7 +390,9 @@ style conventions are in separate documents:
language-specific config). Everything else goes in a subdirectory. Canonical language-specific config). Everything else goes in a subdirectory. Canonical
subdirectory names: subdirectory names:
- `bin/` — executable scripts and tools - `bin/` — executable scripts and tools
- `cmd/` — Go command entrypoints - `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose
body is a single call into `internal/` or `pkg/`, no project logic in
`cmd/`
- `configs/` — configuration templates and examples - `configs/` — configuration templates and examples
- `deploy/` — deployment manifests (k8s, compose, terraform) - `deploy/` — deployment manifests (k8s, compose, terraform)
- `docs/` — documentation and markdown (README.md stays in root) - `docs/` — documentation and markdown (README.md stays in root)