phases/01-gather-evidence.md
## 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).
Next: phases/02-mine-the-session.md
phases/02-mine-the-session.md
## Phase 2: Mine the session
Scan the conversation history for seven 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.
**Context budget.** Retro typically starts inside a print session that is already
deep, and mining is the phase that reads the most. The live conversation is not the
only source: the session transcript exists on disk (Claude Code writes
`~/.claude/projects/<project-slug>/*.jsonl`; other harnesses keep equivalents), so an
orchestrated run may delegate this phase to a subagent that reads the transcript file
plus the Phase 1 evidence and returns only the candidate list. Later phases consume
the candidate list, never the transcript.
**If running in a fresh conversation without generation history:** Prefer mining the
print session's on-disk transcript (above) when it is still available — that path
loses no fidelity. Otherwise 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/references/run-resolution.md` (the Multi-Source Priority Gate),
`skills/printing-press/phases/08-ecosystem-absorb-gate.md` (the Priority inversion check),
and the brief's `## Source Priority` section in `skills/printing-press/phases/04-research-brief.md`
or in the generator if README ordering is template-driven.
Next: phases/03-triage-candidates.md
phases/03-triage-candidates.md
## 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 filed issue. "I want to record this somewhere" belongs in the local
retro doc, not in GitHub.
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`
6. **Would weekday maintainer triage close this?** Drop wishlists, skill/docs
polish, catalog polish, lint/gosec hygiene, one-vendor quirks, one-CLI
grab-bags, already-fixed behavior, theoretical collisions, and conveniences
that do not affect printed-CLI users or shipping gates.
Survivors of these six 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.
Next: phases/04-classify-findings.md
phases/04-classify-findings.md
## 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 do not get a priority and do not go in Do. Put them
in Skip when they survived deep analysis but failed the filing bar, or Drop when
the analysis shows they were triage noise after all.
**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 |
### Actionable issue type mapping
Every actionable work unit must carry exactly one real GitHub issue type label:
`bug` or `enhancement`. Derive it deterministically from the absorbed finding
categories:
| Finding category | Issue type |
|------------------|------------|
| Bug | `bug` |
| Scorer bug | `bug` |
| Assumption mismatch | `bug` |
| Default gap | `bug` |
| Template gap | `enhancement` |
| Recurring friction | `enhancement` |
| Missing scaffolding | `enhancement` |
| Discovered optimization | `enhancement` |
| Skill instruction gap | `enhancement` |
If a work unit absorbs multiple findings, `bug` wins when any absorbed category
maps to `bug`; otherwise use `enhancement`. Priority, component, provenance,
and routing/terminal labels are not substitutes for this type label.
**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/` |
| 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 library 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 public Printing Press Library 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 cannot be filed. Move it to Skip if it otherwise survived deep analysis,
or Drop if the evidence is only a one-off.
**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 — either drop it with a
"raised N times, still not justified" note, or reframe the finding into a smaller
incremental fix that now clears the P1/P2 bar. Recurrence without a sharper,
current-main defect 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 `--dates` for historical data"
- Bad fix: "Add `--dates` with `YYYYMMDD-YYYYMMDD` format, `--sport`/`--league` flags, and monthly chunking to the sync template"
- Good fix: "When the profiler detects a date-range query param, emit a `--dates` flag 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.
Next: phases/05-prioritize.md
phases/05-prioritize.md
## Phase 4: Prioritize
Sort survivors of Phase 3 into three buckets:
- **Do** — survived Phase 3 Step G with a clear case-for and clears P1 or P2
below. Scorer bugs are just findings like any other — rank them by user or
shipping 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.
### Priority rubric
- **P1 — the printed CLI is broken or unsafe.** Generate or regen ships a CLI
that does not work, or a user of that CLI can lose data, spend money, or skip a
safety prompt. Includes commands that fail or no-op; silent wrong or missing
data; broken or fail-open auth; skipped confirmations; regen dropping working
code; or generated CLIs that will not run or load config. "Produces a broken
CLI" is P1.
- **P2 — real generalizing defect, but the printed CLI still works.**
Scorer/dogfood/publish-skill/operator gates that mis-score or block shipping;
noticeable non-breaking polish such as wrong `which` ranking, a dropped
`doctor` render, or empty CSV output on zero rows. The defect must generalize
across CLIs, reproduce on current `main`, and be something that belongs in the
next weekday fix wave.
- **No P3.** If a survivor is not P1 or P2, move it to Skip or Drop. Do not file
it and do not apply any low-priority label.
**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 probably inflating;
force yourself to identify the weakest "Do" and ask whether it is truly a
broken or unsafe printed CLI rather than a P2, Skip, or Drop.
Next: phases/06-write-the-retro.md
phases/06-write-the-retro.md
## 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:
```markdown
# Printing Press Retro: <API name>
## Session Stats
- API: <name>
- Spec source: <public-library/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 |
|---------|-------|-----------|-----------|---------------------|------------|--------|
*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.
```bash
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:
```bash
# 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.
Next: phases/07-plannable-work-units.md
phases/07-plannable-work-units.md
## 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:
```markdown
### WU-1: <Title> (from F1, F3, ...)
- **Stable ID:** WU-1 *(preserve this identifier when sorting; dependency edges
use the stable ID rather than a post-sort array position)*
- **Priority:** P1 / P2 *(max priority among absorbed findings — P1 if any
absorbed finding is P1, else P2)*
- **Type:** bug / enhancement *(use the deterministic category mapping above;
`bug` wins for a mixed work unit)*
- **Component:** generator / openapi-parser / spec-parser / scorer / skill
*(must match one of the five 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:** None, unless another work unit or issue is a real
prerequisite. Explicit prerequisites become native GitHub `blocked-by` /
`blocking` relationships after issue numbers are known. Encode work-unit
edges as `WU-2|wu:WU-1` (dependent stable ID first); existing issues use
`WU-2|issue:123`. The executor validates that every WU ID is known and
resolves both endpoints by stable ID after priority sorting. Related-area
context stays prose in `Related issues`.
- **Complexity:** small / medium / large
```
The five fixed component slugs are: `generator` (`internal/generator/`),
`openapi-parser` (`internal/openapi/`), `spec-parser` (`internal/spec/`),
`scorer` (verify / dogfood / scorecard), and `skill` (`skills/printing-press/SKILL.md`).
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.
Next: phases/08-issue-gate.md
phases/08-issue-gate.md
## Phase 5.6: Issue gate — are there Printing Press improvements?
Apply the router issue-gate boundary. Each WU becomes one flat top-level
issue (no parent, no sub-issue hierarchy).
**Skip filing when** the Do table is empty, every survivor is Skip, or every
survivor is printed-CLI-specific. Still write the local retro (manuscript
proofs plus `/tmp/printing-press/retro/`), present findings, then jump to
Phase 6 Step 6 (local-only paths).
Otherwise continue to packaging and confirmation.
Next: phases/09-package-upload-present.md
phases/09-package-upload-present.md
## Phase 6: Package, upload, and present
### Step 1: Package artifacts into staging folder
Read and apply [../references/artifact-packaging.md](../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
`source:retro` or legacy `retro` issues. Each WU ends up classified as either:
- **File new** — no matching open issue
- **Comment on #N** — Step 2.5 found a `same` match; 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-area` matches; the new issue's body will reference them via `#N` in 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](https://github.com/mvanhorn/cli-printing-press):
>
> **Filing plan:**
>
> | # | Title | Plan | Notes |
> |---|-------|------|-------|
> | 1 | <wu-1 title> | File new (P1, bug, 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 carries `source:retro`, the mapped `bug` or `enhancement` type,
> `priority:P1` or `priority:P2`, and `comp:<slug>` labels — agents filter related work across
> retros with `gh issue list --label source:retro`, `gh issue list --label
> comp:<slug>`, or `gh issue list --label priority:P1`. During the label cutover,
> legacy `retro` issues remain discoverable and a write may use `retro` only when
> the canonical `source:retro` label is not available.
>
> 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 picks **Submit**, set `SUBMISSION_CONFIRMED=true` immediately before
running Step 3. Leave it unset or set it to `false` for the other choices; the
artifact-packaging reference refuses public uploads unless this explicit consent
marker and the successful scrub marker are both present.
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](../references/issue-template.md)
already ran during Step 2 (filing plan + confirm), so labels exist, the safe
provenance marker is selected, WUs are sorted, and `$WU_DEDUP`, `$WU_RELATED`,
and `$WU_DEPENDENCY_EDGES` 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 or sub-issue hierarchy — every new issue stands alone in GitHub's issue
list with its own open/close lifecycle. Explicit prerequisites are applied as
native `blocked-by`/`blocking` relationships after issue numbers are known;
ordinary related-area references remain prose.
Each new issue carries its own provenance marker (`source:retro`, or legacy
`retro` only when the canonical label is unavailable), exactly one mapped
`bug`/`enhancement` type, `priority:P1` or `priority:P2`, 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-area` issue 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,
which is captured in `Dependencies:` and applied natively rather than left as
prose alone.
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.
```bash
if [ -f "$RETRO_PROOF_PATH" ]; then
mkdir -p "$RETRO_SCRATCH_DIR"
cp "$RETRO_PROOF_PATH" "$RETRO_SCRATCH_PATH"
fi
```
### Step 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 → P2 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 `source:retro`,
> their mapped `bug` or `enhancement` type, `comp:<slug>`, and `priority:P1`/`priority:P2`
> labels — agents can filter related work across retros with `gh issue list
> --label source:retro`, `gh issue list --label comp:<slug>`, or `gh issue list
> --label priority:P1`.
> *(if artifacts uploaded)* Artifacts: [retro doc](<URL>) · [manuscripts](<URL>) · [CLI source](<URL>)
> 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`.
Next: return to the router
references/artifact-packaging.md
# 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
```bash
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:
```bash
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):
```bash
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=""
fi
```
## Step 3: Scrub secrets
Read and apply [references/secret-scrubbing.md](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.
Initialize the upload gate before running the scrub, so skipped or failed
verification cannot accidentally enable a public upload:
```bash
SCRUB_VERIFIED=false
```
After applying all scrub layers, run the post-scrub verification block from
`references/secret-scrubbing.md`. Record success only when that block completes
with no unresolved findings (`FINAL_CHECK=false`):
```bash
if [ "${FINAL_CHECK:-true}" = false ]; then
SCRUB_VERIFIED=true
else
echo "Upload blocked: post-scrub verification found unresolved findings or did not run."
fi
```
## Step 4: Zip artifacts
```bash
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)"
fi
```
## Step 5: Upload to catbox.moe
Upload each artifact and capture the returned URL:
```bash
MANUSCRIPTS_URL=""
CLI_SOURCE_URL=""
RETRO_DOC_URL=""
UPLOAD_FAILED=false
if [ "${SUBMISSION_CONFIRMED:-false}" != "true" ] || [ "${SCRUB_VERIFIED:-false}" != "true" ]; then
echo "REFUSING upload: explicit submission consent and successful secret-scrub verification are both required."
UPLOAD_FAILED=true
else
catbox_upload() {
# $1 = path to file. Prints the URL on success, returns non-zero on failure.
local response
response=$(curl -sf -F "reqtype=fileupload" -F "fileToUpload=@$1" https://catbox.moe/user/api.php 2>/dev/null) || return 1
if printf '%s' "$response" | grep -Eq '^https://files\.catbox\.moe/[A-Za-z0-9._-]+$'; then
printf '%s' "$response"
else
return 1
fi
}
# Upload retro document (raw .md — viewable directly in browser)
if RETRO_DOC_URL=$(catbox_upload "$RETRO_PROOF_PATH"); then
echo "Retro document uploaded: $RETRO_DOC_URL"
else
echo "WARNING: Failed to upload retro document to catbox.moe."
RETRO_DOC_URL=""
UPLOAD_FAILED=true
fi
# Upload manuscripts zip
if MANUSCRIPTS_URL=$(catbox_upload "$MANUSCRIPTS_ZIP"); then
echo "Manuscripts uploaded: $MANUSCRIPTS_URL"
else
echo "WARNING: Failed to upload manuscripts to catbox.moe."
MANUSCRIPTS_URL=""
UPLOAD_FAILED=true
fi
# Upload CLI source (only if it was packaged)
if [ -n "$CLI_SOURCE_ZIP" ] && [ -f "$CLI_SOURCE_ZIP" ]; then
if CLI_SOURCE_URL=$(catbox_upload "$CLI_SOURCE_ZIP"); then
echo "CLI source uploaded: $CLI_SOURCE_URL"
else
echo "WARNING: Failed to upload CLI source to catbox.moe."
CLI_SOURCE_URL=""
UPLOAD_FAILED=true
fi
else
echo "No CLI source to upload (manuscripts-only mode)."
fi
fi
```
## Step 6: Handle upload failure (R15)
If either upload failed:
```bash
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."
fi
```
## Step 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.
```bash
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 |
| `$SUBMISSION_CONFIRMED` | `true` only after the user selected Submit in Phase 6 Step 2; uploads are refused otherwise |
| `$SCRUB_VERIFIED` | `true` only when Step 3's post-scrub verification completed with no unresolved findings; uploads are refused otherwise |
| `$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) |
references/issue-template.md
# 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: `source:retro` marks
provenance, `comp:<slug>` surfaces every retro WU touching one component,
`priority:P1` surfaces high-priority work, and `bug`/`enhancement` carry the
real issue type.
- 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).
```bash
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" \
"priority:P1" "priority:P2" \
"bug" "enhancement" "source: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 (5) — 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"
# Priority labels (2) — drive priority-based filtering. The label is the
# primary carrier; titles do not duplicate the priority prefix.
ensure_label "priority:P1" "b60205" "Broken or unsafe printed CLI"
ensure_label "priority:P2" "d93f0b" "Current generalizing defect; printed CLI still works"
# Real issue types. These are actionable taxonomy, not routing or provenance.
ensure_label "bug" "d73a4a" "Something isn't working"
ensure_label "enhancement" "a2eeef" "New feature or request"
# Canonical provenance marker. Do not create the legacy `retro` label.
ensure_label "source:retro" "c59c0f" "Issue produced by /printing-press-retro; systemic Printing Press finding"
fi
REFRESHED_LABELS=$(gh label list --repo "$REPO" --limit 200 --json name --jq '.[].name' 2>/dev/null || true)
if [ -n "$REFRESHED_LABELS" ]; then
EXISTING_LABELS="$REFRESHED_LABELS"
fi
```
The `retro-parent` label is intentionally omitted — there are no parent
issues. If the legacy `retro` label exists from prior retros, leave it for
discovery during the cutover; the skill never creates new issues with it.
### Resolve the provenance marker for writes
`source:retro` is the canonical provenance label. Before creating an issue,
choose a marker from labels that are actually available, preferring
`source:retro` and falling back to legacy `retro` only when the canonical label
cannot be created or is not available. Never pass a guessed label to `gh issue
create`:
```bash
has_label() {
printf '%s\n' "$EXISTING_LABELS" | grep -qFx "$1"
}
RETRO_PROVENANCE_LABEL=""
if has_label "source:retro"; then
RETRO_PROVENANCE_LABEL="source:retro"
elif has_label "retro"; then
RETRO_PROVENANCE_LABEL="retro"
else
gh label create "source:retro" --repo "$REPO" --color "c59c0f" \
--description "Issue produced by /printing-press-retro; systemic Printing Press finding" 2>/dev/null || true
EXISTING_LABELS=$(gh label list --repo "$REPO" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo "")
if has_label "source:retro"; then
RETRO_PROVENANCE_LABEL="source:retro"
elif has_label "retro"; then
RETRO_PROVENANCE_LABEL="retro"
else
echo "ERROR: neither source:retro nor legacy retro is available; cannot file a provenance-marked issue." >&2
exit 1
fi
fi
```
The initial label pass provisions `source:retro` on a fresh fork. The
post-create re-list makes the fallback safe when label creation races or is
denied. Existing issues are not relabeled solely to rename provenance.
## Step 2: Sort work units
Sort WUs by priority: P1 first, then P2. A WU with any other priority is invalid:
return to SKILL.md Phase 4 and move it to Skip or Drop. 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).
```bash
# SORTED_WORK_UNITS is populated by sorting the complete WU records from
# $WORK_UNITS P1 → P2. Each record retains its own `Stable ID: WU-N` field;
# never sort a work-unit array and an ID array independently.
# Dependency edges use the stable ID extracted from each sorted record, never
# a sorted array position.
```
## 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 two label-filtered `gh` calls followed by per-WU agent reasoning over
the de-duplicated result set. **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
```bash
EXISTING_OPEN_RETROS_SOURCE=$(gh issue list \
--repo "$REPO" \
--label source:retro \
--state open \
--limit 200 \
--json number,title,url,labels 2>/dev/null \
|| echo "[]")
EXISTING_OPEN_RETROS_LEGACY=$(gh issue list \
--repo "$REPO" \
--label retro \
--state open \
--limit 200 \
--json number,title,url,labels 2>/dev/null \
|| echo "[]")
EXISTING_OPEN_RETROS=$(jq -s 'add | unique_by(.number)' \
<(printf '%s\n' "$EXISTING_OPEN_RETROS_SOURCE") \
<(printf '%s\n' "$EXISTING_OPEN_RETROS_LEGACY"))
```
The two results are merged by issue number so an issue carrying both labels is
considered once. No per-WU component filtering — the agent reasons over titles
across the whole open-retro set so a related-area issue under a different
component still surfaces. The `labels` field lets the executor preserve the
one-type invariant when commenting on an existing match.
### 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`](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.
```markdown
> 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>
- **Issue type:** `bug` or `enhancement` from the deterministic finding-category mapping
- **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
<"None." unless this work unit has a real prerequisite. For an explicit
prerequisite, name the blocking issue or work unit here. After issue numbers are
known, the filing pass applies GitHub's native relationship with
`gh issue edit <dependent> --repo "$REPO" --add-blocked-by <prerequisite>`.
Related-area references do not belong here and must remain prose in
`Related issues`.
## 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; a real prerequisite is also represented by a native
`blocked-by`/`blocking` relationship, not only by this prose link.>
- #<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:` and becomes a native `blocked-by` relationship after
issue numbers are known).
### Comment body (for `comment:#N` decisions)
```markdown
**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
```bash
declare -a OUTCOME_KIND OUTCOME_URL OUTCOME_TITLE OUTCOME_PRIORITY OUTCOME_COMP OUTCOME_COMPLEXITY
declare -a WU_DEPENDENCY_EDGES
declare -a FAILED_ISSUES
declare -A OUTCOME_ISSUE_NUM_BY_WU_ID SORTED_WU_ID_SEEN
extract_wu_id() {
printf '%s\n' "$1" \
| sed -nE 's/^- \*\*Stable ID:\*\* (WU-[1-9][0-9]*)([[:space:]].*)?$/\1/p' \
| head -1
}
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_ID="$(extract_wu_id "${SORTED_WORK_UNITS[$wu_idx]}")"
if [[ ! "$WU_ID" =~ ^WU-[1-9][0-9]*$ ]]; then
echo "ERROR: sorted WU $wu_idx has missing or duplicate stable ID: ${WU_ID:-<empty>}" >&2
exit 1
fi
if [[ -n "${SORTED_WU_ID_SEEN[$WU_ID]+x}" ]]; then
echo "ERROR: sorted WU $wu_idx has duplicate stable ID: $WU_ID" >&2
exit 1
fi
SORTED_WU_ID_SEEN["$WU_ID"]=1
done
for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
(
WU="${SORTED_WORK_UNITS[$wu_idx]}"
WU_ID="$(extract_wu_id "$WU")"
DEDUP="${WU_DEDUP[$wu_idx]}" # "comment:NN" or empty
# Each WU contributes: $WU_ID, $WU_TITLE, $WU_BODY, $WU_COMMENT_BODY,
# $WU_PRIORITY_NUM, $WU_PRIORITY_LABEL, $WU_TYPE_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 [[ "$WU_PRIORITY_NUM" != "1" && "$WU_PRIORITY_NUM" != "2" ]]; then
KIND="invalid-priority"
FAIL_MSG="$WU_TITLE — invalid retro priority P${WU_PRIORITY_NUM:-<empty>}; only P1/P2 may be filed. Move this WU to Skip or Drop instead."
URL=""
elif ! 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:}"
# A recurrence may target a legacy issue that predates real type labels.
# Normalize the two actionable type labels before adding evidence so the
# existing issue still satisfies the one-type taxonomy invariant.
if ! gh issue edit "$ISSUE_NUM" \
--repo "$REPO" \
--remove-label bug \
--remove-label enhancement \
--add-label "$WU_TYPE_LABEL" >/dev/null 2>&1; then
KIND="type-normalization-failed"
FAIL_MSG="$WU_TITLE — could not normalize #$ISSUE_NUM to exactly one type label ($WU_TYPE_LABEL); comment not posted."
URL=""
elif 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_PROVENANCE_LABEL" \
--label "$WU_TYPE_LABEL" \
--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' "$WU_ID"
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 WU_ID_TMP
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=""
if [ -f "$ISSUE_TMPDIR/issue-$wu_idx" ]; then
{
IFS= read -r _
IFS= read -r KIND_TMP
} < "$ISSUE_TMPDIR/issue-$wu_idx"
fi
[[ "$KIND_TMP" == scrub-failed || "$KIND_TMP" == invalid-priority ]] || 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 WU_ID
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"
EXPECTED_WU_ID="$(extract_wu_id "${SORTED_WORK_UNITS[$wu_idx]}")"
if [[ "$WU_ID" != "$EXPECTED_WU_ID" ]]; then
FAILED_ISSUES+=("sorted WU $wu_idx returned stable ID ${WU_ID:-<empty>}, expected ${EXPECTED_WU_ID:-<empty>}")
continue
fi
OUTCOME_KIND+=("$KIND")
OUTCOME_URL+=("$URL")
OUTCOME_TITLE+=("$TITLE")
OUTCOME_PRIORITY+=("$PRIORITY")
OUTCOME_COMP+=("$COMP")
OUTCOME_COMPLEXITY+=("$COMPLEXITY")
if [[ "$URL" =~ /issues/([0-9]+) ]]; then
OUTCOME_ISSUE_NUM_BY_WU_ID["$WU_ID"]="${BASH_REMATCH[1]}"
elif [[ "${WU_DEDUP[$wu_idx]}" == comment:* ]]; then
OUTCOME_ISSUE_NUM_BY_WU_ID["$WU_ID"]="${WU_DEDUP[$wu_idx]#comment:}"
else
OUTCOME_ISSUE_NUM_BY_WU_ID["$WU_ID"]=""
fi
case "$KIND" in
created|commented)
echo "${KIND^}: $URL"
;;
create-failed|comment-failed|type-normalization-failed|invalid-priority|scrub-failed)
echo "WARNING: $FAIL_MSG"
FAILED_ISSUES+=("$FAIL_MSG")
;;
esac
done
# Apply only explicitly declared prerequisite edges, after all creates and
# dedup decisions have produced issue numbers. Each edge is
# `WU-2|wu:WU-1` or `WU-2|issue:123`. IDs are stable WU-N values, so this
# remains correct when a P1 WU-2 sorts before a P2 WU-1. Resolve both endpoints
# through the associative outcome map; never use sorted array positions.
# Related-area references never enter this array; they remain prose links.
for edge in "${WU_DEPENDENCY_EDGES[@]}"; do
IFS='|' read -r dependent_id prerequisite_ref <<< "$edge"
if [[ ! "$dependent_id" =~ ^WU-[1-9][0-9]*$ ]]; then
FAILED_ISSUES+=("dependency edge $edge — unknown or malformed dependent stable WU ID")
continue
fi
if [[ -z "${SORTED_WU_ID_SEEN[$dependent_id]+x}" ]]; then
FAILED_ISSUES+=("dependency edge $edge — unknown dependent stable WU ID")
continue
fi
dependent_issue="${OUTCOME_ISSUE_NUM_BY_WU_ID[$dependent_id]-}"
case "$prerequisite_ref" in
wu:*)
prerequisite_id="${prerequisite_ref#wu:}"
if [[ ! "$prerequisite_id" =~ ^WU-[1-9][0-9]*$ ]]; then
FAILED_ISSUES+=("dependency edge $edge — unknown or malformed prerequisite stable WU ID")
continue
fi
if [[ -z "${SORTED_WU_ID_SEEN[$prerequisite_id]+x}" ]]; then
FAILED_ISSUES+=("dependency edge $edge — unknown prerequisite stable WU ID")
continue
fi
prerequisite_issue="${OUTCOME_ISSUE_NUM_BY_WU_ID[$prerequisite_id]-}"
;;
issue:*)
prerequisite_issue="${prerequisite_ref#issue:}"
if [[ ! "$prerequisite_issue" =~ ^[1-9][0-9]*$ ]]; then
FAILED_ISSUES+=("dependency edge $edge — malformed existing issue number")
continue
fi
;;
*)
prerequisite_issue=""
;;
esac
if [[ -z "$dependent_issue" || -z "$prerequisite_issue" ]]; then
FAILED_ISSUES+=("dependency edge $edge — issue number unavailable; native relationship not applied")
elif ! gh issue edit "$dependent_issue" --repo "$REPO" \
--add-blocked-by "$prerequisite_issue" >/dev/null 2>&1; then
FAILED_ISSUES+=("dependency edge #$dependent_issue blocked by #$prerequisite_issue — native relationship failed")
fi
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
fi
```
Failure 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 |
| `type-normalization-failed` | Existing matched issue could not be normalized to exactly one real type label | `$FAILED_ISSUES` summary; manual label repair instructions |
| `invalid-priority` | A work unit still carried a non-P1/P2 priority | `$FAILED_ISSUES` summary; agent must return to Phase 4 and move the WU to Skip or Drop |
| `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/` |
| `$RETRO_PROVENANCE_LABEL` | This file Step 1 | Available write marker: canonical `source:retro`, or legacy `retro` only during cutover fallback |
| `$WORK_UNITS` | SKILL.md Phase 5.5 | Array of WU records |
| `$SORTED_WORK_UNITS` | This file Step 2 | Complete `$WORK_UNITS` records sorted P1 → P2; each record retains its own stable WU-N identifier |
| `$EXISTING_OPEN_RETROS` | This file Step 2.5 | De-duplicated JSON of open issues carrying `source:retro` or legacy `retro` |
| `$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) |
| `$WU_DEPENDENCY_EDGES` | SKILL.md Phase 5.5 + this file Step 3 | Explicit stable-ID prerequisite edges such as `WU-2|wu:WU-1` applied with native `--add-blocked-by`; related-area references are excluded |
| `$OUTCOME_ISSUE_NUM_BY_WU_ID` | This file Step 3 | Issue numbers keyed by stable WU-N, independent of sorted array positions |
| `$WU_TYPE_LABEL` | Each sorted WU record | Exactly one mapped real issue type: `bug` or `enhancement` |
| 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` / `type-normalization-failed` / `invalid-priority` / `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:
```bash
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
fi
```
If `GH_AVAILABLE=false`, or if every per-WU action failed, fall back to
printing manual filing instructions:
```bash
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_PROVENANCE_LABEL, bug or enhancement, priority:P1 or priority:P2, comp:<slug>. Use native --add-blocked-by only for explicit prerequisites; keep related-area references in prose."
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
fi
```
Per-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.
references/run-resolution.md
# Run resolution
Resolve the API, run, and CLI directory before entering any phase. The rest of
the skill reads `$API_NAME`, `$RUN_ID`, `$RUN_DIR`, `$CLI_DIR`, `$IN_REPO`, and
`$RETRO_SCRATCH_DIR`.
## Setup
<!-- RETRO_SETUP_START -->
```bash
# 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 -->
## Nothing to retro
```bash
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
fi
```
## Resolve which API
If the user passed an API name as an argument, use that. Validate for path traversal:
```bash
# 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 ;;
esac
```
If no API name was provided and multiple APIs exist, list them with their most recent
run dates and ask the user to choose:
```bash
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)"
done
```
Use `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:
```bash
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
```bash
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=""
fi
```
references/secret-scrubbing.md
# 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.
```bash
# 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,}'
# Opaque ghs_ plus a three-segment JWT form. Do not fold into
# [A-Za-z0-9._-]{36,}: that class swallows a following period.
'github-server-jwt|ghs_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
'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:**
```bash
# 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`:**
```bash
# 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.md
```
Same 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_a1b2c3d4e5f6...bcdef0123456` | `<REDACTED:linear-api-key:lin_a...0456:48ch>` |
| `22eb323ac258...ade77b9c78-us6` | `<REDACTED:mailchimp-api-key:22eb...-us6:36ch>` |
| `ghp_abc123def456...pqr678stu90` | `<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 (`+`, `/`, `.`, `=`).
```bash
# 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
fi
```
## Layer 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.
```bash
# 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
done
```
### Jurisdiction-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.
```bash
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
done
```
Pattern notes:
- **Codice Fiscale** (Italian tax code) is a 16-character `LLLLLLDDLDDLDDDL` shape 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-DDDD` dashed 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-*.json` files 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:
```bash
# 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
done
```
## Layer 3: HAR auth stripping
If the staging manuscripts contain HAR files, strip auth-bearing fields:
```bash
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")"
done
```
## Layer 4: Session state cleanup
Remove any session state files that may contain cookies or tokens:
```bash
find "$STAGING_MANUSCRIPTS" -name "session-state.json" -type f -delete 2>/dev/null
```
## Post-scrub verification
After all layers complete, do a final scan for obvious leaks:
```bash
FINAL_CHECK=false
# CRED_REGEX must mirror the vendor-prefix patterns in Layer 0 and Layer 2
# above; update both together so the verification step does not silently stop
# checking a credential shape the scrub loop still redacts. The JWT shape
# intentionally matches Layer 2's two-segment form; that also catches the
# first two segments of a conventional three-segment JWT.
CRED_REGEX='(sk_live_[A-Za-z0-9]{20,}|sk_test_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,}|ghs_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|ghs_[A-Za-z0-9]{36,}|xoxb-[A-Za-z0-9-]{20,}|xoxp-[A-Za-z0-9-]{20,}|\bAKIA[0-9A-Z]{16}\b|sk-or-v1-[A-Za-z0-9_-]{24,}|sk-ant-api03-[A-Za-z0-9_-]{40,}|\blin_api_[A-Za-z0-9_-]{32,}|\b[a-f0-9]{32}-us[0-9]{1,2}\b|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|Bearer [A-Za-z0-9._~+/=-]{20,})'
# 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)'
SCAN_FAILED=false
for dir in "$STAGING_MANUSCRIPTS" "$STAGING_CLI_SOURCE"; do
[ -d "$dir" ] || continue
if CRED_RAW=$(grep -rEi "$CRED_REGEX" "$dir" 2>/dev/null); then
:
else
CRED_STATUS=$?
if [ "$CRED_STATUS" -gt 1 ]; then
echo "WARNING: Credential scan could not read all files under $dir."
SCAN_FAILED=true
fi
fi
if PII_RAW=$(grep -rEi "$PII_REGEX" "$dir" 2>/dev/null); then
:
else
PII_STATUS=$?
if [ "$PII_STATUS" -gt 1 ]; then
echo "WARNING: PII scan could not read all files under $dir."
SCAN_FAILED=true
fi
fi
CRED_MATCHES=$(printf '%s\n' "$CRED_RAW" | grep -v 'REDACTED' | head -5)
PII_MATCHES=$(printf '%s\n' "$PII_RAW" | 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 [ "$SCAN_FAILED" = true ]; then
FINAL_CHECK=true
echo "WARNING: One or more post-scrub scans did not complete successfully."
echo "Artifacts will NOT be uploaded until the scan errors are resolved."
fi
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."
fi
```
SKILL.md
---
name: printing-press-retro
description: >
Use when the user asks to retro, run a retrospective, file findings,
or improve the Printing Press after a printed-CLI run. Trigger phrases:
"retro", "retrospective", "what went wrong", "improve the press",
"post-mortem", "lessons learned", "what can we improve", "file a retro",
"submit findings". Do not use for printed-CLI polish or a single-CLI fix.
version: 0.1.0
allowed-tools:
- Bash
- Read
- Glob
- Grep
- Write
- Agent
- AskUserQuestion
created_by: user
---
# /printing-press-retro
**Result:** A scrubbed retro document and, when the issue gate passes, GitHub
issues for Printing Press defects only.
**Next consumer:** A maintainer or agent who will change the Printing Press
(generator, scorer, skills, or binary) — not the printed CLI that just shipped.
**Done:** The manuscript proof exists and is scrubbed; every work unit is
file-new, comment-on-existing, or local-only; no unredacted secret or PII
remains; the user has the outcome list.
**Intent:** "Raised the floor" is analysis language, not a filing bar. File
only when the Printing Press must change to stop a current, reproducible,
generalizing P1 or P2 defect. Manual iteration on one CLI is expected work,
not a finding. In user-facing output say "the Printing Press"; name the
subsystem when pointing at a fix.
## Boundaries
- **Redact** every real secret and PII before quoting. Issue bodies and retro
docs are public. Use [references/secret-scrubbing.md](references/secret-scrubbing.md)
Layer 0. Never quote a leaked value as evidence of the leak.
- **Don't change the machine** by default. The burden of proof is on the finding.
- **No P3.** File only P1/P2. Anything else is Skip or Drop, not a low-priority
issue. The issue gate files P1/P2 Do work units only.
- **Never upload unscrubbed artifacts.** Never modify manuscript or library
source trees; scrub copies only.
## Authority
Invocation authorizes reading manuscripts/library and writing the retro proof
plus scratch copies. It does not authorize GitHub filing or public upload until
the user confirms Submit. It does not authorize edits to the printed CLI or to
Printing Press source.
## Steps
Read [references/run-resolution.md](references/run-resolution.md) first. It
resolves `$API_NAME`, `$RUN_ID`, `$RUN_DIR`, `$CLI_DIR`, and `$IN_REPO`.
**Never execute a phase from memory. When you enter a phase, Read its file from phases/ first.**
| Step | File |
|---:|---|
| 1 | [phases/01-gather-evidence.md](phases/01-gather-evidence.md) |
| 2 | [phases/02-mine-the-session.md](phases/02-mine-the-session.md) |
| 3 | [phases/03-triage-candidates.md](phases/03-triage-candidates.md) |
| 4 | [phases/04-classify-findings.md](phases/04-classify-findings.md) |
| 5 | [phases/05-prioritize.md](phases/05-prioritize.md) |
| 6 | [phases/06-write-the-retro.md](phases/06-write-the-retro.md) |
| 7 | [phases/07-plannable-work-units.md](phases/07-plannable-work-units.md) |
| 8 | [phases/08-issue-gate.md](phases/08-issue-gate.md) |
| 9 | [phases/09-package-upload-present.md](phases/09-package-upload-present.md) |
Follow each file's final `Next:` pointer. Late procedure lives in the phase
file or a role-named reference it loads
([references/artifact-packaging.md](references/artifact-packaging.md),
[references/issue-template.md](references/issue-template.md),
[references/secret-scrubbing.md](references/secret-scrubbing.md)). Do not
restate those files here.