[object Object]

← back to Cli Printing Press

feat(cli): credit original printer in per-CLI README (closes #745) (#748)

ee8f6ad99ce0dd4b6400785a69b014a508534862 · 2026-05-08 14:59:06 -0700 · Matt Van Horn

* feat(cli): capture printer @handle and display name through manifest chain

Adds Printer (GitHub @handle) and PrinterName (display) fields that flow
generator -> .printing-press.json -> publish-side rewrite. First print
wins: the resolver tier reads existing manifest before git config so
regens by other contributors do not overwrite the original printer.
Distinct from Owner/OwnerName (which carry the API-spec / wrapper-author
identity); Printer is the human who ran the press.

When git config github.user is unset, Generate() warns and falls back to
the slug-shaped Owner so emission stays non-empty (mirrors the existing
OwnerName empty-fallback). PrinterName has no slug fallback because the
README byline parenthetical is rendered conditionally.

Wires APISpec, CLIManifest, GenerateManifestParams, WriteManifestForGenerate,
writeCLIManifestForPublish carry-forward, and the standalone generate
command's two call sites in cmd/cli/root.go.

* feat(cli): render Printed by byline in per-CLI README between title and Install

Adds a conditional template block in readme.md.tmpl that emits
`Printed by [@handle](https://github.com/handle) (Display Name).`
when spec.Printer is set. Wrapped in {{if .Printer}} so older prints
without the field render unchanged.

Updates goldens for 6 fixtures whose harness env (scripts/golden.sh)
sets GIT_CONFIG_KEY_0=github.user and GIT_CONFIG_KEY_1=user.name to the
deterministic golden_owner slug, exercising the new byline + manifest
fields. Manifest gains `printer` and `printer_name`; README gains the
byline. No other changes.

* docs(cli): document Printer/PrinterName parallel pair and add publish-time strict check

AGENTS.md gains a parallel paragraph to the existing Owner/OwnerName
disambiguation, naming Printer/PrinterName as the second slug-vs-display
pair and explaining its distinct semantics (printer is the human, owner
is the API vendor/wrapper-author). Reinforces the no-"USER"-sentinel
rule and points at the publish-side gate.

skills/printing-press-publish/SKILL.md Step 6 packaging gains a strict
check on the freshly-copied .printing-press.json: refuses to publish
when .printer is empty, equals the literal "USER"/"user" sentinel, or
equals .owner (slug-fallback case). The generator's stderr warning at
print time is advisory; the publish step is the enforcing gate so
broken attribution rows cannot reach the library catalog.

* fix(cli): avoid printer owner fallback false positives

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>

Files touched

Diff

commit ee8f6ad99ce0dd4b6400785a69b014a508534862
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date:   Fri May 8 14:59:06 2026 -0700

    feat(cli): credit original printer in per-CLI README (closes #745) (#748)
    
    * feat(cli): capture printer @handle and display name through manifest chain
    
    Adds Printer (GitHub @handle) and PrinterName (display) fields that flow
    generator -> .printing-press.json -> publish-side rewrite. First print
    wins: the resolver tier reads existing manifest before git config so
    regens by other contributors do not overwrite the original printer.
    Distinct from Owner/OwnerName (which carry the API-spec / wrapper-author
    identity); Printer is the human who ran the press.
    
    When git config github.user is unset, Generate() warns and falls back to
    the slug-shaped Owner so emission stays non-empty (mirrors the existing
    OwnerName empty-fallback). PrinterName has no slug fallback because the
    README byline parenthetical is rendered conditionally.
    
    Wires APISpec, CLIManifest, GenerateManifestParams, WriteManifestForGenerate,
    writeCLIManifestForPublish carry-forward, and the standalone generate
    command's two call sites in cmd/cli/root.go.
    
    * feat(cli): render Printed by byline in per-CLI README between title and Install
    
    Adds a conditional template block in readme.md.tmpl that emits
    `Printed by [@handle](https://github.com/handle) (Display Name).`
    when spec.Printer is set. Wrapped in {{if .Printer}} so older prints
    without the field render unchanged.
    
    Updates goldens for 6 fixtures whose harness env (scripts/golden.sh)
    sets GIT_CONFIG_KEY_0=github.user and GIT_CONFIG_KEY_1=user.name to the
    deterministic golden_owner slug, exercising the new byline + manifest
    fields. Manifest gains `printer` and `printer_name`; README gains the
    byline. No other changes.
    
    * docs(cli): document Printer/PrinterName parallel pair and add publish-time strict check
    
    AGENTS.md gains a parallel paragraph to the existing Owner/OwnerName
    disambiguation, naming Printer/PrinterName as the second slug-vs-display
    pair and explaining its distinct semantics (printer is the human, owner
    is the API vendor/wrapper-author). Reinforces the no-"USER"-sentinel
    rule and points at the publish-side gate.
    
    skills/printing-press-publish/SKILL.md Step 6 packaging gains a strict
    check on the freshly-copied .printing-press.json: refuses to publish
    when .printer is empty, equals the literal "USER"/"user" sentinel, or
    equals .owner (slug-fallback case). The generator's stderr warning at
    print time is advisory; the publish step is the enforcing gate so
    broken attribution rows cannot reach the library catalog.
    
    * fix(cli): avoid printer owner fallback false positives
    
    ---------
    
    Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
    Co-authored-by: Trevin Chow <trevin@trevinchow.com>
---
 AGENTS.md                                          |  2 +
 internal/cli/root.go                               |  4 +
 internal/generator/generator.go                    | 16 ++++
 internal/generator/plan_generate.go                | 43 ++++++++++
 internal/generator/printer_test.go                 | 97 ++++++++++++++++++++++
 internal/generator/templates/readme.md.tmpl        |  4 +
 internal/pipeline/climanifest.go                   | 10 ++-
 internal/pipeline/publish.go                       |  6 ++
 internal/spec/spec.go                              |  2 +
 skills/printing-press-publish/SKILL.md             | 11 +++
 .../printing-press-rich-auth/README.md             |  2 +
 .../cafe-bistro/.printing-press.json               |  2 +
 .../printing-press-golden/.printing-press.json     |  2 +
 .../printing-press-golden/README.md                |  2 +
 .../mcp-cloudflare/.printing-press.json            |  2 +
 .../public-param-golden/README.md                  |  2 +
 .../tier-routing-golden/.printing-press.json       |  2 +
 17 files changed, 208 insertions(+), 1 deletion(-)

diff --git a/AGENTS.md b/AGENTS.md
index cf40f8ad..fddd527b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -94,6 +94,8 @@ See [`docs/GLOSSARY.md`](docs/GLOSSARY.md) for the full term table and disambigu
 
 **`Owner` (slug) vs `OwnerName` (display) — keep them straight.** `APISpec` carries two related but semantically distinct fields. `Owner` is path-safe (e.g. `trevin-chow`) and drives Go module paths and `// Copyright YYYY <slug>.` headers — sanitized in `New()`. `OwnerName` is prose-shaped (e.g. `Trevin Chow`) and flows into Hermes `author:`, README byline, and other human-facing surfaces — preserved verbatim, YAML-escaped at template emission. Resolution paths are deliberately different: `OwnerName` reads raw `git config user.name` only (no `github.user` fallback, no slug sanitization, no `"USER"` default). When `OwnerName` is unset at `Generate()` time, the generator emits a stderr warning and falls back to the slug — non-fatal so the package stays reusable by tests and `mcp-sync`/`regen-merge`. The library-wide sweep tool overrides this code path with its own per-CLI authorship mapping. Don't conflate the two fields when authoring helpers, templates, or test fixtures.
 
+**`Printer` (slug @handle) vs `PrinterName` (display) — parallel pair, distinct from Owner/OwnerName.** `APISpec` and `CLIManifest` carry a second slug-vs-display pair for printer attribution. `Printer` is the GitHub @handle of the human who originally ran the press (e.g. `mvanhorn`), drives the per-CLI README byline link and the library-side registry attribution, and reads `git config github.user`. `PrinterName` is the prose-shaped display name (e.g. `Matt Van Horn`), rendered as the README byline parenthetical, and reads raw `git config user.name`. Resolution is tiered for both: the existing manifest wins over git config, so regens by other contributors do not overwrite the original printer (mirrors `resolveOwnerForExisting`'s preserve-on-regen guarantee). When `Printer` is unset at `Generate()` time, the generator emits a stderr warning and leaves it empty; the publish skill (Step 6) refuses to publish empty or literal `"USER"`/`"user"` sentinel values. Distinct from `Owner`/`OwnerName` (which carry the API-spec / wrapper-author identity); the printer is the human, the owner is the API vendor or wrapper author.
+
 ## Commit Style
 Format: `type(scope): description`. Both type and scope are required.
 
diff --git a/internal/cli/root.go b/internal/cli/root.go
index e7c029e7..f65ed773 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -170,6 +170,8 @@ func newGenerateCmd() *cobra.Command {
 					DocsURL:       docsURL,
 					OutputDir:     absOut,
 					Owner:         parsed.Owner,
+					Printer:       parsed.Printer,
+					PrinterName:   parsed.PrinterName,
 					RunID:         runID,
 					Spec:          parsed,
 					NovelFeatures: novelFeatures,
@@ -332,6 +334,8 @@ func newGenerateCmd() *cobra.Command {
 				SpecURL:       specURL,
 				OutputDir:     absOut,
 				Owner:         apiSpec.Owner,
+				Printer:       apiSpec.Printer,
+				PrinterName:   apiSpec.PrinterName,
 				RunID:         runID,
 				Spec:          apiSpec,
 				NovelFeatures: novelFeatures,
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 65be8a1f..a6ae6f0b 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -177,6 +177,15 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 	if s.OwnerName == "" {
 		s.OwnerName = resolveOwnerNameForExisting(outputDir)
 	}
+
+	// Preserve printer attribution from the manifest before consulting git config.
+	if s.Printer == "" {
+		s.Printer = resolvePrinterForExisting(outputDir)
+	}
+	// Preserve the prose-shaped printer display name when regenerating a CLI.
+	if s.PrinterName == "" {
+		s.PrinterName = resolvePrinterNameForExisting(outputDir)
+	}
 	g := &Generator{
 		Spec:      s,
 		OutputDir: outputDir,
@@ -1544,6 +1553,13 @@ func (g *Generator) Generate() error {
 		)
 		g.Spec.OwnerName = g.Spec.Owner
 	}
+	if g.Spec.Printer == "" {
+		// Publish enforces this so self-owned CLIs can still use matching owner/printer slugs.
+		fmt.Fprintf(os.Stderr,
+			"WARNING: spec.Printer is empty; README printer attribution will be omitted. "+
+				"Set `git config github.user` (your GitHub @handle) to populate this correctly before publishing.\n",
+		)
+	}
 	if err := g.prepareOutput(); err != nil {
 		return err
 	}
diff --git a/internal/generator/plan_generate.go b/internal/generator/plan_generate.go
index 3c2a47df..bb1dbb90 100644
--- a/internal/generator/plan_generate.go
+++ b/internal/generator/plan_generate.go
@@ -411,3 +411,46 @@ func sanitizeOwner(s string) string {
 		return -1
 	}, s)
 }
+
+// resolvePrinterForExisting preserves the original printer before consulting git config.
+func resolvePrinterForExisting(outputDir string) string {
+	if p := readManifestPrinter(outputDir); p != "" {
+		return p
+	}
+	return resolvePrinterForNew()
+}
+
+// readManifestPrinter returns the `printer` @handle from the manifest.
+func readManifestPrinter(outputDir string) string {
+	return readManifestField(outputDir, "printer")
+}
+
+// resolvePrinterForNew returns "" instead of a sentinel when github.user is unset.
+func resolvePrinterForNew() string {
+	if out, err := exec.Command("git", "config", "github.user").Output(); err == nil && len(out) > 0 {
+		return strings.TrimSpace(string(out))
+	}
+	return ""
+}
+
+// resolvePrinterNameForExisting preserves the printer display name on regen.
+func resolvePrinterNameForExisting(outputDir string) string {
+	if name := readManifestPrinterName(outputDir); name != "" {
+		return name
+	}
+	return resolvePrinterNameForNew()
+}
+
+// readManifestPrinterName returns the manifest printer display-name field.
+func readManifestPrinterName(outputDir string) string {
+	return readManifestField(outputDir, "printer_name")
+}
+
+// resolvePrinterNameForNew returns raw git user.name for a fresh print.
+func resolvePrinterNameForNew() string {
+	out, err := exec.Command("git", "config", "user.name").Output()
+	if err != nil {
+		return ""
+	}
+	return strings.TrimSpace(string(out))
+}
diff --git a/internal/generator/printer_test.go b/internal/generator/printer_test.go
new file mode 100644
index 00000000..c268d83f
--- /dev/null
+++ b/internal/generator/printer_test.go
@@ -0,0 +1,97 @@
+package generator
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestResolvePrinterForExistingManifestWins pins the regen-preservation tier.
+func TestResolvePrinterForExistingManifestWins(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"printer":"mvanhorn","cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+
+	assert.Equal(t, "mvanhorn", resolvePrinterForExisting(dir))
+}
+
+// TestResolvePrinterForExistingFallsThroughOnAbsentPrinter exercises the git-config tier.
+func TestResolvePrinterForExistingFallsThroughOnAbsentPrinter(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+
+	// We do not assert the value (depends on runner's git config), only
+	// that the fall-through path is exercised without panicking.
+	_ = resolvePrinterForExisting(dir)
+}
+
+// TestResolvePrinterForExistingFallsThroughOnEmptyPrinter treats empty as absent.
+func TestResolvePrinterForExistingFallsThroughOnEmptyPrinter(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"printer":"","cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+
+	_ = resolvePrinterForExisting(dir)
+}
+
+// TestReadManifestPrinterHandlesMissingFile keeps missing manifests non-fatal.
+func TestReadManifestPrinterHandlesMissingFile(t *testing.T) {
+	assert.Equal(t, "", readManifestPrinter(t.TempDir()))
+}
+
+// TestReadManifestPrinterHandlesMalformed keeps invalid JSON non-fatal.
+func TestReadManifestPrinterHandlesMalformed(t *testing.T) {
+	dir := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte("not json"), 0o644))
+	assert.Equal(t, "", readManifestPrinter(dir))
+}
+
+// TestReadManifestPrinterHandlesNonStringValue treats non-string values as missing.
+func TestReadManifestPrinterHandlesNonStringValue(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"printer":42,"cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+	assert.Equal(t, "", readManifestPrinter(dir))
+}
+
+// TestResolvePrinterNameForExistingManifestWins pins display-name preservation.
+func TestResolvePrinterNameForExistingManifestWins(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"printer_name":"Matt Van Horn","cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+
+	assert.Equal(t, "Matt Van Horn", resolvePrinterNameForExisting(dir))
+}
+
+// TestResolvePrinterNameForExistingFallsThroughOnAbsentName exercises git config.
+func TestResolvePrinterNameForExistingFallsThroughOnAbsentName(t *testing.T) {
+	dir := t.TempDir()
+	manifest := `{"schema_version":1,"cli_name":"foo-pp-cli","api_name":"foo"}`
+	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
+
+	// Returned value depends on runner's git config; just exercise the path.
+	_ = resolvePrinterNameForExisting(dir)
+}
+
+// TestReadManifestPrinterNameHandlesMissingFile keeps missing manifests non-fatal.
+func TestReadManifestPrinterNameHandlesMissingFile(t *testing.T) {
+	assert.Equal(t, "", readManifestPrinterName(t.TempDir()))
+}
+
+// TestGenerateLeavesPrinterEmptyWhenGitHandleUnset prevents owner-slug false positives.
+func TestGenerateLeavesPrinterEmptyWhenGitHandleUnset(t *testing.T) {
+	t.Setenv("GIT_CONFIG_COUNT", "1")
+	t.Setenv("GIT_CONFIG_KEY_0", "github.user")
+	t.Setenv("GIT_CONFIG_VALUE_0", "")
+
+	apiSpec := minimalSpec("self-owned")
+	outputDir := filepath.Join(t.TempDir(), "self-owned-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	assert.Empty(t, gen.Spec.Printer)
+}
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index ed0376ef..f8f1f62e 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -15,6 +15,10 @@
 
 Learn more at [{{.ProseName}}]({{.WebsiteURL}}).
 {{- end}}
+{{- if .Printer}}
+
+Printed by [@{{.Printer}}](https://github.com/{{.Printer}}){{if .PrinterName}} ({{.PrinterName}}){{end}}.
+{{- end}}
 
 ## Install
 
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index f69b6ea7..c0364bf2 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -49,7 +49,11 @@ type CLIManifest struct {
 	// Owner is the attribution recorded in generated copyright headers
 	// (for example "hiten-shah"). Persisted here so subsequent regens
 	// preserve attribution regardless of who's running the generator.
-	Owner                string            `json:"owner,omitempty"`
+	Owner string `json:"owner,omitempty"`
+	// Printer is the original printer's GitHub handle, preserved across regens.
+	Printer string `json:"printer,omitempty"`
+	// PrinterName is the optional display name rendered beside the printer handle.
+	PrinterName          string            `json:"printer_name,omitempty"`
 	SpecURL              string            `json:"spec_url,omitempty"`
 	SpecPath             string            `json:"spec_path,omitempty"`
 	SpecFormat           string            `json:"spec_format,omitempty"`
@@ -379,6 +383,8 @@ type GenerateManifestParams struct {
 	DocsURL       string   // --docs URL, if used
 	OutputDir     string
 	Owner         string                 // resolved owner attribution (manifest preserve > copyright parse > git config)
+	Printer       string                 // resolved printer @handle (manifest preserve > git config github.user > empty)
+	PrinterName   string                 // resolved printer display name (manifest preserve > git config user.name > empty)
 	RunID         string                 // YYYYMMDD-HHMMSS, derived from --research-dir basename when empty
 	Spec          *spec.APISpec          // parsed spec for MCP metadata (nil if unavailable)
 	NovelFeatures []NovelFeatureManifest // transcendence features from research (nil if unavailable)
@@ -417,6 +423,8 @@ func WriteManifestForGenerate(p GenerateManifestParams) error {
 		CLIName:              naming.CLI(p.APIName),
 		RunID:                p.RunID,
 		Owner:                p.Owner,
+		Printer:              p.Printer,
+		PrinterName:          p.PrinterName,
 	}
 
 	// Populate spec_url / spec_path from the first spec source.
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index 98bf4a1a..346339b2 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -221,6 +221,12 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
 			if existing.Owner != "" {
 				m.Owner = existing.Owner
 			}
+			if existing.Printer != "" {
+				m.Printer = existing.Printer
+			}
+			if existing.PrinterName != "" {
+				m.PrinterName = existing.PrinterName
+			}
 			if existing.CatalogEntry != "" {
 				m.CatalogEntry = existing.CatalogEntry
 			}
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 2b4776d1..f7160c0e 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -107,6 +107,8 @@ type APISpec struct {
 	EndpointTemplateVars []string            `yaml:"endpoint_template_vars,omitempty" json:"endpoint_template_vars,omitempty"`
 	Owner                string              `yaml:"owner,omitempty" json:"owner,omitempty"`                   // GitHub owner for import paths and Homebrew tap
 	OwnerName            string              `yaml:"owner_name,omitempty" json:"owner_name,omitempty"`         // Display name (e.g. "Trevin Chow") for prose surfaces — Hermes author:, README byline. Distinct from Owner (slug) which drives module paths and copyright headers.
+	Printer              string              `yaml:"printer,omitempty" json:"printer,omitempty"`               // GitHub @handle of the human who ran the press for this CLI. Drives the per-CLI README byline link and the registry-side attribution. Distinct from Owner (the API-spec owner / wrapper-author identity).
+	PrinterName          string              `yaml:"printer_name,omitempty" json:"printer_name,omitempty"`     // Display name of the printer (e.g. "Matt Van Horn") for prose surfaces — README byline parenthetical. Resolution path mirrors OwnerName: raw git config user.name, no slug fallback, no "USER" sentinel.
 	Kind                 string              `yaml:"kind,omitempty" json:"kind,omitempty"`                     // "rest" (default) or "synthetic" — synthetic CLIs aggregate multiple sources beyond the spec; dogfood's path-validity check is relaxed accordingly
 	SpecSource           string              `yaml:"spec_source,omitempty" json:"spec_source,omitempty"`       // official, community, sniffed, docs — affects generated client defaults
 	ClientPattern        string              `yaml:"client_pattern,omitempty" json:"client_pattern,omitempty"` // rest (default), proxy-envelope — affects generated HTTP client
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index 6589ec0d..8d365f56 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -400,6 +400,17 @@ cp -r "$STAGING_DIR/library/<category>/<cli-name>" "$PUBLISH_REPO_DIR/library/<c
 # Remove binaries (should not be committed)
 rm -f "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>"
 
+# Strict-validate printer attribution before it reaches README and registry surfaces.
+PRINTER=$(jq -r '.printer // ""' "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json")
+if [ -z "$PRINTER" ]; then
+  echo "ERROR: manifest .printer is empty. Set 'git config --global github.user <your-handle>' and re-print before publishing."
+  exit 1
+fi
+if [ "$PRINTER" = "USER" ] || [ "$PRINTER" = "user" ]; then
+  echo "ERROR: manifest .printer is the literal sentinel \"$PRINTER\" (git config github.user was unset at print time). Set it and re-print before publishing."
+  exit 1
+fi
+
 # Regenerate the flat cli-skills mirror from the library tree so library PR CI passes mirror parity.
 if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then
   (cd "$PUBLISH_REPO_DIR" && go run ./tools/generate-skills/main.go)
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 c40c6b35..6ef17e40 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
@@ -2,6 +2,8 @@
 
 Purpose-built fixture for rich auth env-var model coverage.
 
+Printed by [@printing-press-golden](https://github.com/printing-press-golden) (printing-press-golden).
+
 ## Install
 
 The recommended path installs both the `printing-press-rich-pp-cli` binary and the `pp-printing-press-rich` agent skill in one shot:
diff --git a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/.printing-press.json b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/.printing-press.json
index 2d2edcf9..72b0e9e6 100644
--- a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/.printing-press.json
+++ b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/.printing-press.json
@@ -12,6 +12,8 @@
   "mcp_ready": "full",
   "mcp_tool_count": 1,
   "owner": "printing-press-golden",
+  "printer": "printing-press-golden",
+  "printer_name": "printing-press-golden",
   "printing_press_version": "<PRINTING_PRESS_VERSION>",
   "schema_version": 1,
   "spec_checksum": "sha256:8cfba131c384836e8dde461511c9fd63cf2cbab7793553067765d515f2007aa1",
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
index a892f8cd..7059887f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
@@ -13,6 +13,8 @@
   "mcp_ready": "full",
   "mcp_tool_count": 8,
   "owner": "printing-press-golden",
+  "printer": "printing-press-golden",
+  "printer_name": "printing-press-golden",
   "printing_press_version": "<PRINTING_PRESS_VERSION>",
   "schema_version": 1,
   "spec_checksum": "sha256:333b2c06c645b8f394fbbaaedf8126e06919529d7d9305c94c5ebcf1f5a897b5",
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 9d5bdaf8..5c5d2154 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
@@ -2,6 +2,8 @@
 
 Purpose-built fixture for golden generation coverage.
 
+Printed by [@printing-press-golden](https://github.com/printing-press-golden) (printing-press-golden).
+
 ## Install
 
 The recommended path installs both the `printing-press-golden-pp-cli` binary and the `pp-printing-press-golden` agent skill in one shot:
diff --git a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/.printing-press.json b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/.printing-press.json
index b278c199..ca3ffc18 100644
--- a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/.printing-press.json
+++ b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/.printing-press.json
@@ -12,6 +12,8 @@
   "mcp_ready": "full",
   "mcp_tool_count": 1,
   "owner": "printing-press-golden",
+  "printer": "printing-press-golden",
+  "printer_name": "printing-press-golden",
   "printing_press_version": "<PRINTING_PRESS_VERSION>",
   "schema_version": 1,
   "spec_checksum": "sha256:46c60ba489f2e138ffe6ab01531f2ed550369299820342a7f0093b94d0fc17b4",
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 ec2b4076..2a6280ec 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
@@ -2,6 +2,8 @@
 
 Public parameter name golden fixture
 
+Printed by [@printing-press-golden](https://github.com/printing-press-golden) (printing-press-golden).
+
 ## Install
 
 The recommended path installs both the `public-param-golden-pp-cli` binary and the `pp-public-param-golden` agent skill in one shot:
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/.printing-press.json b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/.printing-press.json
index 8d1a7a22..23f1bde6 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/.printing-press.json
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/.printing-press.json
@@ -15,6 +15,8 @@
   "mcp_ready": "full",
   "mcp_tool_count": 3,
   "owner": "printing-press-golden",
+  "printer": "printing-press-golden",
+  "printer_name": "printing-press-golden",
   "printing_press_version": "<PRINTING_PRESS_VERSION>",
   "schema_version": 1,
   "spec_checksum": "sha256:a059b4da997e0e835f0cdcd911ef5894fc048b50506bdcd90260a7478efb2958",

← ca580efc fix(cli): align generated install and rename metadata (#749)  ·  back to Cli Printing Press  ·  fix(cli): harden force-generate preservation (#750) c397021f →