plugins/dev/skills/ticket-retro/SKILL.md
Cross-ticket retrospective VIEW (CTL-789 Loop C / CTL-814). **ALWAYS use when** a ticket's PR has just merged (the workflow's compound closing step — runs automatically per ticket, CTL-831), or when the user says 'ticket retro', 'run a retro', 'retrospective', 'what did we learn lately', or 'how are the estimates calibrating'. Synthesizes everything the compound loops captured since the last retro — friction logs, learnings, compound-log calibration, catalyst.db / merged-PR actuals — into thoughts/shared/retros/ticket/<date>.md with a persisted watch-items block, and surfaces top patterns in the morning briefing's Plan today.
npx skillsauth add coalesce-labs/catalyst ticket-retroInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Loop C of compound engineering: a human-readable reflection across a SET of tickets. It mostly reads what Loop B (friction logs, learnings) and Loop A (compound-log, estimation corpus) captured, then writes ONE artifact: the retro document.
Runs automatically per ticket (CTL-831): merge-pr step 12b and phase-monitor-merge invoke
this skill right after the compound-log entry lands, so the system learns from every ticket it
ships without being asked. Best-effort in those contexts — a retro failure never blocks a merge
or a phase. Several merges per day are normal: same-day re-runs REGENERATE today's file
cumulatively (the gather floor skips today — see Step 3).
Hard contract — read-only VIEW:
thoughts/shared/retros/ticket/<YYYY-MM-DD>.md.thoughts/shared/CONCEPTS.md, or touch ADRs —
that is ticket-compound's job (per-ticket curator). No Linear writes, no corpus writes._none_ — empty stores are the normal early state, never an error./catalyst-dev:ticket-retro # since-last-retro (default scope)
/catalyst-dev:ticket-retro --since 2026-06-01 # explicit window floor
/catalyst-dev:ticket-retro --tickets CTL-1,CTL-2 # explicit ticket set (all time)
Default scope is since-last-retro, no time box (solo-dev rhythm — design decision, plan
line 114): the window floor is the date of the most recent retro in
thoughts/shared/retros/ticket/; the first retro ever falls back to 14 days.
All reads go through the gather helper — one JSON document, every section degrades to empty:
GATHER="${CLAUDE_PLUGIN_ROOT:-plugins/dev}/scripts/ticket-retro/gather-retro.sh"
RETRO_JSON=$(mktemp)
bash "$GATHER" --thoughts-dir thoughts "$@" > "$RETRO_JSON"
jq '{window, prior_retro: (.prior_retro != null), friction: (.friction|length),
learnings: (.learnings|length), calibration: (.calibration.entries // 0),
merged_prs: (.merged_prs|length), db_stats: (.db_stats|length)}' "$RETRO_JSON"
What it returns (see the script header for the full shape):
| Key | Source | Degrades to |
|---|---|---|
| window | latest retros/YYYY-MM-DD.md / --since / --tickets | 14-day default |
| prior_retro.watch_items | the previous retro's fenced yaml watch-items block | null |
| friction[] | thoughts/shared/friction/*.md (## <phase> · <TICKET> · <ISO-8601> records after the floor) | [] |
| learnings[] | thoughts/shared/learnings/** frontmatter, mtime after the floor | [] |
| calibration | compound-log.sh aggregate (estimate_at_start vs estimate_actual) | {} |
| merged_prs[] | gh pr list --state merged in-window, ticket id from branch/title | [] |
| db_stats[] | ~/catalyst/catalyst.db sessions⋈session_metrics per ticket (SPARSE — see note) | [] |
Actuals note: db_stats covers only orchestrator-run tickets with metrics rows (historically
~13 of 333). merged_prs[].additions/deletions (diff churn) is the universal actuals fallback —
use it for the aggregate stats; treat db cost/hours as a bonus column where present.
merged_prs by ticket; one line each. Failed/abandoned tickets that
show up in friction but not in merged_prs belong here too (often highest-signal).friction[].line entries that describe the same
underlying problem (same component, same failure shape — NOT necessarily same wording). A
pattern needs ≥2 records (across tickets or phases). One-off frictions are listed only if
severe. For each pattern: a name, the supporting records (ticket·phase), and one sentence of
synthesis.prior_retro.watch_items[] pattern, check whether this
window's friction/learnings show it again. Verdict per item: recurred (cite evidence),
quiet (no sighting), or resolved (a learning/ADR/fix landed that addresses it — cite it).calibration: count/exact/mean-signed-delta/median-abs-delta
plus a per-ticket start→actual table. When calibration.entries == 0, render _none_ and note
the sink fills at merge (merge-pr 12b / phase-monitor-merge).first_seen) and add
new patterns from (2) worth tracking. Cap at ~7 — a watch list longer than that is a backlog,
not a watch list.Path: thoughts/shared/retros/ticket/<YYYY-MM-DD>.md (today UTC). If today's file already
exists, OVERWRITE it — the gather floor deliberately skips today's retro (CTL-831), so a
same-day re-run covers the same since-prior-retro window plus whatever just merged; today's file
is always the cumulative day view, never a near-empty increment. Template:
---
date: <YYYY-MM-DD>
type: retro
generated_by: ticket-retro
window_since: <window.since>
window_source: <window.source>
tickets_shipped: <N>
---
# Ticket Retro — <YYYY-MM-DD>
Window: <window.since> → today (<window.source>)
## What we did
- `CTL-x` title — #PR (+adds/−dels)
- … (_none_ when empty)
## Aggregate stats
| Metric | Value |
|---|---|
| Tickets shipped | N |
| Diff churn (LOC) | +A / −D |
| Sessions / cost / hours (catalyst.db, sparse) | N / $C / H |
## Recurring friction patterns
- **<pattern name>** (N records: CTL-a·research, CTL-b·implement) — one-sentence synthesis.
- … (_none_ when empty)
## What we learned
- [component] title — `path` (_none_ when empty)
## Estimation calibration
entries: N · exact: N · mean signed delta: +X.X · median |delta|: X
| Ticket | start | actual | Δ |
|---|---|---|---|
… (_none_ when empty)
## Watch items from last retro
- ✅ resolved / 🔁 recurred / 💤 quiet — <pattern> (evidence)
… (_no prior retro_ on the first run)
## Watch items
```yaml watch-items
- pattern: "<short greppable description>"
component: <orchestrator|phase-agent|broker|monitor|cli|ci|worktree|linear|execution-core|estimation|website|plugins>
first_seen: <YYYY-MM-DD of when it first appeared — preserve across retros>
source: <TICKET the clearest record came from>
**The watch-items block is the only stateful contract.** The next retro and the morning briefing
both machine-parse it: keep the exact fence info string `yaml watch-items`, the exact four keys,
and `pattern` values double-quoted. `component` uses the learnings-store enum
(`ticket-compound/reference.md`).
## Step 4: Sync + report
```bash
humanlayer thoughts sync 2>/dev/null || true
echo "ticket-retro: wrote thoughts/shared/retros/ticket/$(date -u +%Y-%m-%d).md"
Report to the user: the retro path, top 3 recurring patterns, the calibration one-liner, and any
recurred watch-items. The next morning briefing surfaces the watch-items automatically
(Plan today → Retro signals).
| Skill | Owns | Scope |
|---|---|---|
| ticket-compound | learnings / CONCEPTS / ADR proposals (writes) | one ticket |
| compound-estimate | estimation numbers → compound-log (writes) | one PR |
| ticket-retro | the cross-ticket VIEW (reads both, writes only its retro doc) | a window of tickets |
development
Migrate a single-harness repo to the dual-harness layout so both Claude Code and Codex load the same instructions and skills — AGENTS.md as the portable canonical doc, a thin CLAUDE.md `@AGENTS.md` bridge, and a `.agents/skills` dir with a `.claude/skills` symlink onto it. Use when asked to migrate to dual-harness, make this repo work in both Claude and Codex, or for agent metadata cleanup.
tools
Goal-driven senior-engineer pipeline-unstick sweep (CTL-1176 rung 3). Given the stuck/failed/needs-human set (or ONE ticket handed by the recovery router), its GOAL is to get the pipeline MOVING again — not to fix one ticket's review findings (that is phase-remediate). It runs AFTER the eyes (diagnostician evidence) and the hands (deterministic unstuck-sweep seams) have already tried, and it CONSUMES their output from a recovery-pass.json brief rather than re-diagnosing or redoing their narrow work. It acts like a senior engineer with full tool access — it resolves merge conflicts, rebases, force-pushes, merges green PRs, and re-dispatches stalled phases AUTONOMOUSLY — and escalates to the operator ONLY for a genuine value judgment / something that degrades other functionality / a real cost-benefit trade-off / a serious architecture change / an ADR conflict. On escalation it AUTHORS the operator inbox row + the push notification (executive-voiced). Dispatched as a `claude --bg` job by phase-agent-dispatch via slash command, AND invocable bare by the operator as a sweep — hence `user-invocable: true`. Ships behind CATALYST_RECOVERY_PASS (off by default — no live behavior change until shadow/enforce).
tools
Diagnose and fix Catalyst setup issues. Validates tools, database, config, OTel, direnv, and thoughts. Automatically fixes what it can — creates directories, initializes the database, sets WAL mode, runs migrations. Use for new installs, upgrades, or when something isn't working.
tools
--- name: phase-triage description: Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies genuine blockers (a semantic second-pass over the backlog — NOT a prose scrape; CTL-838), estimates scope, writes triage.json, and posts a triage analysis comment to Linear. Triage completion is signaled by that comment plus the local triage.json — there is no `triaged` label. Emits phase.triage.complete.<TICKET> on success and phase.triage.fai