printing-press-publish — скилл для Claude Code: публикация CLI
Публикует сгенерированный локальный CLI в mvanhorn/printing-press-library через запрос на слияние. При блокировке тестирования скилл останавливается и сообщает причину.
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a claude-codeЧто делает printing-press-publish
Скилл готовит запрос на слияние для сгенерированного локального CLI в mvanhorn/printing-press-library: может создать форк, отправить ветку и открыть или обновить запрос. Запуск должен быть прямым сообщением пользователя. Если публикация продолжает работу printing-press-polish, нужен отдельный вызов с --from-polish.
Обычный поток включает генерацию, проверку и тестирование в реальном режиме. При блокере скилл сообщает причину и останавливается. Он не подменяет публикацию CLI запросом только с документацией или планом, поэтому готовность CLI стоит проверить заранее. Режим --blocked-api-journal меняет только blocked-apis.json и создаёт отдельный запрос на слияние для журнала.
Какие задачи решает
- опубликовать сгенерированный CLI из локальной библиотеки как запрос на слияние
- открыть или обновить запрос для готового CLI
- остановить публикацию при блокировке проверки
- занести повторяющийся блокер API в
blocked-apis.json
Примеры запросов агенту
- Опубликуй `notion-pp-cli`: `/printing-press publish notion-pp-cli`.
- Если авторизация недоступна, запусти публикацию `notion` без теста: `/printing-press publish notion --skip-live-test=auth-unavailable`.
- Запиши заблокированный API `notion` в журнал по моим данным: `/printing-press publish notion --blocked-api-journal`.
Как установить printing-press-publish
1Через Skills CLI (Vercel)
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a claude-code2Вручную: положить файлы
~/.claude/skills/printing-press-publish/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/printing-press-publish/
1Через Skills CLI (Vercel)
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a codex2Вручную: положить файлы
.agents/skills/printing-press-publish/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a cursor2Вручную: положить файлы
.cursor/skills/printing-press-publish/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install mvanhorn/cli-printing-press printing-press-publish2Вручную: положить файлы
.github/skills/printing-press-publish/SKILL.md1Через Skills CLI (Vercel)
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a gemini-cli2Командой платформы
gemini skills install https://github.com/mvanhorn/cli-printing-press --path skills/printing-press-publishВручную: положить файлы
.devin/skills/printing-press-publish/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
Из того же набора7
Весь набор →Похожие
Вопросы о printing-press-publish
Как установить `printing-press-publish` в Claude Code?
Выполните npx skills add mvanhorn/cli-printing-press --skill printing-press-publish -a claude-code.
Что делает `printing-press-publish`?
Скилл публикует сгенерированный CLI в mvanhorn/printing-press-library через запрос на слияние. Если генерация, проверка или тестирование заблокированы, он сообщает причину и останавливается.
Чем обычная публикация отличается от режима `--blocked-api-journal`?
Обычный режим готовит запрос на слияние для CLI. Режим --blocked-api-journal меняет только blocked-apis.json и создаёт отдельный запрос для журнала блокировок.
Исходный текст
SKILL.md · MIT
---
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
created_by: user
---
# /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 notion --blocked-api-journal notion
/printing-press publish
```
## PR shape guard
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.
## 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 <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/`.
## Blocked API Journal Mode
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`:
```bash
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.json
```
Create a journal branch and PR:
```bash
git 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>
```
Open the PR against `mvanhorn/printing-press-library` with a body that includes:
- the held API slug and reason
- whether the block is permanent or tied to `blocking_issue`
- the expected Phase 0 behavior: future `/printing-press <api-slug>` runs warn
before repeating the attempt
After the PR is open, report the URL and stop. Do not continue into normal
printed-CLI package, live-test, registry, or skill-mirror steps.
## Setup
Before doing anything else:
<!-- PRESS_SETUP_CONTRACT_START -->
```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
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.10 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"
```
<!-- PRESS_SETUP_CONTRACT_END -->
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."
## 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": "<home>/printing-press/.publish-repo-<scope>",
"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 `<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.
### 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 `<argument>-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 **<category>**. OK?"
Give the user the option to change it
2. If the manifest does not provide 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, food-and-dining, health, maps, media-and-entertainment, devices, other
- travel
## Step 3.5: The Greptile review contract — read before opening the PR
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"`](https://github.com/mvanhorn/printing-press-library/blob/main/AGENTS.md#automated-code-review-with-greptile); the essentials:
- **The bar is resolving every Greptile finding before merge — the 0-5 score is a confidence signal, not the gate.** A 4/5 with everything resolved is ready; a 5/5 with open P1s is not. Treat every P0 and P1 as blocking; P2s need a fix or a concrete deferral reply.
- **Reviews are incremental**: every push re-triggers a fresh review that can surface new findings. Drive the PR to a *stable* green — never declare done after round one.
- **Read the latest `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:
```bash
python3 .github/scripts/pr-review-state/greptile_feedback.py <PR_NUMBER>
```
- **Timeout recovery**: if the policy gate fails with `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.
- **The score gate**: the policy gate requires the latest Greptile comment on the current head SHA to carry `Confidence Score: ≥ 4/5`. A new push re-runs it — keep the score meeting threshold on the final head.
## Step 4: Validate
Run:
```bash
cli-printing-press publish validate --dir <cli-dir> --json
```
`govulncheck` in this step is intentionally scoped to `<cli-dir>` only. It
uses the default `govulncheck ./...` mode so reachable symbol findings block
publish, while merely-required vulnerable modules without a call path do not
become release blockers. Do not replace this with a full public-library scan or
`govulncheck -show verbose`.
Parse the JSON result. Display each check result to the user:
```
Validating <api-slug>...
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.
**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:
```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"
RESEARCH_DIR="$(dirname "$PROOFS_DIR")/research"
if [ ! -f "$RESEARCH_DIR/research.json" ] && [ -f "$(dirname "$PROOFS_DIR")/research.json" ]; then
RESEARCH_DIR="$(dirname "$PROOFS_DIR")"
fi
if [ ! -f "$RESEARCH_DIR/research.json" ]; then
echo "ERROR: publish live gate requires the run research.json at $RESEARCH_DIR." >&2
exit 1
fi
```
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. Send the raw `--json` transcript
to a private temporary directory outside every manuscript tree. It contains
API response bodies and absolute host paths. `mktemp -d` creates that directory
mode `0700`; the transcript file is mode `0600`. After the gate, delete the
directory: on failure, print the failing commands first, then delete it; on
success, delete it before continuing. `publish package` enforces the same
boundary: it omits live-dogfood and live-gate JSON reports whatever the
filename (a top-level `tests` array whose entries carry `output_sample`, or
`dir` + `binary` + `verdict` + `tests`), plus `publish-live-gate*.json`,
`*-publish-live-gate.json`, `dogfood-results*.json`,
`*-dogfood-results.json`, and `pipeline/` trees, including copies already
saved under proofs by an earlier dogfood run. It still copies
`phase5-acceptance.json` and `phase5-skip.json`. Home paths under
`printing-press` or a dotfile directory, and any absolute `.runstate` path,
are rewritten to `<cli-dir>` and `<runstate>` in other shipped manuscript
text.
```bash
LIVE_GATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/printing-press-publish.XXXXXX")
chmod 700 "$LIVE_GATE_DIR"
LIVE_GATE_JSON="$LIVE_GATE_DIR/${API_SLUG}-${RUN_ID}-publish-live-gate.json"
LIVE_GATE_ARGS=(
dogfood
--dir "$CLI_DIR"
--live
--level full
--timeout 120s
--research-dir "$RESEARCH_DIR"
--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 ! (
umask 077
set +e
"$PRINTING_PRESS_BIN" "${LIVE_GATE_ARGS[@]}" >"$LIVE_GATE_JSON"
live_gate_status=$?
chmod 600 "$LIVE_GATE_JSON" 2>/dev/null || true
exit "$live_gate_status"
); then
echo "Publish live gate failed. See $PROOFS_DIR/phase5-acceptance.json."
jq -r '.tests[]? | select(.status == "fail") | "- \(.command) [\(.kind)]: \(.reason // "failed")"' "$LIVE_GATE_JSON" 2>/dev/null || true
rm -rf "$LIVE_GATE_DIR"
exit 1
fi
rm -rf "$LIVE_GATE_DIR"
```
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:
```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
SOURCE_FILES=$(find "$CLI_DIR" \( -type d \( -name '.git' -o -name '.manuscripts' -o -name '.printing-press' \) -prune \) -o -type f \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' -o -name 'spec.json' -o -name 'spec.yaml' -o -name 'spec.yml' \) -print | LC_ALL=C sort)
SOURCE_FINGERPRINT=$(
while IFS= read -r SOURCE_FILE; do
[ -z "$SOURCE_FILE" ] && continue
SOURCE_REL="${SOURCE_FILE#"$CLI_DIR"/}"
SOURCE_HASH=$(shasum -a 256 "$SOURCE_FILE" | sed 's/[[:space:]].*//')
printf '%s\0%s\n' "$SOURCE_REL" "$SOURCE_HASH"
done <<EOF | shasum -a 256 | sed 's/[[:space:]].*//'
$SOURCE_FILES
EOF
)
if [ -z "$SOURCE_FINGERPRINT" ]; then
echo "ERROR: unable to fingerprint CLI source before writing the Phase 5 skip marker."
exit 1
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" \
--arg source_fingerprint "$SOURCE_FINGERPRINT" \
--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",
source_fingerprint: $source_fingerprint,
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_...
© mvanhorn · MIT · mvanhorn/cli-printing-press