← back to Cli Printing Press
fix(cli): add scoped govulncheck gates (#699)
ac75b83ccb7804433579d163d0798774c1c7051b · 2026-05-07 22:51:53 -0700 · Trevin Chow
Files touched
M AGENTS.mdM README.mdM docs/GLOSSARY.mdM docs/PIPELINE.mdM go.modM internal/cli/publish.goM internal/cli/publish_test.goM internal/cli/verify_skill.goM internal/cli/verify_skill_test.goM internal/generator/generator_test.goM internal/generator/install_section.goM internal/generator/install_section_test.goM internal/generator/templates/go.mod.tmplM internal/generator/templates/readme.md.tmplM internal/generator/templates/skill.md.tmplM internal/generator/validate.goM internal/generator/validate_test.goA internal/govulncheck/govulncheck.goA internal/govulncheck/govulncheck_test.goM internal/pipeline/fullrun.goM internal/pipeline/fullrun_test.goM internal/pipeline/planner.goM internal/pipeline/planner_artifacts_test.goM internal/pipeline/seeds.goM internal/pipeline/seeds_test.goM skills/printing-press-catalog/SKILL.mdM skills/printing-press-publish/SKILL.mdM skills/printing-press-score/SKILL.mdM skills/printing-press/SKILL.mdM skills/printing-press/references/setup-checks.mdM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.mdM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/README.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/go.modM testdata/golden/expected/generate-public-param-names/public-param-golden/README.mdM testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
Diff
commit ac75b83ccb7804433579d163d0798774c1c7051b
Author: Trevin Chow <trevin@trevinchow.com>
Date: Thu May 7 22:51:53 2026 -0700
fix(cli): add scoped govulncheck gates (#699)
---
AGENTS.md | 3 +-
README.md | 14 ++-----
docs/GLOSSARY.md | 2 +-
docs/PIPELINE.md | 4 +-
go.mod | 4 +-
internal/cli/publish.go | 40 +++++++++++++++-----
internal/cli/publish_test.go | 43 +++++++++++++++++++++-
internal/cli/verify_skill.go | 18 +--------
internal/cli/verify_skill_test.go | 18 ++++-----
internal/generator/generator_test.go | 6 +--
internal/generator/install_section.go | 19 +++-------
internal/generator/install_section_test.go | 6 +--
internal/generator/templates/go.mod.tmpl | 4 +-
internal/generator/templates/readme.md.tmpl | 2 +-
internal/generator/templates/skill.md.tmpl | 2 +-
internal/generator/validate.go | 8 ++++
internal/generator/validate_test.go | 38 ++++++++++++++++++-
internal/govulncheck/govulncheck.go | 14 +++++++
internal/govulncheck/govulncheck_test.go | 21 +++++++++++
internal/pipeline/fullrun.go | 8 ++--
internal/pipeline/fullrun_test.go | 20 +++++++++-
internal/pipeline/planner.go | 2 +-
internal/pipeline/planner_artifacts_test.go | 2 +
internal/pipeline/seeds.go | 4 +-
internal/pipeline/seeds_test.go | 5 +++
skills/printing-press-catalog/SKILL.md | 2 +-
skills/printing-press-publish/SKILL.md | 20 +++++++++-
skills/printing-press-score/SKILL.md | 2 +-
skills/printing-press/SKILL.md | 21 +++++------
skills/printing-press/references/setup-checks.md | 6 +--
.../printing-press-rich-auth/README.md | 2 +-
.../printing-press-rich-auth/SKILL.md | 2 +-
.../printing-press-golden/README.md | 2 +-
.../printing-press-golden/SKILL.md | 2 +-
.../printing-press-golden/go.mod | 2 +-
.../public-param-golden/README.md | 2 +-
.../public-param-golden/SKILL.md | 2 +-
37 files changed, 259 insertions(+), 113 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index c27ea8f5..3edabac0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -147,7 +147,8 @@ Add tests for new non-trivial logic. Match the package's existing style (typical
Run `go test ./...` before considering your work done.
## Quality Gates
-Generated CLIs must pass 7 gates: `go mod tidy`, `go vet`, `go build`, binary build, `--help`, `version`, and `doctor`.
+Generated CLIs must pass 8 gates: `go mod tidy`, `govulncheck`, `go vet`, `go build`, binary build, `--help`, `version`, and `doctor`.
+Run `govulncheck` in default mode only, scoped to the generated or publishing CLI module (`./...` from that CLI directory). Do not use `-show verbose` or a whole public-library scan as a blocking gate; the public library is a historical collection, so its blocking CI should scan only added or changed CLI modules and leave whole-library sweeps to scheduled/reporting workflows.
## Local Artifacts
Generated artifacts live under `~/printing-press/`, not in this repo: `library/<api-slug>/`, `manuscripts/<api-slug>/`, and `.runstate/<scope>/`. The API slug is derived by the generator from the spec title (`cleanSpecName`), and the binary name is `<api-slug>-pp-cli`. Never hardcode an API slug when the generator can derive it. See [`docs/ARTIFACTS.md`](docs/ARTIFACTS.md) for local-vs-public flow and divergence rules.
diff --git a/README.md b/README.md
index 1b64da6f..19fda7b1 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ You need both the **binary** and the **Claude Code skills**. The skills (`/print
The binary alone works (research, generation, verification, scoring) but skips the curated agent loop. The skills alone have nothing to call. Install both.
-**Prerequisites:** [Go 1.26+](https://go.dev/dl/) and [Claude Code](https://claude.ai/code). The skills are tested with Claude Code; other harnesses like Codex may work but aren't tested. **Use Claude Code for the best experience.**
+**Prerequisites:** [Go 1.26.3 or newer](https://go.dev/dl/) and [Claude Code](https://claude.ai/code). The skills are tested with Claude Code; other harnesses like Codex may work but aren't tested. **Use Claude Code for the best experience.**
### 1. Install the binary
@@ -32,7 +32,7 @@ The binary alone works (research, generation, verification, scoring) but skips t
go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
```
-Verify with `printing-press --version`. If `go install` fails with `410 Gone` or an auth error, see [Troubleshooting](#troubleshooting).
+Verify with `printing-press --version`. If `go install` fails, confirm Go 1.26.3 or newer is installed and `$GOPATH/bin` is on your `PATH`.
### 2. Install the skills — pick one path
@@ -463,14 +463,6 @@ Each published CLI ships a research manuscript, verification proofs, and a `.pri
## Troubleshooting
-**`go install` returns `410 Gone` or an auth error.** The repo is currently private. Set `GOPRIVATE` so Go fetches directly from GitHub instead of the public module proxy, and ensure you have GitHub access (SSH key or `gh auth login`):
-
-```bash
-GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
-```
-
-After the public launch, the plain command works for everyone.
-
**`/printing-press` slash command doesn't appear in Claude Code.** Restart your Claude Code session after installing the skills. If you cloned the repo, confirm `claude --plugin-dir .` was run from the cloned repo root. If you installed skills only, run `gh skill list` (or `npx skills list`) to verify the install.
**`printing-press: command not found` after a successful `go install`.** `$GOPATH/bin` (default `~/go/bin`) isn't on your `PATH`. Add it to your shell profile.
@@ -483,7 +475,7 @@ After the public launch, the plain command works for everyone.
## Limitations
-- **Requires Go 1.26+ and Claude Code.** No standalone distribution today; the slash command is the supported entry point.
+- **Requires Go 1.26.3 or newer and Claude Code.** No standalone distribution today; the slash command is the supported entry point.
- **Generated CLIs are domain-shaped, not vendor-replacements.** A `<api>-pp-cli` covers the agent power-user surface, not every back-office knob a vendor's official CLI ships.
- **Browser-sniff requires manual capture.** You point a browser at the site (or import a HAR); the press doesn't crawl autonomously.
- **Live verify is read-only.** Phase 5 runs GET only and never mutates. Real write-path coverage lives in unit tests and the dogfood structural checks.
diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md
index 802f5973..c6f588da 100644
--- a/docs/GLOSSARY.md
+++ b/docs/GLOSSARY.md
@@ -29,7 +29,7 @@ Default disambiguation conventions:
| **emboss** | A second-pass improvement cycle for an already-printed CLI. Audits baseline, re-researches, identifies top improvements, rebuilds, re-verifies, reports delta. Subcommand: `printing-press emboss <api>`. Still active — not deprecated. |
| **polish** | Targeted fix-up of a printed CLI (distinct from emboss's full cycle). Skill: `/printing-press-polish`. The retro improves the machine; polish improves the printed CLI. |
| **retro** / **retrospective** | Post-generation analysis of *the machine itself* — not the printed CLI. Identifies systemic improvements to templates, the Go binary, skill instructions, or catalog. Output goes to `docs/retros/` and `manuscripts/<api>/<run>/proofs/`. |
-| **quality gates** | 7 mechanical static checks every printed CLI must pass: go mod tidy, go vet, go build, binary build, `--help`, version, doctor. These are build-time checks — see **verify** for runtime testing. |
+| **quality gates** | 8 mechanical static checks every printed CLI must pass: go mod tidy, default-mode govulncheck, go vet, go build, binary build, `--help`, version, doctor. These are build-time checks — see **verify** for runtime testing. |
| **verify** | Runtime behavioral testing of a printed CLI — runs every command against the real API (read-only) or a mock server. Produces PASS/WARN/FAIL verdicts. Has `--fix` mode for auto-patching. Distinct from quality gates (static) and dogfood (structural). |
| **dogfood** | Generation-time structural validation of a printed CLI against its source spec. Catches dead flags, invalid API paths, auth mismatches, and MCP surface parity drift. Subcommand: `printing-press dogfood`. Compare with **doctor** (shipped in the CLI for end-users) and **verify** (runtime behavioral). |
| **cliutil** | The generator-owned Go package emitted into every printed CLI at `internal/cliutil/`. Houses shared helpers meant for agent-authored novel code to import: `cliutil.FanoutRun` for aggregation commands (per-source error collection, bounded concurrency, source-order output), `cliutil.CleanText` for HTML/JSON-LD text normalization, `cliutil.IsVerifyEnv()` for the side-effect short-circuit (see **side-effect command convention**). **Generator-reserved namespace** — agents authoring novel code in Phase 3 must not put their code in `internal/cliutil/` or name their own helpers that collide with cliutil's exports. |
diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md
index 35d7fbc4..55835f3b 100644
--- a/docs/PIPELINE.md
+++ b/docs/PIPELINE.md
@@ -107,7 +107,7 @@ Freshness ownership:
- Freshness metadata belongs in the existing JSON provenance envelope at `meta.freshness`. It describes current-cache freshness for the covered path only; it must not be described as full historical backfill or API-specific enrichment.
Gates:
-- All seven generator quality gates pass: `go mod tidy`, `go vet`, `go build`, binary build, `--help`, version, `doctor`
+- All eight generator quality gates pass: `go mod tidy`, default-mode `govulncheck`, `go vet`, `go build`, binary build, `--help`, version, `doctor`
Artifacts:
- Full CLI source tree in the output directory
@@ -146,7 +146,7 @@ Outputs:
Gates:
- Overlay merge completes without conflicts
-- All seven generator quality gates pass again after regeneration
+- All eight generator quality gates pass again after regeneration, including default-mode `govulncheck`
Artifacts:
- Merged spec (format follows the source spec)
diff --git a/go.mod b/go.mod
index ac886e31..36c2ff64 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/mvanhorn/cli-printing-press/v4
-go 1.26.1
+go 1.26.3
require (
github.com/dave/dst v0.27.4
@@ -11,6 +11,7 @@ require (
github.com/stretchr/testify v1.11.1
golang.org/x/mod v0.35.0
golang.org/x/sync v0.20.0
+ golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -54,6 +55,5 @@ require (
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
- golang.org/x/sys v0.43.0 // indirect
golang.org/x/tools v0.44.0 // indirect
)
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 18e671a4..cfc40deb 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -12,6 +12,7 @@ import (
"time"
catalogpkg "github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
"github.com/spf13/cobra"
@@ -19,6 +20,7 @@ import (
const (
goCommandTimeout = 2 * time.Minute
+ vulnCheckTimeout = 5 * time.Minute
binaryCheckTimeout = 15 * time.Second
)
@@ -546,21 +548,31 @@ func runValidation(dir string) ValidateResult {
}
result.Checks = append(result.Checks, tidyCheck)
- // 4. go vet check
+ // 4. govulncheck catches reachable vulnerable code in this one CLI module
+ // before publish. Keep this scoped to dir; the public library may contain
+ // stale historical CLIs, and whole-library vulnerability enforcement
+ // belongs in changed-module CI or a report-only scheduled sweep.
+ vulnCheck := runGoVulnCheck(dir)
+ if !vulnCheck.Passed {
+ allPassed = false
+ }
+ result.Checks = append(result.Checks, vulnCheck)
+
+ // 5. go vet check
vetCheck := runGoCheck(dir, "vet", "./...")
if !vetCheck.Passed {
allPassed = false
}
result.Checks = append(result.Checks, vetCheck)
- // 5. go build check
+ // 6. go build check
buildCheck := runGoCheck(dir, "build", "./...")
if !buildCheck.Passed {
allPassed = false
}
result.Checks = append(result.Checks, buildCheck)
- // 6. --help / --version checks use a dedicated temp binary so validation
+ // 7. --help / --version checks use a dedicated temp binary so validation
// exercises current source without depending on or mutating source-tree artifacts.
binaryPath, cleanupBinary, err := buildValidationBinary(dir, cliName)
if cleanupBinary != nil {
@@ -588,7 +600,7 @@ func runValidation(dir string) ValidateResult {
result.HelpOutput = string(helpOut)
}
- // 7. --version check
+ // 8. --version check
verCtx, verCancel := context.WithTimeout(context.Background(), binaryCheckTimeout)
defer verCancel()
versionCmd := exec.CommandContext(verCtx, binaryPath, "--version")
@@ -605,7 +617,7 @@ func runValidation(dir string) ValidateResult {
}
}
- // 8. SKILL.md verification — fail if the agent-facing skill advertises
+ // 9. SKILL.md verification — fail if the agent-facing skill advertises
// commands, flags, or arguments the shipped CLI source does not provide.
skillCheck := checkVerifySkill(dir)
if !skillCheck.Passed {
@@ -613,7 +625,7 @@ func runValidation(dir string) ValidateResult {
}
result.Checks = append(result.Checks, skillCheck)
- // 9. Manuscripts check (warn-only)
+ // 10. Manuscripts check (warn-only)
// Try CLI name first (new convention), then API name, then fuzzy resolve
apiName := result.APIName
if apiName == "" {
@@ -701,8 +713,11 @@ func checkVerifySkill(dir string) CheckResult {
}
func runGoCheck(dir string, args ...string) CheckResult {
- name := "go " + args[0]
- ctx, cancel := context.WithTimeout(context.Background(), goCommandTimeout)
+ return runGoCommandCheck(dir, "go "+args[0], goCommandTimeout, args...)
+}
+
+func runGoCommandCheck(dir, name string, timeout time.Duration, args ...string) CheckResult {
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = dir
@@ -710,7 +725,7 @@ func runGoCheck(dir string, args ...string) CheckResult {
if err != nil {
errMsg := strings.TrimSpace(string(output))
if ctx.Err() == context.DeadlineExceeded {
- errMsg = fmt.Sprintf("timed out after %s", goCommandTimeout)
+ errMsg = fmt.Sprintf("timed out after %s", timeout)
} else if errMsg == "" {
errMsg = err.Error()
}
@@ -719,6 +734,13 @@ func runGoCheck(dir string, args ...string) CheckResult {
return CheckResult{Name: name, Passed: true}
}
+func runGoVulnCheck(dir string) CheckResult {
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
+ return CheckResult{Name: govulncheck.Name, Passed: false, Error: "go.mod not found"}
+ }
+ return runGoCommandCheck(dir, govulncheck.Name, vulnCheckTimeout, govulncheck.GoRunArgs("./...")...)
+}
+
func checkGoModTidy(dir string) CheckResult {
modPath := filepath.Join(dir, "go.mod")
sumPath := filepath.Join(dir, "go.sum")
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 04c787a3..b97bdc08 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -4,9 +4,11 @@ import (
"encoding/json"
"os"
"path/filepath"
+ "runtime"
"testing"
"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
"github.com/stretchr/testify/assert"
@@ -151,7 +153,7 @@ func TestPublishValidateJSONHasAllChecks(t *testing.T) {
}
// All checks should be present (they may fail in test env, but must exist)
- expectedChecks := []string{"manifest", "transcendence", "phase5", "go mod tidy", "go vet", "go build", "--help", "--version", "verify-skill", "manuscripts"}
+ expectedChecks := []string{"manifest", "transcendence", "phase5", "go mod tidy", "govulncheck", "go vet", "go build", "--help", "--version", "verify-skill", "manuscripts"}
for _, name := range expectedChecks {
assert.True(t, checkNames[name], "should have %q check", name)
}
@@ -244,6 +246,43 @@ func TestPublishValidateExitCode(t *testing.T) {
assert.Equal(t, ExitPublishError, exitErr.Code, "should use ExitPublishError exit code")
}
+func TestRunGoVulnCheckRequiresGoMod(t *testing.T) {
+ result := runGoVulnCheck(t.TempDir())
+ assert.False(t, result.Passed)
+ assert.Equal(t, govulncheck.Name, result.Name)
+ assert.Equal(t, "go.mod not found", result.Error)
+}
+
+func TestRunGoVulnCheckUsesPinnedDefaultCommand(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("fake shell go binary is Unix-only")
+ }
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n\ngo 1.26.3\n"), 0o644))
+
+ fakeBin := t.TempDir()
+ callsPath := filepath.Join(t.TempDir(), "go-calls.txt")
+ fakeGo := filepath.Join(fakeBin, "go")
+ require.NoError(t, os.WriteFile(fakeGo, []byte(`#!/bin/sh
+printf '%s\n' "$*" >> "$FAKE_GO_CALLS"
+echo "fake govulncheck failure" >&2
+exit 42
+`), 0o755))
+ t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))
+ t.Setenv("FAKE_GO_CALLS", callsPath)
+
+ result := runGoVulnCheck(dir)
+ assert.False(t, result.Passed)
+ assert.Equal(t, govulncheck.Name, result.Name)
+ assert.Contains(t, result.Error, "fake govulncheck failure")
+
+ calls, err := os.ReadFile(callsPath)
+ require.NoError(t, err)
+ assert.Equal(t, "run "+govulncheck.ToolModule+" ./...\n", string(calls))
+ assert.NotContains(t, string(calls), "-show")
+ assert.NotContains(t, string(calls), "verbose")
+}
+
func TestPublishPackageMissingDirFlag(t *testing.T) {
cmd := newPublishCmd()
cmd.SetArgs([]string{"package", "--json"})
@@ -855,7 +894,7 @@ func newInsightCmd() *cobra.Command {
return &cobra.Command{Use: "insight", Short: "Show test insight"}
}
`), 0o644))
- skillInstall := generator.CanonicalSkillInstallSection("test", "", false)
+ skillInstall := generator.CanonicalSkillInstallSection("test", "")
require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("# Test CLI\n\n"+skillInstall+"\n## Command Reference\n\n- `test-pp-cli insight` — Show test insight\n\n## Usage\n\n```bash\ntest-pp-cli insight --agent\n```\n"), 0o644))
writeTestManifest(t, dir, pipeline.CLIManifest{
diff --git a/internal/cli/verify_skill.go b/internal/cli/verify_skill.go
index d61d28f5..1f26e771 100644
--- a/internal/cli/verify_skill.go
+++ b/internal/cli/verify_skill.go
@@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path/filepath"
- "regexp"
"strings"
"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
@@ -116,9 +115,6 @@ type pythonReport struct {
Findings []canonicalFinding `json:"findings"`
}
-// goModVersionRE matches the `go X.Y` directive at the top of go.mod.
-var goModVersionRE = regexp.MustCompile(`(?m)^go\s+(\d+)\.(\d+)`)
-
// runCanonicalSectionsCheck verifies that the install/prerequisites section
// of dir/SKILL.md matches what the generator would emit for this CLI today.
// Detects post-publish edits to a generator-owned section (the failure mode
@@ -140,19 +136,9 @@ func runCanonicalSectionsCheck(dir string) (finding canonicalFinding, hasFinding
return canonicalFinding{}, false, true, nil
}
- goModBytes, gErr := os.ReadFile(filepath.Join(dir, "go.mod"))
- if gErr != nil {
+ if _, gErr := os.Stat(filepath.Join(dir, "go.mod")); gErr != nil {
return canonicalFinding{}, false, true, nil
}
- usesBrowserHTTP := false
- if m := goModVersionRE.FindSubmatch(goModBytes); m != nil {
- // browser-HTTP transport raises the floor to 1.25; treat any
- // 1.25+ go directive as the browser-HTTP signal. CLIs not on
- // browser-HTTP stay on 1.23.
- if string(m[1]) == "1" && string(m[2]) >= "25" {
- usesBrowserHTTP = true
- }
- }
skillBytes, sErr := os.ReadFile(filepath.Join(dir, "SKILL.md"))
if sErr != nil {
@@ -160,7 +146,7 @@ func runCanonicalSectionsCheck(dir string) (finding canonicalFinding, hasFinding
}
skill := string(skillBytes)
- expected := generator.CanonicalSkillInstallSection(name, manifest.Category, usesBrowserHTTP)
+ expected := generator.CanonicalSkillInstallSection(name, manifest.Category)
got, ok := ExtractInstallSectionForTest(skill)
if !ok {
return canonicalFinding{
diff --git a/internal/cli/verify_skill_test.go b/internal/cli/verify_skill_test.go
index 72bfe811..2e09876e 100644
--- a/internal/cli/verify_skill_test.go
+++ b/internal/cli/verify_skill_test.go
@@ -332,7 +332,7 @@ func Execute() error {
func TestVerifySkill_CanonicalSectionsPassesOnFreshFixture(t *testing.T) {
t.Parallel()
bin := buildPrintingPressBinary(t)
- dir := writeCanonicalFixture(t, "myapi", "productivity", false, "")
+ dir := writeCanonicalFixture(t, "myapi", "productivity", "")
out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
require.NoError(t, err, "fresh fixture must pass canonical-sections: %s", string(out))
require.Contains(t, string(out), "canonical-sections passed")
@@ -347,10 +347,10 @@ func TestVerifySkill_CanonicalSectionsCatchesFlagStrip(t *testing.T) {
t.Parallel()
bin := buildPrintingPressBinary(t)
tampered := strings.Replace(
- generator.CanonicalSkillInstallSection("myapi", "productivity", false),
+ generator.CanonicalSkillInstallSection("myapi", "productivity"),
" --cli-only", "", 1,
)
- dir := writeCanonicalFixture(t, "myapi", "productivity", false, tampered)
+ dir := writeCanonicalFixture(t, "myapi", "productivity", tampered)
out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
require.Error(t, err, "stripping --cli-only must fail canonical-sections: %s", string(out))
require.Contains(t, string(out), "canonical-sections")
@@ -367,7 +367,7 @@ func TestVerifySkill_CanonicalSectionsCatchesFabricatedInstall(t *testing.T) {
t.Parallel()
bin := buildPrintingPressBinary(t)
fabricated := "## Prerequisites: Install the CLI\n\nInstall via the Printing Press Library plugin (`/ppl install myapi` from Claude Code).\n\nIf `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
- dir := writeCanonicalFixture(t, "myapi", "productivity", false, fabricated)
+ dir := writeCanonicalFixture(t, "myapi", "productivity", fabricated)
out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
require.Error(t, err, "fabricated install instructions must fail canonical-sections: %s", string(out))
require.Contains(t, string(out), "drift")
@@ -398,7 +398,7 @@ func Execute() error { return (&cobra.Command{Use: "fixture-pp-cli"}).Execute()
// for exercising the canonical-sections check end-to-end. When skillBody is
// "", the canonical install section is used verbatim; pass a tampered body
// to simulate hand-editing of the install section.
-func writeCanonicalFixture(t *testing.T, name, category string, usesBrowserHTTP bool, skillBody string) string {
+func writeCanonicalFixture(t *testing.T, name, category, skillBody string) string {
t.Helper()
dir := t.TempDir()
cliDir := filepath.Join(dir, "internal", "cli")
@@ -408,19 +408,15 @@ import "github.com/spf13/cobra"
func Execute() error { return (&cobra.Command{Use: "`+name+`-pp-cli"}).Execute() }
`), 0o644))
- goVersion := "1.23"
- if usesBrowserHTTP {
- goVersion = "1.25"
- }
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"),
- []byte("module github.com/example/"+name+"-pp-cli\n\ngo "+goVersion+"\n"), 0o644))
+ []byte("module github.com/example/"+name+"-pp-cli\n\ngo 1.26.3\n"), 0o644))
manifest := fmt.Sprintf(`{"api_name":%q,"cli_name":%q,"category":%q}`,
name, name+"-pp-cli", category)
require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
if skillBody == "" {
- skillBody = generator.CanonicalSkillInstallSection(name, category, usesBrowserHTTP)
+ skillBody = generator.CanonicalSkillInstallSection(name, category)
}
skill := "---\nname: pp-" + name + "\ndescription: \"fixture\"\n---\n\n# " + name + "\n\n" + skillBody + "\nFixture body.\n"
require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(skill), 0o644))
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 99a34792..2b469101 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1180,7 +1180,7 @@ func TestGenerateBrowserChromeTransport(t *testing.T) {
gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
require.NoError(t, err)
- assert.Contains(t, string(gomod), "go 1.25")
+ assert.Contains(t, string(gomod), "go 1.26.3")
assert.Contains(t, string(gomod), "github.com/enetx/surf")
clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
@@ -1338,7 +1338,7 @@ func TestGenerateHTMLExtractionEndpoint(t *testing.T) {
require.FileExists(t, filepath.Join(outputDir, "internal", "cli", "html_extract.go"))
gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
require.NoError(t, err)
- assert.Contains(t, string(gomod), "golang.org/x/net")
+ assert.Contains(t, string(gomod), "golang.org/x/net v0.53.0")
runGoCommand(t, outputDir, "mod", "tidy")
binaryPath := filepath.Join(outputDir, "webhtml-pp-cli")
@@ -1735,7 +1735,7 @@ func TestGenerateStandardTransportForOfficialAPI(t *testing.T) {
gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
require.NoError(t, err)
- assert.Contains(t, string(gomod), "go 1.23")
+ assert.Contains(t, string(gomod), "go 1.26.3")
assert.NotContains(t, string(gomod), "github.com/enetx/surf")
clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
diff --git a/internal/generator/install_section.go b/internal/generator/install_section.go
index 6e5166e6..b8c223aa 100644
--- a/internal/generator/install_section.go
+++ b/internal/generator/install_section.go
@@ -20,8 +20,7 @@ const SkillInstallSectionEndSubstr = "Do not proceed with skill commands until v
// emits into a printed CLI's SKILL.md install section. Indexed verbs:
//
// %[1]s — CLI slug (e.g. "linear" — produces linear-pp-cli)
-// %[2]s — Go-fallback floor ("Go 1.23+" or "Go 1.25+")
-// %[3]s — catalog category (or "other" when empty)
+// %[2]s — catalog category (or "other" when empty)
//
// Stays in lockstep with internal/generator/templates/skill.md.tmpl via
// TestCanonicalSkillInstallSectionMatchesTemplate.
@@ -36,33 +35,27 @@ const canonicalSkillInstallSectionFormat = "## Prerequisites: Install the CLI\n"
"2. Verify: `%[1]s-pp-cli --version`\n" +
"3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.\n" +
"\n" +
- "If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires %[2]s):\n" +
+ "If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):\n" +
"\n" +
"```bash\n" +
- "go install github.com/mvanhorn/printing-press-library/library/%[3]s/%[1]s/cmd/%[1]s-pp-cli@latest\n" +
+ "go install github.com/mvanhorn/printing-press-library/library/%[2]s/%[1]s/cmd/%[1]s-pp-cli@latest\n" +
"```\n" +
"\n" +
"If `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
// CanonicalSkillInstallSection returns the exact text of the install/
// prerequisites section that the generator emits into a printed CLI's
-// SKILL.md, given the CLI slug, the catalog category (empty -> "other"),
-// and whether the CLI uses the browser HTTP transport (raises the Go-
-// fallback floor from 1.23 to 1.25).
+// SKILL.md, given the CLI slug and catalog category (empty -> "other").
//
// The verify-skill canonical-sections check uses this function to detect
// post-publish edits to the install instructions. The function is the
// authoritative source post-generation; the template stays in sync via
// TestCanonicalSkillInstallSectionMatchesTemplate.
-func CanonicalSkillInstallSection(name, category string, usesBrowserHTTP bool) string {
+func CanonicalSkillInstallSection(name, category string) string {
if category == "" {
category = "other"
}
- goFloor := "Go 1.23+"
- if usesBrowserHTTP {
- goFloor = "Go 1.25+"
- }
- return fmt.Sprintf(canonicalSkillInstallSectionFormat, name, goFloor, category)
+ return fmt.Sprintf(canonicalSkillInstallSectionFormat, name, category)
}
// ExtractSkillInstallSection slices the install/prerequisites block out of
diff --git a/internal/generator/install_section_test.go b/internal/generator/install_section_test.go
index 00fb9a9d..e2b80c18 100644
--- a/internal/generator/install_section_test.go
+++ b/internal/generator/install_section_test.go
@@ -15,7 +15,7 @@ import (
// CanonicalSkillInstallSection and skill.md.tmpl. The two are parallel
// renderings of the same canonical install block — one Go literal, one
// Go template — and they must produce byte-identical output for any
-// (name, category, usesBrowserHTTP) tuple. If either drifts, this test
+// (name, category) tuple. If either drifts, this test
// fails before any printed CLI ships with a desynced install section.
//
// The verify-skill canonical-sections check enforces this contract at
@@ -33,7 +33,7 @@ func TestCanonicalSkillInstallSectionMatchesTemplate(t *testing.T) {
}{
{"empty category and standard transport", "myapi", "", false},
{"explicit category", "myapi", "productivity", false},
- {"browser transport raises Go floor", "myapi", "productivity", true},
+ {"browser transport uses same Go floor", "myapi", "productivity", true},
{"slug with hyphens", "trigger-dev", "developer-tools", false},
}
@@ -57,7 +57,7 @@ func TestCanonicalSkillInstallSectionMatchesTemplate(t *testing.T) {
extracted, ok := ExtractSkillInstallSection(string(rendered))
require.True(t, ok, "extractor must find the install section in a freshly-rendered SKILL.md")
- expected := CanonicalSkillInstallSection(tc.apiName, tc.category, tc.usesBrowserHTTP)
+ expected := CanonicalSkillInstallSection(tc.apiName, tc.category)
require.Equal(t, expected, extracted,
"template-rendered install section must equal CanonicalSkillInstallSection output for %s", tc.label)
})
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index 3f759389..675a549c 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -1,6 +1,6 @@
module {{modulePath}}
-go {{if .UsesBrowserHTTPTransport}}1.25{{else}}1.23{{end}}
+go 1.26.3
require (
{{- if .UsesBrowserHTTPTransport}}
@@ -8,7 +8,7 @@ require (
{{- end}}
github.com/spf13/cobra v1.9.1
{{- if .HasHTMLExtraction}}
- golang.org/x/net v0.43.0
+ golang.org/x/net v0.53.0
{{- end}}
{{- if eq .Config.Format "toml"}}
github.com/pelletier/go-toml/v2 v2.2.4
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index 05b10094..be54695c 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -32,7 +32,7 @@ npx -y @mvanhorn/printing-press install {{.Name}} --cli-only
### Without Node (Go fallback)
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires {{if .UsesBrowserHTTPTransport}}Go 1.25+{{else}}Go 1.23+{{end}}):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 6ec07e55..3032ed40 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -29,7 +29,7 @@ This skill drives the `{{.Name}}-pp-cli` binary. **You must verify the CLI is in
2. Verify: `{{.Name}}-pp-cli --version`
3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires {{if .UsesBrowserHTTPTransport}}Go 1.25+{{else}}Go 1.23+{{end}}):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
diff --git a/internal/generator/validate.go b/internal/generator/validate.go
index 427a84df..4908db7c 100644
--- a/internal/generator/validate.go
+++ b/internal/generator/validate.go
@@ -11,6 +11,7 @@ import (
"time"
"github.com/mvanhorn/cli-printing-press/v4/internal/artifacts"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
)
@@ -46,6 +47,13 @@ func (g *Generator) Validate() error {
return err
},
},
+ {
+ name: "govulncheck ./...",
+ run: func() error {
+ _, err := runCommand(g.OutputDir, qualityGateTimeout, "go", govulncheck.GoRunArgs("./...")...)
+ return err
+ },
+ },
{
name: "go vet ./...",
run: func() error {
diff --git a/internal/generator/validate_test.go b/internal/generator/validate_test.go
index 3d44c127..2f89b011 100644
--- a/internal/generator/validate_test.go
+++ b/internal/generator/validate_test.go
@@ -3,8 +3,10 @@ package generator
import (
"os"
"path/filepath"
+ "runtime"
"testing"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -13,7 +15,7 @@ func TestGoBuildCacheDirIsShared(t *testing.T) {
t.Setenv("GOCACHE", "")
// Two different project directories should get the same cache dir.
- // This is critical for CI performance — shared cache avoids each
+ // This is critical for CI performance because the shared cache avoids each
// parallel test recompiling the Go standard library from scratch.
dir1, err := goBuildCacheDir("/tmp/project-a")
require.NoError(t, err)
@@ -47,3 +49,37 @@ func TestGoBuildCacheDirHonorsExplicitGOCACHE(t *testing.T) {
assert.Equal(t, cacheDir, dir)
assert.DirExists(t, cacheDir)
}
+
+func TestValidateRunsPinnedDefaultGovulncheckGate(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("fake shell go binary is Unix-only")
+ }
+ outputDir := filepath.Join(t.TempDir(), "validate-pp-cli")
+ gen := New(minimalSpec("validate"), outputDir)
+ require.NoError(t, gen.Generate())
+
+ fakeBin := t.TempDir()
+ callsPath := filepath.Join(t.TempDir(), "go-calls.txt")
+ fakeGo := filepath.Join(fakeBin, "go")
+ require.NoError(t, os.WriteFile(fakeGo, []byte(`#!/bin/sh
+printf '%s\n' "$*" >> "$FAKE_GO_CALLS"
+if [ "$1" = "run" ]; then
+ echo "fake govulncheck failure" >&2
+ exit 42
+fi
+exit 0
+`), 0o755))
+ t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))
+ t.Setenv("FAKE_GO_CALLS", callsPath)
+
+ err := gen.Validate()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), `gate "govulncheck ./..." failed`)
+
+ calls, err := os.ReadFile(callsPath)
+ require.NoError(t, err)
+ assert.Contains(t, string(calls), "mod tidy\n")
+ assert.Contains(t, string(calls), "run "+govulncheck.ToolModule+" ./...\n")
+ assert.NotContains(t, string(calls), "-show")
+ assert.NotContains(t, string(calls), "verbose")
+}
diff --git a/internal/govulncheck/govulncheck.go b/internal/govulncheck/govulncheck.go
new file mode 100644
index 00000000..35a4f2ad
--- /dev/null
+++ b/internal/govulncheck/govulncheck.go
@@ -0,0 +1,14 @@
+package govulncheck
+
+const (
+ Name = "govulncheck"
+ ToolVersion = "v1.3.0"
+ ToolModule = "golang.org/x/vuln/cmd/govulncheck@" + ToolVersion
+)
+
+// Default mode keeps blocking focused on reachable findings, not dependency
+// presence alone.
+func GoRunArgs(args ...string) []string {
+ goArgs := []string{"run", ToolModule}
+ return append(goArgs, args...)
+}
diff --git a/internal/govulncheck/govulncheck_test.go b/internal/govulncheck/govulncheck_test.go
new file mode 100644
index 00000000..6a2dce3a
--- /dev/null
+++ b/internal/govulncheck/govulncheck_test.go
@@ -0,0 +1,21 @@
+package govulncheck
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToolModuleIsPinned(t *testing.T) {
+ assert.NotContains(t, ToolModule, "@latest")
+ assert.True(t, strings.HasPrefix(ToolVersion, "v"))
+ assert.Equal(t, "golang.org/x/vuln/cmd/govulncheck@"+ToolVersion, ToolModule)
+}
+
+func TestGoRunArgsUsesDefaultMode(t *testing.T) {
+ args := GoRunArgs("./...")
+ assert.Equal(t, []string{"run", ToolModule, "./..."}, args)
+ assert.NotContains(t, args, "-show")
+ assert.NotContains(t, args, "verbose")
+}
diff --git a/internal/pipeline/fullrun.go b/internal/pipeline/fullrun.go
index 35c82d28..ca6c44ae 100644
--- a/internal/pipeline/fullrun.go
+++ b/internal/pipeline/fullrun.go
@@ -17,6 +17,8 @@ import (
"gopkg.in/yaml.v3"
)
+const fullRunQualityGateCount = 8
+
// FullRunResult holds everything the press produced for one API.
type FullRunResult struct {
APIName string
@@ -421,7 +423,7 @@ func PrintComparisonTable(results []*FullRunResult) string {
writeComparisonRows(&b, results, []comparisonRow{
{"Quality Gates", func(r *FullRunResult) string {
- return fmt.Sprintf("%d/7 PASS", r.GatesPassed)
+ return fmt.Sprintf("%d/%d PASS", r.GatesPassed, fullRunQualityGateCount)
}},
{"Commands", func(r *FullRunResult) string {
return fmt.Sprintf("%d", r.CommandCount)
@@ -570,8 +572,8 @@ func GenerateLearningsPlan(results []*FullRunResult, outputPath string) error {
if len(r.Errors) > 0 {
status = fmt.Sprintf("%d errors", len(r.Errors))
}
- fmt.Fprintf(&b, "- **%s** (%s) - Gates %d/7, Duration %s, Status: %s\n",
- r.APIName, r.Level, r.GatesPassed, r.Duration.Round(time.Second), status)
+ fmt.Fprintf(&b, "- **%s** (%s) - Gates %d/%d, Duration %s, Status: %s\n",
+ r.APIName, r.Level, r.GatesPassed, fullRunQualityGateCount, r.Duration.Round(time.Second), status)
}
b.WriteString("\n")
diff --git a/internal/pipeline/fullrun_test.go b/internal/pipeline/fullrun_test.go
index 2cb4d11e..beb54c08 100644
--- a/internal/pipeline/fullrun_test.go
+++ b/internal/pipeline/fullrun_test.go
@@ -44,7 +44,7 @@ func TestFullRun(t *testing.T) {
result := MakeBestCLI(api.name, api.level, api.flag, api.url, outputDir, pressBinary)
results = append(results, result)
- assert.Equal(t, 7, result.GatesPassed, "%s: all 7 gates should pass", api.name)
+ assert.Equal(t, fullRunQualityGateCount, result.GatesPassed, "%s: all quality gates should pass", api.name)
assert.True(t, result.CommandCount > 0, "%s: should have commands", api.name)
assert.NotNil(t, result.Scorecard, "%s: should have scorecard", api.name)
})
@@ -71,6 +71,22 @@ func TestFullRunQualitySpecPath(t *testing.T) {
assert.Equal(t, "", fullRunQualitySpecPath("--docs", "https://example.com/docs"))
}
+func TestGenerateLearningsPlanUsesQualityGateCount(t *testing.T) {
+ t.Parallel()
+
+ outputPath := filepath.Join(t.TempDir(), "learnings.md")
+ require.NoError(t, GenerateLearningsPlan([]*FullRunResult{{
+ APIName: "demo",
+ Level: "EASY",
+ GatesPassed: 6,
+ }}, outputPath))
+
+ content, err := os.ReadFile(outputPath)
+ require.NoError(t, err)
+ assert.Contains(t, string(content), "Gates 6/8")
+ assert.NotContains(t, string(content), "Gates 6/7")
+}
+
func TestPrintComparisonTableRows(t *testing.T) {
t.Parallel()
@@ -113,7 +129,7 @@ func TestPrintComparisonTableRows(t *testing.T) {
last = idx
}
- assert.Contains(t, table, "6/7 PASS")
+ assert.Contains(t, table, "6/8 PASS")
assert.Contains(t, table, "72/80 (72%)")
assert.Contains(t, table, "1/2")
assert.Contains(t, table, "PASS 100%")
diff --git a/internal/pipeline/planner.go b/internal/pipeline/planner.go
index bfe4df47..e0b49f93 100644
--- a/internal/pipeline/planner.go
+++ b/internal/pipeline/planner.go
@@ -104,7 +104,7 @@ func generateScaffoldPlan(ctx PlanContext) (string, error) {
b.WriteString("## What This Phase Must Produce\n\n")
fmt.Fprintf(&b, "- Generated CLI source tree in %s\n", ctx.SeedData.OutputDir)
- b.WriteString("- All seven generator quality gates passing\n")
+ b.WriteString("- All eight generator quality gates passing, including default-mode govulncheck\n")
fmt.Fprintf(&b, "- Working CLI binary for %s\n\n", ctx.SeedData.APIName)
b.WriteString("## Codebase Pointers\n\n")
diff --git a/internal/pipeline/planner_artifacts_test.go b/internal/pipeline/planner_artifacts_test.go
index 279a41bc..2ba727b2 100644
--- a/internal/pipeline/planner_artifacts_test.go
+++ b/internal/pipeline/planner_artifacts_test.go
@@ -42,6 +42,8 @@ func TestGenerateNextPlanLoadsArtifactsFromRunstateDirs(t *testing.T) {
scaffoldPlan, err := GenerateNextPlan(state, PhaseScaffold)
require.NoError(t, err)
assert.Contains(t, scaffoldPlan, "Novelty score:** 8/10 (proceed)")
+ assert.Contains(t, scaffoldPlan, "All eight")
+ assert.Contains(t, scaffoldPlan, "govulncheck")
comparativePlan, err := GenerateNextPlan(state, PhaseComparative)
require.NoError(t, err)
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
index f6eddfaf..869cb57e 100644
--- a/internal/pipeline/seeds.go
+++ b/internal/pipeline/seeds.go
@@ -175,7 +175,7 @@ Generate the first working {{.APIName}} CLI from the validated OpenAPI spec.
## What This Phase Must Produce
- Generated CLI source tree in {{.OutputDir}}
-- All seven generator quality gates passing
+- All eight generator quality gates passing, including default-mode govulncheck
- Working CLI binary for {{.APIName}}
## Prior Phase Outputs
@@ -250,7 +250,7 @@ Merge the enrichments into the source spec and regenerate the CLI without losing
- Re-generated CLI in {{.OutputDir}} using the merged overlay
- Merged spec artifact suitable for regeneration
-- All seven quality gates still passing after regeneration
+- All eight quality gates still passing after regeneration, including default-mode govulncheck
## Prior Phase Outputs
diff --git a/internal/pipeline/seeds_test.go b/internal/pipeline/seeds_test.go
index 51307cc1..be259a48 100644
--- a/internal/pipeline/seeds_test.go
+++ b/internal/pipeline/seeds_test.go
@@ -33,6 +33,11 @@ func TestRenderSeedProducesThinSeedTemplates(t *testing.T) {
require.Equal(t, "---", lines[0])
require.Contains(t, rendered, "type: feat")
require.Contains(t, rendered, "date: ")
+ if phase == PhaseScaffold || phase == PhaseRegenerate {
+ require.Contains(t, rendered, "All eight")
+ require.Contains(t, rendered, "govulncheck")
+ require.NotContains(t, rendered, "All seven")
+ }
})
}
}
diff --git a/skills/printing-press-catalog/SKILL.md b/skills/printing-press-catalog/SKILL.md
index 661dbb09..f71fcf5e 100644
--- a/skills/printing-press-catalog/SKILL.md
+++ b/skills/printing-press-catalog/SKILL.md
@@ -30,7 +30,7 @@ Browse and install pre-built Go CLIs for popular APIs.
## Prerequisites
-- Go 1.21+ installed
+- Go 1.26.3 or newer installed
- `printing-press` binary on PATH (install with `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest`)
## Setup
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index 25aea36d..f7e15aa3 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -194,6 +194,12 @@ Run:
printing-press publish validate --dir <cli-dir> --json
```
+`govulncheck` in this step is intentionally scoped to `<cli-dir>` only. It
+uses the default `govulncheck ./...` mode so reachable symbol findings block
+publish, while merely-required vulnerable modules without a call path do not
+become release blockers. Do not replace this with a full public-library scan or
+`govulncheck -show verbose`.
+
Parse the JSON result. Display each check result to the user:
```
@@ -201,6 +207,7 @@ Validating <api-slug>...
manifest PASS
phase5 PASS
go mod tidy PASS
+ govulncheck PASS
go vet PASS
go build PASS
--help PASS
@@ -398,10 +405,18 @@ if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then
(cd "$PUBLISH_REPO_DIR" && go run ./tools/generate-skills/main.go)
fi
-# Verify it builds from the publish repo
-cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" && go build ./...
+# Verify this changed/new CLI builds and has no reachable Go vulnerabilities from the publish repo
+cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
+ && go build ./... \
+ && go run golang.org/x/vuln/cmd/govulncheck@v1.3.0 ./...
```
+Keep vulnerability verification scoped to `library/<category>/<api-slug>` in
+publish PRs. The public library is a historical collection and cannot be kept
+fully current on every unrelated PR; whole-library govulncheck sweeps belong in
+a scheduled/reporting workflow, while blocking CI should scan only added or
+changed CLI modules.
+
After the publish repo copy and build verification are complete, remove the staging
directory:
@@ -735,6 +750,7 @@ $ <cli-name> --help
| Manifest | PASS/FAIL |
| Phase 5 | PASS/FAIL |
| go mod tidy | PASS/FAIL |
+| govulncheck (this CLI only, reachable findings) | PASS/FAIL |
| go vet | PASS/FAIL |
| go build | PASS/FAIL |
| --help | PASS/FAIL |
diff --git a/skills/printing-press-score/SKILL.md b/skills/printing-press-score/SKILL.md
index 9af4c181..d76f3519 100644
--- a/skills/printing-press-score/SKILL.md
+++ b/skills/printing-press-score/SKILL.md
@@ -26,7 +26,7 @@ Score generated CLIs against the Steinberger bar. Supports rescoring, scoring by
## Prerequisites
-- Go 1.21+ installed
+- Go 1.26.3 or newer installed
- `printing-press` binary on PATH (install with `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest`)
## Step 0: Setup
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 06ff89b6..e95d1f0f 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -136,19 +136,17 @@ elif ! command -v printing-press >/dev/null 2>&1; then
else
# Refuse: the printing-press binary is required and we will not auto-install
# it. The README's two-step install (binary + plugin) is the source of truth;
- # silent auto-install hides failure modes (GOPRIVATE auth, network, wrong
- # GOPATH) inside an opaque skill invocation.
+ # silent auto-install hides failure modes (network, wrong GOPATH) inside an
+ # opaque skill invocation.
echo ""
echo "[setup-error] printing-press binary not found."
echo ""
if command -v go >/dev/null 2>&1; then
echo "Install it in your terminal:"
- echo " GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
- echo ""
- echo "(GOPRIVATE is required while the repo is private. The plain command works after the public launch.)"
+ echo " go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
else
- echo "Go 1.22+ is also not installed. Install Go from https://go.dev/dl/, then:"
- echo " GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
+ echo "Go 1.26.3 or newer is also not installed. Install Go from https://go.dev/dl/, then:"
+ echo " go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
fi
echo ""
echo "Verify with: printing-press --version"
@@ -173,10 +171,9 @@ mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT
# --- Latest-version advisory (throttled, fail-open) ---
# Once per 24h, check whether a newer printing-press release exists and print a
-# one-line notice if so. Uses `go list` so it works for private modules via
-# GOPRIVATE and public modules via the proxy. Runs in every context — devs
-# ahead of latest stay silent (comparison handles it), devs behind latest get
-# the same nudge anyone else does.
+# one-line notice if so. Uses `go list` through the public module proxy. Runs in
+# every context — devs ahead of latest stay silent (comparison handles it), devs
+# behind latest get the same nudge anyone else does.
PRESS_VERCHECK_FILE="$PRESS_HOME/.version-check"
PRESS_VERCHECK_TTL=86400
_now_ts=$(date +%s)
@@ -190,7 +187,7 @@ fi
if [ "$_should_check" = "true" ] && command -v go >/dev/null 2>&1; then
_installed=$(printing-press version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
- _latest=$(GOPRIVATE=github.com/mvanhorn/* go list -m -versions github.com/mvanhorn/cli-printing-press/v4 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
+ _latest=$(go list -m -versions github.com/mvanhorn/cli-printing-press/v4 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
if [ -n "$_installed" ] && [ -n "$_latest" ] && [ "$_installed" != "$_latest" ]; then
# sort -V is not fully semver-aware: it ranks "3.0.0-rc1" above "3.0.0" instead of below.
diff --git a/skills/printing-press/references/setup-checks.md b/skills/printing-press/references/setup-checks.md
index 944fd811..14dba379 100644
--- a/skills/printing-press/references/setup-checks.md
+++ b/skills/printing-press/references/setup-checks.md
@@ -8,9 +8,9 @@ Apply these in order. Each section is conditional — do nothing if its trigger
If the setup contract output contains a line starting with `[setup-error]`, the printing-press binary is not installed and the contract has already exited non-zero.
-**Stop the skill immediately.** Do not proceed to research, generation, or any other work. Surface the message the contract printed (it includes the exact `go install` command and `GOPRIVATE` guidance) verbatim to the user.
+**Stop the skill immediately.** Do not proceed to research, generation, or any other work. Surface the message the contract printed (it includes the exact `go install` command) verbatim to the user.
-The user must install the binary in their terminal before re-running. Do not offer to auto-install — the README's two-step install is the source of truth, and silent auto-install hides failure modes (GOPRIVATE auth, network, wrong GOPATH) inside an opaque skill invocation.
+The user must install the binary in their terminal before re-running. Do not offer to auto-install — the README's two-step install is the source of truth, and silent auto-install hides failure modes (network, wrong GOPATH) inside an opaque skill invocation.
## 2. Interactive upgrade prompt
@@ -31,7 +31,7 @@ Then ask the user via `AskUserQuestion` before continuing setup:
If the user picks **Yes**, run:
```bash
-GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
+go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
```
After it completes, confirm with `printing-press version --json` and tell the user `"Upgraded to v<new>."` Then continue setup.
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
index 2d657580..9d1e5277 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install printing-press-rich --cli-only
### Without Node (Go fallback)
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
index f7d73a4f..d7dececc 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `printing-press-rich-pp-cli` binary. **You must verify the
2. Verify: `printing-press-rich-pp-cli --version`
3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
index 1d1c4af9..fde3b361 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install printing-press-golden --cli-only
### Without Node (Go fallback)
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
index e1d91b0b..bd0b2761 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `printing-press-golden-pp-cli` binary. **You must verify t
2. Verify: `printing-press-golden-pp-cli --version`
3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
index 4dd91bda..909013bc 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
@@ -1,6 +1,6 @@
module printing-press-golden-pp-cli
-go 1.23
+go 1.26.3
require (
github.com/spf13/cobra v1.9.1
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
index 172237ae..60acc282 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install public-param-golden --cli-only
### Without Node (Go fallback)
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
index aabdf89e..cfa65c8c 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `public-param-golden-pp-cli` binary. **You must verify the
2. Verify: `public-param-golden-pp-cli --version`
3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest
← af70886c chore(main): release 4.0.4 (#692)
·
back to Cli Printing Press
·
chore(main): release 4.0.5 (#700) e601b211 →