
Printing Press Retro
- 4.8k installs
- 4.3k repo stars
- Updated July 23, 2026
- mvanhorn/cli-printing-press
printing-press-retro is an agent skill: Run a retrospective after generating a CLI. Identifies systemic improvementsto the Printing Press — templates, Go binary, skill instructions
About
The printing-press-retro skill Run a retrospective after generating a CLI. Identifies systemic improvementsto the Printing Press — templates, Go binary, skill instructions, catalog —so the next CLI comes out better. Creates a GitHub issue with actionablefindings when there are Printing Press fixes to make.Use after any /printing-press run.Trigger phrases: "retro", "retrospective", "what went wrong", "improvethe press", "post-mortem", "lessons learned", "what can we improve","file a retro", "submit findings".. /printing-press-retro Analyze a Printing Press session to find ways to improve the system that produces CLIs — the Go binary, templates, skills, and catalog. Not fixes to the specific CLI that was just printed, but improvements so the *next* CLI comes out stronger. **It is a non-goal for the Printing Press to produce flawless CLIs without manual tweaks.** That's the nature of the system. We expect agents to reason over the generated CLI, customize for the specific API, build novel features, and iterate. Some hand-built work in every run is
- Covers printing-press-retro quick start, workflow steps, and reference pointers from SKILL.md.
- Tagged for stage operate and subphase iterate in the closed Skillselion taxonomy.
- Documents prerequisites, permissions shell, filesystem, git, and compatible agents.
- Includes AEO tagMeta with task queries, keywords, and evidence quotes for discovery.
- Cross-links related skills and generated REFERENCE.md tables where the repo provides them.
Printing Press Retro by the numbers
- 4,759 all-time installs (skills.sh)
- +258 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #40 of 560 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
printing-press-retro capabilities & compatibility
- Capabilities
- printing press retro documented workflow · quick start examples · reference parameter lookup · taxonomy aligned metadata · aeo discovery fields
- Works with
- github
- Use cases
- documentation · orchestration
npx skills add https://github.com/mvanhorn/cli-printing-press --skill printing-press-retroAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.8k |
|---|---|
| repo stars | ★ 4.3k |
| Security audit | 0 / 3 scanners passed |
| Last updated | July 23, 2026 |
| Repository | mvanhorn/cli-printing-press ↗ |
How do I run printing-press-retro correctly without guessing steps, tools, or parameters?
Run a retrospective after generating a CLI. Identifies systemic improvementsto the Printing Press — templates, Go binary, skill instructions, catalog —so the next CLI comes out better. Creates a
Who is it for?
Teams using printing-press-retro when SKILL.md triggers match the user request.
Skip if: Skip when the task is outside printing-press-retro documented triggers or sibling skill scope.
When should I use this skill?
User mentions printing-press-retro, related trigger phrases, or asks to follow this SKILL.md workflow.
What you get
Completed printing-press-retro workflow with outputs and checks defined in SKILL.md.
- printing-press-retro output per SKILL.md
By the numbers
- Stage operate/iterate
- Category CLI & Terminal
- Complexity advanced
Files
/printing-press-retro
Analyze a Printing Press session to find ways to improve the system that produces CLIs — the Go binary, templates, skills, and catalog. Not fixes to the specific CLI that was just printed, but improvements so the next CLI comes out stronger.
It is a non-goal for the Printing Press to produce flawless CLIs without manual tweaks. That's the nature of the system. We expect agents to reason over the generated CLI, customize for the specific API, build novel features, and iterate. Some hand-built work in every run is normal.
The retro's job is to find the subset of manual work where the machine could have realistically raised the floor — given the agent a better starting point, prevented the issue entirely, or eliminated friction that would recur on the next CLI. Two clear cases qualify:
1. The machine could have completely prevented the issue, and the pattern is generalizable across many printed CLIs. File it. 2. The machine could have raised the floor meaningfully — better default, partial scaffold, helper that absorbs the boilerplate — across multiple CLIs you can name with evidence. File it.
Otherwise, the manual work is normal iteration and should not generate a finding. Some items make it back as machine fixes; not all. The retro is the filter that distinguishes the two.
The retro creates a GitHub issue on the printing-press repo with the findings that survive triage and the adversarial check, plus artifacts, so maintainers (or an AI agent) can fix the Printing Press.
Terminology
- The Printing Press: The whole system that produces CLIs. Use this name in all
user-facing output (issues, retros, prompts). It has four subsystems:
- Generator — templates that emit Go code (
internal/generator/) - Scorer — tools that grade the output: verify, dogfood, scorecard
- Skills — SKILL.md instructions that guide Claude during generation
- Binary — the Go CLI itself: commands, flags, parsers (
cmd/cli-printing-press/) - Printed CLI: A CLI produced by the Printing Press for a specific API (e.g.,
notion-pp-cli). Printed-CLI fixes only help that one CLI.
Use "the Printing Press" when talking about the system. Use the subsystem name when pointing a developer at what to fix — "fix the scorer" and "fix the generator" are different PRs.
Cardinal rules
- Issue bodies and retro docs are public surfaces. Redact every real secret and PII before quoting. Manuscripts contain credentials, account identifiers, real emails, and live API response data — that's why
references/secret-scrubbing.mdscrubs them before artifact upload. Issue body text goes straight to a public GitHub issue, and the retro doc itself is preserved in manuscript proofs and may be uploaded as a zip. When you quote scanner output, dogfood payloads, Greptile review comments, or API response bodies as "evidence," replace the sensitive substring with<REDACTED:<kind>>BEFORE pasting. This applies hardest to findings about secret/PII leaks: the natural impulse is to quote the actual leaked value to prove the leak exists — that re-leaks it in a public issue. Phase 5 (retro doc write) and Phase 6 (pre-post scrub) enforce this mechanically; this rule is the human-readable charter behind the mechanical enforcement. See `references/secret-scrubbing.md` "Layer 0" for the redaction patterns and substitution shapes. - Default is "don't change the machine." The Printing Press is mature — 30+ CLIs printed, most templates exercised across many shapes. The burden of proof is on the finding, not on the Skip path. Most things you encountered while printing one CLI are that CLI's quirks, iteration noise, or upstream API behavior — not generator gaps. Propose a machine change only when cross-CLI evidence is concrete and the finding survives the Phase 3 adversarial check (Step G).
- A retro of three sharp findings is more valuable than ten mixed-quality findings. Each filed finding spends maintainer attention. If you find yourself writing "every finding warrants action" or producing zero drops and zero skips, stop and re-triage — that outcome is the failure mode this skill exists to prevent.
- The retro proposes Printing Press changes that help multiple printed CLIs. Don't propose direct edits to the one CLI that just shipped, and don't propose machine changes whose value is unique to this CLI's quirks — those are printed-CLI fixes wearing a generator costume.
- Never upload un-scrubbed artifacts. All artifacts go through the secrets scrub before upload.
- Never modify source directories. Manuscripts and library directories are read-only. Scrub operations work on temporary copies.
- Never skip the secrets scrub, even if the generation pipeline already ran one. Defense in depth.
- Never work around a scorer bug in the Printing Press. If a scoring tool penalizes something incorrectly, the fix goes in the scoring tool.
Setup
<!-- RETRO_SETUP_START -->
# Path-only setup — no binary detection required.
# The retro skill reads manuscripts and runs gh/curl. It does not invoke the
# cli-printing-press binary. This avoids aborting for users who installed the
# plugin but not the Go binary.
_scope_dir="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
_scope_dir="$(cd "$_scope_dir" && pwd -P)"
PRESS_HOME="${PRINTING_PRESS_HOME:-$HOME/printing-press}"
PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts"
PRESS_LIBRARY="$PRESS_HOME/library"
RETRO_SCRATCH_DIR="/tmp/printing-press/retro"
mkdir -p "$PRESS_MANUSCRIPTS" "$PRESS_LIBRARY" "$RETRO_SCRATCH_DIR"
# Detect whether we're inside the printing-press repo
IN_REPO=false
if [ -f "$_scope_dir/cmd/cli-printing-press/main.go" ]; then
IN_REPO=true
REPO_ROOT="$_scope_dir"
echo "Running from printing-press repo: $REPO_ROOT"
fi<!-- RETRO_SETUP_END -->
Guard rails
Nothing to retro
if [ ! -d "$PRESS_MANUSCRIPTS" ] || [ -z "$(ls -A "$PRESS_MANUSCRIPTS" 2>/dev/null)" ]; then
echo "No manuscripts found. Run /printing-press first to generate a CLI."
exit 1
fiResolve which API
If the user passed an API name as an argument, use that. Validate for path traversal:
# Reject names with /, \, or ..
if echo "$USER_API_NAME" | grep -qE '[/\\]|\.\.'; then
echo "Invalid API name: '$USER_API_NAME'. Names cannot contain path separators or '..'."
exit 1
fi
# Verify resolved path stays under PRESS_MANUSCRIPTS
RESOLVED="$(cd "$PRESS_MANUSCRIPTS/$USER_API_NAME" 2>/dev/null && pwd -P)"
case "$RESOLVED" in
"$PRESS_MANUSCRIPTS"/*) ;; # OK
*) echo "Invalid API name: path resolves outside manuscripts directory."; exit 1 ;;
esacIf no API name was provided and multiple APIs exist, list them with their most recent run dates and ask the user to choose:
echo "Multiple APIs found in manuscripts:"
for api_dir in "$PRESS_MANUSCRIPTS"/*/; do
api_name=$(basename "$api_dir")
latest=$(ls -t "$api_dir" 2>/dev/null | head -1)
echo " - $api_name (latest run: $latest)"
doneUse AskUserQuestion to let the user pick.
Resolve which run
If the API has multiple runs, default to the most recent. If the user specified a run ID, use that. Otherwise:
API_DIR="$PRESS_MANUSCRIPTS/$API_NAME"
RUN_ID=$(ls -t "$API_DIR" 2>/dev/null | head -1)
RUN_DIR="$API_DIR/$RUN_ID"
echo "Retro for: $API_NAME (run $RUN_ID)"
echo "Manuscripts: $RUN_DIR"Resolve CLI directory
API_SLUG="$API_NAME"
CLI_NAME="${API_SLUG}-pp-cli"
CLI_DIR="$PRESS_LIBRARY/$CLI_NAME"
if [ ! -d "$CLI_DIR" ]; then
# Try without -pp-cli suffix (legacy naming)
CLI_DIR="$PRESS_LIBRARY/$API_NAME"
fi
if [ ! -d "$CLI_DIR" ]; then
echo "WARNING: CLI directory not found at $PRESS_LIBRARY/$CLI_NAME"
echo "Proceeding with manuscripts only — CLI source will not be included in artifacts."
CLI_DIR=""
fiWhen to run
Best results come from running in the same conversation where the CLI was generated (post-shipcheck) — the retro can mine the full conversation history for errors, retries, manual edits, and discoveries.
If running in a fresh conversation, the retro proceeds with manuscript evidence only. Phase 2 marks session-dependent findings as "evidence: manuscripts only."
Phase 1: Gather evidence
Read all artifacts from the run:
1. Research brief — $RUN_DIR/research/*brief* 2. Absorb manifest — $RUN_DIR/research/*absorb* 3. Shipcheck proof — $RUN_DIR/proofs/*shipcheck* 4. Build log — $RUN_DIR/proofs/*build-log* (if exists) 5. Live smoke log — $RUN_DIR/proofs/*live-smoke* (if exists) 6. The generated CLI — $CLI_DIR/ (if available)
Also gather the scorecard, verify pass rate, and dogfood report (from the shipcheck proof or by re-running the tools if IN_REPO is true and the binary is available).
Phase 2: Mine the session
Scan the conversation history for six categories of signal and produce a candidate list. The candidate list is not the finding list — Phase 2.5 triage will cull it and Phase 3 will further drop weak survivors. Most candidates will not survive.
While collecting, distinguish:
- Iteration noise — one-off retries, typos, normal trial-and-error during a
long generation. Skip these even at the candidate stage; they don't survive triage.
- Per-CLI quirks — behavior tied to this API's shape (auth oddity, undocumented
endpoint, vendor-specific envelope) that wouldn't recur on another spec. Add to the candidate list with a "looks per-CLI" tag — most will be dropped at triage.
- Systemic friction — patterns that would plausibly recur on the next CLI
(template gap, default that needs to change, skill instruction that misled you). These are what the retro exists to surface.
If running in a fresh conversation without generation history: Note this and proceed with manuscript evidence only. Focus on what the manuscripts reveal — scorecard gaps, verify failures, dogfood issues, and obvious template patterns in the CLI source. Mark session-dependent findings as "evidence: manuscripts only."
2a. Errors and retries
Any time a command failed and was re-run, a build broke, or the Printing Press produced code that didn't compile. What broke and what fixed it?
2b. Manual code edits
Manual edits during iteration are normal — agents reason over the generated CLI and tweak. A single edit to handle this CLI's quirk is the workflow.
For each manual edit, ask: could the machine have raised the floor here?
- Could the machine have completely prevented this edit? Default was wrong for
most APIs, template emitted broken code, parser missed a common pattern. If yes AND the same edit would be needed on multiple CLIs you can name with evidence → candidate.
- *Could the machine have given a better starting point that made the edit
smaller, simpler, or skippable in common cases?* Even if you'd still tweak, raising the floor compounds across future CLIs. If yes AND generalizable → candidate.
- Was this just per-API customization the agent was expected to do? Drop.
- Was this iteration noise (typo, retry, transient confusion)? Drop.
The triage question is whether the machine raising the floor would compound across future CLIs — not whether this one CLI would have shipped a few lines lighter.
2c. Features built from scratch
Hand-built features (transcendence commands, novel commands, helper packages for secondary APIs) are part of the workflow — agents build the domain-specific value layer on top of the API surface the machine emits. Building features by hand is not by itself a finding.
For each hand-built feature, ask: could the machine have raised the floor for this kind of feature?
- *Could the machine have emitted a working default version, even if you'd still
customize it?* (E.g., every list+detail API benefits from a summary aggregation that the machine could scaffold from the spec.) Candidate, if generalizable across multiple named APIs.
- *Could the machine have emitted scaffolding, types, or helpers that would have
cut the build effort meaningfully?* (E.g., a typed secondary-client template for combo CLIs, a fanout-aggregation helper.) Candidate, if generalizable.
- *Is this genuinely custom domain logic the machine couldn't realistically
generate from a spec?* (E.g., booking a slot is custom orchestration; the machine can emit the underlying endpoints but not the choreography.) Drop — the SKILL is the right place to share the recipe, not the generator.
The "raises the floor" test separates "machine fix" from "SKILL recipe": if the machine's contribution would still leave significant per-CLI work, the recipe belongs in the SKILL so the next agent knows the pattern; if the machine could absorb the boilerplate cleanly, it's a generator template.
2d. Recurring friction
Work that happens on every generation, not just this one. For each: is this inherent to the approach, or can the Printing Press eliminate it?
Propose at least two possible fixes at different levels (generator templates, binary post-processing, skill instruction) and assess which is most durable.
2e. Discovered optimizations
Improvements noticed during the session — UX ideas, performance improvements, new command patterns, output format improvements. Could this optimization be detected automatically and applied by the Printing Press?
2f. Scorer accuracy audit
Before proposing Printing Press fixes to improve scores, check whether the scoring itself is correct. Changing the Printing Press to satisfy a broken scorer is worse than doing nothing.
For each score penalty from dogfood, verify, and scorecard:
1. Trace the scorer's logic. Read the scoring tool's source code to understand exactly what it checks. Don't guess. 2. Test the scorer's assumption against reality. Does the CLI actually have the problem the scorer claims? 3. Classify the penalty:
- Scorer is correct — the CLI genuinely has this problem.
- Scorer is wrong — the CLI is fine; the scoring tool has a bug.
- Scorer is partially right — both could be better.
Common scorer bugs: name derivation mismatches, grep-based detection missing patterns, file exclusions too broad, section-counting heuristics.
The scorer audit is not optional. Every finding from a score penalty must have a "Scorer correct?" assessment before proposing a fix direction.
2g. Combo CLI priority audit
Only runs when the briefing named 2+ sources. Check $RUN_DIR/source-priority.json (from the Multi-Source Priority Gate in the main skill). If it doesn't exist but the briefing or user command clearly listed multiple services, that's itself a finding: the priority gate didn't fire when it should have.
For runs with a source-priority.json, cross-reference it against the absorb manifest and the shipped CLI:
1. Command count per source. Count commands attributed to each named source in the manifest. The primary should have at least as many as any secondary. If it has fewer, that's a priority inversion and becomes a finding — even if the user approved the manifest, it means the skill's discovery path for the primary failed silently. 2. Auth scoping. If the primary was declared free in the priority gate but the shipped CLI requires a paid key for the primary's headline commands, that's a finding — the economics check either didn't run or didn't route the paid key correctly to secondary-only scope. 3. README leadership. The primary should lead the README and --help. If a secondary is the first thing the user sees, flag it.
Each of these is a skill instruction gap category finding. The durable fix lives in skills/printing-press/SKILL.md (the Multi-Source Priority Gate, the Priority inversion check before Phase Gate 1.5, and the brief's ## Source Priority section) or in the generator if README ordering is template-driven.
Phase 2.5: Triage candidates
Before Phase 3 spends deep analysis on each candidate, run a fast triage to drop candidates that don't justify the deeper look. Most candidates should die here. The retro is a filter, not a funnel — if everything from Phase 2 makes it to Phase 3 unchanged, triage isn't doing its job.
For each candidate, ask in order:
1. Was this iteration noise? Normal trial-and-error during generation — one-off retry, typo recovery, agent forgetting a flag, transient network blip. Drop. 2. Is this a printed-CLI fix? The fix lives in $PRESS_LIBRARY/<api>/ and helps only this one CLI. If the proposed change is "edit this command in this CLI" or "regenerate after fixing the spec," it's not a retro finding — it's a polish pass on that CLI. Drop. 3. Is this an upstream API quirk? The vendor returns null instead of 404, or ignores a query param the docs claim to honor, or has rate limits the spec doesn't declare. The Printing Press doesn't fix vendors. If the only fix is "work around this in the generator for every CLI," that's almost always wrong; if it's "let one CLI work around it," that's a printed-CLI fix. Drop. 4. Is the only evidence "I noticed this once"? A one-time observation that you can't connect to a recurring pattern across other CLIs is a candidate for Drop, not a P3. P3 means "low priority systemic finding," not "I want to record this somewhere." 5. Does the same finding appear in 2+ prior retros without being implemented? Don't re-raise at the same priority. Either drop it (the cost-benefit math has been "no" twice and the retro is becoming a wishlist), or reframe as a smaller incremental fix that addresses part of the friction. Search: grep -l "<finding keywords>" "$PRESS_MANUSCRIPTS"/*/proofs/*-retro-*.md
Survivors of these five questions go to Phase 3. Dropped candidates are recorded as one-line entries in the retro's "Dropped at triage" section — they exist for your own discipline check and for the maintainer to see triage actually ran.
Anti-pattern to avoid. A recent Pagliacci retro produced "Skip: None. Every finding warrants action." That sentence is the failure mode this triage exists to prevent. Two of those findings (snake_case in Use:, root.go Short: rewrite that the SKILL already documents as a manual step) were classic per-CLI / instructional candidates that should have been dropped here. If you find yourself writing "every finding warrants action," stop and re-run triage.
Phase 3: Classify findings
For each candidate that survived Phase 2.5 triage, answer these seven questions. Question 5 has seven sub-steps (A through G); Step G is the adversarial check. Findings that fail Step G drop out — they don't get a priority, they don't go in the Do/Skip tables, they go on the dropped-candidates list with the reason.
1. What happened? One sentence — the symptom, not the fix.
2. Is the scorer correct? (mandatory for score-penalty findings)
- Scorer correct → fix the Printing Press (templates, binary, or skill)
- Scorer wrong → fix the scoring tool, not the Printing Press
- Both → fix both, label which is primary
3. What category?
| Category | Description |
|---|---|
| Bug | Generated code is wrong |
| Scorer bug | Scoring tool reports a false positive |
| Template gap | No template for a common pattern |
| Assumption mismatch | Printing Press assumes X but API uses Y |
| Recurring friction | Happens every generation, might be inherent |
| Missing scaffolding | Feature class the Printing Press could emit but doesn't |
| Default gap | Printing Press emits a wrong or placeholder default |
| Discovered optimization | Improvement found during use |
| Skill instruction gap | Skill told Claude wrong thing or missed a step |
4. Where in the Printing Press does this originate?
Pick exactly one component. The slug column drives the comp:<slug> label applied to the issue when filed (Phase 6), which is how agents filter related work across retros (gh issue list --label comp:<slug>).
| Component | Slug | Path |
|---|---|---|
| Generator templates | generator | internal/generator/ |
| Spec parser | spec-parser | internal/spec/ |
| OpenAPI parser | openapi-parser | internal/openapi/ |
| Catalog | catalog | catalog/ |
| Main skill | skill | skills/printing-press/SKILL.md |
| Verify/dogfood/scorecard | scorer | CLI commands |
If a finding genuinely spans two components, pick the one where the durable fix lands. Don't multi-label.
5. Blast radius and fallback cost — should the Printing Press handle this?
Step A: Cross-API stress test. Test across API shapes (standard REST, proxy-envelope, RPC-style) and input methods (OpenAPI, crowd-sniffed, HAR-sniffed, no spec).
Step B: Name three concrete APIs from the catalog with direct evidence. Not "every API with multi-word resources" or "any browser-sniffed CLI." Name three specific APIs already in $PRESS_LIBRARY/ (or the embedded catalog/ directory) where you can point to evidence the pattern exists: a path in their spec, a known endpoint shape, a header the vendor documents, an output you can reproduce. "Stripe, Notion, GitHub probably have this" is hand-waving; "Stripe (Stripe-Version header in spec line N), GitHub (X-GitHub-Api-Version on the issues endpoints), Linear (api-version on /v2/)" is evidence. If you can name only two with evidence — or three with hand-waving — the finding drops to P3 max with a `subclass:<name>` annotation*, or moves to Drop.
Step C: Counter-check question. Ask explicitly: "If I implemented this fix, would it actively hurt any API that doesn't have this pattern?" If yes, the fix needs a guard or condition before being P1/P2 — not a default change. Example: turning on client-side ?limit=N truncation by default would hurt APIs that need server-side pagination for correctness; it stays P2 only because it's gated on profiler-detected absence of a paginator. Without that guard the same finding is unsafe to land.
Step D: Recurrence-cost check. Search prior retros under $PRESS_MANUSCRIPTS/*/proofs/*-retro-*.md for the same finding. If the same finding has been raised in 2+ prior retros without being implemented, the prior cost- benefit math has been "no" twice. Don't re-raise it at the same priority — either move to P3 with a "raised N times, still not justified" annotation, or reframe the finding into a smaller incremental fix that addresses part of the friction. Recurrence at the same priority is a triage failure, not stronger evidence.
Capture matched prior retros. When the search returns hits, record each as a structured tuple — retro CLI name, retro file path (or GitHub issue number if the retro file's frontmatter contains one), and a one-word classification:
aligned— the prior retro proposed the same fix direction. Strengthens the case;
reference it in Step F.
contradicts— the prior retro proposed an opposing fix or chose a different
default. Surface this explicitly: a maintainer reading the new finding must see the disagreement. State in one sentence why this retro reaches a different conclusion (e.g., "prior retro saw single-paginator APIs; this one saw an always-paginated API where the prior default would break").
extends— the prior retro raised an adjacent finding in the same component
area but a different specific fix. Useful context, doesn't change the case.
These tuples flow forward into the per-finding template ("Related prior retros") in the retro doc and merge into the issue body's "Related issues" block alongside the Step 2.5 dedup scan's related-area outputs. GitHub auto-cross-links any #N issue number you write, so contradictions and alignments show up in both retro timelines without further action.
Step E: Assess fallback cost. How reliably will Claude catch and fix this across every future API? A "simple" edit Claude forgets 30% of the time means 30% ship with the defect.
Step F: Make the tradeoff. Default is don't change the machine. The burden of proof is on the finding to justify a machine change. Continue to Step G only when all three of these are true:
(a) Step B named three concrete APIs with evidence (not speculation). (b) Step D's recurrence-cost check didn't disqualify the finding. (c) Step C's counter-check didn't surface a hurts-other-APIs concern that lacks a guard.
If a finding can't clear all three, it doesn't get a priority — it goes to Drop with the specific reason ("only named 2 APIs with evidence" / "raised 3 times, still not justified" / "fix would hurt single-paginator APIs without a guard").
When the finding applies to an API subclass, include: Condition (when to activate), Guard (when to skip), Frequency estimate.
Step G: Construct the case against filing. Before recording the finding, write 1-2 sentences arguing the opposite — what makes this look like a printed-CLI fix, an iteration artifact, or a wishlist item. Why might a maintainer close this as "works as designed" or "too narrow for a machine fix"? What's the strongest version of "this shouldn't be filed"?
If the case-against is stronger than the case-for, drop the finding. If they're roughly even, drop the finding (default direction is don't-file). Only when the case-for is clearly stronger does the finding survive to Phase 4.
This step is not a formality. It is the explicit place where weak findings die. A finding that survives Step G should be able to state, in one sentence, why the case-against fails — and that sentence is worth quoting in the retro entry.
6. Is this inherent or fixable? Push hard on whether smarter templates, a post-processing step, or better spec analysis could eliminate the friction. If inherent, propose the cheapest mitigation.
7. What is the durable fix? Prefer: template fix > binary post-processing > skill instruction.
Mark uncertainty explicitly. If you can't confidently isolate one root cause or one fix, say so — list the candidate causes (or candidate fixes) and how an implementer could disambiguate before committing. The issue body surfaces this uncertainty so the agent picking up the work doesn't lock in a wrong-but-plausible diagnosis. Confidence isn't a virtue when it's manufactured; an honest "either A or B; verify by X" is more useful than a wrong prescription.
Strip API-specific details from the proposed fix. The durable fix must work across APIs, not just the one that surfaced the finding. If the fix includes hardcoded param names (e.g., --sport, --league), date formats (e.g., YYYYMMDD), chunking strategies (e.g., monthly), or domain-specific logic, those are printed-CLI details leaking into the machine recommendation. The machine fix should be parameterized — driven by what the profiler detects in the spec, not by what one API happens to need.
Example of the anti-pattern:
- Finding: "ESPN sync needs
--datesfor historical data" - Bad fix: "Add
--dateswithYYYYMMDD-YYYYMMDDformat,--sport/--leagueflags, and monthly chunking to the sync template" - Good fix: "When the profiler detects a date-range query param, emit a
--datesflag that passes the value through to the API"
The bad fix bakes ESPN's date format, scope params, and chunking strategy into the machine. The good fix lets the profiler drive behavior from the spec.
Phase 4: Prioritize
Sort survivors of Phase 3 into three buckets:
- Do — survived Phase 3 Step G with a clear case-for. Assign a priority (P1,
P2, P3) based on frequency, fallback reliability, and complexity. Scorer bugs are just findings like any other — rank them by impact alongside template gaps and parser issues.
- Skip — survived Phase 2.5 triage but didn't clear Phase 3 (Step B couldn't
name 3 APIs with evidence, Step D recurrence-cost disqualified, or Step G's case-against was stronger). State the specific step that failed. These are listed in the retro so the maintainer can see what was considered and rejected.
- Drop — rejected at Phase 2.5 triage as iteration noise, printed-CLI fix,
upstream API quirk, unproven one-off, or recurring-not-implemented. Listed as one-liners only — they don't need full analysis, they need a record so triage is auditable.
No numerical scoring formulas. State the priority reasoning in words.
Sanity check before moving to Phase 5. Look at the bucket distribution. Almost every retro should have some drops and some skips. A retro with "all Do, no Skip, no Drop" is the failure mode — re-run triage and Step G on the weakest findings. Likewise, if every Do is P1, you're not prioritizing, you're inflating; force yourself to identify the weakest "Do" and ask whether it really beats the Skip bar.
Phase 5: Write the retro
The retro document is the durable audit trail — keep all fields below. The GitHub issue body in Phase 6 will use a slim subset (action-shaped fields only); the full triage rationale lives here, in the doc that gets uploaded as an artifact and linked from the issue. See references/issue-template.md for the issue-body shape.
Write the full retro document using this template:
# Printing Press Retro: <API name>
## Session Stats
- API: <name>
- Spec source: <catalog/browser-sniffed/docs/HAR>
- Scorecard: <score>/100 (<grade>)
- Verify pass rate: <X>%
- Fix loops: <N>
- Manual code edits: <N>
- Features built from scratch: <N>
## Findings
### 1. <Title> (<category>)
- **What happened:** ...
- **Scorer correct?** Yes / No / Partially. [details]
- **Root cause:** Component + what's specifically wrong
- **Cross-API check:** Would this recur?
- **Frequency:** every API / most / subclass:<name> / this API only
- **Fallback if the Printing Press doesn't fix it:** ...
- **Worth a Printing Press fix?** ...
- **Inherent or fixable:** ...
- **Durable fix:** ...
- **Test:** How to verify (positive + negative)
- **Evidence:** Session moment that surfaced this
- **Related prior retros:** *(from Phase 3 Step D; "None" if no matches)*
- `<api-slug>` retro #<issue-num-if-known> — `aligned` / `contradicts` / `extends`. <one-sentence note on what changed or what's shared>
- ...
## Prioritized Improvements
### P1 — High priority
| Finding | Title | Component | Frequency | Fallback Reliability | Complexity | Guards |
|---------|-------|-----------|-----------|---------------------|------------|--------|
### P2 — Medium priority
| Finding | Title | Component | Frequency | Fallback Reliability | Complexity | Guards |
|---------|-------|-----------|-----------|---------------------|------------|--------|
### P3 — Low priority
| Finding | Title | Component | Frequency | Fallback Reliability | Complexity | Guards |
|---------|-------|-----------|-----------|---------------------|------------|--------|
*Omit empty priority sections.*
### Skip
| Finding | Title | Why it didn't make it (Step B / Step D / Step G) |
|---------|-------|--------------------------------------------------|
*Findings that survived Phase 2.5 triage but failed Phase 3 — name the specific
step that failed (e.g., "Step B: only 2 APIs with evidence" / "Step G: case-against
stronger; mostly per-CLI"). Empty if every Phase 3 candidate filed.*
### Dropped at triage
| Candidate | One-liner | Drop reason |
|-----------|-----------|-------------|
*Candidates rejected at Phase 2.5. One line each. Reasons: `iteration-noise` /
`printed-CLI` / `API-quirk` / `unproven-one-off` / `raised-N-times`. If this
section is empty, re-check Phase 2.5 — almost every retro has some.*
## Work Units
(see Phase 5.5)
## Anti-patterns
- ...
## What the Printing Press Got Right
- ...Save the retro to manuscript proofs (always) and to the temp retro scratch directory (always). Do not save retro documents under the source repo's docs/retros/ directory; the skill must work the same way for users who do not have the repo checked out, and retro documents are issue artifacts rather than durable repo docs.
RETRO_STAMP="$(date +%Y%m%d-%H%M%S)"
RETRO_PROOF_PATH="$PRESS_MANUSCRIPTS/$API_NAME/$RUN_ID/proofs/$RETRO_STAMP-retro-$CLI_NAME.md"
RETRO_SCRATCH_DIR="/tmp/printing-press/retro"
RETRO_SCRATCH_PATH="$RETRO_SCRATCH_DIR/$RETRO_STAMP-$API_NAME-retro.md"
mkdir -p "$(dirname "$RETRO_PROOF_PATH")" "$RETRO_SCRATCH_DIR"Write the full retro document to $RETRO_PROOF_PATH, then copy that file to $RETRO_SCRATCH_PATH. This must complete before Phase 6 Step 1 copies the manuscripts directory to staging.
Scrub the retro doc immediately after writing
The retro doc is preserved in manuscripts/<api>/<run>/proofs/ (durable), copied to /tmp/printing-press/retro/ (scratch), and read by future runs' Phase 3 Step D dedup scan. If a finding's "What we observed" block pasted unredacted scanner output, dogfood payloads, or Greptile review comments, the secret/PII propagates into all three locations. Run the Layer 0 body scrub from references/secret-scrubbing.md immediately after writing the doc, so the scrubbed version becomes canonical:
# Define scrub_body once at the top of the Phase 5/6 bash blocks (full source
# in references/secret-scrubbing.md Layer 0). Then:
RETRO_PROOF_PATH_SCRUBBED="${RETRO_PROOF_PATH}.scrubbed.md"
if ! scrub_body "$RETRO_PROOF_PATH" "$RETRO_PROOF_PATH_SCRUBBED"; then
echo "" >&2
echo "ERROR: retro doc contains an unredacted vendor-prefix secret." >&2
echo "Open $RETRO_PROOF_PATH, redact each match reported above using" >&2
echo " <REDACTED:<vendor>-<kind>:<first4>...<last4>:<len>ch>" >&2
echo "per references/secret-scrubbing.md Layer 0, then re-run /printing-press-retro." >&2
exit 1
fi
mv "$RETRO_PROOF_PATH_SCRUBBED" "$RETRO_PROOF_PATH"
cp "$RETRO_PROOF_PATH" "$RETRO_SCRATCH_PATH"Hard-fail behavior is intentional: vendor-prefix secrets are unrecoverable leaks once a retro doc gets archived or uploaded. The agent must hand-redact and re-run rather than silently shipping the leak. PII patterns (real emails, phones, account inboxes) auto-redact in place because the substitution is lossless for the retro's purpose.
Phase 5.5: Plannable work units
Group related findings into coherent work units a planner could pick up directly.
For each "Do" finding or group of related findings:
### WU-1: <Title> (from F1, F3, ...)
- **Priority:** P1 / P2 / P3 *(max priority among absorbed findings — P1 if any
absorbed finding is P1, else P2 if any is P2, else P3)*
- **Component:** generator / openapi-parser / spec-parser / scorer / skill / catalog
*(must match one of the six fixed component slugs; drives the `comp:*` label
applied to the issue when filed)*
- **Goal:** One sentence describing the outcome
- **Target:** <component and area, e.g., "Generator templates in internal/generator/">
- **Acceptance criteria:**
- positive test: ...
- negative test: ...
- **Scope boundary:** What this does NOT include
- **Dependencies:** Other work units that must complete first
- **Complexity:** small / medium / largeThe six fixed component slugs are: generator (internal/generator/), openapi-parser (internal/openapi/), spec-parser (internal/spec/), scorer (verify / dogfood / scorecard), skill (skills/printing-press/SKILL.md), catalog (catalog/). If a WU genuinely spans two, pick the primary one — the component where the durable fix will land. Pick exactly one; don't multi-label.
If running from inside the printing-press repo (`IN_REPO=true`): Resolve target file paths using Glob and Grep tool invocations on $REPO_ROOT to make work units more precise. E.g., use Glob to find internal/generator/*.go files, Grep to find where sync code is generated.
If running externally (`IN_REPO=false`): Describe target components by name (e.g., "Generator templates in internal/generator/") and acceptance criteria without resolved file paths. The fixer will resolve paths when they pick up the work.
Phase 5.6: Issue gate — are there Printing Press improvements?
After prioritization and work units are written, decide whether GitHub issues are warranted. Each WU becomes one flat top-level issue (no parent, no sub-issue hierarchy). The purpose of filing is to give someone (human or agent) something to fix in the Printing Press. If every finding is specific to this one printed CLI with nothing to change in the Printing Press, filing is noise — there's nothing to act on.
Skip filing if:
- Every finding landed in "Skip"
- All findings are printed-CLI-specific (manual edits that only apply to this one API
and wouldn't recur across other CLIs)
- The "Do" table is empty
File issues (one per WU) if:
- There is at least one "Do" finding — i.e., something a maintainer or agent could
act on in the Printing Press (templates, binary, skills, or scoring tools)
Use judgment. A retro that found three things but all three are "this API has a weird auth scheme no other API uses" is not worth filing. A retro that found one small template gap that would help every future CLI is worth filing.
If filing is skipped, still save the retro locally (manuscript proofs + /tmp/printing-press/retro/), present the findings to the user, then jump directly to Phase 6 Step 6 (present results — adjusted to show local-only paths).
Phase 6: Package, upload, and present
Step 1: Package artifacts into staging folder
Read and apply references/artifact-packaging.md through Step 4 only (create staging dir, copy, scrub, zip). Do not upload or clean up yet — the staging folder stays alive until the end of Phase 6.
The staging folder ($STAGING_DIR) now contains the scrubbed copies and the zips. This is both the review target and the upload source.
Step 2: Compute filing plan + confirm before publishing
This step only runs if the Phase 5.6 issue gate passed (there are Printing Press findings to act on).
Before showing the confirm prompt, run references/issue-template.md Steps 1, 2, and 2.5 to ensure labels exist, sort the work units, and compute the per-WU filing plan via the dedup scan against open retro-tagged issues. Each WU ends up classified as either:
- File new — no matching open issue
- Comment on #N — Step 2.5 found a
samematch; the new evidence will be added as a comment instead of filing a duplicate - File new with related issues — Step 2.5 found one or more
related-areamatches; the new issue's body will reference them via#Nin the Related issues block
The dedup scan does not need to be bulletproof. Bias toward "file new" when uncertain — duplicates are recoverable, miscomments on the wrong issue are uglier.
Then show the user a summary including the filing plan and ask for confirmation via AskUserQuestion.
Ready to submit your retro.
>
Here's what will happen on mvanhorn/cli-printing-press:
>
Filing plan:
>
| # | Title | Plan | Notes |
|---|-------|------|-------|
| 1 | <wu-1 title> | File new (P1, comp:<slug>) | No match |
| 2 | <wu-2 title> | Comment on #234 | Matches "<existing title>" |
| 3 | <wu-3 title> | File new + reference #189 | Adjacent open issue |
>
Each new issue carriesretro,priority:P<n>,comp:<slug>labels —
agents filter related work across retros with `gh issue list --label
comp:<slug> or gh issue list --label priority:P1`.>
Scrubbed artifact zips uploaded to catbox.moe and linked from each new issue:
- Retro document — full triage rationale, drops, skips, what went right
- Manuscripts (<size>) — research brief, shipcheck proof, build logs
- CLI source (<size>) — the generated Go code (no binary, no vendor/) (omit if not available)
>
Everything is staged at <$STAGING_DIR> if you'd like to inspect the files first.Options: 1. Submit — execute the filing plan 2. Let me review the files first — I'll check the staging folder, then come back 3. Save locally only — skip filing, keep the manuscript proof and temp copy
If the user picks "Let me review the files first," acknowledge and wait. When they come back, re-ask with Submit / Save locally only.
If the user picks "Save locally only," skip Steps 3 and 4 — the retro is already saved to manuscript proofs and /tmp/printing-press/retro/. Clean up the staging folder, then jump to Step 6.
If the user wants to override a dedup decision before submitting (e.g., "file new for WU-2 instead of commenting"), accept the override: clear WU_DEDUP[i] for that WU and proceed.
Step 3: Upload artifacts
Run artifact-packaging.md Step 5 (the catbox upload) using the zips already in $STAGING_DIR. This produces $MANUSCRIPTS_URL and $CLI_SOURCE_URL.
Step 4: Execute the filing plan
Steps 1, 2, and 2.5 of references/issue-template.md already ran during Step 2 (filing plan + confirm), so labels exist, WUs are sorted, and $WU_DEDUP and $WU_RELATED are populated. This step runs Step 3 of the reference: build bodies and execute the plan in parallel.
The "Execution principles" block at the top of issue-template.md is mandatory: build issue bodies inline (heredocs into shell variables, not the Write tool), run the whole step in one Bash invocation, and parallelize the per-WU gh issue create / gh issue comment calls. Skipping these costs real wall-clock latency — an N WU retro should finish in a single round trip's worth of network time, not a serialized stack of them.
Each WU is independent: WUs marked comment:#N get a comment on the existing issue; WUs marked file-new create a new flat top-level issue. No parent, no sub-issue REST linking — every new issue stands alone in GitHub's issue list with its own open/close lifecycle.
Each new issue carries its own priority:P<n> and comp:<slug> labels. This is what enables gh issue list --label comp:openapi-parser to surface every retro WU in that area across every retro — labels are the cross-retro discovery surface, not auto-cross-links inside issue bodies.
Each new issue body's Related issues block combines:
- Prior-retro references from Phase 3 Step D (alignments, contradictions, extensions across retros)
related-areaissue references from Step 2.5 (open issues in adjacent territory)
Both reach across separate filed work where the #N auto-cross-link is real signal. The body does not auto-cross-link to sibling WUs in the same retro; that linkage is noise unless one is genuinely a prerequisite (captured as free-text Dependencies: instead).
If gh is not authenticated or every per-WU action fails, follow the graceful degradation path in the issue-template reference: save locally and print manual filing instructions. Per-WU partial failures (some succeed, some don't) are surfaced through $FAILED_ISSUES in Step 6.
Step 5: Local scratch copy
Ensure the temp scratch copy exists. This is the human-friendly local path for reviewing or manually filing the retro when upload or issue creation fails.
if [ -f "$RETRO_PROOF_PATH" ]; then
mkdir -p "$RETRO_SCRATCH_DIR"
cp "$RETRO_PROOF_PATH" "$RETRO_SCRATCH_PATH"
fiStep 6: Present results
After issues are created and comments posted, show the user a summary in priority order. Group created and commented outcomes — both are real filed work, but the shape differs.
Retro submitted!
>
Filed <C> new issue<s>, added <E> comment<s> on existing issues (P1 → P3 order):
>
New issues:
- [P1] <title> — <full $OUTCOME_URL[i]>
- [P2] <title> — <full $OUTCOME_URL[i]>
- ...
>
Comments on existing issues:
- [P1] <title> → comment on #234 — <comment URL>
- ...
>
<N> findings across <M> work units. New issues are tagged with comp:<slug>and priority:P<n> labels — agents can filter related work across retroswithgh issue list --label comp:<slug>orgh issue list --label priority:P1.
(if artifacts uploaded) Artifacts: retro doc · manuscripts · CLI source
Local copy: <$RETRO_SCRATCH_PATH>
The [P<n>] annotation here is presentation-only — the issue titles themselves do not carry a priority prefix (priority lives on the label). Showing it in the user-facing summary helps the user scan filed work in priority order without opening each issue.
Omit either subsection (New issues: or Comments on existing issues:) when empty. A retro that produced only comments (every WU matched an existing open issue) is a good outcome — it means the issue tracker already covered the findings and the new evidence reinforces them.
If $FAILED_ISSUES is non-empty (set by references/issue-template.md Step 3), append a warning block before the closing line:
⚠️ Some actions need attention:
- <title> — issue creation failed
- <title> — comment on #234 failed
- ...
>
File the missing issue(s) or comment(s) manually using the retro doc at <$RETRO_SCRATCH_PATH>.
If filing wasn't completed (user chose local-only, or gh failed entirely), show the local save paths and the manual filing instructions printed by the issue-template fallback path.
Step 7: Clean up staging folder
Run artifact-packaging.md Step 7 to delete $STAGING_DIR.
Rules
- Prefer automatic fixes (templates, binary) over instructional fixes (skill).
- For recurring friction, always answer "inherent or fixable?" honestly.
- Be honest about what went well. Protecting good patterns matters.
- Default is don't-file. Bias toward filing only when Phase 3 Step B gave you
three concrete cross-API examples with evidence (not speculation), and the Step G case-against was clearly weaker than the case-for. "20% of catalog" without named APIs is optimism. "Every API has multi-word resources" is hand-waving. The retro is a filter, not a wishlist; an issue overloaded with weak findings wastes maintainer attention.
- When in doubt, drop. A finding you're uncertain about almost certainly
shouldn't be filed. The next CLI's retro will surface it again with stronger evidence if it's real; if it doesn't, it wasn't.
- Look for broader patterns. When something does clear the bar, check
whether this is the first sighting of a behavior you'd encounter again.
- When a fix applies to an API subclass, include the condition AND the guard.
- No time estimates. Use complexity sizing (small/medium/large).
- Be thorough on the findings that survive. Include enough detail that someone
reading months later can understand the finding, the reasoning, and the proposed fix without the original conversation.
- Do not add more phases, documents, or gates to the main printing-press skill.
Propose making existing phases smarter or the Printing Press emit better defaults.
- Never quote a leaked secret as "evidence" of a secret-leak finding. The
finding's whole point is that the value should not be public; quoting it in a public GitHub issue re-leaks it. Use the redacted form from `references/secret-scrubbing.md` Layer 0 (<REDACTED:<vendor>-<kind>:<first4>...<last4>:<len>ch>) — the maintainer can fix the scanner without seeing the value. Phase 5 (retro doc write) and Phase 6 Step 3 (pre-post scrub) hard-fail when an unredacted vendor-prefix token is detected; that's the floor, not a substitute for redacting at write time.
Artifact Packaging and Upload
Read this file during Phase 6, Step 1 (packaging) and Step 3 (uploading).
Steps 1-4 run during Phase 6 Step 1 to prepare artifacts locally. Step 5 runs during Phase 6 Step 3 after the user confirms they want to submit. Step 6 (failure handling) runs if Step 5 encounters errors. Step 7 (cleanup) runs at the very end of Phase 6, after everything else is done.
Cardinal rule: Never modify the user's source directories. All operations work on temporary copies. If anything fails, the user's manuscripts and library are untouched.
Step 1: Create staging directory
STAGING_DIR=$(mktemp -d)
STAGING_MANUSCRIPTS="$STAGING_DIR/manuscripts"
STAGING_CLI_SOURCE="$STAGING_DIR/cli-source"
echo "Staging artifacts in $STAGING_DIR"Step 2: Copy artifacts to staging
Copy the manuscript run directory:
mkdir -p "$STAGING_MANUSCRIPTS"
cp -r "$RUN_DIR/." "$STAGING_MANUSCRIPTS/"Copy the CLI source if available. Skip if CLI_DIR is empty (manuscripts-only mode):
if [ -n "$CLI_DIR" ] && [ -d "$CLI_DIR" ]; then
mkdir -p "$STAGING_CLI_SOURCE"
if command -v rsync >/dev/null 2>&1; then
rsync -a \
--exclude="$CLI_NAME" \
--exclude="vendor/" \
--exclude="go.sum" \
--exclude=".git/" \
--exclude="*.test" \
--exclude="*.exe" \
"$CLI_DIR/" "$STAGING_CLI_SOURCE/"
else
cp -r "$CLI_DIR/." "$STAGING_CLI_SOURCE/"
rm -f "$STAGING_CLI_SOURCE/$CLI_NAME"
rm -rf "$STAGING_CLI_SOURCE/vendor" "$STAGING_CLI_SOURCE/.git"
rm -f "$STAGING_CLI_SOURCE/go.sum"
find "$STAGING_CLI_SOURCE" \( -name "*.test" -o -name "*.exe" \) -delete 2>/dev/null
fi
else
echo "No CLI source directory available. Packaging manuscripts only."
STAGING_CLI_SOURCE=""
fiStep 3: Scrub secrets
Read and apply references/secret-scrubbing.md on the staging copies. The scrub file expects $STAGING_MANUSCRIPTS and $STAGING_CLI_SOURCE to be set.
If the post-scrub verification reports unresolved secrets, do not proceed with upload. Save the zips locally and tell the user to review manually.
Step 4: Zip artifacts
MANUSCRIPTS_ZIP="$STAGING_DIR/$API_SLUG-manuscripts.zip"
CLI_SOURCE_ZIP=""
(cd "$STAGING_MANUSCRIPTS" && zip -r "$MANUSCRIPTS_ZIP" . -x "*.DS_Store") 2>/dev/null
echo "Manuscripts zip: $(du -h "$MANUSCRIPTS_ZIP" | cut -f1)"
if [ -n "$STAGING_CLI_SOURCE" ] && [ -d "$STAGING_CLI_SOURCE" ]; then
CLI_SOURCE_ZIP="$STAGING_DIR/$API_SLUG-cli-source.zip"
(cd "$STAGING_CLI_SOURCE" && zip -r "$CLI_SOURCE_ZIP" . -x "*.DS_Store") 2>/dev/null
echo "CLI source zip: $(du -h "$CLI_SOURCE_ZIP" | cut -f1)"
fiStep 5: Upload to catbox.moe
Upload each artifact and capture the returned URL:
MANUSCRIPTS_URL=""
CLI_SOURCE_URL=""
RETRO_DOC_URL=""
UPLOAD_FAILED=false
# Upload retro document (raw .md — viewable directly in browser)
RESPONSE=$(curl -s -F "reqtype=fileupload" -F "fileToUpload=@$RETRO_PROOF_PATH" https://catbox.moe/user/api.php 2>/dev/null)
if echo "$RESPONSE" | grep -q "^https://"; then
RETRO_DOC_URL="$RESPONSE"
echo "Retro document uploaded: $RETRO_DOC_URL"
else
echo "WARNING: Failed to upload retro document to catbox.moe. Response: $RESPONSE"
UPLOAD_FAILED=true
fi
# Upload manuscripts zip
RESPONSE=$(curl -s -F "reqtype=fileupload" -F "fileToUpload=@$MANUSCRIPTS_ZIP" https://catbox.moe/user/api.php 2>/dev/null)
if echo "$RESPONSE" | grep -q "^https://"; then
MANUSCRIPTS_URL="$RESPONSE"
echo "Manuscripts uploaded: $MANUSCRIPTS_URL"
else
echo "WARNING: Failed to upload manuscripts to catbox.moe. Response: $RESPONSE"
UPLOAD_FAILED=true
fi
# Upload CLI source (only if it was packaged)
if [ -n "$CLI_SOURCE_ZIP" ] && [ -f "$CLI_SOURCE_ZIP" ]; then
RESPONSE=$(curl -s -F "reqtype=fileupload" -F "fileToUpload=@$CLI_SOURCE_ZIP" https://catbox.moe/user/api.php 2>/dev/null)
if echo "$RESPONSE" | grep -q "^https://"; then
CLI_SOURCE_URL="$RESPONSE"
echo "CLI source uploaded: $CLI_SOURCE_URL"
else
echo "WARNING: Failed to upload CLI source to catbox.moe. Response: $RESPONSE"
UPLOAD_FAILED=true
fi
else
echo "No CLI source to upload (manuscripts-only mode)."
fiStep 6: Handle upload failure (R15)
If either upload failed:
if [ "$UPLOAD_FAILED" = true ]; then
# Preserve local zips for manual attachment
LOCAL_ZIP_DIR="$RUN_DIR/proofs"
cp "$MANUSCRIPTS_ZIP" "$LOCAL_ZIP_DIR/" 2>/dev/null
cp "$CLI_SOURCE_ZIP" "$LOCAL_ZIP_DIR/" 2>/dev/null
echo ""
echo "Some artifacts could not be uploaded to catbox.moe."
echo "Local copies saved to: $LOCAL_ZIP_DIR/"
echo "You can upload them manually or attach them to the GitHub issue."
fiStep 7: Cleanup
Called by the SKILL.md at the end of Phase 6, after issue creation, local saves, and presenting results are all complete. Do not call this immediately after zipping — the staging folder must stay alive for user review and upload.
rm -rf "$STAGING_DIR"If cleanup fails (permissions, etc.), it's a temp directory — the OS will clean it up eventually. Do not let cleanup failure block the rest of the workflow.
Variables expected by this reference
| Variable | Set by | Contains |
|---|---|---|
$RUN_DIR | SKILL.md Phase 1 | Full path to the manuscript run directory |
$CLI_DIR | SKILL.md Phase 1 | Full path to the CLI library directory |
$CLI_NAME | SKILL.md guard rails | Binary name (e.g., cal-com-pp-cli) |
$API_SLUG | SKILL.md guard rails | API slug (e.g., cal-com) |
$API_KEY_VALUE | User session (optional) | The API key if provided during generation |
Variables produced by this reference
| Variable | Contains |
|---|---|
$RETRO_DOC_URL | catbox URL for retro .md file (viewable in browser), or empty if upload failed |
$MANUSCRIPTS_URL | catbox URL for manuscripts zip, or empty if upload failed |
$CLI_SOURCE_URL | catbox URL for CLI source zip, or empty if upload failed |
$UPLOAD_FAILED | true if any upload failed, false otherwise |
$MANUSCRIPTS_ZIP | Local path to manuscripts zip (in staging, deleted after cleanup) |
$CLI_SOURCE_ZIP | Local path to CLI source zip (in staging, deleted after cleanup) |
GitHub Issue Template
Read this file during Phase 6 when filing GitHub issues from retro findings.
Each work unit becomes one flat top-level issue. There is no parent, no sub-issue hierarchy. The "Findings absorbed" section inside each issue groups the 1+ findings that share a single fix.
This shape was chosen deliberately:
- A retro of N work units produces N issues, each independently trackable —
open, close, assign, comment.
- The default GitHub issue list is not duplicated; there is no parent to
mirror children, no progress bar to drift, no parent-not-auto-closing bookkeeping.
- Cross-retro discovery still works through labels:
comp:<slug>surfaces
every retro WU touching one component; priority:P1 surfaces high-priority work across retros.
- Inter-issue links inside the same retro are not auto-generated. They appear
only when an issue genuinely relates to another (a contradicting prior retro, or a related-area open issue surfaced by the dedup scan).
Formatting rules
Use the F prefix for findings (F1, F3) and WU- for work units inside issue bodies. Real GitHub issue references in the `Related issues` block are intentional `#N` — that's where we want GitHub to auto-link the timelines together, because those references reach across retros and across filed work.
Execution principles (read before running any step)
The retro doc and the manuscript zip are the durable audit trail; each issue is an action surface. Optimize the issue path for speed and signal:
- Issue body is self-contained for debugging. An agent picking up an
issue should be able to start fixing it without opening the manuscripts attachment. Concrete file paths, line numbers, command output, and spec excerpts go in the body. The retro doc URL stays as a supplement, not a required read.
- Don't manufacture confidence. If the retro analyst can't confidently
isolate one root cause or one fix, the body should say so — list candidate causes/fixes and how to disambiguate. Wrong-but-plausible prescriptions are worse than honest uncertainty.
- Title is succinct and problem-stated. Describes what's wrong, not how
to fix it. No priority prefix ([P1]), no WU ordinal (WU-1) — both belong on labels and in the user-facing summary, not in the title that someone scans across retros.
- Build bodies inline; never via the Write tool. Use shell heredocs
into variables (or printf into bash-resident temp files inside a single Bash invocation). Per-WU Write tool round-trips are the single largest source of perceived latency the skill historically had. Bash-resident temp files passed to `gh ... --body-file` are fine — they're written and consumed inside one Bash call with no tool round-trip and unblock the Phase 6 Step 3 scrub step (scrub_body needs a file input/output, and gh --body-file is the natural consumer of the scrubbed output).
- Run issue creates and comments in parallel. Each WU's filing is
independent of every other WU. Background subshells writing to indexed temp files, then wait, then read in order — the pattern is in Step 3.
- Anti-pattern: Do not use
URL=$(gh issue create ... &)or
URL=$(gh issue comment ... &). The & runs inside the command substitution subshell, the subshell exits before gh finishes, URL is empty, and the still-running gh process can create or comment successfully in the background. Retrying then produces duplicate work. Use Step 3's (gh ... > "$ISSUE_TMPDIR/issue-$wu_idx") & capture-file shape instead.
Step 1: Ensure labels exist (idempotent, create-only)
Run once per session before creating any issues. The repo is expected to already have the canonical label set; this step is a safety net for users running the skill against a fresh fork. Create-only — never edit existing labels (the maintainer may have set custom colors or descriptions; the skill must not clobber them).
REPO="mvanhorn/cli-printing-press"
# Fast-path: list existing labels once. If all 10 canonical labels are already
# present, skip the create loop entirely — saves up to 10 gh API calls per
# retro on a repo where prior retros already provisioned the set.
EXISTING_LABELS=$(gh label list --repo "$REPO" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo "")
NEED_CREATE=false
for required in \
"comp:generator" "comp:openapi-parser" "comp:spec-parser" \
"comp:scorer" "comp:skill" "comp:catalog" \
"priority:P1" "priority:P2" "priority:P3" \
"retro"; do
if ! printf '%s\n' "$EXISTING_LABELS" | grep -qFx "$required"; then
NEED_CREATE=true
break
fi
done
if [ "$NEED_CREATE" = true ]; then
ensure_label() {
local name="$1" color="$2" desc="$3"
gh label create "$name" --repo "$REPO" --color "$color" --description "$desc" 2>/dev/null || true
}
# Component labels (6) — drive cross-retro discovery (`gh issue list --label comp:<slug>`)
ensure_label "comp:generator" "5319e7" "Generator templates (internal/generator/)"
ensure_label "comp:openapi-parser" "5319e7" "OpenAPI parser (internal/openapi/)"
ensure_label "comp:spec-parser" "5319e7" "Internal spec parser (internal/spec/)"
ensure_label "comp:scorer" "5319e7" "verify / dogfood / scorecard"
ensure_label "comp:skill" "5319e7" "skills/printing-press/SKILL.md and related skill instructions"
ensure_label "comp:catalog" "5319e7" "catalog/ entries"
# Priority labels (3) — drive priority-based filtering. The label is the
# primary carrier; titles do not duplicate the priority prefix.
ensure_label "priority:P1" "b60205" "Retro priority P1 (high)"
ensure_label "priority:P2" "d93f0b" "Retro priority P2 (medium)"
ensure_label "priority:P3" "fbca04" "Retro priority P3 (low)"
# Marker label
ensure_label "retro" "0e8a16" "Issue produced by /printing-press-retro"
fiThe retro-parent label is intentionally omitted — there are no parent issues. If the label exists from prior retros, leave it; the skill never creates new issues with it.
Step 2: Sort work units
Sort WUs by priority: P1 first, then P2, then P3. Within a priority bucket, keep the order they appeared in Phase 5.5 (typically by ascending WU number, but the skill may have intentionally ordered them by dependency — preserve that).
# SORTED_WORK_UNITS is populated from $WORK_UNITS sorted P1 → P3.Step 2.5: Dedup against open issues
Before filing, check whether any WUs match an issue that's already open. If they do, comment on the existing issue with new evidence rather than file a duplicate.
This is a single gh call followed by per-WU agent reasoning over titles. It does not need to be bulletproof — false negatives (filing new when one exists) are recoverable; false positives (commenting on the wrong issue) are uglier. Bias toward `file-new` when uncertain.
Fetch open retro issues
EXISTING_OPEN_RETROS=$(gh issue list \
--repo "$REPO" \
--label retro \
--state open \
--limit 200 \
--json number,title,url 2>/dev/null \
|| echo "[]")A single call. No per-WU label filtering — the agent reasons over titles across the whole open-retro set so a related-area issue under a different component still surfaces.
Classify each WU against the candidate set
For each WU in $SORTED_WORK_UNITS, the skill executor (the agent running this skill) reads the WU's title and summary, then scans $EXISTING_OPEN_RETROS for matches. Per-candidate verdict:
| Verdict | When | Effect |
|---|---|---|
same | Title and summary describe the same root problem already filed (high confidence) | Marks WU for comment instead of create |
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 |
unrelated | No meaningful overlap | Ignored |
Per WU, fold the per-candidate verdicts into a single $WU_DEDUP[i] slot:
$WU_DEDUP[i]:
"comment:NN" — at least one candidate was `same`. Comment on NN.
If multiple `same`, pick the most-recent and treat
the rest as related (folded into $WU_RELATED).
"" (empty) — no `same`. The agent will create a new issue.And capture related issues separately:
$WU_RELATED[i]:
comma-separated list of issue numbers classified as `related-area`,
combined with prior-retro issue numbers from Phase 3 Step D.
Empty when neither scan surfaced anything.The decisions feed forward into the Phase 6 Step 2 confirm summary so the user sees what will happen and can override before anything is filed.
Step 3: Build bodies and execute (in parallel)
For each WU, build either an issue body or a comment body, then run gh in a background subshell. wait, then collect.
Layer 0 body scrub is mandatory. Before posting any body to GitHub, the parallel loop runs scrub_body on the body file. Source the function from `secret-scrubbing.md` "Layer 0" at the top of the bash block that contains the loop — paste the scrub_body() { ... } definition verbatim, or source an equivalent shell fragment. Without this, the loop will fail with scrub_body: command not found per-WU.
Issue title (for new issues)
Succinct, problem-stated. Examples:
- ✅
Spec-declared version headers dropped during generation - ❌
Emit Stripe-Version header from spec(prescribes the fix) - ✅
Sync template misroutes cursor pagination - ❌
[P1] WU-1: Switch sync template to cursor-aware pagination(priority + WU number + prescription)
Issue body (for new issues)
The body must be self-contained — an agent should be able to act on it without opening the manuscripts attachment. Use the shape below.
> Filed by `/printing-press-retro` from a run of **<api-display-name>**
> (scorecard <score>/100, <X>% verify pass).
## Summary
<2-4 sentences: what the problem is and why it matters. Stay
problem-shaped, not solution-shaped. Someone reading this paragraph alone
should understand what they're being asked to address.>
## Where to look
- **Component:** <comp-slug>
- **Likely area:** <path or files in the printing-press repo, e.g. `internal/generator/templates/`>
- **Triggered when:** <spec shape, API behavior, or runtime context that surfaces this>
## What we observed
<Concrete evidence usable to reproduce or locate the issue without opening
attachments. This is the meat of the body — be specific.>
- File paths and line numbers (in the printed CLI or the printing-press repo)
- Command + output snippet showing the failure
- Spec snippet showing the trigger condition
- Error messages, stack traces, or scorer output verbatim where relevant
> ⚠️ **Redact secrets and PII before pasting.** Issue bodies are public. When
> the evidence comes from scanner output, dogfood payloads, Greptile review
> comments, or live API responses, replace credentials with
> `<REDACTED:<vendor>-<kind>:<first4>...<last4>:<len>ch>` and PII with
> `<REDACTED:<kind>>` per [`secret-scrubbing.md`](secret-scrubbing.md)
> "Layer 0". The Phase 6 Step 3 loop runs `scrub_body` on this file and will
> hard-fail the WU's filing if it finds an unredacted vendor-prefix token —
> that's the floor, not a substitute for redacting at write time.
> **This applies most strictly to findings about secret/PII leaks**: quoting
> the actual leaked value to prove the leak exists re-leaks it.
## Suspected root cause
<Hypothesis with explicit confidence level. Don't manufacture certainty.>
- **If certain:** "The template at `internal/generator/templates/sync.go.tmpl`
hardcodes `pageToken` as the cursor param name."
- **If uncertain:** "Likely the openapi-parser is dropping the version
header on parse, but the generator template could also be omitting the
emit. Either could be the root cause; verify by checking parser output
before pinning a fix."
## Suggested direction
<Proposed fix direction with explicit confidence level. Offer alternatives
when reasonable doubts exist; always include a verification step.>
- **If certain:** "Add a profiler check in `internal/openapi/parser.go`
that detects spec-declared version headers and writes them to the
generator config. Emit them in the client template alongside auth headers."
- **If uncertain:** "Direction A: surface the field through the parser;
Direction B: post-process in the generator. A is more durable but
requires changes in two packages. Reproduce the failure with the
manuscripts evidence first, then choose."
## Acceptance criteria
- positive: <test that proves the fix works on the API class this targets>
- negative: <test that proves the fix doesn't regress unaffected APIs>
## Frequency
every / most APIs / subclass:<name> / this API only
## Complexity
small / medium / large
## Scope boundary
<What this issue does NOT include. Helps prevent fix creep. Skip if not applicable.>
## Dependencies
<Free text — only if there's a real prerequisite. Most issues say "None.">
## Findings absorbed
*Omit if 1:1 (the body above is the finding).*
#### F<n>: <title>
- <one-sentence symptom>
- Evidence: <where it showed up>
#### F<n>: ...
## Related issues
<Combined output from Phase 3 Step D (prior-retro doc archaeology) and
Step 2.5 (open-issue dedup `related-area` classification). Auto-cross-links
via `#N`. Sibling WUs in this same retro do NOT appear here unless one is
genuinely a prerequisite.>
- #<num> — prior retro (`aligned`/`contradicts`/`extends`): <one-sentence note>
- #<num> — open issue (`related-area`): <one-sentence note on the adjacency>
- *(or "None" when neither scan surfaced anything)*
## Artifacts
| Artifact | Link |
|----------|------|
| Retro document | <$RETRO_DOC_URL or "Upload failed — see local copy"> |
| Manuscripts (research + proofs) | <$MANUSCRIPTS_URL or "Upload failed — see local copy"> |
| Generated CLI source code | <$CLI_SOURCE_URL or "Upload failed — see local copy"> |
---
*Generated by `/printing-press-retro` · [CLI Printing Press](https://github.com/mvanhorn/cli-printing-press)*What's not in the body, by design:
- Session Stats table — collapsed into the one-line provenance header.
The retro doc has the full stats.
- What the Printing Press Got Right — retro-wide; lives in the retro doc.
- Skipped table — retro-wide triage record; lives in the retro doc.
- Auto-cross-references to sibling WUs in this same retro — these are
noise unless one WU is genuinely a prerequisite for another (and that goes in Dependencies: as free text, not as an auto-linked #N).
Comment body (for comment:#N decisions)
**Recurrence in <api-display-name> retro** *(<retro-date>)*
Same problem surfaced again in this run. New observations:
- <concrete observation 1: file:line, command output, etc.>
- <concrete observation 2>
Updated frequency estimate: <every / most / subclass:<name>>
[Full retro doc](<$RETRO_DOC_URL>) · [Manuscripts](<$MANUSCRIPTS_URL>)
---
*Comment added by `/printing-press-retro` from the <api-display-name> retro.*The comment is intentionally short — it's recurrence evidence, not a re-statement of the original issue. The retro doc carries the full audit trail for anyone who wants more context.
Parallel execution
declare -a OUTCOME_KIND OUTCOME_URL OUTCOME_TITLE OUTCOME_PRIORITY OUTCOME_COMP OUTCOME_COMPLEXITY
declare -a FAILED_ISSUES
ISSUE_TMPDIR=$(mktemp -d)
ISSUE_RUN_START_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
(
WU="${SORTED_WORK_UNITS[$wu_idx]}"
DEDUP="${WU_DEDUP[$wu_idx]}" # "comment:NN" or empty
# Each WU contributes: $WU_TITLE, $WU_BODY, $WU_COMMENT_BODY,
# $WU_PRIORITY_NUM, $WU_PRIORITY_LABEL, $WU_COMP_SLUG, $WU_COMPLEXITY.
KIND=""
URL=""
FAIL_MSG=""
# Layer 0 body scrub — runs once per WU on whichever body is about to be
# posted. scrub_body is defined in references/secret-scrubbing.md and must
# be sourced or pasted in the bash block enclosing this loop.
#
# Hard-fail behavior: if the body contains an unredacted vendor-prefix
# token (lin_api_, sk_live_, ghp_, etc.) we refuse to post that WU and
# surface it in $FAILED_ISSUES so the agent can hand-redact and retry.
# PII patterns auto-redact in place. The original $WU_BODY /
# $WU_COMMENT_BODY shell var is untouched; only the file passed to gh
# gets the scrubbed text.
BODY_TMP="$ISSUE_TMPDIR/body-$wu_idx.md"
BODY_TMP_SCRUBBED="$ISSUE_TMPDIR/body-$wu_idx.scrubbed.md"
if [[ "$DEDUP" == comment:* ]]; then
printf '%s' "$WU_COMMENT_BODY" > "$BODY_TMP"
else
printf '%s' "$WU_BODY" > "$BODY_TMP"
fi
if ! scrub_body "$BODY_TMP" "$BODY_TMP_SCRUBBED" 2>"$ISSUE_TMPDIR/scrub-$wu_idx.err"; then
KIND="scrub-failed"
SCRUB_REASON=$(tr '\n' ' ' < "$ISSUE_TMPDIR/scrub-$wu_idx.err" | head -c 400)
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."
URL=""
elif [[ "$DEDUP" == comment:* ]]; then
ISSUE_NUM="${DEDUP#comment:}"
if URL=$(gh issue comment "$ISSUE_NUM" \
--repo "$REPO" \
--body-file "$BODY_TMP_SCRUBBED" 2>&1) \
&& [[ "$URL" == https://* ]]; then
KIND="commented"
else
KIND="comment-failed"
FAIL_MSG="$WU_TITLE — comment on #$ISSUE_NUM failed: ${URL:-no-response}"
URL=""
fi
else
if URL=$(gh issue create \
--repo "$REPO" \
--title "$WU_TITLE" \
--body-file "$BODY_TMP_SCRUBBED" \
--label retro \
--label "priority:P${WU_PRIORITY_NUM}" \
--label "comp:${WU_COMP_SLUG}" 2>&1) \
&& [[ "$URL" == https://* ]]; then
KIND="created"
else
KIND="create-failed"
FAIL_MSG="$WU_TITLE — issue creation failed: ${URL:-no-response}"
URL=""
fi
fi
{
printf '%s\n' "$KIND"
printf '%s\n' "$URL"
printf '%s\n' "$WU_TITLE"
printf '%s\n' "$WU_PRIORITY_LABEL"
printf '%s\n' "$WU_COMP_SLUG"
printf '%s\n' "$WU_COMPLEXITY"
printf '%s\n' "$FAIL_MSG"
} > "$ISSUE_TMPDIR/issue-$wu_idx"
) &
done
wait
# Defensive duplicate detector. If an agent accidentally used the malformed
# `URL=$(gh issue create ... &)` shortcut outside this reference, those
# backgrounded issue creates may succeed even though the captured URL is empty.
# Extra issues created by the current user since the run began, beyond the issue
# numbers captured in per-WU temp files, signal that parallel filing leaked
# creates. Use the live REST list endpoint instead of search, whose index can lag
# immediately after creation.
EXPECTED_ISSUE_NUMBERS=$(
for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
if [ -f "$ISSUE_TMPDIR/issue-$wu_idx" ]; then
{
IFS= read -r KIND_TMP
IFS= read -r URL_TMP
} < "$ISSUE_TMPDIR/issue-$wu_idx"
if [[ "$KIND_TMP" == created && "$URL_TMP" =~ /issues/([0-9]+)$ ]]; then
printf '%s\n' "${BASH_REMATCH[1]}"
fi
fi
done | sort -u
)
CURRENT_GH_USER=$(gh api user --jq .login 2>/dev/null || true)
RECENT_CREATED_LINES=""
if [ -n "$CURRENT_GH_USER" ]; then
RECENT_CREATED_LINES=$(gh api --method GET "repos/$REPO/issues" \
-f state=open \
-f since="$ISSUE_RUN_START_ISO" \
-f sort=created \
-f direction=desc \
-f per_page=100 \
--jq ".[] | select((.pull_request | not) and .user.login == \"$CURRENT_GH_USER\" and .created_at >= \"$ISSUE_RUN_START_ISO\") | \"#\(.number) \(.title)\"" 2>/dev/null || true)
fi
UNEXPECTED_CREATED_LINES=$(printf '%s\n' "$RECENT_CREATED_LINES" | sed '/^$/d')
if [ -n "$EXPECTED_ISSUE_NUMBERS" ]; then
while IFS= read -r issue_num; do
[ -z "$issue_num" ] && continue
UNEXPECTED_CREATED_LINES=$(printf '%s\n' "$UNEXPECTED_CREATED_LINES" | grep -v "^#$issue_num " || true)
done <<EOF
$EXPECTED_ISSUE_NUMBERS
EOF
fi
EXPECTED_CREATES=0
for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
if [[ "${WU_DEDUP[$wu_idx]}" != comment:* ]]; then
KIND_TMP=$(head -1 "$ISSUE_TMPDIR/issue-$wu_idx" 2>/dev/null)
[[ "$KIND_TMP" == scrub-failed ]] || EXPECTED_CREATES=$((EXPECTED_CREATES + 1))
fi
done
UNEXPECTED_CREATED_COUNT=$(printf '%s\n' "$UNEXPECTED_CREATED_LINES" | sed '/^$/d' | wc -l | tr -d ' ')
if [ "$UNEXPECTED_CREATED_COUNT" -gt 0 ]; then
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' \
"$UNEXPECTED_CREATED_COUNT" "$ISSUE_RUN_START_ISO" "$EXPECTED_CREATES" >&2
printf '%s\n' "$UNEXPECTED_CREATED_LINES" | sed 's/^/ /' >&2
fi
for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
{
IFS= read -r KIND
IFS= read -r URL
IFS= read -r TITLE
IFS= read -r PRIORITY
IFS= read -r COMP
IFS= read -r COMPLEXITY
IFS= read -r FAIL_MSG
} < "$ISSUE_TMPDIR/issue-$wu_idx"
OUTCOME_KIND+=("$KIND")
OUTCOME_URL+=("$URL")
OUTCOME_TITLE+=("$TITLE")
OUTCOME_PRIORITY+=("$PRIORITY")
OUTCOME_COMP+=("$COMP")
OUTCOME_COMPLEXITY+=("$COMPLEXITY")
case "$KIND" in
created|commented)
echo "${KIND^}: $URL"
;;
create-failed|comment-failed|scrub-failed)
echo "WARNING: $FAIL_MSG"
FAILED_ISSUES+=("$FAIL_MSG")
;;
esac
done
# Cleanup is conditional on scrub-failed WUs. Those WUs' body files are the
# canonical hand-redaction source the failure message points at — wiping
# $ISSUE_TMPDIR would destroy the recovery path. Keep the dir alive when any
# WU scrub-failed; the agent (or user) can read the body file, hand-redact,
# and re-run the affected WUs without re-deriving the body. The dir is in
# $(mktemp -d) so it self-cleans on OS reboot / `tmpwatch` regardless.
SCRUB_FAILED_COUNT=0
for KIND in "${OUTCOME_KIND[@]}"; do
[ "$KIND" = "scrub-failed" ] && SCRUB_FAILED_COUNT=$((SCRUB_FAILED_COUNT + 1))
done
if [ "$SCRUB_FAILED_COUNT" -eq 0 ]; then
rm -rf "$ISSUE_TMPDIR"
else
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
fiFailure modes:
| Mode | What happened | What the user sees in Phase 6 Step 6 |
|---|---|---|
created | New issue filed with labels | Listed in success summary |
commented | Comment added to existing issue | Listed as "commented on #N" |
create-failed | gh issue create returned no usable URL | $FAILED_ISSUES summary; manual filing instructions |
comment-failed | gh issue comment failed | $FAILED_ISSUES summary; manual comment instructions |
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 |
Variables expected
| Variable | Set by | Contains |
|---|---|---|
$REPO | This file Step 1 | Owner/repo string for gh |
$RETRO_DOC_URL | artifact-packaging.md | catbox URL for retro .md, or empty |
$MANUSCRIPTS_URL | artifact-packaging.md | catbox URL or empty |
$CLI_SOURCE_URL | artifact-packaging.md | catbox URL or empty |
$RETRO_PROOF_PATH | SKILL.md Phase 5 | Path to saved retro in manuscript proofs |
$RETRO_SCRATCH_PATH | SKILL.md Phase 5 | Path to temp retro copy under /tmp/printing-press/retro/ |
$WORK_UNITS | SKILL.md Phase 5.5 | Array of WU records |
$SORTED_WORK_UNITS | This file Step 2 | $WORK_UNITS sorted P1 → P3 |
$EXISTING_OPEN_RETROS | This file Step 2.5 | JSON of open retro-tagged issues |
$WU_DEDUP | This file Step 2.5 | Per-WU dedup decision: comment:NN or empty |
$WU_RELATED | This file Step 2.5 + Phase 3 Step D | Per-WU comma-separated related-issue numbers (annotated for the body) |
| All retro findings | SKILL.md Phase 4 | Used to populate each issue's "Findings absorbed" section |
Variables produced
| Variable | Contains |
|---|---|
$OUTCOME_KIND | Array, one per WU: created / commented / create-failed / comment-failed / scrub-failed |
$OUTCOME_URL | Array of issue/comment URLs (empty for failures) |
$FAILED_ISSUES | Array of human-readable failure descriptions; empty if every WU succeeded |
Handling gh failure (graceful degradation)
Check gh auth at the start of Phase 6 Step 4:
if ! gh auth status 2>/dev/null; then
echo "GitHub CLI is not authenticated. Cannot create issues or comments."
GH_AVAILABLE=false
else
GH_AVAILABLE=true
fiIf GH_AVAILABLE=false, or if every per-WU action failed, fall back to printing manual filing instructions:
if [ "$GH_AVAILABLE" = false ] || [ "${#FAILED_ISSUES[@]}" -eq "${#SORTED_WORK_UNITS[@]}" ]; then
echo ""
echo "Could not create issues or comments automatically."
echo ""
echo "To file the retro manually:"
echo " 1. Go to: https://github.com/mvanhorn/cli-printing-press/issues"
echo " 2. Use the body templates from the retro document at:"
echo " $RETRO_PROOF_PATH"
if [ -n "$RETRO_SCRATCH_PATH" ] && [ -f "$RETRO_SCRATCH_PATH" ]; then
echo " $RETRO_SCRATCH_PATH"
fi
echo " 3. File one issue per work unit. Apply labels: retro, priority:P<n>, comp:<slug>."
if [ -n "$MANUSCRIPTS_URL" ]; then
echo " 4. Manuscripts: $MANUSCRIPTS_URL"
fi
if [ -n "$CLI_SOURCE_URL" ]; then
echo " 5. CLI source: $CLI_SOURCE_URL"
fi
fiPer-WU partial failures (some succeed, some don't) are surfaced through $FAILED_ISSUES in Phase 6 Step 6 — the user sees both the successful URLs and the failed ones with manual filing instructions for just the missing ones.
Handling body size
GitHub issue bodies have a practical limit (~65KB). The flat-issue shape keeps each issue narrow, but a single WU with many absorbed findings + long related-issue chains could still approach it. If gh issue create rejects a body for size:
1. Truncate "What we observed" and "Findings absorbed" to one bullet per item, with a pointer to the retro doc. 2. Add: "Full finding analysis available in the retro document linked under Artifacts." 3. Retry.
Secret & PII Scrubbing for Public Artifacts
Read this file during Phase 5 (when writing the retro doc) and during Phase 6 (before zipping artifacts AND before posting issue bodies to GitHub). All scrub operations work on temp copies of the retro doc and issue bodies, never on the user's original manuscripts or library directories.
Layer 0: Single-file body scrub (retro doc + issue bodies)
Use this layer when the input is a single markdown file (the retro doc at Phase 5, or each issue/comment body file at Phase 6 Step 3 before gh issue create/gh issue comment). It's the public-surface counterpart to Layers 1–4 (which scrub directories of artifacts).
Why this layer exists. Layers 1–4 scrub $STAGING_MANUSCRIPTS and $STAGING_CLI_SOURCE (the folders zipped and uploaded to catbox). They do not touch the retro doc itself or the issue body text passed to gh issue create. Without Layer 0, a finding's "What we observed" block can paste raw scanner output, dogfood payloads, or Greptile comments containing real secrets or PII straight to a public GitHub issue. The retro skill's cardinal rule ("Issue bodies and retro docs are public surfaces") is the human-readable charter; this layer is the mechanical enforcement.
scrub_body — reusable shell function
Define this once at the top of the Phase 5 / Phase 6 bash blocks, then call scrub_body <input> <output> to produce a scrubbed copy of a single markdown file. Returns exit codes that callers branch on.
# scrub_body <in-file> <out-file>
#
# Scans the input file for vendor-prefix tokens (HARD FAIL) and PII patterns
# (auto-redact). Writes a scrubbed copy to <out-file>. Behavior:
#
# exit 0 — clean (no findings, or only PII auto-redacted)
# exit 1 — vendor-prefix token detected; refuses to write output. Caller
# must hand-redact before proceeding. The offending file path
# and pattern name go to stderr.
# exit 2 — write failure or invalid arguments
#
# Hard-fail rationale: vendor-prefix tokens (API keys, OAuth tokens, JWTs)
# are unrecoverable leaks once posted. Auto-redacting them would let an agent
# inadvertently strip a real key the maintainer needs to know about
# (e.g., "the test fixture key on line 14 is real — rotate it"). Forcing the
# agent to redact-and-acknowledge keeps the human (or upstream agent) in the
# loop. PII (emails, phones, real names) is auto-redacted because the
# replacement is lossless for the retro use case.
scrub_body() {
local in="$1" out="$2"
if [ -z "$in" ] || [ -z "$out" ] || [ ! -f "$in" ]; then
echo "scrub_body: usage: scrub_body <in-file> <out-file>" >&2
return 2
fi
# Layer 0a: vendor-prefix HARD-FAIL patterns. Order: most-specific first.
# Mirrors the patterns in internal/artifacts/secrets.go and Layer 2 below,
# extended with vendor patterns whose key shape is unambiguous enough to
# anchor without high false-positive rates (Mailchimp's `-us\d{1,2}`
# datacenter suffix, Linear's `lin_api_` prefix, Anthropic's `sk-ant-api03-`
# prefix). Add new patterns here only when the shape is specific enough
# that the regex cannot match generic placeholder strings.
local VENDOR_PATTERNS=(
'stripe-live-key|sk_live_[A-Za-z0-9]{20,}'
'stripe-test-key|sk_test_[A-Za-z0-9]{20,}'
'github-pat|ghp_[A-Za-z0-9]{36,}'
'github-oauth|gho_[A-Za-z0-9]{36,}'
'github-server|ghs_[A-Za-z0-9]{36,}'
'slack-bot-token|xoxb-[A-Za-z0-9-]{20,}'
'slack-user-token|xoxp-[A-Za-z0-9-]{20,}'
'aws-access-key|\bAKIA[0-9A-Z]{16}\b'
'openrouter-key|sk-or-v1-[A-Za-z0-9_-]{24,}'
'anthropic-key|sk-ant-api03-[A-Za-z0-9_-]{40,}'
'linear-key|\blin_api_[A-Za-z0-9_-]{32,}'
'mailchimp-key|\b[a-f0-9]{32}-us[0-9]{1,2}\b'
'jwt-token|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}'
'bearer-with-value|Bearer [A-Za-z0-9._~+/=-]{30,}'
)
local hard_fail=0
for entry in "${VENDOR_PATTERNS[@]}"; do
IFS='|' read -r name regex <<< "$entry"
if grep -qE "$regex" "$in" 2>/dev/null; then
# Surface the finding without quoting the value (the file path + pattern
# name is enough for the agent to locate and fix; printing the value to
# stderr defeats the whole point of refusing to write the output).
lines=$(grep -nE "$regex" "$in" 2>/dev/null | cut -d: -f1 | head -5 | tr '\n' ',' | sed 's/,$//')
echo "scrub_body: HARD FAIL — $name pattern matched in $in (lines: $lines)" >&2
hard_fail=1
fi
done
if [ "$hard_fail" -eq 1 ]; then
echo "scrub_body: refusing to write $out. Hand-redact the matches above with <REDACTED:<vendor>-<kind>:<first4>...<last4>:<len>ch> per references/secret-scrubbing.md Layer 0, then retry." >&2
return 1
fi
# Layer 0b: PII auto-redact patterns. Replaces in the written copy only;
# input file is untouched. Tags use the same <REDACTED:<kind>> shape as the
# rest of the scrubbing layers.
#
# Allowlist: RFC 2606 reserved example domains (example.com / example.net /
# example.org / example.invalid / *.test / *.localhost) and NANP fictional
# phone ranges (555-01XX) are PASS-THROUGH — they exist precisely to be safe
# in documentation. The email regex anchors the allowlist by excluding the
# reserved TLDs in the local-part match. The phone regex carves out the
# 555-01XX range explicitly.
cp "$in" "$out" 2>/dev/null || { echo "scrub_body: failed to copy $in -> $out" >&2; return 2; }
# Order matters: specific account-identifier patterns first, then generic
# email/phone/zip. Otherwise the generic `email` pattern would consume the
# mailchimp-inbox-id email-shaped string and tag it as `<REDACTED:email>`
# instead of `<REDACTED:mailchimp-inbox-id>` — both redact the leak, but
# the specific tag is more useful for diagnostics.
# Mailchimp inbox-id: us<dc>-<hex>-<hex>@inbound.mailchimp.com is the
# account-bound inbound mailbox identifier — leaking it exposes the
# account's identity. Hex segments vary by account; 8+ chars each keeps
# the false-positive rate near zero (the @inbound.mailchimp.com tail is
# the strongest discriminator).
perl -i -pe 's/\bus\d{1,2}-[a-f0-9]{8,}-[a-f0-9]{8,}\@inbound\.mailchimp\.com\b/<REDACTED:mailchimp-inbox-id>/g' "$out" 2>/dev/null
# Real email (excluding RFC 2606 allowlist). The exclusion uses negative
# lookbehind via perl since BSD/GNU sed regex flavors differ.
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/<REDACTED:email>/g' "$out" 2>/dev/null
# NANP US phone (excluding 555-01XX fictional range). Matches 10-digit forms
# with or without dashes/parens/spaces. The 555-01XX exclusion can't use a
# single negative lookahead anchored at the start because the optional `(`
# and optional country-code prefix shift the area-code position — when the
# number is written `(555) 012-3456`, the position before the `(` is not
# where the `555` lives, so the lookahead never fires. Instead, capture the
# area code ($1) and exchange ($2) and decide inline via `/e`.
#
# Perl gotcha: $& and $1..$N are global variables that get rewritten by
# every successful regex match — including the inner `$e =~ /^01/` test
# inside the replacement callback. Snapshot $&, $1, $2 into lexical
# variables ($w, $a, $e) BEFORE running the inner regex, otherwise the
# carve-out path returns `"01"` (the inner match) instead of the whole
# original phone string.
perl -i -pe 's{(?<![0-9])(?:\+?1[-\s.]?)?\(?([2-9][0-9]{2})\)?[-\s.]?([0-9]{3})[-\s.]?[0-9]{4}(?![0-9])}{my $w=$&; my $a=$1; my $e=$2; ($a eq "555" && $e =~ /^01/) ? $w : "<REDACTED:phone-us>"}ge' "$out" 2>/dev/null
# ZIP+4 (5 digits, dash, 4 digits). Bare 5-digit ZIPs are too noisy to redact
# safely (false-positives on order IDs, line counts, etc.); ZIP+4 is the
# dashed form that's unambiguous.
perl -i -pe 's/\b\d{5}-\d{4}\b/<REDACTED:zip-plus-4>/g' "$out" 2>/dev/null
return 0
}When to call scrub_body
Phase 5 — after writing the retro doc:
# Write retro to $RETRO_PROOF_PATH and $RETRO_SCRATCH_PATH as before, then:
RETRO_PROOF_PATH_SCRUBBED="${RETRO_PROOF_PATH}.scrubbed.md"
if ! scrub_body "$RETRO_PROOF_PATH" "$RETRO_PROOF_PATH_SCRUBBED"; then
echo "ERROR: retro doc contains an unredacted vendor-prefix secret." >&2
echo "Open $RETRO_PROOF_PATH, redact the matches reported above per references/secret-scrubbing.md Layer 0, then re-run Phase 5." >&2
exit 1
fi
mv "$RETRO_PROOF_PATH_SCRUBBED" "$RETRO_PROOF_PATH"
cp "$RETRO_PROOF_PATH" "$RETRO_SCRATCH_PATH"The scrubbed retro doc becomes canonical. The original (with potential PII) existed in process memory only and is overwritten by the scrubbed version.
Phase 6 Step 3 — before `gh issue create` and `gh issue comment`:
# Build the WU body as before to /tmp/wu1-body.md, then:
if ! scrub_body /tmp/wu1-body.md /tmp/wu1-body-scrubbed.md; then
echo "ERROR: WU body contains an unredacted vendor-prefix secret. Cannot post." >&2
# Mark this WU as a failed action in $FAILED_ISSUES so Step 6 surfaces it.
FAILED_ISSUES+="WU-1 (issue create): scrub_body hard-failed; body file left at /tmp/wu1-body.md for manual redaction.\n"
continue
fi
gh issue create ... --body-file /tmp/wu1-body-scrubbed.mdSame pattern for gh issue comment. If scrub_body hard-fails, that WU's filing is skipped and reported in the final summary so the agent (or user) knows to manually redact and retry.
Redaction shape reference
When the agent (or a maintainer reviewing the retro doc) needs to hand-redact a vendor-prefix value, use this format:
<REDACTED:<vendor>-<kind>:<first4>...<last4>:<len>ch>Examples:
| Original (anti-pattern) | Redacted (correct) |
|---|---|
lin_api_a1b2c3d4e5f6789abcdef0123456789abcdef0123456 | <REDACTED:linear-api-key:lin_a...0456:48ch> |
22eb323ac258d2e04c3bf9ade77b9c78-us6 | <REDACTED:mailchimp-api-key:22eb...-us6:36ch> |
ghp_abc123def456ghi789jkl012mno345pqr678stu90 | <REDACTED:github-pat:ghp_a...tu90:40ch> |
sk_live_51AbCdEf123456...XyZ | <REDACTED:stripe-live-key:sk_li...XyZ:N-ch> |
The first4 + last4 + length fragment preserves enough shape information for a maintainer to recognize the vendor pattern (and to confirm the same key isn't recurring across multiple findings) without re-exposing the value. The <kind> tag is enough to drive a follow-up retro finding ("the scanner missed a linear-api-key") without quoting the value.
For PII (email, phone-us, zip-plus-4, mailchimp-inbox-id, etc.), just the <kind> tag — no fragment — is the right shape. The maintainer doesn't need to identify which specific email; they need to know an email shape was leaked.
Things to never quote even in redacted form
- Full URLs containing tokens. GitHub's secret-scanning "unblock-secret" URLs (e.g.,
https://github.com/<org>/<repo>/security/secret-scanning/unblock-secret/<token-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<REDACTED:gh-secret-scanning-url>. - 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
<REDACTED:authorization-header>. - 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.
Layer 1: Exact-value scanning
If the user provided an API key during the generation session, scan for its literal value and redact it. This has zero false positives.
Use grep -F (fixed string) — NOT bare grep or sed — because API keys often contain regex metacharacters (+, /, ., =).
# Guard: skip if key is empty or too short (< 16 chars)
if [ -n "$API_KEY_VALUE" ] && [ ${#API_KEY_VALUE} -ge 16 ]; then
LEAK_FOUND=false
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
if [ -d "$dir" ] && grep -rF "$API_KEY_VALUE" "$dir" 2>/dev/null; then
LEAK_FOUND=true
fi
done
if [ "$LEAK_FOUND" = true ]; then
echo "BLOCKING: API key value found in staging artifacts. Auto-redacting."
REDACT_TO='<REDACTED:session-api-key>'
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
if grep -qF "$API_KEY_VALUE" "$f" 2>/dev/null; then
REDACT_OLD="$API_KEY_VALUE" REDACT_NEW="$REDACT_TO" python3 -c "
import sys, os
old, new, path = os.environ['REDACT_OLD'], os.environ['REDACT_NEW'], sys.argv[1]
with open(path) as f: content = f.read()
with open(path, 'w') as f: f.write(content.replace(old, new))
" "$f"
fi
done
done
echo "Auto-redacted session API key."
fi
fiLayer 2: Pattern-based scanning
Scan for common secret formats regardless of whether a session key was provided. Each pattern uses grep -rE with a concrete regex and a labeled redaction tag.
Run each pattern scan independently. A false positive from one pattern does not affect other scans.
# Define patterns: name|regex|redaction-tag
PATTERNS=(
'stripe-live-key|sk_live_[A-Za-z0-9]{20,}|<REDACTED:stripe-live-key>'
'stripe-test-key|sk_test_[A-Za-z0-9]{20,}|<REDACTED:stripe-test-key>'
'github-pat|ghp_[A-Za-z0-9]{36,}|<REDACTED:github-pat>'
'github-oauth|gho_[A-Za-z0-9]{36,}|<REDACTED:github-oauth>'
'bearer-token|Bearer [A-Za-z0-9._~+/=-]{20,}|<REDACTED:bearer-token>'
'jwt-token|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|<REDACTED:jwt-token>'
)
for entry in "${PATTERNS[@]}"; do
IFS='|' read -r name regex tag <<< "$entry"
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
if grep -qE "$regex" "$f" 2>/dev/null; then
# Use perl for in-place regex replacement (more reliable than sed across platforms)
perl -i -pe "s/$regex/$tag/g" "$f" 2>/dev/null
echo "Redacted $name in $(basename "$f")"
fi
done
done
doneJurisdiction-specific PII scanning
Live-API responses captured during browser-sniff or live-key dogfood routinely include identifying data of the data subject and any third parties the API surfaced. These patterns redact common high-confidence shapes before upload. They are defense-in-depth, not bulletproof — free-form names, descriptive fields, and unenumerated jurisdictions still slip through.
The | field separator collides with the (IT|DE|...) country-code alternation in the IBAN regex, so PII patterns use ~ as the field separator.
PII_PATTERNS=(
'codice-fiscale~\b[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]\b~<REDACTED:pii-codice-fiscale>'
'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~<REDACTED:pii-eu-iban>'
'us-ssn~\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b~<REDACTED:pii-us-ssn>'
)
for entry in "${PII_PATTERNS[@]}"; do
IFS='~' read -r name regex tag <<< "$entry"
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
# Case-insensitive: API JSON routinely lowercases IBANs and other identifiers.
if grep -qiE "$regex" "$f" 2>/dev/null; then
perl -i -pe "s/$regex/$tag/gi" "$f" 2>/dev/null
echo "Redacted $name in $(basename "$f")"
fi
done
done
donePattern notes:
- Codice Fiscale (Italian tax code) is a 16-character
LLLLLLDDLDDLDDDLshape with no plausible collision against ordinary text. - 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. - US SSN uses the
DDD-DD-DDDDdashed form, which avoids collisions with bare 9-digit runs in other identifiers.
Out of scope (deferred to follow-up work):
- Bare 11-digit Partita IVA / VAT numbers (false-positive rate against order IDs, phone numbers, and timestamps is too high without an allowlist).
- Free-form residential addresses (not regex-matchable with acceptable precision).
- Refusing upload when
discovery/sample-*.jsonfiles are present (a separate gate at the staging-copy step, tracked separately).
Env var assignment scanning
Separately scan for hardcoded secret assignments in source code:
# Matches assignments where the variable name ENDS with a secret-like suffix:
# API_SECRET = "value", AUTH_TOKEN: 'value', API_KEY="value", DB_PASSWORD='value'
# Does NOT match: CACHE_KEY, PRIMARY_KEY, TOKEN_EXPIRY (keyword is not a suffix)
# Only in .go, .env, .yaml, .yml, .json, .toml files
SECRET_ASSIGN_REGEX='[A-Z_]+(SECRET|_TOKEN|_KEY|PASSWORD)\s*[:=]\s*["'"'"'][^"'"'"']{16,}["'"'"']'
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
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
if grep -qE "$SECRET_ASSIGN_REGEX" "$f" 2>/dev/null; then
perl -i -pe 's/[A-Z_]+(SECRET|_TOKEN|_KEY|PASSWORD)\s*[:=]\s*["\x27][^"\x27]{16,}["\x27]/$1=<REDACTED:env-assignment>/g' "$f" 2>/dev/null
echo "Redacted env assignment in $(basename "$f")"
fi
done
doneLayer 3: HAR auth stripping
If the staging manuscripts contain HAR files, strip auth-bearing fields:
find "$STAGING_MANUSCRIPTS" -name "*.har" -type f -print0 2>/dev/null | while IFS= read -r -d '' har; do
jq 'del(.log.entries[].response.content.text) |
(.log.entries[].request.headers) |= [.[] |
select(.name | test("^(Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token)$"; "i") | not)
] |
(.log.entries[].response.headers) |= [.[] |
select(.name | test("^(Set-Cookie)$"; "i") | not)
] |
(.log.entries[].request.queryString) |= [.[] |
if (.name | test("^(key|api_key|apikey|token|secret|access_token|password)$"; "i"))
then .value = "<REDACTED>"
else . end
] |
(.log.entries[].request.cookies) |= [] |
(.log.entries[].response.cookies) |= []
' "$har" > "${har}.stripped" 2>/dev/null && mv "${har}.stripped" "$har"
echo "Stripped auth from $(basename "$har")"
doneLayer 4: Session state cleanup
Remove any session state files that may contain cookies or tokens:
find "$STAGING_MANUSCRIPTS" -name "session-state.json" -type f -delete 2>/dev/nullPost-scrub verification
After all layers complete, do a final scan for obvious leaks:
FINAL_CHECK=false
CRED_REGEX='(sk_live_|sk_test_|ghp_|gho_|Bearer [A-Za-z0-9]{20})'
# PII_REGEX must mirror the shapes in PII_PATTERNS above; update both together
# (e.g. when adding Partita IVA with an allowlist) so the verification step
# does not silently stop checking a shape the scrub loop still redacts.
PII_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)'
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
CRED_MATCHES=$(grep -rEi "$CRED_REGEX" "$dir" 2>/dev/null | grep -v 'REDACTED' | head -5)
PII_MATCHES=$(grep -rEi "$PII_REGEX" "$dir" 2>/dev/null | grep -v 'REDACTED' | head -5)
if [ -n "$CRED_MATCHES" ] || [ -n "$PII_MATCHES" ]; then
[ -n "$CRED_MATCHES" ] && echo "$CRED_MATCHES"
[ -n "$PII_MATCHES" ] && echo "$PII_MATCHES"
FINAL_CHECK=true
fi
done
if [ "$FINAL_CHECK" = true ]; then
echo "WARNING: Potential secrets or PII still found after scrubbing. Review the matches above."
echo "Artifacts will NOT be uploaded until this is resolved."
fiRelated skills
How it compares
printing-press-retro implements its own SKILL.md workflow rather than a generic substitute skill.
FAQ
Who is printing-press-retro for?
Agents and developers following the printing-press-retro SKILL.md guidance.
When should I use printing-press-retro?
When user intent matches description triggers and quick start scenarios.
Is printing-press-retro safe to install?
Review the Security Audits panel before production shell or network use.