npx skills add ...
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish
Publish a generated CLI to the printing-press-library repo
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish
Publish a generated CLI from your local library to the printing-press-library repo as a pull request.
This skill opens only a generated CLI publish PR or, with
--blocked-api-journal, a blocked-apis.json journal PR. It never opens a
docs-only, plan, proposal, or spec PR as a substitute for a CLI that is not
ready to publish. If generation, validation, or live testing is blocked, report
the exact blocker and stop.
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 <cli-name> --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 request includes --blocked-api-journal, enter Blocked API Journal
Mode below instead of the normal printed-CLI publish flow. This mode may be
invoked from /printing-press's hold-path menu after the user explicitly chose
"Add to blocked-API journal"; that parent menu choice is sufficient user
authorization for the public-library journal write. Do not require a second
fresh-turn invocation for this journal-only mode.
If the fresh user-authored request includes --skip-live-test=<reason>, 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/<category>/<api-slug>/.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-<api-slug>/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.
The public library also owns per-CLI release accounting. Do not manually bump
CHANGELOG.md, .printing-press-release.json, or runtime var version = ...
for a publish PR. Fresh printed CLIs may include blank release-ledger skeletons;
the library's post-merge workflow assigns the final YYYY.M.N release and
stamps the runtime version after merge. When replacing an existing public
library CLI, preserve its existing release-ledger files so changelog history is
not lost in the reprint PR.
blocked-apis.json is different: it is a hand-maintained public-library journal,
not a generated registry surface. Journal-only PRs may edit blocked-apis.json
and must not stage library/, registry.json, README catalog cells, or
cli-skills/.
Use this mode only when the invocation includes --blocked-api-journal. It
records a held /printing-press attempt whose blocker is likely to repeat for
other users until a machine or upstream issue changes.
Required fields from the caller:
slug: canonical API slug, not the CLI binary name.attempted_at: YYYY-MM-DD.verdict: hold.reason: concise blocker reason, with no secrets, local paths, cookies,
tokens, or account-specific details.blocking_issue: Printing Press issue number if known, otherwise null.permanent: boolean.If the caller did not provide one of these fields, infer only safe values from
the current run context. If reason is missing or vague, stop and ask for one
specific blocker sentence; do not write an unhelpful journal entry.
Run the normal Setup, Configuration, scoped clone cleanup, and GitHub auth
checks, then prepare the public-library clone exactly as the normal publish
flow does: fork if needed, ensure upstream points to
mvanhorn/printing-press-library, fetch upstream, and reset the clone to
upstream/main before editing.
Then update only $PUBLISH_REPO_DIR/blocked-apis.json:
Create a journal branch and PR:
Open the PR against mvanhorn/printing-press-library with a body that includes:
blocking_issue/printing-press <api-slug> runs warn
before repeating the attemptAfter the PR is open, report the URL and stop. Do not continue into normal printed-CLI package, live-test, registry, or skill-mirror steps.
Before doing anything else:
After running the setup contract, capture the PRINTING_PRESS_BIN=<abs-path> 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.
If setup emitted [go-toolchain-old] or [low-disk], surface the advisory to the user and continue unless setup also emitted [setup-error]. [go-toolchain-old] means later Go commands may download the required toolchain or fail when downloads are blocked; [low-disk] means this run may need several GiB for generated files, Go build cache, module downloads, or repository clones.
After capturing the binary path, check binary version compatibility. Read the min-binary-version field from this skill's YAML frontmatter. Run <PRINTING_PRESS_BIN> 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."
$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.
The module_path_base field sets the Go module path prefix for published CLIs. During packaging, the full module path is constructed as <module_path_base>/<category>/<api-slug>. 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.
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.
Verify gh is authenticated:
If this fails, stop and tell the user: "GitHub CLI is not authenticated. Run gh auth login first."
Run:
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):
cli_name fields, then derive the API slug from the manifest's api_name field<argument>-pp-cli against cli_name fieldscli_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 pickOnce 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").
Read .printing-press.json from the resolved CLI directory.
Category resolution order:
If the manifest has a category field, present it for confirmation:
"Publishing as . OK?" Give the user the option to change it
If the manifest does not provide a category, present the full list via AskUserQuestion:
Every PR into the public library gets an automated Greptile review plus a Greptile policy gate CI job. The canonical contract is the library's AGENTS.md → "Automated code review with Greptile"; the essentials:
greptile-apps top-level summary, not just inline threads. Summaries can carry actionable Comments Outside Diff blocks even when the thread list is empty. Run the repo's review-state helper before declaring ready:
Timed out waiting for Greptile Review to complete (large new-CLI diffs are the common trigger), the gate auto-posts @greptileai review after ~3 minutes; if that doesn't recover, post @greptileai review yourself and wait.Confidence Score: ≥ 4/5. A new push re-runs it — keep the score meeting threshold on the final head.Run:
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:
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.
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.
Marker invalidation and sync. The acceptance marker carries a source
fingerprint; any .go edit after it was written makes publish package fail
with "phase5 marker source fingerprint does not match". Re-run this live gate
after every source change and write the marker to both copies: the embedded
$CLI_DIR/.manuscripts/<run>/proofs/ and the archived
$PRESS_MANUSCRIPTS/<api>/<run>/proofs/ (manuscript lookup is archive-first;
proof lookup is embedded-first — a stale copy in either location blocks
packaging).
Resolve the Phase 5 proofs directory from the CLI manifest:
Phase 5 markers are bound to the source tree that was exercised. The live
dogfood writer records source_fingerprint and per-file hashes automatically.
Publish validation recomputes the fingerprint from the current CLI directory
and refuses a marker from a drifted tree, naming changed source files when the
marker has them. README-only edits are outside this fingerprint and do not
invalidate the gate.
If SKIP_LIVE_TEST_REASON is unset, run full live dogfood and write a fresh
acceptance marker into that proofs directory:
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=<reason>, write a
fresh skip marker instead of running dogfood:
Then rerun Step 4's validation:
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.
The publish skill manages its own clone of the library repo at $PUBLISH_REPO_DIR.
If $PUBLISH_REPO_DIR does not exist:
Detect push access:
Detect git protocol:
Clone based on access:
Push access (HAS_PUSH is true):
No push access (HAS_PUSH is false):
Cache the config:
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).
Read $PUBLISH_CONFIG, then re-check access in case it changed (user was granted push access, or access was revoked):
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:
Verify the clone is healthy:
If this fails, the clone is corrupt. Remove $PUBLISH_REPO_DIR and re-run first-time setup.
Before creating a new branch, check for uncommitted changes:
If there are uncommitted changes, ask the user via AskUserQuestion:
If reset, run git checkout -- . && git clean -fd.
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/*/<api-slug>, so any collision or publication-path decision made
after packaging must use this pre-package snapshot, not a fresh ls.
Read $PUBLISH_CONFIG to get module_path_base. Construct the full module path using the API slug (not the CLI name):
For example: github.com/mvanhorn/printing-press-library/library/productivity/notion
--module-path is required in --dest mode. When packaging with --dest,
always pass --module-path "$MODULE_PATH". Omitting it silently skips the
go.mod/import rewrite (RewriteModulePath is gated on the flag), so the
packaged CLI keeps module <cli-name> and the library CI rejects the PR with a
module-path mismatch. publish package verifies the staged tree's module path
after the rewrite and fails packaging when it is not library-canonical (whether
--module-path was omitted or set to a non-canonical value). Standalone
publish validate on a source tree surfaces the check as a warning — the bare
module name is expected there pre-rewrite; the authoritative failure is in the
package step.
Run publish package with --target to stage the CLI into a unique temporary
directory, then copy it into the publish repo:
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 while preserving the public library's release ledger files when this is a reprint:
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:
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.
Before collision detection or branch creation, inspect the packaged CLI's customizations index:
The index ships in one of two shapes: the per-patch directory
.printing-press-patches/ (current) or the legacy single-array
.printing-press-patches.json (older prints, not yet normalized). Validate
whichever is present:
Fresh prints from current cli-printing-press generate include the
.printing-press-patches/ directory with just a .gitkeep; leave it unchanged
when no hand customization was made after generation. If neither shape is
present, 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.
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:
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, record
one concise entry per customization before opening the library PR — one
.printing-press-patches/<id>.json file per patch (the directory supersedes the
legacy patches[] array, so concurrent PRs never conflict). These entries are
the durable hand-edit contract that tells future agents and regen tooling what
must be preserved beyond generator output.
Use this shape (one file, .printing-press-patches/<id>.json):
Rules:
id. Use kebab-case ids prefixed with the API slug for
grep-ability across the public library.summary and reason short. Each entry is an index, not a duplicate
of the git diff.files when they are part of the same
code-level customization. README/SKILL.md-only polish does not need a patch
manifest entry. publish validate reads the records and fails if a
recorded files[] path is missing, the per-patch record omits
schema_version or declares an unsupported one, or a declared
call_sites / markers / marker string is absent from the recorded
files. files[] is required for every call_sites / markers /
marker needle so a leftover substring elsewhere cannot mask a dropped
customization. Needles are checked only in those recorded files.// PATCH(...) source comments are optional navigation aids. The public
library verifier requires a patches index (the directory or the legacy file)
and well-formed entries; it does not require a marker/comment pairing.For the authoritative public-library authoring contract, read the
mvanhorn/printing-press-library AGENTS.md section
".printing-press-patches/ records library-side customizations".
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.
Run these checks in sequence:
1. Check merged CLIs in managed clone:
Use the pre-package snapshot from Step 5. Do not re-run ls "$PUBLISH_REPO_DIR/library"/*/"<api-slug>" 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):
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:
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:
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/<api-slug>-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.
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
<api-slug>. Will update it with the new version."
Show the user what was found:
Show all applicable lines. If OWN_PR=true, tag the PR as "(yours)".
Present three options via AskUserQuestion:
If OWN_PR=true (your own open PR exists):
If PR collision exists but is another user's, or merged collision only:
This is the existing update flow with a divergence guard. Set
EXISTING_PR_NUMBER from the detection step and proceed to Step 8, which
fetches the current PR branch head, checks for branch-only fixes that the new
package would revert, and only then handles force-push and PR description
update.
For merged CLIs or your own PR: Standard confirmation:
"This will replace the existing
<api-slug>. Continue?"
For another user's PR: Stronger confirmation naming the other author:
"⚠️ This will replace
<author>'s<api-slug>(PR #N). Are you sure?"
If confirmed:
⚠️ **Replaces existing \`** — <reason provided by user or "newer version">`EXISTING_PR_NUMBER="" (create a new PR, don't update theirs)1. Extract the original API slug from the manifest's api_name field:
2. Generate rename suggestions using slug format. Derive the new CLI name from the chosen slug:
<api-slug>-2 (if that collides, try -3, -4, etc.)<api-slug>-altAfter the user chooses a slug, compute:
Present the format to the user:
"Rename format:
<api-slug>-<qualifier>. Pick a qualifier:"
2→<api-slug>-2alt→<api-slug>-alt- Enter custom qualifier
3. Verify each suggestion is non-colliding before presenting:
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. Rename also rewrites go.mod, leftover module-path slugs, installer slugs, env prefixes, and research.json api_name (including under .manuscripts/). Do not hand-fix those after a successful rename.
Parse the JSON result. Verify "success": true. Note that new_dir should now be $PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG.
5. Update all downstream references for Step 8:
feat/$NEW_API_SLUG (not the old slug)feat($NEW_API_SLUG): add $NEW_API_SLUGfeat($NEW_API_SLUG): add $NEW_API_SLUGname → $NEW_API_SLUGEXISTING_PR_NUMBER="" (always a new PR for a renamed CLI)Proceed to Step 8 with the new name.
Show links to what exists:
library/<category>/<api-slug>/"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.
If EXISTING_PR_NUMBER is set (updating an existing PR):
Fetch and inspect the current PR branch before replacing it. The latest
origin/main plus the newly packaged library/<category>/<api-slug>/ tree is
the proposed update. The remote PR branch may also contain accepted review
fixes from the drive-to-green loop. Those branch-only edits must not be erased
silently.
If the guard exits, offer the user two choices via AskUserQuestion:
.printing-press-patches/<id>.json
records for code-level fixes, rerun Step 6 verification, then rerun this
divergence guard.If the guard finds no branch-only paths or edits, overwrite the local branch:
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:
If EXISTING_PR_NUMBER is empty and BRANCH_MERGED is not set (no open or merged PR):
Check for stale branches and competing PRs:
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
<api-slug>(PR #N by @author). Creating a timestamped branch to avoid conflicts."
Auto-create a timestamped branch: feat/<api-slug>-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/<api-slug>(likely from a previous publish). Overwrite it?"
If no branch exists: Create normally.
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):
If creating a new PR and you chose "Overwrite existing branch" earlier:
Otherwise (new branch, no conflicts):
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).
The SHA stays resolvable on mvanhorn/printing-press-library for the life of the PR (GitHub mirrors fork-PR head commits to refs/pull/<N>/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.
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 <gh_user>:feat/<api-slug> 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/<api-slug> 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:
description, api_name, category, printing_press_version, spec_url)novel_features array from the packaged CLI after Step 6help_output captured in Step 4.manuscripts/<run-id>/research/ and .manuscripts/<run-id>/proofs/. Each link must be a full https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/<subdir>/<filename> 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.--skip-live-test reasonRead novel_features from
$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.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 #<N> — 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:
If updating an existing PR (EXISTING_PR_NUMBER is set):
Display the full PR URL: "Updated PR: <EXISTING_PR_URL>" (use the full https:// URL, not shorthand).
If creating a new PR:
Display the full PR URL (e.g., https://github.com/mvanhorn/printing-press-library/pull/10), not the shorthand org/repo#N format. The full URL is clickable in all terminals and contexts.
Once the PR is open, it enters the public library repo's review contract. That contract is owned by mvanhorn/printing-press-library AGENTS.md → "Automated code review with Greptile"; read it for the canonical version. An agent invoking this skill from cli-printing-press will not have loaded the library's AGENTS.md, so the obligations are summarized here.
Greptile reviews incrementally: every commit you push re-triggers a fresh review, which can surface new findings the previous round didn't. This is a loop, not a single pass — drive the PR to a stable green and don't declare done after round one.
Iterate until all of these hold, confirmed by the review that your most recent fix commit triggered:
verify-library-conventions, Govulncheck, and any other workflow on the PR.Read findings from two surfaces — they don't overlap:
gh pr view <PR> --repo <owner>/<repo> --comments returns the top-level issue conversation (Greptile's summary comment, score, CI bots).gh api repos/<owner>/<repo>/pulls/<PR>/comments returns the inline diff-anchored review comments — Greptile posts each P0/P1/P2 finding here, and these are NOT included in --comments. Skipping this call is how an agent silently declares "all findings resolved" while every inline thread is still open.Monitoring is the harness's job, not a busy-loop you hand-roll. Use whatever PR-activity monitoring your environment provides — react to review/CI events as they arrive, or re-check on an interval if it doesn't push events. After each fix push, wait for the re-triggered review to land before judging done; a new round can reopen the gate.
Don't hand-edit registry.json or cli-skills/pp-<api-slug>/SKILL.md to satisfy a finding — both are bot-regenerated post-merge by [skip ci] commits, and the library's Fail on changes to generated artifacts check pre-rejects any PR that touches them.
Once the PR is stably green, the skill's job is done. Do not merge it and do not poll waiting for it to merge — merges into the public library are the maintainer's manual review, not this skill's and (for a fork contributor) not the user's either.
Read access from $PUBLISH_CONFIG (jq -r .access "$PUBLISH_CONFIG") to determine what to do next:
access is push (maintainer/admin with push access): apply the awaiting-maintainer label to signal the PR is ready for manual review:
access is fork (community contributor): you cannot merge or label the upstream PR. There is nothing more to do once it's green.Then report the terminal state and return control to the caller. Do not offer a retro or any follow-up menu from this skill by default — that decision belongs to whoever invoked publish. The printing-press pipeline offers retro as its own post-publish tail; a direct human invocation without --from-polish just ends here.
If POLISH_HANDOFF=true, offer retro as a soft tail after the PR is green. This preserves the standalone polish -> publish workflow without allowing polish's same-turn AskUserQuestion answer to create or update a public-library PR.
Present via AskUserQuestion:
"PR opened: <PR_URL>. Run a retro? It surfaces systemic gaps from this session (generator misses, scorer bugs, skill-doc drift) as a GitHub issue for the Printing Press maintainers. Every retro filed raises the floor for the next CLI, and your session context is freshest right now."
- No, I'm done (default)
- Yes, run retro now
If the user picks yes, invoke /printing-press-retro.
Before creating the PR, verify that no secrets leaked into the packaged CLI.
This matters because the library repo is public. A leaked API key in a PR is a security incident — anyone can see it, even if the PR is later closed.
The generation skill (/printing-press) runs an exact-value scan during Phase 5.6
if the user provided an API key. By the time publish runs, the Printing Press's own
mistakes should already be caught. But the user may have edited files between
generation and publish.
Mandatory binary scan: cli-printing-press publish package scans the staged CLI and manuscripts for live-looking vendor-prefix tokens (sk-or-v1-*, sk_live_*, ghp_*, ghs_*, xoxb-*, AKIA*, and similar). If it fails with vendor-prefix tokens detected, treat the package as unpublishable. Do not copy, commit, push, or open a PR until the reported file:line findings are removed or redacted.
If the user's exact API key value is known, scan the packaged tree before creating the PR. This catches edits or manuscripts added after /printing-press Phase 5.6:
If gitleaks or trufflehog is installed, run it as an enrichment pass on the staged directory:
These tools use vendor-specific patterns (Steam keys, Stripe keys, GitHub tokens) with low false-positive rates. Their findings add detector breadth beyond the mandatory floor. Review any finding before proceeding.
Always do the lightweight structural check:
.env files, session-state.json, or config.toml with
real credentials exist in the staged directory"your-key-here" placeholders, not real valuesNever include in the staged directory:
.env filessession-state.jsonIf the mandatory binary scan or exact-value scan finds issues, stop. For external-tool or lightweight structural findings, warn the user and ask whether to proceed. The user makes the final call on those non-mandatory findings.
Beyond the secret scans above, run the PII pattern scanning step defined in
references/secret-protection.md in the printing-press skill, section PII
pattern scanning. It carries the Tier 1 pattern set and the sweep loop. This catches PII captured during live dogfood
that the prose guidance missed — emails, real attendee names, account
identifiers — before they ship to the public library repo.
The scan has two tiers:
Bearer cal_live_*, Bearer sk_live_*, Bearer ghp_*, xoxp-*, etc.).
Near-zero false-positive rate.A pre-scrub copy of the staging directory is preserved at
<staging>.pre-pii-scrub/ so the user can recover from a wrong redaction.
Two prior PII leaks shipped to the public library before this scan existed. The scan is the mechanical defense layer the prose guidance alone could not provide.
gh not authenticated: Detect in Step 1, tell user to run gh auth logingh repo fork may fail if the user already has a fork with a different name, or if the org restricts forking. Report the error and suggest the user fork manually via the GitHub web UI.gh pr list or ls commands fail (network, auth), warn but don't block — proceed as if no collision existspublish rename --json. Offer to retry with a different qualifier or bail. If the publish repo is in a partial state, reset with git checkout -- . && git clean -fd before retryinggh auth status and git remote -vgh pr create --head user:branch fails with "head not found", the branch wasn't pushed to the fork. Verify with git ls-remote origin feat/<api-slug>cd "$PUBLISH_REPO_DIR"
if [ ! -f blocked-apis.json ]; then
printf '[]\n' > blocked-apis.json
fi
jq --arg slug "<api-slug>" \
--arg attempted_at "<YYYY-MM-DD>" \
--arg verdict "hold" \
--arg reason "<reason>" \
--argjson blocking_issue '<number-or-null>' \
--argjson permanent '<true-or-false>' '
(if type == "array" then . else [] end)
| map(select(.slug != $slug))
+ [{
slug: $slug,
attempted_at: $attempted_at,
verdict: $verdict,
reason: $reason,
blocking_issue: $blocking_issue,
permanent: $permanent
}]
| sort_by(.slug)
' blocked-apis.json > blocked-apis.json.tmp || {
rm -f blocked-apis.json.tmp
echo "Error: jq failed to update blocked-apis.json"
exit 1
}
if ! jq empty blocked-apis.json.tmp; then
rm -f blocked-apis.json.tmp
echo "Error: blocked-apis.json update produced invalid JSON"
exit 1
fi
mv blocked-apis.json.tmp blocked-apis.jsongit checkout -B chore/blocked-api-<api-slug>
git add blocked-apis.json
git commit -m "chore(<api-slug>): journal blocked API"
git push --force-with-lease -u origin chore/blocked-api-<api-slug># 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
if ! command -v go >/dev/null 2>&1; then
echo ""
echo "[setup-error] Go toolchain not found."
echo ""
echo "This Printing Press flow runs Go-based build or validation commands."
echo "Install Go 1.26.6 or newer from https://go.dev/dl/, then verify with:"
echo " go version"
echo "Then re-run this skill."
echo ""
return 1 2>/dev/null || exit 1
fi
echo "PRINTING_PRESS_BIN=$PRINTING_PRESS_BIN"
_pp_semver_lt() {
if [ -z "${PP_SEMVER_A:-}" ] || [ -z "${PP_SEMVER_B:-}" ]; then
echo "[setup-error] semver comparison inputs are missing." >&2
return 2
fi
awk -v a="${PP_SEMVER_A:-}" -v b="${PP_SEMVER_B:-}" 'BEGIN {
split(a, x, "."); split(b, y, ".")
for (i = 1; i <= 3; i++) {
if ((x[i] + 0) < (y[i] + 0)) exit 0
if ((x[i] + 0) > (y[i] + 0)) exit 1
}
exit 1
}'
}
_pp_go_version_norm() {
printf '%s\n' "${PP_GO_VERSION_INPUT:-}" | sed -nE 's/.*go([0-9]+)\.([0-9]+)(\.([0-9]+))?.*/\1.\2.\4/p' | sed -E 's/\.$/.0/'
}
_pp_check_go_currency() {
_pp_go_installed="$(PP_GO_VERSION_INPUT="$(go env GOVERSION 2>/dev/null)" _pp_go_version_norm)"
_pp_go_required="$(PP_GO_VERSION_INPUT="$(go version "$PRINTING_PRESS_BIN" 2>/dev/null)" _pp_go_version_norm)"
PP_SEMVER_A="$_pp_go_installed"
PP_SEMVER_B="$_pp_go_required"
if [ -z "$_pp_go_installed" ] || [ -z "$_pp_go_required" ] || ! _pp_semver_lt; then
return 0
fi
echo ""
if [ "${GOTOOLCHAIN:-auto}" = "local" ]; then
echo "[setup-error] Go $_pp_go_required or newer is required by this cli-printing-press binary (installed: $_pp_go_installed)."
echo "GOTOOLCHAIN=local disables automatic toolchain downloads, so later Go quality gates would fail."
echo "Install Go $_pp_go_required or newer from https://go.dev/dl/, or unset GOTOOLCHAIN."
echo ""
return 1
fi
echo "[go-toolchain-old] Go $_pp_go_required or newer is required by this cli-printing-press binary (installed: $_pp_go_installed)."
echo "PRESS_GO_INSTALLED=$_pp_go_installed"
echo "PRESS_GO_REQUIRED=$_pp_go_required"
echo "Default GOTOOLCHAIN behavior may download the required toolchain during Go commands."
echo ""
return 0
}
_pp_check_go_currency || { return 1 2>/dev/null || exit 1; }
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"
_pp_check_disk_space() {
_pp_disk_warn_kb="${PRINTING_PRESS_DISK_WARN_KB:-3145728}"
_pp_disk_fail_kb="${PRINTING_PRESS_DISK_FAIL_KB:-524288}"
case "$_pp_disk_warn_kb$_pp_disk_fail_kb" in
""|*[!0-9]*) return 0 ;;
esac
_pp_disk_path="$PRESS_HOME"
while [ ! -e "$_pp_disk_path" ] && [ "$_pp_disk_path" != "/" ]; do
_pp_disk_path="$(dirname "$_pp_disk_path")"
done
_pp_disk_avail_kb="$(df -Pk "$_pp_disk_path" 2>/dev/null | awk 'BEGIN {
if ((getline header) <= 0 || (getline record) <= 0) exit
field_count = split(record, fields)
if (field_count >= 4) print fields[4]
}')"
case "$_pp_disk_avail_kb" in
""|*[!0-9]*) return 0 ;;
esac
if [ "$_pp_disk_avail_kb" -lt "$_pp_disk_fail_kb" ]; then
echo ""
echo "[setup-error] Critically low disk space on the Printing Press workspace volume."
echo "PRESS_DISK_PATH=$_pp_disk_path"
echo "PRESS_DISK_AVAIL_KB=$_pp_disk_avail_kb"
echo "PRESS_DISK_FAIL_KB=$_pp_disk_fail_kb"
echo "Free disk space or set PRINTING_PRESS_HOME to a volume with more room, then re-run this skill."
echo ""
return 1
fi
if [ "$_pp_disk_avail_kb" -lt "$_pp_disk_warn_kb" ]; then
echo ""
echo "[low-disk] Printing Press workspace volume is low on free space."
echo "PRESS_DISK_PATH=$_pp_disk_path"
echo "PRESS_DISK_AVAIL_KB=$_pp_disk_avail_kb"
echo "PRESS_DISK_WARN_KB=$_pp_disk_warn_kb"
echo "This flow may need several GiB for generated files, Go build cache, module downloads, or repository clones."
echo ""
fi
}
_pp_check_disk_space || { return 1 2>/dev/null || exit 1; }
mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT"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"{
"managed_by": "printing-press-publish",
"repo_url": "https://github.com/mvanhorn/printing-press-library",
"access": "push",
"protocol": "ssh",
"clone_path": "<home>/printing-press/.publish-repo-<scope>",
"scope_dir": "/absolute/path/to/source/worktree",
"module_path_base": "github.com/mvanhorn/printing-press-library/library"
}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"
donegh auth statuscli-printing-press library list --json