From 5c758c37917fd0ea80633ffb06d3a05afb543b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Desgagn=C3=A9?= Date: Fri, 8 May 2026 13:58:41 -0400 Subject: [PATCH] Generalize weekly-dev-report for mixed-role teams and rework rating --- skills/weekly-dev-report/SKILL.md | 288 ++++++++++++++++++++++-------- 1 file changed, 218 insertions(+), 70 deletions(-) diff --git a/skills/weekly-dev-report/SKILL.md b/skills/weekly-dev-report/SKILL.md index a4cb3a1..6b2b9d5 100644 --- a/skills/weekly-dev-report/SKILL.md +++ b/skills/weekly-dev-report/SKILL.md @@ -1,12 +1,12 @@ --- name: weekly-dev-report -description: Generate a weekly developer activity report from the active Jira sprint and linked GitHub repos, with per-dev achievability ratings, stuck-ticket flags, stalled-dev flags, and worklog audit. Use when the user wants a weekly dev report, sprint progress audit, developer status report, time-logged audit, or team check-in. Pulls roster from the active sprint, auto-discovers repos from Jira ticket dev-info, emails the report on --send, otherwise writes WEEKLY_REPORT.md and prints to stdout. +description: Generate a weekly team activity report from the active Jira sprint and linked GitHub repos, with per-member achievability ratings, stuck-ticket flags, stalled-member flags, and worklog audit. The roster mixes engineers, QA, content, and consultants; titles and language stay generic so non-engineering members aren't mislabelled. Use when the user wants a weekly activity report, sprint progress audit, contributor status report, time-logged audit, or team check-in. Pulls roster from the active sprint, auto-discovers repos from Jira ticket dev-info, emails the report on --send, otherwise writes WEEKLY_REPORT.md and prints to stdout. allowed-tools: Bash(jira:*), Bash(gh:*), Bash(git:*), Bash(curl:*), Bash(awk:*), Bash(basename:*), Bash(cat:*), Bash(cut:*), Bash(date:*), Bash(echo:*), Bash(grep:*), Bash(head:*), Bash(jq:*), Bash(ls:*), Bash(printenv:*), Bash(printf:*), Bash(sed:*), Bash(sort:*), Bash(tail:*), Bash(tr:*), Bash(uniq:*), Bash(wc:*), Bash(xargs:*), Read, Write, mcp__atlassian__searchJiraIssuesUsingJql, mcp__atlassian__getJiraIssue, mcp__github__list_pull_requests, mcp__github__list_commits, mcp__github__get_pull_request_reviews, mcp__github__search_issues, mcp__github__get_pull_request --- -# Weekly Developer Report +# Weekly Activity Report -Generate a weekly activity report for every developer with tickets in the active Jira sprint. The report includes per-dev sprint achievability (πŸŸ’πŸŸ‘πŸ”΄), ticket and PR activity, time-logged audit, and flags for stuck tickets and stalled developers. Writes `WEEKLY_REPORT.md` to the current directory, prints to stdout, and on `--send` delivers via Gmail. +Generate a weekly activity report for every team member with tickets in the active Jira sprint. The roster typically mixes engineers, QA, content/media folks, and consultants β€” keep all member-facing language generic ("team member", "member", "contributor") so the report does not mislabel anyone as an engineer. The report includes per-member sprint achievability (πŸŸ’πŸŸ‘πŸ”΄), ticket and PR activity, time-logged audit, and flags for stuck tickets and stalled members. Writes `WEEKLY_REPORT.md` to the current directory, prints to stdout, and on `--send` delivers via Gmail. ## Arguments @@ -51,16 +51,21 @@ If the user did not pass `--send`, treat the run as a preview. Never send email curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/agile/1.0/sprint/" | jq '{name, startDate, endDate, state}' ``` -3. Compute the week window (**previous completed week**, never the current running week): +3. Compute the raw week window (**previous completed week**, never the current running week): - `week_end = most recent past Sunday 23:59 local` (if today is Sunday, use today βˆ’ 7 days) - `week_start = week_end βˆ’ 6 days at 00:00 local` (the Monday of that same week) - Apply `--week-offset N` by subtracting `7*N` days from both (0 = previous completed week, 1 = the week before that) - - **Clamp** to sprint bounds: `week_start = max(week_start, sprint.startDate)`, `week_end = min(week_end, sprint.endDate)`. Report dates in local time. - - Rationale: a report covering the still-running week is meaningless because devs are mid-task. Always anchor on a completed Mon β†’ Sun. -4. Compute the **sprint-to-date** window separately: `[sprint.startDate, today 23:59 local]`. This is used for sprint-achievability calculations and the "sprint-to-date" throughput table. Keep it distinct from the weekly window. +4. **Pick the weekly-anchor sprint** β€” the sprint whose tickets, transitions, PRs, and worklogs are the basis for every weekly-window metric in the report: + - If `[week_start, week_end]` overlaps with the active sprint (any day in the window falls within `[sprint.startDate, sprint.endDate]`), the weekly-anchor sprint is the **active sprint**. + - Otherwise (the whole weekly window falls before the active sprint β€” typically because the active sprint started after the previous Sunday), the weekly-anchor sprint is the **previous closed sprint** (the most recent sprint on the same board with `state=closed`). When this fallback fires, every weekly table is computed against that previous sprint's tickets and bounds, and the report header explicitly states `weekly-anchor sprint = `. Sprint-to-date metrics still target the active sprint. + - **Clamp** the window to the chosen anchor sprint's bounds: `week_start = max(week_start, anchor.startDate)`, `week_end = min(week_end, anchor.endDate)`. Report dates in local time. + - Never produce an empty weekly window. If clamping would invert the range under both choices, abort with an explanatory message and ask the user how to proceed. + - Rationale: a report covering the still-running week is meaningless because the team is mid-task. But silently dropping the weekly section when the active sprint is fresh hides a full week of contribution β€” the previous-sprint fallback keeps the weekly view honest. -5. Extract the Jira server URL for browse links β€” prefer the env var, fall back to the jira-cli config (path varies by platform): +5. Compute the **sprint-to-date** window separately: `[active_sprint.startDate, today 23:59 local]`. This is used for sprint-achievability calculations and the "sprint-to-date" throughput table. Keep it distinct from the weekly window. + +6. Extract the Jira server URL for browse links β€” prefer the env var, fall back to the jira-cli config (path varies by platform): ```bash JIRA_SERVER="${JIRA_URL:-$(grep -h '^server:' \ @@ -97,18 +102,18 @@ Extract per issue: - `fields.timeoriginalestimate`, `fields.timeestimate`, `fields.customfield_*` for story points (try `fields.customfield_10016` and `fields.customfield_10002` β€” pick whichever is numeric) - `fields.customfield_*` for Sprint (array of sprint objects including historical sprints) -Build the **roster** = unique assignees across all active-sprint issues. Skip unassigned issues for per-dev sections (but include their totals in the team rollup). +Build the **roster** = unique assignees across all active-sprint issues. Skip unassigned issues for per-member sections (but include their totals in the team rollup). Do not assume roster members are engineers β€” many will be QA, content, or consultants. Use generic terms ("team member", "contributor") in all human-facing output. ### Detect the QA role -Critical for attribution: when a dev moves a ticket to `in QA`, the ticket auto-reassigns to the QA tester. This means current `fields.assignee` reflects who holds the ticket now, not who did the dev work. To correct: +Critical for attribution: when a contributor moves a ticket to `in QA`, the ticket auto-reassigns to the QA tester. This means current `fields.assignee` reflects who holds the ticket now, not who did the upstream work. To correct: 1. Count how many current-sprint issues are currently in status `in QA` per assignee. 2. The assignee with a clear majority (β‰₯ 60% of all `in QA` tickets) is the **QA tester**. Store as `qa_user`. If no one holds a clear majority, `qa_user = null` (team has no single tester and the QA-aware rules below are skipped). -3. **Do not classify runner-up "in QA" holders as testers** β€” those are escalation destinations (e.g. a CTO or tech lead who gets items the primary tester couldn't resolve). They are devs/leaders, not QA. -4. The QA user's row in the team-at-a-glance table should be labelled with a `(QA)` suffix, and their "throughput" is counted as QA validations (transitions they made to `Done`), not dev completions. +3. **Do not classify runner-up "in QA" holders as testers** β€” those are escalation destinations (e.g. a CTO or tech lead who gets items the primary tester couldn't resolve). They are contributors / leaders, not QA. +4. The QA user's row in the team-at-a-glance table should be labelled with a `(QA)` suffix, and their "throughput" is counted as QA validations (transitions they made to `Done`), not feature completions. -Also fetch the **previous sprint** (same board, state=closed, most recent end date) for stalled-dev comparison: +Also fetch the **previous sprint** (same board, state=closed, most recent end date) for stalled-member comparison: ```bash curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/agile/1.0/board//sprint?state=closed" | jq '.values | sort_by(.endDate) | last' @@ -118,27 +123,58 @@ If the board ID isn't obvious, get it from the active sprint's `originBoardId`. ## Step 3: Count transitions and fetch worklogs -### Transition counts per dev (authoritative throughput metric) +### Cycle time per ticket (In Progress β†’ Code Review) + +For each ticket a member transitioned **into** `Code Review` or `in QA` during the weekly window, compute the most recent prior `In Progress` start time and take the delta: + +```bash +curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/api/3/issue/?expand=changelog&fields=summary" \ + | jq '{ + key: .key, + summary: .fields.summary, + ip_to_cr_seconds: ( + (.changelog.histories + | map({when: .created, items: .items}) + | map(.items[] |= ({when: .when} + .)) + | .[].items[]? + | select(.field=="status")) as $h + | null # placeholder β€” see implementation note below + ) + }' +``` + +Implementation note: the cleanest way is to walk `.changelog.histories | sort_by(.created)` and remember `last_in_progress_at`. When you hit a status change `to == "Code Review"` (or `to == "in QA"` if no Code Review step happened in between), record `delta = parsed(history.created) - last_in_progress_at`. If multiple In-Progressβ†’Code-Review cycles happened on the same ticket, take the **last full cycle that ended within the weekly window**. Skip tickets whose cycle started before the previous sprint's start date (treat as no signal). + +Aggregate per member: + +- `cycle_seconds[]` = list of `delta` for each ticket they transitioned in the window +- `cycle_avg_hours` = mean of `cycle_seconds[]` Γ· 3600 (or `β€”` if zero tickets had a measurable cycle) + +This number lands in the team-at-a-glance table as the "Avg cycle (IPβ†’CR)" column (Step 7) and surfaces in the per-member section's why-line when it's significantly above team median. + +### Transition counts per member (authoritative throughput metric) Do **not** use current `fields.assignee` for throughput. Instead, count status transitions each user made within a given window. JQL `BY DURING (...)` is cheap and avoids having to pull full changelogs: ```bash -# per dev, per target status, per window +# per member, per target status, per window jira issue list -q 'sprint = AND status CHANGED TO "" BY "" DURING ("", "")' \ --plain --no-headers --no-truncate --columns KEY | grep -c "^${KEY_PREFIX}-" ``` Important: when the user is invalid or has no results, the CLI prints a `βœ— No result found` line. Always filter by `grep -c "^${KEY_PREFIX}-"` (not `wc -l`) to avoid counting that line as 1. `KEY_PREFIX` is read from the sprint's first issue key (Step 4) and is **not** hardcoded. -For each dev in the roster (skipping `qa_user`), count transitions to **each** of these target states, for **each** of these windows: +For each member in the roster (skipping `qa_user`), count transitions to **each** of these target states, for **each** of these windows: | Target status | Meaning | | --- | --- | -| `Code Review` | dev opened a review (first hand-off) | -| `in QA` | dev finished and handed to QA | -| `Done` | dev closed directly (non-QA items) | +| `Code Review` | member opened a review (first hand-off) | +| `in QA` | member finished and handed to QA | +| `Done` | member closed directly (non-QA items) | | `REJECTED` | triage dispatch (e.g. auto-filed PROD bugs dismissed as noise) | +Also collect, per member, the **set of tickets they touched in the week** = the union of issues returned by `status CHANGED ... BY DURING ()` across **any** transition (any source, any target). For each ticket store `{ key, summary }`. This list feeds the new `Tickets worked` column in the team-at-a-glance table (Step 7) and the per-member detail section. Always render Jira references as `[KEY](.../browse/KEY) β€” short summary` so the reader has context without clicking. + Windows: - **Weekly** = `[week_start, week_end]` (previous completed Mon β†’ Sun) @@ -154,17 +190,27 @@ Only pull full changelogs for issues flagged for stuck-ticket analysis (Step 6). curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/api/3/issue/?expand=changelog" ``` -### Worklogs +### Worklogs (per-member daily breakdown) -Fetch per issue (paginate: the issue-view worklog field caps at 20, but the worklog endpoint returns all): +Fetch per issue in the **weekly-anchor sprint** (paginate via the worklog endpoint, which is unbounded unlike the 20-entry issue-view field): ```bash curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/api/3/issue//worklog?startAt=0&maxResults=1000" | jq '.worklogs' ``` -Sum `timeSpentSeconds` per `author.accountId` where `started` is within `[week_start, week_end]`. Convert to hours (Γ· 3600). +For each entry, record `{ author.accountId, author.displayName, started, timeSpentSeconds, issueKey }`. Convert `started` to local timezone before bucketing. + +**Roster filter for the time table.** Drop a member from the time-logged table if they have **zero worklog entries in the trailing 28 days from today**, across any issue (not just sprint issues). Run a cheap JQL `worklogAuthor = "" AND worklogDate >= -28d` per roster member to confirm. These are typically consultants who don't log in Jira; they remain in throughput tables but are silently absent from the time table β€” do not mark them red, do not list them as "0h". The report header should state how many members were dropped from the time table for this reason. + +For each remaining member, compute over `[week_start, week_end]`: -**Worklog coverage gate**: after aggregating, count how many devs in the roster logged > 0 hours. If fewer than 50% did, mark the hours column `low confidence` in the rendered table header and disable the `hours_ratio` arm of the rating formula for this run. Do not silently degrade β€” explicitly note it. +- `daily_hours[date]` = sum `timeSpentSeconds / 3600` for all entries whose local-day equals `date` (one bucket per Mon, Tue, … Sun in the window β€” pre-fill missing days with 0) +- `total_hours` = sum across the window +- `working_days_below_7h` = count of working days (Mon–Fri inside the window) where `daily_hours[date] < 7.0`. A working day with zero entries counts as 0h and triggers the flag. +- `pattern_flag` = true if the member has **β‰₯ 3 entries in the window** AND every entry shares the same `started` time-of-day (HH:MM, local) AND the same `timeSpentSeconds`. Below 3 entries the signal is too noisy and the flag stays false. +- `logged_tickets` = distinct list of `{ key, summary }` for every issue that received a worklog entry from this member in the window. Resolve `summary` once per key (cache it β€” it's the same string for every entry on that ticket). This list renders into the new `Jira logged` column on the time-logged table (Step 7). + +These per-member numbers feed both the rendered "Time logged" table (Step 7) and the rating formula (Step 6). ## Step 4: Discover linked GitHub repos @@ -192,11 +238,11 @@ gh search prs --owner cloud-officer "DEV-" --json repository,title,url --limit 2 Adjust `cloud-officer` and the ticket key prefix as appropriate (read prefix from the sprint's first issue key). -## Step 5: Gather GitHub metrics per dev +## Step 5: Gather GitHub metrics per member ### Auto-map GitHub users β†’ Jira users via PR-to-transition links -The team's PR template requires Jira keys in PR titles/bodies. Cross-referencing a PR's referenced ticket with **who moved that ticket forward in Jira** (not its current assignee) gives a reliable auto-mapping. Current assignee is unreliable because of QA reassignment: most merged-PR tickets end up assigned to `qa_user`, so assignee-based mapping mis-labels every dev as the QA tester. +The team's PR template requires Jira keys in PR titles/bodies. Cross-referencing a PR's referenced ticket with **who moved that ticket forward in Jira** (not its current assignee) gives a reliable auto-mapping. Current assignee is unreliable because of QA reassignment: most merged-PR tickets end up assigned to `qa_user`, so assignee-based mapping mis-labels every contributor as the QA tester. Procedure: @@ -209,14 +255,14 @@ Procedure: 2. Build the set of `(github_login, ticket_key)` pairs from step 1. -3. For each Jira user in the roster (skipping `qa_user`), list the tickets they transitioned **out of** `In Progress` or `Code Review` within the sprint-to-date window: +3. For each roster member (skipping `qa_user`), list the tickets they transitioned **out of** `In Progress` or `Code Review` within the sprint-to-date window: ```bash jira issue list -q 'sprint = AND status CHANGED FROM "In Progress" BY "" DURING ("", "")' --plain --no-headers --columns KEY jira issue list -q 'sprint = AND status CHANGED FROM "Code Review" BY "" DURING ("", "")' --plain --no-headers --columns KEY ``` - The union of these is this dev's "I worked on it" ticket set. This bypasses QA-reassignment entirely because it asks who did the transition, not who currently holds the ticket. + The union of these is this member's "I worked on it" ticket set. This bypasses QA-reassignment entirely because it asks who did the transition, not who currently holds the ticket. 4. For each `(github_login, jira_user)` pair, count the number of distinct tickets that appear in both sets. Build `score[github_login][jira_user] = overlap_count`. @@ -224,7 +270,7 @@ Procedure: 6. Optional override: if env var `GITHUB_USERNAME_MAP` is set (format `email1=ghuser1,email2=ghuser2`), it **overrides** the auto-detected mapping for those emails. Use this as a last-resort manual patch only. -7. If auto-mapping still leaves a dev unresolved (no PRs in the window), mark their GitHub columns as `β€”` and add a caveat. Do not block the report. +7. If auto-mapping still leaves a member unresolved (no PRs in the window), mark their GitHub columns as `β€”` and add a caveat. Do not block the report. Never map via current `fields.assignee`, never guess by email local-part, never call `gh api users/`. @@ -246,7 +292,7 @@ gh pr list --repo / --author --state open --json number,titl '[.[] | select(.isDraft|not) | select(.reviewDecision != "APPROVED") | select(.updatedAt < $cutoff)]' ``` -Compute per dev: +Compute per member: - `prs_opened`, `prs_merged`, `prs_closed_unmerged` - `commits_count` @@ -286,77 +332,170 @@ Fetch comments if needed: curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/api/3/issue//comment" | jq '[.comments[] | {created, author: .author.displayName}] | sort_by(.created) | last' ``` -### Stalled developer flag +### Stalled member flag -For each dev (excluding `qa_user`), flag if **all** of: +For each member (excluding `qa_user`), flag if **all** of: - Assigned ≀ 2 issues in the active sprint - β‰₯ 50% of their active-sprint issue keys were also in the previous sprint - Zero status transitions **authored by them** (via JQL `BY `) on any sprint issue during both the week window and the full sprint-to-date window -The last condition checks **author of the transition**, not current assignee β€” a dev who moved their last ticket to `in QA` and then got reassigned to Pavlo should NOT be flagged as stalled. +The last condition checks **author of the transition**, not current assignee β€” a member who moved their last ticket to `in QA` and then got reassigned to QA should NOT be flagged as stalled. -### Sprint achievability per dev (πŸŸ’πŸŸ‘πŸ”΄) +### Per-member rating (πŸŸ’πŸŸ‘πŸ”΄) -Computed from transition data, not from current `fields.assignee`. +The rating is anchored on **PR throughput per working day in the weekly window**, with hardness floors from the worklog data. PRs are the primary signal because, with AI assistance, "at least one PR per working day" is the team's working baseline. Note: not every roster member is expected to ship PRs (QA, content, consultants); the rating is meaningful for engineering contributors and is suppressed for the QA row. For non-engineering members the report should call out the role mismatch in the why-line so the rating isn't misread. -Inputs: +Inputs (all over `[week_start, week_end]` against the **weekly-anchor sprint** chosen in Step 1): -- `remaining_sp` = sum of story-point estimates for sprint issues **not yet transitioned to `in QA` or `Done` by anyone**. Story points come from `customfield_10016` or `customfield_10002`; fall back to hour estimates Γ· 8; fall back to issue count Γ— 1. Assignment is resolved via: (a) current assignee if not the QA user, else (b) the user who most recently transitioned the ticket into an active status (`In Progress` / `Code Review`). -- `days_left` = working days between `today` and `sprint.endDate` (weekdays only) -- `velocity_sp_per_day` = `(SP transitioned to "in QA" by this dev in the previous sprint) Γ· (working days in that sprint)`. If no prior data, use `team_avg_velocity Γ— 0.8`. -- `wip` = count of tickets currently in `In Progress` that this dev **last moved** (JQL `status was "In Progress" CHANGED BY ` then current status filter). Do not use raw assignee β€” a ticket reassigned to QA must not count toward a dev's WIP. -- `expected_hours` = 40 Γ— (working days elapsed in sprint Γ· total working days in sprint) -- `logged_hours` = sum of their worklogs since `sprint.startDate` -- `hours_ratio` = `logged_hours Γ· expected_hours` β€” **disabled** when the worklog-coverage gate (Step 3) triggered. +- `prs_merged_week` = number of PRs merged within the window where this member is the **author** (the opener), resolved via the GHβ†’Jira map from Step 5; a PR counts iff `pr.author.login` maps to their Jira identity. The user who clicked merge does **not** get credit β€” that belongs to a separate `Reviews` / `merged-for-others` metric. Count merges only (not opens or closes-without-merge), and exclude bot/automation accounts. +- `working_days_in_week` = count of Mon–Fri inside `[week_start, week_end]` (typically 5; will be smaller if the window was clamped at a sprint boundary). +- `pr_per_day` = `prs_merged_week / working_days_in_week`. +- `worklog_short_day` = true if any working day in the window has `daily_hours < 7.0` (from the worklog section). False (and irrelevant) if the member was dropped from the time table for the 28-day-no-entries rule. +- `worklog_pattern_flag` = the `pattern_flag` from the worklog section. +- `is_stalled` = stalled-member flag from this section above. Rating rules (apply in order, first match wins): -- πŸ”΄ **Red** if any: `remaining_sp > velocity_sp_per_day Γ— days_left`, OR (hours_ratio enabled AND `< 0.80`), OR a stuck ticket is assigned to them, OR `wip β‰₯ 4` -- 🟑 **Yellow** if any: `remaining_sp > velocity_sp_per_day Γ— days_left Γ— 0.8`, OR (hours_ratio enabled AND `< 0.90`), OR `wip β‰₯ 3` -- 🟒 **Green** otherwise +- πŸ”΄ **Red** if any: `is_stalled`, OR `pr_per_day < 0.5` (i.e. fewer than one PR every other working day). +- 🟑 **Yellow** if any: `pr_per_day < 1.0` (at least one PR every other day, but less than one PR per day), OR `worklog_short_day`, OR `worklog_pattern_flag`. +- 🟒 **Green** otherwise (`pr_per_day β‰₯ 1.0` AND no worklog flags). + +The PR threshold is **per working day**. For a normal 5-working-day week: + +- 🟒 β‰₯ 5 PRs merged +- 🟑 3 or 4 PRs merged (or β‰₯ 5 but with a worklog flag) +- πŸ”΄ ≀ 2 PRs merged (or ⚠️ Stalled) + +For a clamped 4-working-day week, the corresponding bands are β‰₯ 4 / 2–3 / ≀ 1, and so on. + +One-line "why" per rating: state the worst driver. Examples: + +- `"Red β€” 1 PR / 5 working days = 0.2/day"` +- `"Red β€” ⚠️ Stalled (1 carryover ticket, 0 transitions sprint-to-date)"` +- `"Yellow β€” 4 PRs / 5 working days = 0.8/day"` +- `"Yellow β€” 6 PRs but Wed = 4h logged (< 7h)"` +- `"Yellow β€” 7 PRs but every worklog entry is 09:00 / 8h exactly"` +- `"Green β€” 8 PRs / 5 working days = 1.6/day"` -One-line "why" per rating: state the single highest-severity driver (e.g. `"Red β€” 13 SP left, 3 days, velocity 2 SP/day"` or `"Yellow β€” WIP of 3 tickets"`). +**Carve-outs.** -`qa_user` is not rated on this scale β€” their row shows QA-specific metrics (validations done, regressions logged). +- `qa_user` is not rated on this scale β€” their row shows QA-specific metrics (validations done, regressions logged). +- A member who was excluded from the time table for the 28-day rule (consultant) is rated on PRs alone β€” both worklog flags evaluate to false for them. +- Members flagged as **content team** or any other non-engineering role in the report's per-member caveats (e.g. via project memory) should still be rated, but the why-line must call out the role mismatch so the reader does not interpret a πŸ”΄ as poor performance. ## Step 7: Render the report Write to `WEEKLY_REPORT.md` in the current working directory, and print the same content. Every visual line break between sections, paragraphs, stats, and table captions must be a `
` (end-of-line or own-line) so markdown renderers don't collapse consecutive lines. Format: -```markdown -# Weekly Developer Report +````markdown +# Weekly Activity Report -**Sprint:**
-**Weekly window:** β†’ (previous completed Mon β†’ Sun)
-**Sprint-to-date window:** β†’
-**Sprint ends:**
+**Active sprint:** (sprint-to-date metrics)
+**Weekly-anchor sprint:** " if the fallback fired>
+**Weekly window:** β†’ ( working days; previous completed Mon β†’ Sun, clamped to weekly-anchor sprint bounds)
+**Sprint-to-date window:** β†’
+**Sprint ends:**
**Team rating:** <🟒|🟑|πŸ”΄>
**QA:**
-**Hours column:** +**Time table:** ## Team at a glance (weekly throughput) -Throughput is measured by status transitions authored by each user during the weekly window. Current `fields.assignee` is NOT used here (tickets auto-reassign when moved to in QA). +Throughput is measured by status transitions authored by each user during the weekly window. Current `fields.assignee` is NOT used here (tickets auto-reassign when moved to in QA). The PR column counts merges where the member was the **author** of the PR (the opener), not the user who clicked merge β€” so when a tech lead merges someone else's PR, credit goes to the opener. + +**Inclusion filter:** drop a roster member from this table if they had **zero status transitions AND zero authored-merged PRs** in the weekly window. Those rows aren't contribution activity; carrying them as πŸ”΄ just adds noise. Dropped members may still appear in: + +- the **Time logged** table (if they meet the 28-day worklog rule), and +- the **⚠️ Stalled members** section below (if they have an active sprint ticket that hasn't moved at all this sprint). + +The header should state how many roster members were dropped under this filter so the reader knows the table is filtered, e.g. "_4 roster members omitted (no Jira movement and no authored PRs this week β€” see Stalled section if applicable)_". + +`qa_user` is exempt from this filter and always shown if they had any activity. + +| Team member | Rating | β†’ Code Review | β†’ in QA | β†’ Done | β†’ REJECTED | PRs authored & merged | PRs/day | Tickets/day | Avg cycle (IPβ†’CR) | Why | +| --- | --- | ---:| ---:| ---:| ---:| ---:| ---:| ---:| ---:| --- | +| Alice | 🟒 | 4 | 3 | 0 | 0 | 6 | 1.2 | 1.4 | 18h | Green β€” 6 PRs / 5 working days = 1.2/day | +| Bob | πŸ”΄ | 0 | 0 | 0 | 0 | 1 | 0.2 | 0.2 | β€” | Red β€” 1 PR / 5 working days = 0.2/day | +| Jamie (QA) | β€” | β€” | 2 | 20 | β€” | β€” | β€” | 4.4 | n/a | QA validations | + +Rows sorted: πŸ”΄ first, 🟑 next, 🟒 last, `qa_user` last. Break ties by `PRs authored & merged` descending, then `β†’ in QA` descending. -| Developer | Rating | β†’ Code Review | β†’ in QA | β†’ Done | β†’ REJECTED | Hours logged | PRs merged | Reviews | Why | -| --- | --- | ---:| ---:| ---:| ---:| ---:| ---:| ---:| --- | -| Alice | 🟒 | 4 | 3 | 0 | 0 | 38.5 | 3 | 5 | On track | -| Bob | πŸ”΄ | 0 | 0 | 0 | 0 | 12.0 | 0 | 1 | Red β€” 18 SP left, 4 days, velocity 2 SP/day | -| Pavlo (QA) | β€” | β€” | 2 | 20 | β€” | 19.5 | β€” | β€” | QA validations | +Column rules: -Rows sorted: πŸ”΄ first, 🟑 next, 🟒 last, `qa_user` last. Break ties by `β†’ in QA` descending. +- `Tickets/day` = (count of distinct tickets touched by transitions in the week) Γ· working days. Reflects how many *different* pieces of work the member moved, complementing `PRs/day` which only counts merges. +- `Avg cycle (IPβ†’CR)` = average of `(Code Review timestamp βˆ’ last In Progress timestamp)` for tickets the member transitioned to Code Review (or to in QA when they skipped CR) during the window. Shown in hours when < 48h, in days otherwise. `β€”` when the member moved no tickets through that gate this week. + +Keep the table focused on numbers β€” the **list of tickets each member transitioned this week** lives in the per-member detail section below (Step 7 / per-member detail), not in this table. That keeps each row to a single line and prevents the table from growing horizontally past the screen. + +## Time logged (week window) + +Members with **zero worklog entries in the trailing 28 days from today** are omitted from this table β€” typically consultants who don't log to Jira. The header notes how many were dropped under that rule. + +| Team member | Mon | Tue | Wed | Thu | Fri | Sat | Sun | Total | Flags | Jira logged | +| --- | ---:| ---:| ---:| ---:| ---:| ---:| ---:| ---:| --- | --- | +| Alice | 8.0 | 7.5 | 7.5 | 8.0 | 8.0 | β€” | β€” | 39.0 | β€” | β€’ [DEV-1201](url) β€” Add X endpoint
β€’ [DEV-1205](url) β€” Fix Y bug | +| Bob | 4.0 | 8.0 | 8.0 | 8.0 | 8.0 | β€” | β€” | 36.0 | ⚠️ Mon < 7h | β€’ [DEV-1310](url) β€” Refactor auth | +| Carol | 8.0 | 8.0 | 8.0 | 8.0 | 8.0 | β€” | β€” | 40.0 | ⚠️ every entry is 09:00 / 8h exactly | β€’ [DEV-1599](url) β€” Maintenance bucket | + +Render rules: + +- One row per member still in scope after the 28-day filter, sorted by total hours descending. +- `Sat`/`Sun` columns show `β€”` when there are no entries (weekend work is allowed but not expected; never flagged). +- Working-day cells (Mon–Fri inside the window) showing `< 7h` are bolded so the eye can scan the column. +- Flags column lists, comma-separated: any working day under 7h (e.g. `⚠️ Wed < 7h`); the pattern flag if active (e.g. `⚠️ every entry is HH:MM / Nh exactly`); both if both apply. +- If the entire team triggers the pattern flag, surface a header note ("⚠️ Worklog entry pattern across team β€” verify clocking practice") rather than tagging every row individually. +- `Jira logged` column = bullet list of every distinct ticket that received a worklog entry from the member in the window, formatted `[KEY](url) β€” summary`. One bullet per row, `
` between bullets. If a member logged on > 8 distinct tickets, show the 8 with the most hours and append `… (+N more)`. ## Sprint-to-date throughput -Same columns as above but covering `[sprint.startDate, today]`. This is the view that correctly credits devs for work that has since been reassigned to QA. +Same columns as above but covering `[sprint.startDate, today]`. This is the view that correctly credits members for work that has since been reassigned to QA. ## Who holds what now (current-assignee snapshot) A separate, lower-priority table showing current `fields.assignee` counts by status. Useful for "what is in my queue" but explicitly labelled as a holdings snapshot, not throughput. Keep this below the throughput tables so readers don't confuse the two. +## Releases this week (Jira ↔ GitHub reconciliation) + +For the weekly window, fetch: + +1. **Jira releases** β€” for each in-scope project (typically the project that owns the active sprint, e.g. DEV): + + ```bash + curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "$JIRA_URL/rest/api/3/project//versions" \ + | jq --arg ws "$WEEK_START" --arg we "$WEEK_END" \ + '[.[] | select(.released==true and (.releaseDate // "") >= $ws and (.releaseDate // "") <= $we) | {name, releaseDate, description, projectId}]' + ``` + +2. **GitHub releases / tags** β€” per repo discovered in Step 4: + + ```bash + gh release list --repo / --limit 50 --json tagName,publishedAt,name + gh api "repos///tags?per_page=100" --jq '.[] | {tag: .name, sha: .commit.sha}' + ``` + + Filter by `publishedAt` falling inside the weekly window (`gh release list` is the cleanest source; raw tags fall back when no formal release was created). + +3. **Reconcile** β€” match Jira version names to GitHub tag/release names. Normalize aggressively (case-insensitive, strip leading `v`, allow trailing `-rc`/`-beta` suffix variants). For each Jira release, expected counterparts are: + + - exact tag in the relevant repo(s) β€” green βœ… + - GitHub release exists but tag spelling differs (e.g. Jira `12.4.0` vs GitHub `v12.4.0`) β€” yellow ⚠️ "name normalization" (still considered matched) + - no GitHub tag/release within the window β€” red ❌ "Jira marks released, GitHub has no tag" + - GitHub tag without a Jira release version β€” yellow ⚠️ "untracked GitHub release" + +Render: + +| Release | Date | Jira project / version | GitHub repo / tag | Status | +| --- | --- | --- | --- | --- | +| 12.4.0 | 2026-04-29 | DEV / 12.4.0 (Android) | ugroupmedia/pnp-android / v12.4.0 | βœ… matched | +| 8.1.2 | 2026-04-30 | DEV / 8.1.2 (API) | ugroupmedia/pnp-api / 8.1.2 | ⚠️ name normalized (Jira `8.1.2` ↔ GitHub `8.1.2`) | +| n/a | 2026-04-28 | _(missing)_ | ugroupmedia/pnp-web-next / 2026.04.28-rc | ⚠️ untracked GitHub release | +| 1.5.0 | 2026-05-01 | DEV / 1.5.0 (Scripts) | _(none)_ | ❌ Jira released, no GitHub tag | + +If neither Jira nor GitHub had any releases in the window, render the section as a single line: "_No releases this week._" Do not omit the section entirely β€” its absence is itself information. + ## 🚩 Stuck tickets (carryover with no activity) | Ticket | Assignee | Sprints bounced | Last status change | Last worklog | @@ -373,13 +512,14 @@ Omit section entirely if no stuck tickets. Top 10 oldest open non-draft PRs across in-scope repos, awaiting review or with changes requested. Omit if empty. -## Per-developer detail +## Per-member detail ### Alice β€” 🟒 _On track. 4 of 6 tickets moved to Done this week._ -**Tickets in sprint** +**Tickets transitioned this week** (full list β€” bullet per ticket, format `[KEY](url) β€” summary`): + - [DEV-1201](url) β€” Add X endpoint β€” **Done** (moved Wed) - [DEV-1205](url) β€” Fix Y bug β€” **In Review** - ... @@ -400,8 +540,8 @@ _Only 2 sprint tickets, both carried over from previous sprint with no status ch --- - -``` + +```` Rendering rules: @@ -437,11 +577,19 @@ If run with `--send`: - **No email unless `--send` is explicitly passed.** A run without that flag must be a pure preview. - **Secrets.** Never print `JIRA_API_TOKEN`, GitHub tokens, or email addresses from `WEEKLY_DEV_REPORT_CC` into the report body or stdout. Recipient list is OK to echo on successful send. - **Timeout.** 20-second timeout on each `jira`, `curl`, and `gh` call. Network flakes happen; retry once, then log a warning and continue rather than aborting the whole run. -- **Partial data is fine.** If one dev's GitHub data fails to resolve, mark their row "GitHub data unavailable" and continue. The report is better incomplete than missing. +- **Partial data is fine.** If one member's GitHub data fails to resolve, mark their row "GitHub data unavailable" and continue. The report is better incomplete than missing. - **Roster source is authoritative.** If someone has no active-sprint tickets but did GitHub work this week, they are NOT in the report. The sprint is the lens. -- **Throughput via transitions, never assignee.** Current `fields.assignee` is a holdings signal, not a throughput signal, because the workflow auto-reassigns tickets at `in QA`. The Team-at-a-glance and per-dev sections must derive throughput from `status CHANGED TO BY DURING (...)`. -- **QA role auto-detected, not hardcoded.** Never hardcode a tester's name or email in the skill. Always detect via the majority-holder rule in Step 2 and label their row `(QA)`. Secondary "in QA" holders (e.g. a CTO receiving escalations) are devs/leaders, not QA. -- **Weekly = previous completed Mon β†’ Sun.** Never report on the currently running week; it is meaningless because work is mid-flight. +- **Generic role language.** The roster mixes engineers, QA, content/media folks, and consultants. Never call the report or its rows "developers" or assume engineering as a default β€” use "team member", "member", "contributor", or the explicitly detected role (`QA`, content team, consultant). The rendered title is "Weekly Activity Report", not "Weekly Developer Report". +- **Throughput via transitions, never assignee.** Current `fields.assignee` is a holdings signal, not a throughput signal, because the workflow auto-reassigns tickets at `in QA`. The Team-at-a-glance and per-member sections must derive throughput from `status CHANGED TO BY DURING (...)`. +- **QA role auto-detected, not hardcoded.** Never hardcode a tester's name or email in the skill. Always detect via the majority-holder rule in Step 2 and label their row `(QA)`. Secondary "in QA" holders (e.g. a CTO receiving escalations) are contributors / leaders, not QA. +- **Weekly = previous completed Mon β†’ Sun.** Never report on the currently running week; it is meaningless because work is mid-flight. When that window falls entirely outside the active sprint (the active sprint is brand-new), use the previous closed sprint as the **weekly-anchor sprint** instead of producing an empty report β€” see Step 1 item 4. +- **Per-day PR threshold drives the rating.** 🟒 β‰₯ 1 PR per working day, 🟑 β‰₯ 0.5/day, πŸ”΄ < 0.5/day or ⚠️ Stalled. Worklog flags (any working day < 7h, or every entry sharing the same start-time + duration) cap a member at 🟑. See Step 6. +- **Time table omits no-clock consultants.** Drop a member from the time-logged table if they have zero worklog entries in the trailing 28 days. They stay in the throughput tables and are still rated on PRs. +- **Throughput table omits non-contributors.** Drop a roster member from the team-at-a-glance and per-member sections if they had zero Jira transitions AND zero authored-merged PRs in the weekly window. Those rows are not contribution activity. They may still show up in the Time-logged table (if they clocked) or the Stalled section (if they hold a sprint ticket that hasn't moved). The report header should state the count of dropped rows. +- **PR credit goes to the author, never the merger.** A PR counts for whoever opened it, even when someone else hits the merge button. "Merged X PRs for other people" is a separate metric and belongs in a Reviews / merged-for-others column, not in the member's authored-PR count. +- **Always cite Jira tickets with key + summary.** Every Jira reference rendered in the report β€” in tables, bullets, why-lines, captions, anywhere β€” must read `[KEY](.../browse/KEY) β€” short summary`. A bare `DEV-1234` link is not enough; the reader needs the title to understand without clicking. Truncate summaries to ~80 chars if needed but never omit them. +- **Releases reconciled across Jira and GitHub.** The Releases-this-week section must compare Jira `released==true` versions in the window with GitHub tags / releases in the same window and surface mismatches. If neither system has releases in the window, render an explicit "_No releases this week._" line β€” do not silently omit the section. +- **No skill / process meta-commentary in the rendered report.** The output is a status report for the team β€” never include sections like "Skill changes shipped this run", "Implementation notes", "TODOs for the script", or any other description of how the report was produced. Those belong in commit messages and the skill source itself, not in `WEEKLY_REPORT.md`. The report ends after the per-member detail and the trailing "Preview written to WEEKLY_REPORT.md. Re-run with --send to email." line. - **Working days.** When computing `days_left` and `expected_hours`, exclude Saturdays and Sundays. Do not attempt to detect holidays. - **Env vars referenced** (document at the top of output if any are unset and affect the run): - `WEEKLY_DEV_REPORT_TO` β€” required when `--send` is used; primary email recipient