--- name: printing-press-publish description: Publish a generated CLI to the printing-press-library repo version: 0.1.0 min-binary-version: "4.0.0" allowed-tools: - Bash - Read - Write - Edit - Glob - Grep - AskUserQuestion --- # /printing-press publish Publish a generated CLI from your local library to the [printing-press-library](https://github.com/mvanhorn/printing-press-library) repo as a pull request. ```bash /printing-press publish notion-pp-cli /printing-press publish notion /printing-press publish notion --from-polish /printing-press publish notion --skip-live-test=auth-unavailable /printing-press publish ``` ## Direct User Invocation Required Publishing can fork `mvanhorn/printing-press-library`, push a branch, and open or update a PR. Before setup or validation, check the invocation context. If this skill was invoked as a chained continuation from `printing-press-polish`'s Publish Offer, including an `AskUserQuestion` answer or auto-resolved polish recommendation, stop immediately and tell the user to send `/printing-press-publish --from-polish` in a fresh message. A fresh user-authored request that explicitly asks to publish is sufficient; do not add another confirmation prompt on top of a direct publish request. If the fresh user-authored request includes `--from-polish`, record `POLISH_HANDOFF=true` for the terminal-state step and ignore that marker when resolving the CLI name. The marker is not a second confirmation and is not passed to `cli-printing-press`; it only preserves standalone polish's old post-publish retro offer after the fresh-turn publish completes. If the fresh user-authored request includes `--skip-live-test=`, record the exact non-empty reason as `SKIP_LIVE_TEST_REASON` and remove the flag before resolving the CLI name. This is the only supported escape valve for the publish-time live test gate. Use it only for auth-unavailable, known upstream outage, LAN-unreachable hardware APIs, or similarly concrete operator-approved cases; never infer a skip from ordinary latency or from the presence of an older Phase 5 marker. The public library treats `library///.printing-press.json` and `manifest.json` as the source of truth for registry-display fields. Do not edit `registry.json`, README catalog cells, or `cli-skills/pp-/SKILL.md` in publish PRs; all three are bot-regenerated post-merge by the library's own workflows. The library's `Fail on changes to generated artifacts` check in `verify-library-conventions.yml` hard-fails any PR — fork or same-repo — whose diff against base touches `registry.json` or `cli-skills/pp-*/SKILL.md`, so a publish that includes either is pre-rejected before review. ## Setup Before doing anything else: ```bash # min-binary-version: 4.0.0 # Derive scope first — needed for local build detection _scope_dir="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")" _scope_dir="$(cd "$_scope_dir" && pwd -P)" # Prefer local build when running from inside the printing-press repo. _press_repo=false if [ -x "$_scope_dir/cli-printing-press" ] && [ -d "$_scope_dir/cmd/cli-printing-press" ]; then _press_repo=true export PATH="$_scope_dir:$PATH" echo "Using local build: $_scope_dir/cli-printing-press" elif ! command -v cli-printing-press >/dev/null 2>&1; then if [ -x "$HOME/go/bin/cli-printing-press" ]; then echo "cli-printing-press found at ~/go/bin/cli-printing-press but not on PATH." echo "Add GOPATH/bin to your PATH: export PATH=\"\$HOME/go/bin:\$PATH\"" else echo "cli-printing-press binary not found." echo "Install with: go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest" fi return 1 2>/dev/null || exit 1 fi # Resolve and emit the absolute path the agent must use for every later # `cli-printing-press` invocation. `export PATH` above only affects this one # Bash tool call; subsequent calls open a fresh shell and resolve bare # `cli-printing-press` against the user's default PATH, where a stale global # can silently shadow the local build. The agent captures this marker and # substitutes the absolute path into every later invocation. if [ "$_press_repo" = "true" ]; then PRINTING_PRESS_BIN="$_scope_dir/cli-printing-press" else PRINTING_PRESS_BIN="$(command -v cli-printing-press 2>/dev/null || true)" fi echo "PRINTING_PRESS_BIN=$PRINTING_PRESS_BIN" PRESS_BASE="$(basename "$_scope_dir" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g; s/^-+//; s/-+$//')" if [ -z "$PRESS_BASE" ]; then PRESS_BASE="workspace" fi PRESS_SCOPE="$PRESS_BASE-$(printf '%s' "$_scope_dir" | shasum -a 256 | cut -c1-8)" PRESS_HOME="${PRINTING_PRESS_HOME:-$HOME/printing-press}" PRESS_RUNSTATE="$PRESS_HOME/.runstate/$PRESS_SCOPE" PRESS_LIBRARY="$PRESS_HOME/library" PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts" PRESS_CURRENT="$PRESS_RUNSTATE/current" mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT" ``` After running the setup contract, capture the `PRINTING_PRESS_BIN=` line from stdout. **Every subsequent `cli-printing-press ...` invocation in this skill must use that absolute path** (substitute the value, not the literal `$PRINTING_PRESS_BIN` token) — `export PATH` above only affects the single Bash tool call it runs in, so later calls open a fresh shell where bare `cli-printing-press` resolves against the user's default `PATH` and a stale global can shadow the local build. After capturing the binary path, check binary version compatibility. Read the `min-binary-version` field from this skill's YAML frontmatter. Run ` version --json` and parse the version from the output. Compare it to `min-binary-version` using semver rules. If the installed binary is older than the minimum, stop immediately and tell the user: "cli-printing-press binary vX.Y.Z is older than the minimum required vA.B.C. Run `go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest` to update." ## Configuration ``` PUBLISH_REPO_URL="https://github.com/mvanhorn/printing-press-library" PUBLISH_REPO_DIR="$PRESS_HOME/.publish-repo-$PRESS_SCOPE" PUBLISH_CONFIG="$PRESS_HOME/.publish-config-$PRESS_SCOPE.json" ``` ### Publish config `$PUBLISH_CONFIG` stores persistent publish settings as JSON. On first publish, create it with defaults. The user can edit it to change the library repo or module path base. ```json { "managed_by": "printing-press-publish", "repo_url": "https://github.com/mvanhorn/printing-press-library", "access": "push", "protocol": "ssh", "clone_path": "/printing-press/.publish-repo-", "scope_dir": "/absolute/path/to/source/worktree", "module_path_base": "github.com/mvanhorn/printing-press-library/library" } ``` The `module_path_base` field sets the Go module path prefix for published CLIs. During packaging, the full module path is constructed as `//`. If the user wants CLIs published to a different repo or path, they edit this field. Store expanded absolute paths for `clone_path` and `scope_dir` so cleanup can check them without relying on shell-specific `~` expansion. The `managed_by` field is required before cleanup may delete anything. ### Scoped clone cleanup Before creating or reusing `$PUBLISH_REPO_DIR`, prune scoped publish clones whose source worktree no longer exists. This keeps concurrent worktrees isolated without accumulating one library clone forever per short-lived worktree. ```bash find "$PRESS_HOME" -maxdepth 1 -name '.publish-config-*.json' -type f | while read -r cfg; do [ "$cfg" = "$PUBLISH_CONFIG" ] && continue managed_by=$(jq -r '.managed_by // empty' "$cfg" 2>/dev/null || true) scope_dir=$(jq -r '.scope_dir // empty' "$cfg" 2>/dev/null || true) clone_path=$(jq -r '.clone_path // empty' "$cfg" 2>/dev/null || true) [ "$managed_by" = "printing-press-publish" ] || continue [ -z "$scope_dir" ] && continue [ -e "$scope_dir" ] && continue [ -d "$clone_path/.git" ] || continue case "$clone_path" in "$PRESS_HOME"/.publish-repo-*) ;; *) continue ;; esac origin=$(git -C "$clone_path" remote get-url origin 2>/dev/null || true) case "$origin" in *mvanhorn/printing-press-library*|*/*/printing-press-library*) ;; *) continue ;; esac [ -z "$(git -C "$clone_path" status --porcelain)" ] || continue [ "$(git -C "$clone_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" = "main" ] || continue rm -rf "$clone_path" "$cfg" done ``` ## Step 1: Prerequisites Verify `gh` is authenticated: ```bash gh auth status ``` If this fails, stop and tell the user: "GitHub CLI is not authenticated. Run `gh auth login` first." ## Step 2: Resolve API Slug Run: ```bash cli-printing-press library list --json ``` Parse the JSON output into a list of CLIs. The library is now keyed by API slug (the directory name), not CLI name. **Name resolution order** (matches the score skill for consistency): 1. **Exact match:** If the argument matches a directory name (API slug) exactly, use it 2. **CLI name match:** If no exact match, try matching against `cli_name` fields, then derive the API slug from the manifest's `api_name` field 3. **Suffix match:** If no match yet, try `-pp-cli` against `cli_name` fields 4. **Glob match:** If no suffix match, search for entries where `cli_name` or `api_name` contains the argument as a substring. Cap at 5 most-recent matches. If multiple matches, present them via AskUserQuestion and let the user pick 5. **No match:** List all available CLIs and ask the user to pick or re-enter 6. **No argument:** If invoked with no name, list all CLIs sorted by modification time and let the user pick Once resolved, read the manifest's `api_name` field to get the API slug. Use this slug for all downstream operations (branch names, registry entries, collision detection, path construction). The `cli_name` from the manifest is only used for binary-level operations. When presenting matches, show the API slug and modification time in a human-friendly format (e.g., "2 hours ago", "3 days ago"). ## Step 3: Determine Category Read `.printing-press.json` from the resolved CLI directory. **Category resolution order:** 1. If the manifest has a `category` field, present it for confirmation: > "Publishing as ****. OK?" Give the user the option to change it 2. If no `category` but `catalog_entry` is present, look it up: ```bash cli-printing-press catalog show --json ``` Extract the category from the result. Present for confirmation 3. If neither provides a category, present the full list via AskUserQuestion: - developer-tools, monitoring, cloud, project-management - productivity, social-and-messaging, sales-and-crm, marketing - payments, auth, commerce, ai, media-and-entertainment, devices, other ## Step 4: Validate Run: ```bash cli-printing-press publish validate --dir --json ``` `govulncheck` in this step is intentionally scoped to `` 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: ``` Validating ... manifest PASS phase5 PASS go mod tidy PASS govulncheck PASS go vet PASS go build PASS --help PASS --version PASS manuscripts WARN (no manuscripts found) ``` If `"passed": false`, report the failing checks and **stop**. Do not create a partial PR. The `manifest` check is authoritative for the public-library provenance contract: current `schema_version`, `run_id`, `printing_press_version`, `printer`, `printer_name`, and MCP metadata files when MCP is advertised. If it fails, tell the user to re-print or re-package with current Printing Press metadata before opening the library PR. Save the `help_output` field from the result — it's used in the PR description. ## Step 4.5: Live End-to-End Gate Before touching the managed publish clone, rerun the live behavioral gate against the CLI that is about to be published. Step 4 proves the source builds and validates structurally; this step proves the current post-edit tree still works against the real upstream API. Do not rely on an older `phase5-acceptance.json` from generation or polish because the CLI may have been hand-edited since that marker was written. Resolve the Phase 5 proofs directory from the CLI manifest: ```bash MANIFEST="$CLI_DIR/.printing-press.json" API_SLUG=$(jq -r '.api_name // empty' "$MANIFEST") CLI_NAME=$(jq -r '.cli_name // empty' "$MANIFEST") RUN_ID=$(jq -r '.run_id // empty' "$MANIFEST") AUTH_TYPE=$(jq -r '.auth_type // "none"' "$MANIFEST") AUTH_ENV=$(jq -r '.auth_env_vars[0] // empty' "$MANIFEST") if [ -z "$API_SLUG" ] || [ -z "$RUN_ID" ]; then echo "ERROR: manifest is missing api_name or run_id; cannot run publish live gate." exit 1 fi PROOFS_DIR="$CLI_DIR/.manuscripts/$RUN_ID/proofs" if [ ! -d "$PROOFS_DIR" ] && [ -n "$API_SLUG" ] && [ -d "$PRESS_MANUSCRIPTS/$API_SLUG/$RUN_ID/proofs" ]; then PROOFS_DIR="$PRESS_MANUSCRIPTS/$API_SLUG/$RUN_ID/proofs" elif [ ! -d "$PROOFS_DIR" ] && [ -n "$CLI_NAME" ] && [ -d "$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID/proofs" ]; then PROOFS_DIR="$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID/proofs" fi mkdir -p "$PROOFS_DIR" ``` If `SKIP_LIVE_TEST_REASON` is unset, run full live dogfood and write a fresh acceptance marker into that proofs directory: ```bash LIVE_GATE_JSON="$PROOFS_DIR/publish-live-gate.json" LIVE_GATE_ARGS=( dogfood --dir "$CLI_DIR" --live --level full --timeout 120s --write-acceptance "$PROOFS_DIR/phase5-acceptance.json" --json ) if [ -n "$AUTH_ENV" ]; then LIVE_GATE_ARGS+=(--auth-env "$AUTH_ENV") fi rm -f "$PROOFS_DIR/phase5-skip.json" if ! "$PRINTING_PRESS_BIN" "${LIVE_GATE_ARGS[@]}" >"$LIVE_GATE_JSON"; then echo "Publish live gate failed. See $LIVE_GATE_JSON and $PROOFS_DIR/phase5-acceptance.json." jq -r '.tests[]? | select(.status == "fail") | "- \(.command) [\(.kind)]: \(.reason // "failed")"' "$LIVE_GATE_JSON" 2>/dev/null || true exit 1 fi ``` On failure, stop exactly like Step 4's `passed: false`: no managed clone, no branch, no package, no PR. Report the failed command, exit code when present, stderr or reason snippet, and the path to the fresh proof files so the operator can re-run dogfood and fix the CLI. If `SKIP_LIVE_TEST_REASON` is set from `--skip-live-test=`, write a fresh skip marker instead of running dogfood: ```bash SKIP_REASON_LOWER=$(printf '%s' "$SKIP_LIVE_TEST_REASON" | tr '[:upper:]' '[:lower:]') case "$AUTH_TYPE" in api_key|bearer_token|oauth2) ;; none) case "$SKIP_REASON_LOWER" in *upstream*outage*|lan-unreachable-from-generation-host) ;; *) echo "ERROR: --skip-live-test is only valid for auth_type=none during a known upstream outage or LAN-unreachable hardware case." exit 1 ;; esac ;; *) echo "ERROR: --skip-live-test is not valid for auth_type=$AUTH_TYPE. Run the live gate instead." exit 1 ;; esac API_KEY_AVAILABLE=false if [ -n "$AUTH_ENV" ] && [ -n "${!AUTH_ENV:-}" ]; then API_KEY_AVAILABLE=true fi rm -f "$PROOFS_DIR/phase5-acceptance.json" jq -n \ --arg api "$API_SLUG" \ --arg run "$RUN_ID" \ --arg reason "$SKIP_LIVE_TEST_REASON" \ --arg auth "$AUTH_TYPE" \ --argjson api_key_available "$API_KEY_AVAILABLE" \ --argjson browser_session_available false \ '{ schema_version: 1, api_name: $api, run_id: $run, status: "skip", level: "none", skip_reason: $reason, auth_context: { type: $auth, api_key_available: $api_key_available, browser_session_available: $browser_session_available } }' > "$PROOFS_DIR/phase5-skip.json" if [ "$SKIP_REASON_LOWER" = "lan-unreachable-from-generation-host" ]; then tmp_marker=$(mktemp "${TMPDIR:-/tmp}/phase5-skip.XXXXXX") jq '.auth_context.local_network_only = true' "$PROOFS_DIR/phase5-skip.json" > "$tmp_marker" && mv "$tmp_marker" "$PROOFS_DIR/phase5-skip.json" fi LIVE_GATE_JSON="" ``` Then rerun Step 4's validation: ```bash "$PRINTING_PRESS_BIN" publish validate --dir "$CLI_DIR" --json ``` This second validation proves the fresh acceptance or skip marker satisfies the same Phase 5 contract that package and publish rely on. If it fails, stop before Step 5. ## Step 5: Managed Clone The publish skill manages its own clone of the library repo at `$PUBLISH_REPO_DIR`. ### First-time setup If `$PUBLISH_REPO_DIR` does not exist: 1. **Detect push access:** ```bash GH_USER=$(gh api user --jq '.login') HAS_PUSH=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false") ``` 2. **Detect git protocol:** ```bash USE_SSH=false if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then USE_SSH=true fi ``` 3. **Clone based on access:** **Push access** (`HAS_PUSH` is `true`): ```bash # Clone directly — origin IS the upstream if [ "$USE_SSH" = "true" ]; then REPO_URL="git@github.com:mvanhorn/printing-press-library.git" else REPO_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$REPO_URL" "$PUBLISH_REPO_DIR" ``` **No push access** (`HAS_PUSH` is `false`): ```bash # Fork first — fail explicitly if forking is blocked if ! gh repo fork mvanhorn/printing-press-library --clone=false 2>&1; then echo "ERROR: Could not fork mvanhorn/printing-press-library." echo "The repo may restrict forking, or you may already have a fork with a different name." echo "Fork manually at https://github.com/mvanhorn/printing-press-library/fork" exit 1 fi FORK="$GH_USER/printing-press-library" # Build URLs based on protocol preference if [ "$USE_SSH" = "true" ]; then FORK_URL="git@github.com:$FORK.git" UPSTREAM_URL="git@github.com:mvanhorn/printing-press-library.git" else FORK_URL="https://github.com/$FORK.git" UPSTREAM_URL="https://github.com/mvanhorn/printing-press-library.git" fi git clone --depth 50 "$FORK_URL" "$PUBLISH_REPO_DIR" cd "$PUBLISH_REPO_DIR" git remote add upstream "$UPSTREAM_URL" git fetch upstream ``` 4. **Cache the config:** ```json { "managed_by": "printing-press-publish", "repo_url": "https://github.com/mvanhorn/printing-press-library", "access": "push or fork", "gh_user": "", "protocol": "ssh or https", "clone_path": "", "scope_dir": "", "module_path_base": "github.com/mvanhorn/printing-press-library/library" } ``` Write to `$PUBLISH_CONFIG`. The `access` field determines the flow for all subsequent steps. The `gh_user` field is used for cross-repo PR heads. The `module_path_base` always references the upstream repo (PRs land there). ### Subsequent publishes Read `$PUBLISH_CONFIG`, then re-check access in case it changed (user was granted push access, or access was revoked): ```bash CURRENT_ACCESS=$(gh api repos/mvanhorn/printing-press-library --jq '.permissions.push' 2>/dev/null || echo "false") CACHED_ACCESS=$(jq -r .access "$PUBLISH_CONFIG") if [ "$CURRENT_ACCESS" = "true" ] && [ "$CACHED_ACCESS" = "fork" ]; then echo "Access upgraded to push. Reconfiguring clone..." rm -rf "$PUBLISH_REPO_DIR" # Re-run first-time setup with push access fi if [ "$CURRENT_ACCESS" = "false" ] && [ "$CACHED_ACCESS" = "push" ]; then echo "Push access revoked. Reconfiguring clone with fork..." rm -rf "$PUBLISH_REPO_DIR" # Re-run first-time setup with fork access fi ``` If the clone was removed due to an access change, re-run first-time setup above. Otherwise, freshen the clone to match the canonical upstream: ```bash cd "$PUBLISH_REPO_DIR" if [ "$(jq -r .access $PUBLISH_CONFIG)" = "push" ]; then # Push access: origin IS the upstream git fetch origin git checkout main git reset --hard origin/main else # Fork: origin is the fork, upstream is canonical git fetch upstream git checkout main git reset --hard upstream/main # Also sync origin (fork) so git push works cleanly git push origin main --force-with-lease 2>/dev/null || true fi ``` Verify the clone is healthy: ```bash git rev-parse --is-inside-work-tree test "$(git rev-parse --abbrev-ref HEAD)" = "main" ``` If this fails, the clone is corrupt. Remove `$PUBLISH_REPO_DIR` and re-run first-time setup. ### Interrupted state recovery Before creating a new branch, check for uncommitted changes: ```bash cd "$PUBLISH_REPO_DIR" git status --porcelain ``` If there are uncommitted changes, ask the user via AskUserQuestion: - "Reset and start fresh" - "Continue with existing changes" If reset, run `git checkout -- . && git clean -fd`. ### Pre-package publication-state snapshot Before Step 6 mutates the managed clone, record whether this API slug already exists in the public library tree. Step 6 removes and replaces `library/*/`, so any collision or publication-path decision made after packaging must use this pre-package snapshot, not a fresh `ls`. ```bash PREEXISTING_MERGED_PATHS=$(ls "$PUBLISH_REPO_DIR/library"/*/"" 2>/dev/null || true) PREEXISTING_MERGED_COLLISION=false if [ -n "$PREEXISTING_MERGED_PATHS" ]; then PREEXISTING_MERGED_COLLISION=true fi ``` ## Step 6: Package Read `$PUBLISH_CONFIG` to get `module_path_base`. Construct the full module path using the API slug (not the CLI name): ``` MODULE_PATH="//" ``` For example: `github.com/mvanhorn/printing-press-library/library/productivity/notion` Run `publish package` with `--target` to stage the CLI into a unique temporary directory, then copy it into the publish repo: ```bash PUBLISH_STAGING_ROOT="/tmp/printing-press/publish" mkdir -p "$PUBLISH_STAGING_ROOT" STAGING_PARENT="$(mktemp -d "$PUBLISH_STAGING_ROOT/-XXXXXX")" STAGING_DIR="$STAGING_PARENT/package" cli-printing-press publish package \ --dir \ --category \ --target "$STAGING_DIR" \ --module-path "$MODULE_PATH" \ --json ``` Parse the JSON result. Note the `staged_dir`, `module_path`, `manuscripts_included`, and `run_id`. The `module_path` field confirms the Go module path that was set in the packaged CLI's `go.mod` and import paths. `publish package` performs the mandatory vendor-prefix secret scan over the staged CLI, including copied manuscripts, before returning success. If it reports `vendor-prefix tokens detected`, stop and remove or redact the reported file:line findings before retrying. This is a hard gate and does not depend on `gitleaks`, `trufflehog`, or destination-repo push protection. Then copy the staged CLI into the publish repo, replacing any existing version: ```bash # Remove existing version (handles category changes) rm -rf "$PUBLISH_REPO_DIR/library"/*/"" # Copy staged CLI into publish repo (slug-keyed directory) cp -r "$STAGING_DIR/library//" "$PUBLISH_REPO_DIR/library//" # Remove root-level binaries (should not be committed). publish package # already strips these before the copy; this rm -f is belt-and-suspenders # for the agent path. Cover all three names the Makefile/`go build ./cmd/...` # can drop: bare slug, CLI binary, MCP peer. rm -f "$PUBLISH_REPO_DIR/library///" \ "$PUBLISH_REPO_DIR/library///" \ "$PUBLISH_REPO_DIR/library///-pp-mcp" # Defense-in-depth: validate printer attribution before README and registry surfaces. PRINTER=$(jq -r '.printer // ""' "$PUBLISH_REPO_DIR/library///.printing-press.json") PRINTER_NAME=$(jq -r '.printer_name // ""' "$PUBLISH_REPO_DIR/library///.printing-press.json") if [ -z "$PRINTER" ]; then echo "ERROR: manifest .printer is empty. Set 'git config --global github.user ' 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 if [ -z "$PRINTER_NAME" ]; then echo "ERROR: manifest .printer_name is empty. Set 'git config --global user.name ' and re-print before publishing." exit 1 fi # Do NOT regenerate or commit `cli-skills/pp-/SKILL.md` or # `registry.json` here. Both are regenerated post-merge by the library's # `generate-skills.yml` and `generate-registry.yml` workflows via # `[skip ci]` bot commits. The library's `Fail on changes to generated # artifacts` check in `verify-library-conventions.yml` hard-fails any PR # whose diff against base touches these files, regardless of fork vs # same-repo origin. The library no longer has an in-PR auto-fix path; # do not re-introduce a mirror or registry regen here. # Verify this changed/new CLI builds and has no reachable Go vulnerabilities from the publish repo cd "$PUBLISH_REPO_DIR/library//" \ && go build ./... \ && go run golang.org/x/vuln/cmd/govulncheck@v1.3.0 ./... ``` Keep vulnerability verification scoped to `library//` 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: ```bash rm -rf "$STAGING_PARENT" ``` Note: `staged_dir` is keyed by the API slug (e.g., `espn`), matching the publish repo's directory layout. The copy step is a same-name copy, not a rename. ## Step 6.5: Record Customizations Before collision detection or branch creation, inspect the packaged CLI's customizations index: ```bash PATCHES_INDEX="$PUBLISH_REPO_DIR/library///.printing-press-patches.json" if [ ! -f "$PATCHES_INDEX" ]; then echo "ERROR: packaged CLI is missing .printing-press-patches.json. Reprint with a current cli-printing-press binary before publishing." exit 1 fi if ! jq -e ' (.schema_version | type == "number") and (.applied_at | type == "string" and length > 0) and (.base_run_id | type == "string" and length > 0) and (.base_printing_press_version | type == "string" and length > 0) and (.patches | type == "array") ' "$PATCHES_INDEX" >/dev/null; then echo "ERROR: packaged CLI has malformed .printing-press-patches.json. Reprint with a current cli-printing-press binary before publishing." exit 1 fi ``` Fresh prints from current `cli-printing-press generate` include this file with `"patches": []`; leave that unchanged when no hand customization was made after generation. If the file is missing, the CLI was generated by an older binary; reprint with a current `cli-printing-press` build rather than synthesizing the deterministic provenance fields by hand. ## Step 6.6: Record contributor attribution When the human running this publish is **not** the CLI's original creator, record them as a contributor so they are credited in the README byline, NOTICE, and the public registry. The command is idempotent — it skips the creator and anyone already listed — so it is safe to run on every publish: ```bash "$PRINTING_PRESS_BIN" contributors add \ --dir "$PUBLISH_REPO_DIR/library//" \ || echo "note: this binary predates 'contributors add'; skipping contributor recording" ``` The step is best-effort: `contributors add` is an additive command, so a binary that predates it simply skips recording rather than blocking the publish (the `min-binary-version` floor only tracks the major). Pass `--front` when this publish is a reprint (a from-scratch regeneration) so the reprinter is listed first among contributors. Never edit `contributors[]` or the `creator` block by hand — the creator is permanent, and the command owns the list (matching the manifest-as-authority rule). If you changed generated CLI files during the print or publish session, append one concise entry per customization to `patches[]` before opening the library PR. This index is the durable hand-edit contract that tells future agents and regen tooling what must be preserved beyond generator output. Use this shape: ```json { "id": "-", "summary": "What changed (one sentence).", "reason": "Why the generated output needed this customization.", "files": ["internal/cli/example.go"], "validated_outcome": "Optional: focused check that proved the customization.", "upstream_issue": "Optional: https://github.com/mvanhorn/cli-printing-press/issues/" } ``` Rules: - Use kebab-case ids prefixed with the API slug for grep-ability across the public library. - Keep `summary` and `reason` short. The manifest is an index, not a duplicate of the git diff. - Include non-Go support files in `files` when they are part of the same code-level customization. README/SKILL.md-only polish does not need a patch manifest entry. - Inline `// PATCH(...)` source comments are optional navigation aids. The public library verifier currently requires the patches file to exist and `patches` to be an array; it does not require a marker/comment pairing. - If an entry exists only to work around an old verifier or pipeline bug that no longer applies, delete the stale workaround entry instead of carrying it forward. For the authoritative public-library authoring contract, read the `mvanhorn/printing-press-library` `AGENTS.md` section "`.printing-press-patches.json` records library-side customizations". ## Step 7: Collision Detection & Resolution After the managed clone is freshened, check for name collisions before creating a branch or PR. This replaces the previous "Check for Existing PR" step. ### Detection Run these checks in sequence: **1. Check merged CLIs in managed clone:** ```bash MERGED_COLLISION="$PREEXISTING_MERGED_COLLISION" MERGED_PATHS="$PREEXISTING_MERGED_PATHS" ``` Use the pre-package snapshot from Step 5. Do not re-run `ls "$PUBLISH_REPO_DIR/library"/*/""` here: Step 6 has already copied the new package into that path, so a fresh `ls` would make every new print look like a merged collision. If `MERGED_COLLISION=true`, note the category path from `MERGED_PATHS`. **2. Check all open PRs (any author):** ```bash gh pr list --repo mvanhorn/printing-press-library --head "feat/" --state open --json number,title,url,author ``` If the list is non-empty, record `PR_COLLISION=true`. For each PR, note the PR number, URL, and author login. **3. Identify own PRs:** Filter the PR list from step 2 by `--author @me`: For fork-based PRs, the head includes the username prefix: ```bash ACCESS=$(jq -r .access "$PUBLISH_CONFIG") GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG") if [ "$ACCESS" = "fork" ]; then HEAD_REF="$GH_USER:feat/" else HEAD_REF="feat/" fi gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state open --author @me --json number,title,url ``` If found, record `OWN_PR=true`, store `EXISTING_PR_NUMBER` and `EXISTING_PR_URL`. **If no open PR was found**, also check for a previously merged PR on the same branch — by ANY author, not just yours: ```bash MERGED_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state merged --json number --jq '.[0].number' 2>/dev/null) ``` If `MERGED_PR` is non-empty, the branch name was already used and merged. Set `BRANCH_MERGED=true` so Step 8 creates a new branch name (e.g., `feat/-YYYYMMDD`) instead of reusing the merged branch. Do NOT force-push onto a merged branch — `gh pr edit` would silently update a closed PR nobody is watching. The author-agnostic lookup also catches **squash-zombie branches**: GitHub squash-merge leaves the source branch behind on the remote, with pre-squash commit refs that look "ahead of main" but are content-equivalent to the squash commit. Without this check, the skill misclassifies the zombie as fresh-publish, then `git push -u` fails because the remote branch already exists. Timestamping sidesteps the issue entirely. ### No collision If no merged CLI exists and no open PRs match (other than your own), set `EXISTING_PR_NUMBER` from the own-PR check (or empty if none) and proceed to Step 8 normally. If an existing open PR of yours was found, inform the user: > "Found your open PR #N for ``. Will update it with the new version." ### Collision detected — display info Show the user what was found: ``` ⚠️ Name collision detected for Merged: / exists in the library Open PR: # by ``` Show all applicable lines. If `OWN_PR=true`, tag the PR as "(yours)". ### Resolution paths Present three options via AskUserQuestion: **If `OWN_PR=true` (your own open PR exists):** - **Update** — Update your existing PR with the new version (default, preserves current behavior) - **Alongside** — Rename yours with a qualifier and publish next to the existing one - **Bail** — Cancel the publish **If PR collision exists but is another user's, or merged collision only:** - **Replace** — Intentionally overwrite the existing CLI - **Alongside** — Rename yours with a qualifier and publish next to the existing one - **Bail** — Cancel the publish and view the existing CLI/PR #### Update path (own PR) This is the existing update flow. Set `EXISTING_PR_NUMBER` from the detection step and proceed to Step 8, which handles force-push and PR description update. #### Replace path **For merged CLIs or your own PR:** Standard confirmation: > "This will replace the existing ``. Continue?" **For another user's PR:** Stronger confirmation naming the other author: > "⚠️ This will replace ``'s `` (PR #N). Are you sure?" If confirmed: - The PR description must include: `⚠️ **Replaces existing \`\`** — ` - Set `EXISTING_PR_NUMBER=""` (create a new PR, don't update theirs) - Proceed to Step 8 normally #### Alongside path (rename) **1. Extract the original API slug** from the manifest's `api_name` field: ```bash # Read from .printing-press.json in the publish repo's staged CLI ORIGINAL_API_SLUG=$(cat "$PUBLISH_REPO_DIR/library///.printing-press.json" | jq -r '.api_name') ``` **2. Generate rename suggestions** using slug format. Derive the new CLI name from the chosen slug: - Numeric: `-2` (if that collides, try `-3`, `-4`, etc.) - Non-numeric: `-alt` - Custom: prompt the user for a qualifier word After the user chooses a slug, compute: ```bash NEW_API_SLUG="" NEW_CLI_NAME="${NEW_API_SLUG}-pp-cli" ``` Present the format to the user: > "Rename format: `-`. Pick a qualifier:" > > 1. `2` → `-2` > 2. `alt` → `-alt` > 3. Enter custom qualifier **3. Verify each suggestion is non-colliding** before presenting: ```bash # Check merged ls "$PUBLISH_REPO_DIR/library"/*/"" 2>/dev/null # Check open PRs gh pr list --repo mvanhorn/printing-press-library --head "feat/" --state open --json number ``` If a suggestion collides, skip it or increment the numeric suffix. **4. Rename the CLI in the publish repo:** Since Step 6 copied the staged CLI into `$PUBLISH_REPO_DIR`, the rename operates on that directory. Note: `--old-name`/`--new-name` still use CLI-name format (e.g., `dub-pp-cli`) because `RenameCLI` does content replacement — bare slugs would cause collateral damage. The `--dir` path uses the slug-keyed directory. ```bash cli-printing-press publish rename \ --dir "$PUBLISH_REPO_DIR/library//" \ --old-name \ --new-name "$NEW_CLI_NAME" \ --json ``` Parse the JSON result. Verify `"success": true`. Note that `new_dir` should now be `$PUBLISH_REPO_DIR/library//$NEW_API_SLUG`. **5. Update all downstream references for Step 8:** - Branch name: `feat/$NEW_API_SLUG` (not the old slug) - PR title: `feat($NEW_API_SLUG): add $NEW_API_SLUG` - Commit message: `feat($NEW_API_SLUG): add $NEW_API_SLUG` - Registry.json entry: `name` → `$NEW_API_SLUG` - Set `EXISTING_PR_NUMBER=""` (always a new PR for a renamed CLI) Proceed to Step 8 with the new name. #### Bail path Show links to what exists: - If merged: "Existing CLI at `library///`" - If open PR: "Open PR: " Exit the publish flow. If Step 6 already wrote files into `$PUBLISH_REPO_DIR`, clean up with `git checkout -- . && git clean -fd` in the managed clone. ## Step 8: Branch, Commit, and PR ### Create branch **If `EXISTING_PR_NUMBER` is set** (updating an existing PR): Always overwrite the branch — the intent is clearly to update: ```bash git checkout -B feat/ ``` **If `EXISTING_PR_NUMBER` is empty and `BRANCH_MERGED` is true** (previous PR was merged): Auto-create a timestamped branch — do not reuse the merged branch name: ```bash git checkout -b feat/-$(date +%Y%m%d) ``` **If `EXISTING_PR_NUMBER` is empty and `BRANCH_MERGED` is not set** (no open or merged PR): Check for stale branches and competing PRs: ```bash # Check local and remote branches LOCAL_BRANCH=$(git branch --list "feat/" | head -1) REMOTE_BRANCH=$(git ls-remote --heads origin "feat/" 2>/dev/null | head -1) # If a remote branch exists, check who owns it if [ -n "$REMOTE_BRANCH" ]; then # Check for ANY open PR on this branch (not just ours) OTHER_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "feat/" --state open --json number,author --jq '.[0]' 2>/dev/null) fi ``` **If another user's open PR exists on this branch** (`OTHER_PR` is non-empty and author is not `@me`): > "Someone else has an open PR for `` (PR #N by @author). Creating a timestamped branch to avoid conflicts." Auto-create a timestamped branch: `feat/-YYYYMMDD`. Do NOT offer to overwrite — that would stomp their work. **If the branch exists but no competing PR** (stale branch from a previously closed/merged PR): Ask via AskUserQuestion: > "Found a stale branch `feat/` (likely from a previous publish). Overwrite it?" - "Overwrite existing branch" — reuse the branch name - "Create timestamped variant (feat/-YYYYMMDD)" **If no branch exists:** Create normally. ```bash # New branch: git checkout -b feat/ # Overwrite existing: git checkout -B feat/ ``` ### Commit and push ```bash cd "$PUBLISH_REPO_DIR" git add library/ git commit -m "feat(): add " ``` Push to origin (which is the fork for non-push users, or the upstream for push users): **If updating an existing PR** (`EXISTING_PR_NUMBER` is set): ```bash git push --force-with-lease -u origin feat/ ``` **If creating a new PR** and you chose "Overwrite existing branch" earlier: ```bash git push --force-with-lease -u origin feat/ ``` **Otherwise** (new branch, no conflicts): ```bash git push -u origin feat/ ``` ### Capture the pushed commit SHA After pushing, capture the head commit SHA. This is used to build durable manuscript links in the PR body (see "Build the PR description" below). ```bash HEAD_SHA=$(git rev-parse HEAD) ``` The SHA stays resolvable on `mvanhorn/printing-press-library` for the life of the PR (GitHub mirrors fork-PR head commits to `refs/pull//head` on the upstream), and remains valid after the PR is merged and the branch is deleted. Each invocation of this skill captures a fresh `HEAD_SHA` after its push and rewrites the body, so links stay current across updates the skill performs. If the branch is force-pushed outside this skill, re-run `/printing-press-publish` to refresh the body — the prior links will still resolve, but they'll point at the manuscript contents from before the out-of-band push. ### Create or update PR Read `access` and `gh_user` from `$PUBLISH_CONFIG`. These determine how `gh pr create` is called. **For fork-based PRs** (`access` is `fork`): use `--head :feat/` so GitHub creates a cross-repo PR from the fork to the upstream. Without `--head`, `gh pr create` would try to find the branch on the upstream repo (where the user can't push) and fail. **For push-access PRs** (`access` is `push`): use `--head feat/` so GitHub creates the PR from the branch this flow just pushed, even when the managed clone or shell session has other branches checked out. Build the PR description from: - The manifest (`description`, `api_name`, `category`, `printing_press_version`, `spec_url`) - The manifest's `novel_features` array from the packaged CLI after Step 6 - The `help_output` captured in Step 4 - The CLI's README (first 2-3 paragraphs, or note that README is missing) - Links to every file under `.manuscripts//research/` and `.manuscripts//proofs/`. Each link must be a full `https://github.com/mvanhorn/printing-press-library/blob//library///.manuscripts///` URL — never a relative path (GitHub resolves those against `…/pull/`, producing broken `…/pull/library/…` URLs) and never a directory (the blob view requires a file). Enumerate the actual files; do not invent or skip them. - The validation results from Step 4 - The publish live gate result from Step 4.5, including any explicit `--skip-live-test` reason - A Gaps section listing any missing manifest fields Read `novel_features` from `$PUBLISH_REPO_DIR/library///.printing-press.json` after packaging. Preserve the manifest order. Do not derive this section from README prose, SKILL prose, root help, or memory of the run: those surfaces may be summarized or hand-edited, while the packaged manifest is the publish-time source of truth. For each entry, include the command, name, and description. If the array is empty, write `No novel commands recorded in .printing-press.json.` and include the missing field in **Gaps**; do not omit the section. Also include a publication-path line so new prints, reprints, PR updates, and collision renames are distinguishable: - `New print` — no merged CLI and no existing PR matched this slug. - `Update existing PR #` — this publish refreshes an open PR. - `Reprint/replace` — a merged library CLI existed before this publish and the selected path replaces it. This must be based on `PREEXISTING_MERGED_COLLISION=true`, not on the post-package tree. - `Alongside print` — this publish renamed the API slug to avoid a collision; include the original slug. If `/printing-press-reprint` handed off a degraded reprint with no prior public-library source, use `New print` and add the degraded-reprint note only if that context is available from the handoff. **MANDATORY: Before constructing the PR body, scrub all workspace PII.** The library repo is public. Scan any live test results, acceptance data, or manuscript excerpts for organization names, team member names, and email addresses. Replace with generic descriptions ("the workspace", "5 team members", "12 users"). Team keys (e.g., "ESP") are OK but org names (e.g., "Acme Corp") are not. See `references/secret-protection.md` in the printing-press skill for the full policy. Write the constructed PR body to a temporary Markdown file and pass it with `--body-file`. Do this for both PR creation and PR updates. Do not inline the body in a shell argument; large fenced help output, Markdown tables, and backticks are too easy to mangle. **PR description template:** ```markdown ## `** — "> **API:** | **Category:** | **Press version:** **Spec:** ### Publication Path > ### CLI Shape \`\`\`bash $ --help \`\`\` ### Novel Commands | Command | Name | Description | |---------|------|-------------| | `` | | | ### What This CLI Does ### Manuscripts - [