/printing-press-retro Result: A scrubbed retro document and, when the issue gate passes, GitHub issues for Printing Press defects only. Next consumer: A maintainer or agent who will change the Printing Press (generator, scorer, skills, or binary) — not the printed CLI that just shipped. Done: The manuscript proof exists and is scrubbed; every work unit is file-new, comment-on-existing, or local-only; no unredacted secret or PII remains; the user has the outcome list. Intent: "Raised the floor" is analysis language, not a filing bar. File only when the Printing Press must change to stop a curr…

; then\n printf '%s' \"$response\"\n else\n return 1\n fi\n}\n\n# Upload retro document (raw .md — viewable directly in browser)\nif RETRO_DOC_URL=$(catbox_upload \"$RETRO_PROOF_PATH\"); then\n echo \"Retro document uploaded: $RETRO_DOC_URL\"\nelse\n echo \"WARNING: Failed to upload retro document to catbox.moe.\"\n RETRO_DOC_URL=\"\"\n UPLOAD_FAILED=true\nfi\n\n# Upload manuscripts zip\nif MANUSCRIPTS_URL=$(catbox_upload \"$MANUSCRIPTS_ZIP\"); then\n echo \"Manuscripts uploaded: $MANUSCRIPTS_URL\"\nelse\n echo \"WARNING: Failed to upload manuscripts to catbox.moe.\"\n MANUSCRIPTS_URL=\"\"\n UPLOAD_FAILED=true\nfi\n\n# Upload CLI source (only if it was packaged)\nif [ -n \"$CLI_SOURCE_ZIP\" ] && [ -f \"$CLI_SOURCE_ZIP\" ]; then\n if CLI_SOURCE_URL=$(catbox_upload \"$CLI_SOURCE_ZIP\"); then\n echo \"CLI source uploaded: $CLI_SOURCE_URL\"\n else\n echo \"WARNING: Failed to upload CLI source to catbox.moe.\"\n CLI_SOURCE_URL=\"\"\n UPLOAD_FAILED=true\n fi\nelse\n echo \"No CLI source to upload (manuscripts-only mode).\"\nfi\n\nfi\n```\n\n## Step 6: Handle upload failure (R15)\n\nIf either upload failed:\n\n```bash\nif [ \"$UPLOAD_FAILED\" = true ]; then\n # Preserve local zips for manual attachment\n LOCAL_ZIP_DIR=\"$RUN_DIR/proofs\"\n cp \"$MANUSCRIPTS_ZIP\" \"$LOCAL_ZIP_DIR/\" 2>/dev/null\n cp \"$CLI_SOURCE_ZIP\" \"$LOCAL_ZIP_DIR/\" 2>/dev/null\n echo \"\"\n echo \"Some artifacts could not be uploaded to catbox.moe.\"\n echo \"Local copies saved to: $LOCAL_ZIP_DIR/\"\n echo \"You can upload them manually or attach them to the GitHub issue.\"\nfi\n```\n\n## Step 7: Cleanup\n\n**Called by the SKILL.md at the end of Phase 6**, after issue creation, local saves,\nand presenting results are all complete. Do not call this immediately after zipping —\nthe staging folder must stay alive for user review and upload.\n\n```bash\nrm -rf \"$STAGING_DIR\"\n```\n\nIf cleanup fails (permissions, etc.), it's a temp directory — the OS will clean it up\neventually. Do not let cleanup failure block the rest of the workflow.\n\n## Variables expected by this reference\n\n| Variable | Set by | Contains |\n|----------|--------|----------|\n| `$RUN_DIR` | SKILL.md Phase 1 | Full path to the manuscript run directory |\n| `$CLI_DIR` | SKILL.md Phase 1 | Full path to the CLI library directory |\n| `$CLI_NAME` | SKILL.md guard rails | Binary name (e.g., `cal-com-pp-cli`) |\n| `$API_SLUG` | SKILL.md guard rails | API slug (e.g., `cal-com`) |\n| `$API_KEY_VALUE` | User session (optional) | The API key if provided during generation |\n\n## Variables produced by this reference\n\n| Variable | Contains |\n|----------|----------|\n| `$RETRO_DOC_URL` | catbox URL for retro .md file (viewable in browser), or empty if upload failed |\n| `$MANUSCRIPTS_URL` | catbox URL for manuscripts zip, or empty if upload failed |\n| `$CLI_SOURCE_URL` | catbox URL for CLI source zip, or empty if upload failed |\n| `$SUBMISSION_CONFIRMED` | `true` only after the user selected Submit in Phase 6 Step 2; uploads are refused otherwise |\n| `$SCRUB_VERIFIED` | `true` only when Step 3's post-scrub verification completed with no unresolved findings; uploads are refused otherwise |\n| `$UPLOAD_FAILED` | `true` if any upload failed, `false` otherwise |\n| `$MANUSCRIPTS_ZIP` | Local path to manuscripts zip (in staging, deleted after cleanup) |\n| `$CLI_SOURCE_ZIP` | Local path to CLI source zip (in staging, deleted after cleanup) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7313,"content_sha256":"911b83d5251cb1a5ac46cea276111abf93ed1bd9c109beb3c382d5a705bcf4c1"},{"filename":"references/issue-template.md","content":"# GitHub Issue Template\n\nRead this file during Phase 6 when filing GitHub issues from retro findings.\n\nEach work unit becomes one flat top-level issue. There is no parent, no\nsub-issue hierarchy. The \"Findings absorbed\" section inside each issue\ngroups the 1+ findings that share a single fix.\n\nThis shape was chosen deliberately:\n\n- A retro of N work units produces N issues, each independently trackable —\n open, close, assign, comment.\n- The default GitHub issue list is not duplicated; there is no parent to\n mirror children, no progress bar to drift, no parent-not-auto-closing\n bookkeeping.\n- Cross-retro discovery still works through labels: `source:retro` marks\n provenance, `comp:\u003cslug>` surfaces every retro WU touching one component,\n `priority:P1` surfaces high-priority work, and `bug`/`enhancement` carry the\n real issue type.\n- Inter-issue links inside the same retro are not auto-generated. They appear\n only when an issue genuinely relates to another (a contradicting prior\n retro, or a `related-area` open issue surfaced by the dedup scan).\n\n## Formatting rules\n\nUse the `F` prefix for findings (`F1`, `F3`) and `WU-` for work units inside\nissue bodies. **Real GitHub issue references in the `Related issues` block\nare intentional `#N`** — that's where we *want* GitHub to auto-link the\ntimelines together, because those references reach across retros and across\nfiled work.\n\n## Execution principles (read before running any step)\n\nThe retro doc and the manuscript zip are the durable audit trail; each\nissue is an action surface. Optimize the issue path for speed and signal:\n\n- **Issue body is self-contained for debugging.** An agent picking up an\n issue should be able to start fixing it without opening the manuscripts\n attachment. Concrete file paths, line numbers, command output, and spec\n excerpts go in the body. The retro doc URL stays as a supplement, not a\n required read.\n- **Don't manufacture confidence.** If the retro analyst can't confidently\n isolate one root cause or one fix, the body should say so — list candidate\n causes/fixes and how to disambiguate. Wrong-but-plausible prescriptions\n are worse than honest uncertainty.\n- **Title is succinct and problem-stated.** Describes what's wrong, not how\n to fix it. No priority prefix (`[P1]`), no WU ordinal (`WU-1`) — both\n belong on labels and in the user-facing summary, not in the title that\n someone scans across retros.\n- **Build bodies inline; never via the Write tool.** Use shell heredocs\n into variables (or `printf` into bash-resident temp files inside a single\n `Bash` invocation). Per-WU `Write` tool round-trips are the single largest\n source of perceived latency the skill historically had. *Bash-resident\n temp files passed to `gh ... --body-file` are fine* — they're written and\n consumed inside one Bash call with no tool round-trip and unblock the\n Phase 6 Step 3 scrub step (`scrub_body` needs a file input/output, and\n `gh --body-file` is the natural consumer of the scrubbed output).\n- **Run issue creates and comments in parallel.** Each WU's filing is\n independent of every other WU. Background subshells writing to indexed\n temp files, then `wait`, then read in order — the pattern is in Step 3.\n - **Anti-pattern:** Do not use `URL=$(gh issue create ... &)` or\n `URL=$(gh issue comment ... &)`. The `&` runs inside the command\n substitution subshell, the subshell exits before `gh` finishes, `URL`\n is empty, and the still-running `gh` process can create or comment\n successfully in the background. Retrying then produces duplicate work.\n Use Step 3's `(gh ... > \"$ISSUE_TMPDIR/issue-$wu_idx\") &` capture-file\n shape instead.\n\n## Step 1: Ensure labels exist (idempotent, create-only)\n\nRun once per session before creating any issues. The repo is expected to already\nhave the canonical label set; this step is a safety net for users running the\nskill against a fresh fork. **Create-only — never edit existing labels** (the\nmaintainer may have set custom colors or descriptions; the skill must not\nclobber them).\n\n```bash\nREPO=\"mvanhorn/cli-printing-press\"\n\n# Fast-path: list existing labels once. If all 10 canonical labels are already\n# present, skip the create loop entirely — saves up to 10 gh API calls per\n# retro on a repo where prior retros already provisioned the set.\nEXISTING_LABELS=$(gh label list --repo \"$REPO\" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo \"\")\nNEED_CREATE=false\nfor required in \\\n \"comp:generator\" \"comp:openapi-parser\" \"comp:spec-parser\" \\\n \"comp:scorer\" \"comp:skill\" \\\n \"priority:P1\" \"priority:P2\" \\\n \"bug\" \"enhancement\" \"source:retro\"; do\n if ! printf '%s\\n' \"$EXISTING_LABELS\" | grep -qFx \"$required\"; then\n NEED_CREATE=true\n break\n fi\ndone\n\nif [ \"$NEED_CREATE\" = true ]; then\n ensure_label() {\n local name=\"$1\" color=\"$2\" desc=\"$3\"\n gh label create \"$name\" --repo \"$REPO\" --color \"$color\" --description \"$desc\" 2>/dev/null || true\n }\n\n # Component labels (5) — drive cross-retro discovery (`gh issue list --label comp:\u003cslug>`)\n ensure_label \"comp:generator\" \"5319e7\" \"Generator templates (internal/generator/)\"\n ensure_label \"comp:openapi-parser\" \"5319e7\" \"OpenAPI parser (internal/openapi/)\"\n ensure_label \"comp:spec-parser\" \"5319e7\" \"Internal spec parser (internal/spec/)\"\n ensure_label \"comp:scorer\" \"5319e7\" \"verify / dogfood / scorecard\"\n ensure_label \"comp:skill\" \"5319e7\" \"skills/printing-press/SKILL.md and related skill instructions\"\n\n # Priority labels (2) — drive priority-based filtering. The label is the\n # primary carrier; titles do not duplicate the priority prefix.\n ensure_label \"priority:P1\" \"b60205\" \"Broken or unsafe printed CLI\"\n ensure_label \"priority:P2\" \"d93f0b\" \"Current generalizing defect; printed CLI still works\"\n\n # Real issue types. These are actionable taxonomy, not routing or provenance.\n ensure_label \"bug\" \"d73a4a\" \"Something isn't working\"\n ensure_label \"enhancement\" \"a2eeef\" \"New feature or request\"\n\n # Canonical provenance marker. Do not create the legacy `retro` label.\n ensure_label \"source:retro\" \"c59c0f\" \"Issue produced by /printing-press-retro; systemic Printing Press finding\"\nfi\n\nREFRESHED_LABELS=$(gh label list --repo \"$REPO\" --limit 200 --json name --jq '.[].name' 2>/dev/null || true)\nif [ -n \"$REFRESHED_LABELS\" ]; then\n EXISTING_LABELS=\"$REFRESHED_LABELS\"\nfi\n```\n\nThe `retro-parent` label is intentionally omitted — there are no parent\nissues. If the legacy `retro` label exists from prior retros, leave it for\ndiscovery during the cutover; the skill never creates new issues with it.\n\n### Resolve the provenance marker for writes\n\n`source:retro` is the canonical provenance label. Before creating an issue,\nchoose a marker from labels that are actually available, preferring\n`source:retro` and falling back to legacy `retro` only when the canonical label\ncannot be created or is not available. Never pass a guessed label to `gh issue\ncreate`:\n\n```bash\nhas_label() {\n printf '%s\\n' \"$EXISTING_LABELS\" | grep -qFx \"$1\"\n}\n\nRETRO_PROVENANCE_LABEL=\"\"\nif has_label \"source:retro\"; then\n RETRO_PROVENANCE_LABEL=\"source:retro\"\nelif has_label \"retro\"; then\n RETRO_PROVENANCE_LABEL=\"retro\"\nelse\n gh label create \"source:retro\" --repo \"$REPO\" --color \"c59c0f\" \\\n --description \"Issue produced by /printing-press-retro; systemic Printing Press finding\" 2>/dev/null || true\n EXISTING_LABELS=$(gh label list --repo \"$REPO\" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo \"\")\n if has_label \"source:retro\"; then\n RETRO_PROVENANCE_LABEL=\"source:retro\"\n elif has_label \"retro\"; then\n RETRO_PROVENANCE_LABEL=\"retro\"\n else\n echo \"ERROR: neither source:retro nor legacy retro is available; cannot file a provenance-marked issue.\" >&2\n exit 1\n fi\nfi\n```\n\nThe initial label pass provisions `source:retro` on a fresh fork. The\npost-create re-list makes the fallback safe when label creation races or is\ndenied. Existing issues are not relabeled solely to rename provenance.\n\n## Step 2: Sort work units\n\nSort WUs by priority: P1 first, then P2. A WU with any other priority is invalid:\nreturn to SKILL.md Phase 4 and move it to Skip or Drop. Within a priority bucket,\nkeep the order they appeared in Phase 5.5 (typically by ascending WU number, but\nthe skill may have intentionally ordered them by dependency — preserve that).\n\n```bash\n# SORTED_WORK_UNITS is populated by sorting the complete WU records from\n# $WORK_UNITS P1 → P2. Each record retains its own `Stable ID: WU-N` field;\n# never sort a work-unit array and an ID array independently.\n# Dependency edges use the stable ID extracted from each sorted record, never\n# a sorted array position.\n```\n\n## Step 2.5: Dedup against open issues\n\nBefore filing, check whether any WUs match an issue that's already open.\nIf they do, comment on the existing issue with new evidence rather than\nfile a duplicate.\n\nThis is two label-filtered `gh` calls followed by per-WU agent reasoning over\nthe de-duplicated result set. **It does not need to be bulletproof** — false negatives (filing new when\none exists) are recoverable; false positives (commenting on the wrong\nissue) are uglier. **Bias toward `file-new` when uncertain.**\n\n### Fetch open retro issues\n\n```bash\nEXISTING_OPEN_RETROS_SOURCE=$(gh issue list \\\n --repo \"$REPO\" \\\n --label source:retro \\\n --state open \\\n --limit 200 \\\n --json number,title,url,labels 2>/dev/null \\\n || echo \"[]\")\n\nEXISTING_OPEN_RETROS_LEGACY=$(gh issue list \\\n --repo \"$REPO\" \\\n --label retro \\\n --state open \\\n --limit 200 \\\n --json number,title,url,labels 2>/dev/null \\\n || echo \"[]\")\n\nEXISTING_OPEN_RETROS=$(jq -s 'add | unique_by(.number)' \\\n \u003c(printf '%s\\n' \"$EXISTING_OPEN_RETROS_SOURCE\") \\\n \u003c(printf '%s\\n' \"$EXISTING_OPEN_RETROS_LEGACY\"))\n```\n\nThe two results are merged by issue number so an issue carrying both labels is\nconsidered once. No per-WU component filtering — the agent reasons over titles\nacross the whole open-retro set so a related-area issue under a different\ncomponent still surfaces. The `labels` field lets the executor preserve the\none-type invariant when commenting on an existing match.\n\n### Classify each WU against the candidate set\n\nFor each WU in `$SORTED_WORK_UNITS`, the skill executor (the agent running\nthis skill) reads the WU's title and summary, then scans\n`$EXISTING_OPEN_RETROS` for matches. Per-candidate verdict:\n\n| Verdict | When | Effect |\n|---|---|---|\n| `same` | Title and summary describe the same root problem already filed (high confidence) | Marks WU for `comment` instead of create |\n| `related-area` | Different problem but in adjacent territory worth cross-referencing (cursor handling vs. cursor format on same template; auth refresh on different envelope) | Will be cited in the new issue's `Related issues` block via `#N` |\n| `unrelated` | No meaningful overlap | Ignored |\n\nPer WU, fold the per-candidate verdicts into a single `$WU_DEDUP[i]` slot:\n\n```\n$WU_DEDUP[i]:\n \"comment:NN\" — at least one candidate was `same`. Comment on NN.\n If multiple `same`, pick the most-recent and treat\n the rest as related (folded into $WU_RELATED).\n \"\" (empty) — no `same`. The agent will create a new issue.\n```\n\nAnd capture related issues separately:\n\n```\n$WU_RELATED[i]:\n comma-separated list of issue numbers classified as `related-area`,\n combined with prior-retro issue numbers from Phase 3 Step D.\n Empty when neither scan surfaced anything.\n```\n\nThe decisions feed forward into the Phase 6 Step 2 confirm summary so the\nuser sees what will happen and can override before anything is filed.\n\n## Step 3: Build bodies and execute (in parallel)\n\nFor each WU, build either an issue body or a comment body, then run `gh`\nin a background subshell. `wait`, then collect.\n\n**Layer 0 body scrub is mandatory.** Before posting any body to GitHub, the\nparallel loop runs `scrub_body` on the body file. Source the function from\n[`secret-scrubbing.md`](secret-scrubbing.md) \"Layer 0\" at the top of the\nbash block that contains the loop — paste the `scrub_body() { ... }`\ndefinition verbatim, or `source` an equivalent shell fragment. Without this,\nthe loop will fail with `scrub_body: command not found` per-WU.\n\n### Issue title (for new issues)\n\nSuccinct, problem-stated. Examples:\n\n- ✅ `Spec-declared version headers dropped during generation`\n- ❌ `Emit Stripe-Version header from spec` *(prescribes the fix)*\n- ✅ `Sync template misroutes cursor pagination`\n- ❌ `[P1] WU-1: Switch sync template to cursor-aware pagination` *(priority + WU number + prescription)*\n\n### Issue body (for new issues)\n\nThe body must be self-contained — an agent should be able to act on it\nwithout opening the manuscripts attachment. Use the shape below.\n\n```markdown\n> Filed by `/printing-press-retro` from a run of **\u003capi-display-name>**\n> (scorecard \u003cscore>/100, \u003cX>% verify pass).\n\n## Summary\n\n\u003c2-4 sentences: what the problem is and why it matters. Stay\nproblem-shaped, not solution-shaped. Someone reading this paragraph alone\nshould understand what they're being asked to address.>\n\n## Where to look\n\n- **Component:** \u003ccomp-slug>\n- **Issue type:** `bug` or `enhancement` from the deterministic finding-category mapping\n- **Likely area:** \u003cpath or files in the printing-press repo, e.g. `internal/generator/templates/`>\n- **Triggered when:** \u003cspec shape, API behavior, or runtime context that surfaces this>\n\n## What we observed\n\n\u003cConcrete evidence usable to reproduce or locate the issue without opening\nattachments. This is the meat of the body — be specific.>\n\n- File paths and line numbers (in the printed CLI or the printing-press repo)\n- Command + output snippet showing the failure\n- Spec snippet showing the trigger condition\n- Error messages, stack traces, or scorer output verbatim where relevant\n\n> ⚠️ **Redact secrets and PII before pasting.** Issue bodies are public. When\n> the evidence comes from scanner output, dogfood payloads, Greptile review\n> comments, or live API responses, replace credentials with\n> `\u003cREDACTED:\u003cvendor>-\u003ckind>:\u003cfirst4>...\u003clast4>:\u003clen>ch>` and PII with\n> `\u003cREDACTED:\u003ckind>>` per [`secret-scrubbing.md`](secret-scrubbing.md)\n> \"Layer 0\". The Phase 6 Step 3 loop runs `scrub_body` on this file and will\n> hard-fail the WU's filing if it finds an unredacted vendor-prefix token —\n> that's the floor, not a substitute for redacting at write time.\n> **This applies most strictly to findings about secret/PII leaks**: quoting\n> the actual leaked value to prove the leak exists re-leaks it.\n\n## Suspected root cause\n\n\u003cHypothesis with explicit confidence level. Don't manufacture certainty.>\n\n- **If certain:** \"The template at `internal/generator/templates/sync.go.tmpl`\n hardcodes `pageToken` as the cursor param name.\"\n- **If uncertain:** \"Likely the openapi-parser is dropping the version\n header on parse, but the generator template could also be omitting the\n emit. Either could be the root cause; verify by checking parser output\n before pinning a fix.\"\n\n## Suggested direction\n\n\u003cProposed fix direction with explicit confidence level. Offer alternatives\nwhen reasonable doubts exist; always include a verification step.>\n\n- **If certain:** \"Add a profiler check in `internal/openapi/parser.go`\n that detects spec-declared version headers and writes them to the\n generator config. Emit them in the client template alongside auth headers.\"\n- **If uncertain:** \"Direction A: surface the field through the parser;\n Direction B: post-process in the generator. A is more durable but\n requires changes in two packages. Reproduce the failure with the\n manuscripts evidence first, then choose.\"\n\n## Acceptance criteria\n\n- positive: \u003ctest that proves the fix works on the API class this targets>\n- negative: \u003ctest that proves the fix doesn't regress unaffected APIs>\n\n## Frequency\n\nevery / most APIs / subclass:\u003cname> / this API only\n\n## Complexity\n\nsmall / medium / large\n\n## Scope boundary\n\n\u003cWhat this issue does NOT include. Helps prevent fix creep. Skip if not applicable.>\n\n## Dependencies\n\n\u003c\"None.\" unless this work unit has a real prerequisite. For an explicit\nprerequisite, name the blocking issue or work unit here. After issue numbers are\nknown, the filing pass applies GitHub's native relationship with\n`gh issue edit \u003cdependent> --repo \"$REPO\" --add-blocked-by \u003cprerequisite>`.\nRelated-area references do not belong here and must remain prose in\n`Related issues`.\n\n## Findings absorbed\n\n*Omit if 1:1 (the body above is the finding).*\n\n#### F\u003cn>: \u003ctitle>\n\n- \u003cone-sentence symptom>\n- Evidence: \u003cwhere it showed up>\n\n#### F\u003cn>: ...\n\n## Related issues\n\n\u003cCombined output from Phase 3 Step D (prior-retro doc archaeology) and\nStep 2.5 (open-issue dedup `related-area` classification). Auto-cross-links\nvia `#N`. Sibling WUs in this same retro do NOT appear here unless one is\ngenuinely a prerequisite; a real prerequisite is also represented by a native\n`blocked-by`/`blocking` relationship, not only by this prose link.>\n\n- #\u003cnum> — prior retro (`aligned`/`contradicts`/`extends`): \u003cone-sentence note>\n- #\u003cnum> — open issue (`related-area`): \u003cone-sentence note on the adjacency>\n- *(or \"None\" when neither scan surfaced anything)*\n\n## Artifacts\n\n| Artifact | Link |\n|----------|------|\n| Retro document | \u003c$RETRO_DOC_URL or \"Upload failed — see local copy\"> |\n| Manuscripts (research + proofs) | \u003c$MANUSCRIPTS_URL or \"Upload failed — see local copy\"> |\n| Generated CLI source code | \u003c$CLI_SOURCE_URL or \"Upload failed — see local copy\"> |\n\n---\n\n*Generated by `/printing-press-retro` · [CLI Printing Press](https://github.com/mvanhorn/cli-printing-press)*\n```\n\nWhat's *not* in the body, by design:\n\n- **Session Stats table** — collapsed into the one-line provenance header.\n The retro doc has the full stats.\n- **What the Printing Press Got Right** — retro-wide; lives in the retro doc.\n- **Skipped table** — retro-wide triage record; lives in the retro doc.\n- **Auto-cross-references to sibling WUs in this same retro** — these are\n noise unless one WU is genuinely a prerequisite for another (and that\n goes in `Dependencies:` and becomes a native `blocked-by` relationship after\n issue numbers are known).\n\n### Comment body (for `comment:#N` decisions)\n\n```markdown\n**Recurrence in \u003capi-display-name> retro** *(\u003cretro-date>)*\n\nSame problem surfaced again in this run. New observations:\n\n- \u003cconcrete observation 1: file:line, command output, etc.>\n- \u003cconcrete observation 2>\n\nUpdated frequency estimate: \u003cevery / most / subclass:\u003cname>>\n\n[Full retro doc](\u003c$RETRO_DOC_URL>) · [Manuscripts](\u003c$MANUSCRIPTS_URL>)\n\n---\n\n*Comment added by `/printing-press-retro` from the \u003capi-display-name> retro.*\n```\n\nThe comment is intentionally short — it's recurrence evidence, not a\nre-statement of the original issue. The retro doc carries the full audit\ntrail for anyone who wants more context.\n\n### Parallel execution\n\n```bash\ndeclare -a OUTCOME_KIND OUTCOME_URL OUTCOME_TITLE OUTCOME_PRIORITY OUTCOME_COMP OUTCOME_COMPLEXITY\ndeclare -a WU_DEPENDENCY_EDGES\ndeclare -a FAILED_ISSUES\ndeclare -A OUTCOME_ISSUE_NUM_BY_WU_ID SORTED_WU_ID_SEEN\n\nextract_wu_id() {\n printf '%s\\n' \"$1\" \\\n | sed -nE 's/^- \\*\\*Stable ID:\\*\\* (WU-[1-9][0-9]*)([[:space:]].*)?$/\\1/p' \\\n | head -1\n}\n\nISSUE_TMPDIR=$(mktemp -d)\nISSUE_RUN_START_ISO=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n\nfor wu_idx in \"${!SORTED_WORK_UNITS[@]}\"; do\n WU_ID=\"$(extract_wu_id \"${SORTED_WORK_UNITS[$wu_idx]}\")\"\n if [[ ! \"$WU_ID\" =~ ^WU-[1-9][0-9]*$ ]]; then\n echo \"ERROR: sorted WU $wu_idx has missing or duplicate stable ID: ${WU_ID:-\u003cempty>}\" >&2\n exit 1\n fi\n if [[ -n \"${SORTED_WU_ID_SEEN[$WU_ID]+x}\" ]]; then\n echo \"ERROR: sorted WU $wu_idx has duplicate stable ID: $WU_ID\" >&2\n exit 1\n fi\n SORTED_WU_ID_SEEN[\"$WU_ID\"]=1\ndone\n\nfor wu_idx in \"${!SORTED_WORK_UNITS[@]}\"; do\n (\n WU=\"${SORTED_WORK_UNITS[$wu_idx]}\"\n WU_ID=\"$(extract_wu_id \"$WU\")\"\n DEDUP=\"${WU_DEDUP[$wu_idx]}\" # \"comment:NN\" or empty\n\n # Each WU contributes: $WU_ID, $WU_TITLE, $WU_BODY, $WU_COMMENT_BODY,\n # $WU_PRIORITY_NUM, $WU_PRIORITY_LABEL, $WU_TYPE_LABEL, $WU_COMP_SLUG,\n # $WU_COMPLEXITY.\n\n KIND=\"\"\n URL=\"\"\n FAIL_MSG=\"\"\n\n # Layer 0 body scrub — runs once per WU on whichever body is about to be\n # posted. scrub_body is defined in references/secret-scrubbing.md and must\n # be sourced or pasted in the bash block enclosing this loop.\n #\n # Hard-fail behavior: if the body contains an unredacted vendor-prefix\n # token (lin_api_, sk_live_, ghp_, etc.) we refuse to post that WU and\n # surface it in $FAILED_ISSUES so the agent can hand-redact and retry.\n # PII patterns auto-redact in place. The original $WU_BODY /\n # $WU_COMMENT_BODY shell var is untouched; only the file passed to gh\n # gets the scrubbed text.\n BODY_TMP=\"$ISSUE_TMPDIR/body-$wu_idx.md\"\n BODY_TMP_SCRUBBED=\"$ISSUE_TMPDIR/body-$wu_idx.scrubbed.md\"\n if [[ \"$DEDUP\" == comment:* ]]; then\n printf '%s' \"$WU_COMMENT_BODY\" > \"$BODY_TMP\"\n else\n printf '%s' \"$WU_BODY\" > \"$BODY_TMP\"\n fi\n if [[ \"$WU_PRIORITY_NUM\" != \"1\" && \"$WU_PRIORITY_NUM\" != \"2\" ]]; then\n KIND=\"invalid-priority\"\n FAIL_MSG=\"$WU_TITLE — invalid retro priority P${WU_PRIORITY_NUM:-\u003cempty>}; only P1/P2 may be filed. Move this WU to Skip or Drop instead.\"\n URL=\"\"\n elif ! scrub_body \"$BODY_TMP\" \"$BODY_TMP_SCRUBBED\" 2>\"$ISSUE_TMPDIR/scrub-$wu_idx.err\"; then\n KIND=\"scrub-failed\"\n SCRUB_REASON=$(tr '\\n' ' ' \u003c \"$ISSUE_TMPDIR/scrub-$wu_idx.err\" | head -c 400)\n FAIL_MSG=\"$WU_TITLE — body scrub hard-failed (vendor-prefix secret in body); not posted. Reason: $SCRUB_REASON. Body left at $BODY_TMP for hand-redaction.\"\n URL=\"\"\n elif [[ \"$DEDUP\" == comment:* ]]; then\n ISSUE_NUM=\"${DEDUP#comment:}\"\n # A recurrence may target a legacy issue that predates real type labels.\n # Normalize the two actionable type labels before adding evidence so the\n # existing issue still satisfies the one-type taxonomy invariant.\n if ! gh issue edit \"$ISSUE_NUM\" \\\n --repo \"$REPO\" \\\n --remove-label bug \\\n --remove-label enhancement \\\n --add-label \"$WU_TYPE_LABEL\" >/dev/null 2>&1; then\n KIND=\"type-normalization-failed\"\n FAIL_MSG=\"$WU_TITLE — could not normalize #$ISSUE_NUM to exactly one type label ($WU_TYPE_LABEL); comment not posted.\"\n URL=\"\"\n elif URL=$(gh issue comment \"$ISSUE_NUM\" \\\n --repo \"$REPO\" \\\n --body-file \"$BODY_TMP_SCRUBBED\" 2>&1) \\\n && [[ \"$URL\" == https://* ]]; then\n KIND=\"commented\"\n else\n KIND=\"comment-failed\"\n FAIL_MSG=\"$WU_TITLE — comment on #$ISSUE_NUM failed: ${URL:-no-response}\"\n URL=\"\"\n fi\n else\n if URL=$(gh issue create \\\n --repo \"$REPO\" \\\n --title \"$WU_TITLE\" \\\n --body-file \"$BODY_TMP_SCRUBBED\" \\\n --label \"$RETRO_PROVENANCE_LABEL\" \\\n --label \"$WU_TYPE_LABEL\" \\\n --label \"priority:P${WU_PRIORITY_NUM}\" \\\n --label \"comp:${WU_COMP_SLUG}\" 2>&1) \\\n && [[ \"$URL\" == https://* ]]; then\n KIND=\"created\"\n else\n KIND=\"create-failed\"\n FAIL_MSG=\"$WU_TITLE — issue creation failed: ${URL:-no-response}\"\n URL=\"\"\n fi\n fi\n\n {\n printf '%s\\n' \"$WU_ID\"\n printf '%s\\n' \"$KIND\"\n printf '%s\\n' \"$URL\"\n printf '%s\\n' \"$WU_TITLE\"\n printf '%s\\n' \"$WU_PRIORITY_LABEL\"\n printf '%s\\n' \"$WU_COMP_SLUG\"\n printf '%s\\n' \"$WU_COMPLEXITY\"\n printf '%s\\n' \"$FAIL_MSG\"\n } > \"$ISSUE_TMPDIR/issue-$wu_idx\"\n ) &\ndone\n\nwait\n\n# Defensive duplicate detector. If an agent accidentally used the malformed\n# `URL=$(gh issue create ... &)` shortcut outside this reference, those\n# backgrounded issue creates may succeed even though the captured URL is empty.\n# Extra issues created by the current user since the run began, beyond the issue\n# numbers captured in per-WU temp files, signal that parallel filing leaked\n# creates. Use the live REST list endpoint instead of search, whose index can lag\n# immediately after creation.\nEXPECTED_ISSUE_NUMBERS=$(\n for wu_idx in \"${!SORTED_WORK_UNITS[@]}\"; do\n if [ -f \"$ISSUE_TMPDIR/issue-$wu_idx\" ]; then\n {\n IFS= read -r WU_ID_TMP\n IFS= read -r KIND_TMP\n IFS= read -r URL_TMP\n } \u003c \"$ISSUE_TMPDIR/issue-$wu_idx\"\n if [[ \"$KIND_TMP\" == created && \"$URL_TMP\" =~ /issues/([0-9]+)$ ]]; then\n printf '%s\\n' \"${BASH_REMATCH[1]}\"\n fi\n fi\n done | sort -u\n)\nCURRENT_GH_USER=$(gh api user --jq .login 2>/dev/null || true)\nRECENT_CREATED_LINES=\"\"\nif [ -n \"$CURRENT_GH_USER\" ]; then\n RECENT_CREATED_LINES=$(gh api --method GET \"repos/$REPO/issues\" \\\n -f state=open \\\n -f since=\"$ISSUE_RUN_START_ISO\" \\\n -f sort=created \\\n -f direction=desc \\\n -f per_page=100 \\\n --jq \".[] | select((.pull_request | not) and .user.login == \\\"$CURRENT_GH_USER\\\" and .created_at >= \\\"$ISSUE_RUN_START_ISO\\\") | \\\"#\\(.number) \\(.title)\\\"\" 2>/dev/null || true)\nfi\nUNEXPECTED_CREATED_LINES=$(printf '%s\\n' \"$RECENT_CREATED_LINES\" | sed '/^$/d')\nif [ -n \"$EXPECTED_ISSUE_NUMBERS\" ]; then\n while IFS= read -r issue_num; do\n [ -z \"$issue_num\" ] && continue\n UNEXPECTED_CREATED_LINES=$(printf '%s\\n' \"$UNEXPECTED_CREATED_LINES\" | grep -v \"^#$issue_num \" || true)\n done \u003c\u003cEOF\n$EXPECTED_ISSUE_NUMBERS\nEOF\nfi\nEXPECTED_CREATES=0\nfor wu_idx in \"${!SORTED_WORK_UNITS[@]}\"; do\n if [[ \"${WU_DEDUP[$wu_idx]}\" != comment:* ]]; then\n KIND_TMP=\"\"\n if [ -f \"$ISSUE_TMPDIR/issue-$wu_idx\" ]; then\n {\n IFS= read -r _\n IFS= read -r KIND_TMP\n } \u003c \"$ISSUE_TMPDIR/issue-$wu_idx\"\n fi\n [[ \"$KIND_TMP\" == scrub-failed || \"$KIND_TMP\" == invalid-priority ]] || EXPECTED_CREATES=$((EXPECTED_CREATES + 1))\n fi\ndone\nUNEXPECTED_CREATED_COUNT=$(printf '%s\\n' \"$UNEXPECTED_CREATED_LINES\" | sed '/^$/d' | wc -l | tr -d ' ')\nif [ \"$UNEXPECTED_CREATED_COUNT\" -gt 0 ]; then\n printf 'WARNING: %s unexpected issue(s) were created by the current user since %s, outside this retro'\\''s %s expected new issue(s). Check for duplicate issues before presenting results.\\n' \\\n \"$UNEXPECTED_CREATED_COUNT\" \"$ISSUE_RUN_START_ISO\" \"$EXPECTED_CREATES\" >&2\n printf '%s\\n' \"$UNEXPECTED_CREATED_LINES\" | sed 's/^/ /' >&2\nfi\n\nfor wu_idx in \"${!SORTED_WORK_UNITS[@]}\"; do\n {\n IFS= read -r WU_ID\n IFS= read -r KIND\n IFS= read -r URL\n IFS= read -r TITLE\n IFS= read -r PRIORITY\n IFS= read -r COMP\n IFS= read -r COMPLEXITY\n IFS= read -r FAIL_MSG\n } \u003c \"$ISSUE_TMPDIR/issue-$wu_idx\"\n\n EXPECTED_WU_ID=\"$(extract_wu_id \"${SORTED_WORK_UNITS[$wu_idx]}\")\"\n if [[ \"$WU_ID\" != \"$EXPECTED_WU_ID\" ]]; then\n FAILED_ISSUES+=(\"sorted WU $wu_idx returned stable ID ${WU_ID:-\u003cempty>}, expected ${EXPECTED_WU_ID:-\u003cempty>}\")\n continue\n fi\n\n OUTCOME_KIND+=(\"$KIND\")\n OUTCOME_URL+=(\"$URL\")\n OUTCOME_TITLE+=(\"$TITLE\")\n OUTCOME_PRIORITY+=(\"$PRIORITY\")\n OUTCOME_COMP+=(\"$COMP\")\n OUTCOME_COMPLEXITY+=(\"$COMPLEXITY\")\n if [[ \"$URL\" =~ /issues/([0-9]+) ]]; then\n OUTCOME_ISSUE_NUM_BY_WU_ID[\"$WU_ID\"]=\"${BASH_REMATCH[1]}\"\n elif [[ \"${WU_DEDUP[$wu_idx]}\" == comment:* ]]; then\n OUTCOME_ISSUE_NUM_BY_WU_ID[\"$WU_ID\"]=\"${WU_DEDUP[$wu_idx]#comment:}\"\n else\n OUTCOME_ISSUE_NUM_BY_WU_ID[\"$WU_ID\"]=\"\"\n fi\n\n case \"$KIND\" in\n created|commented)\n echo \"${KIND^}: $URL\"\n ;;\n create-failed|comment-failed|type-normalization-failed|invalid-priority|scrub-failed)\n echo \"WARNING: $FAIL_MSG\"\n FAILED_ISSUES+=(\"$FAIL_MSG\")\n ;;\n esac\ndone\n\n# Apply only explicitly declared prerequisite edges, after all creates and\n# dedup decisions have produced issue numbers. Each edge is\n# `WU-2|wu:WU-1` or `WU-2|issue:123`. IDs are stable WU-N values, so this\n# remains correct when a P1 WU-2 sorts before a P2 WU-1. Resolve both endpoints\n# through the associative outcome map; never use sorted array positions.\n# Related-area references never enter this array; they remain prose links.\nfor edge in \"${WU_DEPENDENCY_EDGES[@]}\"; do\n IFS='|' read -r dependent_id prerequisite_ref \u003c\u003c\u003c \"$edge\"\n if [[ ! \"$dependent_id\" =~ ^WU-[1-9][0-9]*$ ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — unknown or malformed dependent stable WU ID\")\n continue\n fi\n if [[ -z \"${SORTED_WU_ID_SEEN[$dependent_id]+x}\" ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — unknown dependent stable WU ID\")\n continue\n fi\n dependent_issue=\"${OUTCOME_ISSUE_NUM_BY_WU_ID[$dependent_id]-}\"\n case \"$prerequisite_ref\" in\n wu:*)\n prerequisite_id=\"${prerequisite_ref#wu:}\"\n if [[ ! \"$prerequisite_id\" =~ ^WU-[1-9][0-9]*$ ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — unknown or malformed prerequisite stable WU ID\")\n continue\n fi\n if [[ -z \"${SORTED_WU_ID_SEEN[$prerequisite_id]+x}\" ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — unknown prerequisite stable WU ID\")\n continue\n fi\n prerequisite_issue=\"${OUTCOME_ISSUE_NUM_BY_WU_ID[$prerequisite_id]-}\"\n ;;\n issue:*)\n prerequisite_issue=\"${prerequisite_ref#issue:}\"\n if [[ ! \"$prerequisite_issue\" =~ ^[1-9][0-9]*$ ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — malformed existing issue number\")\n continue\n fi\n ;;\n *)\n prerequisite_issue=\"\"\n ;;\n esac\n if [[ -z \"$dependent_issue\" || -z \"$prerequisite_issue\" ]]; then\n FAILED_ISSUES+=(\"dependency edge $edge — issue number unavailable; native relationship not applied\")\n elif ! gh issue edit \"$dependent_issue\" --repo \"$REPO\" \\\n --add-blocked-by \"$prerequisite_issue\" >/dev/null 2>&1; then\n FAILED_ISSUES+=(\"dependency edge #$dependent_issue blocked by #$prerequisite_issue — native relationship failed\")\n fi\ndone\n\n# Cleanup is conditional on scrub-failed WUs. Those WUs' body files are the\n# canonical hand-redaction source the failure message points at — wiping\n# $ISSUE_TMPDIR would destroy the recovery path. Keep the dir alive when any\n# WU scrub-failed; the agent (or user) can read the body file, hand-redact,\n# and re-run the affected WUs without re-deriving the body. The dir is in\n# $(mktemp -d) so it self-cleans on OS reboot / `tmpwatch` regardless.\nSCRUB_FAILED_COUNT=0\nfor KIND in \"${OUTCOME_KIND[@]}\"; do\n [ \"$KIND\" = \"scrub-failed\" ] && SCRUB_FAILED_COUNT=$((SCRUB_FAILED_COUNT + 1))\ndone\nif [ \"$SCRUB_FAILED_COUNT\" -eq 0 ]; then\n rm -rf \"$ISSUE_TMPDIR\"\nelse\n echo \"NOTE: $SCRUB_FAILED_COUNT WU body file(s) preserved at $ISSUE_TMPDIR for hand-redaction. Delete manually after retrying the affected WU(s).\" >&2\nfi\n```\n\nFailure modes:\n\n| Mode | What happened | What the user sees in Phase 6 Step 6 |\n|---|---|---|\n| `created` | New issue filed with labels | Listed in success summary |\n| `commented` | Comment added to existing issue | Listed as \"commented on #N\" |\n| `create-failed` | `gh issue create` returned no usable URL | `$FAILED_ISSUES` summary; manual filing instructions |\n| `comment-failed` | `gh issue comment` failed | `$FAILED_ISSUES` summary; manual comment instructions |\n| `type-normalization-failed` | Existing matched issue could not be normalized to exactly one real type label | `$FAILED_ISSUES` summary; manual label repair instructions |\n| `invalid-priority` | A work unit still carried a non-P1/P2 priority | `$FAILED_ISSUES` summary; agent must return to Phase 4 and move the WU to Skip or Drop |\n| `scrub-failed` | Body contained an unredacted vendor-prefix secret; `scrub_body` refused to write the scrubbed copy. Body file left at `$BODY_TMP` for hand-redaction | `$FAILED_ISSUES` summary; agent must hand-redact per `secret-scrubbing.md` Layer 0 and retry the WU |\n\n## Variables expected\n\n| Variable | Set by | Contains |\n|---|---|---|\n| `$REPO` | This file Step 1 | Owner/repo string for `gh` |\n| `$RETRO_DOC_URL` | artifact-packaging.md | catbox URL for retro .md, or empty |\n| `$MANUSCRIPTS_URL` | artifact-packaging.md | catbox URL or empty |\n| `$CLI_SOURCE_URL` | artifact-packaging.md | catbox URL or empty |\n| `$RETRO_PROOF_PATH` | SKILL.md Phase 5 | Path to saved retro in manuscript proofs |\n| `$RETRO_SCRATCH_PATH` | SKILL.md Phase 5 | Path to temp retro copy under `/tmp/printing-press/retro/` |\n| `$RETRO_PROVENANCE_LABEL` | This file Step 1 | Available write marker: canonical `source:retro`, or legacy `retro` only during cutover fallback |\n| `$WORK_UNITS` | SKILL.md Phase 5.5 | Array of WU records |\n| `$SORTED_WORK_UNITS` | This file Step 2 | Complete `$WORK_UNITS` records sorted P1 → P2; each record retains its own stable WU-N identifier |\n| `$EXISTING_OPEN_RETROS` | This file Step 2.5 | De-duplicated JSON of open issues carrying `source:retro` or legacy `retro` |\n| `$WU_DEDUP` | This file Step 2.5 | Per-WU dedup decision: `comment:NN` or empty |\n| `$WU_RELATED` | This file Step 2.5 + Phase 3 Step D | Per-WU comma-separated related-issue numbers (annotated for the body) |\n| `$WU_DEPENDENCY_EDGES` | SKILL.md Phase 5.5 + this file Step 3 | Explicit stable-ID prerequisite edges such as `WU-2|wu:WU-1` applied with native `--add-blocked-by`; related-area references are excluded |\n| `$OUTCOME_ISSUE_NUM_BY_WU_ID` | This file Step 3 | Issue numbers keyed by stable WU-N, independent of sorted array positions |\n| `$WU_TYPE_LABEL` | Each sorted WU record | Exactly one mapped real issue type: `bug` or `enhancement` |\n| All retro findings | SKILL.md Phase 4 | Used to populate each issue's \"Findings absorbed\" section |\n\n## Variables produced\n\n| Variable | Contains |\n|---|---|\n| `$OUTCOME_KIND` | Array, one per WU: `created` / `commented` / `create-failed` / `comment-failed` / `type-normalization-failed` / `invalid-priority` / `scrub-failed` |\n| `$OUTCOME_URL` | Array of issue/comment URLs (empty for failures) |\n| `$FAILED_ISSUES` | Array of human-readable failure descriptions; empty if every WU succeeded |\n\n## Handling `gh` failure (graceful degradation)\n\nCheck `gh` auth at the start of Phase 6 Step 4:\n\n```bash\nif ! gh auth status 2>/dev/null; then\n echo \"GitHub CLI is not authenticated. Cannot create issues or comments.\"\n GH_AVAILABLE=false\nelse\n GH_AVAILABLE=true\nfi\n```\n\nIf `GH_AVAILABLE=false`, or if every per-WU action failed, fall back to\nprinting manual filing instructions:\n\n```bash\nif [ \"$GH_AVAILABLE\" = false ] || [ \"${#FAILED_ISSUES[@]}\" -eq \"${#SORTED_WORK_UNITS[@]}\" ]; then\n echo \"\"\n echo \"Could not create issues or comments automatically.\"\n echo \"\"\n echo \"To file the retro manually:\"\n echo \" 1. Go to: https://github.com/mvanhorn/cli-printing-press/issues\"\n echo \" 2. Use the body templates from the retro document at:\"\n echo \" $RETRO_PROOF_PATH\"\n if [ -n \"$RETRO_SCRATCH_PATH\" ] && [ -f \"$RETRO_SCRATCH_PATH\" ]; then\n echo \" $RETRO_SCRATCH_PATH\"\n fi\n echo \" 3. File one issue per work unit. Apply labels: $RETRO_PROVENANCE_LABEL, bug or enhancement, priority:P1 or priority:P2, comp:\u003cslug>. Use native --add-blocked-by only for explicit prerequisites; keep related-area references in prose.\"\n if [ -n \"$MANUSCRIPTS_URL\" ]; then\n echo \" 4. Manuscripts: $MANUSCRIPTS_URL\"\n fi\n if [ -n \"$CLI_SOURCE_URL\" ]; then\n echo \" 5. CLI source: $CLI_SOURCE_URL\"\n fi\nfi\n```\n\nPer-WU partial failures (some succeed, some don't) are surfaced through\n`$FAILED_ISSUES` in Phase 6 Step 6 — the user sees both the successful\nURLs and the failed ones with manual filing instructions for just the\nmissing ones.\n\n## Handling body size\n\nGitHub issue bodies have a practical limit (~65KB). The flat-issue shape\nkeeps each issue narrow, but a single WU with many absorbed findings +\nlong related-issue chains could still approach it. If `gh issue create`\nrejects a body for size:\n\n1. Truncate \"What we observed\" and \"Findings absorbed\" to one bullet\n per item, with a pointer to the retro doc.\n2. Add: \"Full finding analysis available in the retro document linked\n under Artifacts.\"\n3. Retry.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":35936,"content_sha256":"5f10a05636ea799f8fa3c42c76e7f4d8d997b3e5994d33012ce83f8b1563c61f"},{"filename":"references/run-resolution.md","content":"# Run resolution\n\nResolve the API, run, and CLI directory before entering any phase. The rest of\nthe skill reads `$API_NAME`, `$RUN_ID`, `$RUN_DIR`, `$CLI_DIR`, `$IN_REPO`, and\n`$RETRO_SCRATCH_DIR`.\n\n## Setup\n\n\u003c!-- RETRO_SETUP_START -->\n```bash\n# Path-only setup — no binary detection required.\n# The retro skill reads manuscripts and runs gh/curl. It does not invoke the\n# cli-printing-press binary. This avoids aborting for users who installed the\n# plugin but not the Go binary.\n\n_scope_dir=\"$(git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\")\"\n_scope_dir=\"$(cd \"$_scope_dir\" && pwd -P)\"\n\nPRESS_HOME=\"${PRINTING_PRESS_HOME:-$HOME/printing-press}\"\nPRESS_MANUSCRIPTS=\"$PRESS_HOME/manuscripts\"\nPRESS_LIBRARY=\"$PRESS_HOME/library\"\nRETRO_SCRATCH_DIR=\"/tmp/printing-press/retro\"\n\nmkdir -p \"$PRESS_MANUSCRIPTS\" \"$PRESS_LIBRARY\" \"$RETRO_SCRATCH_DIR\"\n\n# Detect whether we're inside the printing-press repo\nIN_REPO=false\nif [ -f \"$_scope_dir/cmd/cli-printing-press/main.go\" ]; then\n IN_REPO=true\n REPO_ROOT=\"$_scope_dir\"\n echo \"Running from printing-press repo: $REPO_ROOT\"\nfi\n```\n\u003c!-- RETRO_SETUP_END -->\n\n## Nothing to retro\n\n```bash\nif [ ! -d \"$PRESS_MANUSCRIPTS\" ] || [ -z \"$(ls -A \"$PRESS_MANUSCRIPTS\" 2>/dev/null)\" ]; then\n echo \"No manuscripts found. Run /printing-press first to generate a CLI.\"\n exit 1\nfi\n```\n\n## Resolve which API\n\nIf the user passed an API name as an argument, use that. Validate for path traversal:\n\n```bash\n# Reject names with /, \\, or ..\nif echo \"$USER_API_NAME\" | grep -qE '[/\\\\]|\\.\\.'; then\n echo \"Invalid API name: '$USER_API_NAME'. Names cannot contain path separators or '..'.\"\n exit 1\nfi\n\n# Verify resolved path stays under PRESS_MANUSCRIPTS\nRESOLVED=\"$(cd \"$PRESS_MANUSCRIPTS/$USER_API_NAME\" 2>/dev/null && pwd -P)\"\ncase \"$RESOLVED\" in\n \"$PRESS_MANUSCRIPTS\"/*) ;; # OK\n *) echo \"Invalid API name: path resolves outside manuscripts directory.\"; exit 1 ;;\nesac\n```\n\nIf no API name was provided and multiple APIs exist, list them with their most recent\nrun dates and ask the user to choose:\n\n```bash\necho \"Multiple APIs found in manuscripts:\"\nfor api_dir in \"$PRESS_MANUSCRIPTS\"/*/; do\n api_name=$(basename \"$api_dir\")\n latest=$(ls -t \"$api_dir\" 2>/dev/null | head -1)\n echo \" - $api_name (latest run: $latest)\"\ndone\n```\n\nUse `AskUserQuestion` to let the user pick.\n\n## Resolve which run\n\nIf the API has multiple runs, default to the most recent. If the user specified a\nrun ID, use that. Otherwise:\n\n```bash\nAPI_DIR=\"$PRESS_MANUSCRIPTS/$API_NAME\"\nRUN_ID=$(ls -t \"$API_DIR\" 2>/dev/null | head -1)\nRUN_DIR=\"$API_DIR/$RUN_ID\"\n\necho \"Retro for: $API_NAME (run $RUN_ID)\"\necho \"Manuscripts: $RUN_DIR\"\n```\n\n## Resolve CLI directory\n\n```bash\nAPI_SLUG=\"$API_NAME\"\nCLI_NAME=\"${API_SLUG}-pp-cli\"\nCLI_DIR=\"$PRESS_LIBRARY/$CLI_NAME\"\n\nif [ ! -d \"$CLI_DIR\" ]; then\n # Try without -pp-cli suffix (legacy naming)\n CLI_DIR=\"$PRESS_LIBRARY/$API_NAME\"\nfi\n\nif [ ! -d \"$CLI_DIR\" ]; then\n echo \"WARNING: CLI directory not found at $PRESS_LIBRARY/$CLI_NAME\"\n echo \"Proceeding with manuscripts only — CLI source will not be included in artifacts.\"\n CLI_DIR=\"\"\nfi\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3101,"content_sha256":"5a208eb7635364248ec1e59ddb2633e175c719feccd0044c5f21d2ede8690d1c"},{"filename":"references/secret-scrubbing.md","content":"# Secret & PII Scrubbing for Public Artifacts\n\nRead this file during Phase 5 (when writing the retro doc) and during Phase 6\n(before zipping artifacts AND before posting issue bodies to GitHub). All scrub\noperations work on **temp copies** of the retro doc and issue bodies, never on\nthe user's original manuscripts or library directories.\n\n## Layer 0: Single-file body scrub (retro doc + issue bodies)\n\nUse this layer when the input is a **single markdown file** (the retro doc at\nPhase 5, or each issue/comment body file at Phase 6 Step 3 before `gh issue\ncreate`/`gh issue comment`). It's the public-surface counterpart to Layers 1–4\n(which scrub directories of artifacts).\n\n**Why this layer exists.** Layers 1–4 scrub `$STAGING_MANUSCRIPTS` and\n`$STAGING_CLI_SOURCE` (the folders zipped and uploaded to catbox). They do\n**not** touch the retro doc itself or the issue body text passed to `gh issue\ncreate`. Without Layer 0, a finding's \"What we observed\" block can paste raw\nscanner output, dogfood payloads, or Greptile comments containing real secrets\nor PII straight to a public GitHub issue. The retro skill's cardinal rule\n(\"Issue bodies and retro docs are public surfaces\") is the human-readable\ncharter; this layer is the mechanical enforcement.\n\n### scrub_body — reusable shell function\n\nDefine this once at the top of the Phase 5 / Phase 6 bash blocks, then call\n`scrub_body \u003cinput> \u003coutput>` to produce a scrubbed copy of a single markdown\nfile. Returns exit codes that callers branch on.\n\n```bash\n# scrub_body \u003cin-file> \u003cout-file>\n#\n# Scans the input file for vendor-prefix tokens (HARD FAIL) and PII patterns\n# (auto-redact). Writes a scrubbed copy to \u003cout-file>. Behavior:\n#\n# exit 0 — clean (no findings, or only PII auto-redacted)\n# exit 1 — vendor-prefix token detected; refuses to write output. Caller\n# must hand-redact before proceeding. The offending file path\n# and pattern name go to stderr.\n# exit 2 — write failure or invalid arguments\n#\n# Hard-fail rationale: vendor-prefix tokens (API keys, OAuth tokens, JWTs)\n# are unrecoverable leaks once posted. Auto-redacting them would let an agent\n# inadvertently strip a real key the maintainer needs to know about\n# (e.g., \"the test fixture key on line 14 is real — rotate it\"). Forcing the\n# agent to redact-and-acknowledge keeps the human (or upstream agent) in the\n# loop. PII (emails, phones, real names) is auto-redacted because the\n# replacement is lossless for the retro use case.\nscrub_body() {\n local in=\"$1\" out=\"$2\"\n if [ -z \"$in\" ] || [ -z \"$out\" ] || [ ! -f \"$in\" ]; then\n echo \"scrub_body: usage: scrub_body \u003cin-file> \u003cout-file>\" >&2\n return 2\n fi\n\n # Layer 0a: vendor-prefix HARD-FAIL patterns. Order: most-specific first.\n # Mirrors the patterns in internal/artifacts/secrets.go and Layer 2 below,\n # extended with vendor patterns whose key shape is unambiguous enough to\n # anchor without high false-positive rates (Mailchimp's `-us\\d{1,2}`\n # datacenter suffix, Linear's `lin_api_` prefix, Anthropic's `sk-ant-api03-`\n # prefix). Add new patterns here only when the shape is specific enough\n # that the regex cannot match generic placeholder strings.\n local VENDOR_PATTERNS=(\n 'stripe-live-key|sk_live_[A-Za-z0-9]{20,}'\n 'stripe-test-key|sk_test_[A-Za-z0-9]{20,}'\n 'github-pat|ghp_[A-Za-z0-9]{36,}'\n 'github-oauth|gho_[A-Za-z0-9]{36,}'\n # Opaque ghs_ plus a three-segment JWT form. Do not fold into\n # [A-Za-z0-9._-]{36,}: that class swallows a following period.\n 'github-server-jwt|ghs_[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'\n 'github-server|ghs_[A-Za-z0-9]{36,}'\n 'slack-bot-token|xoxb-[A-Za-z0-9-]{20,}'\n 'slack-user-token|xoxp-[A-Za-z0-9-]{20,}'\n 'aws-access-key|\\bAKIA[0-9A-Z]{16}\\b'\n 'openrouter-key|sk-or-v1-[A-Za-z0-9_-]{24,}'\n 'anthropic-key|sk-ant-api03-[A-Za-z0-9_-]{40,}'\n 'linear-key|\\blin_api_[A-Za-z0-9_-]{32,}'\n 'mailchimp-key|\\b[a-f0-9]{32}-us[0-9]{1,2}\\b'\n 'jwt-token|\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}'\n 'bearer-with-value|Bearer [A-Za-z0-9._~+/=-]{30,}'\n )\n\n local hard_fail=0\n for entry in \"${VENDOR_PATTERNS[@]}\"; do\n IFS='|' read -r name regex \u003c\u003c\u003c \"$entry\"\n if grep -qE \"$regex\" \"$in\" 2>/dev/null; then\n # Surface the finding without quoting the value (the file path + pattern\n # name is enough for the agent to locate and fix; printing the value to\n # stderr defeats the whole point of refusing to write the output).\n lines=$(grep -nE \"$regex\" \"$in\" 2>/dev/null | cut -d: -f1 | head -5 | tr '\\n' ',' | sed 's/,$//')\n echo \"scrub_body: HARD FAIL — $name pattern matched in $in (lines: $lines)\" >&2\n hard_fail=1\n fi\n done\n if [ \"$hard_fail\" -eq 1 ]; then\n echo \"scrub_body: refusing to write $out. Hand-redact the matches above with \u003cREDACTED:\u003cvendor>-\u003ckind>:\u003cfirst4>...\u003clast4>:\u003clen>ch> per references/secret-scrubbing.md Layer 0, then retry.\" >&2\n return 1\n fi\n\n # Layer 0b: PII auto-redact patterns. Replaces in the written copy only;\n # input file is untouched. Tags use the same \u003cREDACTED:\u003ckind>> shape as the\n # rest of the scrubbing layers.\n #\n # Allowlist: RFC 2606 reserved example domains (example.com / example.net /\n # example.org / example.invalid / *.test / *.localhost) and NANP fictional\n # phone ranges (555-01XX) are PASS-THROUGH — they exist precisely to be safe\n # in documentation. The email regex anchors the allowlist by excluding the\n # reserved TLDs in the local-part match. The phone regex carves out the\n # 555-01XX range explicitly.\n cp \"$in\" \"$out\" 2>/dev/null || { echo \"scrub_body: failed to copy $in -> $out\" >&2; return 2; }\n\n # Order matters: specific account-identifier patterns first, then generic\n # email/phone/zip. Otherwise the generic `email` pattern would consume the\n # mailchimp-inbox-id email-shaped string and tag it as `\u003cREDACTED:email>`\n # instead of `\u003cREDACTED:mailchimp-inbox-id>` — both redact the leak, but\n # the specific tag is more useful for diagnostics.\n\n # Mailchimp inbox-id: us\u003cdc>-\u003chex>-\u003chex>@inbound.mailchimp.com is the\n # account-bound inbound mailbox identifier — leaking it exposes the\n # account's identity. Hex segments vary by account; 8+ chars each keeps\n # the false-positive rate near zero (the @inbound.mailchimp.com tail is\n # the strongest discriminator).\n perl -i -pe 's/\\bus\\d{1,2}-[a-f0-9]{8,}-[a-f0-9]{8,}\\@inbound\\.mailchimp\\.com\\b/\u003cREDACTED:mailchimp-inbox-id>/g' \"$out\" 2>/dev/null\n\n # Real email (excluding RFC 2606 allowlist). The exclusion uses negative\n # lookbehind via perl since BSD/GNU sed regex flavors differ.\n perl -i -pe 's/\\b([A-Za-z0-9._%+-]+)@(?!example\\.(?:com|net|org|invalid)\\b|[^\\s]*\\.(?:test|localhost|example)\\b)([A-Za-z0-9.-]+\\.[A-Za-z]{2,})\\b/\u003cREDACTED:email>/g' \"$out\" 2>/dev/null\n\n # NANP US phone (excluding 555-01XX fictional range). Matches 10-digit forms\n # with or without dashes/parens/spaces. The 555-01XX exclusion can't use a\n # single negative lookahead anchored at the start because the optional `(`\n # and optional country-code prefix shift the area-code position — when the\n # number is written `(555) 012-3456`, the position before the `(` is not\n # where the `555` lives, so the lookahead never fires. Instead, capture the\n # area code ($1) and exchange ($2) and decide inline via `/e`.\n #\n # Perl gotcha: {head-tags} and $1..$N are global variables that get rewritten by\n # every successful regex match — including the inner `$e =~ /^01/` test\n # inside the replacement callback. Snapshot {head-tags}, $1, $2 into lexical\n # variables ($w, $a, $e) BEFORE running the inner regex, otherwise the\n # carve-out path returns `\"01\"` (the inner match) instead of the whole\n # original phone string.\n perl -i -pe 's{(?\u003c![0-9])(?:\\+?1[-\\s.]?)?\\(?([2-9][0-9]{2})\\)?[-\\s.]?([0-9]{3})[-\\s.]?[0-9]{4}(?![0-9])}{my $w={head-tags}; my $a=$1; my $e=$2; ($a eq \"555\" && $e =~ /^01/) ? $w : \"\u003cREDACTED:phone-us>\"}ge' \"$out\" 2>/dev/null\n\n # ZIP+4 (5 digits, dash, 4 digits). Bare 5-digit ZIPs are too noisy to redact\n # safely (false-positives on order IDs, line counts, etc.); ZIP+4 is the\n # dashed form that's unambiguous.\n perl -i -pe 's/\\b\\d{5}-\\d{4}\\b/\u003cREDACTED:zip-plus-4>/g' \"$out\" 2>/dev/null\n\n return 0\n}\n```\n\n### When to call scrub_body\n\n**Phase 5 — after writing the retro doc:**\n\n```bash\n# Write retro to $RETRO_PROOF_PATH and $RETRO_SCRATCH_PATH as before, then:\nRETRO_PROOF_PATH_SCRUBBED=\"${RETRO_PROOF_PATH}.scrubbed.md\"\nif ! scrub_body \"$RETRO_PROOF_PATH\" \"$RETRO_PROOF_PATH_SCRUBBED\"; then\n echo \"ERROR: retro doc contains an unredacted vendor-prefix secret.\" >&2\n echo \"Open $RETRO_PROOF_PATH, redact the matches reported above per references/secret-scrubbing.md Layer 0, then re-run Phase 5.\" >&2\n exit 1\nfi\nmv \"$RETRO_PROOF_PATH_SCRUBBED\" \"$RETRO_PROOF_PATH\"\ncp \"$RETRO_PROOF_PATH\" \"$RETRO_SCRATCH_PATH\"\n```\n\nThe scrubbed retro doc becomes canonical. The original (with potential PII)\nexisted in process memory only and is overwritten by the scrubbed version.\n\n**Phase 6 Step 3 — before `gh issue create` and `gh issue comment`:**\n\n```bash\n# Build the WU body as before to /tmp/wu1-body.md, then:\nif ! scrub_body /tmp/wu1-body.md /tmp/wu1-body-scrubbed.md; then\n echo \"ERROR: WU body contains an unredacted vendor-prefix secret. Cannot post.\" >&2\n # Mark this WU as a failed action in $FAILED_ISSUES so Step 6 surfaces it.\n FAILED_ISSUES+=\"WU-1 (issue create): scrub_body hard-failed; body file left at /tmp/wu1-body.md for manual redaction.\\n\"\n continue\nfi\ngh issue create ... --body-file /tmp/wu1-body-scrubbed.md\n```\n\nSame pattern for `gh issue comment`. If `scrub_body` hard-fails, that WU's\nfiling is skipped and reported in the final summary so the agent (or user)\nknows to manually redact and retry.\n\n### Redaction shape reference\n\nWhen the agent (or a maintainer reviewing the retro doc) needs to hand-redact\na vendor-prefix value, use this format:\n\n```\n\u003cREDACTED:\u003cvendor>-\u003ckind>:\u003cfirst4>...\u003clast4>:\u003clen>ch>\n```\n\nExamples:\n\n| Original (anti-pattern) | Redacted (correct) |\n|---|---|\n| `lin_api_a1b2c3d4e5f6...bcdef0123456` | `\u003cREDACTED:linear-api-key:lin_a...0456:48ch>` |\n| `22eb323ac258...ade77b9c78-us6` | `\u003cREDACTED:mailchimp-api-key:22eb...-us6:36ch>` |\n| `ghp_abc123def456...pqr678stu90` | `\u003cREDACTED:github-pat:ghp_a...tu90:40ch>` |\n| `sk_live_51AbCdEf123456...XyZ` | `\u003cREDACTED:stripe-live-key:sk_li...XyZ:N-ch>` |\n\nThe first4 + last4 + length fragment preserves enough shape information for a\nmaintainer to recognize the vendor pattern (and to confirm the same key isn't\nrecurring across multiple findings) without re-exposing the value. The\n`\u003ckind>` tag is enough to drive a follow-up retro finding (\"the scanner missed\na `linear-api-key`\") without quoting the value.\n\nFor PII (`email`, `phone-us`, `zip-plus-4`, `mailchimp-inbox-id`, etc.),\njust the `\u003ckind>` tag — no fragment — is the right shape. The maintainer\ndoesn't need to identify which specific email; they need to know an email\nshape was leaked.\n\n### Things to never quote even in redacted form\n\n- **Full URLs containing tokens.** GitHub's secret-scanning \"unblock-secret\" URLs (e.g., `https://github.com/\u003corg>/\u003crepo>/security/secret-scanning/unblock-secret/\u003ctoken-id>`) embed a token-id that is itself a credential GitHub uses to authorize the bypass. Even with the token-id redacted, the URL leaks the org/repo/PR triple that the secret-scanning event refers to. Replace the whole URL with `\u003cREDACTED:gh-secret-scanning-url>`.\n- **Authorization-header values** in HAR / dogfood-full.json snippets — even when the value looks like a placeholder, treat it as live until proven otherwise. Replace the whole header line with `\u003cREDACTED:authorization-header>`.\n- **Account-bound inbound mailboxes / webhook callback URLs / OAuth client-secret pairs.** These are not \"secrets\" in the API-key sense but identifying the account is a real privacy regression. Tag and redact.\n\n## Layer 1: Exact-value scanning\n\nIf the user provided an API key during the generation session, scan for its literal\nvalue and redact it. This has zero false positives.\n\nUse `grep -F` (fixed string) — NOT bare `grep` or `sed` — because API keys often\ncontain regex metacharacters (`+`, `/`, `.`, `=`).\n\n```bash\n# Guard: skip if key is empty or too short (\u003c 16 chars)\nif [ -n \"$API_KEY_VALUE\" ] && [ ${#API_KEY_VALUE} -ge 16 ]; then\n LEAK_FOUND=false\n for dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n if [ -d \"$dir\" ] && grep -rF \"$API_KEY_VALUE\" \"$dir\" 2>/dev/null; then\n LEAK_FOUND=true\n fi\n done\n if [ \"$LEAK_FOUND\" = true ]; then\n echo \"BLOCKING: API key value found in staging artifacts. Auto-redacting.\"\n REDACT_TO='\u003cREDACTED:session-api-key>'\n for dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n [ -d \"$dir\" ] || continue\n find \"$dir\" -type f -print0 | while IFS= read -r -d '' f; do\n if grep -qF \"$API_KEY_VALUE\" \"$f\" 2>/dev/null; then\n REDACT_OLD=\"$API_KEY_VALUE\" REDACT_NEW=\"$REDACT_TO\" python3 -c \"\nimport sys, os\nold, new, path = os.environ['REDACT_OLD'], os.environ['REDACT_NEW'], sys.argv[1]\nwith open(path) as f: content = f.read()\nwith open(path, 'w') as f: f.write(content.replace(old, new))\n\" \"$f\"\n fi\n done\n done\n echo \"Auto-redacted session API key.\"\n fi\nfi\n```\n\n## Layer 2: Pattern-based scanning\n\nScan for common secret formats regardless of whether a session key was provided.\nEach pattern uses `grep -rE` with a concrete regex and a labeled redaction tag.\n\nRun each pattern scan independently. A false positive from one pattern does not\naffect other scans.\n\n```bash\n# Define patterns: name|regex|redaction-tag\nPATTERNS=(\n 'stripe-live-key|sk_live_[A-Za-z0-9]{20,}|\u003cREDACTED:stripe-live-key>'\n 'stripe-test-key|sk_test_[A-Za-z0-9]{20,}|\u003cREDACTED:stripe-test-key>'\n 'github-pat|ghp_[A-Za-z0-9]{36,}|\u003cREDACTED:github-pat>'\n 'github-oauth|gho_[A-Za-z0-9]{36,}|\u003cREDACTED:github-oauth>'\n 'bearer-token|Bearer [A-Za-z0-9._~+/=-]{20,}|\u003cREDACTED:bearer-token>'\n 'jwt-token|eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}|\u003cREDACTED:jwt-token>'\n)\n\nfor entry in \"${PATTERNS[@]}\"; do\n IFS='|' read -r name regex tag \u003c\u003c\u003c \"$entry\"\n for dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n [ -d \"$dir\" ] || continue\n find \"$dir\" -type f -print0 | while IFS= read -r -d '' f; do\n if grep -qE \"$regex\" \"$f\" 2>/dev/null; then\n # Use perl for in-place regex replacement (more reliable than sed across platforms)\n perl -i -pe \"s/$regex/$tag/g\" \"$f\" 2>/dev/null\n echo \"Redacted $name in $(basename \"$f\")\"\n fi\n done\n done\ndone\n```\n\n### Jurisdiction-specific PII scanning\n\nLive-API responses captured during browser-sniff or live-key dogfood routinely\ninclude identifying data of the data subject and any third parties the API\nsurfaced. These patterns redact common high-confidence shapes before upload.\nThey are defense-in-depth, not bulletproof — free-form names, descriptive\nfields, and unenumerated jurisdictions still slip through.\n\nThe `|` field separator collides with the `(IT|DE|...)` country-code\nalternation in the IBAN regex, so PII patterns use `~` as the field separator.\n\n```bash\nPII_PATTERNS=(\n 'codice-fiscale~\\b[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]\\b~\u003cREDACTED:pii-codice-fiscale>'\n 'eu-iban~\\b(AD|AT|BE|BG|CH|CY|CZ|DE|DK|EE|ES|FI|FR|GB|GI|GR|HR|HU|IE|IS|IT|LI|LT|LU|LV|MC|MT|NL|NO|PL|PT|RO|SE|SI|SK|SM|VA)[0-9]{2}[A-Z0-9]{11,30}\\b~\u003cREDACTED:pii-eu-iban>'\n 'us-ssn~\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b~\u003cREDACTED:pii-us-ssn>'\n)\n\nfor entry in \"${PII_PATTERNS[@]}\"; do\n IFS='~' read -r name regex tag \u003c\u003c\u003c \"$entry\"\n for dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n [ -d \"$dir\" ] || continue\n find \"$dir\" -type f -print0 | while IFS= read -r -d '' f; do\n # Case-insensitive: API JSON routinely lowercases IBANs and other identifiers.\n if grep -qiE \"$regex\" \"$f\" 2>/dev/null; then\n perl -i -pe \"s/$regex/$tag/gi\" \"$f\" 2>/dev/null\n echo \"Redacted $name in $(basename \"$f\")\"\n fi\n done\n done\ndone\n```\n\nPattern notes:\n\n- **Codice Fiscale** (Italian tax code) is a 16-character `LLLLLLDDLDDLDDDL` shape with no plausible collision against ordinary text.\n- **EU IBAN** is anchored to the SEPA country-code prefix list, so the broad `[A-Z0-9]{11,30}` body cannot match a generic phone number, order ID, or vendor SKU.\n- **US SSN** uses the `DDD-DD-DDDD` dashed form, which avoids collisions with bare 9-digit runs in other identifiers.\n\nOut of scope (deferred to follow-up work):\n\n- Bare 11-digit Partita IVA / VAT numbers (false-positive rate against order IDs, phone numbers, and timestamps is too high without an allowlist).\n- Free-form residential addresses (not regex-matchable with acceptable precision).\n- Refusing upload when `discovery/sample-*.json` files are present (a separate gate at the staging-copy step, tracked separately).\n\n### Env var assignment scanning\n\nSeparately scan for hardcoded secret assignments in source code:\n\n```bash\n# Matches assignments where the variable name ENDS with a secret-like suffix:\n# API_SECRET = \"value\", AUTH_TOKEN: 'value', API_KEY=\"value\", DB_PASSWORD='value'\n# Does NOT match: CACHE_KEY, PRIMARY_KEY, TOKEN_EXPIRY (keyword is not a suffix)\n# Only in .go, .env, .yaml, .yml, .json, .toml files\nSECRET_ASSIGN_REGEX='[A-Z_]+(SECRET|_TOKEN|_KEY|PASSWORD)\\s*[:=]\\s*[\"'\"'\"'][^\"'\"'\"']{16,}[\"'\"'\"']'\n\nfor dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n [ -d \"$dir\" ] || continue\n find \"$dir\" -type f \\( -name \"*.go\" -o -name \"*.env\" -o -name \"*.yaml\" -o -name \"*.yml\" -o -name \"*.json\" -o -name \"*.toml\" \\) -print0 | while IFS= read -r -d '' f; do\n if grep -qE \"$SECRET_ASSIGN_REGEX\" \"$f\" 2>/dev/null; then\n perl -i -pe 's/[A-Z_]+(SECRET|_TOKEN|_KEY|PASSWORD)\\s*[:=]\\s*[\"\\x27][^\"\\x27]{16,}[\"\\x27]/$1=\u003cREDACTED:env-assignment>/g' \"$f\" 2>/dev/null\n echo \"Redacted env assignment in $(basename \"$f\")\"\n fi\n done\ndone\n```\n\n## Layer 3: HAR auth stripping\n\nIf the staging manuscripts contain HAR files, strip auth-bearing fields:\n\n```bash\nfind \"$STAGING_MANUSCRIPTS\" -name \"*.har\" -type f -print0 2>/dev/null | while IFS= read -r -d '' har; do\n jq 'del(.log.entries[].response.content.text) |\n (.log.entries[].request.headers) |= [.[] |\n select(.name | test(\"^(Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token)$\"; \"i\") | not)\n ] |\n (.log.entries[].response.headers) |= [.[] |\n select(.name | test(\"^(Set-Cookie)$\"; \"i\") | not)\n ] |\n (.log.entries[].request.queryString) |= [.[] |\n if (.name | test(\"^(key|api_key|apikey|token|secret|access_token|password)$\"; \"i\"))\n then .value = \"\u003cREDACTED>\"\n else . end\n ] |\n (.log.entries[].request.cookies) |= [] |\n (.log.entries[].response.cookies) |= []\n ' \"$har\" > \"${har}.stripped\" 2>/dev/null && mv \"${har}.stripped\" \"$har\"\n echo \"Stripped auth from $(basename \"$har\")\"\ndone\n```\n\n## Layer 4: Session state cleanup\n\nRemove any session state files that may contain cookies or tokens:\n\n```bash\nfind \"$STAGING_MANUSCRIPTS\" -name \"session-state.json\" -type f -delete 2>/dev/null\n```\n\n## Post-scrub verification\n\nAfter all layers complete, do a final scan for obvious leaks:\n\n```bash\nFINAL_CHECK=false\n# CRED_REGEX must mirror the vendor-prefix patterns in Layer 0 and Layer 2\n# above; update both together so the verification step does not silently stop\n# checking a credential shape the scrub loop still redacts. The JWT shape\n# intentionally matches Layer 2's two-segment form; that also catches the\n# first two segments of a conventional three-segment JWT.\nCRED_REGEX='(sk_live_[A-Za-z0-9]{20,}|sk_test_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,}|ghs_[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+|ghs_[A-Za-z0-9]{36,}|xoxb-[A-Za-z0-9-]{20,}|xoxp-[A-Za-z0-9-]{20,}|\\bAKIA[0-9A-Z]{16}\\b|sk-or-v1-[A-Za-z0-9_-]{24,}|sk-ant-api03-[A-Za-z0-9_-]{40,}|\\blin_api_[A-Za-z0-9_-]{32,}|\\b[a-f0-9]{32}-us[0-9]{1,2}\\b|\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}|Bearer [A-Za-z0-9._~+/=-]{20,})'\n# PII_REGEX must mirror the shapes in PII_PATTERNS above; update both together\n# (e.g. when adding Partita IVA with an allowlist) so the verification step\n# does not silently stop checking a shape the scrub loop still redacts.\nPII_REGEX='(\\b[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]\\b|\\b(AD|AT|BE|BG|CH|CY|CZ|DE|DK|EE|ES|FI|FR|GB|GI|GR|HR|HU|IE|IS|IT|LI|LT|LU|LV|MC|MT|NL|NO|PL|PT|RO|SE|SI|SK|SM|VA)[0-9]{2}[A-Z0-9]{11,30}\\b|\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b)'\nSCAN_FAILED=false\nfor dir in \"$STAGING_MANUSCRIPTS\" \"$STAGING_CLI_SOURCE\"; do\n [ -d \"$dir\" ] || continue\n if CRED_RAW=$(grep -rEi \"$CRED_REGEX\" \"$dir\" 2>/dev/null); then\n :\n else\n CRED_STATUS=$?\n if [ \"$CRED_STATUS\" -gt 1 ]; then\n echo \"WARNING: Credential scan could not read all files under $dir.\"\n SCAN_FAILED=true\n fi\n fi\n if PII_RAW=$(grep -rEi \"$PII_REGEX\" \"$dir\" 2>/dev/null); then\n :\n else\n PII_STATUS=$?\n if [ \"$PII_STATUS\" -gt 1 ]; then\n echo \"WARNING: PII scan could not read all files under $dir.\"\n SCAN_FAILED=true\n fi\n fi\n CRED_MATCHES=$(printf '%s\\n' \"$CRED_RAW\" | grep -v 'REDACTED' | head -5)\n PII_MATCHES=$(printf '%s\\n' \"$PII_RAW\" | grep -v 'REDACTED' | head -5)\n if [ -n \"$CRED_MATCHES\" ] || [ -n \"$PII_MATCHES\" ]; then\n [ -n \"$CRED_MATCHES\" ] && echo \"$CRED_MATCHES\"\n [ -n \"$PII_MATCHES\" ] && echo \"$PII_MATCHES\"\n FINAL_CHECK=true\n fi\ndone\nif [ \"$SCAN_FAILED\" = true ]; then\n FINAL_CHECK=true\n echo \"WARNING: One or more post-scrub scans did not complete successfully.\"\n echo \"Artifacts will NOT be uploaded until the scan errors are resolved.\"\nfi\nif [ \"$FINAL_CHECK\" = true ]; then\n echo \"WARNING: Potential secrets or PII still found after scrubbing. Review the matches above.\"\n echo \"Artifacts will NOT be uploaded until this is resolved.\"\nfi\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21839,"content_sha256":"57b74f9b7cfb7f77433cb6ced9ea98a4915f57dc4e092320d2a76978d49b4838"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"/printing-press-retro","type":"text"}]},{"type":"paragraph","content":[{"text":"Result:","type":"text","marks":[{"type":"strong"}]},{"text":" A scrubbed retro document and, when the issue gate passes, GitHub issues for Printing Press defects only.","type":"text"}]},{"type":"paragraph","content":[{"text":"Next consumer:","type":"text","marks":[{"type":"strong"}]},{"text":" A maintainer or agent who will change the Printing Press (generator, scorer, skills, or binary) — not the printed CLI that just shipped.","type":"text"}]},{"type":"paragraph","content":[{"text":"Done:","type":"text","marks":[{"type":"strong"}]},{"text":" The manuscript proof exists and is scrubbed; every work unit is file-new, comment-on-existing, or local-only; no unredacted secret or PII remains; the user has the outcome list.","type":"text"}]},{"type":"paragraph","content":[{"text":"Intent:","type":"text","marks":[{"type":"strong"}]},{"text":" \"Raised the floor\" is analysis language, not a filing bar. File only when the Printing Press must change to stop a current, reproducible, generalizing P1 or P2 defect. Manual iteration on one CLI is expected work, not a finding. In user-facing output say \"the Printing Press\"; name the subsystem when pointing at a fix.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Boundaries","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Redact","type":"text","marks":[{"type":"strong"}]},{"text":" every real secret and PII before quoting. Issue bodies and retro docs are public. Use ","type":"text"},{"text":"references/secret-scrubbing.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/secret-scrubbing.md","title":null}}]},{"text":" Layer 0. Never quote a leaked value as evidence of the leak.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't change the machine","type":"text","marks":[{"type":"strong"}]},{"text":" by default. The burden of proof is on the finding.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No P3.","type":"text","marks":[{"type":"strong"}]},{"text":" File only P1/P2. Anything else is Skip or Drop, not a low-priority issue. The issue gate files P1/P2 Do work units only.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never upload unscrubbed artifacts.","type":"text","marks":[{"type":"strong"}]},{"text":" Never modify manuscript or library source trees; scrub copies only.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Authority","type":"text"}]},{"type":"paragraph","content":[{"text":"Invocation authorizes reading manuscripts/library and writing the retro proof plus scratch copies. It does not authorize GitHub filing or public upload until the user confirms Submit. It does not authorize edits to the printed CLI or to Printing Press source.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Steps","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/run-resolution.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/run-resolution.md","title":null}}]},{"text":" first. It resolves ","type":"text"},{"text":"$API_NAME","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"$RUN_ID","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"$RUN_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"$CLI_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"$IN_REPO","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Never execute a phase from memory. When you enter a phase, Read its file from phases/ first.","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"Step","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"File","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/01-gather-evidence.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/01-gather-evidence.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/02-mine-the-session.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/02-mine-the-session.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/03-triage-candidates.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/03-triage-candidates.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/04-classify-findings.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/04-classify-findings.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/05-prioritize.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/05-prioritize.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/06-write-the-retro.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/06-write-the-retro.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/07-plannable-work-units.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/07-plannable-work-units.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/08-issue-gate.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/08-issue-gate.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":"right"},"content":[{"type":"paragraph","content":[{"text":"9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phases/09-package-upload-present.md","type":"text","marks":[{"type":"link","attrs":{"href":"phases/09-package-upload-present.md","title":null}}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Follow each file's final ","type":"text"},{"text":"Next:","type":"text","marks":[{"type":"code_inline"}]},{"text":" pointer. Late procedure lives in the phase file or a role-named reference it loads (","type":"text"},{"text":"references/artifact-packaging.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/artifact-packaging.md","title":null}}]},{"text":", ","type":"text"},{"text":"references/issue-template.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/issue-template.md","title":null}}]},{"text":", ","type":"text"},{"text":"references/secret-scrubbing.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/secret-scrubbing.md","title":null}}]},{"text":"). Do not restate those files here.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"printing-press-retro","author":"@skillopedia","source":{"stars":4904,"repo_name":"cli-printing-press","origin_url":"https://github.com/mvanhorn/cli-printing-press/blob/HEAD/skills/printing-press-retro/SKILL.md","repo_owner":"mvanhorn","body_sha256":"b4036df2dcd90e9c97744f9524dd481aef8fe546188e01497c1823be53777e20","cluster_key":"d2b632cad974af582d93db6f754d85dbe087bc0ca7a35b4263203ccf8f7e971f","clean_bundle":{"format":"clean-skill-bundle-v1","source":"mvanhorn/cli-printing-press/skills/printing-press-retro/SKILL.md","text_bytes":109481,"source_commit":"f95635823251564c2c07679be45be40c31b4fd7a","skills_sh_rank":3507,"attachment_count":13,"text_attachments":13,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/printing-press-retro/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"0.1.0","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"0.1.0","category":"general","created_by":"user","import_tag":"skills-sh-top20k-2026-11","description":"Use when the user asks to retro, run a retrospective, file findings, or improve the Printing Press after a printed-CLI run. Trigger phrases: \"retro\", \"retrospective\", \"what went wrong\", \"improve the press\", \"post-mortem\", \"lessons learned\", \"what can we improve\", \"file a retro\", \"submit findings\". Do not use for printed-CLI polish or a single-CLI fix.\n","allowed-tools":["Bash","Read","Glob","Grep","Write","Agent","AskUserQuestion"]}},"renderedAt":1793666567058}

/printing-press-retro Result: A scrubbed retro document and, when the issue gate passes, GitHub issues for Printing Press defects only. Next consumer: A maintainer or agent who will change the Printing Press (generator, scorer, skills, or binary) — not the printed CLI that just shipped. Done: The manuscript proof exists and is scrubbed; every work unit is file-new, comment-on-existing, or local-only; no unredacted secret or PII remains; the user has the outcome list. Intent: "Raised the floor" is analysis language, not a filing bar. File only when the Printing Press must change to stop a curr…