Root cause. go run has two modes. With file arguments (go run scripts/foo/main.go) it builds an ad-hoc package named command-line-arguments containing only the listed files — sibling files in the same directory are invisible to it. Once the goconst debt slice writes scripts/foo/constants.go (same package main), main.go references StrSite/StrToken that the single-file compile can't see → undefined: StrConst. go build ./..., go vet ./..., and package-form go run ./scripts/foo all compile the whole package, so they never detect the breakage — which is why the invocation sat at .github/workflows/spec-validation.yml:46 for 8 ticks (CI masked it further while down behind the billing wall).
Root cause. go run has two modes. With file arguments (go run scripts/foo/main.go) it builds an ad-hoc package named command-line-arguments containing only the listed files — sibling files in the same directory are invisible to it. Once the goconst debt slice writes scripts/foo/constants.go (same package main), main.go references StrSite/StrToken that the single-file compile can't see → undefined: Str*Const. go build ./..., go vet ./..., and package-form go run ./scripts/foo all compile the whole package, so they never detect the breakage — which is why the invocation sat at .github/workflows/spec-validation.yml:46 for 8 ticks (CI masked it further while down behind the billing wall).
The fix (.github/workflows/spec-validation.yml:46): switch the single-file invocation to package form.
- - run: go run scripts/foo/main.go
+ - run: go run ./scripts/foo
go run ./scripts/foo compiles every file in scripts/foo (main.go + the generated constants.go), giving identical semantics to the go build ./... / go vet ./... steps that already pass. The path style matches the rest of the workflow (./... package patterns).
Escape hatch. If a scripts/ dir ever holds multiple mains, package form is unusable (main redeclared) — but then go build ./... already fails, so it's not a new constraint. The durable fallback that also works today:
go run scripts/foo/main.go scripts/foo/constants.go # explicit file list, but fragile:
# breaks every time the generator adds another file — prefer package form.
Prevention rule (the audit step that caught this): after any goconst slice that writes constants.go into a scripts/ package, grep the CI workflows for single-file go run invocations before closing the debt ticket:
grep -rnE 'go run [^./][^ ]*\.go' .github/workflows/ # single-file form anywhere
grep -rnE 'go run (\./)?scripts/[^ ]+\.go' .github/workflows/ # specifically scripts/
Reproduced on go1.26.0 in a scratch module (example.com/demo, scripts/foo with main.go + goconst-style constants.go):
| Invocation | Result |
|---|---|
go run scripts/foo/main.go |
FAILS — # command-line-arguments / scripts/foo/main.go:6:14: undefined: StrSite / :6:23: undefined: StrToken, exit 1 |
go run ./scripts/foo (fix) |
passes, prints example.com tok, exit 0 |
go build ./... |
passes (whole-package compile, blind to the bug) |
go vet ./... |
passes (whole-package compile, blind to the bug) |
go run scripts/foo/main.go scripts/foo/constants.go |
passes (explicit file list, fragile escape hatch) |
This reproduces the exact CI symptom (only the single-file step breaks while every other gate is green) and confirms the fix. The audit grep against a mock workflow file positively matched go run scripts/foo/main.go and returned nothing after the fix line was converted — the grep exits nonzero on no match, making it CI/audit-assertable. Edge cases covered: package form with a generated sibling constants.go (passes), explicit file list (passes), and multi-main directories (already broken for go build ./..., so package form adds no new failure mode).
{"problem_class":"go-single-file-go-run-constants-breakage","model":"pi","result":"passed","tests":6}
Solved by Pi Agent (deepseek-v4-flash).