diff --git a/workflows/jira-hygiene/.ambient/ambient.json b/workflows/jira-hygiene/.ambient/ambient.json new file mode 100644 index 0000000..2bdb1b8 --- /dev/null +++ b/workflows/jira-hygiene/.ambient/ambient.json @@ -0,0 +1,20 @@ +{ + "name": "Jira Hygiene", + "description": "Systematic workflow for maintaining Jira project hygiene. Links orphaned stories and epics, generates weekly activity summaries, closes stale tickets, suggests triage outcomes, and identifies data quality issues. Provides safe bulk operations with review-then-execute pattern.", + "systemPrompt": "You are a Jira hygiene specialist, helping teams maintain clean and well-organized Jira projects.\n\nWORKSPACE NAVIGATION:\n**CRITICAL: Follow these rules to avoid fumbling when looking for files.**\n\nStandard file locations (from workflow root):\n- Config: .ambient/ambient.json (ALWAYS at this path)\n- Commands: .claude/commands/*.md\n- Outputs: artifacts/jira-hygiene/\n\nTool selection rules:\n- Use Read for: Known paths, standard files, files you just created\n- Use Glob for: Discovery (finding multiple files by pattern)\n- Use Grep for: Content search\n\nNever glob for standard files:\n✅ DO: Read .ambient/ambient.json\n❌ DON'T: Glob **/ambient.json\n\nYour role is to:\n1. Maintain Jira project hygiene through systematic checks and bulk operations\n2. Link orphaned stories to epics and epics to initiatives\n3. Generate weekly activity summaries for epics and initiatives\n4. Identify and close stale tickets based on priority-specific thresholds\n5. Suggest triage outcomes for untriaged items\n6. Highlight data quality issues (missing assignees, activity types, blocking mismatches)\n7. Execute all bulk operations with review-then-execute pattern for safety\n\n## Available Commands\n\n**Setup & Configuration:**\n- `/hygiene.setup` - Validate Jira connection, configure project and initiative mapping\n\n**Linking Operations:**\n- `/hygiene.link-epics` - Link orphaned stories to epics (semantic matching, 50% threshold)\n- `/hygiene.link-initiatives` - Link orphaned epics to initiatives (cross-project search)\n\n**Activity & Reporting:**\n- `/hygiene.report` - Generate master hygiene report with health score and all checks\n- `/hygiene.activity-summary` - Generate weekly activity summaries for epics/initiatives (includes PR/MR activity)\n- `/hygiene.show-blocking` - Show tickets that are blocking other work via issue links\n\n**Bulk Operations:**\n- `/hygiene.close-stale` - Close stale tickets by priority (Highest/High: 1w, Medium: 2w, Low: 1m)\n- `/hygiene.triage-new` - Suggest triage for items in New status >1 week\n\n**Data Quality:**\n- `/hygiene.blocking-closed` - Find blocking tickets where blocked items are closed\n- `/hygiene.unassigned-progress` - Show in-progress tickets without assignee\n- `/hygiene.activity-type` - Suggest Activity Type for tickets missing this field\n\n## Jira API Integration\n\nAll commands use Jira REST API v3 with these environment variables:\n- `JIRA_URL` - Your Jira instance URL (e.g., https://company.atlassian.net)\n- `JIRA_EMAIL` - Your Jira email address\n- `JIRA_API_TOKEN` - Your Jira API token\n\nAuthentication: Basic Auth using base64(email:token)\nRate limiting: 0.5s delay between requests\nError handling: Retry on 429, validate all responses\n\n## Base JQL Configuration\n\nThe config file includes a `base_jql` field that customizes the default filter for all commands:\n\n**Structure**: `({base_jql}) AND {command_specific_filters}`\n\n**Example**:\n- Config: `\"base_jql\": \"project = MYPROJ AND resolution = Unresolved AND labels = backend\"`\n- Command: link-epics adds `AND issuetype = Story AND \"Epic Link\" is EMPTY`\n- Final JQL: `(project = MYPROJ AND resolution = Unresolved AND labels = backend) AND issuetype = Story AND \"Epic Link\" is EMPTY`\n\n**Usage rules**:\n- Apply base_jql to ALL primary queries (orphaned stories, stale tickets, blocking, etc.)\n- Do NOT apply to child queries (e.g., `parent = {EPIC_KEY}` should not include base_jql)\n- Do NOT apply to user-provided JQL in activity-summary (user has full control there)\n- For issueFunction queries, apply to both outer AND inner queries\n\n**Default**: If base_jql not in config, use `\"project = {PROJECT} AND resolution = Unresolved\"`\n\n## Pagination Rules\n\n**CRITICAL**: All Jira API queries must fetch ALL results using pagination. Never rely on default limits.\n\n**Standard pagination pattern**:\n```\nall_results = []\nstart_at = 0\nmax_results = 50\n\nwhile True:\n # Fetch page\n response = GET /rest/api/3/search?jql={jql}&startAt={start_at}&maxResults={max_results}\n \n # Extract results\n issues = response['issues']\n all_results.extend(issues)\n \n # Check if done\n total = response['total']\n if start_at + len(issues) >= total:\n break # All results fetched\n \n # Next page\n start_at += max_results\n \n # Rate limit\n sleep(0.5)\n```\n\n**When to paginate**:\n- ✅ Primary queries: orphaned stories, stale tickets, blocking tickets, untriaged\n- ✅ Semantic searches: text ~ \"keywords\" for linking operations\n- ✅ Multiple queries: close-stale has 5 queries (one per priority), paginate each\n- ✅ Nested queries: activity-summary fetches epics (paginate), then children per epic (paginate)\n- ✅ Child queries: `parent = {KEY}` should paginate for safety\n- ❌ Field metadata: `/rest/api/3/field` returns all in one call, no pagination needed\n\n**Progress indicators**:\n- Show: \"Fetched 50/237 orphaned stories...\" during pagination\n- Log: Include total_fetched and pages_processed in operation logs\n\n**Rate limiting**:\n- Maintain 0.5s delay between pages\n- If 429 response: increase delay to 1s, retry\n- Apply same delay to nested queries\n\n**Special cases**:\n\n1. **Multiple queries (close-stale)**:\n - Paginate each priority query separately\n - Example: Highest (3 pages), Medium (1 page), Low (5 pages)\n\n2. **Nested pagination (activity-summary)**:\n - Paginate parent query (e.g., fetch all epics)\n - For each parent, paginate child query\n - Example: 150 epics × (avg 30 children each) = paginate both levels\n\n3. **Issue functions**:\n ```\n # Apply base_jql to outer query AND inner query\n ({base_jql}) AND issueFunction in linkedIssuesOf(\"({base_jql})\", \"blocks\")\n ```\n\n4. **Cross-project searches (link-initiatives)**:\n ```\n # Initiative search uses different project list\n project in ({INIT1},{INIT2}) AND issuetype = Initiative AND text ~ \"keywords\"\n # Still paginate, but base_jql not applicable (different projects)\n ```\n\n## Safety & Best Practices\n\n**Review-then-execute pattern:**\n1. Query and analyze tickets\n2. Write candidates to artifacts/jira-hygiene/candidates/\n3. Display summary to user\n4. Ask for explicit confirmation\n5. Execute operations only after confirmation\n6. Log all operations with timestamps to artifacts/jira-hygiene/operations/\n\n**Key safety rules:**\n- No destructive operations without confirmation\n- No modification of closed tickets (only unresolved)\n- Validate JQL queries before execution\n- Log all operations for audit trail\n- Respect rate limits (0.5s minimum between requests)\n- No sensitive data in logs (redact API tokens)\n- All operations are idempotent (safe to run multiple times)\n- No cross-project operations without explicit mapping\n\n**Dry-run support:**\nAll bulk commands support `--dry-run` flag to show what would happen without making changes.\n\n## Output Locations\n\nAll artifacts are written to `artifacts/jira-hygiene/`:\n- `config.json` - Project configuration and field metadata cache\n- `candidates/*.json` - Review candidates before bulk operations\n- `summaries/{epic-key}-{date}.md` - Generated activity summaries\n- `reports/*.md` - Read-only reports for data quality issues\n- `operations/*-{timestamp}.log` - Audit logs for all executed operations\n\n## Semantic Matching Algorithm\n\nFor linking and triage suggestions:\n1. Extract keywords from ticket summary/description (remove stopwords)\n2. Search using Jira text search: `text ~ \"keyword1 keyword2\"`\n3. Calculate match score: (matching_keywords / total_keywords) * 100\n4. Rank by score, suggest top matches\n5. Threshold: ≥50% = auto-suggest, <50% = suggest creating new item\n\n## Common JQL Patterns\n\nOrphaned stories: `project = PROJ AND issuetype = Story AND \"Epic Link\" is EMPTY`\nOrphaned epics: `project = PROJ AND issuetype = Epic AND \"Parent Link\" is EMPTY`\nStale tickets: `project = PROJ AND priority = PRIORITY AND updated < -Nd AND resolution = Unresolved`\nUntriaged: `project = PROJ AND status = New AND created < -7d`\nBlocking tickets: `project = PROJ AND issueFunction in linkedIssuesOf(\"project = PROJ\", \"blocks\") AND resolution = Unresolved`\nIn-progress unassigned: `project = PROJ AND status = \"In Progress\" AND assignee is EMPTY`\n\nBe helpful, efficient, and always prioritize safety in bulk operations.", + "startupPrompt": "Greet the user and introduce yourself as their Jira hygiene assistant. Explain that you help maintain clean Jira projects through automated hygiene checks and safe bulk operations. Mention the key capabilities: linking orphaned tickets, generating activity summaries, closing stale items, and identifying data quality issues. Suggest starting with `/hygiene.setup` to configure the Jira connection and project settings, or ask what hygiene task they'd like to address.", + "results": { + "Configuration": "artifacts/jira-hygiene/config.json", + "Link Epics Candidates": "artifacts/jira-hygiene/candidates/link-epics.json", + "Link Initiatives Candidates": "artifacts/jira-hygiene/candidates/link-initiatives.json", + "Close Stale Candidates": "artifacts/jira-hygiene/candidates/close-stale.json", + "Triage Candidates": "artifacts/jira-hygiene/candidates/triage-new.json", + "Activity Type Candidates": "artifacts/jira-hygiene/candidates/activity-type.json", + "Activity Summaries": "artifacts/jira-hygiene/summaries/*.md", + "Blocking Tickets Report": "artifacts/jira-hygiene/reports/blocking-tickets.md", + "Blocking-Closed Mismatch Report": "artifacts/jira-hygiene/reports/blocking-closed-mismatch.md", + "Unassigned Progress Report": "artifacts/jira-hygiene/reports/unassigned-progress.md", + "Operation Logs": "artifacts/jira-hygiene/operations/*.log", + "Master Hygiene Report": "artifacts/jira-hygiene/reports/master-report.md" + } +} diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.activity-summary.md b/workflows/jira-hygiene/.claude/commands/hygiene.activity-summary.md new file mode 100644 index 0000000..e829c12 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.activity-summary.md @@ -0,0 +1,294 @@ +# /hygiene.activity-summary - Generate Weekly Activity Summaries + +## Purpose + +Generate weekly activity summaries for selected epics and initiatives by analyzing changes and comments on child items from the past 7 days, then post summaries as comments. + +## Prerequisites + +- `/hygiene.setup` must be run first +- User should specify which epics/initiatives to summarize + +**Optional** (for enhanced PR/MR summaries): +- `GITHUB_TOKEN` - For direct GitHub API access if Jira integration unavailable +- `GITLAB_TOKEN` - For direct GitLab API access if Jira integration unavailable + +## Arguments + +Optional: +- `--dry-run` - Show summaries without posting them as comments (runs steps 1-4 only) + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract project key + +2. **Prompt for selection**: + - Ask user which epics/initiatives to summarize + - Options: + - Provide specific issue keys (comma-separated) + - Provide JQL filter (e.g., "project = PROJ AND issuetype = Epic") + - Use "all active epics" (default: all unresolved epics in project) + - **Always enforce unresolved scope**: Append "AND resolution = Unresolved" to any user-provided JQL + +3. **Fetch selected epics/initiatives WITH PAGINATION**: + - Execute JQL query to get target issues + - **If user provided specific keys**: Fetch each key and filter to only include issues where `fields.resolution == null` + - **If user provided JQL**: Already enforced "AND resolution = Unresolved" in step 2 + + **Pagination logic** (if using JQL filter): + ``` + all_epics = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={user_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,issuetype + epics = response['issues'] + all_epics.extend(epics) + + Print: "Fetched {start_at + len(epics)}/{response['total']} epics/initiatives..." + + if start_at + len(epics) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, issuetype + +4. **For each epic/initiative**: + + a. **Fetch child issues WITH PAGINATION** (logic varies by issue type): + + **If issue type is Initiative**: + 1. First fetch child Epics: + ```jql + parent = {INITIATIVE_KEY} AND resolution = Unresolved + ``` + Use pagination (max 50 per page) to fetch all child epics + + 2. Then for each child Epic, fetch its child issues: + ```jql + parent = {EPIC_KEY} AND resolution = Unresolved + ``` + Use pagination for each epic's children + + **If issue type is Epic**: + - Directly fetch child issues: + ```jql + parent = {EPIC_KEY} AND resolution = Unresolved + ``` + Use pagination to fetch all children + + **Note**: Child queries do NOT use base_jql (children can cross project boundaries) + + **Pagination logic** (apply to both Initiative→Epics and Epic→Children): + ``` + all_children = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={child_jql}&startAt={start_at}&maxResults={max_results} + children = response['issues'] + all_children.extend(children) + + Print: "Fetched {len(all_children)}/{response['total']} children for {PARENT_KEY}..." + + if start_at + len(children) >= response['total']: + break + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Get all child items (not limited to 50) + - Then analyze activity for ALL children across all levels + + b. **Analyze activity for each child** (past 7 days): + - Fetch changelog: GET `/rest/api/3/issue/{childKey}/changelog` + - Filter changes where created >= (now - 7 days) + - Extract: + - Status transitions (e.g., "New" → "In Progress") + - Assignee changes + - Priority changes + - Fetch comments: GET `/rest/api/3/issue/{childKey}/comment` + - Count comments from past 7 days + + **Also check for linked MRs/PRs**: + - Fetch development info: GET `/rest/dev-status/1.0/issue/detail?issueId={issueId}&applicationType=github&dataType=pullrequest` + - Also check GitLab: `applicationType=gitlab&dataType=mergerequest` + - Parse PR/MR URLs from comments and description + - For each linked PR/MR with activity in past 7 days: + - Fetch PR details from GitHub/GitLab API + - Extract: status (open/merged/closed), commits added, reviews, last updated + - Note: PR/MR must have `updated_at` within past 7 days to include + + c. **Generate summary paragraph**: + - Template: "This week, {status_summary}. {pr_summary}. {assignment_summary}. {activity_summary}." + - Status summary: "X stories moved to In Progress, Y completed" + - PR/MR summary: "Z pull requests merged, N in review" (if any PR/MR activity) + - Assignment summary: "M new assignments" (if any) + - Activity summary: "P comments across Q stories" (if significant) + - Keep to 3-5 sentences, business-friendly language + - Prioritize PR/MR activity in summary (shows concrete progress) + + d. **Write summary to file**: + - Save to `artifacts/jira-hygiene/summaries/{epic-key}-{date}.md` + - Include metadata: epic key, date range, child count + +5. **Display all summaries**: + - Show generated summaries for review + - Format as markdown with epic key as header + +6. **Ask for confirmation**: + - If `--dry-run`: Display "DRY RUN - Summaries generated but not posted" and skip to step 8 + - Otherwise prompt: "Post these summaries as comments? (yes/no)" + - Allow user to edit summaries before posting + +7. **Post summaries** (skip if --dry-run): + - For each epic/initiative: + - POST `/rest/api/3/issue/{epicKey}/comment` + - Body: `{"body": "Weekly Activity Summary (YYYY-MM-DD):\n\n{summary_text}"}` + - Rate limit: 0.5s between requests + +8. **Log results**: + - Write to `artifacts/jira-hygiene/operations/activity-summary-{timestamp}.log` + - In --dry-run mode, log "DRY RUN - no comments posted" + +## Output + +- `artifacts/jira-hygiene/summaries/{epic-key}-{date}.md` (one file per epic) +- `artifacts/jira-hygiene/operations/activity-summary-{timestamp}.log` + +## Example Summary + +**EPIC-45-2026-04-07.md**: +```markdown +# Weekly Activity Summary: EPIC-45 Authentication System +**Date Range**: 2026-03-31 to 2026-04-07 +**Child Issues**: 8 stories + +## Summary + +This week, 3 stories moved to In Progress and 2 were completed. The team merged 2 pull requests and has 3 PRs in active review. There were 4 new assignments and 12 comments discussing API integration challenges and OAuth implementation details. + +## Activity Breakdown + +- Status transitions: 5 changes + - New → In Progress: STORY-101, STORY-102, STORY-103 + - In Progress → Done: STORY-98, STORY-99 +- Pull Requests: 5 active + - Merged: PR#145 (OAuth integration), PR#148 (Token refresh) + - In Review: PR#150 (SSO support), PR#151 (Session management), PR#152 (Password reset) + - Commits this week: 18 commits across 5 PRs +- Assignments: 4 new +- Comments: 12 across 6 stories +``` + +## Summary Generation Guidelines + +**Good summary**: +> "This week, 3 stories moved to In Progress and 2 were completed. The team merged 2 pull requests for OAuth integration and has 3 PRs in active review. There were 4 new assignments and 8 comments focused on implementation details." + +**Bad summary** (too technical): +> "This week, STORY-101 transitioned from status ID 10001 to 10002. User john.doe was assigned to STORY-102. Commit SHA abc123 was pushed to PR #145..." + +**Focus on**: +- High-level progress (stories moved, completed) +- PR/MR activity (merged, in review, commit volume) +- Team activity (assignments, discussions) +- Notable trends (if detectable) + +**Avoid**: +- Listing every ticket key +- Commit SHAs or technical identifiers +- Implementation details +- Individual developer names (use "the team") + +**PR/MR Details to Include**: +- Number merged vs in review +- PR titles (if descriptive, e.g., "OAuth integration") +- Significant milestones (e.g., "first PR merged this epic") +- Overall commit volume (e.g., "18 commits this week") + +**PR/MR Details to Exclude**: +- Commit messages +- Code review comments +- Individual file changes +- Specific reviewers + +## Error Handling + +- **No child issues**: Note "No active child issues" in summary +- **No activity**: "No significant activity this week" +- **Changelog unavailable**: Fall back to issue update dates +- **Comment fetch failed**: Skip comment count, note in log +- **Development info unavailable**: Not all Jira instances have GitHub/GitLab integration; skip PR/MR section +- **PR/MR API access denied**: May need GitHub/GitLab tokens; proceed without PR/MR data + +## GitHub/GitLab Integration + +### Jira Development Panel API + +**Endpoint**: `/rest/dev-status/1.0/issue/detail?issueId={issueId}&applicationType={type}&dataType={dataType}` + +**Supported integrations**: +- GitHub: `applicationType=github&dataType=pullrequest` +- GitLab: `applicationType=gitlab&dataType=mergerequest` +- Bitbucket: `applicationType=bitbucket&dataType=pullrequest` + +**Response includes**: +- PR/MR URLs +- Status (open, merged, closed) +- Last updated timestamp +- Review status + +### GitHub API (if direct access needed) + +**Environment variables** (optional): +- `GITHUB_TOKEN` - GitHub personal access token +- `GITHUB_API_URL` - Default: https://api.github.com + +**Endpoint**: `GET /repos/{owner}/{repo}/pulls/{number}` + +**Fetch**: +- `state` (open, closed) +- `merged_at` (if merged) +- `updated_at` (filter by this) +- `commits` count +- `additions`, `deletions` (code churn) +- `reviews` count + +### GitLab API (if direct access needed) + +**Environment variables** (optional): +- `GITLAB_TOKEN` - GitLab personal access token +- `GITLAB_API_URL` - Default: https://gitlab.com/api/v4 + +**Endpoint**: `GET /projects/{id}/merge_requests/{iid}` + +**Fetch**: +- `state` (opened, merged, closed) +- `merged_at` (if merged) +- `updated_at` (filter by this) +- `user_notes_count` (comments) + +### Date Filtering + +Only include PR/MR in summary if: +- `updated_at` >= (now - 7 days) +- OR `merged_at` >= (now - 7 days) + +This ensures only recent PR/MR activity is included in weekly summary. + +### Fallback: Parse URLs from Comments + +If Jira development panel is unavailable: +1. Search issue comments for GitHub/GitLab URLs +2. Extract PR/MR numbers from URLs (e.g., `/pull/123`, `/merge_requests/456`) +3. Fetch details directly from GitHub/GitLab API +4. Filter by update date diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.activity-type.md b/workflows/jira-hygiene/.claude/commands/hygiene.activity-type.md new file mode 100644 index 0000000..645bdb0 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.activity-type.md @@ -0,0 +1,235 @@ +# /hygiene.activity-type - Suggest Activity Type for Tickets + +## Purpose + +Find tickets missing the "Activity Type" custom field value and suggest appropriate values based on semantic analysis of the ticket content. + +## Prerequisites + +- `/hygiene.setup` must be run first +- Activity Type field must be configured in config.json + +## Arguments + +Optional: +- `--dry-run` - Show suggestions without making changes + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql, Activity Type field ID, and available values + - If Activity Type field is not configured: prompt user to run `/hygiene.setup` + +2. **Query tickets missing Activity Type WITH PAGINATION**: + ```jql + ({base_jql}) AND "{ACTIVITY_TYPE_FIELD_ID}" is EMPTY + ``` + + **Pagination logic**: + ``` + all_tickets = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,description,issuetype + tickets = response['issues'] + all_tickets.extend(tickets) + + Print: "Fetched {start_at + len(tickets)}/{response['total']} tickets missing Activity Type..." + + if start_at + len(tickets) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, description, issuetype + - If none found: report success and exit + +3. **For each ticket**: + + a. **Analyze ticket content**: + - Combine summary + description + issuetype + - Extract key terms and phrases + + b. **Match against available Activity Type values**: + - For each available value (e.g., "Development", "Bug Fix", "Documentation", "Research", "Testing") + - Define keyword mappings: + - **Development**: implement, create, add, build, develop, feature, enhance + - **Bug Fix**: fix, bug, error, issue, broken, crash, defect + - **Documentation**: document, doc, readme, guide, wiki, manual + - **Research**: research, investigate, explore, spike, POC, prototype, feasibility + - **Testing**: test, QA, quality, verify, validate, automation + - Count keyword matches for each activity type + - Suggest activity type with highest match count + + c. **Consider issue type**: + - If issuetype = "Bug" → increase "Bug Fix" score + - If issuetype = "Task" and keywords unclear → default to "Development" + - If issuetype = "Story" → likely "Development" unless keywords suggest otherwise + + d. **Assign confidence**: + - High: Clear keywords match (≥3 keyword hits) + - Medium: Some keywords match (1-2 keyword hits) + - Low: No keywords, using issuetype heuristic + +4. **Write candidates file**: + - Save to `artifacts/jira-hygiene/candidates/activity-type.json` + - Include: key, summary, suggested activity type, confidence, matching keywords + +5. **Display summary with Jira links**: + ``` + Found N tickets missing Activity Type: + + High confidence (≥3 keyword matches): 12 tickets + • [{PROJ-100}]({JIRA_URL}/browse/PROJ-100) "Fix broken login flow" + → Bug Fix (keywords: fix, broken, bug) + • [{PROJ-101}]({JIRA_URL}/browse/PROJ-101) "Document API endpoints" + → Documentation (keywords: document, API, guide) + + Medium confidence (1-2 matches): 5 tickets + • [{PROJ-102}]({JIRA_URL}/browse/PROJ-102) "Improve performance" + → Development (keywords: improve) + + Low confidence (issuetype heuristic): 3 tickets + • [{PROJ-103}]({JIRA_URL}/browse/PROJ-103) "Update system" + → Development (Bug issuetype suggests bug fix, but no clear keywords) + ``` + +6. **Ask for confirmation** (batch mode): + - If `--dry-run`: Skip, display "DRY RUN - No changes made" + - Otherwise, split approved tickets into batches of max 50 + - For each batch, prompt: "Apply Activity Type suggestions for {N} tickets? (yes/no/high-confidence-only)" + - Only proceed on exact response "yes" (reject other responses) + - If "high-confidence-only": apply only tickets with ≥3 keyword matches + +7. **Execute updates** (per batch): + - For each approved ticket in the current batch: + - Update custom field via PUT `/rest/api/3/issue/{key}` + - Payload: `{"fields": {"{FIELD_ID}": {"value": "{ACTIVITY_TYPE}"}}}` + - Rate limit: 0.5s between tickets + +8. **Log results**: + - Write to `artifacts/jira-hygiene/operations/activity-type-{timestamp}.log` + +## Output + +- `artifacts/jira-hygiene/candidates/activity-type.json` +- `artifacts/jira-hygiene/operations/activity-type-{timestamp}.log` + +## Example Candidates JSON + +```json +[ + { + "key": "PROJ-100", + "summary": "Fix broken login flow after OAuth upgrade", + "description_snippet": "Users cannot log in after OAuth upgrade...", + "issuetype": "Bug", + "suggested_activity_type": "Bug Fix", + "confidence": "high", + "matching_keywords": ["fix", "broken", "bug", "login"], + "keyword_scores": { + "Bug Fix": 4, + "Development": 1, + "Testing": 0, + "Documentation": 0, + "Research": 0 + } + }, + { + "key": "PROJ-101", + "summary": "Document new API endpoints for partner integration", + "description_snippet": "Need to create documentation for...", + "issuetype": "Task", + "suggested_activity_type": "Documentation", + "confidence": "high", + "matching_keywords": ["document", "documentation", "api", "create"], + "keyword_scores": { + "Documentation": 4, + "Development": 1, + "Bug Fix": 0, + "Testing": 0, + "Research": 0 + } + }, + { + "key": "PROJ-103", + "summary": "Update user permissions", + "description_snippet": "", + "issuetype": "Task", + "suggested_activity_type": "Development", + "confidence": "low", + "matching_keywords": [], + "keyword_scores": { + "Development": 0, + "Bug Fix": 0, + "Documentation": 0, + "Testing": 0, + "Research": 0 + }, + "note": "No clear keywords, defaulting to Development based on Task issuetype" + } +] +``` + +## Keyword Mapping + +Default keyword mappings (can be customized based on your available Activity Type values): + +**Development**: +- implement, create, add, build, develop, feature, enhance, new, update, upgrade, refactor + +**Bug Fix**: +- fix, bug, error, issue, broken, crash, defect, problem, failure, incorrect + +**Documentation**: +- document, doc, readme, guide, wiki, manual, write, specification, help + +**Research**: +- research, investigate, explore, spike, POC, proof of concept, prototype, feasibility, study + +**Testing**: +- test, QA, quality, verify, validate, automation, check, coverage, suite + +**Custom Values**: +If your project has custom Activity Type values, you'll need to define keyword mappings for them or update the semantic matching logic. + +## Custom Field Update Payload + +Activity Type is typically a **select** custom field. The update payload varies by field type: + +**Single select**: +```json +{ + "fields": { + "customfield_10050": { + "value": "Bug Fix" + } + } +} +``` + +**Multi-select** (if configured to allow multiple): +```json +{ + "fields": { + "customfield_10050": [ + {"value": "Bug Fix"}, + {"value": "Testing"} + ] + } +} +``` + +This workflow assumes single-select by default. + +## Error Handling + +- **Activity Type field not configured**: Prompt to run `/hygiene.setup` +- **Field ID changed**: Re-fetch field metadata if update fails with 400 +- **Invalid value**: If suggested value is not in allowed values list, log error and skip +- **Permission denied**: User may not have permission to edit custom fields; log and continue diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.blocking-closed.md b/workflows/jira-hygiene/.claude/commands/hygiene.blocking-closed.md new file mode 100644 index 0000000..0ed4edc --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.blocking-closed.md @@ -0,0 +1,175 @@ +# /hygiene.blocking-closed - Find Blocking Tickets with Closed Dependencies + +## Purpose + +Highlight tickets marked as "Blocking" where all of the blocked tickets are already closed. Suggests either closing the blocking ticket or removing the link. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql + +2. **Query tickets with "blocks" links WITH PAGINATION**: + ```jql + ({base_jql}) AND issueFunction in linkedIssuesOf("({base_jql})", "blocks") + ``` + + **Note**: Both outer query AND inner linkedIssuesOf query use base_jql for consistency + + **Pagination logic**: + ``` + all_blocking_tickets = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,status + tickets = response['issues'] + all_blocking_tickets.extend(tickets) + + Print: "Fetched {start_at + len(tickets)}/{response['total']} blocking tickets..." + + if start_at + len(tickets) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - This finds all unresolved tickets that block other tickets + - Fetch: key, summary, status + +3. **For each blocking ticket**: + + a. **Fetch issue links**: + - GET `/rest/api/3/issue/{key}?fields=issuelinks` + - Extract all "outward" links of type "blocks" + - Get blocked ticket keys + + b. **Check resolution status of blocked tickets**: + - For each blocked ticket, GET `/rest/api/3/issue/{blockedKey}?fields=resolution` + - Check if resolution is not null (ticket is closed) + + c. **Determine if mismatch exists**: + - If ALL blocked tickets are closed: flag for review + - If at least one blocked ticket is open: skip (still validly blocking) + +4. **Write report with Jira links**: + - Save to `artifacts/jira-hygiene/reports/blocking-closed-mismatch.md` + - Include: blocking ticket key, summary, list of closed blocked tickets + - Format ticket keys as clickable links: `[{KEY}]({JIRA_URL}/browse/{KEY})` + - Include search link at top to view all blocking tickets in Jira + +5. **Display report with Jira links**: + ``` + Found N blocking tickets where all dependencies are closed: + + [{PROJ-123}]({JIRA_URL}/browse/PROJ-123) "Fix database migration issue" + Blocks (all closed): + - [{PROJ-145}]({JIRA_URL}/browse/PROJ-145) "Deploy new schema" (Closed 5 days ago) + - [{PROJ-167}]({JIRA_URL}/browse/PROJ-167) "Update migration scripts" (Closed 3 days ago) + + Suggested action: Close PROJ-123 or remove blocking links + + [{PROJ-456}]({JIRA_URL}/browse/PROJ-456) "Security audit blocker" + Blocks (all closed): + - [{PROJ-500}]({JIRA_URL}/browse/PROJ-500) "Implement OAuth" (Closed 2 weeks ago) + + Suggested action: Close PROJ-456 or remove blocking link + + Full report: artifacts/jira-hygiene/reports/blocking-closed-mismatch.md + ``` + +6. **No bulk operation**: + - This command is **report-only** (no automatic changes) + - User must manually review each case + - Closing or unlinking requires human judgment + +## Output + +- `artifacts/jira-hygiene/reports/blocking-closed-mismatch.md` + +## Example Report + +```markdown +# Blocking Tickets with Closed Dependencies + +**Project**: PROJ +**Generated**: 2026-04-07 10:30 UTC +**Total Mismatches**: 3 tickets +**[View all blocking tickets in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+issueFunction+in+linkedIssuesOf%28%22project+%3D+PROJ%22%2C+%22blocks%22%29+AND+resolution+%3D+Unresolved)** + +## Summary + +Found 3 tickets that are still marked as blocking, but all blocked items are already closed. These may be ready to close or the blocking links should be removed. + +## Tickets Requiring Review + +### [PROJ-123](https://company.atlassian.net/browse/PROJ-123) "Fix database migration issue" + +**Status**: In Progress +**Last Updated**: 2026-03-25 + +**Blocks** (all closed): +- [PROJ-145](https://company.atlassian.net/browse/PROJ-145) "Deploy new schema" (Closed: 2026-04-02, 5 days ago) +- [PROJ-167](https://company.atlassian.net/browse/PROJ-167) "Update migration scripts" (Closed: 2026-04-04, 3 days ago) + +**Suggested Actions**: +1. If migration issue is resolved: Close [PROJ-123](https://company.atlassian.net/browse/PROJ-123) +2. If new blockers emerged: Update links to reflect current blockers +3. If no longer blocking: Remove the "blocks" links + +--- + +### [PROJ-456](https://company.atlassian.net/browse/PROJ-456) "Security audit blocker" + +**Status**: To Do +**Last Updated**: 2026-03-15 + +**Blocks** (all closed): +- [PROJ-500](https://company.atlassian.net/browse/PROJ-500) "Implement OAuth" (Closed: 2026-03-24, 14 days ago) + +**Suggested Actions**: +1. If audit is complete: Close [PROJ-456](https://company.atlassian.net/browse/PROJ-456) +2. If audit revealed new work: Create new tickets and update links +3. If audit was cancelled: Close [PROJ-456](https://company.atlassian.net/browse/PROJ-456) + +--- + +## Recommendations + +These tickets require manual review because: +- The blocking ticket may represent ongoing work not yet tracked +- New dependencies may have emerged +- The ticket may have served its purpose and should be closed + +**Action Required**: Review each ticket and either close it or update its blocking relationships. +``` + +## Link Type Detection + +Jira uses different link types for blocking relationships: +- "Blocks" / "is blocked by" +- "Blocker" (custom link type in some instances) + +This command checks for the standard "Blocks" link type. If your project uses custom link types, the field ID may need adjustment. + +## Why No Bulk Operation? + +Unlike other hygiene commands, this one doesn't offer bulk closure because: + +1. **Context required**: Need to understand why the blocker exists +2. **May still be valid**: Blocker work may not be tracked in Jira +3. **Risk of data loss**: Automatically removing links could lose important context +4. **Manual judgment needed**: Each case is unique + +## Error Handling + +- **Issue links unavailable**: Some tickets may have restricted access; skip and log +- **Blocked ticket not found (404)**: Ticket may have been deleted; note in report +- **No blocking links found**: Report "No blocking relationships found in {PROJECT}" diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.close-stale.md b/workflows/jira-hygiene/.claude/commands/hygiene.close-stale.md new file mode 100644 index 0000000..11a64d5 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.close-stale.md @@ -0,0 +1,172 @@ +# /hygiene.close-stale - Close Stale Tickets + +## Purpose + +Find and close stale tickets in bulk based on priority-specific thresholds. Groups candidates by priority for user review before closing. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Arguments + +Optional: Override default thresholds +- `--highest ` - Threshold for Highest priority (default: 7) +- `--high ` - Threshold for High priority (default: 7) +- `--medium ` - Threshold for Medium priority (default: 14) +- `--low ` - Threshold for Low priority (default: 30) +- `--lowest ` - Threshold for Lowest priority (default: 30) +- `--dry-run` - Show what would be closed without making changes + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql and staleness_thresholds + - Apply any command-line overrides + +2. **Query stale tickets by priority WITH PAGINATION**: + + For each priority level (Highest, High, Medium, Low, Lowest): + ```jql + ({base_jql}) AND priority = {PRIORITY} AND updated < -{DAYS}d + ``` + + **Pagination per priority**: + ``` + all_stale = {} + + for priority in ["Highest", "High", "Medium", "Low", "Lowest"]: + days = staleness_thresholds[priority] + jql = f"({base_jql}) AND priority = {priority} AND updated < -{days}d" + + priority_tickets = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,assignee,status,updated,priority + tickets = response['issues'] + priority_tickets.extend(tickets) + + Print: "Fetched {len(priority_tickets)}/{response['total']} {priority} priority tickets..." + + if start_at + len(tickets) >= response['total']: + break + + start_at += max_results + sleep(0.5) + + all_stale[priority] = priority_tickets + ``` + + - Fetch: key, summary, assignee, status, updated, priority + - Group results by priority + +3. **Write candidates file**: + - Save to `artifacts/jira-hygiene/candidates/close-stale.json` + - Include: key, summary, priority, days since update, assignee + +4. **Display summary by priority with Jira links**: + ``` + Found N stale tickets to close: + + Highest/High (>7 days): 3 tickets + • [{PROJ-100}]({JIRA_URL}/browse/PROJ-100) "Old critical bug" (12 days, assigned to John) + • [{PROJ-101}]({JIRA_URL}/browse/PROJ-101) "High priority feature" (9 days, unassigned) + • [{PROJ-102}]({JIRA_URL}/browse/PROJ-102) "Urgent fix needed" (8 days, assigned to Jane) + + Medium (>14 days): 5 tickets + ... + + Low/Lowest (>30 days): 12 tickets + ... + + Total: 20 tickets will be closed + + View all candidates: See artifacts/jira-hygiene/candidates/close-stale.json + ``` + +5. **Ask for confirmation** (batch mode): + - If `--dry-run`: Skip this step, display "DRY RUN - No changes made" + - Otherwise prompt: "Close these stale tickets? (yes/no/by-priority)" + - "by-priority": Let user approve each priority group separately + - **Batch limit**: Split each priority group into batches of max 50 tickets + - For each batch, require explicit "yes" response to proceed (deny other responses) + +6. **Execute closure** (per batch): + - For each approved ticket in current batch: + - Add comment: "Due to lack of activity, this item has been closed. If you feel that it should be addressed, please reopen it." + - Transition to "Closed" or "Done" status (use project's closed status) + - POST `/rest/api/3/issue/{key}/comment` then POST `/rest/api/3/issue/{key}/transitions` + - Rate limit: 0.5s between tickets + +7. **Log results**: + - Write to `artifacts/jira-hygiene/operations/close-stale-{timestamp}.log` + - Include: timestamp, key, priority, days stale, result (success/error) + +## Output + +- `artifacts/jira-hygiene/candidates/close-stale.json` +- `artifacts/jira-hygiene/operations/close-stale-{timestamp}.log` + +## Example Candidates JSON + +```json +{ + "thresholds": { + "Highest": 7, + "High": 7, + "Medium": 14, + "Low": 30, + "Lowest": 30 + }, + "candidates_by_priority": { + "Highest": [ + { + "key": "PROJ-100", + "summary": "Old critical bug in payment flow", + "priority": "Highest", + "days_stale": 12, + "last_updated": "2026-03-26", + "assignee": "John Doe", + "status": "In Progress" + } + ], + "Medium": [...], + "Low": [...] + }, + "total_count": 20 +} +``` + +## Staleness Calculation + +**Days stale** = Days since last update (not created date) + +Last update includes: +- Status changes +- Comments +- Field updates +- Assignee changes + +If a ticket has recent activity, it's not stale (even if created long ago). + +## Closure Message + +Standard message posted as comment before closing: + +> Due to lack of activity, this item has been closed. If you feel that it should be addressed, please reopen it. + +This message: +- Is polite and non-judgmental +- Acknowledges the ticket may still be valid +- Provides clear action (reopen if needed) +- Doesn't assign blame + +## Error Handling + +- **Transition failed**: Some tickets may not have "Close" transition; try "Done", then "Resolved" +- **No permission**: Log error, skip ticket, continue with others +- **Ticket already closed**: Skip silently (idempotent) +- **Rate limit**: Increase delay to 1s, retry diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.link-epics.md b/workflows/jira-hygiene/.claude/commands/hygiene.link-epics.md new file mode 100644 index 0000000..d3d417c --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.link-epics.md @@ -0,0 +1,170 @@ +# /hygiene.link-epics - Link Orphaned Stories to Epics + +## Purpose + +Find stories without epic links and suggest appropriate epics to link them to, using semantic matching. If no good match exists (score <50%), suggest creating a new epic. + +## Prerequisites + +- `/hygiene.setup` must be run first to create `artifacts/jira-hygiene/config.json` +- Project key must be configured + +## Arguments + +Optional: +- `--dry-run` - Run steps 1-4 (Query, Analyze, Save, Display) only, skip confirmation and API mutations + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql + +2. **Query orphaned stories WITH PAGINATION**: + ```jql + ({base_jql}) AND issuetype = Story AND "Epic Link" is EMPTY + ``` + + **Pagination logic**: + ``` + all_orphaned_stories = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,description + stories = response['issues'] + all_orphaned_stories.extend(stories) + + Print: "Fetched {start_at + len(stories)}/{response['total']} orphaned stories..." + + if start_at + len(stories) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, description + - If none found: report success and exit + +3. **For each orphaned story**: + + a. **Extract keywords**: + - Combine summary + description + - Remove stopwords (the, a, an, is, for, to, with, in, on, at, etc.) + - Keep technical terms (API, auth, payment, database, etc.) + - Lowercase and deduplicate + + b. **Search for matching epics WITH PAGINATION**: + ```jql + ({base_jql}) AND issuetype = Epic AND text ~ "keyword1 keyword2 keyword3" + ``` + + **Pagination for semantic search**: + ``` + matching_epics = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={search_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary + epics = response['issues'] + matching_epics.extend(epics) + + if start_at + len(epics) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Start with all keywords; if no results, try top 3 keywords + - Fetch: key, summary + - Calculate match scores for ALL epics returned (not just first 50) + + c. **Calculate match scores**: + - For each epic found, count keywords that appear in epic summary + - Score = (matching_keywords / total_keywords) * 100 + - Sort by score descending + + d. **Determine suggestion**: + - If best score ≥50%: suggest linking to top epic + - If best score <50%: suggest creating new epic + - If no epics found: suggest creating new epic + +4. **Write candidates file**: + - Save to `artifacts/jira-hygiene/candidates/link-epics.json` + - Include: story key, story summary, suggested action, epic key (if linking), match score + +5. **Display summary with Jira links**: + ``` + Found N orphaned stories: + - M stories with good matches (≥50%) + - P stories need new epics (<50% match) + + View orphaned stories: {JIRA_URL}/issues/?jql=project+%3D+{PROJECT}+AND+issuetype+%3D+Story+AND+%22Epic+Link%22+is+EMPTY + + Top suggestions: + • [{STORY-123}]({JIRA_URL}/browse/STORY-123) "Implement user login" + → [{EPIC-45}]({JIRA_URL}/browse/EPIC-45) "Authentication System" (75% match) + • [{STORY-124}]({JIRA_URL}/browse/STORY-124) "Add payment gateway" + → Create new epic (0% match) + ``` + +6. **Ask for confirmation**: + - If `--dry-run`: Display "DRY RUN - No changes made" and skip to step 8 + - Otherwise prompt: "Apply these suggestions? (yes/no/show-details)" + - If "show-details": display full candidate list with match details + - If "no": exit without changes + - If "yes": proceed to execution + +7. **Execute linking operations** (skip if --dry-run): + - For each approved linking suggestion: + - **TOCTOU check**: GET `/rest/api/3/issue/{storyKey}?fields=customfield_epic_link` to verify Epic Link is still empty + - If Epic Link is not empty: skip and log "Story already linked to {existing_epic}" + - Otherwise, update story via PUT `/rest/api/3/issue/{storyKey}` + - Set Epic Link field (typically using "update" operation) + - Rate limit: 0.5s between requests + - For "create epic" suggestions: skip for now, just log recommendation + +8. **Log results**: + - Write to `artifacts/jira-hygiene/operations/link-epics-{timestamp}.log` + - Include: timestamp, story key, action taken, result + +## Output + +- `artifacts/jira-hygiene/candidates/link-epics.json` +- `artifacts/jira-hygiene/operations/link-epics-{timestamp}.log` + +## Example Candidates JSON + +```json +[ + { + "story_key": "STORY-123", + "story_summary": "Implement user login functionality", + "keywords": ["implement", "user", "login", "functionality"], + "suggestion": "link", + "epic_key": "EPIC-45", + "epic_summary": "Authentication System", + "match_score": 75, + "matching_keywords": ["user", "login", "authentication"] + }, + { + "story_key": "STORY-124", + "story_summary": "Add payment gateway integration", + "keywords": ["add", "payment", "gateway", "integration"], + "suggestion": "create_epic", + "match_score": 0, + "reason": "No existing epics match these keywords" + } +] +``` + +## Error Handling + +- **Config not found**: Prompt user to run `/hygiene.setup` first +- **No Epic Link field**: Some Jira instances use different field names; fetch field ID dynamically +- **API errors**: Log error, continue with next story (don't fail entire batch) +- **Rate limit (429)**: Increase delay to 1s, retry diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.link-initiatives.md b/workflows/jira-hygiene/.claude/commands/hygiene.link-initiatives.md new file mode 100644 index 0000000..2375630 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.link-initiatives.md @@ -0,0 +1,166 @@ +# /hygiene.link-initiatives - Link Orphaned Epics to Initiatives + +## Purpose + +Find epics without initiative links and suggest appropriate initiatives from configured initiative projects, using semantic matching across projects. + +## Prerequisites + +- `/hygiene.setup` must be run first +- Initiative projects must be configured in config.json + +## Arguments + +Optional: +- `--dry-run` - Run steps 1-4 (Query, Analyze, Save, Display) only, skip confirmation and API modifications + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql and initiative_projects list + - If initiative_projects is empty: prompt user to configure via `/hygiene.setup` + +2. **Query orphaned epics WITH PAGINATION**: + ```jql + ({base_jql}) AND issuetype = Epic AND "Parent Link" is EMPTY + ``` + + **Pagination logic**: + ``` + all_orphaned_epics = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,description + epics = response['issues'] + all_orphaned_epics.extend(epics) + + Print: "Fetched {start_at + len(epics)}/{response['total']} orphaned epics..." + + if start_at + len(epics) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, description + - If none found: report success and exit + +3. **For each orphaned epic**: + + a. **Extract keywords**: + - Same process as `/hygiene.link-epics` + - Combine summary + description, remove stopwords + + b. **Search for matching initiatives WITH PAGINATION** (cross-project): + ```jql + project in ({INIT1},{INIT2}) AND issuetype = Initiative AND resolution = Unresolved AND text ~ "keyword1 keyword2" + ``` + + **Note**: Initiative search uses different project list, so base_jql is NOT applied here + + **Pagination for cross-project search**: + ``` + matching_initiatives = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={search_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,project + initiatives = response['issues'] + matching_initiatives.extend(initiatives) + + if start_at + len(initiatives) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Search across all configured initiative projects + - Fetch: key, summary, project + - Calculate match scores for ALL initiatives returned (not just first 50) + + c. **Calculate match scores**: + - Score = (matching_keywords / total_keywords) * 100 + - Sort by score descending + + d. **Determine suggestion**: + - If best score ≥50%: suggest linking to top initiative + - If best score <50%: note "No good match found" + - Unlike epics, don't suggest creating initiatives (typically higher-level planning) + +4. **Write candidates file**: + - Save to `artifacts/jira-hygiene/candidates/link-initiatives.json` + - Include: epic key, epic summary, suggested initiative (if any), match score + +5. **Display summary with Jira links**: + ``` + Found N orphaned epics: + - M epics with good matches (≥50%) + - P epics with no good match + + View orphaned epics: {JIRA_URL}/issues/?jql=project+%3D+{PROJECT}+AND+issuetype+%3D+Epic+AND+%22Parent+Link%22+is+EMPTY + + Top suggestions: + • [{EPIC-45}]({JIRA_URL}/browse/EPIC-45) "Authentication System" + → [{INIT-12}]({JIRA_URL}/browse/INIT-12) "User Management Platform" (80% match) + • [{EPIC-46}]({JIRA_URL}/browse/EPIC-46) "Payment Gateway" + → No good match found (20% best match) + ``` + +6. **Ask for confirmation**: + - If `--dry-run`: Display "DRY RUN - No changes made" and skip to step 8 + - Otherwise prompt: "Apply these suggestions? (yes/no/show-details)" + - Only link epics with good matches (≥50%) + +7. **Execute linking operations** (skip if --dry-run): + - For each approved linking: + - **TOCTOU check**: GET `/rest/api/3/issue/{epicKey}?fields=parent` to verify Parent Link is still empty + - If Parent Link is not empty: skip and log "Epic already linked to {existing_initiative}" + - Otherwise, update epic via PUT `/rest/api/3/issue/{epicKey}` + - Set Parent Link field to initiative key + - Rate limit: 0.5s between requests + +8. **Log results**: + - Write to `artifacts/jira-hygiene/operations/link-initiatives-{timestamp}.log` + +## Output + +- `artifacts/jira-hygiene/candidates/link-initiatives.json` +- `artifacts/jira-hygiene/operations/link-initiatives-{timestamp}.log` + +## Example Candidates JSON + +```json +[ + { + "epic_key": "EPIC-45", + "epic_summary": "Authentication System", + "keywords": ["authentication", "system", "user", "login"], + "suggestion": "link", + "initiative_key": "INIT-12", + "initiative_summary": "User Management Platform", + "initiative_project": "INIT1", + "match_score": 80, + "matching_keywords": ["authentication", "user", "management"] + }, + { + "epic_key": "EPIC-46", + "epic_summary": "Payment Gateway Integration", + "keywords": ["payment", "gateway", "integration"], + "suggestion": "no_match", + "best_match_score": 20, + "reason": "No initiatives found with >50% keyword match" + } +] +``` + +## Error Handling + +- **No initiative projects configured**: Prompt to run `/hygiene.setup` and configure +- **Cross-project access denied**: Some initiatives may not be accessible; log and skip +- **Parent Link field not found**: Fetch field metadata dynamically diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.report.md b/workflows/jira-hygiene/.claude/commands/hygiene.report.md new file mode 100644 index 0000000..797bea3 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.report.md @@ -0,0 +1,412 @@ +# /hygiene.report - Generate Master Hygiene Report + +## Purpose + +Generate a comprehensive master report that combines all hygiene checks into a single dashboard view. This provides an at-a-glance overview of all project hygiene issues. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Arguments + +Optional: +- `--output ` - Custom output path (default: artifacts/jira-hygiene/reports/master-report.md) +- `--format ` - Output format (default: md) + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql and settings + +2. **Run all hygiene checks WITH PAGINATION** (read-only queries): + + **Note**: All queries use base_jql and paginate to fetch complete counts + + **Standard pagination pattern for each query**: + ``` + all_results = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={jql}&startAt={start_at}&maxResults={max_results} + results = response['issues'] + all_results.extend(results) + + if start_at + len(results) >= response['total']: + break + + start_at += max_results + sleep(0.5) + ``` + + a. **Orphaned Stories WITH PAGINATION**: + ```jql + ({base_jql}) AND issuetype = Story AND "Epic Link" is EMPTY + ``` + - Paginate to count ALL orphaned stories + - List top 5 by age + + b. **Orphaned Epics WITH PAGINATION**: + ```jql + ({base_jql}) AND issuetype = Epic AND "Parent Link" is EMPTY + ``` + - Paginate to count ALL orphaned epics + - List top 5 by age + + c. **Blocking Tickets WITH PAGINATION**: + ```jql + ({base_jql}) AND issueFunction in linkedIssuesOf("({base_jql})", "blocks") + ``` + - Paginate to count ALL blocking tickets + - Count tickets being blocked + - List all with blocked ticket counts + + d. **Stale Tickets BY PRIORITY WITH PAGINATION**: + - For each priority: `({base_jql}) AND priority = {PRIORITY} AND updated < -{DAYS}d` + - Apply configured thresholds + - Paginate each priority query separately + - Count by priority level + - List top 5 oldest per priority + + e. **Untriaged Items WITH PAGINATION**: + ```jql + ({base_jql}) AND status = New AND created < -7d + ``` + - Paginate to count ALL untriaged + - List top 5 by age + + f. **Blocking-Closed Mismatches WITH PAGINATION**: + - Query blocking tickets (with pagination) + - For each, check if all blocked items are closed + - Count mismatches + - List all + + g. **In-Progress Unassigned WITH PAGINATION**: + ```jql + ({base_jql}) AND status = "In Progress" AND assignee is EMPTY + ``` + - Paginate to count ALL + - List all + + h. **Missing Activity Type WITH PAGINATION**: + ```jql + ({base_jql}) AND "{activity_type_field_id}" is EMPTY + ``` + - Use activity_type_field_id from config.json (e.g., "customfield_10050") + - Paginate to count ALL + - List top 10 by priority + +3. **Calculate health score**: + + **Scoring formula**: + - Start with 100 points + - Deduct points for each issue: + - Orphaned story: -0.5 points + - Orphaned epic: -1 point + - Blocking ticket: -2 points + - Stale ticket (High): -1 point + - Stale ticket (Medium): -0.5 points + - Stale ticket (Low): -0.25 points + - Untriaged item: -0.5 points + - Blocking-closed mismatch: -1 point + - In-progress unassigned: -1 point + - Missing activity type: -0.25 points + - Minimum score: 0 + + **Score interpretation**: + - 90-100: Excellent (🟢) + - 70-89: Good (🟡) + - 50-69: Needs Attention (🟠) + - 0-49: Critical (🔴) + +4. **Generate master report**: + - Write to `artifacts/jira-hygiene/reports/master-report.md` + - Include: + - Executive summary with health score + - Quick stats dashboard + - Detailed sections for each category + - Recommended actions (which commands to run) + - Links to detailed reports + - JQL search links for each category + +5. **Display summary**: + ``` + Project Hygiene Report: {PROJECT} + Health Score: {SCORE}/100 ({RATING}) + + Issues Found: + • {N} orphaned stories + • {N} orphaned epics + • {N} blocking tickets + • {N} stale tickets + • {N} untriaged items + • {N} blocking-closed mismatches + • {N} in-progress unassigned + • {N} missing activity types + + Full report: artifacts/jira-hygiene/reports/master-report.md + ``` + +## Output + +- `artifacts/jira-hygiene/reports/master-report.md` (or custom path) + +## Example Master Report + +```markdown +# Jira Hygiene Master Report: PROJ + +**Generated**: 2026-04-07 10:30 UTC +**Health Score**: 73/100 🟡 Good +**[View Project in Jira](https://company.atlassian.net/projects/PROJ)** + +--- + +## Executive Summary + +Your project has **good** overall hygiene with some areas needing attention. The main issues are: +- 15 orphaned stories need epic links +- 8 stale medium-priority tickets ready for closure +- 3 blocking tickets preventing other work + +**Recommended Actions**: +1. Run `/hygiene.link-epics` to link 15 orphaned stories +2. Run `/hygiene.close-stale` to close 12 stale tickets +3. Review 3 blocking tickets manually + +--- + +## Quick Stats Dashboard + +| Category | Count | Status | Action | +|----------|-------|--------|--------| +| Orphaned Stories | 15 | 🟡 | [Link to epics](#orphaned-stories) | +| Orphaned Epics | 2 | 🟢 | [Link to initiatives](#orphaned-epics) | +| Blocking Tickets | 3 | 🟡 | [Review](#blocking-tickets) | +| Stale Tickets | 12 | 🟠 | [Close stale](#stale-tickets) | +| Untriaged Items | 5 | 🟡 | [Triage](#untriaged-items) | +| Blocking-Closed | 1 | 🟢 | [Review](#blocking-closed-mismatches) | +| In-Progress Unassigned | 2 | 🟢 | [Assign](#in-progress-unassigned) | +| Missing Activity Type | 8 | 🟡 | [Set type](#missing-activity-type) | + +**Status Legend**: 🟢 Good (0-5) | 🟡 Monitor (6-15) | 🟠 Action Needed (16-30) | 🔴 Critical (30+) + +--- + +## Orphaned Stories + +**Count**: 15 stories +**Impact**: Stories without epic links are hard to organize and prioritize +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+issuetype+%3D+Story+AND+%22Epic+Link%22+is+EMPTY)** + +**Oldest 5**: + +| Story | Summary | Age | Priority | +|-------|---------|-----|----------| +| [PROJ-123](https://company.atlassian.net/browse/PROJ-123) | Implement user login | 45d | High | +| [PROJ-145](https://company.atlassian.net/browse/PROJ-145) | Add export feature | 38d | Medium | +| [PROJ-167](https://company.atlassian.net/browse/PROJ-167) | Fix broken link | 32d | Low | +| [PROJ-189](https://company.atlassian.net/browse/PROJ-189) | Update documentation | 28d | Low | +| [PROJ-201](https://company.atlassian.net/browse/PROJ-201) | Improve performance | 25d | High | + +**Recommended Action**: Run `/hygiene.link-epics` + +--- + +## Orphaned Epics + +**Count**: 2 epics +**Impact**: Epics without initiative links lack strategic alignment +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+issuetype+%3D+Epic+AND+%22Parent+Link%22+is+EMPTY)** + +**All Orphaned Epics**: + +| Epic | Summary | Age | Story Count | +|------|---------|-----|-------------| +| [EPIC-12](https://company.atlassian.net/browse/EPIC-12) | Payment Integration | 60d | 8 stories | +| [EPIC-15](https://company.atlassian.net/browse/EPIC-15) | Mobile App | 45d | 5 stories | + +**Recommended Action**: Run `/hygiene.link-initiatives` + +--- + +## Blocking Tickets + +**Count**: 3 tickets blocking 5 other tickets +**Impact**: Work is blocked, preventing progress on 5 tickets +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+issueFunction+in+linkedIssuesOf%28%22project+%3D+PROJ%22%2C+%22blocks%22%29)** + +**All Blocking Tickets**: + +| Blocking Ticket | Summary | Blocks | Assignee | Status | +|-----------------|---------|--------|----------|--------| +| [PROJ-50](https://company.atlassian.net/browse/PROJ-50) | Database migration | [PROJ-51](https://company.atlassian.net/browse/PROJ-51), [PROJ-52](https://company.atlassian.net/browse/PROJ-52) | John Doe | In Progress | +| [PROJ-75](https://company.atlassian.net/browse/PROJ-75) | Security audit | [PROJ-80](https://company.atlassian.net/browse/PROJ-80) | Unassigned | To Do | +| [PROJ-90](https://company.atlassian.net/browse/PROJ-90) | API changes | [PROJ-91](https://company.atlassian.net/browse/PROJ-91), [PROJ-92](https://company.atlassian.net/browse/PROJ-92) | Jane Smith | Code Review | + +**Recommended Action**: Review progress, assign unassigned blockers + +--- + +## Stale Tickets + +**Count**: 12 tickets (by priority) +**Impact**: Cluttering backlog, unclear if still relevant + +**Breakdown by Priority**: + +| Priority | Threshold | Count | Oldest | +|----------|-----------|-------|--------| +| High | 7 days | 2 | 15d | +| Medium | 14 days | 8 | 45d | +| Low | 30 days | 2 | 60d | + +**Top 5 Oldest**: + +| Ticket | Summary | Priority | Days Stale | +|--------|---------|----------|------------| +| [PROJ-100](https://company.atlassian.net/browse/PROJ-100) | Old feature request | Low | 60d | +| [PROJ-110](https://company.atlassian.net/browse/PROJ-110) | Performance issue | Medium | 45d | +| [PROJ-120](https://company.atlassian.net/browse/PROJ-120) | UI bug | Medium | 38d | +| [PROJ-130](https://company.atlassian.net/browse/PROJ-130) | Documentation update | Medium | 32d | +| [PROJ-140](https://company.atlassian.net/browse/PROJ-140) | Integration request | Medium | 28d | + +**Recommended Action**: Run `/hygiene.close-stale` + +--- + +## Untriaged Items + +**Count**: 5 items in "New" status for >7 days +**Impact**: Backlog not properly prioritized +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+status+%3D+New+AND+created+%3C+-7d)** + +**All Untriaged**: + +| Ticket | Summary | Age | Reporter | +|--------|---------|-----|----------| +| [PROJ-200](https://company.atlassian.net/browse/PROJ-200) | Add export feature | 12d | John Doe | +| [PROJ-201](https://company.atlassian.net/browse/PROJ-201) | Fix broken link | 10d | Jane Smith | +| [PROJ-202](https://company.atlassian.net/browse/PROJ-202) | Improve performance | 9d | Bob Johnson | +| [PROJ-203](https://company.atlassian.net/browse/PROJ-203) | New integration | 8d | Alice Lee | +| [PROJ-204](https://company.atlassian.net/browse/PROJ-204) | Update docs | 8d | John Doe | + +**Recommended Action**: Run `/hygiene.triage-new` + +--- + +## Blocking-Closed Mismatches + +**Count**: 1 ticket +**Impact**: Blocker may be ready to close + +**All Mismatches**: + +| Blocking Ticket | Summary | Blocks (All Closed) | +|-----------------|---------|---------------------| +| [PROJ-300](https://company.atlassian.net/browse/PROJ-300) | Security audit | [PROJ-305](https://company.atlassian.net/browse/PROJ-305) (closed 5d ago) | + +**Recommended Action**: Review and close or update links + +--- + +## In-Progress Unassigned + +**Count**: 2 tickets +**Impact**: Unclear ownership, work may be abandoned +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+status+%3D+%22In+Progress%22+AND+assignee+is+EMPTY)** + +**All Unassigned**: + +| Ticket | Summary | Status | Age | +|--------|---------|--------|-----| +| [PROJ-400](https://company.atlassian.net/browse/PROJ-400) | Refactor module | In Progress | 8d | +| [PROJ-401](https://company.atlassian.net/browse/PROJ-401) | API endpoint | In Progress | 5d | + +**Recommended Action**: Assign or move back to backlog + +--- + +## Missing Activity Type + +**Count**: 8 tickets +**Impact**: Reporting and categorization incomplete + +**Top 10 by Priority**: + +| Ticket | Summary | Priority | Issue Type | +|--------|---------|----------|------------| +| [PROJ-500](https://company.atlassian.net/browse/PROJ-500) | Fix login bug | High | Bug | +| [PROJ-501](https://company.atlassian.net/browse/PROJ-501) | Document API | Medium | Task | +| [PROJ-502](https://company.atlassian.net/browse/PROJ-502) | Add feature | Medium | Story | +| [PROJ-503](https://company.atlassian.net/browse/PROJ-503) | Update system | Low | Task | +| [PROJ-504](https://company.atlassian.net/browse/PROJ-504) | Research spike | Low | Task | +| [PROJ-505](https://company.atlassian.net/browse/PROJ-505) | Test automation | Low | Task | + +**Recommended Action**: Run `/hygiene.activity-type` + +--- + +## Health Score Breakdown + +**Total Score**: 73/100 🟡 Good + +**Deductions**: +- Orphaned stories (15 × 0.5): -7.5 points +- Orphaned epics (2 × 1): -2 points +- Blocking tickets (3 × 2): -6 points +- Stale High (2 × 1): -2 points +- Stale Medium (8 × 0.5): -4 points +- Stale Low (2 × 0.25): -0.5 points +- Untriaged (5 × 0.5): -2.5 points +- Blocking-closed (1 × 1): -1 point +- In-progress unassigned (2 × 1): -2 points +- Missing activity type (8 × 0.25): -2 points + +**Total Deductions**: -27 points + +--- + +## Next Steps + +**Priority 1 - High Impact** (address first): +1. `/hygiene.link-epics` - Link 15 orphaned stories +2. Review 3 blocking tickets - Unblock 5 downstream tickets + +**Priority 2 - Medium Impact** (address this week): +3. `/hygiene.close-stale` - Close 12 stale tickets +4. `/hygiene.triage-new` - Triage 5 items + +**Priority 3 - Low Impact** (address as time allows): +5. `/hygiene.link-initiatives` - Link 2 orphaned epics +6. `/hygiene.activity-type` - Set activity type for 8 tickets +7. Assign 2 in-progress tickets +8. Review 1 blocking-closed mismatch + +**Estimated Time**: 30-45 minutes to address all issues + +--- + +## Report Details + +**Project**: PROJ +**Generated**: 2026-04-07 10:30 UTC +**Total Unresolved Tickets**: 250 +**Issues Found**: 48 (19% of tickets need hygiene attention) + +**Related Reports**: +- [Blocking Tickets](./blocking-tickets.md) +- [Blocking-Closed Mismatches](./blocking-closed-mismatch.md) +- [In-Progress Unassigned](./unassigned-progress.md) +``` + +## Notes + +- All queries are read-only (no modifications made) +- Health score is a guideline, not absolute measure +- Customize thresholds via config.json +- Run this report weekly for ongoing hygiene monitoring +- Consider adding to cron for automated reporting diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.setup.md b/workflows/jira-hygiene/.claude/commands/hygiene.setup.md new file mode 100644 index 0000000..f648e2e --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.setup.md @@ -0,0 +1,100 @@ +# /hygiene.setup - Initial Configuration and Validation + +## Purpose + +Validate Jira API connection and configure project settings for all hygiene operations. + +## Prerequisites + +Environment variables must be set: +- `JIRA_URL` - Your Jira instance URL (e.g., https://company.atlassian.net) +- `JIRA_EMAIL` - Your Jira email address +- `JIRA_API_TOKEN` - Your Jira API token (generate at id.atlassian.com) + +## Process + +1. **Check environment variables**: + ```bash + if [ -z "$JIRA_URL" ] || [ -z "$JIRA_EMAIL" ] || [ -z "$JIRA_API_TOKEN" ]; then + echo "Error: Missing required environment variables" + exit 1 + fi + ``` + +2. **Test API connection**: + - Call `/rest/api/3/myself` to validate credentials + - Display authenticated user information + - If fails: provide troubleshooting guidance + +3. **Prompt for project configuration**: + - **Target project key**: The Jira project to operate on (e.g., "PROJ") + - **Initiative project keys**: Comma-separated list of projects containing initiatives (e.g., "INIT1,INIT2") + - User must provide the exact project keys they want to use + +4. **Prompt for base JQL filter (optional)**: + - **Base JQL filter**: Custom JQL to scope all operations + - If empty/skipped: Use default `"project = {PROJECT} AND resolution = Unresolved"` + - Examples: + - `"project = MYPROJ AND resolution = Unresolved AND labels = backend"` + - `"project in (PROJ1, PROJ2) AND resolution = Unresolved AND team = Platform"` + - `"project = MYPROJ AND resolution = Unresolved AND component = API"` + - Explain: This filter will be combined with each command's specific conditions + +4a. **Validate base JQL (if provided)**: + - Test query via `GET /rest/api/3/search?jql={encoded_jql}&maxResults=1` + - If 400 error: Show JQL syntax error, ask user to correct and retry + - If 200: Proceed with valid JQL + - If empty/skipped: Use default `"project = {PROJECT} AND resolution = Unresolved"` + +5. **Fetch Activity Type field metadata**: + - Call `/rest/api/3/field` to get all custom fields + - Search for field with name matching "Activity Type" (case-insensitive) + - Extract field ID (e.g., "customfield_10050") + - Fetch allowed values for this field + - If not found: note in config, skip this feature + +6. **Create config file**: + - Write all settings to `artifacts/jira-hygiene/config.json` + - Include base_jql (either user-provided or default) + - Include default staleness thresholds + - Format as pretty JSON for readability + +7. **Display summary**: + - Show configured project key + - Show base JQL filter + - Show initiative project keys + - Show Activity Type field ID and available values + - Confirm setup is complete + +## Output + +- `artifacts/jira-hygiene/config.json` + +## Example Config Structure + +```json +{ + "jira_url": "https://company.atlassian.net", + "project_key": "PROJ", + "base_jql": "project = PROJ AND resolution = Unresolved", + "initiative_projects": ["INIT1", "INIT2"], + "activity_type_field_id": "customfield_10050", + "activity_type_values": ["Development", "Bug Fix", "Documentation", "Research", "Testing"], + "staleness_thresholds": { + "Highest": 7, + "High": 7, + "Medium": 14, + "Low": 30, + "Lowest": 30 + }, + "configured_at": "2026-04-08T10:30:00Z" +} +``` + +## Error Handling + +- **Missing env vars**: Provide setup instructions with links to Jira API token generation +- **Auth failed (401)**: Suggest checking email/token, regenerating token +- **Network error**: Check JIRA_URL format (must start with https://) +- **Field not found**: Activity Type feature will be disabled, note in config + diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.show-blocking.md b/workflows/jira-hygiene/.claude/commands/hygiene.show-blocking.md new file mode 100644 index 0000000..2770494 --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.show-blocking.md @@ -0,0 +1,121 @@ +# /hygiene.show-blocking - Show Blocking Tickets + +## Purpose + +Display all tickets that are blocking other tickets via "Blocks" issue links. This highlights items that are preventing other work from progressing. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql + +2. **Query blocking tickets WITH PAGINATION**: + ```jql + ({base_jql}) AND issueFunction in linkedIssuesOf("({base_jql})", "blocks") + ``` + + **Note**: Both outer query AND inner linkedIssuesOf query use base_jql for consistency + + **Pagination logic**: + ``` + all_blocking_tickets = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,assignee,status,created,updated,priority,issuelinks&orderBy=updated DESC + tickets = response['issues'] + all_blocking_tickets.extend(tickets) + + Print: "Fetched {start_at + len(tickets)}/{response['total']} blocking tickets..." + + if start_at + len(tickets) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - This finds tickets that have outward "blocks" links to other tickets + - Fetch: key, summary, assignee, status, created, updated, priority, issuelinks + - Also fetch issue links to see what tickets are being blocked + - Order by updated descending (most recent first) + + **Alternative approach** (if issueFunction not available): + - Get all unresolved tickets + - For each, fetch issue links via `/rest/api/3/issue/{key}?fields=issuelinks` + - Filter tickets that have outward "blocks" type links + +3. **Format as markdown table with Jira links**: + ```markdown + # Blocking Tickets in {PROJECT} + + **Total**: N tickets blocking M other tickets + **Generated**: {timestamp} + **[View in Jira]({JIRA_URL}/issues/?jql=project+%3D+{PROJECT}+AND+issueFunction+in+linkedIssuesOf%28%22project+%3D+{PROJECT}%22%2C+%22blocks%22%29+AND+resolution+%3D+Unresolved)** + + | Blocking Ticket | Summary | Blocks | Assignee | Status | Priority | Last Updated | + |-----------------|---------|--------|----------|--------|----------|--------------| + | [PROJ-123]({JIRA_URL}/browse/PROJ-123) | Database migration issue | [PROJ-145]({JIRA_URL}/browse/PROJ-145), [PROJ-167]({JIRA_URL}/browse/PROJ-167) | John Doe | In Progress | High | 2d ago | + | [PROJ-456]({JIRA_URL}/browse/PROJ-456) | Security audit | [PROJ-500]({JIRA_URL}/browse/PROJ-500) | Unassigned | To Do | Medium | 3d ago | + ``` + + **Link format**: + - Ticket links: `[{KEY}]({JIRA_URL}/browse/{KEY})` + - Search link: `[View in Jira]({JIRA_URL}/issues/?jql={URL_ENCODED_JQL})` + - URL-encode JQL: spaces → `+`, special chars → percent-encoded + - List blocked tickets in "Blocks" column as comma-separated links + +4. **Write report**: + - Save to `artifacts/jira-hygiene/reports/blocking-tickets.md` + +5. **Display summary**: + - Show table in output + - Highlight unassigned blockers (if any) + - Note oldest blocker + +## Output + +- `artifacts/jira-hygiene/reports/blocking-tickets.md` + +## Example Report + +```markdown +# Blocking Tickets in PROJ + +**Total**: 3 tickets blocking 5 other tickets +**Generated**: 2026-04-07 10:30 UTC +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+issueFunction+in+linkedIssuesOf%28%22project+%3D+PROJ%22%2C+%22blocks%22%29+AND+resolution+%3D+Unresolved)** + +## Summary + +- 2 tickets assigned +- 1 ticket unassigned ⚠️ +- Oldest blocker: 12 days (PROJ-456) + +## Tickets + +| Blocking Ticket | Summary | Blocks | Assignee | Status | Priority | Last Updated | +|-----------------|---------|--------|----------|--------|----------|--------------| +| [PROJ-123](https://company.atlassian.net/browse/PROJ-123) | Critical API failure in auth endpoint | [PROJ-150](https://company.atlassian.net/browse/PROJ-150), [PROJ-151](https://company.atlassian.net/browse/PROJ-151) | John Doe | In Progress | High | 2d ago | +| [PROJ-456](https://company.atlassian.net/browse/PROJ-456) | Database migration blocked by schema lock | [PROJ-460](https://company.atlassian.net/browse/PROJ-460) | Unassigned | To Do | Medium | 3d ago | +| [PROJ-789](https://company.atlassian.net/browse/PROJ-789) | Production deployment failing | [PROJ-800](https://company.atlassian.net/browse/PROJ-800), [PROJ-801](https://company.atlassian.net/browse/PROJ-801) | Jane Smith | Code Review | High | 1d ago | + +## Recommendations + +- **[PROJ-456](https://company.atlassian.net/browse/PROJ-456)**: Assign to database team immediately (unassigned, blocking [PROJ-460](https://company.atlassian.net/browse/PROJ-460)) +- **[PROJ-123](https://company.atlassian.net/browse/PROJ-123)**: Follow up on progress (blocking 2 tickets for 5 days) +- **[PROJ-789](https://company.atlassian.net/browse/PROJ-789)**: In code review, close to unblocking deployment work +``` + +## Error Handling + +- **No blocking tickets found**: Report "No tickets are currently blocking other work in {PROJECT}" (good news!) +- **issueFunction not available**: Fall back to API approach (fetch all tickets, check issue links) +- **Query failed**: Check JQL syntax, validate project key +- **Issue links unavailable**: Some Jira instances may restrict issue link access; note in report diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.triage-new.md b/workflows/jira-hygiene/.claude/commands/hygiene.triage-new.md new file mode 100644 index 0000000..c8d0c7b --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.triage-new.md @@ -0,0 +1,203 @@ +# /hygiene.triage-new - Suggest Triage for Untriaged Items + +## Purpose + +Find items in "New" status for more than 1 week and suggest triage outcomes (priority and move to backlog) based on analysis of similar items in the project. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Arguments + +Optional: +- `--days ` - Threshold for untriaged items (default: 7 days) +- `--dry-run` - Show suggestions without making changes + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql + +2. **Query untriaged items WITH PAGINATION**: + ```jql + ({base_jql}) AND status = New AND status changed TO New BEFORE -{DAYS}d + ``` + + **Note**: Uses time-in-status (status changed TO New) instead of creation date to avoid misclassifying tickets moved back to New + + **Pagination logic**: + ``` + all_untriaged = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,description,issuetype,created + items = response['issues'] + all_untriaged.extend(items) + + Print: "Fetched {start_at + len(items)}/{response['total']} untriaged items..." + + if start_at + len(items) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, description, issuetype, created + - If none found: report success and exit + +3. **For each untriaged item**: + + a. **Extract keywords**: + - Combine summary + description + - Remove stopwords + + b. **Find similar items WITH PAGINATION**: + ```jql + ({base_jql}) AND text ~ "keyword1 keyword2" AND status != New + ``` + + **Pagination for semantic search**: + ``` + similar_items = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={search_jql}&startAt={start_at}&maxResults={max_results}&fields=priority,status + items = response['issues'] + similar_items.extend(items) + + if start_at + len(items) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Find items that have been triaged (not in New status) + - Fetch: priority, status + - Analyze priority distribution across ALL similar items (not just first 50) + + c. **Analyze priority distribution**: + - Count priorities of similar items: {High: 5, Medium: 8, Low: 2} + - Suggest most common priority (Medium in this case) + - If no similar items found: suggest "Medium" as default + + d. **Suggest triage outcome**: + - Recommended priority: Most common among similar items + - Recommended action: Move to "Backlog" status + - Confidence: High if ≥5 similar items, Medium if 2-4, Low if 0-1 + +4. **Write candidates file**: + - Save to `artifacts/jira-hygiene/candidates/triage-new.json` + - Include: key, summary, suggested priority, confidence, similar item count + +5. **Display summary with Jira links**: + ``` + Found N untriaged items (>7 days): + + View untriaged: {JIRA_URL}/issues/?jql=project+%3D+{PROJECT}+AND+status+%3D+New+AND+created+%3C+-7d + + High confidence (≥5 similar items): 8 items + • [{PROJ-200}]({JIRA_URL}/browse/PROJ-200) "Add export feature" + → Priority: Medium (based on 8 similar items) + • [{PROJ-201}]({JIRA_URL}/browse/PROJ-201) "Fix broken link" + → Priority: Low (based on 6 similar items) + + Medium confidence (2-4 similar): 3 items + • [{PROJ-202}]({JIRA_URL}/browse/PROJ-202) "Improve performance" + → Priority: High (based on 3 similar items) + + Low confidence (0-1 similar): 2 items + • [{PROJ-203}]({JIRA_URL}/browse/PROJ-203) "New integration request" + → Priority: Medium (default, no similar items) + ``` + +6. **Ask for confirmation** (batch mode): + - If `--dry-run`: Skip, display "DRY RUN - No changes made" + - Otherwise, split approved items into batches of max 50 + - For each batch, prompt: "Apply triage suggestions? (yes/no/high-confidence-only)" + - Only proceed on exact response "yes" (reject other responses) + - "high-confidence-only": Only apply suggestions with ≥5 similar items + +7. **Execute triage** (per batch): + - For each approved item in current batch: + - Update priority via PUT `/rest/api/3/issue/{key}` + - Transition to "Backlog" status + - Add comment: "Auto-triaged based on similar items. Priority set to {PRIORITY}." + - Rate limit: 0.5s between items + +8. **Log results**: + - Write to `artifacts/jira-hygiene/operations/triage-new-{timestamp}.log` + +## Output + +- `artifacts/jira-hygiene/candidates/triage-new.json` +- `artifacts/jira-hygiene/operations/triage-new-{timestamp}.log` + +## Example Candidates JSON + +```json +[ + { + "key": "PROJ-200", + "summary": "Add CSV export feature for reports", + "keywords": ["export", "csv", "reports", "feature"], + "suggested_priority": "Medium", + "confidence": "high", + "similar_items_found": 8, + "priority_distribution": { + "High": 2, + "Medium": 5, + "Low": 1 + }, + "days_untriaged": 10, + "current_status": "New" + }, + { + "key": "PROJ-203", + "summary": "Integration with new CRM system", + "keywords": ["integration", "crm", "system"], + "suggested_priority": "Medium", + "confidence": "low", + "similar_items_found": 0, + "priority_distribution": {}, + "days_untriaged": 15, + "current_status": "New", + "note": "No similar items found, using default priority" + } +] +``` + +## Priority Suggestion Logic + +1. **Find similar items**: Search by keywords, exclude items still in "New" +2. **Count priority distribution**: Tally priorities of similar items +3. **Suggest most common**: Pick priority with highest count +4. **Confidence levels**: + - High: ≥5 similar items found + - Medium: 2-4 similar items + - Low: 0-1 similar items (use default: Medium) + +## Default Backlog Status + +Most Jira projects use "Backlog" status, but some may use: +- "To Do" +- "Open" +- "Ready for Development" + +The workflow will: +1. Try "Backlog" first +2. If transition fails, try "To Do" +3. If still fails, log warning and skip status change (only update priority) + +## Error Handling + +- **No "Backlog" status**: Try alternative statuses, log which was used +- **Priority update failed**: Some projects have restricted priority changes; log error +- **Similar items query too broad**: If >100 results, limit to top 50 by updated date diff --git a/workflows/jira-hygiene/.claude/commands/hygiene.unassigned-progress.md b/workflows/jira-hygiene/.claude/commands/hygiene.unassigned-progress.md new file mode 100644 index 0000000..2017cbc --- /dev/null +++ b/workflows/jira-hygiene/.claude/commands/hygiene.unassigned-progress.md @@ -0,0 +1,155 @@ +# /hygiene.unassigned-progress - Show In-Progress Tickets Without Assignee + +## Purpose + +Simple query to find tickets that are marked as "In Progress" but have no assignee. This highlights potential ownership issues. + +## Prerequisites + +- `/hygiene.setup` must be run first + +## Process + +1. **Load configuration**: + - Read `artifacts/jira-hygiene/config.json` + - Extract base_jql (or use default if not present) + +2. **Query unassigned in-progress tickets WITH PAGINATION**: + ```jql + ({base_jql}) AND statusCategory = "In Progress" AND assignee is EMPTY + ``` + + **Note**: Uses statusCategory instead of hardcoded status name to match all in-progress statuses across different projects + + **Pagination logic**: + ``` + all_tickets = [] + start_at = 0 + max_results = 50 + + Loop: + response = GET /rest/api/3/search?jql={encoded_jql}&startAt={start_at}&maxResults={max_results}&fields=key,summary,status,created,updated,reporter&orderBy=updated DESC + tickets = response['issues'] + all_tickets.extend(tickets) + + Print: "Fetched {start_at + len(tickets)}/{response['total']} tickets..." + + if start_at + len(tickets) >= response['total']: + break # All results fetched + + start_at += max_results + sleep(0.5) # Rate limit + ``` + + - Fetch: key, summary, status, created, updated, reporter + - Order by updated descending + - If none found: report "No in-progress tickets without assignee" and exit + +3. **Format as markdown table with Jira links**: + ```markdown + # In-Progress Tickets Without Assignee + + **Total**: N tickets + **Generated**: {timestamp} + **[View in Jira]({JIRA_URL}/issues/?jql=project+%3D+{PROJECT}+AND+status+%3D+%22In+Progress%22+AND+assignee+is+EMPTY+AND+resolution+%3D+Unresolved)** + + | Key | Summary | Status | Reporter | Age | Last Updated | + |-----|---------|--------|----------|-----|--------------| + | [PROJ-123]({JIRA_URL}/browse/PROJ-123) | Implement new API | In Progress | John Doe | 8d | 2d ago | + | [PROJ-456]({JIRA_URL}/browse/PROJ-456) | Fix login bug | In Progress | Jane Smith | 5d | 1d ago | + ``` + + **Link format**: + - Ticket links: `[{KEY}]({JIRA_URL}/browse/{KEY})` + - Search link: URL-encode JQL (spaces → `+` or `%20`, quotes → `%22`) + +4. **Write report**: + - Save to `artifacts/jira-hygiene/reports/unassigned-progress.md` + +5. **Display summary**: + - Show table in output + - Highlight oldest ticket + - Group by reporter if helpful + +## Output + +- `artifacts/jira-hygiene/reports/unassigned-progress.md` + +## Example Report + +```markdown +# In-Progress Tickets Without Assignee + +**Project**: PROJ +**Generated**: 2026-04-07 10:30 UTC +**Total**: 4 tickets +**[View in Jira](https://company.atlassian.net/issues/?jql=project+%3D+PROJ+AND+status+%3D+%22In+Progress%22+AND+assignee+is+EMPTY+AND+resolution+%3D+Unresolved)** + +## Summary + +Found 4 tickets marked as "In Progress" but with no assignee. These tickets may be orphaned or forgotten. + +- Oldest: 12 days (PROJ-789) +- Most recent update: 1 day ago (PROJ-456) + +## Tickets + +| Key | Summary | Status | Reporter | Age | Last Updated | +|-----|---------|--------|----------|-----|--------------| +| [PROJ-789](https://company.atlassian.net/browse/PROJ-789) | Refactor authentication module | In Progress | John Doe | 12d | 5d ago | +| [PROJ-123](https://company.atlassian.net/browse/PROJ-123) | Implement new API endpoint | In Progress | John Doe | 8d | 2d ago | +| [PROJ-456](https://company.atlassian.net/browse/PROJ-456) | Fix login bug on mobile | In Progress | Jane Smith | 5d | 1d ago | +| [PROJ-234](https://company.atlassian.net/browse/PROJ-234) | Update documentation | In Progress | Bob Johnson | 3d | 6h ago | + +## Recommendations + +**Immediate Action Needed**: +- **[PROJ-789](https://company.atlassian.net/browse/PROJ-789)**: No updates in 5 days, assign or move back to backlog +- **[PROJ-123](https://company.atlassian.net/browse/PROJ-123)**: Assign to team member actively working on API + +**By Reporter**: +- John Doe (2 tickets): Follow up on [PROJ-789](https://company.atlassian.net/browse/PROJ-789) and [PROJ-123](https://company.atlassian.net/browse/PROJ-123) +- Jane Smith (1 ticket): Assign [PROJ-456](https://company.atlassian.net/browse/PROJ-456) or update status +- Bob Johnson (1 ticket): Recent activity on [PROJ-234](https://company.atlassian.net/browse/PROJ-234), verify assignment needed + +## Common Causes + +Tickets end up "In Progress" without assignee when: +1. Assignee was removed but status not updated +2. Ticket was started but never formally assigned +3. Team member left and tickets weren't reassigned +4. Workflow allows status change without assignment + +## Suggested Actions + +For each ticket: +1. **Assign to owner**: If work is ongoing, assign to current owner +2. **Move to backlog**: If work was abandoned, revert to "To Do" or "Backlog" +3. **Close if duplicate**: Check for duplicate tickets that may have superseded this one +``` + +## Status Variations + +Different Jira projects may use different status names for "in progress": +- "In Progress" +- "In Development" +- "Work In Progress" +- "Doing" + +This command checks for "In Progress" by default. If your project uses a different name, the JQL will need adjustment or the workflow can be enhanced to detect all "in progress category" statuses. + +## Why This Matters + +Unassigned in-progress tickets indicate: +- **Lost ownership**: Work may be forgotten +- **Stale work**: Previous assignee moved on +- **Process gaps**: Status changed without assignment +- **Coordination issues**: Team doesn't know who's working on what + +Regular checks help maintain accountability and prevent work from falling through cracks. + +## Error Handling + +- **No tickets found**: Report "No in-progress tickets without assignee" (good news!) +- **Status name mismatch**: If query returns empty but you expect results, check project's status names +- **Query failed**: Verify project key is correct diff --git a/workflows/jira-hygiene/CLAUDE.md b/workflows/jira-hygiene/CLAUDE.md new file mode 100644 index 0000000..af309db --- /dev/null +++ b/workflows/jira-hygiene/CLAUDE.md @@ -0,0 +1,209 @@ +# Jira Hygiene Workflow + +Systematic Jira project hygiene through 11 specialized commands: + +**Setup**: `/hygiene.setup` +**Reporting**: `/hygiene.report` (master report with health score) +**Linking**: `/hygiene.link-epics`, `/hygiene.link-initiatives` +**Activity**: `/hygiene.activity-summary`, `/hygiene.show-blocking` +**Bulk Ops**: `/hygiene.close-stale`, `/hygiene.triage-new` +**Data Quality**: `/hygiene.blocking-closed`, `/hygiene.unassigned-progress`, `/hygiene.activity-type` + +All commands are defined in `.claude/commands/hygiene.*.md`. +Artifacts written to `artifacts/jira-hygiene/`. + +## Principles + +- **Safety first**: All bulk operations use review-then-execute pattern +- **Transparency**: Show what will change before making changes +- **Auditability**: Log all operations with timestamps +- **Idempotency**: Safe to run commands multiple times +- **Semantic matching**: Use intelligent keyword-based matching for linking (50% threshold) +- **Priority-aware**: Different staleness thresholds by priority (High: 1w, Medium: 2w, Low: 1m) + +## Hard Limits + +### API Safety + +- **No operations without environment variables** - Validate JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN before any API call +- **No API token logging** - Always redact tokens in logs, use `len(token)` if needed +- **Respect rate limits** - Minimum 0.5s delay between requests, retry on 429 +- **No modification of closed tickets** - Only operate on `resolution = Unresolved` +- **Validate HTTP responses** - Check status codes, parse JSON safely + +### Bulk Operations + +- **No destructive operations without confirmation** - All bulk operations require explicit user approval +- **No cross-project operations without mapping** - Initiative linking requires configured project mapping +- **Maximum 50 tickets per confirmation** - Batch large operations for user review +- **Dry-run support required** - All bulk commands must support `--dry-run` flag +- **Log every operation** - Write timestamp, action, ticket key, result to operation logs + +### Data Integrity + +- **Validate JQL before execution** - Test queries return expected types/fields +- **No silent failures** - Report errors clearly, don't skip without notification +- **Preserve existing data** - Don't overwrite assignees, priorities, or custom fields without explicit intent +- **No duplicate links** - Check if link already exists before creating +- **Verify field IDs** - Fetch custom field metadata, don't hardcode field IDs + +## Safety + +### Review-then-Execute Pattern + +Every bulk operation follows this flow: + +1. **Query** - Execute JQL, fetch ticket data +2. **Analyze** - Extract keywords, calculate match scores, determine candidates +3. **Save** - Write candidates to `artifacts/jira-hygiene/candidates/{operation}.json` +4. **Display** - Show summary with ticket counts, match scores, or suggestions +5. **Confirm** - Ask user for explicit approval ("yes" to proceed) +6. **Execute** - Only if confirmed, make API calls with rate limiting +7. **Log** - Write results to `artifacts/jira-hygiene/operations/{operation}-{timestamp}.log` + +### Dry-Run Mode + +When user passes `--dry-run` flag: + +- Execute steps 1-4 only +- Display "DRY RUN" header prominently +- Show what **would** happen without making changes +- Skip steps 5-7 entirely + +### Error Handling + +- **Connection errors**: Check network, validate JIRA_URL format +- **Auth errors (401)**: Validate email/token, suggest regenerating token +- **Rate limit (429)**: Wait and retry, increase delay to 1s +- **Not found (404)**: Ticket may have been deleted, log and continue +- **Bad request (400)**: JQL syntax error or invalid field, show error message +- **Server error (500)**: Jira issue, suggest trying again later + +## Quality + +### JQL Best Practices + +- Always include `resolution = Unresolved` for active tickets +- Use `text ~` for keyword search, not exact match +- Escape quotes in JQL: use single quotes for values with spaces +- Test JQL in Jira UI before using in commands +- Use project key, not project name (e.g., `PROJ` not `"My Project"`) + +### Semantic Matching + +For linking orphaned tickets: + +1. Extract keywords: Remove stopwords (the, a, an, is, for, to, etc.) +2. Keep technical terms: Preserve API, auth, payment, etc. +3. Search strategy: Start with all keywords, fallback to top 3 if no results +4. Score calculation: `(matching_keywords / total_keywords) * 100` +5. Thresholds: + - ≥50%: Suggest linking with confidence + - <50%: Suggest creating new epic/initiative + - 0%: Always suggest creating new + +### Activity Summary Quality + +When generating weekly summaries: + +- Focus on status changes (New → In Progress → Done) +- Highlight new assignments or reassignments +- Include comment count (not full text) +- Summarize in 2-4 sentences +- Use business language, not technical jargon +- Format: "This week, {X} stories were {action}. {Y} items are {status}. {Notable events}." + +## Escalation + +Stop and request human guidance when: + +- **Environment variables missing** - Cannot proceed without credentials +- **Project key unknown** - User must specify which project to operate on +- **Initiative project mapping unclear** - Cross-project linking requires explicit configuration +- **Custom field name ambiguous** - Multiple fields match "Activity Type", need field ID +- **Bulk operation >100 tickets** - Confirm user wants to proceed with large batch +- **API errors persist** - After 3 retries, suggest checking Jira status + +## Configuration + +The workflow uses `artifacts/jira-hygiene/config.json` to cache: + +```json +{ + "jira_url": "https://company.atlassian.net", + "project_key": "PROJ", + "base_jql": "project = PROJ AND resolution = Unresolved", + "initiative_projects": ["INIT1", "INIT2"], + "activity_type_field_id": "customfield_10050", + "activity_type_values": ["Development", "Bug Fix", "Documentation", "Research"], + "staleness_thresholds": { + "Highest": 7, + "High": 7, + "Medium": 14, + "Low": 30, + "Lowest": 30 + } +} +``` + +This file is created by `/hygiene.setup` and read by other commands. It avoids repeated API calls for field metadata. + +## Pagination + +All commands automatically fetch ALL matching results using pagination: + +**How it works**: +- Jira API returns max 50 results by default +- Commands use `startAt` parameter to fetch in pages (0, 50, 100, ...) +- Loop continues until all results fetched +- Progress shown: "Fetched 150/237 tickets..." + +**User impact**: +- No manual intervention needed +- Large projects (>50 orphaned stories, >100 stale tickets) now fully supported +- Slightly longer execution time for large datasets (0.5s per page) + +**Example**: Project with 237 orphaned stories +- Old behavior: Only first 50 analyzed (187 missed) +- New behavior: All 237 fetched (5 pages × 0.5s = 2.5s extra time) + +## Base JQL Filter + +Customize which tickets are included in all operations using base_jql: + +**Setup**: During `/hygiene.setup`, provide optional base JQL filter + +**Default**: `project = {PROJECT} AND resolution = Unresolved` + +**Examples**: +- Scope to team: `project = MYPROJ AND resolution = Unresolved AND labels = backend` +- Multiple projects: `project in (PROJ1, PROJ2) AND resolution = Unresolved` +- Custom field: `project = MYPROJ AND resolution = Unresolved AND "Team" = Platform` + +**How it's used**: +- Combined with command-specific filters +- Example: link-epics uses `({base_jql}) AND issuetype = Story AND "Epic Link" is EMPTY` +- Applied to all queries except child relationships + +**When NOT to use**: +- Don't include `issuetype` (commands add this) +- Don't filter by status for specific tickets (may break linking logic) +- Don't add `updated < -Xd` (close-stale handles this) + +## Testing + +Before submitting PR, verify: + +1. **Validate JSON**: `jq . .ambient/ambient.json` (no syntax errors) +2. **Check commands**: All 11 command files exist in `.claude/commands/` +3. **Test dry-run**: Run `/hygiene.close-stale --dry-run` without making changes +4. **Verify logging**: Operation logs contain timestamp, action, results +5. **Check rate limiting**: Monitor API call timing (≥0.5s gaps) + +## Custom Workflow Testing + +Test in ACP using Custom Workflow: + +- **URL**: `https://github.com/YOUR-USERNAME/workflows.git` (your fork) +- **Branch**: `feature/jira_hygiene_workflows` +- **Path**: `workflows/jira-hygiene` diff --git a/workflows/jira-hygiene/README.md b/workflows/jira-hygiene/README.md new file mode 100644 index 0000000..8b0d3c4 --- /dev/null +++ b/workflows/jira-hygiene/README.md @@ -0,0 +1,491 @@ +# Jira Hygiene Workflow + +Systematic Jira project hygiene through automated detection, intelligent suggestions, and safe bulk operations. + +## Overview + +This workflow helps maintain clean and well-organized Jira projects by: + +- **Linking orphaned tickets**: Connect stories to epics and epics to initiatives using semantic matching +- **Generating activity summaries**: Create weekly summaries for epics/initiatives by analyzing child item changes +- **Closing stale tickets**: Bulk-close inactive tickets based on priority-specific thresholds +- **Suggesting triage outcomes**: Recommend priority and status for untriaged items +- **Identifying data quality issues**: Find missing assignees, activity types, and broken blocking relationships + +All bulk operations use a **review-then-execute pattern** for safety: you see what will change before any changes are made. + +## Prerequisites + +### Jira API Credentials + +Set these environment variables before using the workflow: + +```bash +export JIRA_URL="https://your-instance.atlassian.net" +export JIRA_EMAIL="your-email@company.com" +export JIRA_API_TOKEN="your-api-token" +``` + +**To generate a Jira API token**: +1. Go to [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) +2. Click "Create API token" +3. Name it (e.g., "Jira Hygiene Workflow") +4. Copy the token (you won't be able to see it again) + +### Required Permissions + +Your Jira account must have: +- Read access to the target project(s) +- Edit access to update issues +- Permission to add comments +- Permission to close issues + +## Getting Started + +1. **Run setup** to configure the workflow: + ``` + /hygiene.setup + ``` + This validates your Jira connection and configures project settings. + +2. **Choose a hygiene task**: + - Start with simple reports: `/hygiene.show-blocking` or `/hygiene.unassigned-progress` + - Try bulk operations in dry-run mode: `/hygiene.close-stale --dry-run` + - Use linking operations to organize your backlog: `/hygiene.link-epics` + +3. **Review artifacts** in `artifacts/jira-hygiene/`: + - Check candidate files before bulk operations + - Review operation logs for audit trail + - Read generated summaries before posting + +## Commands + +The workflow provides **11 specialized commands** for comprehensive Jira hygiene management. + +### Setup & Configuration + +#### `/hygiene.setup` + +Validate Jira connection and configure project settings. + +**What it does**: +- Tests API credentials +- Prompts for project key and initiative project mapping +- Fetches Activity Type field metadata +- Creates `artifacts/jira-hygiene/config.json` + +**When to use**: First command to run, or when changing projects + +--- + +### Linking Operations + +#### `/hygiene.link-epics` + +Link orphaned stories to epics using semantic matching. + +**What it does**: +- Finds stories without epic links +- Extracts keywords from story summary/description +- Searches for matching epics (50% keyword overlap threshold) +- Suggests creating new epic if no good match exists + +**Review-then-execute**: Yes +**Dry-run support**: Via manual review step + +**Example output**: +``` +Found 15 orphaned stories: +- 10 stories with good matches (≥50%) +- 5 stories need new epics (<50% match) + +[STORY-123] "Implement user login" → [EPIC-45] "Authentication System" (75% match) +[STORY-124] "Add payment gateway" → Create new epic (0% match) +``` + +--- + +#### `/hygiene.link-initiatives` + +Link orphaned epics to initiatives across projects. + +**What it does**: +- Finds epics without parent initiative links +- Searches configured initiative projects for matches +- Suggests best matches based on keyword overlap + +**Review-then-execute**: Yes +**Dry-run support**: Via manual review step + +**Note**: Requires initiative project mapping in config + +--- + +### Activity & Reporting + +#### `/hygiene.report` + +Generate comprehensive master hygiene report with health score. + +**What it does**: +- Runs all hygiene checks (read-only, no modifications) +- Calculates project health score (0-100) +- Provides executive summary with issue counts +- Lists top issues in each category +- Recommends which commands to run +- Generates detailed sections for all hygiene categories + +**Health Score**: +- 90-100: Excellent 🟢 +- 70-89: Good 🟡 +- 50-69: Needs Attention 🟠 +- 0-49: Critical 🔴 + +**Categories Checked**: +- Orphaned stories and epics +- Blocking tickets +- Stale tickets (by priority) +- Untriaged items +- Blocking-closed mismatches +- In-progress unassigned +- Missing activity types + +**Arguments**: +- `--output ` - Custom output path +- `--format ` - Output format (default: md) + +**Example output**: +``` +Project Hygiene Report: PROJ +Health Score: 73/100 🟡 Good + +Issues Found: +• 15 orphaned stories +• 3 blocking tickets +• 12 stale tickets +• 5 untriaged items + +Full report: artifacts/jira-hygiene/reports/master-report.md +``` + +**Use case**: Weekly hygiene check, stakeholder reporting, project health dashboard + +--- + +#### `/hygiene.activity-summary` + +Generate weekly activity summaries for epics/initiatives. + +**What it does**: +- Analyzes child items for the past 7 days +- Tracks status transitions, assignments, comments +- **Includes linked PR/MR activity** (merged, in review, commits) +- Generates business-friendly summary paragraph +- Posts summary as comment on epic/initiative + +**Review-then-execute**: Yes (shows summaries before posting) + +**PR/MR Integration**: +- Automatically detects linked GitHub/GitLab PRs via Jira development panel +- Falls back to parsing PR URLs from comments +- Filters by last update date (past 7 days only) +- Optional: Set `GITHUB_TOKEN` or `GITLAB_TOKEN` for direct API access + +**Example summary**: +> This week, 3 stories moved to In Progress and 2 were completed. The team merged 2 pull requests for OAuth integration and has 3 PRs in active review. There were 4 new assignments and 8 comments focused on implementation details. + +--- + +#### `/hygiene.show-blocking` + +Show all blocking tickets in the project. + +**What it does**: +- Queries tickets with "Blocker" priority +- Displays formatted table with status, assignee, age +- Highlights unassigned blockers + +**Review-then-execute**: No (read-only report) + +--- + +### Bulk Operations + +#### `/hygiene.close-stale` + +Close stale tickets based on priority-specific thresholds. + +**Default thresholds**: +- Highest/High: 7 days +- Medium: 14 days +- Low/Lowest: 30 days + +**Arguments**: +- `--highest ` - Override threshold for Highest priority +- `--high ` - Override for High priority +- `--medium ` - Override for Medium priority +- `--low ` - Override for Low priority +- `--lowest ` - Override for Lowest priority +- `--dry-run` - Show what would be closed without making changes + +**What it does**: +- Finds tickets not updated within threshold +- Groups by priority for review +- Adds closure comment and closes tickets + +**Closure message**: +> Due to lack of activity, this item has been closed. If you feel that it should be addressed, please reopen it. + +**Review-then-execute**: Yes +**Dry-run support**: Yes + +**Example**: +```bash +# Close stale tickets using defaults +/hygiene.close-stale + +# See what would be closed without making changes +/hygiene.close-stale --dry-run + +# Use custom thresholds +/hygiene.close-stale --high 14 --medium 30 --low 60 +``` + +--- + +#### `/hygiene.triage-new` + +Suggest triage outcomes for untriaged items. + +**What it does**: +- Finds items in "New" status for >1 week +- Analyzes similar triaged items to suggest priority +- Recommends moving to "Backlog" status +- Provides confidence level based on similar item count + +**Arguments**: +- `--days ` - Override threshold (default: 7) +- `--dry-run` - Show suggestions without making changes + +**Review-then-execute**: Yes +**Dry-run support**: Yes + +**Confidence levels**: +- High: ≥5 similar items found +- Medium: 2-4 similar items +- Low: 0-1 similar items (uses default: Medium) + +--- + +### Data Quality + +#### `/hygiene.blocking-closed` + +Find blocking tickets where all blocked items are closed. + +**What it does**: +- Finds tickets with "blocks" issue links +- Checks if all blocked tickets are resolved +- Suggests closing blocker or removing links + +**Review-then-execute**: No (manual review required) + +**Note**: This is a report-only command because each case requires human judgment. + +--- + +#### `/hygiene.unassigned-progress` + +Show tickets in progress without assignee. + +**What it does**: +- Finds "In Progress" tickets with no assignee +- Displays formatted table by age +- Groups by reporter for follow-up + +**Review-then-execute**: No (read-only report) + +--- + +#### `/hygiene.activity-type` + +Suggest Activity Type for tickets missing this field. + +**What it does**: +- Finds tickets with empty Activity Type field +- Analyzes summary/description for keywords +- Matches against available Activity Type values +- Suggests best match with confidence level + +**Arguments**: +- `--dry-run` - Show suggestions without making changes + +**Review-then-execute**: Yes +**Dry-run support**: Yes + +**Keyword mappings**: +- Development: implement, create, add, build, feature +- Bug Fix: fix, bug, error, broken, defect +- Documentation: document, guide, wiki, manual +- Research: investigate, explore, spike, POC +- Testing: test, QA, verify, validate + +--- + +## Output Structure + +All artifacts are written to `artifacts/jira-hygiene/`: + +``` +artifacts/jira-hygiene/ +├── config.json # Project configuration +├── candidates/ # Review before bulk ops +│ ├── link-epics.json +│ ├── link-initiatives.json +│ ├── close-stale.json +│ ├── triage-new.json +│ └── activity-type.json +├── summaries/ # Generated summaries +│ └── {epic-key}-{date}.md +├── reports/ # Read-only reports +│ ├── blocking-tickets.md +│ ├── blocking-closed-mismatch.md +│ └── unassigned-progress.md +└── operations/ # Audit logs + ├── link-epics-{timestamp}.log + ├── close-stale-{timestamp}.log + └── ... +``` + +## Safety Features + +### Review-then-Execute Pattern + +All bulk operations follow this flow: + +1. **Query**: Execute JQL to find candidates +2. **Analyze**: Apply semantic matching or rules +3. **Save**: Write candidates to JSON file +4. **Display**: Show summary of what will change +5. **Confirm**: Ask for explicit approval +6. **Execute**: Make changes only if confirmed +7. **Log**: Write audit log with results + +### Dry-Run Mode + +Commands that support `--dry-run`: +- `/hygiene.close-stale` +- `/hygiene.triage-new` +- `/hygiene.activity-type` + +Dry-run mode shows what **would** happen without making any changes. + +### Rate Limiting + +All API calls include: +- 0.5s delay between requests (minimum) +- Automatic retry on 429 (rate limit) errors +- Increased delay to 1s after rate limit hit + +### Operation Logging + +Every bulk operation writes a timestamped log: + +``` +2026-04-07 10:30:15 - START: Close stale tickets +2026-04-07 10:30:16 - CLOSED: PROJ-100 (Highest priority, 12 days stale) +2026-04-07 10:30:17 - CLOSED: PROJ-101 (High priority, 9 days stale) +2026-04-07 10:30:18 - ERROR: PROJ-102 - Transition failed (permission denied) +2026-04-07 10:30:19 - END: 2 closed, 1 error +``` + +## Best Practices + +### Regular Hygiene Routine + +**Weekly**: +- Generate activity summaries for key epics: `/hygiene.activity-summary` +- Check for untriaged items: `/hygiene.triage-new` +- Review blocking tickets: `/hygiene.show-blocking` + +**Monthly**: +- Close stale tickets: `/hygiene.close-stale` +- Link orphaned stories: `/hygiene.link-epics` +- Check in-progress items: `/hygiene.unassigned-progress` + +**Quarterly**: +- Link orphaned epics to initiatives: `/hygiene.link-initiatives` +- Review blocking-closed mismatches: `/hygiene.blocking-closed` +- Fill in missing activity types: `/hygiene.activity-type` + +### Using with Multiple Projects + +Run `/hygiene.setup` each time you switch projects. The config file stores the current project context. + +### Customizing Thresholds + +Adjust staleness thresholds based on your team's velocity: + +**Fast-moving team** (releases weekly): +```bash +/hygiene.close-stale --high 3 --medium 7 --low 14 +``` + +**Slower cadence** (releases monthly): +```bash +/hygiene.close-stale --high 14 --medium 30 --low 60 +``` + +## Troubleshooting + +### "Authentication failed (401)" + +**Cause**: Invalid credentials +**Solution**: +1. Verify `JIRA_EMAIL` matches your Atlassian account email +2. Regenerate API token at id.atlassian.com +3. Check `JIRA_URL` format (must start with https://) + +### "Field not found" errors + +**Cause**: Custom field names vary by project +**Solution**: +1. Run `/hygiene.setup` to fetch field metadata +2. Check field names in Jira (Admin → Issues → Custom Fields) +3. If "Epic Link" or "Parent Link" are named differently, update JQL + +### "Rate limit exceeded (429)" + +**Cause**: Too many requests +**Solution**: +- Workflow automatically retries with increased delay +- For large operations, work in smaller batches +- Jira Cloud typically allows 10 requests/second + +### "No transition available" + +**Cause**: Status workflow restrictions +**Solution**: +- Check Jira workflow for allowed transitions +- Some tickets may require intermediate states +- Logs will note which tickets couldn't be transitioned + +## Contributing + +To modify or extend this workflow: + +1. Read `CLAUDE.md` for safety rules and principles +2. Update command files in `.claude/commands/` +3. Test with `--dry-run` flags before live operations +4. Update this README with any new commands or features + +## Support + +For issues or feature requests: +- File an issue in the repository +- Include operation logs from `artifacts/jira-hygiene/operations/` +- Provide example JQL queries that aren't working + +## License + +Part of the Ambient Code Workflows repository. See main repository LICENSE.