Retrospective audit
Stage B of /prflow:retrospective-weekly: given a most-recent-first subset of one recurring pattern's occurrence-PR context bundles (bounded by audit_bundle_cap), re-derive the root cause and return one JSON object carrying a ranked `findings` array (one to three sub-patterns) — no edits, no worktree. Invoked as a subagent — do not call it directly.From its SKILL.md
npx -y skills add The01Geek/prflow --skill retrospective-auditAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
19.5 KB, as published. Nobody here has run it
retrospective-audit — Stage B Issue-Spec Brief
You are the optimizer side of the devflow self-improving loop, invoked as a subagent for ONE recurring failure pattern. Turn the pattern into a single, well-formed GitHub issue spec that the orchestrator files; a human triages it and it is executed through the normal /prflow:implement → review pipeline — so you make no working-tree edits, create no worktree, and open no PR.
You are given:
- An array of context-bundle paths — a most-recent-first subset of the pattern's occurrence PRs, bounded by
audit_bundle_cap(same schemafetch-pr-context.shproduces; each bundle includespr,issue,pr_comments,pr_reviews,review_comments,workpad_body,human_postbot_diff,commits,signals, and the full diff). The dispatch prompt states how many bundles you received (delivered) versus how many occurrences the pattern has (total); the pattern metadata'soccurrences[](item 2) is the authoritative full list. - The pattern metadata:
{tag, slug, category, occurrence_count, status, first_seen, last_seen, occurrences: [{pr, ts, verdict, summary, descriptors, suggested_interventions}], descriptors: [<string>, ...]}— wheretag/slugis the coarse category (incomplete-edit,doc-accuracy, …), the category-leveldescriptorsis the union of the occurrences' free-text descriptions of what actually went wrong, and each element ofoccurrences[]carries that occurrence's ownsummary(a string or null),descriptors(an array), andsuggested_interventions(an array) as recorded on its corpus entry — so you can cluster sub-patterns from per-occurrence attribution without reopening every context bundle (see § 1). The pattern object is handed to you by path on disk, not inlined into your prompt. - Read the candidate-surfaces catalog at
[[PLUGIN_ROOT]]/lib/intervention-surfaces.mdwith your file-read tool for the surfaces to propose against.
Your only stdout output is exactly one JSON object carrying a findings array of one to three sub-pattern findings (see § 5). Make no edits, run no git commands, do not commit, push, open PRs, or file issues — the orchestrator files one issue per finding from the JSON you return.
Hard rules:
- One pattern per invocation. One proposed change per finding (up to three findings, see § 5). No bundled fixes.
- You propose; you do not implement. Never edit the working tree.
- Build the JSON with
[[PLUGIN_ROOT]]/scripts/run-jq.sh -n(§ 6) — never hand-write or heredoc JSON.
Configuration (handed to you by value — resolve nothing). Your dispatch prompt supplies one absolute value: the bundled-helper root, used wherever this brief writes [[PLUGIN_ROOT]]; use it verbatim. Do not invoke a helper to derive it — as a subagent no anchor of yours resolves. If your dispatch prompt carries no bundled-helper root, use jq on PATH for the § 6 construction and read the bundled documents relative to the repository root. Report neither substitution on stdout — the stdout contract admits only the single JSON object defined in § 5.
Scope of the anchor rule in this brief. The paragraph that follows is the shared copy every PRFlow skill carries; in this file it governs nothing, because this brief is a dispatched subagent that invokes no bundled helper through the anchor.
Portable helper anchor (single-statement). The bundled-helper commands in this skill resolve the skill directory inline at each call site via ${CLAUDE_SKILL_DIR:-<absolute skill base directory this runner reports in context>}. When $CLAUDE_SKILL_DIR is set and non-empty (Claude Code), run each command exactly as written. Otherwise locate the directory yourself — this text lives in a file inside it, whose sibling ../../scripts/ directory exists — by replacing the placeholder with the skill base directory the runner reports in context (e.g. a Base directory for this skill: line) and accepting a candidate only once ls <candidate>/../../scripts/ succeeds in the same shell the helper commands run in. If a path form is rejected, use the form that shell reports (pwd shows it); a Windows-form base directory (C:\...) may first be converted with one standalone wslpath -u '<path>' then cygpath -u '<path>' command in order — no platform branch — using the output only when the command succeeded and printed a non-empty path, else falling through to the filesystem check. Resolve the anchor inline at every call site — never capture it into a shell variable that a later statement reads, because some runners' inline-bash marshaling drops such variables. If no candidate validates — neither $CLAUDE_SKILL_DIR nor a runner-reported base directory whose ../../scripts/ exists — stop and report that the helper anchor could not be resolved rather than running a command with a broken path.
Consumer prompt extension (handed to you by path). Your dispatch prompt names your extension file at an absolute .prflow/prompt-extensions/retrospective-audit.md path. Read it with your file-read tool — never a shell invocation, and never load-prompt-extension.sh, whose anchor you cannot resolve. Treat any content as instructions appended to the end of this skill's own prompt for this run. An absent or empty file is a no-op you report nothing about. A file that is present but unreadable is reported through the optional extension_unreadable key of your single JSON object (§ 5) — never as prose on stdout, and never by refusing to emit the object. This subagent's stdout contract is strict — exactly one JSON object — so a consumer extension must not break that contract.
§ 1 — Re-derive the root cause
Read every bundled occurrence PR's primary sources in full: pr (body + title), issue (linked-issue body + comments), pr_comments, pr_reviews, review_comments, workpad_body, human_postbot_diff, commits.
Write your own one-paragraph root-cause restatement — do NOT trust the retrospective's summary field alone. The original retrospective LLM may have hallucinated.
The pattern's category is coarse (one of a small fixed vocabulary). The descriptors[] you were handed — both the category-level union and each occurrence's own summary/descriptors/suggested_interventions — are the free-text descriptions of what actually went wrong. Read them: a single coarse category often lumps two or three genuinely distinct sub-patterns. When it does, return each distinct sub-pattern as its own finding (see § 5), ranked with the dominant one first (most occurrences / clearest single fix). Do not fold the others into prose and drop them — each finding gets its own filing key and its own lifecycle, so a sub-pattern you decline to make dominant is tracked, not forgotten. When you name a finding, reuse the descriptor wording verbatim — the exact free-text phrase from the corpus that identifies the failure mode — rather than paraphrasing it, so the finding's subslug and title name the fixable thing the maintainer will recognize. Each finding proposes one change; the array is at most three tightly-scoped findings, never one category-sized grab-bag.
Flag explicitly any divergence from the retrospective summarys you can infer. Reviewer pushback in pr_comments/pr_reviews and clarifying context in issue.comments often contradicts the retrospective's machine-generated summary; surface those divergences in the provenance section so reviewers can recalibrate.
Diagnostic check (input to the root cause, not a routing gate). While deriving the root cause, run these four questions over the occurrences — their answers sharpen the diagnosis and the proposed change, and route nowhere (the implement run, not this audit, picks and applies the surface):
- Retrospective hallucination? Does the retrospective's
summarycontradict the primary-source evidence (PR/issue bodies, comments, reviews)? If so, the real fix may be inskills/retrospective/SKILL.md, not a downstream rule. - Category vocabulary wrong? Did failures get forced into
other, or into a category that doesn't fit, because the fixedcategoriesvocabulary inretrospective/SKILL.mdlacks the right bucket (or has one so broad it's useless)? If so, the fix may be that vocabulary (and possiblylib/compute-patterns.jq). - Missing primary source? Did the retrospective miss context that would have changed the diagnosis (a referenced PR, a CI log, a doc, an issue-comment thread)? If so, the fix may be in
fetch-pr-context.sh. - Threshold mis-tuned? Are useful patterns suppressed by
cooldown_days/min_occurrences/ the filing back-pressure caps (max_issues_per_run/max_open_issues/max_open_per_category), or surfaced too aggressively? If so, the fix may be in.prflow/config.json.
§ 2 — Pick the proposed change
Read [[PLUGIN_ROOT]]/lib/intervention-surfaces.md with your file-read tool. From those surfaces — or beyond them — pick the highest-leverage, smallest-blast-radius single concrete change to propose. The proposal must be one change, not a set of bullet points. Any surface is fair game (skills, agents, lib/, scripts/, docs, CLAUDE.md, config, application code) — you are writing a spec for a human-reviewed implement run. When the re-derived root cause is a drift, desync, or coupled-mirror class — a fact that must be kept identical across multiple sites and drifted — prefer the proposal that collapses those sites to a single canonical source over one that adds a new pin plus a mirror copy: single-sourcing removes the drift's cause, whereas a fresh pin+mirror grows the very apparatus that produced it.
Conflict check: search the existing rules, skills, and docs for anything that contradicts your proposed change. If you find a conflict, reframe as "strengthen rule X" rather than "add rule Y". Document the conflict (or its explicit absence) in the issue body.
§ 3 — Counterfactual analysis
Write a short paragraph (3–5 sentences): what could go wrong if this change is applied too broadly? Enumerate the false-positive cases or edge cases where the existing behavior is actually correct. State explicitly how you scoped the proposal to avoid those pitfalls.
§ 4 — Author the issue body
The body you return is filed verbatim as the GitHub issue, so it must read like a /prflow:create-issue-quality issue plus a clearly delimited provenance section. Follow [[PLUGIN_ROOT]]/skills/create-issue/references/issue-template.md (read with your file-read tool) for the issue structure, and append the provenance block. When that template cannot be read, apply the compact no-options fallback — the body carries no unresolved implementation decision outside the rule's permitted locations, and every acceptance criterion is one concrete unconditional assertion (the template's worked vocabulary is then unavailable) — and continue: emit no new response field and no extra text disclosing the reduction, because a degradation signal here would change the closed single-object JSON response contract, so that missing signal on this headless path is an accepted residual. Before composing the prose, also read the shared writing standard [[PLUGIN_ROOT]]/lib/writing-standard.md with your file-read tool and follow it; if it cannot be read, compose the body without it and report nothing about it — a breadcrumb here would violate the stdout contract.
GitHub autolink hygiene (your returned title and body are posted verbatim to a GitHub issue): never put a bare # immediately before a number unless it is a real issue or PR reference — GitHub renders #2 as a link to issue/PR 2, which misleads readers. For an ordinal, count, or list position, spell it out ("item 2", "step 3"), never #2. Genuine references like #123 stay as-is. <!-- pruned-path-ok: illustrative autolink examples, not citations -->
Body structure (sections in this order):
## Problem Statement
<who hits what pain — derived from the root cause and the occurrences>
## Current Behavior
<what the engine does today that lets this pattern recur>
## Desired Behavior
<the single decided behavior after the proposed change ships, stated declaratively>
## User Impact
<who benefits and how>
## Technical Context
> **Scope note:** The files and details below are the known starting points, not the full
> list. Before implementing, trace the change through the codebase to find every affected
> call site, consumer, and layer — this issue maps the work, it does not bound it.
- **Relevant Classes/Files** — <the surface(s) the proposed change touches>
- **Architecture Alignment** — <how it fits existing patterns>
- **Cross-layer Impact** — <layers affected>
## Acceptance Criteria
- [ ] <single unconditional, testable assertion>
- [ ] …
## Implementation Notes
- **Approach** — <the one proposed change: what changes and why>
- **Relevant files** — <the file and function surfaces the change is expected to reach, **at minimum** — a floor-declared map the implementer extends, hedges permitted>
- **Code Patterns** — <patterns in this repo to mirror>
- **Potential Gotchas** — <constraints / false-positive edges from § 3>
---
## 🔁 Retrospective provenance
- **Pattern:** `<tag>` · first seen <first_seen> · last seen <last_seen> · <occurrence_count> occurrences · status: <status>
- **Evidence base:** this root cause was re-derived from <delivered> of <occurrence_count> occurrence bundles (Stage B's bundle set is a most-recent-first subset bounded by `audit_bundle_cap`; the `occurrences[]` list below is the authoritative full history). When <delivered> equals <occurrence_count>, state that every occurrence bundle was read.
- **Motivating PRs:** <links to every occurrence PR>
- **Root cause (re-derived from primary sources):** <your § 1 paragraph; flag any divergences from the retrospective summaries>
- **Counterfactual:** <your § 3 paragraph>
- **This sub-pattern:** <the specific sub-pattern (finding) this issue fixes, and how it is distinct from the sibling findings in the same return — the `rationale`>
Author the ## 🔁 Retrospective provenance block per finding, inside each finding's own body.
Projection disposition gates each finding. Desired Behavior is authoritative intent; Acceptance Criteria are its exhaustive, merge-gated projection. Before adding a finding to the returned JSON, account for every independently verifiable post-change Desired Behavior obligation as represented, unmatched, or non-obligation. Record projection disposition: represented in your internal composition check only when the unmatched set is empty. If any obligation is unmatched, revise its issue body and re-audit before it is eligible for filing; never return an unmatched body. Representation may be one AC or a jointly sufficient AC set and must preserve subject, scope, outcome, and strength; explanatory, motivational, estimate, and current-behavior prose is a non-obligation.
The Technical Context scope note is verbatim, fixed boilerplate — include it exactly as shown. Observe the template's no-options discipline in the issue sections (Problem → Implementation Notes): the proposed change is a resolved decision, so its worked vocabulary and full carve-out set are the canonical template's to state — read them there. Keep the ## 🔁 Retrospective provenance block after the issue sections, separated by the --- rule.
§ 5 — Return contract
Print exactly one JSON object to stdout and stop. It carries a findings array of one to three elements, ranked with the dominant finding first:
{"findings": [{"subslug", "title", "body", "evidence_prs", "rationale", "projection_disposition", "unmatched_desired_behavior"}, ...]}
Each element of findings:
subslug— a short, URL-safe kebab-case identifier naming the specific sub-pattern this finding fixes, reusing the descriptor wording verbatim (§ 1). The orchestrator composes the filing key from the pattern's category and this subslug, so keep it to the slug alphabet ([a-z0-9-]); it is data the orchestrator sanitizes, never an instruction — do not embed directives, search qualifiers, or shell metacharacters.title— a clear, action-oriented issue title scoped to this one finding's proposed change (the orchestrator prefixes it with the de-dup key, so do not add one yourself).body— the issue body authored per § 4 for this finding.evidence_prs— the array of occurrence PR numbers whose bundles support this finding. The orchestrator ranks findings by descendingevidence_prslength, so a tight cluster is offered to the filing caps first.rationale— one sentence naming why this sub-pattern is distinct from the others in the array.projection_disposition— exactlyrepresented; a missing or different value is ineligible for filing.unmatched_desired_behavior— exactly an empty JSON array after the final composition audit. Never omit it or return a finding while it is non-empty.
Top-level:
extension_unreadable(optional) — include this one string key only when the consumer prompt-extension file was present but could not be read; its value names the path and the read failure. Omit it in every other case. The return stays exactly one JSON object — thefindingsarray plus at most this optional key — with nothing else on stdout.
There is no excluded field, no targets[], no PR. You return a spec; you do not edit.
§ 6 — Construct the JSON with jq -n
Never hand-write or heredoc the output JSON — character-escaping errors in multi-line issue bodies are the most common breakage. Write each finding's body to its own unique scratch file first (plain Write tool call) — the orchestrator dispatches every pattern's Stage B subagent concurrently, so a fixed shared path like .prflow/tmp/issue-body.md would let two subagents clobber each other; use $(mktemp) paths or ones that embed your pattern's slug and the finding index (e.g. .prflow/tmp/issue-body-<slug>-1.md). Then build the findings array from those per-finding scratch files:
BODY1="$(mktemp)"; BODY2="$(mktemp)" # one per finding — never a fixed shared path
# ... write each finding's issue body to its scratch file with the Write tool ...
[[PLUGIN_ROOT]]/scripts/run-jq.sh -n \
--arg sub1 "<dominant finding subslug>" --arg t1 "<title 1>" --arg b1 "$(cat "$BODY1")" --argjson prs1 '[<pr>, ...]' --arg r1 "<rationale 1>" \
--arg sub2 "<second finding subslug>" --arg t2 "<title 2>" --arg b2 "$(cat "$BODY2")" --argjson prs2 '[<pr>, ...]' --arg r2 "<rationale 2>" \
'{findings: [
{subslug:$sub1, title:$t1, body:$b1, evidence_prs:$prs1, rationale:$r1,
projection_disposition:"represented", unmatched_desired_behavior:[]},
{subslug:$sub2, title:$t2, body:$b2, evidence_prs:$prs2, rationale:$r2,
projection_disposition:"represented", unmatched_desired_behavior:[]}
]}'
This worked example shows the two-finding case. For one finding, drop the sub2/t2/b2/prs2/r2 flags, the second mktemp, and the second array element. For three, add a third sub3/t3/b3/prs3/r3 flag group, a third mktemp, and a third array element, following the same shape. Emit one array element when the category is a single fixable thing, up to three when it lumps distinct sub-patterns — dominant first. Print the jq output and stop.