From report
Generate daily, weekly, quarterly, or yearly work reports by orchestrating Jira and GitHub MCP servers to fetch data and format it into multiple report formats (Markdown, brief text, CherryTree). Automatically aggregates PRs, issues, and activities across specified time periods.
How this skill is triggered — by the user, by Claude, or both
Slash command
/report:work-reportThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Automatically generate comprehensive work reports by fetching data from Jira and GitHub MCP servers, then formatting into multiple output formats. Supports daily, weekly, quarterly, and yearly reports with intelligent date range handling.
Automatically generate comprehensive work reports by fetching data from Jira and GitHub MCP servers, then formatting into multiple output formats. Supports daily, weekly, quarterly, and yearly reports with intelligent date range handling.
Use this skill when:
Don't use when:
Required MCP Servers:
mcp__jira__jira_search - Jira issue searchmcp__github__search_pull_requests - GitHub PR searchmcp__work-report-generator__generate_daily_report - Report formattingConfiguration:
| Report Type | Date Range | Jira Query | GitHub Query |
|---|---|---|---|
| daily | Today only | updated >= YYYY-MM-DD | updated:>=YYYY-MM-DD |
| weekly | Last 7 days | updated >= -7d | updated:>=YYYY-MM-DD |
| quarterly | Last 90 days | updated >= -90d | updated:>=YYYY-MM-DD |
| yearly | Last 365 days | updated >= -365d | updated:>=YYYY-MM-DD |
1. Determine report type and date range
↓
2. Fetch Jira issues (mcp__jira__jira_search)
↓
3. Fetch GitHub PRs (mcp__github__search_pull_requests)
↓
4. Transform data to report format
↓
5. Call work-report-generator MCP server
↓
6. Report success with file paths
# Determine report type from user input
report_type = "daily" # default
if "weekly" in user_request or "week" in user_request:
report_type = "weekly"
elif "quarterly" in user_request or "quarter" in user_request:
report_type = "quarterly"
elif "yearly" in user_request or "year" in user_request:
report_type = "yearly"
# Get user information from environment variables
# JIRA_USERNAME - Full email used for Jira JQL (e.g., "[email protected]") — PREFERRED
# JENKINS_USER_ID - Short username fallback (e.g., "kewang") — may not work on Jira Cloud
# GITHUB_USER_ID - Used for GitHub username (e.g., "wangke19")
jira_username = os.environ.get("JIRA_USERNAME") or os.environ.get("JENKINS_USER_ID")
github_username = os.environ.get("GITHUB_USER_ID", "@me")
# CRITICAL: For Jira Cloud (*.atlassian.net), always use the email address in JQL,
# not the short username. Short usernames like "kewang" silently return 0 results.
# Use quoted email: assignee = "[email protected]"
# Fallback to currentUser() only if no email is available.
if not jira_username:
jira_jql_user = "currentUser()"
elif "@" in jira_username:
jira_jql_user = f'"{jira_username}"' # email → quoted string in JQL
else:
jira_jql_user = "currentUser()" # short username unreliable, use currentUser()
from datetime import datetime, timedelta, timezone
# CRITICAL: User is in CST (UTC+8). Jira JQL dates are interpreted as UTC.
# To cover "April 7 CST", we need UTC range: 2026-04-06 16:00 → 2026-04-07 16:00
# Always convert local date → UTC window using local UTC offset.
local_utc_offset_hours = 8 # CST = UTC+8; adjust if user is in a different timezone
today_local = datetime.now() # local time
report_date = today_local.strftime("%Y-%m-%d") # YYYY-MM-DD in local date
# UTC window start = local date 00:00 minus offset → previous UTC day at 16:00 (for UTC+8)
utc_start = today_local.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(hours=local_utc_offset_hours)
utc_end = utc_start + timedelta(days=1)
utc_start_str = utc_start.strftime("%Y-%m-%d %H:%M")
utc_end_str = utc_end.strftime("%Y-%m-%d %H:%M")
# For daily: use UTC-bounded range. For longer periods use simpler relative syntax.
# IMPORTANT: Include QA Contact field in Jira query to capture issues you're testing
if report_type == "daily":
jira_jql = (
f"updated >= '{utc_start_str}' AND updated < '{utc_end_str}' AND "
f"(assignee = {jira_jql_user} OR reporter = {jira_jql_user} OR 'QA Contact' = {jira_jql_user})"
)
github_query = f"involves:{github_username} updated:>={report_date}"
elif report_type == "weekly":
start_date = (today_local - timedelta(days=7)).strftime("%Y-%m-%d")
jira_jql = f"updated >= -7d AND (assignee = {jira_jql_user} OR reporter = {jira_jql_user} OR 'QA Contact' = {jira_jql_user})"
github_query = f"involves:{github_username} updated:>={start_date}"
elif report_type == "quarterly":
start_date = (today_local - timedelta(days=90)).strftime("%Y-%m-%d")
jira_jql = f"updated >= -90d AND (assignee = {jira_jql_user} OR reporter = {jira_jql_user} OR 'QA Contact' = {jira_jql_user})"
github_query = f"involves:{github_username} updated:>={start_date}"
elif report_type == "yearly":
start_date = (today_local - timedelta(days=365)).strftime("%Y-%m-%d")
jira_jql = f"updated >= -365d AND (assignee = {jira_jql_user} OR reporter = {jira_jql_user} OR 'QA Contact' = {jira_jql_user})"
github_query = f"involves:{github_username} updated:>={start_date}"
IMPORTANT: Filter by assignee, reporter, AND QA Contact to capture all work!
Get Jira username from environment:
// JIRA_USERNAME (email) is the correct identifier for Jira Cloud JQL.
// JENKINS_USER_ID (short name like "kewang") silently returns 0 results on Jira Cloud.
// Always prefer the email; fall back to currentUser() if unavailable.
const jiraEmail = process.env.JIRA_USERNAME; // e.g. "[email protected]"
const jiraJqlUser = jiraEmail ? `"${jiraEmail}"` : "currentUser()";
const githubUsername = process.env.GITHUB_USER_ID || "@me";
Call Jira MCP server:
// Use ToolSearch to load Jira MCP tool if not already loaded
await ToolSearch({ query: "select:mcp__jira__jira_search" });
// IMPORTANT: Include assignee, reporter, AND QA Contact in JQL query
// This captures: assigned issues, reported issues, and QA testing work
const jql = jira_jql; // Already includes the complete filter from Step 2
// Fetch Jira issues
const jiraResult = await mcp__jira__jira_search({
jql: jql,
fields: "key,summary,status,updated,assignee,issuetype,priority",
limit: 50
});
Transform Jira response:
const jiraIssues = jiraResult.issues.map(issue => ({
key: issue.key,
summary: issue.summary,
status: issue.status?.name || "Unknown",
updated: issue.updated,
assignee: issue.assignee?.display_name || "Unassigned",
priority: issue.priority?.name || "Undefined"
}));
const jiraData = { issues: jiraIssues };
Call GitHub MCP server:
// Use ToolSearch to load GitHub MCP tool if not already loaded
await ToolSearch({ query: "select:mcp__github__search_pull_requests" });
// Fetch GitHub PRs using the query built in Step 2
// Query uses "involves:{username}" to capture PRs you authored, reviewed, or were mentioned in
const githubResult = await mcp__github__search_pull_requests({
query: github_query, // e.g., "involves:wangke19 updated:>=2026-02-09"
perPage: 50
});
Transform GitHub response:
After deduplicating and verifying actual Apr-N activity per the gh CLI steps above,
set the section field on each PR so the report generator classifies them correctly.
// CRITICAL: The report generator uses `section` (NOT `tags`) to classify open PRs.
// Without section="working_on", open PRs are silently dropped from 🦀 Working On.
//
// Rules for setting section:
// "working_on" → I authored/commented/pushed on this PR TODAY
// "next" → I am reviewer/assignee but have NOT commented yet
// (omit) → merged PRs don't need section; merged_at field handles them
const githubPRs = verifiedPRs.map(pr => {
const htmlUrl = pr.url || "";
const repo = htmlUrl.replace("https://github.com/", "").split("/pull/")[0] || "unknown";
const isMerged = pr.state === "merged";
// Determine section for open PRs
let section = undefined;
if (!isMerged) {
if (pr.iAmAuthor || pr.iCommentedToday || pr.iPushedToday) {
section = "working_on";
} else if (pr.iAmReviewer && !pr.iCommentedToday) {
section = "next";
}
}
return {
repo,
number: pr.number,
title: pr.title,
url: htmlUrl,
state: isMerged ? "merged" : "open",
merged_at: isMerged ? pr.closedAt : null,
draft: pr.isDraft || false,
is_author: pr.iAmAuthor || false,
has_my_comments: pr.iCommentedToday || false,
...(section ? { section } : {})
};
});
const githubData = { prs: githubPRs };
Call work-report-generator MCP server:
// Use ToolSearch to load report generator MCP tool
await ToolSearch({ query: "select:mcp__work-report-generator__generate_daily_report" });
// Get user display name from git config or environment
const userDisplayName = process.env.GIT_AUTHOR_NAME || "Unknown User";
// Generate all report formats
// NOTE: Currently the MCP server hardcodes "Ke Wang" in the report header
// A PR should be created to accept user_name parameter
const result = await mcp__work_report_generator__generate_daily_report({
date: report_date,
jira_data: jiraData,
github_data: githubData,
report_format: "all", // Generates: Markdown, Brief, CherryTree
user_name: userDisplayName // TODO: MCP server needs to be updated to accept this
});
if (result.success) {
return `
✅ ${report_type.charAt(0).toUpperCase() + report_type.slice(1)} work report generated successfully!
**Summary:**
- Date: ${result.date}
- Jira Issues: ${result.summary.jira_issues}
- GitHub PRs: ${result.summary.github_prs}
**Generated Files:**
${result.files.full ? `- Full Report: ${result.files.full}` : ''}
${result.files.brief ? `- Brief Report: ${result.files.brief}` : ''}
${result.files.cherrytree ? `- CherryTree: ${result.files.cherrytree}` : ''}
`.trim();
} else {
return `❌ Report generation failed: ${result.error}`;
}
User: "generate daily report"
↓
Skill: Determine type = "daily", date = "2026-02-06"
↓
Skill: Call mcp__jira__jira_search(jql: "updated >= '2026-02-06'")
↓
Skill: Transform Jira response → jiraData
↓
Skill: Call mcp__github__search_pull_requests(query: "author:@me updated:>=2026-02-06")
↓
Skill: Transform GitHub response → githubData
↓
Skill: Call mcp__work_report_generator__generate_daily_report(date, jiraData, githubData)
↓
Skill: Report success with file paths
Handle MCP server failures gracefully:
try {
const jiraResult = await mcp__jira__jira_search({ jql, fields, limit: 50 });
} catch (error) {
console.error("Jira fetch failed:", error);
// Continue with empty Jira data
jiraData = { issues: [] };
}
try {
const githubResult = await mcp__github__search_pull_requests({ query, perPage: 50 });
} catch (error) {
console.error("GitHub fetch failed:", error);
// Continue with empty GitHub data
githubData = { prs: [] };
}
// Generate report even if one source fails
const result = await mcp__work_report_generator__generate_daily_report({
date: report_date,
jira_data: jiraData,
github_data: githubData,
report_format: "all"
});
User: "generate daily report"
User: "create today's work report"
User: "daily standup report"
→ Report Type: daily
→ Date Range: Today only
→ Output: All formats (Markdown, Brief, CherryTree)
User: "generate weekly report"
User: "create report for this week"
User: "weekly work summary"
→ Report Type: weekly
→ Date Range: Last 7 days
→ Output: All formats with aggregated data
User: "generate quarterly report"
User: "Q1 work summary"
User: "3-month report"
→ Report Type: quarterly
→ Date Range: Last 90 days
→ Output: All formats with long-term trends
User: "generate yearly report"
User: "annual work summary"
User: "2026 report"
→ Report Type: yearly
→ Date Range: Last 365 days
→ Output: All formats with yearly statistics
Location: ~/work/work-reports/YYYY/MM/Week-WW/daily_report_YYYY-MM-DD.md
Contents:
Location: ~/work/work-reports/YYYY/MM/Week-WW/daily_report_YYYY-MM-DD_brief.txt
Contents:
Use case: Quick standup, Slack/email copy-paste
Location: ~/work/work-reports/daily_report.ctd (or .ctx if encrypted)
Contents:
User: "generate report for 2026-02-05"
→ Override report_date = "2026-02-05"
→ Use specified date instead of today
User: "generate brief report only"
→ report_format = "brief"
→ Skips Markdown and CherryTree
User: "generate report for last 30 days"
→ Custom date range calculation
→ start_date = today - 30 days
The work-report-generator MCP server uses:
# Optional: Enable encrypted CherryTree
export CHERRYTREE_ENCRYPTED="true"
export CHERRYTREE_PASSWORD="your-password"
# Report base directory (default: ~/work/work-reports)
# No env var needed - uses default
~/work/work-reports/
├── 2026/
│ ├── 01/
│ │ ├── Week-01/
│ │ │ ├── daily_report_2026-01-01.md
│ │ │ └── daily_report_2026-01-01_brief.txt
│ │ ├── Week-02/
│ │ └── ...
│ ├── 02/
│ │ ├── Week-05/
│ │ ├── Week-06/
│ │ └── ...
│ └── ...
└── daily_report.ctd # CherryTree database (all reports)
Cause: Jira MCP server not configured or wrong credentials
Solution:
JIRA_USERNAME and JIRA_API_TOKEN environment variablesmcp__jira__jira_search({ jql: "updated >= -1d", limit: 1 })Cause: GitHub MCP server not authenticated
Solution:
gh CLI)mcp__github__search_pull_requests({ query: "author:@me", perPage: 1 })Cause: work-report-generator MCP server not running
Solution:
ps aux | grep work-report-generatorcd ~/work/gitlab/work-report-generator && python src/server.pyToolSearch({ query: "work-report" })Cause: No activities in specified date range
Solution: Normal behavior - verify with direct Jira/GitHub queries to confirm no data exists
# Add to cron or daily routine
# Generate report at end of workday
Quick copy-paste for Slack:
1. Open: ~/work/work-reports/2026/02/Week-06/daily_report_2026-02-06_brief.txt
2. Copy content
3. Paste in standup channel
Open CherryTree database to review:
- Weekly trends
- Monthly progress
- Quarterly achievements
- Daily: For active projects
- Weekly: For status updates
- Quarterly: For performance reviews
- Yearly: For retrospectives
When ready to migrate this skill to the ai-helpers repository:
Copy skill directory:
cp -r ~/work/gitlab/my-claude-skills/skills/work-report \
~/go/src/github.com/ai-helpers/plugins/utils/skills/
Update plugin metadata:
plugins/utils/.claude-plugin/plugin.jsonplugins/utils/commands/PLUGINS.md documentationTest in ai-helpers context:
cd ~/go/src/github.com/ai-helpers
# Test the skill works in new location
Create PR:
feat(utils): add work-report skillDaily Standup:
User: "generate daily report"
[30 seconds later]
✅ Report ready with 9 Jira issues, 4 GitHub PRs
Copy brief report to Slack → Done!
Quarterly Review:
User: "generate quarterly report"
[1 minute later]
✅ Report shows 127 Jira issues, 43 GitHub PRs over 90 days
Use for performance review → Comprehensive!
generate daily report → Today only
generate weekly report → Last 7 days
generate quarterly report → Last 90 days
generate yearly report → Last 365 days
mcp__jira__jira_search - Fetch Jira issuesmcp__github__search_pull_requests - Fetch GitHub PRsmcp__work_report_generator__generate_daily_report - Format reports~/work/work-reports/YYYY/MM/Week-WW/daily_report_YYYY-MM-DD.md - Full~/work/work-reports/YYYY/MM/Week-WW/daily_report_YYYY-MM-DD_brief.txt - Brief~/work/work-reports/daily_report.ctd - CherryTree databaseWhen implementing this skill:
JIRA_USERNAME env var (email) for JQL — NOT JENKINS_USER_ID (short name)gh CLI (NOT GitHub MCP) for PR search — run all 4 searches, deduplicatesection: "working_on" on open PRs with activity (NOT tags)section: "next" on PRs where user is reviewer but hasn't commented| # | Bug | Root Cause | Fix |
|---|---|---|---|
| 1 | Jira returned 0 results | Used JENKINS_USER_ID (kewang) in JQL — Jira Cloud requires email | Use JIRA_USERNAME ([email protected]) as quoted string in JQL |
| 2 | Jira missed issues at day boundary | JQL updated >= 'YYYY-MM-DD' uses UTC midnight; user is in CST (UTC+8) | Use UTC window: updated >= 'YYYY-MM-DD 16:00' AND updated < 'YYYY-MM-DD+1 16:00' |
| 3 | Open PRs dropped from report | Passed tags: ["working_on"] — generator ignores tags, requires section field | Set section: "working_on" for open PRs with activity today |
JIRA_USERNAME env var holds the email; always quote it in JQLsection not tags: The report generator classifies open PRs by section field onlygh search prs + verify activitySearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub wangke19/my-claude-skills --plugin report