printing-press

star 3.5k

Generate a ship-ready CLI for an API with a lean research -> generate -> build -> shipcheck loop.

mvanhorn By mvanhorn schedule Updated 6/9/2026

name: printing-press description: Generate a ship-ready CLI for an API with a lean research -> generate -> build -> shipcheck loop. version: 2.0.0 min-binary-version: "4.0.0" allowed-tools: - Bash - Read - Write - Edit - Glob - Grep - WebFetch - WebSearch - AskUserQuestion - Agent

/printing-press

Generate the best useful CLI for an API without burning an hour on phase theater.

/printing-press Notion
/printing-press Discord codex
/printing-press --spec ./openapi.yaml
/printing-press --har ./capture.har --name MyAPI
/printing-press https://postman.com/explore
/printing-press https://postman.com

What Changed In v2

The old skill inflated the path to ship:

  • too many mandatory research documents before code existed
  • too many separate late-stage validation phases after code existed
  • too many chances to discover obvious failures late

This version uses one lean loop:

  1. Resolve the spec and write one research brief
  2. Generate
  3. Build the highest-value gaps
  4. Run one shipcheck block
  5. Optionally run live API smoke tests

Artifacts are still written, but only the ones that materially help the next step.

Modes

Default

Normal mode. Claude does research, generation orchestration, implementation, and verification.

Codex Mode

If the arguments include codex or --codex, offload pure code-writing tasks to Codex CLI.

Use Codex for:

  • writing store/data-layer code
  • writing workflow commands
  • fixing dead flags / dead code / path issues
  • README cookbook edits

Keep on Claude:

  • research and product positioning
  • choosing which gaps matter
  • verification results and ship decisions

If Codex fails 3 times in a row, stop delegating and finish locally.

Polish Mode (Standalone Skill)

For second-pass improvements to an existing CLI, use the standalone polish skill:

/printing-press-polish redfin

See the printing-press-polish skill for details. It runs diagnostics, fixes verify failures, removes dead code, cleans up descriptions and README, and offers to publish.

Rules

  • Do not ship a CLI that hasn't been behaviorally tested against real targets. go build and verify pass-rate are structural signals, not correctness signals. Phase 5's mechanical test matrix runs every subcommand + --json + error paths; if that matrix was not executed, the CLI is not shippable. Quick Check is the floor; Full Dogfood is required when the user asks for thoroughness.
  • Bugs found during dogfood are fix-before-ship, not "file for v0.2". If a 1-3 file edit resolves it, do it now. ship-with-gaps is deprecated as a default verdict (see Phase 4). Context is freshest in-session; a v0.2 backlog that may never be revisited ships known-broken CLIs.
  • Features approved in Phase 1.5 are shipping scope. Do not downgrade a shipping-scope feature to a stub mid-build. If implementation becomes infeasible, return to Phase 1.5 with a revised manifest and get explicit re-approval.
  • Do not quote human-time estimates for sub-tasks ("15-30 min", "1 hour", "quick fix") in AskUserQuestion options, phase descriptions, or reference docs. The agent does the work, not the user; agent-fabricated estimates are notoriously bad and train users to distrust the prompt. Describe scope instead (lines of code, files touched, relative size). The carve-outs are wall-clock estimates for genuinely time-bound things: the whole-CLI run (set the user's expectation up front — most CLIs take 30+ minutes), tool installs (go install takes ~10 seconds), and printing-press subcommands that do network-bound work (crowd-sniff scans npm + GitHub, ~5-10 minutes). Anything bounded by agent reasoning time is not time-bound — describe scope.
  • Use raw captures for contract research. When reading official docs, auth/error/rate-limit pages, endpoint references, OpenAPI/Postman links, or source pages whose exact identifiers affect the generated CLI, read references/fetch-docs.md and use its fetch-docs.sh helper. Reserve WebFetch for quick TL;DR reads where losing field-level details is acceptable.
  • Optimize for time-to-ship, not time-to-document.
  • Reuse prior research whenever it is already good enough.
  • Do not split one idea across multiple mandatory artifacts.
  • Durable files produced by this skill go under $PRESS_RUNSTATE/ (working state) or $PRESS_MANUSCRIPTS/ (archived). Short-lived command captures may use /tmp/printing-press/ and must be removed after use.
  • Do not create a separate narrative phase for dogfood, dead-code audit, runtime verification, and final score. Treat them as one shipcheck block.
  • Run cheap, high-signal checks early.
  • Fix blockers and high-leverage failures first.
  • Reuse the same spec path across generate, dogfood, verify, and scorecard.
  • YAML, JSON, local paths, and URLs are all valid spec inputs for the verification tools.
  • Maximum 2 verification fix loops unless the user explicitly asks for more.

Secret & PII Protection (Cardinal Rules)

These rules are non-negotiable. They apply at ALL times during a run.

API key values, token values, passwords, and session cookies must NEVER appear in any artifact: source code, manuscripts, proofs, READMEs, HARs, or anything committed to git. Env var names (e.g., STEAM_API_KEY) and placeholders (e.g., "your-key-here") are safe.

During Phase 5.6 (archiving) and before publishing, read and apply references/secret-protection.md for:

  • Exact-value scanning and auto-redaction of artifacts
  • HAR auth stripping (headers, query strings, cookies)
  • API key handling rules during the run
  • Session state cleanup ordering

Preflight

This section MUST run before any user-facing prompt — including the Orientation and Briefing flow below. A missing binary or available upgrade is information the user needs before they commit to an API. Do not invoke AskUserQuestion, print the orientation prose, or otherwise engage the user until preflight has completed and any signals from references/setup-checks.md have been handled.

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

_press_repo=false
if [ -d "$_scope_dir/cmd/cli-printing-press" ] && [ -f "$_scope_dir/go.mod" ]; then
  _press_repo=true
fi

_resolve_press_bin() {
  if command -v cli-printing-press >/dev/null 2>&1; then
    command -v cli-printing-press
    return 0
  fi
  if command -v printing-press >/dev/null 2>&1 && printing-press version --json >/dev/null 2>&1; then
    command -v printing-press
    return 0
  fi
  return 1
}

# Strict-older semver compare on the first three components. Pre-release
# suffixes collapse to their GA counterpart (acceptable: we ship no pre-release
# tags).
_semver_lt() {
  awk -v a="$1" -v b="$2" '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
  }'
}

_source_press_version() {
  sed -nE 's/^var[[:space:]]+Version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' \
    "$_scope_dir/internal/version/version.go" 2>/dev/null | head -n 1
}

_rebuild_local_press_bin_if_stale() {
  if [ "$_press_repo" != "true" ] || [ ! -x "$_scope_dir/cli-printing-press" ]; then
    return 0
  fi

  _local_v="$("$_scope_dir/cli-printing-press" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"
  _source_v="$(_source_press_version)"
  if [ -z "$_local_v" ] || [ -z "$_source_v" ] || ! _semver_lt "$_local_v" "$_source_v"; then
    return 0
  fi

  echo ""
  echo "[local-binary-stale] local build v$_local_v is older than source v$_source_v"
  if ! command -v go >/dev/null 2>&1; then
    echo "[setup-error] local cli-printing-press binary is stale and Go is not on PATH, so it cannot be rebuilt."
    return 1 2>/dev/null || exit 1
  fi

  if (cd "$_scope_dir" && go build -o ./cli-printing-press ./cmd/cli-printing-press); then
    echo "[local-binary-rebuilt] rebuilt $_scope_dir/cli-printing-press"
    echo ""
  else
    echo "[setup-error] local cli-printing-press binary is stale and rebuild failed."
    return 1 2>/dev/null || exit 1
  fi
}

# Prefer local build when running from inside the printing-press repo.
# Lefthook may keep ./cli-printing-press current, but hooks can be absent or
# disabled. Compare against the checked-out source version before trusting it.
_rebuild_local_press_bin_if_stale || { return 1 2>/dev/null || exit 1; }
if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
  export PATH="$_scope_dir:$PATH"
  echo "Using local build: $_scope_dir/cli-printing-press"
elif ! _resolve_press_bin >/dev/null; then
  # Augment PATH if the binary is in ~/go/bin but not on the user's interactive PATH.
  if [ -x "$HOME/go/bin/cli-printing-press" ]; then
    export PATH="$HOME/go/bin:$PATH"
  elif [ -x "$HOME/go/bin/printing-press" ] && "$HOME/go/bin/printing-press" version --json >/dev/null 2>&1; then
    export PATH="$HOME/go/bin:$PATH"
  else
    # Refuse: the cli-printing-press binary is required and we will not auto-install
    # it. The README's install flow is the source of truth;
    # silent auto-install hides failure modes (network, wrong GOPATH) inside an
    # opaque skill invocation.
    echo ""
    echo "[setup-error] cli-printing-press binary not found."
    echo ""
    if command -v go >/dev/null 2>&1; then
      echo "Install it in your terminal:"
      echo "  go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest"
    else
      echo "Go 1.26.4 or newer is also not installed. Install Go from https://go.dev/dl/, then:"
      echo "  go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest"
    fi
    echo ""
    echo "Verify with: cli-printing-press --version"
    echo "Then re-run /printing-press."
    return 1 2>/dev/null || exit 1
  fi
fi

# Verify the Go toolchain is on PATH. Generation runs Go-based quality gates
# (go mod tidy, go vet, etc.) after writing thousands of lines of scaffolding,
# so a missing `go` only surfaces 5+ minutes in. Fail-fast costs one command -v
# call when Go is present and converts a late, opaque failure into a 30-second
# actionable abort.
if ! command -v go >/dev/null 2>&1; then
  echo ""
  echo "[setup-error] Go toolchain not found."
  echo ""
  echo "The Printing Press generator runs Go-based quality gates after generation."
  echo "Install Go 1.26.4 or newer from https://go.dev/dl/, then verify with:"
  echo "  go version"
  echo "Then re-run /printing-press."
  echo ""
  return 1 2>/dev/null || exit 1
fi

# Verify the installed Go tree can compile and run common standard library
# imports. A truncated Go extraction can leave the binary working enough for
# `go version` while missing packages under $GOROOT/src, which otherwise fails
# deep into generation during later Go quality gates.
_go_smoke_root="${PRINTING_PRESS_GO_SMOKE_DIR:-$HOME/.printing-press-smoke}"
if ! mkdir -p "$_go_smoke_root"; then
  echo ""
  echo "[setup-error] Unable to create Go smoke-test workspace at $_go_smoke_root."
  echo "Set PRINTING_PRESS_GO_SMOKE_DIR to a writable non-temp directory and retry."
  echo ""
  return 1 2>/dev/null || exit 1
fi
_go_smoke_dir="$(mktemp -d "$_go_smoke_root/stdlib.XXXXXX" 2>/dev/null || true)"
if [ -z "$_go_smoke_dir" ]; then
  echo ""
  echo "[setup-error] Unable to create Go smoke-test workspace under $_go_smoke_root."
  echo "Set PRINTING_PRESS_GO_SMOKE_DIR to a writable non-temp directory and retry."
  echo ""
  return 1 2>/dev/null || exit 1
fi
cat > "$_go_smoke_dir/go.mod" <<'__PP_GO_SMOKE_MOD__'
module pp-go-stdlib-smoke

go 1.20
__PP_GO_SMOKE_MOD__
cat > "$_go_smoke_dir/main.go" <<'__PP_GO_SMOKE_MAIN__'
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "regexp"
)

func main() {
    ctx := context.Background()
    payload, err := json.Marshal(map[string]string{"status": "ok"})
    if err != nil {
        panic(err)
    }
    if !regexp.MustCompile(`ok`).Match(payload) {
        panic("regexp mismatch")
    }
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
    if err != nil {
        panic(err)
    }
    _, _ = fmt.Fprint(io.Discard, req.Method)
}
__PP_GO_SMOKE_MAIN__
if ! (cd "$_go_smoke_dir" && GOFLAGS= GOWORK=off go run . >/dev/null 2>"$_go_smoke_dir/error.log"); then
  _go_smoke_output="$(sed -n '1,12p' "$_go_smoke_dir/error.log" 2>/dev/null || true)"
  rm -rf "$_go_smoke_dir"
  echo ""
  echo "[setup-error] Go std library is incomplete (truncated or corrupted install)."
  echo "Reinstall Go from https://go.dev/dl/ and verify with the smoke test before retrying."
  if [ -n "$_go_smoke_output" ]; then
    echo ""
    echo "Go smoke test output:"
    printf '%s\n' "$_go_smoke_output"
  fi
  echo ""
  return 1 2>/dev/null || exit 1
fi
rm -rf "$_go_smoke_dir"

# 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. When a global is
# installed at a stale version, that silently shadows the local build the
# preflight just chose. Handing the agent an absolute path eliminates the
# shadow.
if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
  PRINTING_PRESS_BIN="$_scope_dir/cli-printing-press"
else
  PRINTING_PRESS_BIN="$(_resolve_press_bin 2>/dev/null || true)"
fi
echo "PRINTING_PRESS_BIN=$PRINTING_PRESS_BIN"
echo "PRESS_REPO_MODE=$_press_repo"

# Shadow detector (advisory). When a local build is in use, surface any
# differing global so the user can see at a glance that the two binaries
# disagree. Detect-only: the absolute path emitted above is the one the
# agent will actually invoke; this warning does not change selection.
if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
  _global_bin=""
  for _candidate in "$HOME/go/bin/cli-printing-press" "/usr/local/bin/cli-printing-press" "/opt/homebrew/bin/cli-printing-press" "$HOME/go/bin/printing-press" "/usr/local/bin/printing-press" "/opt/homebrew/bin/printing-press"; do
    if [ -x "$_candidate" ] && [ "$_candidate" != "$_scope_dir/cli-printing-press" ] && "$_candidate" version --json >/dev/null 2>&1; then
      _global_bin="$_candidate"
      break
    fi
  done
  if [ -n "$_global_bin" ]; then
    _local_v="$("$_scope_dir/cli-printing-press" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"
    _global_v="$("$_global_bin" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"
    if [ -n "$_local_v" ] && [ -n "$_global_v" ] && [ "$_local_v" != "$_global_v" ]; then
      echo ""
      echo "[binary-shadow] local build v$_local_v differs from global v$_global_v at $_global_bin"
      echo "PRESS_BIN_LOCAL_VERSION=$_local_v"
      echo "PRESS_BIN_GLOBAL_VERSION=$_global_v"
      echo "PRESS_BIN_GLOBAL_PATH=$_global_bin"
      echo ""
    fi
  fi
fi

PRESS_BASE="$(basename "$_scope_dir" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g; s/^-+//; s/-+$//')"
if [ -z "$PRESS_BASE" ]; then
  PRESS_BASE="workspace"
fi

PRESS_SCOPE="$PRESS_BASE-$(printf '%s' "$_scope_dir" | shasum -a 256 | cut -c1-8)"
PRESS_HOME="${PRINTING_PRESS_HOME:-$HOME/printing-press}"
PRESS_RUNSTATE="$PRESS_HOME/.runstate/$PRESS_SCOPE"
PRESS_LIBRARY="$PRESS_HOME/library"
PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts"
PRESS_CURRENT="$PRESS_RUNSTATE/current"

mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT"

# --- Latest-version advisory (fail-open) ---
# Repo checkouts track origin/main because their skills and local binary come
# from the checkout. Standalone installs track the latest released Go module.
PRESS_VERCHECK_FILE="$PRESS_HOME/.version-check"
PRESS_VERCHECK_TTL=86400
_now_ts=$(date +%s)

_should_check=true
if [ -f "$PRESS_VERCHECK_FILE" ] && [ -z "$PRESS_VERCHECK_FORCE" ]; then
  _last_ts=$(awk -F= '/^last_check=/{print $2}' "$PRESS_VERCHECK_FILE" 2>/dev/null)
  if [ -n "$_last_ts" ] && [ "$((_now_ts - _last_ts))" -lt "$PRESS_VERCHECK_TTL" ]; then
    _should_check=false
  fi
fi

if [ "$_press_repo" = "true" ]; then
  # Repo mode checks origin/main every run because the checkout and local build
  # move quickly; skipped_repo_main suppresses repeated prompts for one SHA.
  if git -C "$_scope_dir" remote get-url origin >/dev/null 2>&1 &&
     git -C "$_scope_dir" fetch --quiet origin main >/dev/null 2>&1; then
    _head_rev=$(git -C "$_scope_dir" rev-parse HEAD 2>/dev/null || true)
    _main_rev=$(git -C "$_scope_dir" rev-parse origin/main 2>/dev/null || true)
    _skipped_repo_main=""
    if [ -f "$PRESS_VERCHECK_FILE" ] && [ -z "$PRESS_VERCHECK_FORCE" ]; then
      _skipped_repo_main=$(awk -F= '/^skipped_repo_main=/{value=$2} END{print value}' "$PRESS_VERCHECK_FILE" 2>/dev/null)
    fi
    if [ -n "$_head_rev" ] && [ -n "$_main_rev" ] &&
       [ "$_head_rev" != "$_main_rev" ] &&
       [ "$_skipped_repo_main" != "$_main_rev" ] &&
       git -C "$_scope_dir" merge-base --is-ancestor "$_head_rev" "$_main_rev" 2>/dev/null; then
      echo ""
      echo "[repo-upgrade-available] origin/main has newer Printing Press changes"
      echo "PRESS_REPO_DIR=$_scope_dir"
      echo "PRESS_REPO_HEAD=$_head_rev"
      echo "PRESS_REPO_MAIN=$_main_rev"
      echo ""
    fi

    printf "last_check=%s\nlatest=%s\nmode=repo\nskipped_repo_main=%s\n" "$_now_ts" "${_main_rev:-unknown}" "$_skipped_repo_main" > "$PRESS_VERCHECK_FILE" 2>/dev/null || true
  fi
elif [ "$_should_check" = "true" ] && command -v go >/dev/null 2>&1; then
  _installed=$("$PRINTING_PRESS_BIN" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
  _latest=""

  if [ -n "$_installed" ]; then
    _latest=$(go list -m -json github.com/mvanhorn/cli-printing-press/v4@latest 2>/dev/null | awk '
      /"Version":/ {
        version=$2
        gsub(/[",]/, "", version)
        sub(/^v/, "", version)
        print version
        exit
      }
    ')
  fi

  # Currency floor: the lowest release still considered safe to generate with,
  # published out-of-band so maintainers can raise it without a binary or skill
  # release. Fetched here (throttled by the TTL above) and cached for the
  # always-run enforcement gate below.
  _min_supported=""
  _min_reason=""
  if command -v curl >/dev/null 2>&1; then
    _floor_doc=$(curl -fsSL --max-time 5 \
      https://raw.githubusercontent.com/mvanhorn/cli-printing-press/main/supported-versions.txt 2>/dev/null || true)
    if [ -n "$_floor_doc" ]; then
      _min_supported=$(printf '%s\n' "$_floor_doc" | awk -F= '/^min_supported=/{print $2; exit}')
      _min_reason=$(printf '%s\n' "$_floor_doc" | sed -nE 's/^reason=//p' | head -n 1)
    fi
  fi

  if [ -n "$_installed" ] && [ -n "$_latest" ] && _semver_lt "$_installed" "$_latest"; then
    # Marker for the skill prose below to detect and offer an interactive upgrade.
    # The skill reads PRESS_UPGRADE_AVAILABLE / PRESS_UPGRADE_INSTALLED from this output.
    echo ""
    echo "[upgrade-available] printing-press v$_latest is available (you have v$_installed)"
    echo "PRESS_UPGRADE_AVAILABLE=$_latest"
    echo "PRESS_UPGRADE_INSTALLED=$_installed"
    echo ""
  fi

  printf "last_check=%s\nlatest=%s\nmode=standalone\nmin_supported=%s\nreason=%s\n" \
    "$_now_ts" "${_latest:-$_installed}" "$_min_supported" "$_min_reason" > "$PRESS_VERCHECK_FILE" 2>/dev/null || true
fi

# --- Currency-floor enforcement (standalone, every run, fail-open) ---
# The floor *fetch* above is throttled to once per TTL, but enforcement must run
# every invocation: a fresh cache must never let a stale binary keep generating
# CLIs with since-fixed bugs. Compare the always-fresh installed version against
# the cached floor; the network is never touched here. Only enforce a floor that
# is itself <= latest, so a typo'd or tampered floor above the newest release
# cannot brick every install.
if [ "$_press_repo" != "true" ] && [ -f "$PRESS_VERCHECK_FILE" ]; then
  _floor_min=$(awk -F= '/^min_supported=/{print $2; exit}' "$PRESS_VERCHECK_FILE" 2>/dev/null)
  _floor_latest=$(awk -F= '/^latest=/{print $2; exit}' "$PRESS_VERCHECK_FILE" 2>/dev/null)
  _floor_reason=$(sed -nE 's/^reason=//p' "$PRESS_VERCHECK_FILE" 2>/dev/null | head -n 1)
  _floor_installed=$("$PRINTING_PRESS_BIN" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
  if [ -n "$_floor_min" ] && [ -n "$_floor_installed" ] && [ -n "$_floor_latest" ] &&
     _semver_lt "$_floor_installed" "$_floor_min" &&
     ! _semver_lt "$_floor_latest" "$_floor_min"; then
    echo ""
    echo "[upgrade-required] printing-press v$_floor_min is the minimum supported version (you have v$_floor_installed)"
    echo "PRESS_REQUIRED_MIN=$_floor_min"
    echo "PRESS_REQUIRED_INSTALLED=$_floor_installed"
    echo "PRESS_REQUIRED_REASON=$_floor_reason"
    echo ""
  fi
fi

# --- Browser-sniff backend advisory (fail-open, every-run) ---
# browser-use and agent-browser are the preferred Phase 1.7 browser-sniff
# backends. They are not hard requirements — vendor-spec, --spec, and --har
# runs never invoke them — but when discovery does need them, mid-flight
# install prompts are disruptive. Emit a marker every run so setup-checks.md
# can strongly offer install. No decline caching: a run that didn't need them
# yesterday may need them today, and the prompt cost is small.
_browser_use_missing=true
_agent_browser_missing=true
# Use `command -v` only. Do NOT use `uvx browser-use --help` as a fallback
# probe: when uvx exists but browser-use doesn't, that command silently
# downloads and caches the package, which would be an unconsented install.
# Downstream capture commands also invoke `browser-use` directly (not via
# uvx), so a uvx-cache-only state would lie to the detection.
if command -v browser-use >/dev/null 2>&1; then
  _browser_use_missing=false
fi
if command -v agent-browser >/dev/null 2>&1; then
  _agent_browser_missing=false
fi

if [ "$_browser_use_missing" = "true" ] || [ "$_agent_browser_missing" = "true" ]; then
  echo ""
  echo "[browser-tools-missing] one or more browser-sniff backends not installed"
  echo "PRESS_BROWSER_USE_MISSING=$_browser_use_missing"
  echo "PRESS_AGENT_BROWSER_MISSING=$_agent_browser_missing"
  echo ""
fi

# --- Codex mode detection (must run as part of setup, not a separate step) ---
# Codex mode: opt-in only. User must pass "codex" or "--codex" to enable.
if echo "$ARGUMENTS" | grep -qiE '(^| )(--?codex|codex)( |$)'; then
  CODEX_MODE=true
else
  CODEX_MODE=false
fi

# Environment guard: don't delegate if already inside a Codex sandbox
if [ "$CODEX_MODE" = "true" ]; then
  if [ -n "$CODEX_SANDBOX" ] || [ -n "$CODEX_SESSION_ID" ]; then
    CODEX_MODE=false
  fi
fi

# Health check: verify codex binary exists
if [ "$CODEX_MODE" = "true" ]; then
  if command -v codex >/dev/null 2>&1; then
    # Model and reasoning effort inherit from ~/.codex/config.toml. Do not pin -m / -c here.
    CODEX_MODEL=$(grep -E '^model[[:space:]]*=' ~/.codex/config.toml 2>/dev/null | head -1 | sed -E 's/^model[[:space:]]*=[[:space:]]*"?([^"]+)"?.*$/\1/')
    [ -z "$CODEX_MODEL" ] && CODEX_MODEL="codex default"
    echo "Codex mode enabled (model: $CODEX_MODEL). Code-writing tasks will be delegated to Codex."
  else
    echo "Codex CLI not found - running in standard mode."
    CODEX_MODE=false
  fi
fi

# Circuit breaker state
CODEX_CONSECUTIVE_FAILURES=0

MANDATORY: Read and apply references/setup-checks.md immediately after the setup contract bash block runs, before any other action. It handles the contract output signals: [setup-error] (refuse to run, surface the install instructions), optional [local-binary-stale] / [local-binary-rebuilt] repo-mode rebuild markers, [repo-upgrade-available] (interactive AskUserQuestion prompt + optional repo pull), PRESS_REPO_MODE=<true|false> plus the targeted global open-agent-skills freshness check, the min-binary-version compatibility check (hard stop if binary is too old), [upgrade-required] (hard gate below the published currency floor — interactive upgrade-or-abort, no skip), [upgrade-available] (interactive AskUserQuestion prompt + optional standalone binary upgrade), [browser-tools-missing] (interactive AskUserQuestion prompt + optional install of browser-use and/or agent-browser), and the PRINTING_PRESS_BIN=<abs-path> marker plus optional [binary-shadow] warning (capture the path; use it for every subsequent generator invocation). Skipping the reference will cause the skill to proceed with a missing or out-of-date binary, run with stale global skill text when the session is managed by open-agent-skills, hit a mid-flight install prompt if browser-sniff is later needed, or invoke the wrong binary because a stale global or the public catalog installer on PATH shadowed the local build. Do not skip.

Absolute-path rule. The preflight contract always emits PRINTING_PRESS_BIN=<absolute path> to stdout. Capture this value and substitute it (the resolved absolute path, not the literal $PRINTING_PRESS_BIN token) for every subsequent cli-printing-press ... invocation in this skill, references, and any sub-skill you delegate to. The export PATH=... line inside the contract only affects the single Bash tool call it runs in; later Bash tool calls open fresh shells and resolve bare cli-printing-press against the user's default PATH, where a stale globally-installed binary ($HOME/go/bin/cli-printing-press, Homebrew copy, etc.) will silently shadow the local build the preflight just chose. Bash code examples below are written cli-printing-press generate ... for readability — replace cli-printing-press with the captured absolute path each time you actually run one.

Only after preflight completes successfully (no [setup-error]; no [upgrade-required] left unresolved — the user either upgraded or the run was aborted; no global skill update that requires restart; any [repo-upgrade-available], [upgrade-available], or [browser-tools-missing] was offered to the user; PRINTING_PRESS_BIN is captured) should you proceed to the Orientation & Briefing section below.

Orientation & Briefing

After preflight has completed, check whether the user provided arguments. Handle two cases:

No Arguments: Orientation

If the user typed /printing-press with no arguments (no API name, no --spec, no --har, no URL), print an orientation and ask what they'd like to build:

The Printing Press generates a fully functional CLI for any API. You give it an API name, a spec file, or a URL. It researches the landscape, catalogs every feature that exists in any competing tool, invents novel features of its own, then generates a Go CLI that matches and beats everything out there — with offline search, agent-native output, and a local SQLite data layer.

By the end, you'll have a working CLI in $PRESS_LIBRARY/ that you can use for yourself, ship on your own, or apply to add to the printing-press library.

The process takes 30-60 minutes depending on API complexity. Simple APIs with official specs (Stripe, GitHub) are faster. Undocumented APIs that need discovery (ESPN, Domino's) take longer.

Print these example invocations as plain text BEFORE the AskUserQuestion call (so they appear as context above the question, not as competing menu options):

/printing-press Notion
/printing-press Discord codex
/printing-press --spec ./openapi.yaml
/printing-press --har ./capture.har --name MyAPI
/printing-press https://postman.com

Then ask via AskUserQuestion:

  • question: "What API would you like to build a CLI for?"
  • header: "API target"
  • multiSelect: false
  • options:
    1. label: "Type it (recommended)"description: "Provide an API name, URL, spec path, or HAR file via the 'Other' option below."
    2. label: "Browse existing CLIs first"description: "Visit the public library to see what's already been printed before deciding what to build."

Do not add additional options — no "Show me popular options", no pre-populated buttons for Notion / Stripe / GitHub / Linear / Discord. The example invocations above already cover the common shapes, and most popular APIs are already in the public library (offering to re-print them is noise). The two options above plus the automatic "Other" field is the entire interface.

If the user picks "Type it (recommended)", they will provide their answer via the auto "Other" field. Set their input as the argument and proceed to the briefing below.

If the user picks "Browse existing CLIs first", print the public library URL prominently and try to open it in the browser, then end the skill so the user can browse before deciding:

echo ""
echo "Public library: https://github.com/mvanhorn/printing-press-library"
echo "(If you have the Printing Press Library plugin, you can also run /ppl in Claude Code.)"
echo ""
command -v open >/dev/null 2>&1 && open https://github.com/mvanhorn/printing-press-library

After printing, end the skill cleanly. Do not proceed to briefing or research — the user is exploring, not building yet. They can re-invoke /printing-press <api> once they've decided.

With Arguments: Briefing

When the user provided an argument (API name, --spec, --har, or URL), print a brief process overview. This sets expectations and collects any upfront context. (Preflight has already run at this point.)

Print as prose, matching the style of the example below:

Very well. Setting the type for <API>.

Here is how this will proceed:

  1. I shall research <API> across the internet: official docs, community wrappers, competing CLIs, MCP servers, and npm/PyPI packages
  2. I shall catalog every feature that exists in any tool, then devise novel features of my own that no existing tool offers
  3. I shall present what I found and what I invented — you will have a chance to add your own ideas or adjust the plan before I build
  4. I shall generate a Go CLI, build every feature from the plan, then verify quality through dogfood, runtime verification, and scoring

What you will have at the end: A fully functional CLI at $PRESS_LIBRARY/<api> that you can use yourself, ship on your own, or apply to add to the printing-press library.

Time: 30-60 minutes depending on API complexity.

Things that help if you have them:

  • An API key (for live smoke testing at the end)
  • A logged-in browser session (for discovering authenticated endpoints)
  • A spec file or HAR capture (skips discovery)

If the user provided --spec, adapt: "You have provided a spec, so I shall skip discovery and proceed directly to analysis and generation. Should be faster."

If the user provided --har, adapt: "You have provided a HAR capture, so I shall generate a spec from your traffic and skip browser browser-sniffing."

Then ask via AskUserQuestion:

  • question: "Anything you want me to know before I begin? A vision for what this CLI should do, specific features you care about, or auth context I should have?"
  • header: "Briefing"
  • multiSelect: false
  • options:
    1. label: "Let's go (recommended)"description: "Start research now. I'll ask about API keys, browser auth, or other context when I need them."
    2. label: "I have context to share"description: "Tell me your vision, specific features, or auth context (API key, logged-in browser session) before research starts."

Do not add additional options — auth is already handled by Phase 0.5 (API Key Gate) and Phase 1.6 (Pre-Browser-Sniff Auth Intelligence) downstream. A user who wants to volunteer auth context can do so via option 2's free-text response. The two options above plus the automatic "Other" field is the entire interface.

If the user picks "Let's go (recommended)", proceed to the Multi-Source Priority Gate (or, for single-source runs, directly to Phase 0).

If the user picks "I have context to share", capture their free-text response as USER_BRIEFING_CONTEXT. The response may include:

  • Vision / specific features — captured as-is. This context will be:
    • Added to the Phase 1 Research Brief under a ## User Vision section
    • Used as a 4th self-brainstorm question in Phase 1.5c.5: "Based on the user's stated vision, what features directly serve their stated goals that the absorbed features don't cover?"
    • Referenced at the Phase Gate 1.5 absorb gate: "You mentioned [summary] at the start. Want to add more, or does the manifest already cover it?"
  • Auth context — if the user mentions an API key, env var, or logged-in browser session, set the corresponding AUTH_CONTEXT fields so the API Key Gate (Phase 0.5) and Pre-Browser-Sniff Auth Intelligence (Phase 1.6) do not re-ask.

Multi-Source Priority Gate

After the briefing question resolves, inspect the user's original argument AND any USER_BRIEFING_CONTEXT they provided. If together they name two or more distinct services, sites, or APIs (e.g., "Google Flights and Kayak", "Notion + Linear combo CLI", "flightgoat: Google Flights, Kayak.com/direct, and FlightAware"), this is a combo CLI and priority ordering MUST be confirmed before Phase 1 research.

Why this gate exists: Phase 1 research defaults to the first resolvable spec as the primary source. When the user listed services in a specific order, that order is their intent — but the generator's spec-first bias will silently invert it (picking a well-documented paid API over a free reverse-engineered one the user actually wanted as the headline feature). This has caused real user-visible failures where the CLI shipped with the wrong primary and required a paid API key for what the user intended as the free primary command.

Parse the order from the prose. Use the user's wording verbatim. Commas, "then", "and", explicit "primary/secondary", or numbered lists all signal ordering. If the user wrote "Google Flights, Kayak, FlightAware" — that is the order. Do not reorder by spec availability, tier, or ease of generation.

Confirm via AskUserQuestion:

"You mentioned , , and . I'll treat as the primary — it gets the headline commands, the top of the README, and the first-run experience. Is that the right order?"

Options:

  1. Yes, that order is correct — Proceed with SOURCE_PRIORITY=[A, B, C] captured to run state.
  2. Different order — User provides the correct ordering; capture it.
  3. They're peers, no primary — Rare; capture as equal weighting but warn the user that one will still lead the README.

Write the confirmed ordering to $API_RUN_DIR/source-priority.json:

{
  "sources": ["google-flights", "kayak-direct", "flightaware"],
  "confirmed_at": "<ISO timestamp>",
  "raw_user_phrasing": "<verbatim text that established the order>"
}

Phase 1 MUST consult this file. When selecting a spec source, the primary source wins even if it has no spec and a later source has a clean OpenAPI. When the primary has no official spec, flag that openly in the brief under ## Source Priority (see template below) and route to the browser-sniff/docs path for the primary — do not promote a secondary source just because its spec is cleaner.

Economics check. If the confirmed primary source is free (no API key required) AND the generator's default path would make the primary CLI commands require a paid key (because the auth applies broadly or because a paid secondary source is bleeding into the primary path), surface the tradeoff explicitly before generating:

"The primary source () is free, but the default path would require a for the headline commands because . Options: (1) keep primary free, gate only the secondary commands on the paid key; (2) require the paid key for everything; (3) drop the paid source."

Default to option 1 unless the user overrides. Record the decision in source-priority.json under auth_scoping.

Single-source runs: If only one service is named, skip this gate entirely — no ordering to confirm.


Run Initialization

After you know <api> (from the Orientation & Briefing flow above; preflight already ran at the top), initialize the run-scoped artifact paths:

mkdir -p "$PRESS_RUNSTATE/runs"
RUN_ID=""
API_RUN_DIR=""
for attempt in 1 2 3 4 5; do
  RUN_SUFFIX="$(LC_ALL=C tr -dc 'a-f0-9' </dev/urandom 2>/dev/null | head -c 8 || true)"
  if [ -z "$RUN_SUFFIX" ]; then
    RUN_SUFFIX="pid$$-$attempt"
  fi
  CANDIDATE_RUN_ID="$(date +%Y%m%d-%H%M%S)-$RUN_SUFFIX"
  CANDIDATE_RUN_DIR="$PRESS_RUNSTATE/runs/$CANDIDATE_RUN_ID"
  if mkdir "$CANDIDATE_RUN_DIR" 2>/dev/null; then
    RUN_ID="$CANDIDATE_RUN_ID"
    API_RUN_DIR="$CANDIDATE_RUN_DIR"
    break
  fi
done
if [ -z "$RUN_ID" ]; then
  echo "could not allocate a unique run directory under $PRESS_RUNSTATE/runs" >&2
  exit 1
fi
RESEARCH_DIR="$API_RUN_DIR/research"
PROOFS_DIR="$API_RUN_DIR/proofs"
PIPELINE_DIR="$API_RUN_DIR/pipeline"
DISCOVERY_DIR="$API_RUN_DIR/discovery"
CLI_WORK_DIR="$API_RUN_DIR/working/<api>-pp-cli"
STAMP="$(date +%Y-%m-%d-%H%M%S)"

# Session state (live cookies, CSRF tokens captured during authenticated
# browser-sniff) lives OUTSIDE $API_RUN_DIR so the Phase 5.5 archive
# `cp -r "$DISCOVERY_DIR"` cannot pick it up. Containment by location, not by
# manual rm-before-archive.
#
# Base prefix is user-scoped (`printing-press-$(id -u)`) so that on a Linux
# host with a shared /tmp, the umask-077 subshell below does not lock the
# top-level `printing-press` directory to a single user. macOS already gives
# us a per-user $TMPDIR; the $(id -u) suffix keeps semantics identical there.
SESSION_BASE="${TMPDIR:-/tmp}/printing-press-$(id -u)"
SESSION_DIR="$SESSION_BASE/session/$RUN_ID"
SESSION_STATE_FILE="$SESSION_DIR/session-state.json"

mkdir -p "$RESEARCH_DIR" "$PROOFS_DIR" "$PIPELINE_DIR" "$CLI_WORK_DIR"
# Create $SESSION_DIR inside a subshell with a tight umask so it lands at 0700
# at creation, not after a follow-up chmod. The two-step `mkdir; chmod` form
# leaves a TOCTOU window where a concurrent process could open the directory
# (and any session-state.json written into it) while perms are still
# umask-derived (typically 0755 on Linux). The umask propagates to every
# directory `mkdir -p` creates; the user-scoped $SESSION_BASE above is what
# keeps that from blocking other users on the same host.
(umask 077 && mkdir -p "$SESSION_DIR")
STATE_FILE="$API_RUN_DIR/state.json"

Maintain a lightweight state file at $STATE_FILE so /printing-press-score can rediscover the current run. It should always contain:

{
  "api_name": "<api>",
  "run_id": "$RUN_ID",
  "working_dir": "$CLI_WORK_DIR",
  "output_dir": "$CLI_WORK_DIR",
  "spec_path": "<absolute spec path if known>"
}

run_id is the unique value allocated above from the wall-clock stamp plus a short random suffix. mkdir "$CANDIDATE_RUN_DIR" is the collision guard: if another run already owns a candidate directory, allocate another ID instead of reusing the directory. Persisting this value in state.json makes the state file the source of truth for generate, dogfood acceptance, promote, /printing-press-score, and future state-loading consumers. Without run_id in either state or legacy path fallback, cli-printing-press dogfood --live --write-acceptance refuses to write the gate marker.

Do not create a go.work file in $CLI_WORK_DIR. Generated modules must build and test as standalone modules; a mismatched workspace go directive can break Go 1.25+ toolchains and lefthook checks. Editor/gopls workspace noise is cosmetic and must not be traded for broken go build or go test.

There are exactly three durable writable locations. Every generated artifact this skill preserves goes to one of them:

  • $PRESS_RUNSTATE/ — mutable working state for the current run (research, proofs, pipeline artifacts, plans, intermediate docs)
  • $PRESS_LIBRARY/ — published CLIs (<api-slug>/ subdirectories)
  • $PRESS_MANUSCRIPTS/ — archived run evidence (research, proofs, discovery)

Short-lived command captures may use /tmp/printing-press/ with unique mktemp paths and must be deleted after use.

Examples of the current naming/layout:

  • $PRESS_LIBRARY/notion/ — published CLI directory (keyed by API slug)
  • notion-pp-cli — the binary name inside the directory
  • /printing-press emboss notion — emboss accepts both slug and CLI name
  • discord-pp-cli/internal/store/store.go — internal source paths still use CLI name
  • linear-pp-cli stale --days 30 --team ENG — binary invocations use CLI name
  • github.com/mvanhorn/discord-pp-cli — Go module paths use CLI name

Outputs

Every run writes up to 5 concise artifacts under the current managed run and archives them to $PRESS_MANUSCRIPTS/<api-slug>/<run-id>/:

  1. research/<stamp>-feat-<api>-pp-cli-brief.md
  2. research/<stamp>-feat-<api>-pp-cli-absorb-manifest.md
  3. proofs/<stamp>-fix-<api>-pp-cli-build-log.md
  4. proofs/<stamp>-fix-<api>-pp-cli-shipcheck.md
  5. proofs/<stamp>-fix-<api>-pp-cli-live-smoke.md (only if live testing runs)

These do not need to be 200+ lines. Keep them dense, evidence-backed, and directly useful.

Phase 0: Resolve And Reuse

Before new research:

  1. Resolve the spec source.

    Local physical device detection. If the user's target is a local Bluetooth/BLE-controlled physical device (for example an appliance, toy, light, sensor, exercise machine, lock, or other device controlled from a phone app over Bluetooth), do not route it through browser-sniff as the primary discovery path. Read and apply references/device-sniff-ble.md. Use device-sniff ble for normalized BLE evidence and bluetooth-sniff as the discoverable alias. Community libraries, docs, Android logs, Wireshark/nRF captures, and manual action journals are evidence inputs; they are not a reason to hardcode a vendor-specific generator path.

    BLE mapping research gate. A BLE scan/inspect/read/subscribe pass only discovers identity, services, characteristics, and telemetry candidates; it does not by itself discover what write payloads mean. Before generating callable control commands or running any live write, establish a command mapping from at least one concrete source: user-provided mapping, official docs, community protocol/library code, Android/iOS/Bluetooth logs, Wireshark/nRF captures, or an operator action journal that correlates a real user action with observed writes. If no mapping source is found, generate only read/status/capability metadata or stop and ask the user for mapping evidence. Do not invent mutating payloads or brute-force probe a physical device.

    URL Detection — If the argument contains ://, it's a URL. Determine whether it's a spec or a website before proceeding.

    Step 1: Content probe. Fetch the URL with the raw docs helper from references/fetch-docs.md and inspect the response status, Content-Type, and first few lines of the returned file:

    • Check the Content-Type header and the first few lines of the body.
    • If the fetch fails (timeout, 404, DNS error), record the exact status/error, then skip to Step 2 — treat it as a website.

    If the content starts with openapi:, swagger:, or is valid JSON containing an "openapi" or "swagger" key → it's a spec. Treat as --spec and proceed directly. No disambiguation needed.

    If the content is a HAR file (JSON with "log" and "entries" keys) → treat as --har and proceed directly.

    Step 2: Disambiguation. If the content is HTML or the probe failed, ask the user what they want. Extract the site name from the hostname (e.g., postman.com → "Postman", app.linear.app → "Linear"). Derive <api> from the site name using the same cleanSpecName normalization the generator uses.

    Use AskUserQuestion with:

    • question: "What kind of CLI do you want for <SiteName>?"
    • header: "CLI target"
    • multiSelect: false
    • options:
      1. label: "<SiteName>'s official API"description: "Build a CLI for <SiteName>'s documented API (e.g. REST endpoints, webhooks, OAuth)"
      2. label: "The <SiteName> website itself"description: "Build from the website itself — I may open or attach to Chrome during generation to capture site traffic, then generate a lightweight CLI from replayable HTTP/HTML surfaces"

    The user can also pick the automatic "Other" option to describe what they're after in free text.

    Routing after disambiguation:

    • "'s official API" → use <api> as the argument, proceed with normal discovery (Phase 1 research, then Phase 1.7 browser-sniff gate evaluates independently as usual)
    • "The website itself" → use <api> as the argument, set BROWSER_SNIFF_TARGET_URL=<url>. Proceed to Phase 1 research. When Phase 1.7 is reached, skip the browser-sniff gate decision and go directly to "If user approves browser-sniff" (the user already approved temporary browser discovery in Phase 0 — do not re-ask). Use BROWSER_SNIFF_TARGET_URL as the starting URL for browser capture. The printed CLI must still use a replayable runtime surface; do not ship a resident browser transport.
    • "Other" → read the user's free-form response and adapt

    End of URL detection. The remaining spec resolution rules apply when the argument is NOT a URL:

    • If the user passed --har <path>, this is a HAR-first run. Before invoking browser-sniff, parse the file as JSON and verify it contains traffic: HAR files must have .log.entries | length > 0; enriched capture JSON must have .entries | length > 0. If the file is missing, invalid JSON, or has zero entries, do not continue silently. Print HAR/capture contains no network entries; the export likely recorded no traffic. and ask via AskUserQuestion with exactly these choices: Capture again (Recommended) — "Re-export a HAR after recording a real user flow"; Proceed with docs-only — "Skip HAR/browser-sniff and continue from docs/spec discovery only"; HOLD — "Stop this run until a valid capture is available." Only proceed to browser-sniff when the parsed entry count is non-zero.
    • For a valid --har <path>, run cli-printing-press browser-sniff --har <path> --name <api> --output "$RESEARCH_DIR/<api>-browser-sniff-spec.yaml" --analysis-output "$DISCOVERY_DIR/traffic-analysis.json" to generate a spec and traffic analysis from captured traffic. If $API_RUN_DIR/source-priority.json exists with two or more sources, add --preserve-hosts so combo-CLI captures retain peer API hosts with per-endpoint base_url overrides instead of collapsing them into secondary evidence. Immediately inspect $DISCOVERY_DIR/traffic-analysis.json: if it contains the empty_response_shapes warning, or if every endpoint cluster has size_class: "empty" and response_shape: {}, use curl/direct HTTP to call each discovered endpoint and capture response structure before writing or trusting the spec. Do not proceed to generate with a sniffed spec that has no type information. Use the generated spec as the primary spec source for the rest of the pipeline only after this quality check passes. Skip the browser-sniff gate in Phase 1.7 (browser-sniff already ran).
    • If the user passed --spec, use it directly (existing behavior).
    • Otherwise, proceed with normal discovery (catalog, KnownSpecs, apis-guru, web search).

    Directory spec-source guard

    If any resolved spec source is a local directory, do not pass the directory itself to cli-printing-press generate and do not silently pick the first file. Enumerate candidate specs first:

    find "$SPEC_SOURCE_DIR" -type f \( -iname '*.json' -o -iname '*.yaml' -o -iname '*.yml' \) | sort
    

    Keep only files whose head looks like an OpenAPI or Swagger root document (openapi:, swagger:, or JSON with a top-level "openapi" or "swagger" key). Ignore unrelated JSON/YAML config files.

    When the filtered candidate list is empty, abort with: No OpenAPI/Swagger spec found under <directory>. Pass --spec <file> directly. Do not continue with the raw directory as the spec source.

    When the directory contains exactly one candidate, use that file as the spec source and write it to state.json as spec_path.

    When the directory contains more than one candidate:

    • Print a prominent warning before generation: N OpenAPI/Swagger specs found under <directory>; no single file represents the whole API surface.
    • List every candidate when N <= 20; otherwise list the first 20 sorted paths and print ...and N-20 more.
    • Record the directory and candidates in $STATE_FILE before continuing: spec_path is the directory and spec_candidates is the sorted list.
    • Ask the user to choose one spec, several specs, or all specs. If this runtime cannot ask a blocking question, stop after printing the warning and tell the user to re-run with explicit --spec <file> arguments. This is the minimum safe floor: never let a directory run finish while hiding that additional specs were ignored.
    • After the user confirms the selection, update $STATE_FILE with selected_spec_paths set to the list that will be generated.
    • For multiple selected specs, default to one independent printed CLI per spec using a derived <api>-<spec-slug> name and a distinct working directory under $API_RUN_DIR/working/. Do not merge all selected specs into one CLI unless the user explicitly asks for a combined surface and provides the umbrella name for --name.
  2. Check for prior research in:

    • $PRESS_MANUSCRIPTS/<api-slug>/*/research/*
  3. Reuse good prior work instead of redoing it.

  4. Library Check — Check if a CLI for this API already exists in the library or is actively being built, and present the user with context and options.

    First, check lock status to detect active builds:

    LOCK_STATUS=$("$PRINTING_PRESS_BIN" lock status --cli <api>-pp-cli --json 2>/dev/null)
    LOCK_HELD=$(echo "$LOCK_STATUS" | grep -o '"held"[[:space:]]*:[[:space:]]*[a-z]*' | head -1 | sed 's/.*: *//')
    LOCK_STALE=$(echo "$LOCK_STATUS" | grep -o '"stale"[[:space:]]*:[[:space:]]*[a-z]*' | head -1 | sed 's/.*: *//')
    LOCK_PHASE=$(echo "$LOCK_STATUS" | grep -o '"phase"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"phase"[[:space:]]*:[[:space:]]*"//;s/"//')
    LOCK_AGE=$(echo "$LOCK_STATUS" | grep -o '"age_seconds"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*: *//')
    

    Then check the library directory:

    CLI_DIR="$PRESS_LIBRARY/<api>"
    HAS_LIBRARY=false
    HAS_GOMOD=false
    CLI_RELEASE_VERSION=""
    PRIOR_STEINBERGER_SCORE=""
    PRIOR_SUB60_REPRINT=false
    if [ -d "$CLI_DIR" ]; then
      HAS_LIBRARY=true
      if [ -f "$CLI_DIR/go.mod" ]; then
        HAS_GOMOD=true
      fi
      # Read manifest if available
      MANIFEST="$CLI_DIR/.printing-press.json"
      if [ -f "$MANIFEST" ]; then
        PRESS_VERSION=$(cat "$MANIFEST" | grep -o '"printing_press_version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"printing_press_version"[[:space:]]*:[[:space:]]*"//;s/"//')
        GENERATED_AT=$(cat "$MANIFEST" | grep -o '"generated_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"generated_at"[[:space:]]*:[[:space:]]*"//;s/"//')
        PRIOR_STEINBERGER_SCORE=$(jq -r '.scorecard.steinberger.percentage // empty' "$MANIFEST" 2>/dev/null || true)
        if [ -n "$PRIOR_STEINBERGER_SCORE" ] && awk "BEGIN { exit !($PRIOR_STEINBERGER_SCORE < 60) }"; then
          PRIOR_SUB60_REPRINT=true
        fi
      fi
      RELEASE_MANIFEST="$CLI_DIR/.printing-press-release.json"
      if [ -f "$RELEASE_MANIFEST" ]; then
        CLI_RELEASE_VERSION=$(jq -r '.version // empty' "$RELEASE_MANIFEST" 2>/dev/null || true)
      fi
      # Get directory modification time as fallback
      CLI_MTIME=$(stat -f "%Sm" -t "%Y-%m-%d" "$CLI_DIR" 2>/dev/null || stat -c "%y" "$CLI_DIR" 2>/dev/null | cut -d' ' -f1)
    fi
    

    Decision matrix:

    Library dir? Lock? Stale? Has go.mod? Action
    No No N/A N/A Proceed normally
    No Yes No N/A Warn: "Actively being built (phase: <phase>, <age> seconds ago). Wait, use a different name, or pick a different API."
    No Yes Yes N/A Offer reclaim: "Interrupted build detected (stale since <age>s ago). Reclaim and start fresh?"
    Yes No N/A Yes Existing "Found existing" flow (see below)
    Yes No N/A No Debris: "Found <api> directory in library but it appears incomplete (no go.mod). Clean up and start fresh?" If user approves, rm -rf "$CLI_DIR" and proceed normally.
    Yes Yes No Any Warn: "Actively being rebuilt (phase: <phase>, <age> seconds ago). Wait, use a different name, or pick a different API."
    Yes Yes Yes Any Offer reclaim: "Interrupted rebuild detected (stale since <age>s ago). Reclaim and start fresh?"

    If actively locked (not stale): Present via AskUserQuestion with options to wait, pick a different API, or force-reclaim (cli-printing-press lock acquire --cli <api>-pp-cli --scope "$PRESS_SCOPE" --force).

    If stale lock: Reclaiming is automatic on lock acquire in Phase 2. If user approves, proceed normally — the lock acquire in Phase 2 will auto-reclaim the stale lock.

    If library exists with go.mod and no lock (completed CLI): Display context and present options using AskUserQuestion:

    Found existing <api> in library (last modified <date>).

    If PRESS_VERSION is available, append: Built with printing-press v<version>. If CLI_RELEASE_VERSION is available, append: Published CLI release: <version>. This is the public-library per-CLI CalVer release, not the generator version; do not use it for the generator staleness comparison below. If PRIOR_SUB60_REPRINT=true, append: Prior Steinberger score: <score>%. Reprint will require all approved transcendence rows to ship unless you explicitly accept partial coverage.

    If prior research was also found (step 2), include the research summary alongside the library info.

    Then ask:

    1. "Generate a fresh CLI" — Re-runs the Printing Press into a working directory, overwrites generated code, then rebuilds transcendence features. Prior research is reused if recent.
    2. "Improve existing CLI" — Keeps all current code, audits for quality gaps, implements top improvements. The Printing Press is not re-run.
    3. "Review prior research first" — Show the full research brief and absorb manifest before deciding.

    If the user picks option 1, proceed to Phase 1 (research) and then Phase 2 (generate) as normal. If the user picks option 2, invoke /printing-press-polish <api> to improve the existing CLI. If the user picks option 3, display the prior research, then re-present options 1 and 2.

    MANDATORY when re-using prior research after a binary upgrade. If the user picks "Generate a fresh CLI" (option 1) AND PRESS_VERSION from the manifest differs from the current binary's version (parse both via semver and compare; only fire when the leading minor or major segment changed — patch-level deltas don't trigger this), prompt the user once before kicking off Phase 1 research.

    Construct the prompt's "what changed" list from these category buckets — the categories are stable across versions; the specific machine deltas inside each category are not. Read docs/CHANGELOG.md (or run git log --oneline v<PRESS_VERSION>..v<CURRENT> -- internal/) and tag each notable change to one of these buckets:

    Category Affects prior-brief assumption about...
    Transport / reachability Which sources are reachable, what auth/clearance is needed, which clients (stdlib, Surf, browser-clearance) the brief assumed
    Scoring rubrics What Phase 1.5/scorecard dimensions the brief targets, whether prior "high-priority" features still rank as such
    Auth modes Whether brief's auth choice (api-key, cookie, composed, oauth) is still the right pick, whether new modes unlock new endpoints
    MCP surface Whether brief's MCP shape (endpoint-mirror vs intent vs code-orchestration) matches the latest emit defaults
    Discovery Whether browser-sniff / crowd-sniff workflows changed, whether prior gate decisions are still valid

    For the prompt itself, list only the buckets that have at least one notable change between the two versions. If the CHANGELOG / git log is unavailable, list all five buckets generically and let the user decide.

    "The prior <api> was generated with printing-press v<PRESS_VERSION>. The current binary is v<CURRENT>. Categories where the machine has changed since then: <applicable buckets>. Each can invalidate prior research assumptions. Re-validate the prior brief against the current machine before reusing it?"

    Options:

    1. Yes, re-validate the prior research — fold the validation into Phase 1 (briefly re-probe reachability for previously-blocked sources, confirm scoring still classifies the prior CLI's pattern correctly, etc.) before reusing the brief.
    2. No, reuse the prior research as-is — proceed with the brief verbatim, even if the underlying machine assumptions are stale.

    The prompt forces the user to acknowledge the version delta and explicitly accept (or refuse) re-validation. Skip it entirely on first generation, on same-version regenerations, or when no prior manifest exists.

    If no CLI exists in the local library and no lock is active, run the Public-library check below before proceeding to Phase 1.

    Public-library check (registry.json + blocked-apis.json)

    The local library check above only sees CLIs this machine has already printed. A user on a fresh checkout — or one who typed a slightly different name than the published slug (Slack vs slack-bot, Cal vs cal-com), or who described what they wanted in their own words (Hacker News reader, Notion clone, prediction market) — will miss CLIs that already exist in the public library. Scan mvanhorn/printing-press-library/registry.json to catch those cases before Phase 1 research begins (the expensive 30-60-minute portion of the pipeline).

    The public library also carries blocked-apis.json, a shared journal of APIs that were attempted and put on hold for reachability or buildability reasons. Scan it in the same Phase 0 window so a user does not repeat an already-known dead-end run before the blocking issue is fixed.

    Skip this check entirely when:

    • The local-library check above already prompted (mutual exclusion — do not double-ask).
    • BROWSER_SNIFF_TARGET_URL is set (the user is building a from-website CLI; the registry indexes API CLIs and naming collisions are unlikely and intentional).
    • The user passed --har <path> with an explicit --name <api> for a private capture.

    Fetch the registry and blocked journal. Match the pattern /printing-press-import and /printing-press-reprint already use:

    REGISTRY=$(mktemp)
    if ! gh api -H "Accept: application/vnd.github.v3.raw" \
         repos/mvanhorn/printing-press-library/contents/registry.json \
         > "$REGISTRY" 2>/dev/null; then
      echo "Public-library check failed: registry.json is unreachable. Stop here instead of treating the API as unpublished; retry when GitHub/library access is available."
      rm -f "$REGISTRY"
      exit 1
    fi
    
    BLOCKED_APIS=$(mktemp)
    if ! gh api -H "Accept: application/vnd.github.v3.raw" \
         repos/mvanhorn/printing-press-library/contents/blocked-apis.json \
         > "$BLOCKED_APIS" 2>/dev/null; then
      echo "Blocked-API journal check skipped: blocked-apis.json unreachable or absent. Proceeding with the registry check."
      rm -f "$BLOCKED_APIS"
      BLOCKED_APIS=""
    fi
    

    Do not continue past a registry fetch/parse failure: it can hide an already published nested CLI and route a duplicate greenfield build. Network failure or missing blocked-apis.json is still non-blocking. After step 4 finishes, clean up tempfiles only if the fetch succeeded: [ -n "$REGISTRY" ] && rm -f "$REGISTRY" and [ -n "$BLOCKED_APIS" ] && rm -f "$BLOCKED_APIS". The blocked-journal failure branch above already removed its file and set its variable to empty, so an unconditional rm -f "$BLOCKED_APIS" would run rm -f "".

    Read the blocked journal before reasoning about registry matches. If BLOCKED_APIS is non-empty, read it directly. Expected shape:

    [
      {
        "slug": "1001tracklists",
        "attempted_at": "2026-05-25",
        "verdict": "hold",
        "reason": "Cloudflare Turnstile clearance gate; pure-HTTP cannot mint fsuid",
        "blocking_issue": 2140,
        "permanent": false
      }
    ]
    

    Entries are API-slug records, not printed-CLI registry entries. Match the user's requested API against slug using the same slug-normalization judgment as the registry check (<api>, <api>-cli, <api>-pp-cli, punctuation and case variants). Do not use vague category or description matching for the blocked journal. A false positive here stops a potentially valid run; only prompt when the entry appears to be the same API under a slug or brand spelling variant.

    If a blocked entry matches, prompt before reading registry matches:

    "<entry.slug> was attempted on <entry.attempted_at> and held — <entry.reason>. The shared blocked-API journal exists so users do not repeat known unreachable or unbuildable runs before the blocker changes. Proceed anyway?"

    Where <tracking suffix> is:

    • (tracking #<entry.blocking_issue>; marked permanent) when blocking_issue is non-null and permanent is true.
    • (tracking #<entry.blocking_issue>) when blocking_issue is non-null and permanent is false.
    • (marked permanent) when permanent is true and blocking_issue is null.
    • empty when neither applies.

    Options:

    1. Stop here (recommended) — end this run. If a tracking issue is present, tell the user to re-attempt only after that issue closes or the journal entry is updated.
    2. Proceed anyway — continue to the registry check and then Phase 1. Use this only when the user has new evidence that the blocker no longer applies or wants a deliberate fresh attempt.

    If multiple blocked entries somehow match, pick the most recent attempted_at value and mention that additional older journal entries exist. If blocked-apis.json is malformed, print "Blocked-API journal check skipped: blocked-apis.json is malformed. Proceeding with the registry check." and continue; do not let a bad journal file block fresh prints.

    Read the registry and reason about matches — do not gate on string equality alone. The file is small (~88 KB, ~135 entries today); read it directly and use judgment. Each entry has fields name (slug), category, api (brand display), description, path, printer.

    The user's argument may arrive in many shapes, and only some are catchable by deterministic match:

    • Slug or near-slugNotion, notion-cli, notion-pp-cli
    • Brand with punctuationCal.com, Customer.io, Archive.today, Trigger.dev
    • Concept or categoryHacker News reader, Notion clone, prediction market, prediction-market CLI
    • Adjacent productPolymarket when the registry has kalshi (peer prediction market)
    • Genuinely novel — no useful overlap

    Classify the best match at three confidence levels and act only on the top two:

    • High — same product under a different name (slug variant, brand vs slug form, -cli/-pp-cli suffix variant, well-known alias). Examples: Cal.comcal-com, Notionnotion-cli, slackslack-bot.
    • Medium — same category and overlapping function; a reasonable user would want to know before building. Examples: prediction market finds kalshi, Hacker News reader finds hackernews, Polymarket surfaces kalshi as a peer.
    • Low — vaguely adjacent (e.g. "payment gateway" finding every payment-related CLI). Skip silently — false-positive prompts get dismissed reflexively at this gate.

    Resist over-matching on description keywords. Most descriptions mention several adjacent concepts; matching liberally on description text produces noise. Use the description to confirm a name-or-category candidate, not to discover candidates from scratch.

    Combo CLIs. When SOURCE_PRIORITY is set (from the Multi-Source Priority Gate above), skip the single-source High/Medium/No-match branches below. Classify matches per source, then present a single combined prompt rather than asking N times. For combo runs the existing single-source CLIs are usually informational — the user came here to build a combo, so the recommended default is to continue with the combo rather than reprint a component standalone.

    Cap displayed reprint options at 2 across all sources combined so the prompt fits the 4-option AskUserQuestion limit (2 reprints + continue + abort). Pick the 2 best candidates by judgment in this order: (1) High over Medium, (2) primary-source over secondary-source (the first entry in SOURCE_PRIORITY wins ties), (3) canonical slug over variant. If additional matches exist beyond the displayed 2, append "(plus N other source matches)" to the prompt body so the user knows the list is truncated. Omit sources with no match rather than listing them as empty rows. If no source has any match at High or Medium, print nothing and proceed to Phase 1.

    Found matches across the sources you listed:

    • <source1>: <entry1.name> (<entry1.api>) [High] — same product as <source1>
    • <source2>: <entry2.name> (<entry2.api>) [Medium] — similar/adjacent

    This is informational — these components already exist as single-source CLIs. Continue building the combo, switch to reprinting one standalone, or abort?

    Options:

    1. Continue with the combo as planned (recommended) — the combo itself is the value-add; proceed to Phase 1 with all sources.
    2. Reprint <entry1.name> standalone instead — invoke /printing-press-reprint <entry1.name> (abandons the combo for now).
    3. Reprint <entry2.name> standalone instead — same, for the second candidate.
    4. Abort — stop here.

    The [High] / [Medium] tags surface the confidence so the user can distinguish "this is literally the thing you named" from "this is adjacent." Tag in the bullet, not the option label, to keep options scannable.

    Single-source CLIs. When SOURCE_PRIORITY is not set, use the branches below.

    High match — prompt strongly. Under Claude Code, use AskUserQuestion; under another harness, use the equivalent native prompt primitive. The option set is the same either way.

    Found <entry.api> in the public library (printed by @<entry.printer>, path <entry.path>).

    <entry.description>

    URL: https://github.com/mvanhorn/printing-press-library/tree/main/<entry.path>

    This CLI already exists. What would you like to do?

    Options:

    1. Reprint with the current Printing Press (recommended) — end this run and invoke /printing-press-reprint <entry.name>. That skill pulls the existing CLI, carries prior research and post-publish patches into reconciliation, and regenerates under the current binary. Almost always the right choice when a user discovers the CLI exists.
    2. Continue and build a fresh one anyway — proceed with the current run from scratch. Rare; appropriate only for a deliberate fork or variant.
    3. Abort — stop here.

    Multiple High matches — present each candidate, do not use the Medium-match phrasing. Rare — typically only happens when the user's argument is ambiguous between siblings like slack and slack-bot. Cap displayed candidates at 2 to stay within the 4-option prompt limit alongside continue/abort. If 3+ High candidates somehow qualify, pick the 2 best by judgment (typically the canonical slug match plus the next-most-likely alternative) and note "(plus N other close matches)" in the prompt body so the user knows the list is truncated.

    Found multiple matches for <api> in the public library — each appears to be the same product under a different name:

    • <entry1.name> (<entry1.api>) — <entry1.description>
    • <entry2.name> (<entry2.api>) — <entry2.description>

    Pick one to reprint, or continue/abort.

    Options:

    1. Reprint <entry1.name> — invoke /printing-press-reprint <entry1.name>.
    2. Reprint <entry2.name> — same, for the second candidate.
    3. Continue and build a fresh one anyway — rare; appropriate only for a deliberate fork or variant.
    4. Abort — stop here.

    Medium match — present alternatives. Cap candidates at 2 to stay within the 4-option prompt limit alongside continue/abort.

    Found similar entries in the public library that don't exactly match <api> but may overlap:

    • <entry1.name> (<entry1.api>) — <entry1.description>
    • <entry2.name> (<entry2.api>) — <entry2.description>

    Continue with <api> as planned, or reprint one of these instead?

    Options:

    1. Continue with <api> as planned — proceed to Phase 1.
    2. Reprint <entry1.name> instead — invoke /printing-press-reprint <entry1.name>.
    3. Reprint <entry2.name> instead — same, for the second candidate.
    4. Abort — stop here.

    No High or Medium match: print nothing, proceed to Phase 1.

  5. API Key Gate — Check whether this API requires authentication, then handle accordingly.

First, determine if the API needs auth. Use these signals:

  • The spec has no security or securityDefinitions section → likely no auth needed
  • The API's endpoints are accessible without authentication (e.g., ESPN's undocumented endpoints, weather APIs, public data feeds) — note: "no auth required" does NOT mean the service has an official public API
  • No env var matching the API name exists AND no known token pattern applies
  • Community docs or npm/PyPI wrappers describe the API as "no auth required"

If no auth is required, skip the key gate entirely. Proceed with: "No authentication required for <API> — skipping API key gate." Do NOT call it "a public API" unless the service officially publishes one. Many services (ESPN, etc.) have unauthenticated endpoints without having an official API. Live smoke testing in Phase 5 will work without a key.

If the API DOES require auth, run the key gate:

Token detection order:

  • GitHub: GITHUB_TOKEN, GH_TOKEN, or gh auth token
  • Discord: DISCORD_TOKEN, DISCORD_BOT_TOKEN
  • Linear: LINEAR_API_KEY
  • Notion: NOTION_TOKEN
  • Stripe: STRIPE_SECRET_KEY
  • Generic: API_KEY, API_TOKEN

If a token IS found, stop and explain:

Found <ENV_VAR> in your environment. This key will be used only for read-only live smoke testing in Phase 5 — listing, fetching, and health checks. It will never be used for write operations (create, update, delete). OK to use it?

  • If the user approves → proceed with the key available for Phase 5.
  • If the user declines → proceed without the key and display: "Live smoke testing (Phase 5) will be skipped. The CLI will still be generated and verified against mock responses."

If no token is found, stop and ask:

No API key detected for <API>. You can provide one now for read-only live smoke testing in Phase 5, or continue without it.

Set it with export <ENV_VAR>=<your-key> or paste the key here.

  • If the user provides a key → proceed with the key available for Phase 5.
  • If the user declines → proceed without the key and display: "Live smoke testing (Phase 5) will be skipped. The CLI will still be generated and verified against mock responses."

Resolve the API key gate (or skip it for public APIs) before moving to Phase 1.

Phase 1: Research Brief

When BROWSER_SNIFF_TARGET_URL is set: Skip the catalog check, spec/docs search, and SDK wrapper search — none of these exist for an undocumented website feature. Focus research on understanding what the site/feature does, who uses it, what workflows it supports, and what competitors offer similar functionality. The spec will come from browser-sniffing in Phase 1.7.

Before reading documentation, read references/fetch-docs.md. Use fetch-docs.sh for the API's primary docs, OpenAPI/Postman links, auth guides, error handling, rate limits, pagination, webhooks, and any per-endpoint reference page. Preserve exact status codes and inspect the returned local file directly so enum values, field constraints, casing, examples, and nav/link variants are not lost through summarization.

Before starting research, check if the API has a built-in catalog entry:

cli-printing-press catalog show <api> --json 2>/dev/null

If the catalog has an entry for this API, branch on the entry type:

Spec-based entry (spec_url populated) — present the user with a choice:

" is in the built-in catalog (spec: ). Use the catalog config to skip discovery, or run full discovery?"

  • If catalog config: use the spec_url from the catalog entry, skip the research/discovery phase
  • If full discovery: proceed with the normal research workflow

Wrapper-only entry (no spec_url, wrapper_libraries populated) — this is a reverse-engineered API that has no official spec but has known community libraries. The catalog entry is a discovery aid only: cli-printing-press generate requires --spec and does not consume wrapper-library metadata, so there is no direct generation path from a wrapper-only entry today. Tell the user this up front via AskUserQuestion:

" has no official spec. The catalog knows about these community-maintained wrappers, but the Printing Press cannot generate a CLI directly from a wrapper. The next step has to be browser-sniffing the upstream to author an internal YAML spec, browser-sniffing or HAR-capturing the dominant source first and then using the multi-source aggregator pattern for secondary hand-authored sources, or hand-writing a Go module that imports the wrapper. Which path do you want?"

Present each wrapper_libraries entry alongside the question with language, integration mode, and notes so the user can see what implementation backing exists. Example for google-flights:

  • krisukox/google-flights-api (Go, native, MIT) — Pure Go, importable; single-binary CLI with no runtime deps.

Record the user's choice (and the selected wrapper, when relevant) in $API_RUN_DIR/state.json under an implementation field so later phases can read it. For wrapper or hand-written-module paths, use { "kind": "wrapper", "library": "<name>", "url": "<url>", "integration_mode": "native|subprocess|html-scrape", "next_step": "browser-sniff|hand-written-module" }. For the aggregator path, use { "kind": "aggregator-pattern", "dominant_source": "<source>", "spec_source": "browser-sniff|har|provided-spec", "spec_path": "<path-to-generated-spec>", "secondary_sources": ["<source>"], "next_step": "aggregator-pattern" }; do not populate library or integration_mode unless a specific secondary source is backed by a wrapper. This field is for skill bookkeeping; the generator does not currently read it. If the user picks browser-sniff, route into the Phase 1.7 browser-sniff path to produce a spec, then run generate --spec against it. If the user picks the aggregator path, first route the dominant source through Phase 1.7 browser-sniff or HAR capture to produce the primary spec, then read and apply references/aggregator-pattern.md: generate from that spec, then hand-author the secondary source clients and sources command tree. If the user picks a hand-written module, stop the press here and hand off — there is no generator path to drop them into.

No catalog hit — proceed normally without mentioning the catalog.

Adding new wrapper-only APIs: drop a YAML file in catalog/ with wrapper_libraries populated and rebuild the binary. No skill changes needed.

Write one build-driving brief, not a stack of phase essays.

The brief must answer:

  1. What is this API actually used for?
  2. What are the top 3-5 power-user workflows?
  3. What are the top table-stakes competitor features?
  4. What data deserves a local store?
  5. Why would someone install this CLI instead of the incumbent?
  6. What is the product name and thesis?

Research checklist:

  • Find the spec or docs source. For docs pages whose details affect generation, fetch the raw page with fetch-docs.sh, then read/grep the returned path directly.
  • Find the top 1-2 competitors
  • Check GitHub issues on the top wrapper/SDK repo for "403", "blocked", "broken", "deprecated", "rate limit". If multiple issues report the API is inaccessible or broken, flag this in the research brief as a reachability risk. This is critical for unofficial/reverse-engineered APIs.
  • Find official and popular SDK wrappers on npm (site:npmjs.com) and PyPI (site:pypi.org)
  • Find 2-3 concrete user pain points
  • Identify the highest-gravity entities
  • Pick the top 3-5 commands that matter most

Do not produce separate mandatory documents for:

  • workflow ideation
  • parity audit
  • data-layer prediction
  • product thesis

Put them in the one brief.

Write:

$RESEARCH_DIR/<stamp>-feat-<api>-pp-cli-brief.md

Suggested shape:

# <API> CLI Brief

## API Identity
- Domain:
- Users:
- Data profile:

## Reachability Risk
- [None / Low / High] [evidence: e.g., "6 open issues on reteps/redfin about 403 errors since 2025"]
- Tier/permission hints from 4xx body: [omit when absent; otherwise quote the matched bounded line(s) from Phase 1.9]
- Probe-safe endpoint used: [omit when absent; otherwise "<METHOD> <path>" from `x-pp-safe-probe`]

## Top Workflows
1. ...

## Table Stakes
- ...

## Data Layer
- Primary entities:
- Sync cursor:
- FTS/search:

## Codebase Intelligence
- [DeepWiki findings if available, otherwise omit this section]
- Source: DeepWiki analysis of {owner}/{repo}
- Auth: [token type, header, env var pattern]
- Data model: [primary entities and relationships]
- Rate limiting: [limits and behavior]
- Architecture: [key insight about internal design]

## User Vision
- [USER_BRIEFING_CONTEXT if provided, otherwise omit this section]

## Source Priority
- [Only present for combo CLIs. Copy the confirmed ordering from `source-priority.json`.]
- Primary: <Source A> — [spec state: official / community-wrapper / no-spec-browser-sniff-required] — [auth: free / paid]
- Secondary: <Source B> — [...]
- Tertiary: <Source C> — [...]
- **Economics:** [e.g., "Primary is free; paid key for <Source B> is scoped to its own commands only."]
- **Inversion risk:** [e.g., "Primary has no OpenAPI; secondary has 53-endpoint spec. Do NOT let spec completeness invert the ordering."]

## Product Thesis
- Name:
- Why it should exist:

## Build Priorities
1. ...
2. ...
3. ...

MANDATORY: Before proceeding to Phase 1.5 (Absorb Gate), you MUST evaluate Phase 1.6 (Pre-Browser-Sniff Auth Intelligence), Phase 1.7 (Browser-Sniff Gate), and Phase 1.8 (Crowd-Sniff Gate) below. If no spec source has been resolved yet (no --spec, no --har, no catalog spec URL), the browser-sniff gate decision matrix MUST be evaluated. Do not skip to Phase 1.5.

Phase 1.5 will refuse to proceed without a browser-browser-sniff-gate.json marker file. Phase 1.7 writes this file with one entry per source (one entry for single-source CLIs, one entry per named source for combo CLIs). Missing marker = HARD STOP back to Phase 1.7. See Phase 1.7 "Enforcement" below for the contract.

Phase 1.6: Pre-Browser-Sniff Auth Intelligence

After Phase 1 research completes, analyze findings to proactively assess what auth context the user could provide. This step uses research intelligence to ask the right question before browser-sniffing starts, rather than waiting for the user to volunteer "I logged in."

Skip this step if: The briefing (Orientation & Briefing section) already captured auth context (AUTH_CONTEXT is set from the user selecting "I have an API key or I'm logged in").

Classify the API's auth profile from research findings:

Signal from research Auth profile What to ask
Community wrappers use API keys (e.g., STRIPE_SECRET_KEY), MCP source shows Authorization: Bearer headers, spec has security section API key auth "Do you have an API key for <API>?"
Site has user accounts, research found auth-only features (order history, saved items, rewards, account settings), login pages exist Browser session auth "This API has authenticated endpoints ([list specific features from research, e.g., order history, saved addresses, rewards]). Are you logged in to <site> in your browser? The browser-sniff will find more endpoints if you are."
Endpoints accessible without auth, no login-gated features found, community wrappers describe API as "no auth required" No auth needed Skip this step silently
Both API key AND browser session features found Dual auth Ask about both: API key for smoke testing, browser session for browser-sniff

Name the specific features the user would unlock. Do not say "auth would help." Say "This API has order history, saved addresses, and rewards that require a logged-in session."

Where signals come from:

  • Phase 1 brief's "Data profile" and "Top Workflows" sections
  • Phase 1.5a MCP source code analysis (auth patterns, token formats)
  • Community wrapper README "auth" or "authentication" sections
  • The API Key Gate's token detection (Phase 0.5) — if it already found a key, don't re-ask

For API key auth: Present via AskUserQuestion:

"Do you have an API key for <API>? It will be used for read-only live smoke testing in Phase 5."

  1. Yes — user provides the key or confirms it's in the environment
  2. No, continue without it — skip live smoke testing

If the user provides a key, set it in AUTH_CONTEXT so the API Key Gate (Phase 0.5) does not re-ask.

For browser session auth: Present via AskUserQuestion:

"<API> has authenticated endpoints ([list features]). Are you logged in to <site> in your browser? If so, the generated CLI will support auth login --chrome — you'll be able to authenticate just by being logged into the site in Chrome. No API key needed."

  1. Yes, I'm logged in — I'll use your session during browser-sniff and enable browser auth in the CLI
  2. No, but I can log in — I'll help you log in before browser-sniffing
  3. No, skip authenticated endpoints — browser-sniff only public endpoints

Set AUTH_SESSION_AVAILABLE=true if the user selects option 1 or 2. The Browser-Sniff Gate (Phase 1.7) will use this flag. After traffic capture, Step 2d in references/browser-sniff-capture.md validates that cookie replay works before enabling browser auth in the generated CLI.

For dual auth: Ask about both in sequence — API key first (simple env var check), then browser session.


Phase 1.7: Browser-Sniff Gate

After Phase 1 research, evaluate whether browser-sniffing the live site would improve the spec. This phase MUST produce a decision marker file for every source named in the briefing before Phase 1.5 can proceed.

Browser discovery is temporary discovery, not a printed-CLI runtime. Use browser-use, agent-browser, the Claude chrome-MCP (mcp__claude-in-chrome__*, when the runtime exposes it), or a manual HAR (optionally augmented with computer-use screenshots for visual guidance, when mcp__computer-use__* is exposed) to learn the hidden web contract: URLs, methods, persisted GraphQL hashes, BFF envelopes, response shapes, cookies, CSRF/header construction, HTML/SSR/RSS/JSON-LD surfaces, and whether replay is viable. The final printed CLI must use replayable HTTP, Surf/browser-compatible HTTP, browser-clearance cookie import plus replay, or structured HTML/SSR/RSS extraction. If the only working path requires live page-context execution, HOLD or pivot scope — do not generate a resident browser sidecar transport.

Automatic offer, explicit consent. The Printing Press decides when browser discovery should be offered, but opening Chrome, attaching to a browser session, installing browser-use/agent-browser, asking the user to solve a challenge, or driving the user's logged-in Chrome via the chrome-MCP requires explicit user approval through the Phase 0 website choice or the Phase 1.7 AskUserQuestion prompt. Approval at Phase 1.7 covers the full fallback set including chrome-MCP and computer-use when Step 2c.5's recovery menu later offers them — picking chrome-MCP at the recovery menu is a refinement of the Phase 1.7 consent, not a new consent surface. The disclosure language used at the Phase 1.7 prompt MUST enumerate these possibilities so the user understands what they are approving:

"Approving browser-sniff means the agent may run browser-use, agent-browser, ask you for a manual DevTools HAR export, or — if the default backends get blocked by an anti-bot gate and your runtime exposes them — drive your already-running Chrome via the chrome-MCP browser extension, or take read-only screenshots of your DevTools window via computer-use to guide you through the HAR export. Capture artifacts are written to $DISCOVERY_DIR/ and credential headers are stripped at write time. The chrome-MCP option uses your real logged-in Chrome session in a fresh capture tab; the agent never navigates your existing tabs."

If chrome-MCP picks up later in Step 2c.5's recovery menu, do NOT re-fire a per-invocation consent prompt — Phase 1.7's pre-approval covers it. The recovery menu lists chrome-MCP as one of the fallback options the user already pre-approved; the user's selection in the menu is a backend choice, not a new consent step.

Enforcement: the browser-browser-sniff-gate.json marker file

Phase 1.7 is a hard gate. Phase 1.5 reads a marker file and refuses to proceed without it. The model cannot skip this phase by reasoning around it.

Marker file location: $PRESS_RUNSTATE/runs/$RUN_ID/browser-browser-sniff-gate.json

Marker file shape:

{
  "run_id": "20260411-000903",
  "sources": [
    {
      "source_name": "<exact name from briefing, e.g., kayak-direct>",
      "decision": "approved | declined | skip-silent | pre-approved",
      "reason": "<one-line justification>",
      "asked_at": "2026-04-11T00:10:00Z"
    }
  ]
}

Decision values:

  • approved — user selected a browser-sniff option via AskUserQuestion. Proceed to "If user approves browser-sniff".
  • declined — user explicitly declined browser-sniff via AskUserQuestion. Proceed to "If user declines browser-sniff".
  • skip-silent — gate was silently skipped per the decision matrix (spec complete, --har provided, --spec provided, or login required with AUTH_SESSION_AVAILABLE=false). The reason field names which.
  • pre-approved — user already chose "The website itself" in Phase 0, where the prompt disclosed temporary Chrome/browser capture during generation, so BROWSER_SNIFF_TARGET_URL was set and the question was answered there.

Every path through Phase 1.7 MUST write a marker entry — approve, decline, and every silent-skip case. There is no code path that proceeds to Phase 1.5 without writing the marker.

asked_at is mandatory. It must reflect the actual time AskUserQuestion was invoked (or the time the silent-skip decision was made). Fabricated timestamps are a plan violation.

Banned skip reasons

The following rationales are NOT valid reasons to skip the browser-sniff gate. If any of these apply, you MUST still ask the user via AskUserQuestion and record their answer in the marker file:

  • "The target is client-rendered and needs Playwright" — browser capture tools (browser-use, agent-browser) exist specifically to handle client-rendered sites. A hard-to-browser-sniff target is not the same as an impossible one. Ask.
  • "Direct HTTP/curl got 403, 429, Cloudflare, Vercel, WAF, DataDome, or bot-detection HTML" — direct HTTP reachability failure is exactly when browser capture is valuable. Do not pivot to RSS, docs-only, official API, or a smaller product shape before attempting the approved browser-sniff. Route to cleared-browser capture instead.
  • "Direct HTTP/curl got HTTP 200 but only a content-less shell, interstitial, or deterministic-size truncation" — a 200-served shell is a clearance or JavaScript challenge, not a clean response. Do not conclude IP-blocked, rate-limited, or wait it out from this shape. Before declaring the target unreachable, climb the ladder: probe-reachability body-check, curl-impersonate/TLS check, real-browser cookie-warm via the cleared-browser path or chrome-MCP when available, then ask the user. Use chrome-MCP to understand the wall even when it cannot export cookie values.
  • "The 3-minute time budget looks tight" — the time budget applies AFTER the user approves browser-sniff, not before. You do not pre-judge whether a browser-sniff will fit the budget. Ask. If the budget blows after the user approves, fall back per the Time Budget rules below.
  • "We have a substitute data source from another API" — substituting one source for another is the user's call, not yours. If the user named a specific site or feature (e.g., Kayak /direct), they chose it deliberately. Ask about that exact source. Offering a different data source is a separate conversation AFTER the gate, not a reason to skip it.
  • "Installing browser-use or agent-browser is friction" — the browser-sniff capture reference already documents the install path. Tooling friction is not a valid skip reason. Ask.
  • "The documentation looks thorough enough" — the decision matrix already handles this case explicitly. If research found that competitors or community projects reference more endpoints than the spec covers, that IS a gap and you MUST ask.
  • "The user said 'let's go' earlier and implicitly approved everything" — "let's go" at the briefing stage is consent to proceed with research, not standing approval for every future decision. Ask each gate individually.
  • "The default browser-use / agent-browser path got hard-blocked by a WAF, so the only remaining option is to pivot scope or fall back to RSS/docs" — this is exactly when the chrome-MCP and computer-use fallback options enter, when the runtime exposes them. Step 1 of references/browser-sniff-capture.md detects which fallback MCPs are available; Step 2c.5 composes the recovery menu including those fallbacks; the gate is "ask before giving up," not "auto-pivot when blocked." Do NOT skip the Step 2c.5 menu. Do NOT pivot scope or substitute an alternate target without first asking the user via that menu.

These banned reasons all fired at once in a past combo-CLI run and caused a user-critical source to be silently swapped out. The marker file exists so this cannot happen again. If you find yourself writing a phrase like "skipping browser-sniff because X" where X is one of the above, stop and call AskUserQuestion.

Combo CLIs: per-source enforcement

When the briefing names multiple sources (e.g., "Google Flights + Kayak + FlightAware"), each named source is evaluated independently. The marker file has one entry per source. All entries must be present before Phase 1.5 can proceed.

Source identification rule: source names come from the briefing, verbatim. Use the user's exact wording as the source_name (normalized to kebab-case is fine: "Kayak /direct" → kayak-direct, "Google Flights" → google-flights, "FlightAware" → flightaware). Do not merge sources. Do not drop one in favor of another.

Per-source decision flow:

For each named source, run the "When to offer browser-sniff" decision matrix independently, using the research findings for THAT source. Each source produces its own AskUserQuestion call or its own silent-skip marker entry.

Combo CLI example (flightgoat pattern — directional guidance, not prescription):

Source Spec state Expected decision
flightaware Documented OpenAPI spec found (53 endpoints, appears complete) skip-silent with reason spec-complete
google-flights No official spec, but community wrapper exists (krisukox/google-flights-api) Ask via AskUserQuestion → record user's answer
kayak-direct No spec, no wrapper, user named this as a key feature Ask via AskUserQuestion → record user's answer

The marker file for this run would contain three entries. Phase 1.5 would HALT if any were missing.

When the user cares about only one source: you still ask for all sources that trigger the gate. The user can decline the others. Asking is cheap. Skipping silently breaks the contract.

Skip this gate entirely when

These are the only cases where Phase 1.7 is bypassed as a whole (not just skipped for one source). Even in these cases, a marker file with a single skip-silent entry is written to satisfy Phase 1.5's check:

  • User passed --spec and the spec is the canonical source for every named source → marker: { "source_name": "<api>", "decision": "skip-silent", "reason": "user-provided-spec" }
  • User passed --har → marker: { "source_name": "<api>", "decision": "skip-silent", "reason": "user-provided-har" }
  • BROWSER_SNIFF_TARGET_URL is set from Phase 0 (user chose "The website itself") → marker: { "source_name": "<api>", "decision": "pre-approved", "reason": "phase-0-website-choice" }, then go directly to "If user approves browser-sniff"

Direct HTTP challenge rule

If a reachability probe during Phase 1 research returns bot-protection evidence (403, 429, cf-mitigated: challenge, x-vercel-mitigated: challenge, x-vercel-challenge-token, AWS WAF, DataDome, PerimeterX, CAPTCHA, "Just a moment", "access denied"), run the no-browser reachability probe before announcing any browser escalation:

cli-printing-press probe-reachability "<url>" --json

This is non-negotiable. Do not present transport tiers as a peer menu for the user to choose between. Phrases like "Browser-sniff + clearance cookie", "Browser-sniff with Surf-only", "Try without browser at all", or "Browser-sniff, prefer Surf" route the user through implementation choices (Surf vs cookie vs full browser) they don't have context to make. The classifier is probe-reachability; the agent runs it and decides. Intent-level menus are fine — "Browser-sniff or HOLD?", "Browser-sniff or pick a different API?", or the standard yes/no browser-sniff offers below all ask about goals, not transport, and remain available.

Escalate consent in the order the agent actually needs it, not bundled up-front:

  1. Runtime probe (silent)probe-reachability runs without prompting. The user already opted into "the website itself" or equivalent in Phase 0; running an HTTP request needs no further consent.
  2. Browser-sniff offer (intent prompt) — Phase 1.7's normal "Browser-Sniff as enrichment" / "Browser-Sniff as primary" prompts ask whether to do browser-sniff at all. These are intent-level. Show them when the discovery matrix says to.
  3. Chrome attach (separate consent if escalation happens) — when the agent actually needs to open or attach to Chrome (because the discovery flow requires a real browser, or because mode: browser_clearance_http means the runtime needs cookie capture), surface that as its own moment so the user knows they may need to solve a challenge or sign in. The user-facing prompts at lines below already disclose Chrome attach as a possibility; that is the right place to confirm. Do not pre-announce Chrome attach when the probe has already settled the runtime as browser_http and the spec is complete enough to skip discovery — there is no Chrome attach to announce in that path.

Two concerns are decided here, separately:

  • Runtime (does the printed CLI need browser-compatible HTTP, a clearance cookie, or live page-context execution?) — settled entirely by probe-reachability.
  • Discovery (does Phase 1.7 need to capture XHR traffic via a real browser to learn endpoints?) — settled by Phase 1.7's normal "When to offer browser-sniff" decision matrix above. Independent of runtime.

The probe runs stdlib HTTP, then Surf with a Chrome TLS fingerprint, and emits one of standard_http | browser_http | browser_clearance_http | unknown. Apply mode to the runtime decision:

  • mode: standard_http — runtime is plain HTTP (the original probe was transient). Continue Phase 1.7 normally; the discovery decision is unchanged.
  • mode: browser_httpruntime is settled: ship Surf transport (UsesBrowserHTTPTransport will be set in the generator's traffic-analysis hints). The printed CLI will not include auth login --chrome for clearance cookies — Surf alone clears the challenge. Continue Phase 1.7's discovery decision normally; the existing "Browser-Sniff as enrichment" / "Browser-Sniff as primary" prompts (above) are framed around endpoint discovery and are correct as-written. Do not add clearance-cookie language to those prompts.
  • mode: browser_clearance_http — both probes hit protection signals. The runtime needs more than Surf (clearance cookie or live page-context execution; the probe cannot distinguish), so a real browser capture is required to find out. Proceed through Phase 1.7's normal browser-sniff offer (intent-level yes/no). The consent for Chrome attach happens at the moment the agent actually opens/attaches, where the user-facing prompts in references/browser-sniff-capture.md already disclose what's about to happen and may ask the user to solve a challenge. Note in the brief that runtime is provisionally browser_clearance_http pending capture results.
  • mode: unknown — probes failed at the transport layer (DNS/timeout/5xx). Fall through to the existing browser-sniff offer; the user decides whether to retry or pivot.

When browser-sniff is approved or pre-approved AND the probe says browser_clearance_http or unknown:

  • Do not offer alternate CLI shapes (RSS-first, official API, docs-only, narrower scope, "try anyway") before a real browser capture has been attempted.
  • Do not write the brief as if browser-sniff is complete after only curl/direct HTTP probes.
  • If browser automation tooling is unavailable, offer the user a manual HAR path before offering any scope pivot.

Only after the browser capture attempt fails by the criteria in references/browser-sniff-capture.md may you ask whether to pivot to RSS, official API, docs-only, or a smaller CLI scope.

Time budget

The browser-sniff gate should complete within 3 minutes of the user approving browser-sniff. If browser automation tooling fails to produce results after 3 minutes of attempts, fall back immediately:

  • If a spec already exists (enrichment mode): "Browser-Sniff failed after 3 minutes — proceeding with existing spec."
  • If no spec exists (primary mode): "Browser-Sniff failed after 3 minutes — falling back to --docs generation."
  • If browser-sniff was approved or pre-approved and direct HTTP showed challenge/bot-protection evidence, do not auto-fall back to docs/official API, even when BROWSER_SNIFF_TARGET_URL is unset. Ask whether the user wants to provide a HAR manually, retry cleared-browser capture, or discuss alternate CLI scope.

Do NOT spend time debugging tool integration issues. Browser-sniff is a temporary discovery aid, not the product runtime. If the first approach fails, fall back to the next option — do not retry the same broken approach.

The time budget applies AFTER the user approves. Do not use it as a reason to skip the gate before asking.

When to offer browser-sniff

Spec found? Research shows gaps? Auth required? Action
Yes Yes — docs or competitors show significantly more endpoints than the spec No MUST offer browser-sniff as enrichment
Yes No — spec appears complete Any Skip silently (write marker with decision: skip-silent)
No Community docs exist (e.g., Public-ESPN-API) No MUST offer browser-sniff OR --docs — present both options so the user decides
No No docs found either No MUST offer browser-sniff as primary discovery
No N/A Yes (login) + AUTH_SESSION_AVAILABLE=true Offer authenticated browser-sniff — the user confirmed a session in Phase 1.6
No N/A Yes (login) + AUTH_SESSION_AVAILABLE=false Skip — fall back to --docs (write marker with decision: skip-silent, reason: login-required-no-session)

Gap detection heuristic: If Phase 1 research found documentation, competitor tools, or community projects that reference significantly more endpoints or features than the resolved spec covers, that's a gap signal. Example: "The Zuplo OpenAPI spec has 42 endpoints, but the Public-ESPN-API docs describe 370+."

When the decision matrix says "Offer browser-sniff", you MUST ask the user via AskUserQuestion. Skipping the question and writing a skip-silent marker is a contract violation — skip-silent is only valid when the matrix says "Skip silently" or one of the Banned Skip Reasons is the only thing holding you back (in which case, you should be asking anyway).

Every browser-sniff approval prompt must make the consent boundary explicit:

  • browser discovery may open or attach to Chrome during generation,
  • it may ask the user to log in or solve a challenge,
  • it may request permission to install or upgrade browser-use/agent-browser if missing,
  • the printed CLI will only ship if discovery finds a replayable surface and will not keep a browser running as normal command transport.

Browser-Sniff as enrichment (spec exists but has gaps)

Present to the user via AskUserQuestion:

"Found a spec with N endpoints, but research shows the live API likely has more (competitors reference M+ features). Want me to use temporary browser discovery on <url> to find replayable endpoints the spec missed? I may open or attach to Chrome during generation, and I will ask before installing or upgrading browser-use/agent-browser."

Options:

  1. Yes — browser-sniff and merge (temporarily open or attach to Chrome during generation, capture traffic, then merge only replayable discovered endpoints with the existing spec. Ask before installing capture tools.)
  2. No — use existing spec (proceed with what we have)

Browser-Sniff as primary (no spec found)

Present to the user via AskUserQuestion. If AUTH_SESSION_AVAILABLE=true, include an authenticated browser-sniff option:

"No OpenAPI spec found for <API>. Want me to browser-sniff <likely-url> to discover the API from live traffic?"

Options:

  1. Yes — authenticated browser-sniff (temporarily open or attach to Chrome during generation, use your browser session to discover public and authenticated traffic, and generate only replayable CLI surfaces. Recommended since you confirmed a session.) (Only show when AUTH_SESSION_AVAILABLE=true)
  2. Yes — browser-sniff the live site (temporarily browse <url> anonymously, capture API/HTML traffic, and generate a spec only from replayable surfaces. Ask before installing capture tools.)
  3. No — use docs instead (attempt --docs generation from documentation pages)
  4. No — I'll provide a spec or HAR (user will supply input manually)

When AUTH_SESSION_AVAILABLE=false, show only options 2-4 (the existing 3-option prompt).

If user approves browser-sniff

Before doing anything else, write the marker entry for this source:

{
  "source_name": "<normalized name from briefing>",
  "decision": "approved",
  "reason": "<which option they picked, e.g., 'authenticated browser-sniff' or 'browser-sniff and merge'>",
  "asked_at": "<current ISO8601 timestamp>"
}

Append it to $PRESS_RUNSTATE/runs/$RUN_ID/browser-browser-sniff-gate.json (create the file if it doesn't exist).

Step 0: Identify the User Goal

Before building the capture plan, answer one question: What does the end user of this CLI actually want to do?

Read the research brief's Top Workflows. The #1 workflow IS the primary browser-sniff goal. State it in one sentence:

  • Domino's: "Order a pizza for delivery"
  • Linear: "Create an issue and assign it to a sprint"
  • Stripe: "Create a payment intent and confirm it"
  • ESPN: "Check today's scores and standings"
  • Notion: "Create a page and organize it in a database"

If the API is read-only (news, weather, data feeds), the primary goal is "fetch and filter data" and the flow is search/filter/paginate rather than a multi-step transaction.

The browser-sniff will walk through this goal as an interactive user flow. Secondary workflows become secondary browser-sniff passes if time permits.

State the goal explicitly before proceeding: "Primary browser-sniff goal: [goal]. I will walk through this as a user flow."

Then read and follow references/browser-sniff-capture.md for the complete browser-sniff implementation: tool detection, installation, session transfer, browser-use/agent-browser/manual HAR capture, replayability analysis, and discovery report writing.

If user declines browser-sniff

Write the marker entry for this source before proceeding:

{
  "source_name": "<normalized name from briefing>",
  "decision": "declined",
  "reason": "<which option they picked, e.g., 'use existing spec' or 'use docs instead'>",
  "asked_at": "<current ISO8601 timestamp>"
}

Append it to $PRESS_RUNSTATE/runs/$RUN_ID/browser-browser-sniff-gate.json.

Proceed with whatever spec source exists. If no spec was found, fall back to --docs or ask the user to provide a spec/HAR manually.

Before leaving Phase 1.7

Every source named in the briefing must have exactly one entry in browser-browser-sniff-gate.json. Before proceeding to Phase 1.8, re-read the marker file and verify the count matches the number of named sources from the briefing. If a source is missing, return to the decision matrix for that source. Phase 1.5 will HALT if this check fails.


Phase 1.8: Crowd-Sniff Gate

After Phase 1.7 (Browser-Sniff Gate), evaluate whether mining community signals (npm SDKs and GitHub code search) would improve the spec. Skip this gate entirely if the user already passed --spec (spec source is already resolved and appears complete).

Time budget: The crowd-sniff gate should complete within 10 minutes. If cli-printing-press crowd-sniff fails or times out, fall back immediately:

  • If a spec already exists: "Crowd-sniff failed — proceeding with existing spec."
  • If no spec exists: "Crowd-sniff failed — falling back to --docs generation."

When to offer crowd-sniff

Spec found? Research shows gaps? Action
Yes Yes — competitors or community projects reference more endpoints Offer crowd-sniff as enrichment
Yes No — spec appears complete Skip silently
No Community SDKs exist on npm Offer crowd-sniff as primary discovery
No No SDKs or code found Skip — fall back to --docs

Crowd-sniff as enrichment (spec exists but has gaps)

Present to the user via AskUserQuestion:

"Found a spec with N endpoints, but research shows the live API likely has more. Want me to search npm packages and GitHub code for <api> to discover additional endpoints? This typically takes 5-10 minutes."

Options:

  1. Yes — crowd-sniff and merge (search npm SDKs and GitHub code, merge discovered endpoints with the existing spec)
  2. No — use existing spec (proceed with what we have)

Crowd-sniff as primary (no spec found)

Present to the user via AskUserQuestion:

"No OpenAPI spec found for <API>. Want me to search npm packages and GitHub code to discover the API from community usage? This typically takes 5-10 minutes."

Options:

  1. Yes — crowd-sniff the community (search npm SDKs and GitHub code, generate a spec from discovered endpoints)
  2. No — use docs instead (attempt --docs generation from documentation pages)
  3. No — I'll provide a spec or HAR (user will supply input manually)

If user approves crowd-sniff

Read and follow references/crowd-sniff.md for the crowd-sniff command, provenance capture, and discovery report writing.

If user declines crowd-sniff

Proceed with whatever spec source exists. If no spec was found, fall back to --docs or ask the user to provide a spec/HAR manually.


Phase 1.5: Ecosystem Absorb Gate

THIS IS A MANDATORY STOP GATE. Do not generate until this is complete and approved.

Pre-flight check: browser-sniff-gate marker

Before any absorb work, verify $PRESS_RUNSTATE/runs/$RUN_ID/browser-browser-sniff-gate.json exists and contains an entry for every source named in the briefing.

If the file is missing: HARD STOP. Print:

Phase 1.7 Browser-Sniff Gate did not record a decision. Return to Phase 1.7 and evaluate the browser-sniff gate for every source named in the briefing.

Do not proceed to Step 1.5a until the file exists.

If the file exists but is missing an entry for a named source: HARD STOP. Print:

Browser-Sniff Gate missing decision for source <name>. Return to Phase 1.7 and evaluate the decision matrix for that source.

Do not proceed until every briefing source has a marker entry.

Resume leniency: If the run was started by an older version of the skill that didn't write markers, warn and continue — do not hard-fail on legacy resumes. Distinguish by checking whether state.json predates the marker contract (the marker file didn't exist before 2026-04-11). New runs always hard-fail on a missing marker.

Pre-check (existing): If no spec or HAR file has been resolved by this point and Phase 1.7 (Browser-Sniff Gate) was not evaluated, STOP. Go back and run the browser-sniff gate decision matrix. The absorb manifest depends on knowing the API surface, which requires a spec.

The GOAT CLI doesn't "find gaps." It absorbs EVERY feature from EVERY tool and then transcends with compound use cases nobody thought of. This phase builds the absorb manifest.

Step 1.5a: Search for every tool that touches this API

Run these searches in parallel:

  1. WebSearch: "<API name>" Claude Code plugin site:github.com
  2. WebSearch: "<API name>" MCP server model context protocol
  3. WebSearch: "<API name>" Claude skill SKILL.md site:github.com
  4. WebSearch: "<API name>" CLI tool site:github.com (competing CLIs)
  5. WebSearch: "<API name>" CLI site:npmjs.com (npm packages)
  6. Raw fetch: Check github.com/anthropics/claude-plugins-official/tree/main/external_plugins for official plugins with the helper from references/fetch-docs.md, or with gh api when it can return the file/listing directly.
  7. WebSearch: "<API name>" MCP site:lobehub.com OR site:mcpmarket.com OR site:fastmcp.me
  8. WebSearch: "<API name>" automation script workflow site:github.com
  9. WebSearch: "<API name>" SDK wrapper site:npmjs.com
  10. WebSearch: "<API name>" client library site:pypi.org

Step 1.5a.5: Read MCP source code (if found)

If step 1.5a discovered MCP server repos with public source code on GitHub, read the actual source to extract ground-truth API usage — not just README feature descriptions.

Time budget: Max 3 minutes total. If extraction is unproductive, fall back to README-only research.

For the top 1-2 MCP repos found:

  1. Identify the main source file. Use gh api, raw GitHub URLs, or the helper from references/fetch-docs.md to inspect the repo tree and source files without a summarization layer. Find the entry point — typically src/index.ts, server.ts, server.py, main.go, or a tools/ directory. MCP servers are usually small (one main file + tool definitions).

  2. Extract three things:

    • API endpoint paths: Look for HTTP client calls (fetch(, axios., requests., http.Get, client.) and extract the URL paths (e.g., GET /v1/issues, POST /graphql). These are the endpoints the MCP maintainer proved work.
    • Auth patterns: Look for how the MCP constructs auth headers — token format (Bearer, Bot, Basic), header name (Authorization, X-API-Key), environment variable names. This informs our auth setup guidance.
    • Response field selections: Look for which fields are extracted from API responses — these are the high-gravity fields that power users actually need.
  3. Feed into absorb manifest. In step 1.5b, endpoints extracted from source get attributed as <MCP name> (source) in the "Best Source" column, distinguishing them from README-derived features. Source-extracted endpoints are high-confidence signals — the maintainer verified they work.

  4. Feed auth patterns into research brief. If the MCP source reveals token format (e.g., xoxp- for Slack, sk_live_ for Stripe), credential setup steps, or required scopes, note them in the Phase 1 brief's auth section. These hints improve the generated CLI's auth onboarding.

Skip this step when:

  • No MCP repos were found in 1.5a
  • MCP repos are private or archived
  • The MCP is a monorepo where the relevant server is hard to locate within 3 minutes

Step 1.5a.6: DeepWiki Codebase Analysis (if GitHub repos found)

If Phase 1 or Step 1.5a discovered GitHub repos for the API (SDK repos, server repos, MCP server repos), query DeepWiki for a semantic understanding of how the API works - architecture, auth flows, data models, error handling. This complements crowd-sniff (endpoints) and MCP source reading (auth headers) with "how things actually work" context.

Time budget: 2 minutes max. If DeepWiki is slow or unavailable, skip silently.

Run in parallel with Steps 1.5a through 1.5a.5 when possible. DeepWiki queries do not depend on MCP source reading results.

Read and follow references/deepwiki-research.md for the query procedure: wiki structure fetch, targeted section extraction (auth, data model, architecture), and synthesis into the research brief and absorb manifest.

Skip this step when:

  • No GitHub repos were discovered during Phase 1 or Step 1.5a
  • The API is trivially simple (1-2 endpoints, no auth)

Step 1.5b: Catalog every feature into the absorb manifest

For EACH tool found, list EVERY feature/tool/command it provides. Then define how our CLI matches AND beats it:

## Absorb Manifest

### Absorbed (match or beat everything that exists)
| # | Feature | Best Source | Our Implementation | Added Value |
|---|---------|-----------|-------------------|-------------|
| 1 | Search issues by text | Linear MCP search_issues | <api>-pp-cli search | Works offline, regex, SQL composable |
| 2 | Create issue | Linear MCP create_issue | <api>-pp-cli issue create --stdin --dry-run | Agent-native, scriptable, idempotent |
| 3 | Sprint board view | jira-cli sprint view | <api>-pp-cli sprint view | Historical velocity, offline |

Every row = a feature we MUST build. No exceptions. If someone else has it, we have it AND it works offline, with --json, --dry-run, typed exit codes, and SQLite persistence.

SDK wrapper methods should be treated as features to absorb — each public method/function is a feature the CLI should match.

Our Implementation must start with a parseable disposition. Use one of these prefixes so Phase 3 can verify the row mechanically:

  • <api>-pp-cli <clean command path> for a promoted or hand-built Cobra command path that must resolve via <binary> <path> --help.
  • (generated endpoint) <resource> <endpoint> for generator-emitted typed endpoint commands that retain the upstream resource shape and are covered by the generated endpoint surface.
  • (behavior in <api>-pp-cli <command path>) ... for features implemented as flags, modes, output shapes, or store behavior inside another command. The named command path still must resolve; the prose after the closing parenthesis explains the behavior to verify later.
  • (stub) ... only for explicitly approved stubs per the rule below.

Do not leave Our Implementation as freeform prose like FTS5 offline search or SQLite-backed sprint query. If the row maps to a clean user-facing command, put that command path first. If it does not, choose the explicit disposition that explains why Phase 3 should not treat the whole cell as a new command path.

Stubs must be explicit. If any row in the manifest will ship as a stub (placeholder implementation that emits "not yet wired" / "wip" messaging), start Our Implementation with (stub) plus a one-line reason why the full implementation is deferred (e.g., "(stub - requires paid API)", "(stub - requires headless Chrome)"). If the manifest also has a Status column, set that value to (stub) too, but the Our Implementation prefix is the Phase 3 gate's source of truth. Do NOT quietly ship stubs for features the user approved as shipping scope.

The Phase Gate 1.5 prose showcase (below) MUST read out stub items separately so the user explicitly approves the stub list. After approval, Phase 3 builds shipping-scope features fully and stubs with honest messaging; no mid-build downgrade from shipping-scope to stub is permitted. If an agent discovers during Phase 3 that a shipping-scope feature cannot be implemented in-session, they must return to Phase 1.5 with a revised manifest — not unilaterally downgrade to a stub.

Step 1.5c: Identify transcendence features

Start with the users, not the technology. The best features come from understanding who uses this service, what their rituals are, and what questions they can't answer today. "What can SQLite do?" is the wrong question. "What would make a power user say 'I need this'?" is the right one.

The actual brainstorming runs as a Task subagent in Step 1.5c.5 below — customer model → 2× candidates → adversarial cut. Step 1.5c is the motivation; do not generate transcendence features inline here.

The transcendence table in the manifest (Step 1.5d) renders rows in this shape, which mirrors the subagent's ### Survivors output. The Buildability column tags each row spec-emits or hand-code per [references/novel-fea

Content truncated for page performance. Open the source repository for the full SKILL.md file.

Install via CLI
npx skills add https://github.com/mvanhorn/cli-printing-press --skill printing-press
Repository Details
star Stars 3,512
call_split Forks 372
navigation Branch main
article Path SKILL.md
More from Creator