From 155926b399c983334a937c99c8c023d56735bed7 Mon Sep 17 00:00:00 2001 From: Manu Lorente Date: Mon, 18 May 2026 13:20:08 -0600 Subject: [PATCH 1/2] chore: refresh CLAUDE.md project structure and pin golangci-lint Two related DX papercuts surfaced by the 2026-05-18 multi-dim audit: 1. CLAUDE.md "Tech Stack" claimed `main.go ~785 lines` and "single-file" architecture. After ARCH-002 (PR #14), main.go is ~270 lines of orchestration plus four `internal/` packages (config, proxy, health, telemetry). The "Key Paths" table listed only main_test.go and main_integration_test.go, missing every internal/ package and the specs/ + scripts/tests/ trees added since. New contributors landing on the wrong file wastes onboarding time; tooling (claude-mem, vault refs, ADRs in the doc itself) was the only source of truth. Stale memory is worse than no memory. 2. .github/workflows/ci.yml pinned golangci-lint to `version: latest`, so contributors running `make lint` locally with whatever they installed could see green while CI sees red (or vice versa). Pinned to v1.62.2 in CI and documented the same install command in CLAUDE.md's Commands block so the two stay in lockstep. CLAUDE.md also gains explicit references to ADR-006 (Dialer) and ADR-007 (multi-package split) in the Architecture Decisions section, making the layout discoverable from the project memory file. Audit reference: 40-runbooks/audit-2026-05-18-multi-dim.md (Major DX-1 and DX-2 entries). --- .github/workflows/ci.yml | 4 +++- CLAUDE.md | 29 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1889f54..ac1fb15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,9 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: - version: latest + # Pinned so local `golangci-lint run` and CI report identical findings. + # Bump deliberately when the project upgrades; do not silently track latest. + version: v1.62.2 install-mode: goinstall security: diff --git a/CLAUDE.md b/CLAUDE.md index 989bb32..a8a2ea9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Portable TCP bridge over Tailscale/Headscale mesh networks using tsnet. - **Language:** Go 1.25+ - **Key dependency:** `tailscale.com/tsnet` v1.80.0 -- **Architecture:** Single-binary, single-file (`main.go` ~785 lines) +- **Architecture:** Single binary, multi-package — `main.go` (~270 lines orchestrator) + four `internal/` packages (see ADR-007). - **Config:** Environment variables only (no config files) — see `.env.example` - **Logging:** `log/slog` (structured, text or JSON) - **Metrics:** `sync/atomic` counters, JSON endpoint at `/metrics` @@ -15,14 +15,20 @@ Portable TCP bridge over Tailscale/Headscale mesh networks using tsnet. | Path | Purpose | |------|---------| -| `main.go` | All application code | -| `main_test.go` | Unit tests | -| `main_integration_test.go` | Integration tests (loopback, no tsnet) | -| `.env.example` | Configuration reference (2 required vars) | -| `scripts/client/` | Client launchers (run.sh, run.ps1, bootstrap) | -| `scripts/host/` | Host setup (setup.ps1, ts-bridge.service) | -| `.github/workflows/ci.yml` | CI pipeline (test, lint, security, shellcheck, build-matrix) | -| `.github/workflows/release.yml` | Automated releases via release-please | +| `main.go` | Orchestrator: flags, logger, signals, init, drain. Thin glue, no business logic. | +| `main_test.go` | Unit tests for main-package helpers (error diagnosis, etc.) | +| `main_integration_test.go` | Integration tests with mock Dialer (loopback, no tsnet) | +| `internal/config/` | Env-var parsing + `Config` struct + validation | +| `internal/proxy/` | `Dialer` interface, `AcceptLoop`, `handleConn`, `proxyConnections`, `idleConn`, `ReconnectDialer` | +| `internal/health/` | `/health/live`, `/health/ready`, `/metrics` HTTP server | +| `internal/telemetry/` | Atomic counters + read accessors | +| `specs/` (and `specs/archive/`) | Per-feature SDD folders (proposal + tasks + verification) — see §Workflow Rules | +| `.env.example` | Configuration reference (2 required vars + commented optionals) | +| `scripts/client/` | Client launchers (`run.sh`, `run.ps1`, `bootstrap.{sh,ps1}`) | +| `scripts/host/` | Host setup (`setup.ps1`, `ts-bridge.service`) | +| `scripts/tests/` | BATS tests for the launchers | +| `.github/workflows/ci.yml` | CI: test, lint, security (gosec), shellcheck, bats, build-matrix | +| `.github/workflows/release.yml` | Automated releases via release-please (PAT-driven) | ## Commands @@ -33,7 +39,8 @@ go build -o ts-bridge . # Test (always use race detector) go test -race -v ./... -# Lint +# Lint (CI pins this version — run the same locally to avoid drift) +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 golangci-lint run # Security scan @@ -47,6 +54,8 @@ gosec ./... - **ADR-002:** Single binary, no config files, env-var driven - **ADR-004:** Atomic metrics, no mutexes +- **ADR-006:** `Dialer` interface for testability +- **ADR-007:** Multi-package split under `internal/` (this is the current layout — see Key Paths) - Full ADR index in vault: `knowledge/10_projects/ts-bridge/30-architecture/` ## Workflow Rules (read before first tool call) From f820f3e74c56bc20d772d6aff821c1c5dab2a087 Mon Sep 17 00:00:00 2001 From: Manu Lorente Date: Mon, 18 May 2026 13:30:49 -0600 Subject: [PATCH 2/2] fix: simplify isPermanentDialError final return (gosec gosimple S1008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pinning golangci-lint to v1.62.2 in this PR activated gosimple S1008, which flagged the final 'if ... { return true }; return false' in isPermanentDialError. Collapsed to a direct return of the bool expression — semantically identical, lints clean. The block survived the previous 'latest' golangci-lint runs because that version had different rule defaults. Pinning catches the drift, exactly as intended. --- internal/proxy/reconnect.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/proxy/reconnect.go b/internal/proxy/reconnect.go index 548b559..eefe715 100644 --- a/internal/proxy/reconnect.go +++ b/internal/proxy/reconnect.go @@ -121,13 +121,8 @@ func isPermanentDialError(err error) bool { return true } - msg := err.Error() // tsnet emits this string when the backend is in a terminal state // (Stopped, NeedsMachineAuth). Source: tsnet.Server.awaitRunning at // tailscale.com/tsnet@v1.80.0/tsnet.go:203. - if strings.HasPrefix(msg, "tsnet: backend in state ") { - return true - } - - return false + return strings.HasPrefix(err.Error(), "tsnet: backend in state ") }