[object Object]

← back to Cli Printing Press

feat(skills): offer publish after CLI generation completes (#57)

4a1bd2892ce3576b6c0a9b116797903b6413d28c · 2026-03-29 15:25:44 -0700 · Trevin Chow

* feat(skills): offer publish after CLI generation completes

Add Phase 6 to the /printing-press skill that offers to publish the CLI
after shipcheck passes. Also teach the publish skill to detect existing
open PRs and update them via force-push + gh pr edit, so re-generating
and re-publishing a CLI updates the existing PR instead of failing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): remove old CLI files before copying new package

When re-publishing a CLI (especially after a previous PR was merged),
cp -r overlays the new package on top of old files in the managed clone.
Stale files from the previous generation persist. Use rm -rf with a glob
before copying to ensure a clean replacement. The glob library/*/<cli-name>
also handles category changes between publishes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): scope existing-PR check to current user with --author @me

Without this filter, the publish skill would match and overwrite someone
else's open PR if they published the same CLI name. Adding --author @me
to both the publish skill's Step 7 and the main skill's Phase 6 PR check
ensures we only detect and update our own PRs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): use final shipcheck verdict for publish gate

* fix(skills): archive manuscripts before publish offer

The publish package command looks for manuscripts at
~/printing-press/manuscripts/<api>/, but the main skill writes them to
$PRESS_RUNSTATE/runs/$RUN_ID/. Without archiving first, publish package
finds no manuscripts (or stale ones from a previous run) and the PR
ships without the research brief and shipcheck results.

Add an archive step in Phase 6 that copies research/ and proofs/ from
runstate to the manuscripts directory before offering to publish.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(cli): verify publish package includes manuscripts in staged output

Add TestPublishPackageIncludesManuscripts which creates manuscripts at
the archived location (~/printing-press/manuscripts/<api>/<run-id>/),
runs publish package, and verifies both research/brief.md and
proofs/shipcheck.md end up in the staged package under .manuscripts/.
Also checks ManuscriptsIncluded and RunID in the JSON result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4a1bd2892ce3576b6c0a9b116797903b6413d28c
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sun Mar 29 15:25:44 2026 -0700

    feat(skills): offer publish after CLI generation completes (#57)
    
    * feat(skills): offer publish after CLI generation completes
    
    Add Phase 6 to the /printing-press skill that offers to publish the CLI
    after shipcheck passes. Also teach the publish skill to detect existing
    open PRs and update them via force-push + gh pr edit, so re-generating
    and re-publishing a CLI updates the existing PR instead of failing.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(skills): remove old CLI files before copying new package
    
    When re-publishing a CLI (especially after a previous PR was merged),
    cp -r overlays the new package on top of old files in the managed clone.
    Stale files from the previous generation persist. Use rm -rf with a glob
    before copying to ensure a clean replacement. The glob library/*/<cli-name>
    also handles category changes between publishes.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(skills): scope existing-PR check to current user with --author @me
    
    Without this filter, the publish skill would match and overwrite someone
    else's open PR if they published the same CLI name. Adding --author @me
    to both the publish skill's Step 7 and the main skill's Phase 6 PR check
    ensures we only detect and update our own PRs.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(skills): use final shipcheck verdict for publish gate
    
    * fix(skills): archive manuscripts before publish offer
    
    The publish package command looks for manuscripts at
    ~/printing-press/manuscripts/<api>/, but the main skill writes them to
    $PRESS_RUNSTATE/runs/$RUN_ID/. Without archiving first, publish package
    finds no manuscripts (or stale ones from a previous run) and the PR
    ships without the research brief and shipcheck results.
    
    Add an archive step in Phase 6 that copies research/ and proofs/ from
    runstate to the manuscripts directory before offering to publish.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * test(cli): verify publish package includes manuscripts in staged output
    
    Add TestPublishPackageIncludesManuscripts which creates manuscripts at
    the archived location (~/printing-press/manuscripts/<api>/<run-id>/),
    runs publish package, and verifies both research/brief.md and
    proofs/shipcheck.md end up in the staged package under .manuscripts/.
    Also checks ManuscriptsIncluded and RunID in the JSON result.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 ...6-03-29-002-feat-post-gen-publish-offer-plan.md | 192 +++++++++++++++++++++
 internal/cli/publish_test.go                       |  37 ++++
 skills/printing-press-publish/SKILL.md             |  74 ++++++--
 skills/printing-press/SKILL.md                     |  60 +++++++
 4 files changed, 353 insertions(+), 10 deletions(-)

diff --git a/docs/plans/2026-03-29-002-feat-post-gen-publish-offer-plan.md b/docs/plans/2026-03-29-002-feat-post-gen-publish-offer-plan.md
new file mode 100644
index 00000000..00b5f1f2
--- /dev/null
+++ b/docs/plans/2026-03-29-002-feat-post-gen-publish-offer-plan.md
@@ -0,0 +1,192 @@
+---
+title: "feat: Offer publish after CLI generation completes"
+type: feat
+status: completed
+date: 2026-03-29
+---
+
+# Offer Publish After CLI Generation Completes
+
+## Overview
+
+After the `/printing-press` skill finishes generating a CLI (Phase 4 shipcheck or Phase 5 live smoke), offer the user the option to publish it via `/printing-press publish`. Also teach the publish skill to detect existing open PRs so re-generating and re-publishing a CLI updates the existing PR rather than failing on `gh pr create`.
+
+## Problem Frame
+
+PR #54 added a full publish workflow (`/printing-press publish`) that validates, packages, and opens a PR against `printing-press-library`. But the main generation skill (`/printing-press`) never mentions or offers it. Users who just generated a CLI have no prompt to publish.
+
+Worse, if someone re-generates a CLI they previously published (e.g., after emboss or spec update), the publish skill will hit an existing branch `feat/<cli-name>` and — if they overwrite the branch — `gh pr create` fails because a PR already exists for that head ref. There's no path to update an existing PR.
+
+## Requirements Trace
+
+- R1. After a CLI passes shipcheck with "ship" or "ship-with-gaps", the main skill offers to publish
+- R2. The offer is optional — never auto-publish
+- R3. If the user accepts, invoke `/printing-press publish <cli-name>`
+- R4. If the user declines, the skill ends as it does today
+- R5. The offer appears after Phase 5 (live smoke) when it runs, since that's the final phase
+- R6. The publish skill must detect existing open PRs for the same CLI and offer to update them
+- R7. When updating an existing PR: force-push the new content to the same branch and update the PR description
+- R8. The main skill's offer should surface whether an existing PR exists, so the user has context
+
+## Scope Boundaries
+
+- Skill changes only — no Go binary changes needed
+- No changes to the `fullrun` pipeline (it calls `PublishWorkingCLI` programmatically, which publishes to the local library, not to GitHub)
+- No changes to emboss mode (emboss improves an existing CLI; publishing is a separate step)
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `skills/printing-press/SKILL.md` — Phase 4 ends with ship/ship-with-gaps/hold. Phase 5 is optional live smoke. Neither mentions publish.
+- `skills/printing-press-publish/SKILL.md` — Step 7 checks for existing *branches* but not existing *PRs*. If a branch exists, it asks overwrite vs timestamp. But `gh pr create` will fail if a PR already exists for that branch.
+- `gh pr list --repo <repo> --head <branch> --state open --json number,url` — detects existing open PRs for a branch. This is the missing check.
+- `gh pr edit <number> --body <body>` — updates an existing PR description without creating a new one.
+
+### Institutional Learnings
+
+- Published CLIs live at `~/printing-press/library/<api>-pp-cli/` (see `docs/solutions/best-practices/checkout-scoped-printing-press-output-layout-2026-03-28.md`)
+- Validation must not mutate source directories (see `docs/solutions/best-practices/validation-must-not-mutate-source-directory-2026-03-29.md`)
+
+## Key Technical Decisions
+
+- **Offer placement:** After the final phase (Phase 4 or 5), not embedded within shipcheck. Rationale: publish is a post-generation handoff, not quality verification.
+- **Use `/printing-press publish` skill, not CLI commands directly:** The publish skill handles interactive decisions (category, branch conflicts, PR description). The main skill just invokes it with the CLI name.
+- **Gate on ship verdict:** Only offer on "ship" or "ship-with-gaps". A "hold" means the CLI isn't ready.
+- **Detect existing PRs in the publish skill, not the main skill:** The publish skill already manages the clone and branch operations. Adding PR detection there keeps the logic centralized. The main skill just runs a lightweight `gh pr list` check to surface context in the offer text.
+- **Update existing PR via force-push + `gh pr edit`:** When an open PR exists for `feat/<cli-name>`, overwrite the branch (as the skill already supports), force-push, then use `gh pr edit` to refresh the description. This avoids creating duplicate PRs and preserves any review comments on the existing PR.
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Should the main skill check for existing PRs?** Yes — a lightweight `gh pr list` check lets it say "update PR #42" vs "publish new" in the offer text. The check is fast (one API call) and gives the user important context before they commit.
+- **What about merged PRs?** If a previous PR was merged (the CLI is already in the library), the user is publishing an update. The publish skill should treat this the same as a fresh publish — create a new branch and PR. The `--state open` filter on `gh pr list` naturally excludes merged PRs.
+- **What if `gh` isn't authenticated when checking from the main skill?** Skip the PR check silently and show the generic offer. The publish skill will catch the auth issue in its own Step 1.
+
+### Deferred to Implementation
+
+- Exact wording of `AskUserQuestion` prompts — best tuned during implementation
+
+## Implementation Units
+
+- [x] **Unit 1: Teach the publish skill to detect and update existing PRs**
+
+**Goal:** When an open PR already exists for `feat/<cli-name>`, update it (force-push + edit description) instead of failing on `gh pr create`.
+
+**Requirements:** R6, R7
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `skills/printing-press-publish/SKILL.md`
+
+**Approach:**
+Add a new step between the current Step 6 (Managed Clone) and Step 7 (Branch, Commit, PR). This step:
+
+1. After freshening the clone (Step 6), check for an existing open PR:
+   ```
+   gh pr list --repo mvanhorn/printing-press-library --head "feat/<cli-name>" --state open --json number,title,url
+   ```
+2. Store the result — either `EXISTING_PR_NUMBER` and `EXISTING_PR_URL`, or empty if no PR exists.
+3. In Step 7, branch the flow:
+   - **Existing PR found:** Always overwrite the branch (skip the "overwrite vs timestamp" question — the intent is clearly to update). After commit and force-push, run `gh pr edit <number> --body "<new description>"` to refresh the PR description. Display "Updated PR #N: <url>".
+   - **No existing PR:** Current flow unchanged — ask about branch conflicts if any, then `gh pr create`.
+
+The existing branch-conflict question ("overwrite vs timestamp") should still apply when there's an existing *branch* but *no* open PR (e.g., a previous PR was closed or merged). In that case, the user might want a fresh branch.
+
+**Patterns to follow:**
+- The existing Step 7 branch-conflict detection pattern
+- The `gh pr create` call at the end of Step 7 — the `gh pr edit` call mirrors its structure
+
+**Test scenarios:**
+- Happy path (fresh): No existing branch or PR -> standard flow, `gh pr create`
+- Happy path (update): Existing open PR #42 on `feat/notion-pp-cli` -> branch overwritten automatically, force-push, `gh pr edit #42`, displays "Updated PR #42"
+- Edge case: Existing branch but no open PR (PR was merged or closed) -> asks overwrite vs timestamp as before, then `gh pr create` for the new PR
+- Edge case: Existing branch AND closed PR -> treated as no-open-PR (the closed PR is irrelevant), standard branch-conflict question
+- Error path: `gh pr list` fails (network, auth) -> fall back to current behavior (branch-conflict question, `gh pr create`)
+
+**Verification:**
+- The publish skill has a PR detection step before Step 7
+- When an existing open PR is found, it force-pushes and runs `gh pr edit` instead of `gh pr create`
+- When no open PR exists, the current flow is unchanged
+
+---
+
+- [x] **Unit 2: Add Phase 6 (Publish Offer) to the main skill**
+
+**Goal:** After the final phase of generation, offer to publish with context about any existing PR.
+
+**Requirements:** R1, R2, R3, R4, R5, R8
+
+**Dependencies:** Unit 1 (publish skill must handle existing PRs before the main skill starts routing users to it for updates)
+
+**Files:**
+- Modify: `skills/printing-press/SKILL.md`
+
+**Approach:**
+Add a `## Phase 6: Publish` section after Phase 5. The section should:
+
+1. **Gate on verdict:** If the shipcheck verdict from Phase 4 was "hold", skip this phase entirely. Only proceed for "ship" or "ship-with-gaps".
+
+2. **Check for existing PR** (lightweight, may fail silently):
+   ```
+   gh pr list --repo mvanhorn/printing-press-library --head "feat/<cli-name>" --state open --json number,url --jq '.[0]'
+   ```
+   If `gh` isn't authenticated or the call fails, continue without PR context.
+
+3. **Present the offer via `AskUserQuestion`:**
+
+   - If an existing open PR was found:
+     > "There's an open publish PR for <cli-name> (#N). Want to update it with this regenerated version?"
+     > 1. **Yes — update PR #N** (re-validate, re-package, and push to the existing PR)
+     > 2. **No — I'm done**
+
+   - If no existing PR:
+     > "<cli-name> passed shipcheck. Want to publish it to the printing-press-library?"
+     > 1. **Yes — publish now** (validate, package, and open a PR)
+     > 2. **No — I'm done**
+
+   - If verdict was "ship-with-gaps", prepend a note: "Note: shipcheck found minor gaps (see shipcheck report above)."
+
+4. **If accepted:** Invoke `/printing-press publish <cli-name>`. The publish skill handles everything from there — name resolution, category, validation, packaging, git ops, and PR creation/update.
+
+5. **If declined:** End normally.
+
+**Patterns to follow:**
+- Phase 1.5's `AskUserQuestion` gate pattern
+- The "ship/ship-with-gaps/hold" verdict from Phase 4
+
+**Test scenarios:**
+- Happy path (fresh publish): "ship" verdict, no existing PR -> offer "publish now" -> user accepts -> `/printing-press publish <cli-name>` invoked
+- Happy path (update): "ship" verdict, existing open PR #42 -> offer "update PR #42" -> user accepts -> `/printing-press publish <cli-name>` invoked (publish skill handles the update)
+- Ship-with-gaps: offer includes gap note, user accepts -> publish invoked
+- Decline: user says no -> skill ends normally
+- Hold verdict: no offer shown, skill ends at Phase 4/5
+- After live smoke: Phase 5 runs -> publish offer appears after Phase 5
+- `gh` not available: PR check fails silently -> generic "publish now" offer shown (publish skill catches auth in its own Step 1)
+
+**Verification:**
+- The main skill has a Phase 6 section with `AskUserQuestion`
+- The offer text varies based on whether an existing PR was found
+- "hold" verdict skips the phase entirely
+- The phase invokes `/printing-press publish <cli-name>`, not CLI commands directly
+
+## System-Wide Impact
+
+- **Interaction graph:** The main skill now has a dependency on the publish skill at the end of its flow. The publish skill remains independently invocable.
+- **Unchanged invariants:** The publish skill's standalone invocation (`/printing-press publish`) works identically. The main skill's Phases 0-5 are untouched. The Go binary is not modified.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| `gh` not authenticated during Phase 6 PR check | Fail silently, show generic offer. Publish skill catches auth in Step 1. |
+| User runs `/printing-press publish` standalone after this change | Still works — Unit 1's PR detection applies regardless of how the skill is invoked |
+| Existing PR has review comments the user wants to preserve | Force-push + `gh pr edit` preserves the PR thread. Only the description and branch content change. |
+
+## Sources & References
+
+- Related PRs: #54 (publish skill)
+- Publish skill: `skills/printing-press-publish/SKILL.md`
+- Main skill: `skills/printing-press/SKILL.md`
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 878615ee..2ff0e945 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -306,6 +306,43 @@ func TestPublishPackageFailsWhenManuscriptsCopyFails(t *testing.T) {
 	assert.ErrorIs(t, statErr, os.ErrNotExist, "failed packaging should clean up the staging target")
 }
 
+func TestPublishPackageIncludesManuscripts(t *testing.T) {
+	home := setLibraryTestEnv(t)
+	cliDir := filepath.Join(home, "library", "test-pp-cli")
+	writePublishableTestCLI(t, cliDir)
+
+	// Create manuscripts at the archived location where publish package looks
+	runID := "20260329-100000"
+	researchDir := filepath.Join(home, "manuscripts", "test", runID, "research")
+	proofsDir := filepath.Join(home, "manuscripts", "test", runID, "proofs")
+	require.NoError(t, os.MkdirAll(researchDir, 0o755))
+	require.NoError(t, os.MkdirAll(proofsDir, 0o755))
+	require.NoError(t, os.WriteFile(filepath.Join(researchDir, "brief.md"), []byte("# Research Brief"), 0o644))
+	require.NoError(t, os.WriteFile(filepath.Join(proofsDir, "shipcheck.md"), []byte("# Shipcheck"), 0o644))
+
+	target := filepath.Join(t.TempDir(), "staging")
+	cmd := newPublishCmd()
+	cmd.SetArgs([]string{"package", "--dir", cliDir, "--category", "other", "--target", target, "--json"})
+
+	output, err := runWithCapturedStdout(t, cmd.Execute)
+	require.NoError(t, err)
+
+	var result PackageResult
+	require.NoError(t, json.Unmarshal([]byte(output), &result))
+	assert.True(t, result.ManuscriptsIncluded, "manuscripts should be included")
+	assert.Equal(t, runID, result.RunID, "run ID should match the most recent run")
+
+	// Verify manuscripts are in the staged package
+	stagedResearch := filepath.Join(result.StagedDir, ".manuscripts", runID, "research", "brief.md")
+	stagedProofs := filepath.Join(result.StagedDir, ".manuscripts", runID, "proofs", "shipcheck.md")
+
+	_, err = os.Stat(stagedResearch)
+	assert.NoError(t, err, "research brief should be in staged package")
+
+	_, err = os.Stat(stagedProofs)
+	assert.NoError(t, err, "shipcheck proofs should be in staged package")
+}
+
 func TestFindMostRecentRun(t *testing.T) {
 	dir := t.TempDir()
 
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index bd8f5f9e..c92a1d54 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -234,10 +234,39 @@ If there are uncommitted changes, ask the user via AskUserQuestion:
 
 If reset, run `git checkout -- . && git clean -fd`.
 
-## Step 7: Branch, Commit, and PR
+## Step 7: Check for Existing PR
+
+Before creating a branch, check whether you have an open PR for this CLI. The `--author @me` filter ensures we only match PRs owned by the current user — if someone else published the same CLI name, we won't stomp their PR.
+
+```bash
+gh pr list --repo mvanhorn/printing-press-library --head "feat/<cli-name>" --state open --author @me --json number,title,url
+```
+
+Parse the result:
+- If the list is non-empty, store `EXISTING_PR_NUMBER` and `EXISTING_PR_URL` from the first entry
+- If the list is empty or the command fails (network, auth), set `EXISTING_PR_NUMBER=""` — proceed as if no PR exists
+
+If an existing open PR was found, inform the user:
+> "Found your open PR #N for `<cli-name>`. Will update it with the new version."
+
+This determines the flow in Step 8:
+- **Existing open PR:** Overwrite the branch automatically, force-push, update the PR description
+- **No open PR:** Standard flow — ask about branch conflicts if any, create a new PR
+
+## Step 8: Branch, Commit, and PR
 
 ### Create branch
 
+**If `EXISTING_PR_NUMBER` is set** (updating an existing PR):
+
+Always overwrite the branch — the intent is clearly to update:
+
+```bash
+git checkout -B feat/<cli-name>
+```
+
+**If `EXISTING_PR_NUMBER` is empty** (no open PR):
+
 Check for an existing branch:
 
 ```bash
@@ -245,11 +274,11 @@ git branch --list "feat/<cli-name>"
 git ls-remote --heads origin "feat/<cli-name>"
 ```
 
-If exists, ask via AskUserQuestion:
+If a branch exists (from a previously closed or merged PR), ask via AskUserQuestion:
 - "Overwrite existing branch"
 - "Create timestamped variant (feat/<cli-name>-YYYYMMDD)"
 
-Create the branch (use `-B` to force-create when overwriting an existing branch):
+Create the branch:
 
 ```bash
 # New branch:
@@ -259,9 +288,12 @@ git checkout -b feat/<cli-name>
 git checkout -B feat/<cli-name>
 ```
 
-### Copy staged package
+### Replace CLI package
+
+Remove any existing version of this CLI before copying. This prevents stale files from a previous generation persisting (e.g., deleted commands, renamed files). The glob also handles category changes — if the CLI moved from `productivity` to `developer-tools`, the old directory is cleaned up.
 
 ```bash
+rm -rf "$PUBLISH_REPO_DIR/library"/*/"<cli-name>"
 cp -r <staging-dir>/library/* "$PUBLISH_REPO_DIR/library/"
 ```
 
@@ -288,16 +320,27 @@ Write back with `jq` or via the Write tool.
 cd "$PUBLISH_REPO_DIR"
 git add library/ registry.json
 git commit -m "feat(<api-name>): add <cli-name>"
-git push -u origin feat/<cli-name>
 ```
 
-If you chose "Overwrite existing branch" earlier, replace the push command with:
+**If updating an existing PR** (`EXISTING_PR_NUMBER` is set):
 
 ```bash
 git push --force-with-lease -u origin feat/<cli-name>
 ```
 
-### Create PR
+**If creating a new PR** and you chose "Overwrite existing branch" earlier:
+
+```bash
+git push --force-with-lease -u origin feat/<cli-name>
+```
+
+**Otherwise** (new branch, no conflicts):
+
+```bash
+git push -u origin feat/<cli-name>
+```
+
+### Create or update PR
 
 Build the PR description from:
 - The manifest (`description`, `api_name`, `category`, `printing_press_version`, `spec_url`)
@@ -350,7 +393,17 @@ $ <cli-name> --help
 <List any missing manifest fields, or omit this section if everything is present>
 ```
 
-Create the PR:
+**If updating an existing PR** (`EXISTING_PR_NUMBER` is set):
+
+```bash
+gh pr edit "$EXISTING_PR_NUMBER" \
+  --repo mvanhorn/printing-press-library \
+  --body "<constructed PR body>"
+```
+
+Display: "Updated PR #N: <EXISTING_PR_URL>"
+
+**If creating a new PR:**
 
 ```bash
 gh pr create \
@@ -367,6 +420,7 @@ Display the PR URL prominently.
 - **CLI not found:** Show available CLIs in Step 2, let user pick
 - **Validation fails:** Show per-check results in Step 4, stop
 - **Repo unreachable:** Report clearly in Step 6
-- **Branch conflict:** Ask user in Step 7 (overwrite or timestamp)
+- **Existing PR check fails:** Fall back to standard branch-conflict flow (treat as no existing PR)
+- **Branch conflict (no existing PR):** Ask user in Step 8 (overwrite or timestamp)
 - **Push fails:** Report the error, suggest checking `gh auth status`
-- **Staging cleanup:** If any step after packaging (Steps 6-7) fails, remove the staging directory created in Step 5 before stopping. This prevents accumulation of full CLI copies in temp directories across retries
+- **Staging cleanup:** If any step after packaging (Steps 6-8) fails, remove the staging directory created in Step 5 before stopping. This prevents accumulation of full CLI copies in temp directories across retries
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 00c87902..fd391a06 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -911,6 +911,66 @@ Write:
 
 `$PROOFS_DIR/<stamp>-fix-<api>-pp-cli-live-smoke.md`
 
+## Phase 6: Publish
+
+After the final phase completes (Phase 4 if no live smoke, Phase 5 if it ran), archive the run artifacts and offer to publish the CLI to the library repo.
+
+### Gate
+
+Use the most recent shipcheck verdict:
+- if Phase 5 reran shipcheck after a live-smoke fix, use that rerun verdict
+- otherwise use the Phase 4 verdict
+
+Skip this phase entirely if the final shipcheck verdict is `hold`. Only proceed for `ship` or `ship-with-gaps`.
+
+### Archive manuscripts
+
+The run's research and proofs are in `$API_RUN_DIR/` (runstate). The `publish package` command looks for them at `$PRESS_MANUSCRIPTS/<api>/<run-id>/`. Archive them now so they're available whether the user publishes immediately or later.
+
+```bash
+mkdir -p "$PRESS_MANUSCRIPTS/<api>/$RUN_ID"
+cp -r "$RESEARCH_DIR" "$PRESS_MANUSCRIPTS/<api>/$RUN_ID/research" 2>/dev/null || true
+cp -r "$PROOFS_DIR" "$PRESS_MANUSCRIPTS/<api>/$RUN_ID/proofs" 2>/dev/null || true
+```
+
+### Check for existing PR
+
+Run a lightweight check for your own open publish PR. The `--author @me` filter avoids matching someone else's PR for the same CLI name.
+
+```bash
+gh pr list --repo mvanhorn/printing-press-library --head "feat/<cli-name>" --state open --author @me --json number,url --jq '.[0]' 2>/dev/null
+```
+
+If this fails (gh not authenticated, network error, etc.), continue without PR context — the publish skill will handle auth in its own Step 1.
+
+### Offer
+
+Present via `AskUserQuestion`:
+
+**If an existing open PR was found:**
+
+> "<cli-name> passed shipcheck. There's an open publish PR (#N). Want to update it with this version?"
+>
+> 1. **Yes — update PR #N** (re-validate, re-package, and push to the existing PR)
+> 2. **No — I'm done**
+
+**If no existing PR:**
+
+> "<cli-name> passed shipcheck. Want to publish it to the printing-press-library?"
+>
+> 1. **Yes — publish now** (validate, package, and open a PR)
+> 2. **No — I'm done**
+
+If the verdict was `ship-with-gaps`, prepend: "Note: shipcheck found minor gaps (see the shipcheck report above)."
+
+### If accepted
+
+Invoke `/printing-press publish <cli-name>`. The publish skill handles everything from there — name resolution, category, validation, packaging, git ops, and PR creation or update.
+
+### If declined
+
+End normally. The CLI is in `$PRESS_LIBRARY/<api>-pp-cli` and the user can run `/printing-press publish` later.
+
 ## Fast Guidance
 
 ### When to use `printing-press print`

← 590a07b1 feat(skills): show existing CLI context and clarify regenera  ·  back to Cli Printing Press  ·  docs(cli): add "Why These CLIs Win" section to README (#59) f27c735e →