[object Object]

← back to Cli Printing Press

feat(skills): flatten retro issues and dedup against open issues (#641)

7eb951e07f994c44846b5a7119b69527274201c9 · 2026-05-05 21:07:31 -0700 · Trevin Chow

Replace the parent + sub-issue mode in printing-press-retro with one flat
top-level issue per work unit. Drops the retro-parent label, the parent
body template, the sub-issues REST linking, and the auto-cross-references
between sibling WUs that were doubling the issue list and orphaning
parents whose children had all been closed.

Add a dedup scan (Phase 6 Step 2.5): one gh issue list call against open
retro-tagged issues, agent reasons over titles per WU and classifies as
same (comment instead of file), related-area (file new + reference via
#N), or unrelated. Decisions surface in the confirm summary so the user
sees the filing plan before anything happens.

Restructure the issue body for self-contained debugging: succinct
problem-stated title with no priority or WU prefix, summary + where to
look + observed evidence + suspected root cause + suggested direction,
with explicit guidance to mark uncertainty rather than manufacture
prescriptions when the cause or fix isn't clear.

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

Files touched

Diff

commit 7eb951e07f994c44846b5a7119b69527274201c9
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 5 21:07:31 2026 -0700

    feat(skills): flatten retro issues and dedup against open issues (#641)
    
    Replace the parent + sub-issue mode in printing-press-retro with one flat
    top-level issue per work unit. Drops the retro-parent label, the parent
    body template, the sub-issues REST linking, and the auto-cross-references
    between sibling WUs that were doubling the issue list and orphaning
    parents whose children had all been closed.
    
    Add a dedup scan (Phase 6 Step 2.5): one gh issue list call against open
    retro-tagged issues, agent reasons over titles per WU and classifies as
    same (comment instead of file), related-area (file new + reference via
    #N), or unrelated. Decisions surface in the confirm summary so the user
    sees the filing plan before anything happens.
    
    Restructure the issue body for self-contained debugging: succinct
    problem-stated title with no priority or WU prefix, summary + where to
    look + observed evidence + suspected root cause + suggested direction,
    with explicit guidance to mark uncertainty rather than manufacture
    prescriptions when the cause or fix isn't clear.
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 skills/printing-press-retro/SKILL.md               | 241 +++----
 .../references/issue-template.md                   | 723 +++++++++------------
 2 files changed, 432 insertions(+), 532 deletions(-)

diff --git a/skills/printing-press-retro/SKILL.md b/skills/printing-press-retro/SKILL.md
index 79b020f9..190e061b 100644
--- a/skills/printing-press-retro/SKILL.md
+++ b/skills/printing-press-retro/SKILL.md
@@ -416,8 +416,8 @@ the Do/Skip tables, they go on the dropped-candidates list with the reason.
 **4. Where in the Printing Press does this originate?**
 
 Pick exactly one component. The `slug` column drives the `comp:<slug>` label
-applied to the WU sub-issue when filed (Phase 6), which is how agents filter
-related WUs across retros (`gh issue list --label comp:<slug>`).
+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 |
 |-----------|------|------|
@@ -476,9 +476,10 @@ retro file's frontmatter contains one), and a one-word classification:
   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")
-and into the issue body when the finding becomes a sub-issue. GitHub auto-cross-
-links any `#N` issue number you write, so contradictions and alignments will show
-up in both retro timelines without further action.
+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.
@@ -518,6 +519,13 @@ 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
@@ -671,7 +679,7 @@ For each "Do" finding or group of related findings:
   absorbed finding is P1, else P2 if any is P2, else P3)*
 - **Component:** generator / openapi-parser / spec-parser / scorer / skill / catalog
   *(must match one of the six fixed component slugs; drives the `comp:*` label
-  applied to the sub-issue when filed)*
+  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:**
@@ -700,26 +708,28 @@ they pick up the work.
 
 ## Phase 5.6: Issue gate — are there Printing Press improvements?
 
-After prioritization and work units are written, decide whether a GitHub issue is
-warranted. The purpose of the issue is to give someone (human or agent) something to
-fix in the Printing Press. If every finding is specific to this one printed CLI with
-nothing to change in the Printing Press, the issue is noise — there's nothing to act on.
+After prioritization and work units are written, decide whether GitHub issues are
+warranted. Each WU becomes one flat top-level issue (no parent, no sub-issue
+hierarchy). The purpose of filing is to give someone (human or agent) something
+to fix in the Printing Press. If every finding is specific to this one printed
+CLI with nothing to change in the Printing Press, filing is noise — there's
+nothing to act on.
 
-**Skip the GitHub issue if:**
+**Skip filing if:**
 - Every finding landed in "Skip"
 - All findings are printed-CLI-specific (manual edits that only apply to this one API
   and wouldn't recur across other CLIs)
 - The "Do" table is empty
 
-**Create the GitHub issue if:**
+**File issues (one per WU) if:**
 - There is at least one "Do" finding — i.e., something a maintainer or agent could
   act on in the Printing Press (templates, binary, skills, or scoring tools)
 
 Use judgment. A retro that found three things but all three are "this API has a weird
-auth scheme no other API uses" is not worth an issue. A retro that found one small
-template gap that would help every future CLI *is* worth an issue.
+auth scheme no other API uses" is not worth filing. A retro that found one small
+template gap that would help every future CLI *is* worth filing.
 
-If the issue is skipped, still save the retro locally (manuscript proofs +
+If filing is skipped, still save the retro locally (manuscript proofs +
 `/tmp/printing-press/retro/`), present the findings to the user, then jump
 directly to Phase 6 Step 6 (present results — adjusted to show local-only paths)
 and Step 8 (offer next steps).
@@ -735,53 +745,53 @@ 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: Confirm before publishing
+### 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 uploading anything, show the user a friendly summary and ask for confirmation
-via `AskUserQuestion`. Tailor the wording to whether this is single-issue or
-parent-with-sub-issues mode (see `references/issue-template.md` Step 2 for the
-`WU_COUNT` dispatch).
+Before showing the confirm prompt, run `references/issue-template.md`
+**Steps 1, 2, and 2.5** to ensure labels exist, sort the work units, and
+compute the per-WU filing plan via the dedup scan against open
+retro-tagged issues. Each WU ends up classified as either:
+
+- **File new** — no matching open issue
+- **Comment on #N** — Step 2.5 found a `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
 
-For **2+ WUs** (parent-with-sub-issues mode):
+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):
 >
-> - **1 parent issue** with retro context (session stats, summary, WU table, artifact links)
-> - **<M> sub-issues** — one per work unit, each with its own priority, component, and finding details, so each fix can be tracked, assigned, and closed independently
-> - Labels: `retro`, `retro-parent` (parent only), `priority:P1`/`priority:P2`/`priority:P3` and `comp:*` (sub-issues), so future agents can filter all retro WUs by area or priority
-> - Scrubbed artifact zips uploaded to catbox.moe and linked from the parent:
->   - **Manuscripts** (<size>) — research brief, shipcheck proof, build logs
->   - **CLI source** (<size>) — the generated Go code (no binary, no vendor/) *(omit if not available)*
->
-> **Top findings:**
-> - <1-3 sentence summary of the highest-priority Do Now items>
+> **Filing plan:**
 >
-> Everything is staged at `<$STAGING_DIR>` if you'd like to inspect the files first.
-
-For **1 WU** (single-issue mode):
-
-> **Ready to submit your retro.**
+> | # | Title | Plan | Notes |
+> |---|-------|------|-------|
+> | 1 | <wu-1 title> | File new (P1, comp:<slug>) | No match |
+> | 2 | <wu-2 title> | Comment on #234 | Matches "<existing title>" |
+> | 3 | <wu-3 title> | File new + reference #189 | Adjacent open issue |
 >
-> Here's what will happen on [mvanhorn/cli-printing-press](https://github.com/mvanhorn/cli-printing-press):
+> Each new issue carries `retro`, `priority:P<n>`, `comp:<slug>` labels —
+> agents filter related work across retros with `gh issue list --label
+> comp:<slug>` or `gh issue list --label priority:P1`.
 >
-> - **1 GitHub issue** with the retro context, work unit, and absorbed findings inline
-> - Labels: `retro`, `priority:P<n>`, `comp:<slug>`, so future agents can filter related work across retros
-> - Scrubbed artifact zips uploaded to catbox.moe and linked from the issue:
+> 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 *(omit if not available)*
->
-> **Top finding:** <one-sentence summary>
+>   - **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** — upload artifacts and create the issue
+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 the issue, keep the manuscript proof and temp copy
+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.
@@ -790,46 +800,53 @@ If the user picks "Save locally only," skip Steps 3 and 4 — the retro is alrea
 saved to manuscript proofs and `/tmp/printing-press/retro/`. Clean up the staging
 folder, then jump to Step 6.
 
+If the user wants to override a dedup decision before submitting (e.g.,
+"file new for WU-2 instead of commenting"), accept the override: clear
+`WU_DEDUP[i]` for that WU and proceed.
+
 ### Step 3: Upload artifacts
 
 Run artifact-packaging.md Step 5 (the catbox upload) using the zips already in
 `$STAGING_DIR`. This produces `$MANUSCRIPTS_URL` and `$CLI_SOURCE_URL`.
 
-### Step 4: Create GitHub issue(s)
-
-Read and apply [references/issue-template.md](references/issue-template.md).
-The "Execution principles" block at the top of that file is mandatory: build
-issue bodies inline (heredocs into shell variables, not the Write tool), run
-the whole step in one Bash invocation, use `gh api --method POST` for
-sub-issue creation, and parallelize sub-issue work. Skipping these costs
-real wall-clock latency — a 2 WU retro should finish issue creation in a
-single round trip's worth of network time, not a serialized stack of them.
-
-The reference covers:
-
-1. **Step 1: Ensure labels exist** — fast-path probe (single `gh label list`)
-   followed by create-only fallbacks for the 11 canonical labels (6 `comp:*`
-   + 3 `priority:P*` + `retro` + `retro-parent`). On a repo where prior retros
-   already provisioned the set, the create loop is skipped entirely.
-2. **Step 2: Dispatch on WU count** — `single` mode (1 WU, inline body) or
-   `parent-with-subs` mode (2+ WUs, parent issue + one sub-issue per WU linked
-   via the GitHub sub-issues REST API).
-3. The body templates (action-shaped per-finding subset; the full triage
-   rationale lives in the retro doc linked from the parent's Artifacts) and
-   the parallel sub-issue creation block.
-
-Each sub-issue carries its own `priority:P<n>` priority label and `comp:<slug>` component
-label. This is what enables `gh issue list --label comp:openapi-parser` to surface
-every retro WU in that area across every retro — the cross-retro discovery that
-single-issue retros couldn't support.
-
-Each WU sub-issue body also includes the **Related prior retros** block populated
-in Phase 3 Step D, so contradictions and alignments with earlier retros are
-visible and auto-cross-linked in GitHub timelines.
-
-If `gh` is not authenticated, the sub-issues REST endpoint is unavailable, or any
-issue creation fails, follow the graceful degradation path in the issue-template
-reference: save locally and print manual filing instructions.
+### 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, WUs are
+sorted, and `$WU_DEDUP` and `$WU_RELATED` are populated. This step runs
+**Step 3** of the reference: build bodies and execute the plan in parallel.
+
+The "Execution principles" block at the top of `issue-template.md` is
+mandatory: build issue bodies inline (heredocs into shell variables, not
+the Write tool), run the whole step in one Bash invocation, and parallelize
+the per-WU `gh issue create` / `gh issue comment` calls. Skipping these
+costs real wall-clock latency — an N WU retro should finish in a single
+round trip's worth of network time, not a serialized stack of them.
+
+Each WU is independent: WUs marked `comment:#N` get a comment on the
+existing issue; WUs marked file-new create a new flat top-level issue. No
+parent, no sub-issue REST linking — every new issue stands alone in
+GitHub's issue list with its own open/close lifecycle.
+
+Each new issue carries its own `priority:P<n>` and `comp:<slug>` labels.
+This is what enables `gh issue list --label comp:openapi-parser` to surface
+every retro WU in that area across every retro — labels are the cross-retro
+discovery surface, not auto-cross-links inside issue bodies.
+
+Each new issue body's **Related issues** block combines:
+
+- Prior-retro references from Phase 3 Step D (alignments, contradictions, extensions across retros)
+- `related-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
+(captured as free-text `Dependencies:` instead).
+
+If `gh` is not authenticated or every per-WU action fails, follow the
+graceful degradation path in the issue-template reference: save locally and
+print manual filing instructions. Per-WU partial failures (some succeed,
+some don't) are surfaced through `$FAILED_ISSUES` in Step 6.
 
 ### Step 5: Local scratch copy
 
@@ -845,50 +862,52 @@ fi
 
 ### Step 6: Present results
 
-After the issue(s) are created, show the user a summary tailored to `$ISSUE_MODE`.
-
-For **`parent-with-subs`** (`WU_COUNT >= 2`):
+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!**
 >
-> Parent issue: <full $PARENT_URL>
-> Sub-issues (<M> WUs, P1 → P3 order):
->   - [P1] WU-1: <title> — <full WU-1 URL>
->   - [P1] WU-2: <title> — <full WU-2 URL>
->   - [P2] WU-3: <title> — <full WU-3 URL>
+> Filed <C> new issue<s>, added <E> comment<s> on existing issues (P1 → P3 order):
+>
+> *New issues:*
+>   - [P1] <title> — <full $OUTCOME_URL[i]>
+>   - [P2] <title> — <full $OUTCOME_URL[i]>
 >   - ...
 >
-> Found <N> findings across <M> work units. Sub-issues are tagged with
-> `comp:<slug>` and `priority:P<n>` labels — agents can filter related WUs
-> across retros with `gh issue list --label comp:<slug>` or
-> `gh issue list --label priority:P1`.
-> *(if artifacts uploaded)* Artifacts: [manuscripts](<URL>) · [CLI source](<URL>)
+> *Comments on existing issues:*
+>   - [P1] <title> → comment on #234 — <comment URL>
+>   - ...
+>
+> <N> findings across <M> work units. New issues are tagged with `comp:<slug>`
+> and `priority:P<n>` labels — agents can filter related work across retros
+> with `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>
 
-If `$FAILED_WUS` is non-empty (set by `references/issue-template.md` Step P3),
-append a warning block before the closing line:
+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.
 
-> ⚠️ Some WUs need attention:
->   - WU-2 (...) — issue creation failed
->   - WU-4 (#1234) — sub-issue link failed (cross-link in body remains)
->
-> The parent issue's "Work Units" table marks failed creations as
-> "**FAILED — file manually**" so the gap is visible. Re-run the skill or file
-> the missing sub-issue(s) manually using the retro doc at <$RETRO_SCRATCH_PATH>.
+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.
 
-For **`single`** (`WU_COUNT == 1`):
+If `$FAILED_ISSUES` is non-empty (set by `references/issue-template.md`
+Step 3), append a warning block before the closing line:
 
-> **Retro submitted!**
->
-> Issue: <full $ISSUE_URL>
+> ⚠️ Some actions need attention:
+>   - <title> — issue creation failed
+>   - <title> — comment on #234 failed
+>   - ...
 >
-> 1 work unit (P<n>, comp:<slug>) absorbing <N> findings.
-> *(if artifacts uploaded)* Artifacts: [manuscripts](<URL>) · [CLI source](<URL>)
-> Local copy: <$RETRO_SCRATCH_PATH>
+> File the missing issue(s) or comment(s) manually using the retro doc at <$RETRO_SCRATCH_PATH>.
 
-If issue creation wasn't completed (user chose local-only, or gh failed), show
-the local save paths and the manual filing instructions printed by the
-issue-template fallback 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
 
diff --git a/skills/printing-press-retro/references/issue-template.md b/skills/printing-press-retro/references/issue-template.md
index 1918611e..5640b6db 100644
--- a/skills/printing-press-retro/references/issue-template.md
+++ b/skills/printing-press-retro/references/issue-template.md
@@ -1,53 +1,58 @@
 # GitHub Issue Template
 
-Read this file during Phase 6 when creating GitHub issue(s) from retro findings.
-
-The shape depends on the work-unit count:
-
-- **0 WUs** — Phase 5.6 already gated this out; no issue gets filed.
-- **1 WU** — single issue; the WU is inlined in the issue body, no parent/child
-  hierarchy. Labels are still applied so cross-retro filtering still works.
-- **2+ WUs** — **parent issue** (retro context, summary tables, artifact links) +
-  **one sub-issue per WU** (full WU details + absorbed findings + prior-retro
-  links). Sub-issues are linked to the parent via the GitHub sub-issues REST
-  API. This is the structural fix for "monster issues that are impossible to
-  track" — each WU gets its own open/closed/assignee state.
+Read this file during Phase 6 when filing GitHub issues from retro findings.
+
+Each work unit becomes one flat top-level issue. There is no parent, no
+sub-issue hierarchy. The "Findings absorbed" section inside each issue
+groups the 1+ findings that share a single fix.
+
+This shape was chosen deliberately:
+
+- A retro of N work units produces N issues, each independently trackable —
+  open, close, assign, comment.
+- The default GitHub issue list is not duplicated; there is no parent to
+  mirror children, no progress bar to drift, no parent-not-auto-closing
+  bookkeeping.
+- Cross-retro discovery still works through labels: `comp:<slug>` surfaces
+  every retro WU touching one component; `priority:P1` surfaces high-priority
+  work across retros.
+- Inter-issue links inside the same retro are not auto-generated. They appear
+  only when an issue genuinely relates to another (a contradicting prior
+  retro, or a `related-area` open issue surfaced by the dedup scan).
 
 ## Formatting rules
 
-**Never use `#N` notation** for finding or work unit numbers in *summary* text where
-GitHub auto-linking would be confusing. Use the `F` prefix for findings (`F1`, `F3`)
-and `WU-` for work units. **Real GitHub issue references in the `Related prior retros`
-block and the parent's "Sub-issues" backfill table are intentional `#N`** — that's
-where we *want* GitHub to auto-link the timelines together.
+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; the issue
-body is an action surface. Optimize the issue path for speed and signal:
-
-- **Issue body is a slim subset.** Per-finding fields in the issue templates
-  below are intentionally fewer than in the retro doc. Don't re-introduce the
-  triage-rationale fields (`Scorer correct?`, `Cross-API check`, `Worth a
-  fix?`, `Inherent or fixable?`, per-finding `Test`) — they live in the retro
-  doc, which the parent issue links via Artifacts. A maintainer reading the
-  issue wants the action; the audit trail is one click away.
-- **Generate bodies inline, never via the Write tool.** Use shell heredocs into
-  variables in a single `Bash` invocation. Writing each body to a file and
-  passing `--body-file` adds a tool round-trip per issue and is the single
-  largest source of perceived latency the skill historically had.
-- **One Bash call where the work allows it.** Phase 6 Step 4 should typically be
-  a single Bash invocation that defines all bodies via heredocs, creates the
-  parent, creates and links sub-issues in parallel, and edits the parent's
-  WU-table backfill — surface the produced URLs at the end.
-- **Use `gh api --method POST /repos/.../issues` instead of `gh issue create`
-  for sub-issues.** The REST endpoint returns `id`, `number`, and `html_url` in
-  one response, so the separate `gh api repos/.../issues/N` lookup for the
-  integer DB id is no longer needed (the sub-issues link API requires that DB
-  id, not the issue number).
-- **Run sub-issue creates in parallel.** Each WU is independent of every other
-  WU once the parent number is known. Background subshells writing to indexed
-  temp files, then `wait`, then read in order — the pattern is in Step P3.
+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.
+- **Generate bodies inline, never via the Write tool.** Use shell heredocs
+  into variables in a single `Bash` invocation. Writing each body to a file
+  and passing `--body-file` adds a tool round-trip per issue and is the
+  single largest source of perceived latency the skill historically had.
+- **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.
 
 ## Step 1: Ensure labels exist (idempotent, create-only)
 
@@ -60,8 +65,8 @@ clobber them).
 ```bash
 REPO="mvanhorn/cli-printing-press"
 
-# Fast-path: list existing labels once. If all 11 canonical labels are already
-# present, skip the create loop entirely — saves up to 11 gh API calls per
+# 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
@@ -69,7 +74,7 @@ for required in \
   "comp:generator" "comp:openapi-parser" "comp:spec-parser" \
   "comp:scorer" "comp:skill" "comp:catalog" \
   "priority:P1" "priority:P2" "priority:P3" \
-  "retro" "retro-parent"; do
+  "retro"; do
   if ! printf '%s\n' "$EXISTING_LABELS" | grep -qFx "$required"; then
     NEED_CREATE=true
     break
@@ -79,9 +84,6 @@ done
 if [ "$NEED_CREATE" = true ]; then
   ensure_label() {
     local name="$1" color="$2" desc="$3"
-    # Create only if missing; failure (label exists, no permissions, rate
-    # limit) is non-fatal — issue creation later will fail loudly if a
-    # required label is genuinely absent.
     gh label create "$name" --repo "$REPO" --color "$color" --description "$desc" 2>/dev/null || true
   }
 
@@ -93,500 +95,379 @@ if [ "$NEED_CREATE" = true ]; then
   ensure_label "comp:skill"          "5319e7" "skills/printing-press/SKILL.md and related skill instructions"
   ensure_label "comp:catalog"        "5319e7" "catalog/ entries"
 
-  # Priority labels (3) — duplicate the title prefix to enable label-based filtering.
+  # Priority labels (3) — drive priority-based filtering. The label is the
+  # primary carrier; titles do not duplicate the priority prefix.
   ensure_label "priority:P1" "b60205" "Retro priority P1 (high)"
   ensure_label "priority:P2" "d93f0b" "Retro priority P2 (medium)"
   ensure_label "priority:P3" "fbca04" "Retro priority P3 (low)"
 
-  # Marker labels
-  ensure_label "retro"        "0e8a16" "Issue produced by /printing-press-retro"
-  ensure_label "retro-parent" "0e8a16" "Parent retro issue with sub-issue WUs"
+  # Marker label
+  ensure_label "retro" "0e8a16" "Issue produced by /printing-press-retro"
 fi
 ```
 
-### Priority labels
+The `retro-parent` label is intentionally omitted — there are no parent
+issues. If the label exists from prior retros, leave it; the skill never
+creates new issues with it.
 
-Apply `priority:P<n>` matching the WU's internal priority. The label and the
-`[P<n>]` title prefix carry the same value — the label enables
-`gh issue list --label priority:P1` filtering across retros, the title prefix
-gives at-a-glance scanning in issue lists.
+## Step 2: Sort work units
 
-## Step 2: Resolve the work-unit count and dispatch
+Sort WUs by priority: P1 first, then P2, then P3. Within a priority bucket,
+keep the order they appeared in Phase 5.5 (typically by ascending WU number,
+but the skill may have intentionally ordered them by dependency — preserve
+that).
 
 ```bash
-WU_COUNT="${#WORK_UNITS[@]}"  # populated from Phase 5.5
-
-if [ "$WU_COUNT" -eq 0 ]; then
-  echo "Phase 5.6 should have gated this out. Aborting."
-  exit 1
-elif [ "$WU_COUNT" -eq 1 ]; then
-  ISSUE_MODE="single"
-else
-  ISSUE_MODE="parent-with-subs"
-fi
+# SORTED_WORK_UNITS is populated from $WORK_UNITS sorted P1 → P3.
 ```
 
-## Single-issue mode (`WU_COUNT == 1`)
+## Step 2.5: Dedup against open issues
 
-### Title
+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.
 
-```
-Retro: <api-display-name> — 1 work unit (P<n>)
-```
-
-Example: `Retro: Cal.com — 1 work unit (P1)`
-
-### Body
-
-```markdown
-## Session Stats
+This is a single `gh` call followed by per-WU agent reasoning over titles.
+**It does not need to be bulletproof** — false negatives (filing new when
+one exists) are recoverable; false positives (commenting on the wrong
+issue) are uglier. **Bias toward `file-new` when uncertain.**
 
-| Metric | Value |
-|--------|-------|
-| API | <api-display-name> |
-| Spec source | <catalog / browser-sniffed / docs / HAR> |
-| Scorecard | <score>/100 (<grade>) |
-| Verify pass rate | <X>% |
-| Fix loops | <N> |
-| Manual code edits | <N> |
-| Features built from scratch | <N> |
-| Triage | <K> candidates → 1 filed / <S> skipped / <X> dropped |
+### Fetch open retro issues
 
-## What the Printing Press Got Right
-
-- <pattern>
-- <pattern>
-
-## Work Unit
+```bash
+EXISTING_OPEN_RETROS=$(gh issue list \
+  --repo "$REPO" \
+  --label retro \
+  --state open \
+  --limit 200 \
+  --json number,title,url 2>/dev/null \
+  || echo "[]")
+```
 
-### WU-1: <title>
+A single call. No per-WU label filtering — the agent reasons over titles
+across the whole open-retro set so a related-area issue under a different
+component still surfaces.
 
-- **Priority:** P<n>
-- **Component:** <comp-slug>
-- **Complexity:** small / medium / large
-- **Goal:** <one sentence>
-- **Target:** <component and area>
-- **Acceptance criteria:**
-  - positive: ...
-  - negative: ...
-- **Scope boundary:** ...
-- **Dependencies:** <other WUs or "None">
+### Classify each WU against the candidate set
 
-### Findings absorbed
+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:
 
-> Triage rationale (Scorer correct?, Cross-API check, Worth a fix?, Inherent or
-> fixable?) and the long-form root-cause prose live in the retro doc linked
-> under Artifacts. The bullets below are the action surface.
+| 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 |
 
-#### F<n>: <title> (<category>)
+Per WU, fold the per-candidate verdicts into a single `$WU_DEDUP[i]` slot:
 
-- **What happened:** <one-sentence symptom>
-- **Frequency:** every / most / subclass:<name>
-- **Durable fix:** <prescription, with code snippet if it clarifies the change>
-- **Evidence:** <session moment that surfaced this — file, line, command, or proof artifact>
-- **Related prior retros:**
-  - #<num> (`<api-slug>` retro, `aligned`/`contradicts`/`extends`) — <one-sentence note>
-  - *(or "None" if Phase 3 Step D found no matches)*
+```
+$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.
+```
 
-*(Repeat for each absorbed finding. Per-finding positive/negative tests fold
-into the WU's Acceptance criteria above, prefixed `F<n>:` so each finding is
-verifiable.)*
+And capture related issues separately:
 
-## Skipped
+```
+$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.
+```
 
-| Finding | Title | Why it didn't make it (Step B / Step D / Step G) |
-|---------|-------|--------------------------------------------------|
-| F<n>    | ...   | ...                                              |
+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.
 
-*(Omit if no findings were skipped.)*
+## Step 3: Build bodies and execute (in parallel)
 
-## Artifacts
+For each WU, build either an issue body or a comment body, then run `gh`
+in a background subshell. `wait`, then collect.
 
-| Artifact | Link |
-|----------|------|
-| Retro document | <$RETRO_DOC_URL or "Upload failed — see below"> |
-| Manuscripts (research + proofs) | <$MANUSCRIPTS_URL or "Upload failed — see below"> |
-| Generated CLI source code | <$CLI_SOURCE_URL or "Upload failed — see below"> |
+### Issue title (for new issues)
 
----
+Succinct, problem-stated. Examples:
 
-*Generated by `/printing-press-retro` · [CLI Printing Press](https://github.com/mvanhorn/cli-printing-press)*
-```
+- ✅ `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)*
 
-### Create
+### Issue body (for new issues)
 
-```bash
-ISSUE_URL=$(gh issue create \
-  --repo "$REPO" \
-  --title "$ISSUE_TITLE" \
-  --body "$BODY" \
-  --label retro \
-  --label "priority:P${PRIORITY_NUM}" \
-  --label "comp:${COMPONENT_SLUG}")
-```
+The body must be self-contained — an agent should be able to act on it
+without opening the manuscripts attachment. Use the shape below.
 
-## Parent-with-sub-issues mode (`WU_COUNT >= 2`)
+```markdown
+> Filed by `/printing-press-retro` from a run of **<api-display-name>**
+> (scorecard <score>/100, <X>% verify pass).
 
-### Step P1: Build the WU bodies first (in priority order)
+## Summary
 
-Sort WUs by priority: P1 first, then P2, then P3. Within a priority bucket, keep
-the order they appeared in Phase 5.5 (typically by ascending WU number, but the
-skill may have intentionally ordered them by dependency — preserve that).
+<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.>
 
-For each WU, build the sub-issue body:
+## Where to look
 
-```markdown
-**Parent retro:** #<PARENT_NUMBER>  *(backfilled after parent is created — see Step P3)*
+- **Component:** <comp-slug>
+- **Likely area:** <path or files in the printing-press repo, e.g. `internal/generator/templates/`>
+- **Triggered when:** <spec shape, API behavior, or runtime context that surfaces this>
 
-## Work Unit
+## What we observed
 
-- **Priority:** P<n>
-- **Component:** <comp-slug>
-- **Complexity:** small / medium / large
-- **Goal:** <one sentence>
-- **Target:** <component and area>
-- **Acceptance criteria:**
-  - positive: ...
-  - negative: ...
-- **Scope boundary:** ...
-- **Dependencies:** <other WU sub-issue numbers, backfilled if known, otherwise "None">
+<Concrete evidence usable to reproduce or locate the issue without opening
+attachments. This is the meat of the body — be specific.>
 
-## Findings absorbed
+- 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
 
-> Triage rationale (Scorer correct?, Cross-API check, Worth a fix?, Inherent or
-> fixable?) and the long-form root-cause prose live in the retro doc linked
-> from the parent issue under Artifacts. The bullets below are the action
-> surface.
+## Suspected root cause
 
-#### F<n>: <title> (<category>)
+<Hypothesis with explicit confidence level. Don't manufacture certainty.>
 
-- **What happened:** <one-sentence symptom>
-- **Frequency:** every / most / subclass:<name>
-- **Durable fix:** <prescription, with code snippet if it clarifies the change>
-- **Evidence:** <session moment — file, line, command, or proof artifact>
-- **Related prior retros:**
-  - #<num> (`<api-slug>` retro, `aligned`/`contradicts`/`extends`) — <one-sentence note>
-  - *(or "None")*
+- **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."
 
-*(Repeat for each absorbed finding. Per-finding positive/negative tests fold
-into the WU's Acceptance criteria above, prefixed `F<n>:` so each finding
-remains independently verifiable.)*
+## Suggested direction
 
----
+<Proposed fix direction with explicit confidence level. Offer alternatives
+when reasonable doubts exist; always include a verification step.>
 
-*Sub-issue of the [<api-display-name> retro](#<PARENT_NUMBER>) · Generated by `/printing-press-retro`*
-```
+- **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."
 
-Sub-issue title:
+## Acceptance criteria
 
-```
-[P<n>] WU-<m>: <title>
-```
+- positive: <test that proves the fix works on the API class this targets>
+- negative: <test that proves the fix doesn't regress unaffected APIs>
 
-Examples: `[P1] WU-1: Emit Stripe-Version header from spec`, `[P2] WU-2: Default
-auth scaffold for cookie+CSRF APIs`.
+## Frequency
 
-### Step P2: Create the parent issue
+every / most APIs / subclass:<name> / this API only
 
-Title:
+## Complexity
 
-```
-Retro: <api-display-name> — <N> findings, <M> work units
-```
+small / medium / large
 
-Body — note the explicit placeholder `<!-- WU_TABLE -->`. This is replaced in
-Step P5 once sub-issue URLs are known.
+## Scope boundary
 
-```markdown
-## Session Stats
-
-| Metric | Value |
-|--------|-------|
-| API | <api-display-name> |
-| Spec source | <catalog / browser-sniffed / docs / HAR> |
-| Scorecard | <score>/100 (<grade>) |
-| Verify pass rate | <X>% |
-| Fix loops | <N> |
-| Manual code edits | <N> |
-| Features built from scratch | <N> |
-| Triage | <K> candidates → <D> filed / <S> skipped / <X> dropped |
+<What this issue does NOT include. Helps prevent fix creep. Skip if not applicable.>
 
-## Summary
+## Dependencies
 
-<2-3 sentences that tell the maintainer what to do without making them open
-sub-issues: the headline pattern across findings (e.g., "framework command
-templates ship without `--json` or `Examples:` blocks"), the component(s)
-touched, and which WU is the highest-leverage fix. Skip if the WU table below
-already conveys this clearly — never restate it twice.>
+<Free text — only if there's a real prerequisite. Most issues say "None.">
 
-## What the Printing Press Got Right
+## Findings absorbed
 
-- <pattern>
-- <pattern>
+*Omit if 1:1 (the body above is the finding).*
 
-## Skipped
+#### F<n>: <title>
 
-| Finding | Title | Why it didn't make it |
-|---------|-------|------------------------|
-| F<n> | ... | Step B / Step D / Step G: ... |
+- <one-sentence symptom>
+- Evidence: <where it showed up>
 
-*Omit if empty. Drops (rejected at Phase 2.5 triage) live in the full retro
-document linked under Artifacts — they're triage hygiene, not maintainer signal.*
+#### F<n>: ...
 
-## Work Units
+## Related issues
 
-Each WU is filed as a sub-issue for independent tracking. GitHub renders the
-sub-issue list above the comments; the table below mirrors it for at-a-glance
-reading and search.
+<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.>
 
-<!-- WU_TABLE -->
+- #<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 below"> |
-| Manuscripts (research + proofs) | <$MANUSCRIPTS_URL or "Upload failed — see below"> |
-| Generated CLI source code | <$CLI_SOURCE_URL or "Upload failed — see below"> |
+| 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)*
 ```
 
-Create the parent. The parent gets `retro` + `retro-parent` labels and **no
-priority/component labels** — those are per-WU concerns.
+What's *not* in the body, by design:
 
-```bash
-PARENT_URL=$(gh issue create \
-  --repo "$REPO" \
-  --title "$PARENT_TITLE" \
-  --body "$PARENT_BODY" \
-  --label retro \
-  --label retro-parent)
+- **Session Stats table** — collapsed into the one-line provenance header.
+  The retro doc has the full stats.
+- **What the Printing Press Got Right** — retro-wide; lives in the retro doc.
+- **Skipped table** — retro-wide triage record; lives in the retro doc.
+- **Auto-cross-references to sibling WUs in this same retro** — these are
+  noise unless one WU is genuinely a prerequisite for another (and that
+  goes in `Dependencies:` as free text, not as an auto-linked `#N`).
 
-if ! echo "$PARENT_URL" | grep -q '^https://'; then
-  echo "WARNING: parent issue creation failed. Falling back to manual filing instructions."
-  GH_AVAILABLE=false
-else
-  GH_AVAILABLE=true
-  PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
-  echo "Parent issue: $PARENT_URL"
-fi
-```
+### Comment body (for `comment:#N` decisions)
+
+```markdown
+**Recurrence in <api-display-name> retro** *(<retro-date>)*
 
-If `GH_AVAILABLE=false`, skip Steps P3-P5 and continue with the graceful
-degradation path documented at the bottom of this file. Do not create WU issues
-without a parent issue number.
+Same problem surfaced again in this run. New observations:
 
-### Step P3: Create each WU sub-issue and link via the sub-issues API (parallel)
+- <concrete observation 1: file:line, command output, etc.>
+- <concrete observation 2>
 
-Spawn one background subshell per WU. Each subshell:
+Updated frequency estimate: <every / most / subclass:<name>>
 
-1. Creates the issue via `gh api --method POST /repos/.../issues` — the REST
-   endpoint returns `id`, `number`, and `html_url` in a single response, so
-   the legacy "`gh issue create` + `gh api repos/.../issues/N` to fetch the DB
-   id" pair collapses into one round trip per WU.
-2. Links the new issue under the parent via the sub-issues REST endpoint.
-3. Writes a fixed-shape result file to `$WU_TMPDIR/wu-$idx` (one field per
-   line — robust against tabs or other whitespace in titles).
+[Full retro doc](<$RETRO_DOC_URL>) · [Manuscripts](<$MANUSCRIPTS_URL>)
 
-After `wait`, the parent shell collects results in original order so the WU
-table builds correctly. Per-WU work has no cross-WU dependency once
-`$PARENT_NUM` is known, so parallelism is safe and saves roughly `(N-1) ×
-round-trip-latency` on N WUs.
+---
+
+*Comment added by `/printing-press-retro` from the <api-display-name> retro.*
+```
 
-The parent body's findings/WU tables reference `WU-N` by ordinal, so each WU
-contributes a row to the final WU table whether it succeeded or failed.
-Successful ones link to their sub-issue; failed ones surface as `FAILED —
-file manually` so the maintainer sees the gap.
+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 WU_URLS WU_NUMS WU_TITLES WU_PRIORITIES WU_COMP_SLUGS WU_COMPLEXITIES WU_STATUSES
-declare -a FAILED_WUS  # human-readable failures for the final summary
-SUB_ISSUE_API_OK=true
+declare -a OUTCOME_KIND OUTCOME_URL OUTCOME_TITLE OUTCOME_PRIORITY OUTCOME_COMP OUTCOME_COMPLEXITY
+declare -a FAILED_ISSUES
 
-WU_TMPDIR=$(mktemp -d)
+ISSUE_TMPDIR=$(mktemp -d)
 
 for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
   (
     WU="${SORTED_WORK_UNITS[$wu_idx]}"
-    # Each WU contributes: $WU_TITLE, $WU_BODY_TEMPLATE, $WU_PRIORITY_NUM,
-    # $WU_PRIORITY_LABEL, $WU_COMP_SLUG, and $WU_COMPLEXITY.
+    DEDUP="${WU_DEDUP[$wu_idx]}"  # "comment:NN" or empty
 
-    WU_BODY="${WU_BODY_TEMPLATE/<PARENT_NUMBER>/$PARENT_NUM}"
+    # Each WU contributes: $WU_TITLE, $WU_BODY, $WU_COMMENT_BODY,
+    # $WU_PRIORITY_NUM, $WU_PRIORITY_LABEL, $WU_COMP_SLUG, $WU_COMPLEXITY.
 
-    STATUS=""
-    WU_DB_ID=""
-    WU_NUM=""
-    WU_URL=""
+    KIND=""
+    URL=""
     FAIL_MSG=""
 
-    # Single-call create: returns id + number + html_url as a tab-separated
-    # triple. gh's bundled jq runs the filter against the response.
-    if WU_TSV=$(gh api --method POST "/repos/$REPO/issues" \
-        -f title="$WU_TITLE" \
-        -f body="$WU_BODY" \
-        -f "labels[]=retro" \
-        -f "labels[]=priority:P${WU_PRIORITY_NUM}" \
-        -f "labels[]=comp:${WU_COMP_SLUG}" \
-        --jq '"\(.id)\t\(.number)\t\(.html_url)"' 2>&1) \
-        && [ -n "$WU_TSV" ] && [[ "$WU_TSV" == *https://* ]]; then
-      IFS=$'\t' read -r WU_DB_ID WU_NUM WU_URL <<<"$WU_TSV"
-
-      # Link the new issue under the parent. The REST sub-issues endpoint
-      # exists on github.com and most GHES versions; older GHES instances
-      # return 404 here.
-      if LINK_OUT=$(gh api --method POST \
-          -H "Accept: application/vnd.github+json" \
-          -H "X-GitHub-Api-Version: 2022-11-28" \
-          "/repos/$REPO/issues/$PARENT_NUM/sub_issues" \
-          -F "sub_issue_id=$WU_DB_ID" \
-          --jq '.id // .number // empty' 2>&1) \
-          && [ -n "$LINK_OUT" ]; then
-        STATUS="ok"
+    if [[ "$DEDUP" == comment:* ]]; then
+      ISSUE_NUM="${DEDUP#comment:}"
+      if URL=$(gh issue comment "$ISSUE_NUM" \
+            --repo "$REPO" \
+            --body "$WU_COMMENT_BODY" 2>&1) \
+            && [[ "$URL" == https://* ]]; then
+        KIND="commented"
       else
-        STATUS="link-failed"
-        FAIL_MSG="WU-$((wu_idx+1)) (#$WU_NUM) — sub-issue link failed (cross-link in body remains)"
+        KIND="comment-failed"
+        FAIL_MSG="$WU_TITLE — comment on #$ISSUE_NUM failed: ${URL:-no-response}"
+        URL=""
       fi
     else
-      STATUS="create-failed"
-      FAIL_MSG="WU-$((wu_idx+1)) ($WU_TITLE) — issue creation failed: ${WU_TSV:-no-response}"
+      if URL=$(gh issue create \
+            --repo "$REPO" \
+            --title "$WU_TITLE" \
+            --body "$WU_BODY" \
+            --label retro \
+            --label "priority:P${WU_PRIORITY_NUM}" \
+            --label "comp:${WU_COMP_SLUG}" 2>&1) \
+            && [[ "$URL" == https://* ]]; then
+        KIND="created"
+      else
+        KIND="create-failed"
+        FAIL_MSG="$WU_TITLE — issue creation failed: ${URL:-no-response}"
+        URL=""
+      fi
     fi
 
-    # One field per line — survives tabs/spaces in titles.
     {
-      printf '%s\n' "$STATUS"
-      printf '%s\n' "$WU_URL"
-      printf '%s\n' "$WU_NUM"
+      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"
-    } > "$WU_TMPDIR/wu-$wu_idx"
+    } > "$ISSUE_TMPDIR/issue-$wu_idx"
   ) &
 done
 
 wait
 
-# Collect in original WU order so the table indexing (WU-1, WU-2, ...) matches
-# what the parent body's prose references.
 for wu_idx in "${!SORTED_WORK_UNITS[@]}"; do
   {
-    IFS= read -r STATUS
+    IFS= read -r KIND
     IFS= read -r URL
-    IFS= read -r NUM
     IFS= read -r TITLE
     IFS= read -r PRIORITY
     IFS= read -r COMP
     IFS= read -r COMPLEXITY
     IFS= read -r FAIL_MSG
-  } < "$WU_TMPDIR/wu-$wu_idx"
-
-  WU_URLS+=("$URL")
-  WU_NUMS+=("$NUM")
-  WU_TITLES+=("$TITLE")
-  WU_PRIORITIES+=("$PRIORITY")
-  WU_COMP_SLUGS+=("$COMP")
-  WU_COMPLEXITIES+=("$COMPLEXITY")
-  WU_STATUSES+=("$STATUS")
-
-  case "$STATUS" in
-    ok)
-      echo "WU-$((wu_idx+1)) linked: $URL"
-      ;;
-    create-failed)
-      echo "WARNING: $FAIL_MSG"
-      FAILED_WUS+=("$FAIL_MSG")
+  } < "$ISSUE_TMPDIR/issue-$wu_idx"
+
+  OUTCOME_KIND+=("$KIND")
+  OUTCOME_URL+=("$URL")
+  OUTCOME_TITLE+=("$TITLE")
+  OUTCOME_PRIORITY+=("$PRIORITY")
+  OUTCOME_COMP+=("$COMP")
+  OUTCOME_COMPLEXITY+=("$COMPLEXITY")
+
+  case "$KIND" in
+    created|commented)
+      echo "${KIND^}: $URL"
       ;;
-    link-failed)
+    create-failed|comment-failed)
       echo "WARNING: $FAIL_MSG"
-      FAILED_WUS+=("$FAIL_MSG")
-      SUB_ISSUE_API_OK=false
+      FAILED_ISSUES+=("$FAIL_MSG")
       ;;
   esac
 done
 
-rm -rf "$WU_TMPDIR"
-```
-
-Three distinct failure modes, three distinct outcomes:
-
-| Mode | What happened | What the parent shows | What the user sees in Phase 6 Step 6 |
-|------|---------------|----------------------|--------------------------------------|
-| `create-failed` | `gh api POST /repos/.../issues` for the WU returned no usable response | WU table row reads `FAILED — file manually` | `FAILED_WUS` summary names this WU |
-| `link-failed` | Issue created OK but sub-issues REST POST failed | WU table row links the issue normally; native sub-issue panel won't include it | `FAILED_WUS` notes the issue exists but isn't natively linked |
-| `ok` | Issue created and linked | WU table row links the issue; native sub-issue panel includes it | nothing |
-
-### Step P4: Build the sub-issue table
-
-The table iterates over **every** WU, including failed ones. A failed WU's row
-reads `FAILED — file manually` in the Sub-issue column so the parent doesn't
-silently advertise sub-issues that don't exist.
-
-```bash
-WU_TABLE=$'| WU | Title | Priority | Component | Complexity | Sub-issue |\n'
-WU_TABLE+=$'|----|-------|----------|-----------|------------|-----------|\n'
-
-for i in "${!WU_TITLES[@]}"; do
-  if [ "${WU_STATUSES[$i]}" = "create-failed" ]; then
-    SUB_CELL="**FAILED — file manually**"
-  else
-    SUB_CELL="#${WU_NUMS[$i]}"
-  fi
-  WU_TABLE+="| WU-$((i+1)) | ${WU_TITLES[$i]} | ${WU_PRIORITIES[$i]} | comp:${WU_COMP_SLUGS[$i]} | ${WU_COMPLEXITIES[$i]} | ${SUB_CELL} |"$'\n'
-done
-```
-
-### Step P5: Edit the parent body to backfill the WU table
-
-```bash
-PARENT_BODY_FINAL="${PARENT_BODY//<!-- WU_TABLE -->/$WU_TABLE}"
-
-gh issue edit "$PARENT_NUM" --repo "$REPO" --body "$PARENT_BODY_FINAL"
+rm -rf "$ISSUE_TMPDIR"
 ```
 
-If the edit fails (rate limit, permissions), the parent stays with its placeholder
-visible — readable enough that the user understands what's missing, and GitHub's
-native sub-issue panel still shows the linked WUs.
-
-### Step P6: Cross-reference behavior
+Failure modes:
 
-If `SUB_ISSUE_API_OK=false` (sub-issues endpoint unavailable, e.g., older GHES,
-permissions, feature not enabled), the WUs still cross-link to the parent via
-the `**Parent retro:** #<PARENT_NUMBER>` line in their body. The parent's WU
-table also still shows `#<num>` references, which GitHub auto-links. This means
-even with the sub-issues API completely broken, the relationship between parent
-and WUs is preserved as ordinary issue cross-references — the only thing lost
-is GitHub's native sub-issue rendering and progress bar.
+| 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 |
 
 ## Variables expected
 
 | Variable | Set by | Contains |
-|----------|--------|----------|
+|---|---|---|
 | `$REPO` | This file Step 1 | Owner/repo string for `gh` |
 | `$RETRO_DOC_URL` | artifact-packaging.md | catbox URL for retro .md, or empty |
 | `$MANUSCRIPTS_URL` | artifact-packaging.md | catbox URL or empty |
 | `$CLI_SOURCE_URL` | artifact-packaging.md | catbox URL or empty |
 | `$RETRO_PROOF_PATH` | SKILL.md Phase 5 | Path to saved retro in manuscript proofs |
 | `$RETRO_SCRATCH_PATH` | SKILL.md Phase 5 | Path to temp retro copy under `/tmp/printing-press/retro/` |
-| `$WORK_UNITS` | SKILL.md Phase 5.5 | Array of WU records (title, priority, comp slug, complexity, body) |
-| `$SORTED_WORK_UNITS` | This file Step P1 | `$WORK_UNITS` sorted P1 → P3 |
-| All retro findings | SKILL.md Phase 4 | Used to populate the parent's Summary section and each WU sub-issue's "Findings absorbed" section |
+| `$WORK_UNITS` | SKILL.md Phase 5.5 | Array of WU records |
+| `$SORTED_WORK_UNITS` | This file Step 2 | `$WORK_UNITS` sorted P1 → P3 |
+| `$EXISTING_OPEN_RETROS` | This file Step 2.5 | JSON of open retro-tagged issues |
+| `$WU_DEDUP` | This file Step 2.5 | Per-WU dedup decision: `comment:NN` or empty |
+| `$WU_RELATED` | This file Step 2.5 + Phase 3 Step D | Per-WU comma-separated related-issue numbers (annotated for the body) |
+| All retro findings | SKILL.md Phase 4 | Used to populate each issue's "Findings absorbed" section |
 
 ## Variables produced
 
 | Variable | Contains |
-|----------|----------|
-| `$PARENT_URL` | Parent issue URL (only in `parent-with-subs` mode) |
-| `$PARENT_NUM` | Parent issue number (only in `parent-with-subs` mode) |
-| `$WU_URLS` | Array of WU sub-issue URLs (empty string for failed creations; only in `parent-with-subs` mode) |
-| `$WU_STATUSES` | Array, one per WU: `ok` / `create-failed` / `link-failed` (only in `parent-with-subs` mode) |
-| `$FAILED_WUS` | Array of human-readable failure descriptions; empty if every WU succeeded |
-| `$SUB_ISSUE_API_OK` | `false` if any sub-issue link failed; `true` otherwise |
-| `$ISSUE_URL` | Single issue URL (only in `single` mode) |
-| `$ISSUE_MODE` | `single` or `parent-with-subs` — used by Phase 6 Step 6 to format presentation |
+|---|---|
+| `$OUTCOME_KIND` | Array, one per WU: `created` / `commented` / `create-failed` / `comment-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)
 
@@ -594,31 +475,29 @@ 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 issue(s)."
+  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 any of the issue-creation commands fail with a
-network/permissions error that the per-step fallbacks didn't already absorb, fall
-back to printing manual filing instructions:
+If `GH_AVAILABLE=false`, or if every per-WU action failed, fall back to
+printing manual filing instructions:
 
 ```bash
-if [ "$GH_AVAILABLE" = false ]; then
+if [ "$GH_AVAILABLE" = false ] || [ "${#FAILED_ISSUES[@]}" -eq "${#SORTED_WORK_UNITS[@]}" ]; then
   echo ""
-  echo "Could not create GitHub issue(s) automatically."
+  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/new"
-  echo "  2. Use the title and body from the retro document at:"
+  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. For the 2+ WU case, file one issue per WU and link them as sub-issues"
-  echo "     via the issue page's 'Sub-issues' panel after creation."
+  echo "  3. File one issue per work unit. Apply labels: retro, priority:P<n>, comp:<slug>."
   if [ -n "$MANUSCRIPTS_URL" ]; then
     echo "  4. Manuscripts: $MANUSCRIPTS_URL"
   fi
@@ -628,18 +507,20 @@ if [ "$GH_AVAILABLE" = false ]; then
 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 breakout into sub-issues
-makes hitting this limit much less likely than the old monster-issue mode, but a
-single WU with many absorbed findings + long prior-retro chains could still
-approach it. If `gh issue create` rejects a body for 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 the absorbed findings within the WU body to: title + one-sentence
-   summary + `Evidence:` link.
-2. Add: "Full finding analysis available in the manuscripts artifact linked
-   from the parent issue."
+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.
-
-For the parent: drop "What the Printing Press Got Right" and the Skipped table
-first; keep Session Stats, the Summary section, the WU table, and Artifacts.

← 5776553b docs(cli): rewrite install steps and collapse secondary sect  ·  back to Cli Printing Press  ·  feat(cli): widen auth env-var model with kind/required/sensi 3bc1a23a →