mcpField
P
printing-press-publish

printing-press-publish

github.com/mvanhorn開発4.9k6.9k 回インストール今日更新
リポジトリ

printing-press-publish について

printing-press-publishは、ローカルで生成したコマンドラインツールをプルリクエスト経由でprinting-press-libraryに公開するスキルです。生成が阻まれたAPIについて、ジャーナルだけを送るプルリクエストにも対応します。

主な機能

  1. 01ツール名からライブラリエントリーを特定
  2. 02生成ツールをプルリクエストで公開
  3. 03阻害APIジャーナルのプルリクエスト作成
  4. 04GitHub CLIの認証確認
  5. 05公開と実環境テストのゲート適用

活用シーン

  1. 01生成したツールを公開ライブラリへ提出
  2. 02繰り返し起きるAPIの阻害要因を記録
  3. 03ライブラリエントリーを特定してツールを公開

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mvanhorn/cli-printing-press --skill printing-press-publish

インストール後、Claude Code で/printing-press-publishを入力して読み込まれたか確認します。

詳細

/printing-press publish

ローカルライブラリから生成した CLI を、プルリクエストとして printing-press-library リポジトリに公開します。

/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 の形式チェック

このスキルが作成するのは、生成した CLI の公開 PR のみです。ただし、--blocked-api-journal を指定した場合は、blocked-apis.json の記録用 PR を作成します。公開準備が整っていない CLI の代わりに、ドキュメントのみの PR、計画 PR、提案 PR、仕様 PR を作成することはありません。生成、検証、または実環境でのテストが妨げられた場合は、具体的なブロッカーを報告して停止してください。

ユーザーによる直接の呼び出しが必要です

公開処理では、mvanhorn/printing-press-library をフォークし、ブランチをプッシュして、PR を作成または更新する場合があります。セットアップや検証の前に、呼び出しのコンテキストを確認してください。このスキルが、printing-press-polish の Publish Offer からの連続した呼び出しとして実行された場合(AskUserQuestion への回答や、自動で解決された polish の推奨を含む)は、直ちに停止し、新しいメッセージで /printing-press-publish <cli-name> --from-polish を送信するようユーザーに伝えてください。ユーザー自身が作成した、公開を明示的に依頼する新しいリクエストであれば十分です。ユーザーが直接公開を依頼している場合は、追加の確認を求めないでください。

ユーザー自身が作成した新しいリクエストに --from-polish が含まれる場合は、最終状態の処理で POLISH_HANDOFF=true を記録し、CLI 名を特定する際にはこのマーカーを無視してください。このマーカーは追加の確認ではなく、新しいターンでの公開が完了した後に、単独の polish 実行時に行われていた公開後の振り返りの提案を維持するためだけに使用します。

リクエストに --blocked-api-journal が含まれる場合は、通常の生成済み CLI の公開フローではなく、以下の Blocked API Journal Mode に進んでください。このモードは、ユーザーが「Add to blocked-API journal」を明示的に選択した後、/printing-press の保留パスのメニューから呼び出す場合があります。この親メニューでの選択は、公開ライブラリへの記録を行うユーザー承認として十分です。この記録のみのモードでは、新しいターンでの再呼び出しを求めないでください。

ユーザー自身が作成した新しいリクエストに --skip-live-test=<reason> が含まれる場合は、空でない理由をそのまま SKIP_LIVE_TEST_REASON に記録し、CLI 名を特定する前にこのフラグを取り除いてください。これは公開時の実環境テストのゲートを回避するためにサポートされている唯一の手段です。認証を利用できない場合、既知の上流障害、LAN から到達できないハードウェア API、または同様に具体的で運用担当者が承認したケースに限って使用してください。通常の遅延や、古い Phase 5 マーカーがあることを理由にスキップしてはなりません。

公開ライブラリでは、レジストリ表示用フィールドの正本として、library/<category>/<api-slug>/.printing-press.json と manifest.json を使用します。公開 PR で registry.json、README のカタログセル、cli-skills/pp-<api-slug>/SKILL.md を編集しないでください。これらはすべて、マージ後にライブラリ独自のワークフローによってボットが再生成します。ライブラリの verify-library-conventions.yml にある Fail on changes to generated artifacts チェックは、差分に registry.json または cli-skills/pp-*/SKILL.md が含まれる PR を、フォークか同一リポジトリかを問わず必ず失敗させます。そのため、どちらかを含む公開 PR はレビュー前に却下されます。

公開ライブラリは、CLI ごとのリリース管理も担います。公開 PR で CHANGELOG.md、.printing-press-release.json、またはランタイムの var version = ... を手動で更新しないでください。新しく生成した CLI には、内容が空のリリース台帳のひな形を含めてもかまいません。ライブラリのマージ後ワークフローが、マージ後に最終的な YYYY.M.N リリースを割り当て、ランタイムのバージョンを設定します。既存の公開ライブラリ CLI を置き換える場合は、再生成 PR で変更履歴が失われないよう、既存のリリース台帳ファイルを保持してください。

blocked-apis.json は別扱いです。これは手動で管理する公開ライブラリの記録であり、生成されたレジストリの対象ではありません。記録のみの PR では blocked-apis.json を編集してかまいませんが、library/、registry.json、README のカタログセル、cli-skills/ をステージしてはなりません。

Blocked API ジャーナルモード

このモードは、呼び出しに --blocked-api-journal が含まれている場合にのみ使用します。繰り返し発生する可能性が高く、マシンまたは上流の問題が変わるまで他のユーザーにも影響する、保留中の /printing-press 試行を記録します。

呼び出し元から必要なフィールド:

  • slug:CLI バイナリ名ではなく、正規の API スラッグ。
  • attempted_at:YYYY-MM-DD。
  • verdict:hold。
  • reason:機密情報、ローカルパス、Cookie、トークン、アカウント固有の詳細を含めず、ブロッカーの理由を簡潔に記述します。
  • blocking_issue:分かっている場合は Printing Press の Issue 番号、それ以外は null。
  • permanent:ブール値。

呼び出し元からこれらのフィールドのいずれかが提供されていない場合は、現在の実行コンテキストから安全な値のみを推測します。reason がない、または曖昧な場合は処理を停止し、具体的なブロッカーの説明を1文で尋ねてください。有用性のないジャーナルエントリーは書き込まないでください。

通常の Setup、Configuration、スコープを指定したクローンのクリーンアップ、GitHub 認証チェックを実行します。その後、通常の公開フローとまったく同じ方法で公開ライブラリのクローンを準備します。必要であればフォークし、upstream が mvanhorn/printing-press-library を指すことを確認し、upstream をフェッチして、編集前にクローンを upstream/main にリセットします。

その後、$PUBLISH_REPO_DIR/blocked-apis.json のみを更新します。

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

ジャーナル用ブランチと PR を作成します。

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>

mvanhorn/printing-press-library を対象に PR を開き、本文には次の内容を含めます。

  • 保留された API スラッグと理由
  • ブロックが恒久的なものか、blocking_issue に関連するものか
  • 期待される Phase 0 の動作:今後の /printing-press <api-slug> の実行では、試行を繰り返す前に警告を表示する

PR を開いたら、URL を報告して停止します。通常の印刷 CLI パッケージ、ライブテスト、レジストリ、スキルミラーの手順には進まないでください。

セットアップ

何よりも先に、次を実行します。

# 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"

セットアップ契約の実行後、標準出力から PRINTING_PRESS_BIN=<abs-path> の行を取得します。このスキルで以降に実行するすべての cli-printing-press ... 呼び出しでは、この絶対パスを使用してください(値に置き換え、文字どおりの $PRINTING_PRESS_BIN トークンは使用しません)。上記の export PATH が適用されるのは、それを実行する単一の Bash ツール呼び出しのみです。そのため、後続の呼び出しでは新しいシェルが起動し、cli-printing-press をそのまま指定すると、ユーザーのデフォルトの PATH から解決され、古いグローバル版がローカルビルドを優先してしまう可能性があります。

セットアップで [go-toolchain-old] または [low-disk] が出力された場合は、その注意事項をユーザーに伝え、[setup-error] も出力されていない限り続行します。[go-toolchain-old] は、後続の Go コマンドで必要なツールチェーンがダウンロードされるか、ダウンロードがブロックされている場合に失敗する可能性があることを意味します。[low-disk]` は、生成ファイル、Go のビルドキャッシュ、モジュールのダウンロード、リポジトリのクローンに、この実行で数 GiB の空き容量が必要になる可能性があることを意味します。

バイナリのパスを取得したら、バイナリのバージョン互換性を確認します。このスキルの YAML フロントマターから min-binary-version フィールドを読み取ります。<PRINTING_PRESS_BIN> version --json を実行し、出力からバージョンを解析します。セマンティックバージョニングのルールを使用して、これを min-binary-version と比較します。インストール済みバイナリが最小バージョンより古い場合は、直ちに停止し、ユーザーに次のように伝えます:「cli-printing-press バイナリ vX.Y.Z は必要な最小バージョン vA.B.C より古いです。更新するには go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest を実行してください。」

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 には、永続的な公開設定が 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"
}

module_path_base フィールドは、公開する CLI の Go モジュールパスのプレフィックスを設定します。パッケージ化の際、完全なモジュールパスは <module_path_base>/<category>/<api-slug> として構成されます。別のリポジトリまたはパスに CLI を公開する場合は、このフィールドを編集します。 クリーンアップでシェル固有の ~ の展開に頼らずにパスを確認できるよう、clone_path と scope_dir には展開済みの絶対パスを保存します。クリーンアップで何かを削除するには、managed_by フィールドが必須です。

スコープ指定クローンのクリーンアップ

$PUBLISH_REPO_DIR を作成または再利用する前に、元のワークツリーが存在しなくなったスコープ指定の公開用クローンを整理します。これにより、同時に使用するワークツリーを分離したまま、短期間しか存在しないワークツリーごとにライブラリのクローンが増え続けるのを防げます。

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

ステップ 1:前提条件

gh が認証済みであることを確認します。

gh auth status

失敗した場合は処理を停止し、ユーザーに「GitHub CLI が認証されていません。まず gh auth login を実行してください。」と伝えます。

ステップ 2:API スラッグの解決

次を実行します。

cli-printing-press library list --json

JSON 出力を CLI のリストに変換します。ライブラリでは、CLI 名ではなく API スラッグ(ディレクトリ名)をキーとして使用するようになりました。

名前の解決順序(一貫性を保つため、スコアスキルと一致させます):

  1. 完全一致: 引数がディレクトリ名(API スラッグ)と完全に一致する場合は、それを使用します
  2. CLI 名で一致: 完全一致しない場合は、cli_name フィールドで照合し、次にマニフェストの api_name フィールドから API スラッグを導出します
  3. サフィックスで一致: まだ一致しない場合は、<argument>-pp-cli を cli_name フィールドと照合します
  4. グロブで一致: サフィックスでも一致しない場合は、cli_name または api_name に引数が部分文字列として含まれるエントリを検索します。該当するものは更新日時が新しい順に最大 5 件まで表示します。複数の候補がある場合は、AskUserQuestion で候補を提示し、ユーザーに選択してもらいます
  5. 一致なし: 利用可能な CLI をすべて一覧表示し、ユーザーに選択または再入力を求めます
  6. 引数なし: 名前を指定せずに呼び出された場合は、更新日時順にすべての CLI を一覧表示し、ユーザーに選択してもらいます

解決後、マニフェストの api_name フィールドを読み取り、API スラッグを取得します。以降のすべての操作(ブランチ名、レジストリエントリ、競合検出、パス構築)では、このスラッグを使用します。マニフェストの cli_name は、バイナリレベルの操作にのみ使用します。

候補を提示する際は、API スラッグと更新時刻をわかりやすい形式(例:「2 時間前」「3 日前」)で表示します。

ステップ 3:カテゴリの決定

解決した CLI ディレクトリから .printing-press.json を読み取ります。

カテゴリの解決順序:

  1. マニフェストに category フィールドがある場合は、確認のために提示します:

    「 として公開します。よろしいですか?」 ユーザーが変更できるようにします

  2. マニフェストにカテゴリがない場合は、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

ステップ 3.5:Greptile レビューの契約 — PR を開く前に読む

公開ライブラリへのすべての PR は、Greptile による自動レビューと Greptile policy gate CI ジョブの対象になります。正式な契約は、ライブラリの AGENTS.md → "Automated code review with Greptile" に記載されています。要点は次のとおりです。

  • マージの条件は、すべての Greptile の指摘を解決することです。0~5 のスコアは信頼度の指標であり、ゲートではありません。 指摘がすべて解決済みの 4/5 は準備完了ですが、未解決の P1 がある 5/5 は準備完了ではありません。すべての P0 と P1 をブロッカーとして扱い、P2 は修正するか、具体的な延期理由を返信してください。
  • レビューは段階的に行われます。プッシュのたびに新たなレビューが実行され、新しい指摘が見つかることがあります。PR が安定してグリーンになるまで対応してください。最初のレビュー後に完了と判断してはいけません。
  • インラインのスレッドだけでなく、最新の greptile-apps のトップレベルサマリーを確認してください。 スレッドの一覧が空でも、サマリーに対応が必要な Comments Outside Diff ブロックが含まれることがあります。準備完了と判断する前に、リポジトリのレビュー状態ヘルパーを実行してください:
    python3 .github/scripts/pr-review-state/greptile_feedback.py <PR_NUMBER>
    
  • タイムアウトからの復旧: Timed out waiting for Greptile Review to complete でポリシーゲートが失敗した場合(大規模な新規 CLI の差分がよくある原因です)、ゲートは約 3 分後に @greptileai review を自動投稿します。それでも復旧しない場合は、自分で @greptileai review を投稿して、完了を待ってください。
  • スコアゲート: ポリシーゲートでは、現在のヘッド SHA に対する最新の Greptile コメントに Confidence Score: ≥ 4/5 が含まれている必要があります。新たにプッシュすると再実行されるため、最終のヘッドでもスコアがしきい値を満たすようにしてください。

ステップ 4:検証

次を実行します。

cli-printing-press publish validate --dir <cli-dir> --json

このステップの govulncheck は、意図的に <cli-dir> のみに範囲を限定しています。デフォルトの govulncheck ./... モードを使用するため、到達可能なシンボルに関する検出結果は公開をブロックしますが、呼び出し経路のない、単に必要とされている脆弱なモジュールはリリースのブロッカーにはなりません。これをライブラリ全体のパブリックスキャンや govulncheck -show verbose に置き換えないでください。

JSON の結果を解析します。各チェック結果をユーザーに表示します。

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)

"passed": false の場合は、失敗したチェックを報告して、停止してください。不完全な PR は作成しないでください。 manifest チェックは、公開ライブラリの来歴に関する契約について、正式な判定基準となります。対象には、現在の schema_version、run_id、printing_press_version、printer、printer_name、および MCP が公開されている場合の MCP メタデータファイルが含まれます。このチェックが失敗した場合は、ライブラリの PR を開く前に、最新の Printing Press メタデータを使用して再度印刷またはパッケージ化するようユーザーに伝えてください。

結果の help_output フィールドを保存します。これは PR の説明に使用します。

ステップ 4.5:ライブエンドツーエンドゲート

管理対象の公開用クローンに手を加える前に、公開予定の CLI に対してライブ動作ゲートを再実行します。ステップ 4 ではソースがビルドでき、構造上の検証に合格することを確認します。このステップでは、現在の編集後のツリーが実際の上流 API に対して引き続き動作することを確認します。生成または調整の際に作成された古い phase5-acceptance.json に依存しないでください。そのマーカーが書き込まれた後に CLI が手動編集されている可能性があります。

マーカーの無効化と同期。 受け入れマーカーにはソースフィンガープリントが含まれます。書き込み後に .go ファイルを編集すると、publish package は "phase5 marker source fingerprint does not match" というエラーで失敗します。ソースを変更するたびにこのライブゲートを再実行し、マーカーを両方のコピーに書き込みます。埋め込み先の $CLI_DIR/.manuscripts/<run>/proofs/ と、アーカイブ先の $PRESS_MANUSCRIPTS/<api>/<run>/proofs/ です(原稿の検索はアーカイブ優先、証明ファイルの検索は埋め込み優先です。どちらかの場所に古いコピーがあるとパッケージングがブロックされます)。

CLI マニフェストから Phase 5 の証明ファイルディレクトリを解決します:

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 マーカーは、実行されたソースツリーに紐付けられています。ライブのドッグフードテストのライターは、source_fingerprint とファイルごとのハッシュを自動的に記録します。公開時の検証では、現在の CLI ディレクトリからフィンガープリントを再計算し、変更されたソースファイルがマーカーに記録されている場合はそのファイル名を示して、変更されたツリーのマーカーを拒否します。README のみの編集はこのフィンガープリントの対象外であり、ゲートを無効化しません。

SKIP_LIVE_TEST_REASON が未設定の場合は、ライブのドッグフードテストを完全に実行し、その証明ファイルディレクトリに新しい受け入れマーカーを書き込みます。未加工の --json トランスクリプトは、すべての原稿ツリーの外側にある非公開の一時ディレクトリに送ります。これには API のレスポンス本文とホストの絶対パスが含まれます。mktemp -d は権限モード 0700 のディレクトリを作成し、トランスクリプトファイルの権限モードは 0600 です。ゲートの実行後はディレクトリを削除します。失敗した場合は、先に失敗したコマンドを出力してから削除します。成功した場合は、続行する前に削除します。publish package も同じ境界を適用します。ファイル名にかかわらず、ライブドッグフードおよびライブゲートの JSON レポート(トップレベルに tests 配列があり、そのエントリに output_sample が含まれるもの、または dir + binary + verdict + tests を含むもの)、さらに publish-live-gate*.json、*-publish-live-gate.json、dogfood-results*.json、*-dogfood-results.json、および pipeline/ ツリーを除外します。以前のドッグフードテストで証明ファイルの下に保存されたコピーも除外対象です。phase5-acceptance.json と phase5-skip.json は引き続きコピーされます。printing-press またはドットファイルディレクトリ配下のホームパス、および絶対パスの .runstate は、その他の公開対象の原稿テキスト内で <cli-dir> と <runstate> に置き換えられます。

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"

失敗した場合は、ステップ 4 で passed: false となった場合と同じように、そこで停止します。管理対象クローン、ブランチ、パッケージ、PR は作成しません。失敗したコマンド、該当する場合は終了コード、stderr または理由の抜粋、新しい証明ファイルのパスを報告し、オペレーターがドッグフードテストを再実行して CLI を修正できるようにします。

--skip-live-test=<reason> から SKIP_LIVE_TEST_REASON が設定されている場合は、ドッグフードテストを実行する代わりに、新しいスキップマーカーを書き込みます:

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=""

その後、ステップ 4 の検証を再実行します:

"$PRINTING_PRESS_BIN" publish validate --dir "$CLI_DIR" --json

この 2 回目の検証では、新しい受け入れマーカーまたはスキップマーカーが、パッケージングと公開で必要とされる同じ Phase 5 の要件を満たすことを確認します。失敗した場合は、ステップ 5 に進む前に停止します。

ステップ 5:管理対象クローン

公開スキルは、ライブラリリポジトリの専用クローンを $PUBLISH_REPO_DIR に管理します。

初回セットアップ

$PUBLISH_REPO_DIR が存在しない場合:

  1. プッシュアクセスを検出します:

    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. git プロトコルを検出します:

    USE_SSH=false
    if ssh -T [email protected] 2>&1 | grep -q "successfully authenticated"; then
      USE_SSH=true
    fi
    
  3. アクセス権に基づいてクローンします:

    プッシュアクセスあり(HAS_PUSH が true):

    # Clone directly — origin IS the upstream
    if [ "$USE_SSH" = "true" ]; then
      REPO_URL="[email protected]:mvanhorn/printing-press-library.git"
    else
      REPO_URL="https://github.com/mvanhorn/printing-press-library.git"
    fi
    # Lightweight clone: blobless + shallow + sparse. The publish flow only
    # touches the target CLI's own directory — it no longer regenerates the
    # cli-skills/registry mirror (see Step 6) — so materializing every other
    # CLI's source is wasted bandwidth and disk (a full clone is multiple GB;
    # this is tens of MB). The cone keeps `tools`, `cli-skills`, and the target
    # `library/<category>` so the in-category find/rm/copy operations below work
    # on a real working tree. Cross-category collision checks use `git ls-tree`
    # (which reads the full tree from the blobless clone) instead of `ls`.
    git clone --filter=blob:none --depth 1 --sparse "$REPO_URL" "$PUBLISH_REPO_DIR"
    # Skill-managed clones are owned by this flow; force LF checkout behavior so
    # Windows core.autocrlf defaults do not create CRLF-only mirror diffs.
    git -C "$PUBLISH_REPO_DIR" config core.autocrlf false
    git -C "$PUBLISH_REPO_DIR" sparse-checkout set tools cli-skills library/<category>
    

    プッシュアクセスなし(HAS_PUSH が false):

    # 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="[email protected]:$FORK.git"
      UPSTREAM_URL="[email protected]:mvanhorn/printing-press-library.git"
    else
      FORK_URL="https://github.com/$FORK.git"
      UPSTREAM_URL="https://github.com/mvanhorn/printing-press-library.git"
    fi
    
    # Lightweight clone (blobless + shallow + sparse) — see the push-access
    # branch above for the rationale and cone contents.
    git clone --filter=blob:none --depth 1 --sparse "$FORK_URL" "$PUBLISH_REPO_DIR"
    # Skill-managed clones are owned by this flow; force LF checkout behavior so
    # Windows core.autocrlf defaults do not create CRLF-only mirror diffs.
    git -C "$PUBLISH_REPO_DIR" config core.autocrlf false
    cd "$PUBLISH_REPO_DIR"
    git sparse-checkout set tools cli-skills library/<category>
    git remote add upstream "$UPSTREAM_URL"
    git fetch --filter=blob:none --depth 1 upstream
    
  4. 設定をキャッシュします:

    {
      "managed_by": "printing-press-publish",
      "repo_url": "https://github.com/mvanhorn/printing-press-library",
      "access": "push or fork",
      "gh_user": "<gh username>",
      "protocol": "ssh or https",
      "clone_path": "<expanded $PUBLISH_REPO_DIR>",
      "scope_dir": "<absolute source worktree path>",
      "module_path_base": "github.com/mvanhorn/printing-press-library/library"
    }
    

    $PUBLISH_CONFIG に書き込みます。access フィールドは、その後のすべてのステップで使用するフローを決定します。gh_user フィールドは、リポジトリをまたぐ PR のヘッドに使用されます。module_path_base は常に上流リポジトリを参照します(PR はそのリポジトリに送られます)。

2 回目以降のパブリッシュ

$PUBLISH_CONFIG を読み取り、アクセスが変更されていないか再確認します(ユーザーにプッシュアクセスが付与された、またはアクセスが取り消された可能性があります):

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

アクセス変更によりクローンが削除されていた場合は、上記の初回セットアップを再実行します。そうでなければ、正規のアップストリームに一致するようクローンを最新化します:

cd "$PUBLISH_REPO_DIR"
git config core.autocrlf false

if [ "$(jq -r .access $PUBLISH_CONFIG)" = "push" ]; then
  # Push access: origin IS the upstream
  git fetch --filter=blob:none --depth 1 origin
  git checkout main
  git reset --hard origin/main
  # Remove stale untracked library fragments from prior publish branches before
  # copying this CLI. Ignored files hidden by a branch-local .gitignore can
  # become ordinary untracked files after checkout, and a later broad library
  # add must not sweep another CLI's leftovers into this PR.
  git clean -fdq library/
else
  # Fork: origin is the fork, upstream is canonical
  git fetch --filter=blob:none --depth 1 upstream
  git checkout main
  git reset --hard upstream/main
  # Remove stale untracked library fragments from prior publish branches before
  # copying this CLI. Ignored files hidden by a branch-local .gitignore can
  # become ordinary untracked files after checkout, and a later broad library
  # add must not sweep another CLI's leftovers into this PR.
  git clean -fdq library/
  # Also sync origin (fork) so git push works cleanly
  git push origin main --force-with-lease 2>/dev/null || true
fi

# Existing managed clones may already be sparse for a different publish
# category. Refresh the cone for the current target category before Step 6 uses
# filesystem-based removal and copy operations.
if git -C "$PUBLISH_REPO_DIR" config --bool core.sparseCheckout | grep -qx true; then
  git -C "$PUBLISH_REPO_DIR" sparse-checkout set tools cli-skills library/<category>
fi

クローンが正常であることを検証します:

git rev-parse --is-inside-work-tree
test "$(git rev-parse --abbrev-ref HEAD)" = "main"

これが失敗した場合、クローンは破損しています。$PUBLISH_REPO_DIR を削除し、初回セットアップを再実行してください。

中断状態からの復旧

新しいブランチを作成する前に、コミットされていない変更がないか確認します:

cd "$PUBLISH_REPO_DIR"
git status --porcelain

コミットされていない変更がある場合は、AskUserQuestion でユーザーに確認します:

  • 「リセットして最初からやり直す」
  • 「既存の変更をそのまま続行する」

リセットする場合は、git checkout -- . && git clean -fd を実行します。

パッケージ化前のパブリケーション状態スナップショット

ステップ 6 が管理対象のクローンを変更する前に、この API スラッグが公開ライブラリツリーにすでに存在するかどうかを記録します。ステップ 6 は library/*/<api-slug> を削除して置き換えるため、パッケージ化後に衝突やパブリケーションパスに関する判断を行う場合は、新たな ls ではなく、このパッケージ化前のスナップショットを使用する必要があります。

# Read from the git tree, not the working dir: the sparse checkout only
# materializes the target category, but a slug can collide in any category.
PREEXISTING_MERGED_PATHS=$(git -C "$PUBLISH_REPO_DIR" ls-tree -r --name-only HEAD \
  | sed -n 's#^\(library/[^/]*/<api-slug>\)/.*#\1#p' | sort -u || true)
PREEXISTING_MERGED_COLLISION=false
if [ -n "$PREEXISTING_MERGED_PATHS" ]; then
  PREEXISTING_MERGED_COLLISION=true
  # If this is a category-change reprint, materialize the existing category path
  # before Step 6 runs filesystem-based ledger preservation and removal.
  if git -C "$PUBLISH_REPO_DIR" config --bool core.sparseCheckout | grep -qx true; then
    while IFS= read -r EXISTING_MERGED_PATH; do
      [ -n "$EXISTING_MERGED_PATH" ] || continue
      if [ "$EXISTING_MERGED_PATH" != "library/<category>/<api-slug>" ]; then
        git -C "$PUBLISH_REPO_DIR" sparse-checkout add "$EXISTING_MERGED_PATH"
      fi
    done <<EOF
$PREEXISTING_MERGED_PATHS
EOF
  fi
fi

ステップ 6: パッケージ化

$PUBLISH_CONFIG を読み取り、module_path_base を取得します。API スラッグ(CLI 名ではない)を使って完全なモジュールパスを構築します:

MODULE_PATH="<module_path_base>/<category>/<api-slug>"

例: github.com/mvanhorn/printing-press-library/library/productivity/notion

--module-path は --dest モードで必須です。 --dest でパッケージ化する場合は、 常に --module-path "$MODULE_PATH" を渡してください。これを省略すると go.mod/インポートの書き換えが通知なくスキップされ(RewriteModulePath はこのフラグを条件としています)、 パッケージ化された CLI は module <cli-name> のままとなり、ライブラリ CI は モジュールパスの不一致で PR を拒否します。publish package は書き換え後に ステージングされたツリーのモジュールパスを検証します: --module-path を指定した場合、 ステージングされた go.mod は正確にそのパスを宣言している必要があります(CLI 名のみのモジュールは依然として失敗します)。--module-path を指定しない場合、 正規の github.com/mvanhorn/printing-press-library/library/ プレフィックスが 必要です。ソースツリーに対する単独の publish validate はこのチェックを 警告として表示します — 書き換え前のその段階では CLI 名のみのモジュール名が想定されるためです。決定的な失敗はパッケージ化ステップで発生します。

publish package を --target 付きで実行し、CLI を一意の一時ディレクトリに ステージングしてから、パブリッシュリポジトリにコピーします:

PUBLISH_STAGING_ROOT="/tmp/printing-press/publish"
mkdir -p "$PUBLISH_STAGING_ROOT"
STAGING_PARENT="$(mktemp -d "$PUBLISH_STAGING_ROOT/<api-slug>-XXXXXX")"
STAGING_DIR="$STAGING_PARENT/package"

# Reprints pass the existing public-library entry so package stamps its
# runtime version declaration layout instead of leaving 0.0.0-dev.
BASE_CLI_DIR="$(find "$PUBLISH_REPO_DIR/library" -mindepth 2 -maxdepth 2 -type d -name "<api-slug>" -print -quit)"
PACKAGE_BASE_ARGS=()
if [ -n "$BASE_CLI_DIR" ]; then
  PACKAGE_BASE_ARGS=(--base-dir "$BASE_CLI_DIR")
fi

cli-printing-press publish package \
  --dir <cli-dir> \
  --category <category> \
  --target "$STAGING_DIR" \
  --module-path "$MODULE_PATH" \
  "${PACKAGE_BASE_ARGS[@]}" \
  --json

JSON の結果を解析します。staged_dir、module_path、manuscripts_included、run_id を記録します。module_path フィールドは、パッケージ化された CLI の go.mod とインポートパスに設定された Go モジュールパスを確認するものです。

publish package は、成功を返す前に、コピーされたマニュスクリプトを含む ステージングされた CLI に対して必須のベンダープレフィックスのシークレットスキャンを実行します。vendor-prefix tokens detected が報告された場合は、 中断し、報告された file:line の検出結果を削除または伏せてから再試行してください。これはハードゲートであり、gitleaks、trufflehog、または宛先リポジトリのプッシュ保護には依存しません。

次に、ステージングされた CLI をパブリッシュリポジトリにコピーし、 これが再版の場合は公開ライブラリのリリース台帳ファイルを保持しつつ、 既存のバージョンを置き換えます:

STAGED_CLI_DIR="$STAGING_DIR/library/<category>/<api-slug>"
DEST_CATEGORY_DIR="$PUBLISH_REPO_DIR/library/<category>"
DEST_CLI_DIR="$DEST_CATEGORY_DIR/<api-slug>"

if [ ! -d "$STAGED_CLI_DIR" ]; then
  echo "missing staged CLI directory: $STAGED_CLI_DIR" >&2
  exit 1
fi
mkdir -p "$DEST_CATEGORY_DIR"

# Preserve release-ledger files and existing shipcheck reports from the
# current public-library entry before removing it. New CLIs omit .printing-press-release.json until the library's
# post-merge workflow stamps a real release; reprints keep existing changelog
# history and release metadata until that workflow stamps the next release.
# Fresh prints strip dogfood-results.json and workflow-verify-report.json from
# the staged tree; reprints must copy those catalog files back so the overlay
# does not delete them.
RELEASE_LEDGER_TMP="$(mktemp -d)"
PUBLISH_SWAP_DIR="$(mktemp -d "$DEST_CATEGORY_DIR/.<api-slug>.XXXXXX")"
trap 'rm -rf "$RELEASE_LEDGER_TMP" "$PUBLISH_SWAP_DIR"' EXIT
for LEDGER_FILE in CHANGELOG.md .printing-press-release.json dogfood-results.json workflow-verify-report.json; do
  EXISTING_LEDGER="$(find "$PUBLISH_REPO_DIR/library" -mindepth 3 -maxdepth 3 -path "*/<api-slug>/$LEDGER_FILE" -print -quit)"
  if [ -n "$EXISTING_LEDGER" ]; then
    cp "$EXISTING_LEDGER" "$RELEASE_LEDGER_TMP/$LEDGER_FILE"
  fi
done

# Copy staged CLI into a same-category swap dir before deleting the current
# public-library entry. This keeps a failed copy from leaving the publish repo
# with the old CLI removed.
cp -R "$STAGED_CLI_DIR/." "$PUBLISH_SWAP_DIR/"

for LEDGER_FILE in CHANGELOG.md .printing-press-release.json dogfood-results.json workflow-verify-report.json; do
  if [ -f "$RELEASE_LEDGER_TMP/$LEDGER_FILE" ]; then
    cp "$RELEASE_LEDGER_TMP/$LEDGER_FILE" "$PUBLISH_SWAP_DIR/$LEDGER_FILE"
  fi
done

# Remove existing version (handles category changes), then atomically install
# the prepared replacement within the destination category.
rm -rf "$PUBLISH_REPO_DIR/library"/*/"<api-slug>"
mv "$PUBLISH_SWAP_DIR" "$DEST_CLI_DIR"
rm -rf "$RELEASE_LEDGER_TMP"
trap - EXIT

# Reprints must preserve the base CLI's runtime version declaration layout as
# well as its ledger files. Fresh prints can move `var version = ...` between
# files, but the public library's release-ledger guard rejects those moves in a
# normal publish PR because the post-merge release workflow owns version stamps.
cd "$PUBLISH_REPO_DIR"
VERSION_DECL_BASE_REF=upstream/main
if ! git rev-parse --verify --quiet "$VERSION_DECL_BASE_REF" >/dev/null; then
  VERSION_DECL_BASE_REF=origin/main
fi
VERSION_DECL_DIFF="$(git diff --unified=0 "$VERSION_DECL_BASE_REF" -- \
  "library/*/<api-slug>/internal/cli/root.go" \
  "library/*/<api-slug>/internal/cli/version.go" \
  "library/*/<api-slug>/cmd/<api-slug>-pp-mcp/main.go")" || {
  echo "failed to compare runtime version declarations with ${VERSION_DECL_BASE_REF}" >&2
  exit 1
}
printf '%s\n' "$VERSION_DECL_DIFF" \
  | grep -E '^[+-][[:space:]]*var version[[:space:]]*=' || true

# If the command prints a change, stop. Do not hand-edit version declarations.
# publish package --base-dir should already have reconciled the replacement to
# the base tree:
# - A root.go declaration stays in root.go with the exact stamped value; remove
#   only the duplicate declaration from version.go and keep its command code.
# - A version.go declaration stays in version.go with the exact stamped value;
#   remove any fresh declaration added elsewhere in the internal CLI package.
# - An MCP main declaration stays in MCP main with the exact stamped value. If
#   the base MCP main hardcodes the version instead, preserve that expression.
# - If the base has no declaration in one of these runtime surfaces, preserve
#   that no declaration layout and its existing literal/reference form. Do not
#   introduce the fresh print's 0.0.0-dev declaration.
# Re-run publish package with --base-dir pointing at the existing library entry,
# then repeat this diff. Do not continue until the command prints no matching lines.

# 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 the names local build paths can drop: bare slug,
# CLI binary, live-dogfood probe binary, and MCP peer.
rm -f "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>" \
      "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>" \
      "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>-dogfood" \
      "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>-pp-mcp"

# publish package removes `.printing-press-live-check-*` directories and
# compiled executables at any depth from the staged tree before this copy.
# Do not copy those paths back from <cli-dir>; a depth-1 rm does not see them.

# Defense-in-depth: validate printer attribution before README and registry surfaces.
PRINTER=$(jq -r '.printer // ""' "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json")
PRINTER_NAME=$(jq -r '.printer_name // ""' "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json")
if [ -z "$PRINTER" ]; then
  echo "ERROR: manifest .printer is empty. Set 'git config --global github.user <your-handle>' and re-print before publishing."
  exit 1
fi
if [ "$PRINTER" = "USER" ] || [ "$PRINTER" = "user" ]; then
  echo "ERROR: manifest .printer is the literal sentinel \"$PRINTER\" (git config github.user was unset at print time). Set it and re-print before publishing."
  exit 1
fi
if [ -z "$PRINTER_NAME" ]; then
  echo "ERROR: manifest .printer_name is empty. Set 'git config --global user.name <your display name>' and re-print before publishing."
  exit 1
fi

# Do NOT regenerate or commit `cli-skills/pp-<api-slug>/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. Also do NOT hand-update
# CHANGELOG.md, .printing-press-release.json, or runtime version strings for
# release accounting; the library release-ledger workflow owns those post-merge.

# Verify this changed/new CLI builds and has no reachable Go vulnerabilities from the publish repo
cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
  && go build ./... \
  && go run golang.org/x/vuln/cmd/[email protected] ./...

脆弱性の検証は、パブリッシュ PR では library/<category>/<api-slug> に スコープを限定してください。公開ライブラリは歴史的なコレクションであり、 無関係なすべての PR で完全に最新の状態に保つことはできません。ライブラリ全体の govulncheck スイープは スケジュール実行/レポートのワークフローに属し、ブロッキング CI は追加または 変更された CLI モジュールのみをスキャンすべきです。

パブリッシュリポジトリへのコピーとビルド検証が完了したら、ステージング ディレクトリを削除します:

rm -rf "$STAGING_PARENT"

注: staged_dir は API スラッグ(例: espn)をキーとしており、パブリッシュリポジトリのディレクトリ構成と一致します。コピー手順は同名コピーであり、名前変更ではありません。

ステップ 6.5:カスタマイズを記録する

衝突検出やブランチ作成の前に、パッケージ済み CLI のカスタマイズインデックスを確認します。

インデックスには次のいずれかの形式が含まれます。パッチごとのディレクトリ .printing-press-patches/(現行形式)、または従来の単一配列 .printing-press-patches.json(まだ正規化されていない旧形式のプリント)です。 存在する形式を検証してください。

PATCHES_DIR="$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press-patches"
PATCHES_INDEX="$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press-patches.json"
if [ -d "$PATCHES_DIR" ]; then
  # Per-patch directory: every <id>.json must be a JSON object carrying the same
  # provenance the legacy single-array file kept at its top level (now per file),
  # so validation is at parity with the legacy branch below. _meta.json
  # (CLI-global lists) and .gitkeep are exempt.
  for f in "$PATCHES_DIR"/*.json; do
    [ -e "$f" ] || continue
    [ "$(basename "$f")" = "_meta.json" ] && continue
    if ! jq -e '
      (type == "object") and
      (.schema_version | type == "number") and
      (.id | type == "string" and length > 0) 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)
    ' "$f" >/dev/null; then
      echo "ERROR: packaged CLI has a malformed patch file $f. Reprint with a current cli-printing-press binary before publishing."
      exit 1
    fi
  done
elif [ -f "$PATCHES_INDEX" ]; then
  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
else
  echo "ERROR: packaged CLI is missing its patches index (.printing-press-patches/ or .printing-press-patches.json). Reprint with a current cli-printing-press binary before publishing."
  exit 1
fi

現行の cli-printing-press generate で作成した新しいプリントには、 .gitkeep だけを含む .printing-press-patches/ ディレクトリが含まれます。 生成後に手動でカスタマイズしていない場合は、そのままにしてください。 どちらの形式も存在しない場合、CLI は古いバイナリで生成されています。 決定論的な来歴フィールドを手作業で作成するのではなく、 現行の cli-printing-press ビルドで再プリントしてください。

ステップ 6.6:コントリビューターの帰属を記録する

この公開作業を行う人が CLI の最初の作成者ではない場合、README の署名欄、 NOTICE、および公開レジストリでクレジットされるよう、コントリビューターとして記録します。 このコマンドはべき等です。作成者と、すでに一覧に含まれている人はスキップされるため、 公開のたびに実行しても問題ありません。

"$PRINTING_PRESS_BIN" contributors add \
  --dir "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
  || echo "note: this binary predates 'contributors add'; skipping contributor recording"

このステップはベストエフォートです。contributors add は追加専用のコマンドなので、 この機能より前のバイナリでは、公開を妨げずに記録をスキップします (min-binary-version の下限はメジャーバージョンのみを追跡します)。 この公開が再プリント(ゼロからの再生成)である場合は --front を指定し、 再プリント担当者をコントリビューターの先頭に記載してください。 contributors[] や creator ブロックは手作業で編集しないでください。 作成者は恒久的なものであり、一覧はコマンドが管理します(マニフェストを権威ある情報源とするルールに準拠)。

プリントまたは公開作業中に生成された CLI ファイルを変更した場合は、 ライブラリの PR を作成する前に、カスタマイズごとに簡潔なエントリーを 1 つ記録してください。 パッチごとに .printing-press-patches/<id>.json ファイルを 1 つ作成します (このディレクトリは従来の patches[] 配列に代わるもので、PR が同時に進行しても競合しません)。 これらのエントリーは、ジェネレーターの出力を超えて保持する必要があるものを将来のエージェントや 再生成ツールに伝える、永続的な手動編集の取り決めです。

次の形式を使用してください(ファイルは 1 つ、.printing-press-patches/<id>.json)。

{
  "schema_version": 2,
  "id": "<api-slug>-<short-feature-name>",
  "applied_at": "<YYYY-MM-DD>",
  "base_run_id": "<copy from .printing-press.json>",
  "base_printing_press_version": "<copy from .printing-press.json>",
  "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/<n>"
}

ルール:

  • ファイル名にはパッチの id を使用します。公開ライブラリ内で検索しやすいよう、 API スラッグを接頭辞にしたケバブケースの ID を使用してください。
  • summary と reason は簡潔にします。各エントリーはインデックスであり、 git diff の複製ではありません。
  • 同じコードレベルのカスタマイズの一部である場合は、Go 以外のサポートファイルも files に含めてください。 README/SKILL.md だけの仕上げには、パッチマニフェストのエントリーは不要です。 publish validate は記録を読み取り、記録された files[] のパスが存在しない場合、 パッチごとのレコードに schema_version がない場合や未対応のバージョンを宣言している場合、 または宣言された call_sites/markers/marker の文字列が記録されたファイルに存在しない場合に失敗します。 カスタマイズの削除を別の場所に残った部分文字列が隠せないよう、 files[] はすべての call_sites/markers/marker の検索文字列に必須です。 検索文字列は、記録されたファイル内でのみ確認されます。
  • インラインの // PATCH(...) ソースコメントは、任意のナビゲーション補助です。 公開ライブラリの検証ツールが必要とするのは、パッチインデックス(ディレクトリまたは従来のファイル)と 適切な形式のエントリーです。マーカーとコメントの対応付けは必須ではありません。
  • 適用されなくなった古い検証ツールやパイプラインのバグを回避するためだけに存在するエントリーは、 古い回避策ファイルを引き継ぐのではなく削除してください。

公開ライブラリの正式な作成者向け規約については、 mvanhorn/printing-press-library の AGENTS.md にある 「.printing-press-patches/ records library-side customizations」セクションを読んでください。

ステップ 7:衝突の検出と解決

管理対象のクローンを最新の状態にした後、ブランチや PR を作成する前に、名前の衝突を確認してください。 これは以前の「既存 PR の確認」ステップに代わるものです。

検出

次のチェックを順番に実行します。

1. 管理対象のクローン内で、マージ済みの CLI を確認します。

MERGED_COLLISION="$PREEXISTING_MERGED_COLLISION"
MERGED_PATHS="$PREEXISTING_MERGED_PATHS"

Step 5 のパッケージ化前のスナップショットを使用してください。ここでは ls "$PUBLISH_REPO_DIR/library"/*/"<api-slug>" を再実行しないでください。Step 6 ですでに新しいパッケージがそのパスにコピーされているため、新たに表示されたすべてのパッケージがマージ済みの衝突と誤認されます。MERGED_COLLISION=true の場合は、MERGED_PATHS からカテゴリパスを記録してください。

2. すべてのオープン PR を確認します(作成者を問いません)。

gh pr list --repo mvanhorn/printing-press-library --head "feat/<api-slug>" --state open --json number,title,url,author

一覧が空でない場合は、PR_COLLISION=true を記録します。各 PR について、PR 番号、URL、作成者のログイン名を記録してください。

3. 自分の PR を特定します。

Step 2 の PR 一覧を --author @me で絞り込みます。

フォークベースの PR では、head にユーザー名のプレフィックスが含まれます。

ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")

if [ "$ACCESS" = "fork" ]; then
  HEAD_REF="$GH_USER:feat/<api-slug>"
else
  HEAD_REF="feat/<api-slug>"
fi

gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state open --author @me --json number,title,url

見つかった場合は、OWN_PR=true を記録し、EXISTING_PR_NUMBER と EXISTING_PR_URL を保存します。

オープン PR が見つからなかった場合は、同じブランチ上に以前マージされた PR がないかも確認してください。作成者は自分に限りません。

MERGED_PR=$(gh pr list --repo mvanhorn/printing-press-library --head "$HEAD_REF" --state merged --json number --jq '.[0].number' 2>/dev/null)

MERGED_PR が空でない場合、ブランチ名はすでに使用され、マージ済みです。Step 8 でマージ済みブランチを再利用せず、新しいブランチ名(例:feat/<api-slug>-YYYYMMDD)を作成するように、BRANCH_MERGED=true を設定します。マージ済みブランチに強制プッシュしないでください。gh pr edit は、誰も確認していないクローズ済み PR を警告なく更新してしまいます。

作成者を問わない検索では、スカッシュゾンビブランチも検出できます。GitHub でスカッシュマージしても、元のブランチはリモートに残り、スカッシュ前のコミット参照が「main より先に進んでいる」ように見えますが、内容はスカッシュコミットと同等です。このチェックを行わないと、スキルはゾンビブランチを新規公開と誤判定し、その後 git push -u がリモートブランチの存在を理由に失敗します。タイムスタンプを付ければ、この問題を完全に回避できます。

衝突なし

マージ済みの CLI が存在せず、自分の PR を除いて一致するオープン PR もない場合は、自分の PR の確認結果から EXISTING_PR_NUMBER を設定し(該当しない場合は空にします)、通常どおり Step 8 に進みます。

自分のオープン PR が見つかった場合は、ユーザーに次のように伝えます。

「<api-slug> のオープン PR #N が見つかりました。新しいバージョンで更新します。」

衝突を検出 — 情報を表示

見つかった内容をユーザーに表示します。

⚠️  Name collision detected for <api-slug>

  Merged: <category>/<api-slug> exists in the library
  Open PR: #<number> by <author> — <url>

該当する行をすべて表示します。OWN_PR=true の場合は、PR に「(あなたの PR)」と付記します。

解決方法

AskUserQuestion を使って、次の 3 つの選択肢を提示します。

OWN_PR=true の場合(自分のオープン PR が存在する場合):

  • Update — 既存の PR を新しいバージョンで更新します(デフォルト。現在の動作を維持します)
  • Alongside — 自分のものに修飾子を付けて名前を変更し、既存のものと並べて公開します
  • Bail — 公開をキャンセルします

他のユーザーの PR との衝突がある場合、またはマージ済みのものとの衝突のみの場合:

  • Replace — 既存の CLI を意図的に上書きします
  • Alongside — 自分のものに修飾子を付けて名前を変更し、既存のものと並べて公開します
  • Bail — 公開をキャンセルし、既存の CLI/PR を確認します
更新パス(自分の PR)

これは、ブランチの差分に対するガードを加えた既存の更新フローです。検出ステップで EXISTING_PR_NUMBER を設定して Step 8 に進みます。Step 8 では、現在の PR ブランチの head を取得し、新しいパッケージによって元に戻されるブランチ固有の修正がないか確認してから、強制プッシュと PR の説明の更新を行います。

置き換えパス

マージ済みの CLI または自分の PR の場合: 通常の確認を行います。

「これにより、既存の <api-slug> が置き換えられます。続行しますか?」

他のユーザーの PR の場合: 他の作成者の名前を含む、より強い確認を行います。

「⚠️ これにより、<author> の <api-slug>(PR #N)が置き換えられます。本当によろしいですか?」

確認された場合:

  • PR の説明に、次の内容を含める必要があります。⚠️ **Replaces existing \`** — <ユーザーが提示した理由、または「newer version」>`
  • EXISTING_PR_NUMBER="" を設定します(新しい PR を作成し、相手の PR は更新しません)
  • 通常どおり Step 8 に進みます
並行パス(リネーム)

1. マニフェストの api_name フィールドから元の API スラッグを抽出します:

# Read from .printing-press.json in the publish repo's staged CLI
ORIGINAL_API_SLUG=$(cat "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json" | jq -r '.api_name')

2. スラッグ形式を使用してリネーム候補を生成します。 選択したスラッグから新しい CLI 名を導出します:

  • 数値: <api-slug>-2(衝突する場合は -3、-4 などを試します)
  • 非数値: <api-slug>-alt
  • カスタム: 修飾語をユーザーに入力してもらいます

ユーザーがスラッグを選択したら、次を計算します:

NEW_API_SLUG="<chosen-slug>"
NEW_CLI_NAME="${NEW_API_SLUG}-pp-cli"

ユーザーに形式を提示します:

「リネーム形式: <api-slug>-<qualifier>。修飾子を選んでください:」

  1. 2 → <api-slug>-2
  2. alt → <api-slug>-alt
  3. カスタム修飾子を入力

3. 提示する前に、各候補が衝突しないことを確認します:

# Check merged (read the git tree, not the sparse working dir)
git -C "$PUBLISH_REPO_DIR" ls-tree -r --name-only HEAD \
  | sed -n 's#^\(library/[^/]*/<suggestion>\)/.*#\1#p' | sort -u
# Check open PRs
gh pr list --repo mvanhorn/printing-press-library --head "feat/<suggestion>" --state open --json number

候補が衝突する場合は、それをスキップするか、数値サフィックスをインクリメントします。

4. 公開リポジトリで CLI をリネームします:

Step 6 でステージング済みの CLI を $PUBLISH_REPO_DIR にコピーしているため、リネームはそのディレクトリに対して実行します。注意: RenameCLI はコンテンツ置換を行うため、--old-name/--new-name は引き続き CLI 名形式(例: dub-pp-cli)を使用します。スラッグだけにすると副次的な破損を引き起こす可能性があります。--dir パスはスラッグをキーとしたディレクトリを使用します。リネームは go.mod、残存するモジュールパスのスラッグ、インストーラーのスラッグ、環境変数プレフィックス、および research.json の api_name(.manuscripts/ 配下を含む)も書き換えます。リネーム成功後にこれらを手動で修正しないでください。

cli-printing-press publish rename \
  --dir "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
  --old-name <old-cli-name> \
  --new-name "$NEW_CLI_NAME" \
  --json

JSON の結果をパースします。"success": true を確認します。new_dir が $PUBLISH_REPO_DIR/library/<category>/$NEW_API_SLUG になっているはずである点に注意してください。

5. Step 8 のためにすべての下流参照を更新します:

  • ブランチ名: feat/$NEW_API_SLUG(古いスラッグではない)
  • PR タイトル: feat($NEW_API_SLUG): add $NEW_API_SLUG
  • コミットメッセージ: feat($NEW_API_SLUG): add $NEW_API_SLUG
  • registry.json エントリ: name → $NEW_API_SLUG
  • EXISTING_PR_NUMBER="" を設定(リネームされた CLI では常に新しい PR)

新しい名前で Step 8 に進みます。

中断パス

存在するものへのリンクを表示します:

  • マージ済みの場合: 「既存の CLI: library/<category>/<api-slug>/」
  • オープンな PR がある場合: 「オープンな PR: 」

公開フローを終了します。Step 6 で既に $PUBLISH_REPO_DIR にファイルを書き込んでいる場合は、管理対象クローンで git checkout -- . && git clean -fd を実行してクリーンアップします。

Step 8: ブランチ、コミット、PR

ブランチを作成する

EXISTING_PR_NUMBER が設定されている場合(既存の PR を更新する場合):

置き換える前に、現在の PR ブランチを取得して内容を確認します。最新の origin/main と、新たにパッケージ化された library/<category>/<api-slug>/ ツリーが 更新案になります。リモートの PR ブランチには、drive-to-green ループで受け入れられた レビュー修正が含まれている場合もあります。ブランチのみにある編集内容を 黙って消してはいけません。

UPDATE_BRANCH="feat/<api-slug>"
UPDATE_BASE_REF="refs/printing-press-update-base/<api-slug>"

git fetch origin "+main:refs/remotes/origin/main" "+$UPDATE_BRANCH:$UPDATE_BASE_REF"

# Show the scoped change from the current PR head to the new packaged working
# tree. This is informational for clean updates and mandatory context for holds.
git diff --stat "$UPDATE_BASE_REF" -- "library/<category>/<api-slug>/"

# Branch-only paths are files that exist on the current PR branch but are absent
# from the new packaged working tree. These are always a hold because a
# force-push would delete them.
WORKTREE_PATHS=$(find "library/<category>/<api-slug>" -type f -print 2>/dev/null | sort)
BRANCH_ONLY_PATHS=$(comm -23 \
  <(git ls-tree -r --name-only "$UPDATE_BASE_REF" -- "library/<category>/<api-slug>/" | sort) \
  <([ -n "$WORKTREE_PATHS" ] && printf '%s\n' "$WORKTREE_PATHS" || true))

# Modified paths need human review only when a branch patch relative to
# origin/main is not present in the new packaged working tree. A strict superset
# passes: if the branch patch can be reverse-applied from the working tree, the
# fix is still there even if the file also has fresh generated changes.
BRANCH_ONLY_EDITS=$(git diff --name-only origin/main "$UPDATE_BASE_REF" -- "library/<category>/<api-slug>/" | while read -r path; do
  [ -n "$path" ] || continue
  if git diff origin/main "$UPDATE_BASE_REF" -- "$path" | git apply --check --reverse >/dev/null 2>&1; then
    continue
  else
    printf '%s\n' "$path"
  fi
done | sort -u)

if [ -n "$BRANCH_ONLY_PATHS$BRANCH_ONLY_EDITS" ]; then
  echo "HOLD: updating PR #$EXISTING_PR_NUMBER would overwrite branch-only changes."
  if [ -n "$BRANCH_ONLY_PATHS" ]; then
    echo "Files present on the PR branch but missing from the new package:"
    printf '%s\n' "$BRANCH_ONLY_PATHS" | sed 's/^/- /'
  fi
  if [ -n "$BRANCH_ONLY_EDITS" ]; then
    echo "Files edited on the PR branch and changed again by the new package:"
    printf '%s\n' "$BRANCH_ONLY_EDITS" | sed 's/^/- /'
  fi
  echo "Do not push yet. Reconcile by restoring the branch-only fixes onto the new tree, or ask the user for explicit overwrite confirmation after showing the paths above."
  exit 1
fi

ガード処理が終了した場合は、AskUserQuestion を使ってユーザーに次の 2 つの選択肢を提示します。

  • 先に差分を調整する — 新しいパッケージ化済みツリーに、名前が示されたブランチのみのファイルや編集内容を復元し、コードレベルの修正については対応する .printing-press-patches/<id>.json レコードを維持または追加してから、Step 6 の検証を再実行し、この差分ガードを再実行します。
  • 意図的に上書きする — 一覧に記載されたパスが不要であることをユーザーが確認した後に限り、続行し、上書きしたブランチのみのパスを明記した PR 本文の注記を含めます。

ガードでブランチのみのパスや編集内容が見つからなかった場合は、ローカルブランチを上書きします。

git checkout -B feat/<api-slug>

EXISTING_PR_NUMBER が空で、BRANCH_MERGED が true の場合(以前の PR がマージ済みの場合):

タイムスタンプ付きブランチを自動作成します。マージ済みのブランチ名は再利用しないでください。

git checkout -b feat/<api-slug>-$(date +%Y%m%d)

EXISTING_PR_NUMBER が空で、BRANCH_MERGED が設定されていない場合(未完了の PR もマージ済みの PR もない場合):

古いブランチと競合する PR がないか確認します。

# Check local and remote branches
LOCAL_BRANCH=$(git branch --list "feat/<api-slug>" | head -1)
REMOTE_BRANCH=$(git ls-remote --heads origin "feat/<api-slug>" 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/<api-slug>" --state open --json number,author --jq '.[0]' 2>/dev/null)
fi

別のユーザーの未完了 PR がこのブランチに存在する場合(OTHER_PR が空でなく、作成者が @me ではない場合):

"Someone else has an open PR for <api-slug> (PR #N by @author). Creating a timestamped branch to avoid conflicts."

競合を避けるため、タイムスタンプ付きブランチ feat/<api-slug>-YYYYMMDD を自動作成します。上書きは相手の作業を踏みにじることになるため、選択肢として提示しないでください。

ブランチは存在するが、競合する PR がない場合(以前にクローズまたはマージされた PR の古いブランチの場合):

AskUserQuestion で確認します。

"Found a stale branch feat/<api-slug> (likely from a previous publish). Overwrite it?"

  • "Overwrite existing branch" — ブランチ名を再利用する
  • "Create timestamped variant (feat/-YYYYMMDD)"

ブランチが存在しない場合: 通常どおり作成します。

# New branch:
git checkout -b feat/<api-slug>

# Overwrite existing:
git checkout -B feat/<api-slug>
コミットしてプッシュする
cd "$PUBLISH_REPO_DIR"
git add -A library/
# The staged package has already stripped local binaries and passed the
# mandatory secret/PII scans, so it is the source of truth for this publish.
# Force-add the whole CLI directory after the broad add: destination-repo or
# package-local .gitignore rules such as `*-pp-cli`, `*-pp-mcp`,
# `/.manuscripts/`, or report filenames must not silently suppress required
# publish artifacts under cmd/, .manuscripts/, or metadata files.
git add -f "library/<category>/<api-slug>/"

# Pre-commit scope guard: only this CLI's replacement plus any pre-existing
# merged paths for the same slug may be staged. This catches stale untracked
# fragments from previous publish branches before they leak into the wrong PR.
EXPECTED_STAGE_PREFIXES=$(printf '%s\n' "library/<category>/<api-slug>/" "$PREEXISTING_MERGED_PATHS" | sed '/^$/d; s#/*$#/#' | sort -u)
UNEXPECTED_STAGED=$(git diff --cached --name-only | awk -v prefixes="$EXPECTED_STAGE_PREFIXES" '
BEGIN {
  n = split(prefixes, p, "\n")
  while ((getline line) > 0) {
    matched = 0
    for (i = 1; i <= n; i++) {
      if (p[i] != "" && (line == p[i] || index(line, p[i]) == 1)) {
        matched = 1
        break
      }
    }
    if (!matched) print line
  }
}')
if [ -n "$UNEXPECTED_STAGED" ]; then
  echo "ERROR: publish staged paths outside the expected CLI scope:" >&2
  printf '%s\n' "$UNEXPECTED_STAGED" | sed 's/^/- /' >&2
  echo "Reset the managed clone and rerun publish package before committing." >&2
  exit 1
fi
git commit -m "feat(<api-slug>): add <api-slug>"

origin(プッシュ権限のないユーザーの場合はフォーク、プッシュ権限のあるユーザーの場合はアップストリーム)にプッシュします。

既存の PR を更新する場合(EXISTING_PR_NUMBER が設定されている場合):

# Only run this after the update-path divergence guard above has passed, or
# after the user explicitly confirmed an intentional overwrite of the named
# branch-only paths.
git push --force-with-lease -u origin feat/<api-slug>

新しい PR を作成し、以前に「Overwrite existing branch」を選択した場合:

git push --force-with-lease -u origin feat/<api-slug>

それ以外の場合(新しいブランチで、競合がない場合):

git push -u origin feat/<api-slug>
プッシュしたコミットの SHA を取得する

プッシュ後、HEAD コミットの SHA を取得します。これは、PR 本文に永続的な原稿へのリンクを作成するために使用します(下記の「PR の説明を作成する」を参照)。

HEAD_SHA=$(git rev-parse HEAD)

SHA は PR が存続する間、mvanhorn/printing-press-library 上で解決可能です(GitHub はフォーク PR の HEAD コミットをアップストリームの refs/pull/<N>/head にミラーリングします)。また、PR がマージされ、ブランチが削除された後も有効です。このスキルを実行するたびに、プッシュ後の最新の HEAD_SHA を取得して本文を書き換えるため、このスキルによる更新後もリンクは最新の状態に保たれます。このスキル以外でブランチが強制プッシュされた場合は、/printing-press-publish を再実行して本文を更新してください。以前のリンクも引き続き有効ですが、スキル外のプッシュ前の原稿内容を指します。

PR を作成または更新する

$PUBLISH_CONFIG から access と gh_user を読み取ります。これらは gh pr create の呼び出し方法を決定します。

フォークベースの PR の場合(access が fork):--head <gh_user>:feat/<api-slug> を使用し、GitHub がフォークからアップストリームへのリポジトリ間 PR を作成できるようにします。--head を指定しないと、gh pr create はユーザーがプッシュできないアップストリームリポジトリでブランチを探そうとして失敗します。

プッシュ権限がある場合の PR では(access が push):--head feat/<api-slug> を使用し、管理対象のクローンやシェルセッションで別のブランチがチェックアウトされている場合でも、このフローでプッシュしたブランチから GitHub が PR を作成できるようにします。 PR の説明文を次の情報から作成します:

  • マニフェスト(description、api_name、category、printing_press_version、spec_url)
  • Step 6 の後、パッケージ化された CLI のマニフェストにある novel_features 配列
  • Step 4 で取得した help_output
  • CLI の README(最初の 2~3 段落、または README がない旨)
  • .manuscripts/<run-id>/research/ と .manuscripts/<run-id>/proofs/ 以下のすべてのファイルへのリンク。各リンクは、必ず https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/<subdir>/<filename> 形式の完全な URL にします。相対パスは使用しないでください(GitHub では …/pull/ を基準に解決され、…/pull/library/… という壊れた URL になります)。ディレクトリへのリンクも使用しないでください(blob ビューにはファイルが必要です)。実際のファイルを列挙し、存在しないファイルをでっち上げたり、省略したりしないでください。
  • Step 4 の検証結果
  • Step 4.5 の公開ライブゲートの結果。--skip-live-test の理由が明示されている場合は、それも含めます
  • 不足しているマニフェストフィールドを列挙する Gaps セクション

パッケージ化後、$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json から novel_features を読み取ります。マニフェストの順序を維持してください。このセクションを README の文章、SKILL の文章、ルートのヘルプ、または実行時の記憶から導き出さないでください。それらの情報は要約または手動編集されている可能性があるため、パッケージ化されたマニフェストを公開時の信頼できる情報源として使用します。各エントリーには、コマンド、名前、説明を含めます。配列が空の場合は No novel commands recorded in .printing-press.json. と記載し、不足しているフィールドを Gaps に含めます。このセクションを省略しないでください。

新規公開、再公開、PR の更新、衝突による名前変更を区別できるよう、公開パスの行も含めます:

  • New print — マージ済み CLI がなく、このスラッグに一致する既存の PR もありません。
  • Update existing PR #<N> — この公開でオープン中の PR を更新します。
  • Reprint/replace — この公開前にマージ済みのライブラリ CLI が存在し、選択したパスで置き換えます。これは PREEXISTING_MERGED_COLLISION=true に基づいて判定し、パッケージ化後のツリーに基づいてはいけません。
  • Alongside print — 衝突を避けるため、この公開で API スラッグの名前を変更しました。元のスラッグも含めます。 /printing-press-reprint によって、公開ライブラリに以前のソースがない状態の劣化した再公開が引き継がれた場合は、New print とし、引き継ぎ時にその情報が得られる場合に限り、劣化した再公開である旨を注記します。

必須:PR の本文を作成する前に、ワークスペースの個人情報をすべて除去してください。 ライブラリのリポジトリは公開されています。ライブテスト結果、受け入れデータ、原稿の抜粋に、組織名、チームメンバー名、メールアドレスが含まれていないか確認します。「ワークスペース」「チームメンバー 5 人」「ユーザー 12 人」などの一般的な表現に置き換えてください。チームキー(例:「ESP」)は使用できますが、組織名(例:「Acme Corp」)は使用できません。詳しいポリシーについては、printing-press スキルの references/secret-protection.md を参照してください。

作成した PR 本文は一時的な Markdown ファイルに書き込み、--body-file を使って渡します。PR の新規作成時も更新時も同様にしてください。本文をシェル引数に直接インラインで記述しないでください。長いコードフェンス付きのヘルプ出力、Markdown テーブル、バッククォートは、書式が崩れやすいためです。

PR の説明文テンプレート:

## <api-slug>

<If this is a Replace path, add: "⚠️ **Replaces existing `<api-slug>`** — <reason from user>">

<description from manifest, or "No description available">

**API:** <api_name> | **Category:** <category> | **Press version:** <printing_press_version>
**Spec:** <spec_url or "Not specified">

### Publication Path

<New print | Update existing PR #N | Reprint/replace | Alongside print from <original-api-slug>>

### CLI Shape

\`\`\`bash
$ <cli-name> --help
<help_output from validation>
\`\`\`

### Novel Commands

| Command | Name | Description |
|---------|------|-------------|
| `<command>` | <name> | <description> |

### What This CLI Does

<First 2-3 paragraphs from README.md in the CLI directory, or "README not found">

### Manuscripts



- [<label>](https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/research/<filename>)
- [<label>](https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/research/<filename>)
- … (one bullet for each remaining file in `.manuscripts/<run-id>/research/`)
- [<label>](https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/proofs/<filename>)
- [<label>](https://github.com/mvanhorn/printing-press-library/blob/<HEAD_SHA>/library/<category>/<api-slug>/.manuscripts/<run-id>/proofs/<filename>)
- … (one bullet for each remaining file in `.manuscripts/<run-id>/proofs/`)

### Validation Results

| Check | Result |
|-------|--------|
| Manifest | PASS/FAIL |
| Phase 5 | PASS/FAIL |
| go mod tidy | PASS/FAIL |
| govulncheck (this CLI only, reachable findings) | PASS/FAIL |
| go vet | PASS/FAIL |
| go build | PASS/FAIL |
| --help | PASS/FAIL |
| --version | PASS/FAIL |
| Manuscripts | PRESENT/MISSING |

### Publish Live Gate

<If Step 4.5 ran dogfood: "Full live dogfood reran at publish time and passed. Proof: `<proof path or manuscript link>`">
<If Step 4.5 was skipped: "Skipped with explicit reason: `<SKIP_LIVE_TEST_REASON>`">

### Gaps

<List any missing manifest fields, or omit this section if everything is present>

既存の PR を更新する場合(EXISTING_PR_NUMBER が設定されている場合):

cd "$PUBLISH_REPO_DIR"
PR_BODY_FILE="$(mktemp)"
# Write the constructed PR body Markdown to "$PR_BODY_FILE".
gh pr edit "$EXISTING_PR_NUMBER" \
  --repo mvanhorn/printing-press-library \
  --body-file "$PR_BODY_FILE"
rm -f "$PR_BODY_FILE"

PR の完全な URL を表示します:「Updated PR: <EXISTING_PR_URL>」(省略形ではなく、完全な https:// URL を使います)。

新しい PR を作成する場合:

cd "$PUBLISH_REPO_DIR"

# Read access mode from config
ACCESS=$(jq -r .access "$PUBLISH_CONFIG")
GH_USER=$(jq -r .gh_user "$PUBLISH_CONFIG")

if [ "$ACCESS" = "fork" ]; then
  PR_HEAD_REF="$GH_USER:feat/<api-slug>"
else
  PR_HEAD_REF="feat/<api-slug>"
fi

PR_BODY_FILE="$(mktemp)"
# Write the constructed PR body Markdown to "$PR_BODY_FILE".

gh pr create \
  --repo mvanhorn/printing-press-library \
  --head "$PR_HEAD_REF" \
  --base main \
  --title "feat(<api-slug>): add <api-slug>" \
  --body-file "$PR_BODY_FILE"

rm -f "$PR_BODY_FILE"

PR の完全な URL(例:https://github.com/mvanhorn/printing-press-library/pull/10)を表示します。省略形の org/repo#N 形式は使わないでください。完全な URL は、どのターミナルやコンテキストでもクリックできます。

PR が作成された後

PR が作成されると、公開ライブラリリポジトリのレビューポリシーが適用されます。このポリシーは mvanhorn/printing-press-library AGENTS.md →「Greptile による自動コードレビュー」 で管理されています。正式な内容はそちらを参照してください。cli-printing-press からこのスキルを実行するエージェントは、ライブラリの AGENTS.md を読み込んでいないため、ここで要件を要約します。

Greptile のレビューは段階的に行われます。コミットをプッシュするたびに新しいレビューが実行され、前回のレビューでは見つからなかった指摘が出ることがあります。これは一度きりの確認ではなく、反復して行うものです。PR が安定してグリーンになるまで対応を続け、最初のレビューだけで完了と判断しないでください。

PR を安定してグリーンにする

直近の修正コミットによって実行されたレビューで確認しながら、以下のすべてを満たすまで反復してください。

  • Greptile の指摘をすべて解決する。 0~5 のスコアは信頼度を示すものであり、合格条件ではありません。基準となるのは、すべての指摘を解決することです。すべて解決済みの 4/5 は問題ありませんが、P1 が未解決の 5/5 は問題があります。各 P0/P1/P2 のスレッドについて、修正をプッシュするか、指摘に該当しない具体的な理由を返信してください。「修正しない」ではなく、コードが現状のままで正しい理由、または対応を保留する妥当性を説明してください。また、ポリシー上、現在の HEAD SHA に対する最新スコアが 4/5 以上である必要があります。最終 HEAD もこの基準を満たすようにしてください。
  • すべての CI チェックに合格する。 verify-library-conventions、Govulncheck、および PR 上のその他のワークフローが対象です。

指摘は、重複しない以下の 2 つの場所から確認してください。

  • gh pr view <PR> --repo <owner>/<repo> --comments では、トップレベルの課題に関する会話(Greptile の要約コメント、スコア、CI ボットのコメント)が表示されます。
  • gh api repos/<owner>/<repo>/pulls/<PR>/comments では、差分の該当箇所に紐付いたインラインレビューコメントが表示されます。Greptile は各 P0/P1/P2 の指摘をここに投稿しますが、これらは --comments には含まれません。このコマンドを省略すると、インラインスレッドがすべて未解決のままなのに、エージェントが気付かず「すべての指摘を解決した」と判断してしまいます。

監視はハーネスの役割であり、自分で忙しいループを組む必要はありません。 環境で利用できる PR アクティビティの監視方法を使い、レビューや CI のイベントに応じて対応するか、イベントが通知されない場合は一定間隔で再確認してください。修正をプッシュした後は、再実行されたレビューが届くまで待ってから完了かどうかを判断してください。新しいレビューによって、条件が再び未達になることがあります。

指摘に対応するために registry.json や cli-skills/pp-<api-slug>/SKILL.md を手作業で編集しないでください。 どちらもマージ後に [skip ci] コミットでボットが再生成します。また、ライブラリの Fail on changes to generated artifacts チェックにより、これらに触れる PR は事前に却下されます。

ターミナル状態 — その後、制御を返します

PR が安定してグリーンになったら、このスキルの役割は完了です。それをマージしないでください。また、マージされるのを待ち続けないでください — 公開ライブラリへのマージはメンテナーによる手動レビューであり、このスキルの仕事ではなく、(フォークのコントリビューターにとっては) ユーザーの仕事でもありません。

$PUBLISH_CONFIG から access を読み取り (jq -r .access "$PUBLISH_CONFIG")、次に何をすべきかを判断します:

  • access が push の場合 (プッシュ権限を持つメンテナー/管理者): PR が手動レビューの準備完了であることを示すために awaiting-maintainer ラベルを付けます:
    gh pr edit <PR> --repo mvanhorn/printing-press-library --add-label awaiting-maintainer
    
  • access が fork の場合 (コミュニティコントリビューター): アップストリームの PR をマージしたりラベルを付けたりすることはできません。グリーンになったら、もうやることはありません。

その後、ターミナル状態を報告し、制御を呼び出し元に返します。 このスキルからデフォルトでレトロやフォローアップメニューを提供しないでください — その判断は publish を呼び出した側に属します。printing-press パイプラインは、公開後の独自の末尾処理としてレトロを提供します。--from-polish を付けずに人間が直接呼び出した場合は、ここで終了します。

POLISH_HANDOFF=true の場合、PR がグリーンになった後にソフトな末尾処理としてレトロを提供します。これにより、スタンドアロンの polish -> publish ワークフローを維持しつつ、polish の同一ターン内の AskUserQuestion の回答が公開ライブラリの PR を作成または更新することを防ぎます。

AskUserQuestion で提示します:

"PR を作成しました: <PR_URL>。レトロを実行しますか?このセッションからシステム的なギャップ (ジェネレーターの漏れ、スコアラーのバグ、スキルドキュメントのドリフト) を、Printing Press メンテナー向けの GitHub issue として浮かび上がらせます。レトロを提出するたびに、次の CLI の水準が上がり、今があなたのセッションコンテキストが最も新鮮なタイミングです。"

  1. いいえ、終わりです (デフォルト)
  2. はい、今すぐレトロを実行してください

ユーザーが「はい」を選んだ場合は、/printing-press-retro を呼び出します。

シークレットと PII の保護

PR を作成する前に、パッケージ化された CLI にシークレットが漏れていないことを確認してください。

これはライブラリリポジトリが公開されているため重要です。 PR 内の漏洩した API キーは セキュリティインシデントです — PR が後でクローズされたとしても、誰でもそれを見ることができます。

Printing Press がチェックするもの (決定的)

生成スキル (/printing-press) は、ユーザーが API キーを提供した場合、フェーズ 5.6 で正確な値のスキャンを実行します。publish が実行される頃には、Printing Press 自身のミスはすでに捕捉されているはずです。ただし、ユーザーが生成と公開の間でファイルを編集した可能性があります。

publish がチェックするもの
  1. 必須のバイナリスキャン: cli-printing-press publish package は、ステージングされた CLI と原稿をスキャンして、実際に有効に見えるベンダープレフィックスのトークン (sk-or-v1-*、sk_live_*、ghp_*、ghs_*、xoxb-*、AKIA* など) を探します。vendor-prefix tokens detected で失敗した場合は、そのパッケージを公開不可として扱ってください。報告された file:line の検出事項が削除または墨消しされるまで、コピー、コミット、プッシュ、PR の作成を行わないでください。

  2. ユーザーの正確な API キー値が判明している場合、PR を作成する前にパッケージ化されたツリーをスキャンします。これは /printing-press フェーズ 5.6 の後に追加された編集や原稿を捕捉します:

    if [ -n "$API_KEY_VALUE" ] && [ ${#API_KEY_VALUE} -ge 16 ]; then
      if grep -rF "$API_KEY_VALUE" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" 2>/dev/null; then
        echo "BLOCKING: API key value found in staged publish tree."
        exit 1
      fi
    fi
    
  3. gitleaks または trufflehog がインストールされている場合、ステージングされたディレクトリに対して拡充パスとして実行します:

    if command -v gitleaks >/dev/null 2>&1; then
      gitleaks detect --source "<staging-dir>/library" --no-git --verbose 2>&1
    elif command -v trufflehog >/dev/null 2>&1; then
      trufflehog filesystem "<staging-dir>/library" 2>&1
    fi
    

    これらのツールは、誤検出率の低いベンダー固有のパターン (Steam キー、Stripe キー、GitHub トークン) を使用します。その検出事項は、必須の最低基準を超えて検出範囲を広げます。先に進む前に、検出事項を確認してください。

  4. 常に軽量な構造チェックを実行します:

    • ステージングされたディレクトリに、実際の認証情報を含む .env ファイル、session-state.json、または config.toml が存在しないことを確認する
    • README の例が実際の値ではなく "your-key-here" プレースホルダーを使用していることを確認する
    • 原稿 (含まれている場合) に認証ヘッダーや Cookie 値が含まれていないことを確認する
  5. ステージングされたディレクトリに決して含めないもの:

    • .env ファイル
    • session-state.json
    • 実際の認証情報を含む設定ファイル
    • 認証ヘッダーが除去されていない HAR キャプチャ

必須のバイナリスキャンまたは正確な値のスキャンで問題が見つかった場合は、停止してください。外部ツールまたは軽量な構造チェックの検出事項については、ユーザーに警告し、続行するかどうかを尋ねてください。これらの非必須の検出事項については、ユーザーが最終判断を下します。

PII パターンスキャン(必須)

上記のシークレットスキャンに加えて、printing-press スキルの references/secret-protection.md にある PII パターンスキャン セクションで定義された手順を実行します。 ここには Tier 1 のパターンセットとスイープループが記載されています。これにより、ライブ環境でのドッグフーディング中に記録されたものの、説明文のガイダンスでは見落とされていた PII(メールアドレス、実際の参加者名、アカウント識別子など)が、公開ライブラリリポジトリに公開される前に検出されます。

スキャンには 2 つのティアがあります。

  • Tier 1(自動で通知なく墨消し): ベンダープレフィックスに固定されたベアラートークン (Bearer cal_live_*、Bearer sk_live_*、Bearer ghp_*、xoxp-* など)。 誤検出率はほぼゼロです。
  • Tier 2(警告し、まとめてユーザーに確認): 一般的なメールアドレス、一般的なベアラートークン、 大文字で始まる姓名のパターン。許可リストにより、仕様由来の API 用語(「Event Types」、「Booking Links」)は自動的に除外されます。

ステージングディレクトリのスキャン前コピーは <staging>.pre-pii-scrub/ に保存されるため、誤った墨消しから復元できます。

このスキャンが導入される前に、PII の漏えいが 2 件、公開ライブラリに公開されました。 このスキャンは、説明文のガイダンスだけでは実現できなかった機械的な防御層です。

エラー処理

  • gh が認証されていない: Step 1 で検出し、gh auth login を実行するようユーザーに伝えます
  • CLI が見つからない: Step 2 で利用可能な CLI を表示し、ユーザーに選択してもらいます
  • 検証に失敗: Step 4 で各チェックの結果を表示し、停止します
  • リポジトリに接続できない: Step 5 で明確に報告します
  • フォークの作成に失敗: ユーザーがすでに別の名前のフォークを持っている場合や、組織がフォークを制限している場合、gh repo fork が失敗することがあります。エラーを報告し、GitHub の Web UI から手動でフォークするよう提案します。
  • 衝突チェックに失敗: gh pr list または ls コマンドが失敗した場合(ネットワーク、認証)、警告しますが処理はブロックせず、衝突がないものとして続行します
  • 名前の変更に失敗: publish rename --json のエラーを表示します。別の修飾子で再試行するか、終了するかを提案します。公開リポジトリが不完全な状態の場合は、再試行する前に git checkout -- . && git clean -fd でリセットします
  • ブランチの競合(既存の PR がない場合): Step 8 でユーザーに上書きするかタイムスタンプを付けるか尋ねます
  • プッシュに失敗: フォークを使用するユーザーの場合、upstream ではなく自身のフォーク(origin)にプッシュしていることを確認します。エラーを報告し、gh auth status と git remote -v を確認するよう提案します
  • リポジトリ間の PR 作成に失敗: gh pr create --head user:branch が "head not found" で失敗した場合、ブランチはフォークにプッシュされていません。git ls-remote origin feat/<api-slug> で確認します

コメント