plugins/dev/skills/describe-pr/SKILL.md
Generate or update PR description with incremental changes. **ALWAYS use when** the user says 'describe the PR', 'update PR description', 'generate PR description', or after pushing new commits to an existing PR. Supports incremental updates that preserve manual edits.
npx skillsauth add coalesce-labs/catalyst describe-prInstall 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.
Generates or updates PR description with incremental information, auto-updates title, and links Linear tickets.
# Check project setup (thoughts, CLAUDE.md snippet, config)
if [[ -f "${CLAUDE_PLUGIN_ROOT}/scripts/check-project-setup.sh" ]]; then
"${CLAUDE_PLUGIN_ROOT}/scripts/check-project-setup.sh" || exit 1
fi
# Check if template exists
if [ ! -f "thoughts/shared/pr_description.md" ]; then
echo "❌ PR description template not found"
fi
If missing:
❌ PR description template missing
Your humanlayer thoughts setup is incomplete. Create a template at:
thoughts/shared/pr_description.md
See the PR description template you created earlier for reference.
Read template fully to understand all sections.
If argument provided:
/describe_pr 123If no argument:
# Try current branch
gh pr view --json number,url,title,state,body,headRefName,baseRefName 2>/dev/null
If no PR on current branch OR on main/master:
# List recent PRs
gh pr list --limit 10 --json number,title,headRefName,state
Ask user: "Which PR would you like to describe? (enter number)"
From multiple sources:
# 1. From branch name
branch=$(gh pr view $pr_number --json headRefName -q .headRefName)
if [[ "$branch" =~ ([A-Z]+)-([0-9]+) ]]; then
ticket="${BASH_REMATCH[0]}"
fi
# 2. From PR title
title=$(gh pr view $pr_number --json title -q .title)
if [[ "$title" =~ ([A-Z]+)-([0-9]+) ]]; then
ticket="${BASH_REMATCH[0]}"
fi
# 3. From existing PR body
body=$(gh pr view $pr_number --json body -q .body)
if [[ "$body" =~ Refs:\ ([A-Z]+-[0-9]+) ]]; then
ticket="${BASH_REMATCH[1]}"
fi
Read current PR body from GitHub:
current_body=$(gh pr view $pr_number --json body -q .body)
Read saved description (if exists):
saved_desc="thoughts/shared/prs/${pr_number}_description.md"
if [ -f "$saved_desc" ]; then
# Read fully
# Note what sections exist vs what's new
fi
Check for metadata header:
<!-- Auto-generated: 2025-10-06T10:30:00Z -->
<!-- Last updated: 2025-10-06T14:45:00Z -->
<!-- PR: #123 -->
<!-- Previous commits: abc123,def456 -->
# Full diff
gh pr diff $pr_number
# Commit history with messages
gh pr view $pr_number --json commits
# Changed files
gh pr view $pr_number --json files
# PR metadata
gh pr view $pr_number --json url,title,number,state,baseRefName,headRefName,author
# CI/CD status
gh pr checks $pr_number
If this is an UPDATE (saved description exists):
# Extract previous commit list from metadata
prev_commits=$(grep "Previous commits:" $saved_desc | sed 's/.*: //')
# Get current commits
current_commits=$(gh pr view $pr_number --json commits -q '.commits[].oid' | tr '\n' ',' | sed 's/,$//')
# Compare
new_commits=$(comm -13 <(echo "$prev_commits" | tr ',' '\n' | sort) <(echo "$current_commits" | tr ',' '\n' | sort))
Analysis:
Auto-generated sections (always update):
Preserve manual edits in:
Merging strategy:
## Changes Made
### Backend Changes
[Existing changes from previous description]
**New changes** (since last update):
- [New change 1]
- [New change 2]
### Frontend Changes
[Existing + new merged together]
Add change summary at top:
<!-- Auto-generated: 2025-10-06T15:00:00Z -->
<!-- Last updated: 2025-10-06T15:00:00Z -->
<!-- PR: #123 -->
<!-- Previous commits: abc123,def456,ghi789 -->
---
**Update History:**
- 2025-10-06 15:00: Added validation logic, updated tests (3 new commits)
- 2025-10-06 10:30: Initial implementation (5 commits)
---
If ticket found:
## Related Issues/PRs
- Fixes https://linear.app/{workspace}/issue/{ticket}
- Related to #NNN (reference sibling work by its **GitHub PR number**)
CTL-623 — sibling reference format (REQUIRED): When referencing related/sibling
work in prose, reference it by its GitHub PR number (#NNN), never by a bare
Linear token (TEAM-NNN) or a Linear issue URL. A bare sibling TEAM-NNN token in the
body is auto-linked by Linear's GitHub integration and drags that sibling's workflow
status (Done → Implement) on PR open/merge. Do not emit bare sibling Linear tokens
in prose. The own ticket's Fixes https://linear.app/... line is correct and stays —
that link/transition is intended. Sibling neutralization is handled mechanically by the
guard block appended at write-back time (see below).
Get Linear ticket details using the Linearis CLI (run linearis issues usage for read syntax).
Extract title and description with jq. Use ticket title and description for context.
Title generation rules:
# If ticket exists and linearis available, read ticket title (see `linearis issues usage`)
if [[ "$ticket" ]] && command -v linearis &>/dev/null; then
ticket_title=$(linearis issues read "$ticket" | jq -r '.title')
title="$ticket: ${ticket_title:0:60}"
elif [[ "$ticket" ]]; then
# Fallback: generate title from branch name + commits
title="$ticket: $(echo "$branch" | sed "s/^.*$ticket-//" | tr '-' ' ')"
else
# No ticket: generate from primary change
title="Brief summary of main change"
fi
Auto-update without prompt - title is auto-generated section.
For each checklist item in "How to Verify It":
# Example: "- [ ] Build passes: `make build`"
# Extract command: make build
# Try to run
if command -v make >/dev/null 2>&1; then
if make build 2>&1; then
# Mark as checked
checkbox="- [x] Build passes: \`make build\` ✅"
else
# Mark unchecked with error
checkbox="- [ ] Build passes: \`make build\` ❌ (failed: $error)"
fi
else
# Can't run
checkbox="- [ ] Build passes: \`make build\` (manual verification required)"
fi
Common checks to attempt:
make test / npm test / pytestmake lint / npm run lintnpm run typecheck / tsc --noEmitmake build / npm run buildDocument results:
IMPORTANT: Document Storage Rules
thoughts/shared/prs/ for PR descriptionsthoughts/searchable/ — this is a read-only search indexSave description:
# Add metadata header
cat > "thoughts/shared/prs/${pr_number}_description.md" <<EOF
<!-- Auto-generated: $(date -u +%Y-%m-%dT%H:%M:%SZ) -->
<!-- Last updated: $(date -u +%Y-%m-%dT%H:%M:%SZ) -->
<!-- PR: #$pr_number -->
<!-- Previous commits: $commit_list -->
[Full description content]
EOF
Sync thoughts:
humanlayer thoughts sync
CRITICAL: NO CLAUDE ATTRIBUTION
Before updating the PR, ensure the description contains NO Claude attribution:
❌ Remove these if present:
✅ Keep descriptions professional and human-authored:
Update title:
gh pr edit $pr_number --title "$new_title"
Update body:
body_file="thoughts/shared/prs/${pr_number}_description.md"
# CTL-623: append a Linear automation guard block so sibling tickets embedded in
# the branch name or pulled into the body (Linear relations/description) are NOT
# auto-linked and dragged backward in status when this PR opens/merges. Scans the
# branch AND the assembled body; no-op for single-ticket PRs. See
# https://linear.app/docs/github (skip/ignore negative magic word).
# CTL-633: branch and body are scanned in DIFFERENT modes — the branch goes
# through the awk segmenter (legitimate sibling-number recovery from
# build_orch_name slugs); the body goes through a canonical-only regex
# (\b[A-Z]+-[0-9]+\b) so prose like "Released 2026-05-25", "UTF-8", and
# "abc123" cannot fabricate fake `skip TEAM-NNN` lines. Both modes are also
# filtered through the optional team-key allowlist for defense in depth.
# shellcheck source=/dev/null
source "${CLAUDE_PLUGIN_ROOT}/scripts/lib/linear-pr-skip.sh"
body="$(cat "$body_file")"
skip_block="$( {
linear_sibling_skip_block_from_branch "$ticket" "$branch"
linear_sibling_skip_block_from_body "$ticket" "$body"
} | awk '/^skip /{if(!seen[$0]++) print; next} {if(!h){print; h=1}}' )"
[[ -n "$skip_block" ]] && printf '\n%s\n' "$skip_block" >>"$body_file"
# Ensure no Claude attribution in the description file
gh pr edit $pr_number --body-file "thoughts/shared/prs/${pr_number}_description.md"
If ticket found:
# If Linearis CLI is available:
# 1. Update ticket status to stateMap.inReview from config
# 2. Add a comment with the PR link and verification summary
# Use `linearis issues usage` and `linearis comments usage` for exact syntax.
# Skip silently if CLI not available.
Skip the status transition (step 1) when CATALYST_PHASE is set — under a
phase agent the deterministic coordinator (CTL-558) owns the Linear status
write-back. This status transition is only for interactive
/catalyst-dev:describe-pr use; the PR-link comment (step 2) is still posted in
both modes.
If first-time generation:
✅ PR description generated!
**PR**: #123 - {title}
**URL**: {url}
**Verification**: {X}/{Y} automated checks passed
**Linear**: {ticket} updated
Manual verification steps remaining:
- [ ] Test feature in staging
- [ ] Verify UI on mobile
Review PR on GitHub!
If incremental update:
✅ PR description updated!
**Changes since last update**:
- 3 new commits
- Added validation logic
- Updated tests
**Verification**: {X}/{Y} automated checks passed
**Sections updated**: Summary, Changes Made, How to Verify It
**Sections preserved**: Reviewer Notes, Screenshots
**What changed**:
Updated: Summary, Backend Changes, Automated Checks
Preserved: Manual verification steps, Reviewer notes
Added: New validation section
Review updated PR: {url}
First generation:
<!-- Auto-generated: 2025-10-06T10:00:00Z -->
<!-- Last updated: 2025-10-06T10:00:00Z -->
<!-- PR: #123 -->
<!-- Previous commits: abc123,def456 -->
Subsequent updates:
<!-- Auto-generated: 2025-10-06T10:00:00Z -->
<!-- Last updated: 2025-10-06T15:30:00Z -->
<!-- PR: #123 -->
<!-- Previous commits: abc123,def456,ghi789,jkl012 -->
---
**Update History:**
- 2025-10-06 15:30: Added error handling, fixed tests (2 commits)
- 2025-10-06 10:00: Initial implementation (2 commits)
---
Each subsequent call detects new commits since the last description update, appends changes to the appropriate sections, reruns verification checks, preserves manual edits (reviewer notes, screenshots, checked boxes), and adds entries to the update history log.
Uses .catalyst/config.json:
{
"catalyst": {
"project": {
"ticketPrefix": "PROJ"
},
"linear": {
"teamKey": "PROJ",
"stateMap": {
"inReview": "In Review"
}
},
"pr": {
"testCommand": "make test",
"lintCommand": "make lint",
"buildCommand": "make build"
}
}
}
State names are read from stateMap with sensible defaults. See .catalyst/config.json for all
keys.
linearis skill referencetesting
Phase-agent that fixes a failing verify verdict so the pipeline self-heals instead of stalling to needs-human (CTL-653). Reads `${ORCH_DIR}/workers/<ticket>/verify.json`, fixes the `findings[]` (every severity:"high" plus the regression_risk drivers) directly via Edit/Write, commits the remediation, and emits `phase.remediate.complete.<ticket>`. The scheduler's router then re-dispatches `verify` to re-check (the verify⇄remediate cycle, cap 3). Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
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
tools
Phase agent for the research step of the 9-phase orchestrator pipeline (CTL-450). Wraps /catalyst-dev:research-codebase and produces thoughts/shared/research/<date>-<ticket>.md, then emits phase.research.complete.<ticket>. Reads triage.json from the worker dir as its prior-phase artifact. Spawned via plugins/dev/scripts/phase-agent-dispatch, which invokes it via slash command — hence `user-invocable: true`.
development
Phase-agent wrapper that opens the pull request after implementation completes (CTL-449 Initiative 1 Phase 3). Delegates to `/catalyst-dev:create-pr` (which already auto-runs `describe-pr` and transitions Linear to `inReview`), then writes the PR number + URL into the phase signal file so the downstream `phase-monitor-merge` agent can read it without re-querying GitHub. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.