
Jira Expert
- 126 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
For integrating A developer tool for AI integration and automation
About
A developer tool for AI integration and automation. This is a developer tool for building and integrating AI-powered features.
- AI
- Developer tool
Jira Expert by the numbers
- 126 all-time installs (skills.sh)
- Ranked #3,699 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill jira-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 126 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
For integrating A developer tool for AI integration and automation
Files
Atlassian Jira Expert
Master-level expertise in Jira configuration, project management, JQL, workflows, automation, and reporting. Handles all technical and operational aspects of Jira.
Core Capabilities
- Project configuration — create Scrum/Kanban/custom projects, design custom workflows, configure issue types/fields/screens, set permission and security schemes
- JQL mastery — author advanced queries, build complex filters, optimize query performance, create saved filters for teams
- Automation & integration — design automation rules, configure webhooks and notifications, integrate Confluence/Slack/external tools
- Reporting & dashboards — custom dashboards with gadgets, sprint/velocity/burndown reports, portfolio-level and executive reporting
When to Use
- Setting up a new project and its workflow, fields, and board
- Writing or debugging JQL for filters, reports, or dashboards
- Designing automation rules and avoiding trigger loops
- Building dashboards and configuring gadgets for a team or exec
- Managing custom fields, issue links, security levels, or bulk operations
References
Load the reference that matches the task — keep this file lean and pull detail on demand:
- [references/jira-playbook.md](references/jira-playbook.md) — full detail behind this map: competencies, workflows (project creation, workflow design, dashboards, automation), JQL operators/functions, advanced features (custom fields, linking, security, bulk ops), reporting templates, decision/handoff protocols, best practices, troubleshooting table, and success criteria. Read when executing any Jira task.
- [references/jql-examples.md](references/jql-examples.md) — extended library of ready-to-run JQL queries grouped by use case (sprints, and more). Read when you need a specific query pattern fast.
- [references/automation-examples.md](references/automation-examples.md) — concrete Jira automation rule recipes (auto-assignment, and more) with triggers/conditions/actions. Read when building or troubleshooting automation rules.
- [references/red-flags.md](references/red-flags.md) — common ways a Jira configuration, JQL query, automation rule, or dashboard goes wrong, with bad/good examples. Read before shipping any change to a team.
Scope & Limitations
In Scope: Jira project creation and configuration, workflow design and implementation, JQL query authoring and optimization, automation rule design, dashboard and reporting setup, custom field management, board configuration, bulk operations, issue linking strategies.
Out of Scope: Org-wide Atlassian administration (hand off to atlassian-admin/), Confluence space management (hand off to confluence-expert/), sprint execution and team coaching (hand off to scrum-master/), strategic project prioritization (hand off to senior-pm/).
Limitations: Jira Cloud automation has monthly execution limits per plan tier. Complex JQL on large instances (>100K issues) may hit performance ceilings. Workflow changes to active projects require careful migration planning -- retroactive changes do not apply to in-flight issues.
Integration Points
| Integration | Direction | What Flows |
|---|---|---|
atlassian-admin/ | Admin -> Jira | Global schemes, permission templates, user provisioning |
scrum-master/ | SM -> Jira | Sprint board configuration requests, velocity report needs |
senior-pm/ | PM -> Jira | Portfolio-level reporting requirements, cross-project dashboards |
confluence-expert/ | Bidirectional | Jira macros embedded in Confluence pages; documentation links in issue descriptions |
atlassian-templates/ | Templates -> Jira | Issue description templates, workflow documentation |
delivery-manager/ | DM -> Jira | Release version management, deployment tracking fields |
Example: Jira Setup for a 50-Person Engineering Org
Real-world scenario showing how to apply this skill end-to-end.
Context
Acme Analytics (B2B analytics SaaS, Series B, 80 people) just consolidated three Jira instances (one per acquired team) into a single cloud instance. The engineering org is now 50 people in five squads: Search Platform, Workspace, Onboarding, Data Platform, and Reliability. The PMO (Priya) is setting up the new instance from scratch.
The previous setups failed for predictable reasons: every squad invented its own workflow, JQL queries did not work across boards, custom fields proliferated to 187 (most unused), and automation rules were untracked and conflicted. The new setup must be opinionated, documented, and minimal.
Inputs
- 50 engineers, 5 squads, 5 squad leads, 1 VP Engineering
- One Jira Cloud instance, Premium plan
- Connected: GitHub, Slack, PagerDuty, Confluence
- Constraint: 12 custom fields maximum; one company-wide workflow with squad-specific extensions only by exception
- Naming convention:
<SQUAD>-<NNNN>(e.g.,SEARCH-1042)
Applying the skill
1. Define the projects and naming convention before anything else. 2. Design one canonical workflow and a small set of issue types. 3. Limit custom fields to a documented list -- everything else is rejected. 4. Write the JQL query library that the org will use day-to-day. 5. Define automation rules as code (YAML-ish) in Confluence, not just in the Jira UI. 6. Build the standard dashboards every squad/lead/VP uses.
The artifact
Project structure
| Key | Project name | Type | Lead |
|---|---|---|---|
| SEARCH | Search Platform | Software (Scrum) | Sarah Khoury |
| WS | Workspace | Software (Scrum) | Marcus Vella |
| ONB | Onboarding | Software (Kanban) | Jin Lee |
| DATA | Data Platform | Software (Scrum) | Tomas Veliz |
| REL | Reliability | Software (Kanban) | Ramon Cortes |
| PMO | PMO Cross-Project | Service Management | Priya |
| ARCH | Architecture Decisions | Business | Devraj (VP Eng) |
Issue types (canonical)
- Epic (planning level, has Fix Version)
- Story (user-facing change)
- Task (technical work, no user-visible change)
- Bug (defect in shipped behavior)
- Spike (time-boxed investigation; outputs are docs, not code)
- Incident (created automatically from PagerDuty; closed via post-mortem doc)
Canonical workflow (one-size-fits-most)
Backlog -> Ready -> In Progress -> In Review -> Verified -> Done
\-> Won't Do (terminal)Transitions:
- Backlog -> Ready: requires Definition of Ready checklist passing (linked to a Confluence page)
- In Progress -> In Review: requires linked PR
- In Review -> Verified: requires PR merged + linked deployment record
- Verified -> Done: requires acceptance criteria checked off
Exceptions (granted, documented, dated):
- REL adds
In Progress -> Pagedtransition (for active incident states) - DATA adds
Verified -> Data QAstep (one-day async data validation)
Custom field allowlist (12 max)
| # | Field name | Type | Purpose |
|---|---|---|---|
| 1 | Squad | Single-select | Search/Workspace/Onboarding/Data/Reliability |
| 2 | Story Points | Number | Standard agile points |
| 3 | RICE Score | Calculated number | (Reach * Impact * Confidence) / Effort |
| 4 | Fix Version | Multi-select | Release version |
| 5 | Customer Impact | Single-select | None / Low / Medium / High / SEV |
| 6 | Definition of Ready | Checkbox | DoR checklist (5 items) |
| 7 | Acceptance Criteria | Wiki text | WWAS-style criteria |
| 8 | Linked PR | URL | GitHub PR (auto-populated) |
| 9 | Deploy Record | URL | Deployment ID (auto-populated) |
| 10 | Risk Score | Single-select | L/M/H |
| 11 | OKR Link | URL | Link to OKR (Quarterly) |
| 12 | Post-Mortem Doc | URL | Confluence link (Incident issue type only) |
JQL query library
My day
assignee = currentUser()
AND statusCategory != Done
AND sprint in openSprints()
ORDER BY priority DESC, updated DESCSquad standup query (run by all squad leads)
project = "SEARCH"
AND sprint in openSprints()
AND status in (
"Backlog", "Ready", "In Progress", "In Review", "Verified"
)
ORDER BY status, "Story Points" DESCBugs aging > 30 days
type = Bug
AND statusCategory != Done
AND created < -30d
ORDER BY priority DESC, created ASCHigh customer impact items in flight (VP Eng dashboard)
"Customer Impact" in (High, SEV)
AND statusCategory != Done
ORDER BY priority DESC, updated DESCRICE top-20 (planning query)
project in (SEARCH, WS, ONB, DATA, REL)
AND statusCategory = "To Do"
AND "RICE Score" > 0
ORDER BY "RICE Score" DESC(Use limit 20 in the dashboard gadget.)
Sprint cleanup (find orphan tickets at sprint end)
sprint = closedSprintsBefore(2)
AND statusCategory != Done
ORDER BY assignee, updated DESCItems missing acceptance criteria (DoR enforcement)
type in (Story, Task)
AND status = Ready
AND "Acceptance Criteria" is EMPTYOpen incidents in last 30 days
type = Incident
AND created > -30d
ORDER BY priority DESC, created DESCItems linked to a specific OKR
"OKR Link" ~ "OKR-Q4-2026-O1-KR1"
AND statusCategory != DoneAutomation rules (the 7 we run)
Rule 1: PR-merged -> Verified
trigger: github_pr_merged
condition: branch_name matches "^[A-Z]+-\d+-.*"
action:
- extract issue_key from branch_name
- transition issue from "In Review" to "Verified"
- add_comment "Merged via {{pr.url}}"
- set field "Linked PR" = pr.urlRule 2: PR-opened -> In Review
trigger: github_pr_opened
action:
- transition issue from "In Progress" to "In Review"
- link pr.url to issueRule 3: PagerDuty incident -> Incident issue
trigger: pagerduty_incident_created
action:
- create_issue:
project: REL
type: Incident
summary: pagerduty.summary
priority: pagerduty.priority
- set field "Customer Impact" = mapped_from_pagerduty_severity
- notify Slack channel #eng-incidentsRule 4: Bug aged > 60 days -> escalate
trigger: schedule_daily
jql: "type = Bug AND statusCategory != Done AND created < -60d"
action:
- add_label "aging-bug"
- notify squad_lead via Slack DM
- add to weekly squad-health digestRule 5: Acceptance Criteria empty -> auto-comment in standup
trigger: status_transition_to "Ready"
condition: field "Acceptance Criteria" is empty
action:
- revert to "Backlog"
- add_comment: "Auto-reverted: Definition of Ready not met -- Acceptance Criteria empty."
- notify assignee + reporterRule 6: Customer-Impact High -> notify VP Eng
trigger: field_changed "Customer Impact"
condition: new value in (High, SEV)
action:
- send_email vp_eng@acme.com
- tag in Slack #cs-escalationsRule 7: Sprint end -> archive orphans
trigger: sprint_completed
jql: "sprint = ${sprint.id} AND statusCategory != Done"
action:
- add_comment "Auto-archived from Sprint {{sprint.name}}; revisit in next planning."
- remove from sprint (return to backlog)Standard dashboards
Dashboard: Squad Health (each squad lead)
Gadgets: 1. Sprint Burndown 2. JQL: My-squad in-progress (linked to "Squad standup query") 3. JQL: My-squad bugs aging > 30 days 4. JQL: Items missing Acceptance Criteria 5. Velocity chart (last 6 sprints) 6. Customer-impact High/SEV in my squad
Dashboard: VP Engineering
Gadgets: 1. Cross-squad in-progress count by squad 2. Open incidents last 30 days 3. High-customer-impact tickets in flight 4. RICE top-20 across all squads 5. Aging bugs > 60 days (escalated) 6. Sprint goal commitment vs delivered (each squad)
Dashboard: PMO weekly status pull
Gadgets: 1. JQL for current sprint per squad 2. Filtered list: changes shipped in last 7 days, grouped by squad 3. Items linked to current quarter OKRs 4. DACI decisions due this week (PMO project)
Permission scheme (simple)
| Role | Can do |
|---|---|
| Engineer | Create/edit own issues; transition through workflow; view all |
| Squad Lead | All of Engineer + edit anyone's issue in own project + manage sprints |
| PM | All of Squad Lead across PM-owned projects |
| Admin | Schema and workflow changes; one person per squad designated |
Documentation handoff
A single Confluence page titled "Jira Conventions at Acme" lists:
- Issue type definitions + when to use each
- The canonical workflow with allowed transitions
- The 12 custom fields and why they exist
- JQL queries (above)
- All 7 automation rules (above) with their owners
- The exception process: who approves a custom field, new workflow, etc.
Every new engineer reviews this on day one.
Why this works
- One canonical workflow + a small, deliberate exception process. Squads cannot invent workflows in private.
- Custom fields capped at 12. New requests must displace an existing field or be rejected.
- JQL queries are a library, not folklore. Same query works everywhere because the schema is consistent.
- Automation rules are documented as code in Confluence with named owners. Rules that drift get caught at quarterly review.
- Dashboards are role-targeted (squad lead, VP Eng, PMO). No "everyone dashboard."
- Permission scheme is simple. Most engineers do not need fine-grained access; they need to ship.
What's next
- Lift dashboards into Confluence reports via
../confluence-expert/. - Use
../sprint-retrospective/to track which automation rules are working. - For cross-team coordination, the PMO project uses
../execution/dependency-map/. - New PM onboarding: read this page, build their first dashboard. Covered by
../career/pm-onboarding/. - Reconsider field allowlist quarterly; remove fields with low engagement.
Jira Automation Examples
Auto-Assignment Rules
Auto-assign by component
Trigger: Issue created Conditions:
- Component is not EMPTY
Actions:
- Assign issue to component lead
Auto-assign to reporter for feedback
Trigger: Issue transitioned to "Waiting for Feedback" Actions:
- Assign issue to reporter
- Add comment: "Please provide additional information"
Round-robin assignment
Trigger: Issue created Conditions:
- Project = ABC
- Assignee is EMPTY
Actions:
- Assign to next team member in rotation (use smart value)
---
Status Sync Rules
Sync subtask status to parent
Trigger: Issue transitioned Conditions:
- Issue type = Sub-task
- Transition is to "Done"
- Parent issue exists
- All subtasks are Done
Actions:
- Transition parent issue to "Done"
Sync parent to subtasks
Trigger: Issue transitioned Conditions:
- Issue type has subtasks
- Transition is to "Cancelled"
Actions:
- For each: Sub-tasks
- Transition issue to "Cancelled"
Epic progress tracking
Trigger: Issue transitioned Conditions:
- Epic link is not EMPTY
- Transition is to "Done"
Actions:
- Add comment to epic: "{{issue.key}} completed"
- Update epic custom field "Progress"
---
Notification Rules
Slack notification for high-priority bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority IN (Highest, High)
Actions:
- Send Slack message to #engineering:
🚨 High Priority Bug Created
{{issue.key}}: {{issue.summary}}
Reporter: {{issue.reporter.displayName}}
Priority: {{issue.priority.name}}
{{issue.url}}Email assignee when mentioned
Trigger: Issue commented Conditions:
- Comment contains @mention of assignee
Actions:
- Send email to {{issue.assignee.emailAddress}}:
Subject: You were mentioned in {{issue.key}}
Body: {{comment.author.displayName}} mentioned you:
{{comment.body}}SLA breach warning
Trigger: Scheduled - Every hour Conditions:
- Status != Done
- SLA time remaining < 2 hours
Actions:
- Send email to {{issue.assignee}}
- Add comment: "⚠️ SLA expires in <2 hours"
- Set priority to Highest
---
Field Automation Rules
Auto-set due date
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority = Highest
Actions:
- Set due date to {{now.plusDays(1)}}
Clear assignee when in backlog
Trigger: Issue transitioned Conditions:
- Transition is to "Backlog"
- Assignee is not EMPTY
Actions:
- Assign issue to Unassigned
- Add comment: "Returned to backlog, assignee cleared"
Auto-populate sprint field
Trigger: Issue transitioned Conditions:
- Transition is to "In Progress"
- Sprint is EMPTY
Actions:
- Add issue to current sprint
Set fix version based on component
Trigger: Issue created Conditions:
- Component = "Mobile App"
Actions:
- Set fix version to "Mobile v2.0"
---
Escalation Rules
Auto-escalate stale issues
Trigger: Scheduled - Daily at 9:00 AM Conditions:
- Status = "Waiting for Response"
- Updated < -7 days
Actions:
- Add comment: "@{{issue.reporter}} This issue needs attention"
- Send email to project lead
- Add label: "needs-attention"
Escalate overdue critical issues
Trigger: Scheduled - Every hour Conditions:
- Priority IN (Highest, High)
- Due date < now()
- Status != Done
Actions:
- Transition to "Escalated"
- Assign to project manager
- Send Slack notification
Auto-close inactive issues
Trigger: Scheduled - Daily at 10:00 AM Conditions:
- Status = "Waiting for Customer"
- Updated < -30 days
Actions:
- Transition to "Closed"
- Add comment: "Auto-closed due to inactivity"
- Send email to reporter
---
Sprint Automation Rules
Move incomplete work to next sprint
Trigger: Sprint closed Conditions:
- Issue status != Done
Actions:
- Add issue to next sprint
- Add comment: "Moved from {{sprint.name}}"
Auto-remove completed items from active sprint
Trigger: Issue transitioned Conditions:
- Transition is to "Done"
- Sprint IN openSprints()
Actions:
- Remove issue from sprint
- Add comment: "Removed from active sprint (completed)"
Sprint start notification
Trigger: Sprint started Actions:
- Send Slack message to #team:
🚀 Sprint {{sprint.name}} Started!
Goal: {{sprint.goal}}
Committed: {{sprint.issuesCount}} issues---
Approval Workflow Rules
Request approval for large stories
Trigger: Issue created Conditions:
- Issue type = Story
- Story points >= 13
Actions:
- Transition to "Pending Approval"
- Assign to product owner
- Send email notification
Auto-approve small bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority IN (Low, Lowest)
Actions:
- Transition to "Approved"
- Add comment: "Auto-approved (low-priority bug)"
Require security review
Trigger: Issue transitioned Conditions:
- Transition is to "Ready for Release"
- Labels contains "security"
Actions:
- Transition to "Security Review"
- Assign to security-team
- Send email to security@company.com
---
Integration Rules
Create GitHub issue
Trigger: Issue transitioned Conditions:
- Transition is to "In Progress"
- Labels contains "needs-tracking"
Actions:
- Send webhook to GitHub API:
{
"title": "{{issue.key}}: {{issue.summary}}",
"body": "{{issue.description}}",
"assignee": "{{issue.assignee.name}}"
}Update Confluence page
Trigger: Issue transitioned Conditions:
- Issue type = Epic
- Transition is to "Done"
Actions:
- Send webhook to Confluence:
- Update epic status page
- Add completion date
---
Quality & Testing Rules
Require test cases for features
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Transition is to "Ready for QA"
- Custom field "Test Cases" is EMPTY
Actions:
- Transition back to "In Progress"
- Add comment: "❌ Test cases required before QA"
Auto-create test issue
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Transition is to "Ready for QA"
Actions:
- Create linked issue:
- Type: Test
- Summary: "Test: {{issue.summary}}"
- Link type: "tested by"
- Assignee: QA team
Flag regression bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Affects version is in released versions
Actions:
- Add label: "regression"
- Set priority to High
- Add comment: "🚨 Regression in released version"
---
Documentation Rules
Require documentation for features
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Labels contains "customer-facing"
- Transition is to "Done"
- Custom field "Documentation Link" is EMPTY
Actions:
- Reopen issue
- Add comment: "📝 Documentation required for customer-facing feature"
Auto-create doc task
Trigger: Issue transitioned Conditions:
- Issue type = Epic
- Transition is to "In Progress"
Actions:
- Create subtask:
- Type: Task
- Summary: "Documentation for {{issue.summary}}"
- Assignee: {{issue.assignee}}
---
Time Tracking Rules
Log work reminder
Trigger: Issue transitioned Conditions:
- Transition is to "Done"
- Time spent is EMPTY
Actions:
- Add comment: "⏱️ Reminder: Please log your time"
Warn on high time spent
Trigger: Work logged Conditions:
- Time spent > original estimate * 1.5
Actions:
- Add comment: "⚠️ Time spent exceeds estimate by 50%"
- Send notification to assignee and project manager
---
Advanced Conditional Rules
Conditional assignee based on priority
Trigger: Issue created Conditions:
- Issue type = Bug
Actions:
- If: Priority = Highest
- Assign to on-call engineer
- Else if: Priority = High
- Assign to team lead
- Else:
- Assign to next available team member
Multi-step approval flow
Trigger: Issue transitioned Conditions:
- Transition is to "Request Approval"
- Budget estimate > $10,000
Actions:
- If: Budget > $50,000
- Assign to CFO
- Send email to executive team
- Else if: Budget > $10,000
- Assign to Director
- Add comment: "Director approval required"
- Add label: "pending-approval"
---
Smart Value Examples
Dynamic assignee based on component
{{issue.components.first.lead.accountId}}Days since created
{{issue.created.diff(now).days}}Conditional message
{{#if(issue.priority.name == "Highest")}}
🚨 CRITICAL
{{else}}
ℹ️ Normal priority
{{/}}List all subtasks
{{#issue.subtasks}}
- {{key}}: {{summary}} ({{status.name}})
{{/}}Calculate completion percentage
{{issue.subtasks.filter(item => item.status.statusCategory.key == "done").size.divide(issue.subtasks.size).multiply(100).round()}}%---
Best Practices
1. Test in sandbox - Always test rules on test project first 2. Start simple - Begin with basic rules, add complexity incrementally 3. Use conditions wisely - Narrow scope to reduce unintended triggers 4. Monitor audit log - Check automation execution history regularly 5. Limit actions - Keep rules focused, don't chain too many actions 6. Name clearly - Use descriptive names: "Auto-assign bugs to component lead" 7. Document rules - Add description explaining purpose and owner 8. Review regularly - Audit rules quarterly, disable unused ones 9. Handle errors - Add error handling for webhooks and integrations 10. Performance - Avoid scheduled rules that query large datasets hourly
Jira Expert Playbook
Read this when you need the full detail behind the SKILL.md map — competencies, workflows, JQL operators/functions, inline JQL examples, advanced features, reporting templates, decision/handoff protocols, best practices, troubleshooting, and success criteria.
Core Competencies (detail)
Project Configuration
- Create and configure Jira projects (Scrum, Kanban, custom)
- Design and implement custom workflows
- Configure issue types, fields, and screens
- Set up project permissions and security schemes
JQL Mastery
- Write advanced JQL queries for any use case
- Create complex filters with multiple conditions
- Optimize query performance
- Build saved filters for team use
Automation & Integration
- Design Jira automation rules
- Configure webhooks and integrations
- Set up email notifications
- Integrate with external tools (Confluence, Slack, etc.)
Reporting & Dashboards
- Create custom dashboards with gadgets
- Build reports for sprint metrics, velocity, burndown
- Configure portfolio-level reporting
- Export data for executive reporting
Workflows
Project Creation
1. Determine project type (Scrum, Kanban, Bug Tracking, etc.) 2. Create project with appropriate template 3. Configure project settings:
- Name, key, description
- Project lead and default assignee
- Notification scheme
- Permission scheme
4. Set up issue types and workflows 5. Configure custom fields if needed 6. Create initial board/backlog view 7. HANDOFF TO: Scrum Master for team onboarding
Workflow Design
1. Map out process states (To Do → In Progress → Done) 2. Define transitions and conditions 3. Add validators, post-functions, and conditions 4. Configure workflow scheme 5. Associate workflow with project 6. Test workflow with sample issues 7. USE: References for complex workflow patterns
JQL Query Building
Basic Structure: field operator value
Common Operators:
=, !=: equals, not equals~, !~: contains, not contains>, <, >=, <=: comparisonin, not in: list membershipis empty, is not emptywas, was in, was notchanged
Powerful JQL Examples:
Find overdue issues:
dueDate < now() AND status != DoneSprint burndown issues:
sprint = 23 AND status changed TO "Done" DURING (startOfSprint(), endOfSprint())Find stale issues:
updated < -30d AND status != DoneCross-project epic tracking:
"Epic Link" = PROJ-123 ORDER BY rankVelocity calculation:
sprint in closedSprints() AND resolution = DoneTeam capacity:
assignee in (user1, user2) AND sprint in openSprints()Dashboard Creation
1. Create new dashboard (personal or shared) 2. Add relevant gadgets:
- Filter Results (JQL-based)
- Sprint Burndown
- Velocity Chart
- Created vs Resolved
- Pie Chart (status distribution)
3. Arrange layout for readability 4. Configure automatic refresh 5. Share with appropriate teams 6. HANDOFF TO: Senior PM or Scrum Master for use
Automation Rules
1. Define trigger (issue created, field changed, scheduled) 2. Add conditions (if applicable) 3. Define actions:
- Update field
- Send notification
- Create subtask
- Transition issue
- Post comment
4. Test automation with sample data 5. Enable and monitor 6. USE: References for complex automation patterns
Advanced Features
Custom Fields
When to Create:
- Track data not in standard fields
- Capture process-specific information
- Enable advanced reporting
Field Types:
- Text: Short text, paragraph
- Numeric: Number, decimal
- Date: Date picker, date-time
- Select: Single select, multi-select, cascading
- User: User picker, multi-user picker
Configuration: 1. Create custom field 2. Configure field context (which projects/issue types) 3. Add to appropriate screens 4. Update search templates if needed
Issue Linking
Link Types:
- Blocks / Is blocked by
- Relates to
- Duplicates / Is duplicated by
- Clones / Is cloned by
- Epic-Story relationship
Best Practices:
- Use Epic linking for feature grouping
- Use blocking links to show dependencies
- Document link reasons in comments
Permissions & Security
Permission Schemes:
- Browse Projects
- Create/Edit/Delete Issues
- Administer Projects
- Manage Sprints
Security Levels:
- Define confidential issue visibility
- Control access to sensitive data
- Audit security changes
Bulk Operations
Bulk Change: 1. Use JQL to find target issues 2. Select bulk change operation 3. Choose fields to update 4. Preview changes 5. Execute and confirm 6. Monitor background task
Bulk Transitions:
- Move multiple issues through workflow
- Useful for sprint cleanup
- Requires appropriate permissions
JQL Functions Reference
Date Functions:
startOfDay(),endOfDay()startOfWeek(),endOfWeek()startOfMonth(),endOfMonth()startOfYear(),endOfYear()
Sprint Functions:
openSprints()closedSprints()futureSprints()
User Functions:
currentUser()membersOf("group")
Advanced Functions:
issueHistory()linkedIssues()issuesWithFixVersions()
Reporting Templates
Sprint Report:
project = PROJ AND sprint = 23Team Velocity:
assignee in (team) AND sprint in closedSprints() AND resolution = DoneBug Trend:
type = Bug AND created >= -30dBlocker Analysis:
priority = Blocker AND status != DoneDecision Framework
When to Escalate to Atlassian Admin:
- Need new project permission scheme
- Require custom workflow scheme across org
- User provisioning or deprovisioning
- License or billing questions
- System-wide configuration changes
When to Collaborate with Scrum Master:
- Sprint board configuration
- Backlog prioritization views
- Team-specific filters
- Sprint reporting needs
When to Collaborate with Senior PM:
- Portfolio-level reporting
- Cross-project dashboards
- Executive visibility needs
- Multi-project dependencies
Handoff Protocols
FROM Senior PM:
- Project structure requirements
- Workflow and field needs
- Reporting requirements
- Integration needs
TO Senior PM:
- Cross-project metrics
- Issue trends and patterns
- Workflow bottlenecks
- Data quality insights
FROM Scrum Master:
- Sprint board configuration requests
- Workflow optimization needs
- Backlog filtering requirements
- Velocity tracking setup
TO Scrum Master:
- Configured sprint boards
- Velocity reports
- Burndown charts
- Team capacity views
Best Practices
Data Quality:
- Enforce required fields
- Use field validation
- Regular cleanup of stale issues
- Consistent naming conventions
Performance:
- Optimize JQL queries
- Limit dashboard gadgets
- Use saved filters
- Archive old projects
Governance:
- Document workflow rationale
- Version control for schemes
- Change management for major updates
- Regular permission audits
Atlassian MCP Integration
Primary Tool: Jira MCP Server
Key Operations:
- Create and configure projects
- Execute JQL queries for data extraction
- Update issue fields and statuses
- Create and manage sprints
- Generate reports and dashboards
- Configure workflows and automation
- Manage boards and filters
Integration Points:
- Pull metrics for Senior PM reporting
- Configure sprint boards for Scrum Master
- Create documentation pages for Confluence Expert
- Support template creation for Template Creator
Troubleshooting
| Problem | Likely Cause | Resolution |
|---|---|---|
| JQL queries return unexpected results or time out | Overly broad filters, missing indexes, or incorrect field references | Add specificity with project/date constraints; use ORDER BY sparingly on large datasets; verify field names with autocomplete |
| Automation rules fire multiple times for a single event | Cascading triggers where one rule's action triggers another rule | Add conditions to prevent loops (e.g., check initiator is automation); use the audit log to trace execution chains |
| Board shows wrong issues or missing cards | Filter behind the board is misconfigured or sprint assignment is incorrect | Verify the board's saved filter JQL; check that issues have the correct sprint field value; review board settings > general |
| Workflow transitions fail with validator errors | Required fields not populated, or post-function order is incorrect | Check validator configuration; ensure required fields have values before transition; reorder post-functions so field-setting happens before validation |
| Custom fields not appearing on screens | Field added to wrong screen scheme or issue type context is too narrow | Verify field context includes the target project and issue type; confirm the correct screen scheme is associated with the project |
| Bulk operations fail or time out | Too many issues selected (>1000) or complex post-functions on transitions | Reduce batch size to 100-200 issues; disable non-essential automation rules temporarily during bulk operations |
| Dashboard gadgets show "No data" | Underlying filter returns no results for the current user due to permissions | Verify the filter is shared with the dashboard viewers; check project permission schemes |
Success Criteria
- All active projects use standardized workflow schemes (no more than 5 unique workflows org-wide)
- JQL saved filters cover 90%+ of recurring reporting needs without ad-hoc queries
- Automation rules reduce manual status updates by 60%+ across managed projects
- Data quality score (required fields populated, consistent labeling) exceeds 85% for active issues
- Average dashboard load time stays under 3 seconds with all gadgets rendering
- New project setup (from request to team-ready board) completes within 2 business days
- Zero critical permission escalations caused by misconfigured schemes per quarter
JQL Query Examples
Sprint Queries
Current sprint issues:
sprint IN openSprints() ORDER BY rankIssues in specific sprint:
sprint = "Sprint 23" ORDER BY priority DESCAll sprint work (current and backlog):
project = ABC AND issuetype IN (Story, Bug, Task)
ORDER BY sprint DESC, rankUnscheduled stories:
project = ABC AND issuetype = Story AND sprint IS EMPTY
AND status != Done ORDER BY priority DESCSpillover from last sprint:
sprint IN closedSprints() AND sprint NOT IN (latestReleasedVersion())
AND status != Done ORDER BY created DESCSprint completion rate:
sprint = "Sprint 23" AND status = DoneUser & Team Queries
My open issues:
assignee = currentUser() AND status != Done
ORDER BY priority DESC, created ASCUnassigned in my project:
project = ABC AND assignee IS EMPTY AND status != Done
ORDER BY priority DESCIssues I'm watching:
watcher = currentUser() AND status != DoneTeam workload:
assignee IN membersOf("engineering") AND status IN ("In Progress", "In Review")
ORDER BY assignee, priority DESCIssues I reported that are still open:
reporter = currentUser() AND status != Done ORDER BY created DESCIssues commented on by me:
comment ~ currentUser() AND status != DoneDate Range Queries
Created today:
created >= startOfDay() ORDER BY created DESCUpdated in last 7 days:
updated >= -7d ORDER BY updated DESCCreated this week:
created >= startOfWeek() AND created <= endOfWeek()Created this month:
created >= startOfMonth() AND created <= endOfMonth()Not updated in 30 days:
status != Done AND updated <= -30d ORDER BY updated ASCResolved yesterday:
resolved >= startOfDay(-1d) AND resolved < startOfDay()Due this week:
duedate >= startOfWeek() AND duedate <= endOfWeek() AND status != DoneOverdue:
duedate < now() AND status != Done ORDER BY duedate ASCStatus & Workflow Queries
In Progress issues:
project = ABC AND status = "In Progress" ORDER BY assigneeBlocked issues:
project = ABC AND labels = blocked AND status != DoneIssues in review:
project = ABC AND status IN ("Code Review", "QA Review", "Pending Approval")
ORDER BY updated ASCReady for development:
project = ABC AND status = "Ready" AND sprint IS EMPTY
ORDER BY priority DESCRecently done:
project = ABC AND status = Done AND resolved >= -7d
ORDER BY resolved DESCStatus changed today:
status CHANGED AFTER startOfDay() ORDER BY updated DESCLong-running in progress:
status = "In Progress" AND status CHANGED BEFORE -14d
ORDER BY status CHANGED ASCPriority & Type Queries
High priority bugs:
issuetype = Bug AND priority IN (Highest, High) AND status != Done
ORDER BY priority DESC, created ASCCritical blockers:
priority = Highest AND status != Done ORDER BY created ASCAll epics:
issuetype = Epic ORDER BY status, priority DESCStories without acceptance criteria:
issuetype = Story AND "Acceptance Criteria" IS EMPTY AND status = BacklogTechnical debt:
labels = tech-debt AND status != Done ORDER BY priority DESCComplex Multi-Condition Queries
My team's sprint work:
sprint IN openSprints()
AND assignee IN membersOf("engineering")
AND status != Done
ORDER BY assignee, priority DESCBugs created this month, not in sprint:
issuetype = Bug
AND created >= startOfMonth()
AND sprint IS EMPTY
AND status != Done
ORDER BY priority DESC, created DESCHigh-priority work needing attention:
project = ABC
AND priority IN (Highest, High)
AND status IN ("In Progress", "In Review")
AND updated <= -3d
ORDER BY priority DESC, updated ASCStale issues:
project = ABC
AND status NOT IN (Done, Cancelled)
AND (assignee IS EMPTY OR updated <= -30d)
ORDER BY created ASCEpic progress:
"Epic Link" = ABC-123 ORDER BY status, rankComponent & Version Queries
Issues in component:
project = ABC AND component = "Frontend" AND status != DoneIssues without component:
project = ABC AND component IS EMPTY AND status != DoneTarget version:
fixVersion = "v2.0" ORDER BY status, priority DESCReleased versions:
fixVersion IN releasedVersions() ORDER BY fixVersion DESCLabel & Text Search Queries
Issues with label:
labels = urgent AND status != DoneMultiple labels (AND):
labels IN (frontend, bug) AND status != DoneSearch in summary:
summary ~ "authentication" ORDER BY created DESCSearch in summary and description:
text ~ "API integration" ORDER BY created DESCIssues with empty description:
description IS EMPTY AND issuetype = StoryPerformance-Optimized Queries
Good - Specific project first:
project = ABC AND status = "In Progress" AND assignee = currentUser()Bad - User filter first:
assignee = currentUser() AND status = "In Progress" AND project = ABCGood - Use functions:
sprint IN openSprints() AND status != DoneBad - Hardcoded sprint:
sprint = "Sprint 23" AND status != DoneGood - Specific date:
created >= 2024-01-01 AND created <= 2024-01-31Bad - Relative with high cost:
created >= -365d AND created <= -335dReporting Queries
Velocity calculation:
sprint = "Sprint 23" AND status = DoneThen sum story points
Bug rate:
project = ABC AND issuetype = Bug AND created >= startOfMonth()Average cycle time:
project = ABC AND resolved >= startOfMonth()
AND resolved <= endOfMonth()Calculate time from In Progress to Done
Stories delivered this quarter:
project = ABC AND issuetype = Story
AND resolved >= startOfYear() AND resolved <= endOfQuarter()Team capacity:
assignee IN membersOf("engineering")
AND sprint IN openSprints()Sum original estimates
Notification & Watching Queries
Issues I need to review:
status = "Pending Review" AND assignee = currentUser()Issues assigned to me, high priority:
assignee = currentUser() AND priority IN (Highest, High)
AND status != DoneIssues created by me, not resolved:
reporter = currentUser() AND status != Done
ORDER BY created DESCAdvanced Functions
Issues changed from status:
status WAS "In Progress" AND status = "Done"
AND status CHANGED AFTER startOfWeek()Assignee changed:
assignee CHANGED BY currentUser() AFTER -7dIssues re-opened:
status WAS Done AND status != Done ORDER BY updated DESCLinked issues:
issue IN linkedIssues("ABC-123") ORDER BY issuetypeParent epic:
parent = ABC-123 ORDER BY rankSaved Filter Examples
Daily Standup Filter:
assignee = currentUser() AND sprint IN openSprints()
AND status != Done ORDER BY priority DESCTeam Sprint Board Filter:
project = ABC AND sprint IN openSprints() ORDER BY rankBugs Dashboard Filter:
project = ABC AND issuetype = Bug AND status != Done
ORDER BY priority DESC, created ASCTech Debt Backlog:
project = ABC AND labels = tech-debt AND status = Backlog
ORDER BY priority DESCNeeds Triage:
project = ABC AND status = "To Triage"
AND created >= -7d ORDER BY created ASCRed Flags: Jira Expert
Common ways this skill's output goes wrong -- concrete examples, why they're bad, and how to fix them.
How to use this document
Scan every Jira configuration change, JQL query, automation rule, or dashboard before publishing to a team. Each red flag has bad and good quoted examples.
---
Red Flag 1: Workflow Proliferation
Symptom. Each project has its own custom workflow with 12+ states; the org has 47 distinct workflows across 30 projects. Why it's bad. Workflow proliferation makes cross-project reporting impossible (no shared statuses), trains every new joiner from scratch, and means automation rules cannot be reused. The Jira admin spends most of their week mapping workflow A's "In Review" to workflow B's "Code Review In Progress". Bad example:
"Project A workflow: Open -> In Triage -> Ready -> In Progress -> In Code Review -> In QA -> Ready for UAT -> In UAT -> Done -> Closed -> Archived.
Project B workflow: To Do -> Doing -> Reviewing -> Validated -> Released.
(Cross-project velocity report breaks.)"
Good example:
"Org-wide shared workflow: Open -> In Progress -> In Review -> Done. Custom states only in projects with a defended exception (e.g., regulated team needs an explicit 'Awaiting QA Signoff' state). Exceptions logged in the Jira-admin charter."
How to catch it. Count distinct active workflows. > 5 across the org = proliferation; consolidate.
---
Red Flag 2: JQL Spaghetti
Symptom. Filters and dashboards use 200-character JQL queries with nested AND / OR / NOT chains nobody can read. Why it's bad. Spaghetti JQL is unmaintainable. The original author leaves; the query becomes load-bearing infrastructure nobody dares touch. Subtle bugs (an OR scoping wrong, a missing parenthesis) silently exclude tickets. Bad example:
"project in (PROJ, OPS, INFRA) AND ((status not in (Done, Closed) AND assignee in membersOf('eng-team')) OR (labels in (urgent, blocker) AND status != Done)) AND created >= -90d AND (component != 'legacy' OR resolution = Unresolved)"
Good example:
"Saved filterEng Active Work(definition stored injira-filters.mdwith comments):
```
project in (PROJ, OPS, INFRA)
AND status != Done
AND (assignee in membersOf('eng-team') OR labels in (urgent, blocker))
AND created >= -90d
```
Then a dashboard widget references the named filter, not raw JQL."
How to catch it. Any inline JQL > 120 chars or > 2 levels of parens = refactor into named saved filters with comments.
---
Red Flag 3: Dashboards Nobody Opens
Symptom. The team has 14 Jira dashboards, all created during onboarding, used by 0 people in the last 30 days. Why it's bad. Dashboards are infrastructure. Unused ones drift, embed stale assumptions, and mislead new joiners who do find them. The right number of dashboards is the number people actually use. Bad example:
"Dashboards in the team's space: Sprint Health, Quarterly Velocity, Bug Bash 2024, Q1 Retrospective Data, Hiring Pipeline, Old Atlas Project, ..."
Good example:
"Quarterly Jira-hygiene review: open dashboard audit log; any dashboard with 0 views in 30 days is archived. Standing dashboards: 3 (Sprint Health, Quarter-View, Incident Tracker). New dashboards require an owner + a use case stated in the description."
How to catch it. Run Dashboard activity report (Jira admin); any dashboard with 0 views in 30 days = archive.
---
Red Flag 4: Custom Fields Sprawl
Symptom. The Jira instance has 340 custom fields, half referencing departed team configurations. Why it's bad. Custom-field sprawl slows Jira queries, clutters issue-creation forms, and increases the chance of inconsistent data ('Story Points' vs 'Story Pts' vs 'Estimate' all in use). It also exhausts Jira's custom-field index limits. Bad example:
"Issue creation form has 47 fields, half pre-filled with default values nobody understands, 6 abandoned fields from a 2022 process. Search performance degraded by ~30%."
Good example:
"Custom fields: 28, each with a documented owner and a use case. Quarterly audit removes orphaned fields. Issue creation forms use field configuration schemes to hide non-essential fields per issue type (Bug: 6 fields; Story: 8 fields; Spike: 5 fields)."
How to catch it. Custom field count > 80 = audit. Any field with 0 issues touched in 90 days = candidate for removal.
---
Red Flag 5: Automation Rules That Conflict
Symptom. Two automation rules fire on the same transition: one moves the ticket to "Done", the other transitions it back to "In Review". Tickets oscillate. Why it's bad. Conflicting rules produce non-deterministic Jira behavior. Engineers cannot trust the board state. Worse, the bug is hard to diagnose because each rule looks correct in isolation. Bad example:
"Rule 1: 'On commit message [DONE-XYZ], transition to Done.'
Rule 2: 'On status change to Done without a QA sign-off, transition back to In Review.'
(Tickets bounce.)"
Good example:
"Automation rules are catalogued in jira-automation.md with each rule's trigger, condition, action, and known interactions. Quarterly review tests all rules together in a staging Jira project before promoting to production."How to catch it. Any ticket that has changed status > 4 times in a day = inspect for rule conflict.
---
Red Flag 6: Story Points as a Performance Metric
Symptom. Manager pulls "story points completed per engineer per sprint" and uses it in performance reviews. Why it's bad. Story points are a team relative-sizing tool, not an individual productivity metric. Treating them as performance data trains engineers to inflate estimates, decline complex work, and avoid pair programming. Velocity collapses as a planning signal. Bad example:
"Performance review: 'Sarah completed 38 SP last sprint; Tom completed 18. Tom needs a PIP.' (Ignoring that Tom's work included on-call + a complex migration.)"
Good example:
"Story points are used only at the team-level (sprint capacity + velocity forecasting). Performance is reviewed via the career-ladder rubric (pm-career-ladder skill), not Jira numbers. Engineering managers have explicit org-wide guidance: 'no individual story-point reports'."How to catch it. Any Jira filter / dashboard grouping story points by assignee = remove. See scrum-master/references/red-flags.md Red Flag 1.
---
Red Flag 7: Sprints That Never End
Symptom. "Sprint 47" has been "active" for 8 weeks because tickets keep getting carried over. Why it's bad. A sprint is a time-boxed iteration. Letting it run past its end-date corrupts velocity, makes retrospectives meaningless, and erodes the "complete what you commit" discipline. Worse, it hides scope creep -- the sprint just expands. Bad example:
"Sprint 47: started March 1, originally 2 weeks. Today is May 20. 22 tickets still open. Sprint 48 cannot start because 47 has not closed."
Good example:
"Sprint discipline: sprints close on their scheduled end-date, regardless of completion. Incomplete tickets are explicitly moved (either back to the backlog with a retrospective lesson, or into the next sprint with a documented reason). Closing a sprint is the scrum master's responsibility on the scheduled day."
How to catch it. Sprint open longer than 1.5x its planned duration = close it; write a retro lesson.
---
Red Flag 8: Epic / Story / Subtask Hierarchy Inversion
Symptom. Stories with 12 subtasks; subtasks larger than the parent story; epics containing one story. Why it's bad. The hierarchy exists to keep work coherent. Inversions signal under-thought structure -- subtasks-as-stories is a misuse of the type (subtasks are within-sprint slices). Reports break: epic-level burndown shows tiny epics; subtask totals dwarf parent stories. Bad example:
"Epic 'Bulk Edit' contains 1 story 'Implement Bulk Edit', which contains 22 subtasks ranging from UI to backend to DB migration."
Good example:
"Epic 'Bulk Edit' contains 6 stories (one per vertical slice from story-splitting). Each story has 2-5 subtasks for within-sprint coordination only (e.g. 'write tests', 'cut release'). Stories are the unit of value; subtasks are the unit of within-sprint work."How to catch it. Any story with > 6 subtasks, or any subtask > 5 SP = restructure.
---
Red Flag 9: Inconsistent Issue Types Across Projects
Symptom. Project A uses 'Bug, Story, Task, Epic'. Project B uses 'Defect, Feature, Improvement, Initiative'. Why it's bad. Inconsistent types break org-wide reporting and make every cross-project ritual (release notes, security audit, executive reporting) require manual translation. It is also confusing to people who work across projects. Bad example:
"Org-wide bug report: cannot be generated because 'Bug' and 'Defect' are distinct issue types in different projects."
Good example:
"Issue-type scheme is org-standardized: Story, Task, Bug, Epic, Spike, Subtask. New projects inherit the scheme. Exceptions for regulated teams approved via the Jira-admin charter."
How to catch it. Cross-project issue-type count > 8 distinct = consolidate.
---
Red Flag 10: Permissions Configured Per-Project By Hand
Symptom. Each project has a hand-edited permission scheme; the QA team has read access in 11 of 14 projects and nobody knows why the 3 are different. Why it's bad. Per-project hand-editing is unscalable, error-prone, and creates security risk (forgotten access). Reconciling who-has-what-where takes hours per incident. Bad example:
"47 distinct permission schemes across 30 projects. Half were copies of another scheme with one tweak."
Good example:
"3 org-wide permission schemes: 'Open' (read-all, write-team), 'Restricted' (read-team, write-team), 'Confidential' (read-named-roles). Projects pick one. Role memberships drive access. Documented in jira-permissions.md."How to catch it. Permission schemes > 5 = consolidate.
---
Red Flag 11: JQL in Documentation Without an As-of Date
Symptom. "To see open bugs: project = PROJ AND issuetype = Bug AND status != Done." Documentation written 2 years ago. Why it's bad. JQL semantics shift as projects evolve (issue types renamed, statuses added). Old JQL silently returns wrong results. The reader trusts the documentation; the documentation is wrong. Bad example:
"[Old runbook]: 'Open bugs: project = PROJ AND issuetype = Bug AND status != Done. (No date, no verification.)'"Good example:
"[Runbook, 2026-05-21]: 'Open bugs: saved filter OPEN-BUGS-PROJ (definition link). Verified against current schema; renewed on the quarterly runbook review.' (Filter ownership: <name>; expires if not renewed.)"How to catch it. Any JQL in a runbook with no date / no saved-filter wrapper = re-validate.
---
Red Flag Quick Reference
| # | Anti-pattern | One-line check |
|---|---|---|
| 1 | Workflow proliferation | > 5 distinct workflows = consolidate |
| 2 | JQL spaghetti | Inline JQL > 120 chars = saved filter |
| 3 | Dashboards nobody opens | 0 views in 30 days = archive |
| 4 | Custom field sprawl | > 80 custom fields = audit |
| 5 | Automation rules conflict | Tickets oscillating between states |
| 6 | SP as performance metric | No SP-by-assignee filters |
| 7 | Sprints that never end | Open > 1.5x planned duration = close |
| 8 | Hierarchy inversion | Story with > 6 subtasks = restructure |
| 9 | Inconsistent issue types | > 8 cross-project types = consolidate |
| 10 | Per-project permissions | Permission schemes > 5 = consolidate |
| 11 | JQL in docs without date | All runbook JQL via saved filters |
Related Reading
SKILL.md-- Jira administration patternsreferences/jql-patterns.md-- canonical JQL recipesreferences/automation-recipes.md-- standard automation rules- Sibling skill:
linear-expert/-- Linear's equivalent patterns - Sibling skill:
atlassian-admin/-- broader Atlassian-suite admin - Sibling skill:
scrum-master/-- the data Jira-expert serves - Sibling skill:
confluence-expert/-- the documentation paired with Jira
#!/usr/bin/env python3
"""Board Optimizer - Analyze Jira board configuration and recommend optimizations.
Evaluates board settings (columns, WIP limits, swimlanes, filters) against
best practices and produces actionable recommendations.
Usage:
python board_optimizer.py --board board.json
python board_optimizer.py --board board.json --json
python board_optimizer.py --example
"""
import argparse
import json
import sys
BEST_PRACTICES = {
"columns": {
"min": 3,
"max": 8,
"recommended": ["To Do", "In Progress", "In Review", "Done"],
},
"wip_limits": {
"enabled": True,
"rule": "WIP limit per column should be ~1.5x team size for that column",
},
"swimlanes": {
"recommended_types": ["Expedite", "Standard", "Fixed Date"],
},
}
def load_data(path: str) -> dict:
"""Load board configuration from JSON file."""
with open(path, "r") as f:
return json.load(f)
def analyze_board(data: dict) -> dict:
"""Analyze board configuration against best practices."""
board_name = data.get("name", "Unknown Board")
board_type = data.get("type", "scrum")
team_size = data.get("team_size", 5)
columns = data.get("columns", [])
swimlanes = data.get("swimlanes", [])
filter_jql = data.get("filter_jql", "")
issues = []
recommendations = []
score = 100 # Start at 100, deduct for problems
# Column analysis
col_count = len(columns)
if col_count < BEST_PRACTICES["columns"]["min"]:
issues.append(f"Too few columns ({col_count}). Minimum recommended: {BEST_PRACTICES['columns']['min']}")
recommendations.append("Add distinct columns for key workflow states (e.g., 'In Review' or 'QA') to improve visibility.")
score -= 15
elif col_count > BEST_PRACTICES["columns"]["max"]:
issues.append(f"Too many columns ({col_count}). Maximum recommended: {BEST_PRACTICES['columns']['max']}")
recommendations.append("Consolidate columns with low usage. Too many columns create visual noise and slow scanning.")
score -= 10
# WIP limit analysis
columns_without_wip = []
columns_with_high_wip = []
for col in columns:
name = col.get("name", "Unknown")
wip = col.get("wip_limit")
if name.lower() in ("to do", "backlog", "done"):
continue # WIP limits not typically needed for start/end columns
if wip is None or wip == 0:
columns_without_wip.append(name)
elif wip > team_size * 2:
columns_with_high_wip.append({"name": name, "wip": wip, "recommended": int(team_size * 1.5)})
if columns_without_wip:
issues.append(f"No WIP limits on active columns: {', '.join(columns_without_wip)}")
recommendations.append(f"Set WIP limits on active columns. Start with ~{int(team_size * 1.5)} (1.5x team size) and adjust based on flow.")
score -= 20
if columns_with_high_wip:
for c in columns_with_high_wip:
issues.append(f"'{c['name']}' WIP limit ({c['wip']}) is too high. Recommended: ~{c['recommended']}")
recommendations.append("Lower WIP limits to expose bottlenecks. High limits mask flow problems.")
score -= 10
# Swimlane analysis
if not swimlanes:
if board_type == "kanban":
recommendations.append("Consider adding swimlanes (e.g., 'Expedite' and 'Standard') to distinguish priority classes of service.")
score -= 5
else:
has_expedite = any(s.get("name", "").lower() in ("expedite", "urgent", "blocker") for s in swimlanes)
if not has_expedite:
recommendations.append("Add an 'Expedite' swimlane for urgent items with a strict WIP limit of 1.")
# Filter analysis
if not filter_jql:
issues.append("No board filter JQL defined")
recommendations.append("Define a specific board filter to prevent irrelevant issues from appearing. Example: project = PROJ AND type in (Story, Bug, Task)")
score -= 10
else:
if "project" not in filter_jql.lower():
recommendations.append("Board filter should include a project clause to prevent cross-project noise.")
score -= 5
# Done column analysis
done_columns = [c for c in columns if c.get("name", "").lower() == "done"]
if done_columns:
done_col = done_columns[0]
if not done_col.get("auto_close", False):
recommendations.append("Enable automatic resolution when issues move to 'Done' column to keep data clean.")
# Board type specific
if board_type == "scrum":
if not data.get("estimation_enabled", True):
recommendations.append("Enable story point estimation for velocity tracking and sprint planning accuracy.")
score -= 5
elif board_type == "kanban":
if not any(c.get("wip_limit") for c in columns):
issues.append("Kanban board has no WIP limits -- this defeats the purpose of Kanban")
score -= 25
# Card layout
card_fields = data.get("card_fields", [])
essential_fields = ["assignee", "priority", "story_points"]
missing_fields = [f for f in essential_fields if f not in card_fields]
if missing_fields and card_fields:
recommendations.append(f"Add these fields to card layout for better visibility: {', '.join(missing_fields)}")
# Score classification
if score >= 90:
rating = "Excellent"
elif score >= 70:
rating = "Good"
elif score >= 50:
rating = "Needs Improvement"
else:
rating = "Poor"
return {
"board_name": board_name,
"board_type": board_type,
"team_size": team_size,
"score": max(0, score),
"rating": rating,
"column_count": col_count,
"columns": [{"name": c.get("name"), "wip_limit": c.get("wip_limit"), "statuses": c.get("statuses", [])} for c in columns],
"swimlane_count": len(swimlanes),
"issues": issues,
"recommendations": recommendations,
}
def print_report(result: dict) -> None:
"""Print human-readable board analysis."""
print(f"\nBoard Analysis: {result['board_name']}")
print(f"Type: {result['board_type'].title()} | Team Size: {result['team_size']}")
print("=" * 55)
print(f"Score: {result['score']}/100 ({result['rating']})")
print()
print("Column Configuration:")
for col in result["columns"]:
wip = f"WIP: {col['wip_limit']}" if col.get("wip_limit") else "No WIP limit"
print(f" [{col['name']:<20}] {wip}")
if result["issues"]:
print(f"\nIssues Found ({len(result['issues'])}):")
for issue in result["issues"]:
print(f" ! {issue}")
if result["recommendations"]:
print(f"\nRecommendations ({len(result['recommendations'])}):")
for i, rec in enumerate(result["recommendations"], 1):
print(f" {i}. {rec}")
print()
def print_example() -> None:
"""Print example board configuration JSON."""
example = {
"name": "Team Alpha Board",
"type": "kanban",
"team_size": 6,
"filter_jql": "project = ALPHA AND type in (Story, Bug, Task)",
"estimation_enabled": True,
"columns": [
{"name": "Backlog", "wip_limit": None, "statuses": ["Backlog"]},
{"name": "To Do", "wip_limit": None, "statuses": ["To Do"]},
{"name": "In Progress", "wip_limit": 4, "statuses": ["In Progress"]},
{"name": "In Review", "wip_limit": None, "statuses": ["In Review"]},
{"name": "Done", "wip_limit": None, "auto_close": True, "statuses": ["Done"]},
],
"swimlanes": [
{"name": "Expedite", "jql": "priority = Blocker"},
{"name": "Standard", "jql": ""},
],
"card_fields": ["assignee", "priority"],
}
print(json.dumps(example, indent=2))
def main():
parser = argparse.ArgumentParser(
description="Analyze Jira board configuration and recommend optimizations."
)
parser.add_argument("--board", type=str, help="Path to board config JSON file")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
parser.add_argument("--example", action="store_true", help="Print example board config and exit")
args = parser.parse_args()
if args.example:
print_example()
return
if not args.board:
parser.error("--board is required (use --example to see the expected format)")
data = load_data(args.board)
result = analyze_board(data)
if args.json:
print(json.dumps(result, indent=2))
else:
print_report(result)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Issue Quality Checker - Evaluate Jira issue quality against best practices.
Reads a set of Jira issues and scores them for completeness, clarity, and
adherence to team standards. Produces a quality report with actionable fixes.
Usage:
python issue_quality_checker.py --issues issues.json
python issue_quality_checker.py --issues issues.json --json
python issue_quality_checker.py --example
"""
import argparse
import json
import re
import sys
QUALITY_CHECKS = {
"has_summary": {"weight": 10, "description": "Issue has a non-empty summary"},
"summary_length": {"weight": 5, "description": "Summary is 10-80 characters (concise but descriptive)"},
"has_description": {"weight": 15, "description": "Issue has a non-empty description"},
"description_length": {"weight": 5, "description": "Description is at least 50 characters"},
"has_acceptance_criteria": {"weight": 15, "description": "Description contains acceptance criteria"},
"has_assignee": {"weight": 10, "description": "Issue is assigned to someone"},
"has_priority": {"weight": 5, "description": "Priority is set"},
"has_story_points": {"weight": 10, "description": "Story points are estimated (for stories)"},
"has_labels": {"weight": 5, "description": "At least one label is applied"},
"has_epic_link": {"weight": 10, "description": "Issue is linked to an epic"},
"has_sprint": {"weight": 5, "description": "Issue is assigned to a sprint"},
"no_vague_summary": {"weight": 5, "description": "Summary does not use vague words like 'fix stuff', 'update things'"},
}
def load_data(path: str) -> dict:
"""Load issues from JSON file."""
with open(path, "r") as f:
return json.load(f)
VAGUE_PATTERNS = [
r"\bfix stuff\b",
r"\bupdate things\b",
r"\bmisc\b",
r"\bvarious\b",
r"\btodo\b",
r"\bwip\b",
r"\btest$",
r"^bug$",
r"^task$",
r"^story$",
]
def check_issue(issue: dict) -> dict:
"""Run quality checks on a single issue."""
key = issue.get("key", "UNKNOWN")
issue_type = issue.get("type", "Story").lower()
summary = issue.get("summary", "").strip()
description = issue.get("description", "").strip()
assignee = issue.get("assignee", "")
priority = issue.get("priority", "")
story_points = issue.get("story_points")
labels = issue.get("labels", [])
epic_link = issue.get("epic_link", "")
sprint = issue.get("sprint", "")
results = {}
total_possible = 0
total_earned = 0
# has_summary
check = QUALITY_CHECKS["has_summary"]
passed = len(summary) > 0
results["has_summary"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# summary_length
check = QUALITY_CHECKS["summary_length"]
passed = 10 <= len(summary) <= 80
results["summary_length"] = {"passed": passed, "weight": check["weight"], "value": len(summary)}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_description
check = QUALITY_CHECKS["has_description"]
passed = len(description) > 0
results["has_description"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# description_length
check = QUALITY_CHECKS["description_length"]
passed = len(description) >= 50
results["description_length"] = {"passed": passed, "weight": check["weight"], "value": len(description)}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_acceptance_criteria
check = QUALITY_CHECKS["has_acceptance_criteria"]
ac_patterns = [r"acceptance criteria", r"\bac\b.*:", r"given.*when.*then", r"\[ \]", r"\[x\]", r"- \[ \]"]
passed = any(re.search(p, description, re.IGNORECASE) for p in ac_patterns)
results["has_acceptance_criteria"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_assignee
check = QUALITY_CHECKS["has_assignee"]
passed = bool(assignee)
results["has_assignee"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_priority
check = QUALITY_CHECKS["has_priority"]
passed = bool(priority) and priority.lower() not in ("none", "")
results["has_priority"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_story_points (only for stories)
check = QUALITY_CHECKS["has_story_points"]
if issue_type in ("story", "task"):
passed = story_points is not None and story_points > 0
results["has_story_points"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_labels
check = QUALITY_CHECKS["has_labels"]
passed = len(labels) > 0
results["has_labels"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_epic_link
check = QUALITY_CHECKS["has_epic_link"]
passed = bool(epic_link)
results["has_epic_link"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# has_sprint
check = QUALITY_CHECKS["has_sprint"]
passed = bool(sprint)
results["has_sprint"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
# no_vague_summary
check = QUALITY_CHECKS["no_vague_summary"]
passed = not any(re.search(p, summary, re.IGNORECASE) for p in VAGUE_PATTERNS)
results["no_vague_summary"] = {"passed": passed, "weight": check["weight"]}
total_possible += check["weight"]
if passed:
total_earned += check["weight"]
score = round(total_earned / total_possible * 100, 1) if total_possible > 0 else 0
# Failures
failures = [
{"check": k, "description": QUALITY_CHECKS[k]["description"]}
for k, v in results.items()
if not v["passed"]
]
return {
"key": key,
"type": issue.get("type", "Unknown"),
"summary": summary[:60] + "..." if len(summary) > 60 else summary,
"score": score,
"checks_passed": sum(1 for v in results.values() if v["passed"]),
"checks_total": len(results),
"failures": failures,
}
def analyze_issues(data: dict) -> dict:
"""Analyze all issues and produce aggregate report."""
project = data.get("project", "Unknown")
issues = data.get("issues", [])
results = [check_issue(issue) for issue in issues]
scores = [r["score"] for r in results]
avg_score = round(sum(scores) / len(scores), 1) if scores else 0
# Aggregate failure counts
failure_counts = {}
for r in results:
for f in r["failures"]:
check = f["check"]
failure_counts[check] = failure_counts.get(check, 0) + 1
top_failures = sorted(failure_counts.items(), key=lambda x: x[1], reverse=True)
# Quality distribution
excellent = sum(1 for s in scores if s >= 90)
good = sum(1 for s in scores if 70 <= s < 90)
fair = sum(1 for s in scores if 50 <= s < 70)
poor = sum(1 for s in scores if s < 50)
if avg_score >= 85:
rating = "Excellent"
elif avg_score >= 70:
rating = "Good"
elif avg_score >= 50:
rating = "Needs Improvement"
else:
rating = "Poor"
return {
"project": project,
"total_issues": len(issues),
"average_score": avg_score,
"rating": rating,
"distribution": {"excellent": excellent, "good": good, "fair": fair, "poor": poor},
"top_failures": [
{"check": t[0], "count": t[1], "description": QUALITY_CHECKS.get(t[0], {}).get("description", "")}
for t in top_failures[:5]
],
"issues": results,
}
def print_report(result: dict) -> None:
"""Print human-readable quality report."""
print(f"\nIssue Quality Report: {result['project']}")
print(f"Issues Analyzed: {result['total_issues']}")
print("=" * 60)
print(f"Average Quality Score: {result['average_score']:.1f}% ({result['rating']})")
d = result["distribution"]
print(f"\nDistribution:")
print(f" Excellent (90+): {d['excellent']}")
print(f" Good (70-89): {d['good']}")
print(f" Fair (50-69): {d['fair']}")
print(f" Poor (<50): {d['poor']}")
if result["top_failures"]:
print(f"\nTop Quality Gaps:")
for f in result["top_failures"]:
print(f" [{f['count']} issues] {f['description']}")
print(f"\nPer-Issue Scores:")
print(f" {'Key':<15} {'Type':<10} {'Score':>6} {'Issues'}")
print(f" {'-'*15} {'-'*10} {'-'*6} {'-'*30}")
for issue in sorted(result["issues"], key=lambda x: x["score"]):
failures_str = ", ".join(f["check"] for f in issue["failures"][:3])
if len(issue["failures"]) > 3:
failures_str += f" +{len(issue['failures'])-3} more"
print(f" {issue['key']:<15} {issue['type']:<10} {issue['score']:>5.0f}% {failures_str}")
print()
def print_example() -> None:
"""Print example issues JSON."""
example = {
"project": "PROJ",
"issues": [
{
"key": "PROJ-101",
"type": "Story",
"summary": "Add user onboarding wizard for new signups",
"description": "## Acceptance Criteria\n- [ ] Wizard appears on first login\n- [ ] User can skip at any step\n- [ ] Progress is saved if user leaves",
"assignee": "alice",
"priority": "High",
"story_points": 5,
"labels": ["onboarding"],
"epic_link": "PROJ-50",
"sprint": "Sprint 24",
},
{
"key": "PROJ-102",
"type": "Bug",
"summary": "fix stuff",
"description": "",
"assignee": "",
"priority": "",
"story_points": None,
"labels": [],
"epic_link": "",
"sprint": "",
},
],
}
print(json.dumps(example, indent=2))
def main():
parser = argparse.ArgumentParser(
description="Evaluate Jira issue quality against best practices."
)
parser.add_argument("--issues", type=str, help="Path to issues JSON file")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
parser.add_argument("--example", action="store_true", help="Print example input JSON and exit")
args = parser.parse_args()
if args.example:
print_example()
return
if not args.issues:
parser.error("--issues is required (use --example to see the expected format)")
data = load_data(args.issues)
result = analyze_issues(data)
if args.json:
print(json.dumps(result, indent=2))
else:
print_report(result)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Workflow Analyzer - Analyze Jira workflow efficiency and identify bottlenecks.
Reads issue transition data and calculates time-in-status metrics, identifies
bottlenecks, and recommends workflow optimizations.
Usage:
python workflow_analyzer.py --issues issues.json
python workflow_analyzer.py --issues issues.json --json
python workflow_analyzer.py --example
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
def load_data(path: str) -> dict:
"""Load issue data from JSON file."""
with open(path, "r") as f:
return json.load(f)
def parse_date(date_str: str) -> datetime:
"""Parse ISO date string."""
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
raise ValueError(f"Cannot parse date: {date_str}")
def analyze_workflow(data: dict) -> dict:
"""Analyze workflow transitions and time-in-status."""
project = data.get("project", "Unknown")
issues = data.get("issues", [])
if not issues:
return {"project": project, "error": "No issues provided"}
status_times = {} # status -> list of durations in hours
transition_counts = {} # "from -> to" -> count
back_transitions = 0
total_transitions = 0
for issue in issues:
transitions = issue.get("transitions", [])
for i, t in enumerate(transitions):
from_status = t.get("from_status", "Unknown")
to_status = t.get("to_status", "Unknown")
entered = t.get("entered")
exited = t.get("exited")
# Track transition counts
key = f"{from_status} -> {to_status}"
transition_counts[key] = transition_counts.get(key, 0) + 1
total_transitions += 1
# Detect backward transitions (rework)
standard_flow = ["To Do", "In Progress", "In Review", "QA", "Done"]
from_idx = standard_flow.index(from_status) if from_status in standard_flow else -1
to_idx = standard_flow.index(to_status) if to_status in standard_flow else -1
if from_idx > to_idx >= 0:
back_transitions += 1
# Calculate time in status
if entered and exited:
try:
entered_dt = parse_date(entered)
exited_dt = parse_date(exited)
hours = (exited_dt - entered_dt).total_seconds() / 3600
if hours >= 0:
if from_status not in status_times:
status_times[from_status] = []
status_times[from_status].append(round(hours, 1))
except ValueError:
pass
# Calculate per-status metrics
status_metrics = {}
for status, times in status_times.items():
avg = sum(times) / len(times)
sorted_times = sorted(times)
n = len(sorted_times)
p50 = sorted_times[n // 2]
p90 = sorted_times[int(n * 0.9)] if n >= 10 else sorted_times[-1]
status_metrics[status] = {
"count": n,
"avg_hours": round(avg, 1),
"median_hours": round(p50, 1),
"p90_hours": round(p90, 1),
"max_hours": round(max(times), 1),
}
# Identify bottleneck (longest average time)
bottleneck = None
if status_metrics:
bottleneck_status = max(status_metrics.items(), key=lambda x: x[1]["avg_hours"])
bottleneck = {
"status": bottleneck_status[0],
"avg_hours": bottleneck_status[1]["avg_hours"],
}
# Top transitions
sorted_transitions = sorted(transition_counts.items(), key=lambda x: x[1], reverse=True)
# Rework rate
rework_rate = round(back_transitions / total_transitions * 100, 1) if total_transitions > 0 else 0
# Recommendations
recommendations = []
if bottleneck and bottleneck["avg_hours"] > 24:
recommendations.append(f"Bottleneck detected at '{bottleneck['status']}' (avg {bottleneck['avg_hours']:.0f}h). Investigate WIP limits, resource allocation, or definition clarity.")
if rework_rate > 15:
recommendations.append(f"High rework rate ({rework_rate:.0f}%). Improve Definition of Ready and add acceptance criteria reviews before development starts.")
elif rework_rate > 5:
recommendations.append(f"Moderate rework rate ({rework_rate:.0f}%). Consider adding code review checklists and automated quality gates.")
for status, metrics in status_metrics.items():
if metrics["p90_hours"] > metrics["avg_hours"] * 3:
recommendations.append(f"'{status}' has high variance (avg {metrics['avg_hours']:.0f}h, P90 {metrics['p90_hours']:.0f}h). Some issues are stalling -- add aging WIP alerts.")
if not recommendations:
recommendations.append("Workflow appears healthy. Continue monitoring and consider reducing WIP limits for further optimization.")
return {
"project": project,
"total_issues": len(issues),
"total_transitions": total_transitions,
"rework_rate_pct": rework_rate,
"status_metrics": status_metrics,
"bottleneck": bottleneck,
"top_transitions": [{"transition": t[0], "count": t[1]} for t in sorted_transitions[:10]],
"recommendations": recommendations,
}
def print_report(result: dict) -> None:
"""Print human-readable workflow analysis."""
if "error" in result:
print(f"Error: {result['error']}")
return
print(f"\nWorkflow Analysis: {result['project']}")
print(f"Issues: {result['total_issues']} | Transitions: {result['total_transitions']}")
print("=" * 65)
print(f"\nRework Rate: {result['rework_rate_pct']:.1f}%")
if result["bottleneck"]:
b = result["bottleneck"]
print(f"Bottleneck: {b['status']} (avg {b['avg_hours']:.1f} hours)")
print(f"\nTime-in-Status Metrics:")
print(f" {'Status':<20} {'Count':>6} {'Avg(h)':>8} {'Med(h)':>8} {'P90(h)':>8} {'Max(h)':>8}")
print(f" {'-'*20} {'-'*6} {'-'*8} {'-'*8} {'-'*8} {'-'*8}")
for status, m in sorted(result["status_metrics"].items(), key=lambda x: x[1]["avg_hours"], reverse=True):
print(f" {status:<20} {m['count']:>6} {m['avg_hours']:>8.1f} {m['median_hours']:>8.1f} {m['p90_hours']:>8.1f} {m['max_hours']:>8.1f}")
print(f"\nTop Transitions:")
for t in result["top_transitions"][:5]:
print(f" {t['transition']:<35} {t['count']:>5}x")
print(f"\nRecommendations:")
for i, r in enumerate(result["recommendations"], 1):
print(f" {i}. {r}")
print()
def print_example() -> None:
"""Print example issues JSON."""
example = {
"project": "PROJ",
"issues": [
{
"key": "PROJ-101",
"type": "Story",
"transitions": [
{"from_status": "To Do", "to_status": "In Progress", "entered": "2026-03-01", "exited": "2026-03-03"},
{"from_status": "In Progress", "to_status": "In Review", "entered": "2026-03-03", "exited": "2026-03-05"},
{"from_status": "In Review", "to_status": "Done", "entered": "2026-03-05", "exited": "2026-03-06"},
],
},
{
"key": "PROJ-102",
"type": "Bug",
"transitions": [
{"from_status": "To Do", "to_status": "In Progress", "entered": "2026-03-02", "exited": "2026-03-04"},
{"from_status": "In Progress", "to_status": "In Review", "entered": "2026-03-04", "exited": "2026-03-07"},
{"from_status": "In Review", "to_status": "In Progress", "entered": "2026-03-07", "exited": "2026-03-09"},
{"from_status": "In Progress", "to_status": "In Review", "entered": "2026-03-09", "exited": "2026-03-10"},
{"from_status": "In Review", "to_status": "Done", "entered": "2026-03-10", "exited": "2026-03-11"},
],
},
],
}
print(json.dumps(example, indent=2))
def main():
parser = argparse.ArgumentParser(
description="Analyze Jira workflow efficiency and identify bottlenecks."
)
parser.add_argument("--issues", type=str, help="Path to issues JSON file with transition data")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
parser.add_argument("--example", action="store_true", help="Print example input JSON and exit")
args = parser.parse_args()
if args.example:
print_example()
return
if not args.issues:
parser.error("--issues is required (use --example to see the expected format)")
data = load_data(args.issues)
result = analyze_workflow(data)
if args.json:
print(json.dumps(result, indent=2))
else:
print_report(result)
if __name__ == "__main__":
main()