krzysztofkrolikowski

jira-worklog

Generate bash scripts for bulk logging work hours to Jira. Use when user asks to log hours, create worklog script, or automate Jira time tracking for a specific month. Handles task discovery via JQL, existing worklog detection, hour distribution, skip days (holidays/vacation), and API authentication.

krzysztofkrolikowski 2 Updated 3mo ago

Resources

5
GitHub

Install

npx skillscat add krzysztofkrolikowski/copilot-jira-worklog

Install via the SkillsCat registry.

SKILL.md

Jira Worklog Script Generation

This skill generates bash scripts for bulk logging work hours to Jira for a specific month.

Prerequisites

  1. Atlassian MCP Server must be running — verify that mcp_atlassian_searchJiraIssuesUsingJql is available. See Atlassian MCP Server for setup. If an atlassian-mcp skill exists in the project, run it first.
  2. jq must be installed on the user's machine (required by the generated script for duplicate detection and JSON parsing).
  3. python3 must be available — used for changelog verification scripts and timestamp computation.
  4. Config file.worklog.config.json in the skill directory. See Configuration section.

Configuration

Read .worklog.config.json from the skill directory. If it doesn't exist, ask the user to create one based on .worklog.config.example.json.

The config file should be added to .gitignore (it contains personal settings). Add this entry:

.worklog.config.json

Config Structure

{
  "email": "your.email@company.com",
  "site": "your-company.atlassian.net",
  "timezone": "auto",
  "hoursPerDay": 8,
  "startTime": "09:00:00",
  "includeComments": false,
  "recurringTasks": [
    {
      "issueKey": "PROJ-123",
      "description": "Sprint Planning, Refinement, Retrospective",
      "hours": 8,
      "dayOfWeek": "friday",
      "frequency": "biweekly"
    }
  ]
}

Config Fields

Field Required Default Description
email Yes Atlassian account email
site Yes Atlassian site (e.g., company.atlassian.net)
timezone No "auto" "auto" = detect from system via date +%z, or explicit like "+0100", "+0200"
hoursPerDay No 8 Expected hours per working day
startTime No "09:00:00" Worklog start time
includeComments No false Whether to include task description as worklog comment
recurringTasks No [] Tasks that repeat on specific days (see below)

Recurring Tasks

Recurring tasks are a generic replacement for hardcoded sprint planning. Any regularly scheduled task can be configured:

Field Values Description
issueKey e.g., "PROJ-5" Jira issue key
description string Display text in script output
hours number Hours to log (usually equals hoursPerDay)
dayOfWeek "monday""friday" Which day of the week
frequency "weekly" or "biweekly" Every week or every 2 weeks

If frequency is "biweekly", ask the user which weeks of the month apply (1st & 3rd, or 2nd & 4th).

Workflow

Step 1: Load Configuration

Read .worklog.config.json from the skill directory. Validate required fields (email, site). If missing, guide the user to create one from the example config in the same directory.

Step 2: Ensure MCP Connectivity

Verify the Atlassian MCP server is running. If the project has an atlassian-mcp skill, run it. Otherwise, manually verify that mcp_atlassian_searchJiraIssuesUsingJql is available.

Step 3: Fetch Existing Worklogs for the Month (CRITICAL — prevents duplicates)

This is the most important safety step. Before generating any script, check what's already logged.

3a. Find issues with existing worklogs

Tool: mcp_atlassian_searchJiraIssuesUsingJql
JQL: worklogAuthor = currentUser() AND worklogDate >= "YYYY-MM-01" AND worklogDate <= "YYYY-MM-31"

3b. Fetch worklog details for each issue

Important: The MCP mcp_atlassian_fetch tool does NOT support fetching worklogs. Use run_in_terminal with curl instead.

First, ask the user if JIRA_API_TOKEN is set in their environment. If yes, fetch details for each issue found in 3a:

# Compute month boundaries (Unix timestamps in ms) for date filtering
START_TS=$(python3 -c "import datetime; d=datetime.datetime(YYYY,M,1,tzinfo=datetime.timezone.utc); print(int(d.timestamp()*1000))")
# Handles December→January rollover (M+1=13 would crash)
END_TS=$(python3 -c "import datetime; y,m=(YYYY,M+1) if M<12 else (YYYY+1,1); d=datetime.datetime(y,m,1,tzinfo=datetime.timezone.utc); print(int(d.timestamp()*1000))")

# For each issue key from Step 3a (use startedAfter/startedBefore to avoid downloading full history):
curl -s -u "{email}:{JIRA_API_TOKEN}" \
  -H "Accept: application/json" \
  "https://{site}/rest/api/3/issue/{issueKey}/worklog?startedAfter=${START_TS}&startedBefore=${END_TS}" \
  | jq '[.worklogs[] | select(.author.emailAddress == "{email}")]'

Run this in a single terminal command for all issues (batch with && or a loop). Filter worklogs where:

  • author.emailAddress matches the configured email
  • started falls within the target month

If JIRA_API_TOKEN is NOT available, skip detailed fetching and:

  • Inform the user that duplicate detection will happen at runtime (the script's check_duplicate() function handles this)
  • Still present the list of issues that have worklogs from Step 3a
  • Mark Step 3c summary as "partial — issue list only, no hour details"

3c. Build and present existing worklogs summary

Full summary (when worklog details were fetched):

Already logged for January 2026:
  2026-01-02 (Thu): 8h total
    - PROJ-123: 4h
    - PROJ-456: 4h
  2026-01-03 (Fri): 8h total
    - PROJ-5: 8h
  ...

Days fully logged (8h): 12
Days partially logged: 2 (2026-01-15: 4h, 2026-01-20: 6h)
Days with no worklogs: 6

Partial summary (when only issue list is available):

Issues with existing worklogs for January 2026:
  PROJ-123, PROJ-456, PROJ-5, PROJ-789
  (12 issues found — exact hours unknown, runtime duplicate detection will prevent re-logging)

Present this to the user before proceeding. The script's check_duplicate() function provides a runtime safety net regardless of whether this step fetched details or not.

Step 4: Identify Tasks via JQL

Run multiple JQL queries to find tasks the user worked on:

Query 1 — Currently assigned, status changed:

assignee = currentUser()
AND status changed DURING ("YYYY-MM-01", "YYYY-MM-31")
ORDER BY updated DESC

Query 2 — Reassigned tasks (worked on before reassignment):

assignee changed FROM currentUser()
AND status changed DURING ("YYYY-MM-01", "YYYY-MM-31")
ORDER BY updated DESC

Query 3 — Tasks with existing worklogs (catches tasks without status changes):

worklogAuthor = currentUser()
AND worklogDate >= "YYYY-MM-01" AND worklogDate <= "YYYY-MM-31"
ORDER BY updated DESC

Merge and deduplicate results from all three queries.

Step 4b: Verify Assignment Dates via Changelog (MANDATORY GATE — HARD STOP if skipped)

🚨 THIS STEP IS NON-NEGOTIABLE. EVERY SINGLE TASK must pass changelog verification before it can appear in the generated script. NO EXCEPTIONS. NO SHORTCUTS.

Why this exists

JQL queries are discovery tools, not proof of assignment. They return false positives:

  • Query 1 (assignee = currentUser()) — user could have been assigned 1 day then reassigned
  • Query 2 (assignee changed FROM currentUser()) — catches tasks from ANY month (e.g., assigned in December, reassigned in January, but Query 2 still returns it in February because status changed)
  • Query 3 (worklogAuthor = currentUser()) — only proves previous logs exist, not current assignment

JQL IS DISCOVERY. CHANGELOG IS PROOF. Never trust JQL alone.

Verification scope

Verify ALL tasks from ALL queries — not just Query 2. Every single task that could appear in the script must be changelog-verified. The only exception is recurring tasks from config.

How to verify — automated Python script

ALWAYS use an automated script. Never manually inspect changelogs one by one — this is error-prone and doesn't scale.

Generate and run a Python verification script via run_in_terminal that:

  1. Fetches each issue with expand=changelog&fields=assignee,summary
  2. Parses all assignee field changes from changelog histories
  3. Builds assignment ranges(start_date, end_date) tuples for when the user was active assignee
  4. Determines valid logging dates — intersection of assignment ranges with target month working days
  5. Outputs a structured report with pass/fail per task
Changelog parsing algorithm (v2 — bugfixed)
# For each issue:
# 1. Collect all assignee changes from changelog histories, sort by created date
# 2. Walk through changes to build assignment ranges:
#    - If change.toString == TARGET_USER:
#        → assignment_start = change.created (the date of THIS changelog entry)
#        → This is when the user BECAME the assignee
#    - If change.fromString == TARGET_USER:
#        → assignment_end = change.created (the date of THIS changelog entry)
#        → This is when the user STOPPED being the assignee
# 3. Handle initial assignment (user was first assignee):
#    - If the first assignee change has fromString == TARGET_USER,
#      user was assigned from issue creation date until that change
# 4. Handle still-assigned (no reassignment after last assignment):
#    - If last change assigned TO user and current assignee matches,
#      range extends to today (or month end, whichever is earlier)
# 5. Build (start_date, end_date) tuples for each continuous assignment period
# 6. Intersect ranges with target month working days → valid_dates list
#
# CRITICAL BUG TO AVOID (v1 mistake):
#   Do NOT use the date of the NEXT changelog entry as the start date.
#   The start date is ALWAYS the date of the entry where toString == TARGET_USER.
#   Example: if changelog shows "assigned to John" at Feb 4 13:44,
#   the assignment starts on Feb 4, NOT on any later date.
What the script must output for each task
TASK       | STATUS | ASSIGNED RANGES              | VALID LOGGING DATES IN MONTH
-----------+--------+------------------------------+------------------------------
PROJ-110   | ✅ OK  | Jan 28 → now                 | Feb 2,3,4,9,12,13,16,17,18,19,20
PROJ-231   | ✅ OK  | Feb 3 → now                  | Feb 3,4,9,12,13,16,17,18,19,20
PROJ-436   | ⚠️ LTD | Feb 18 → Feb 18              | Feb 18 ONLY
PROJ-180   | ❌ OUT | Dec 19 → Jan 8               | NONE (last assigned in January)
PROJ-502   | ❌ OUT | never assigned               | NONE
PROJ-373   | ❌ OUT | never assigned               | NONE
Final summary the script must print
============================================================
ASSIGNMENT VERIFICATION SUMMARY — February 2026
============================================================
✅ Fully valid (assigned entire month):     12 tasks
⚠️  Limited dates (assigned part of month):  6 tasks
❌ REJECTED (not assigned in month):         8 tasks

❌ REJECTED TASKS (will NOT be included):
   PROJ-180 — assigned Dec 19 → Jan 8 (NO February overlap)
   PROJ-502 — never assigned to user
   ...

⚠️  LIMITED TASKS (can ONLY be logged on specific dates):
   PROJ-436 — ONLY on Feb 18
   PROJ-394 — ONLY on Feb 19, 20
   ...
============================================================

HARD RULES — violations are BLOCKING errors

Rule Description
R1: No assignment = No logging If the user was NEVER assigned to a task during the target month, that task MUST NOT appear in the script. Period.
R2: Date-locked logging A task can ONLY be logged on dates when the user was the ACTIVE assignee. If assigned Feb 18-20, it can ONLY appear on Feb 18, 19, 20 — never on Feb 2 or Feb 16.
R3: No stale assignments The assignment range must overlap with at least one working day in the target month. A task assigned in December and still assigned in February is valid (continuous assignment spans months). But a task assigned in December and reassigned away in January has no February overlap — reject it.
R4: Verify ALL sources Tasks from Query 1, Query 2, AND Query 3 must all be verified. Being "currently assigned" (Query 1) doesn't mean assigned for the whole month.
R5: Automated verification only Always run the Python verification script. Never manually assume a task is valid based on JQL results alone.
R6: User confirmation required Present the full verification report (with rejected tasks explicitly listed) and get user confirmation before proceeding.

Anti-patterns — NEVER do these

Anti-pattern Why it's wrong
Trust Query 2 results without changelog check Query 2 returns tasks from ANY month the user was ever assigned to
Assume "status changed in month" = "assigned in month" Status can change by anyone; doesn't prove the user was assigned
Log task on arbitrary date within month Must match actual assignment date range
Skip verification for "currently assigned" tasks User could have been assigned yesterday but task is being logged for start of month
Log a task for the full month when assigned only 2 days Assignment range limits which dates are valid

Integration with Step 6 (script generation)

The verification script produces a task-to-valid-dates mapping. This mapping is the ONLY source of truth for Step 6:

VALID_TASK_DATES = {
    "PROJ-110": ["2026-02-02", "2026-02-03", ..., "2026-02-20"],  # full month
    "PROJ-436": ["2026-02-18"],                                     # single day
    "PROJ-394": ["2026-02-19", "2026-02-20"],                       # partial
}

When distributing tasks across days in Step 6:

  • Only place a task on a date that exists in its valid dates list
  • If a task has very few valid dates, schedule it on those dates first (they're constrained)
  • Schedule unconstrained tasks (full month assignment) to fill remaining slots

Step 4c: Verify Actual Work via Status Changes (MANDATORY)

Assignment alone is NOT proof of work. A task can be assigned to the user for the entire month, but if DEV DONE / QA / Code Review was reached BEFORE the target month, the user only clicked status buttons in the target month — no actual development work happened.

Why this exists

Common pattern: Task was assigned Jan 28, user coded it Jan 28-29, moved to DEV DONE Jan 29. In February, user just clicks "DEV DONE → Feature acceptance" — that's a 2-second click, not 8h of work. Logging hours for this in February is dishonest.

How to verify

Generate and run a Python script via run_in_terminal that checks status change history for each task:

  1. Fetch each issue with expand=changelog&fields=summary,status
  2. Find all status changes — especially transitions to "done" states: DEV DONE, QA, Code Review, Feature Acceptance, Done, Closed
  3. Determine when DEV work completed — the first transition to a "done" state (Code Review, QA, DEV DONE, etc.)
  4. Compare to target month — if DEV DONE happened BEFORE the target month, flag it

Decision criteria

Scenario Verdict
DEV DONE / QA / CR reached before target month, only status clicks in target month REMOVE — no real work
DEV DONE / QA reached before target month, but task returned to development in target month (re-opened, rework) KEEP — actual rework happened
DEV DONE reached during target month KEEP — development work happened
No DEV DONE yet (still in development) KEEP — work is ongoing
Task moved To Do → Development In Progress → QA/CR all within target month KEEP — full dev cycle
Only "Feature acceptance" or "Done" click in target month (everything else was before) REMOVE — just a status button

Integration with Step 4b

Run this check AFTER Step 4b (assignment verification). Tasks that pass Step 4b but fail Step 4c are removed. The output should clearly state why each task was removed.

Present suspicious tasks to the user with their status timeline and ask for confirmation before removing.

Step 5: Confirm with User (MANDATORY)

ALWAYS ask ALL of the following before generating the script. NEVER skip this step.

Use the ask_questions tool to ask all questions in a single call:

  1. Date range & cutoff: Determine the cutoff date. NEVER log future dates — only log working days that have already passed as of today. If today is mid-month, the script should only cover days up to and including the last past working day. Calculate: if today is a weekday, cutoff = yesterday; if today is weekend, cutoff = last Friday. Ask the user to confirm: "Today is {date}. I'll log only up to {cutoff_date} ({N} working days, {N*8}h). Remaining days ({list}) will need a separate run later. OK?"
  2. Skip days: Any holidays, vacation, sick days, or days to log manually within the date range?
  3. Recurring tasks: Config has these recurring tasks: [list from config]. Are they still correct for this month? Which specific weeks? (Only include recurring days that fall within the cutoff range.)
  4. Existing worklogs: Show the summary from Step 3. Ask: "These are already logged. Should I skip these days entirely, or fill in remaining hours on partial days?"
  5. Task corrections: Present the full merged task list. Ask if anything should be added/removed.

Wait for all answers before proceeding.

Step 6: Generate Script

Use the worklog template as the base. Fill in:

  1. Config values (site, email, timezone, start time)
  2. Cutoff date — if generating mid-month, only include log_work calls for past working days. Redistribute all tasks proportionally across the available (past) days. Each day must still total exactly hoursPerDay. Use smaller per-task durations (2h, 3h, 4h) to fit more tasks per day.
  3. MONTH_START_TS and MONTH_END_TS — Unix timestamps in milliseconds for date range filtering. CRITICAL: Always compute with python3 to avoid errors:
    # MONTH_START_TS — first day of target month
    python3 -c "import datetime; d=datetime.datetime(YYYY,M,1,tzinfo=datetime.timezone.utc); print(int(d.timestamp()*1000))"
    # MONTH_END_TS — first day of NEXT month (handles December→January rollover)
    python3 -c "import datetime; y,m=(YYYY,M+1) if M<12 else (YYYY+1,1); d=datetime.datetime(y,m,1,tzinfo=datetime.timezone.utc); print(int(d.timestamp()*1000))"
    • MONTH_START_TS = first day of the target month at 00:00 UTC
    • MONTH_END_TS = first day of the NEXT month at 00:00 UTC
    • NEVER compute timestamps manually or approximate — wrong timestamps cause duplicate detection to fail silently
  4. log_work calls for each working day — skip days already fully logged
  5. Recurring task entries on configured days (only those within cutoff range)
  6. Regular task entries distributed across remaining days — date-locked (see below)

Date-Lock Enforcement (from Step 4b verification)

The VALID_TASK_DATES mapping from Step 4b is the sole authority for which tasks can appear on which dates. When distributing tasks across working days:

  1. Constrained tasks first — Tasks with ≤3 valid dates MUST be scheduled first on those exact dates (they have no flexibility)
  2. Limited tasks next — Tasks valid for part of the month fill slots on their valid dates
  3. Full-month tasks last — Tasks valid for the entire month fill remaining slots as needed
  4. NEVER place a task outside its valid date range — If a task is valid only for Feb 18-20, it can ONLY appear on those 3 days
  5. Each log_work call must include an inline comment showing the task's assignment range for traceability

Example:

# Feb 18: PROJ-511 (assigned Feb 18+), PROJ-436 (assigned Feb 12+)
log_work "PROJ-511" "2026-02-18" 10800 "Implement first user login via link"
log_work "PROJ-436" "2026-02-18" 10800 "Redesign sign in flow"

Present the user with a full distribution table before generating the script. The table must show:

  • Date, task, hours, description, and assignment range for each entry
  • Daily totals (must equal hoursPerDay)
  • Grand total (must equal working days × hoursPerDay)
  • Rejected tasks with reasons

Get explicit user approval of the distribution before generating the script.

Script naming: jira-worklog-{month}-{year}.sh (e.g., jira-worklog-january-2026.sh)

Step 7: Run the Script (MANDATORY — DO NOT SKIP)

🚨 After generating the script, you MUST offer to run it immediately. This step is the whole point of the skill — generating a script without executing it is incomplete work.

Procedure

  1. Ask the user: "Script is ready. Do you want me to run it now and log the hours to Jira?"
  2. If the user says yes:
    a. Check JIRA_API_TOKEN: Ask the user if the token is set in their environment. If not, guide them:
    export JIRA_API_TOKEN="your-token-here"
    # Generate at: https://id.atlassian.com/manage-profile/security/api-tokens
    b. Run dry-run first: Execute bash jira-worklog-{month}-{year}.sh --dry-run to verify everything looks correct
    c. Show dry-run output to the user and ask for final confirmation: "Dry-run looks good — {N} entries, {H}h total. Proceed with actual logging?"
    d. Execute for real: Run bash jira-worklog-{month}-{year}.sh and show the output
    e. Report results: Summarize successes, skipped duplicates, and any failures
  3. If the user says no: Provide instructions for manual execution later (dry-run command, then real command, token setup)

Why this is mandatory

The user asked for hours to be logged, not for a script to be generated. The script is a means to an end. If the skill stops after generating the script, the user's request is not fulfilled.

Rollback — Deleting Wrong Worklogs

If the script logged incorrect hours, use this to find and delete them:

# List worklogs for a specific issue in the target month:
curl -s -u "$EMAIL:$JIRA_API_TOKEN" \
  "https://{site}/rest/api/3/issue/{issueKey}/worklog?startedAfter={MONTH_START_TS}&startedBefore={MONTH_END_TS}" \
  | jq '.worklogs[] | select(.author.emailAddress == "{email}") | {id, started, timeSpentSeconds}'

# Delete a specific worklog by ID:
curl -s -u "$EMAIL:$JIRA_API_TOKEN" \
  -X DELETE \
  "https://{site}/rest/api/3/issue/{issueKey}/worklog/{worklogId}"

API Reference

Endpoint

POST https://{site}/rest/api/3/issue/{issueKey}/worklog

Note: {site} is the full domain from config (e.g., your-company.atlassian.net).

Authentication

Always use curl -u flag (NOT Base64 Authorization header — that causes 404 errors):

curl -u "$EMAIL:$API_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -X POST \
  --data "$PAYLOAD" \
  "$URL"

Token Source

The script reads the API token from the JIRA_API_TOKEN environment variable. NEVER hardcode tokens in the script.

# User sets before running:
export JIRA_API_TOKEN="your-token-here"

Generate new tokens at: https://id.atlassian.com/manage-profile/security/api-tokens

Payload Format

Without comments (includeComments: false, default):

{
  "started": "2026-01-05T09:00:00.000+0100",
  "timeSpentSeconds": 7200
}

With comments (includeComments: true):

{
  "started": "2026-01-05T09:00:00.000+0100",
  "timeSpentSeconds": 7200,
  "comment": {
    "type": "doc",
    "version": 1,
    "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Description here"}]}]
  }
}

Timezone Handling

  • "auto" → script uses date +%z at runtime (handles DST automatically)
  • Explicit offset → used directly (e.g., "+0100" for CET winter, "+0200" for CEST summer)

Important: If a month spans a DST change (March/October in Europe), use "auto".

Time Calculations

  • 1 hour = 3600 seconds
  • 8 hours = 28800 seconds

Hour Distribution Rules

Daily Limit

  • Exactly hoursPerDay per working day (default 8h = 28800s)
  • Never exceed or go under

Recurring Task Days

  • Log configured hours to the recurring task issue
  • If recurringTask.hours < hoursPerDay, distribute remaining hours to other tasks

Regular Working Days — SP-Weighted Distribution

Story Points (SP) determine how many hours a task gets. Tasks with higher SP get proportionally more hours.

Fetching Story Points

Fetch SP for all tasks via Jira API:

curl -s -u "$EMAIL:$API_TOKEN" \
  "https://{site}/rest/api/3/issue/{key}?fields=summary,story_points,customfield_10016,customfield_10028"

Try story_points, then customfield_10016, then customfield_10028 (the field name varies by Jira instance).

Weighting tiers

SP Tier Target hours per task Rationale
8+ XL 16-24h (3-4 entries) Epic-level complexity
5 L 10-14h (2-3 entries) Major feature, multi-day effort
3 M 4-8h (1-2 entries) Standard task, ~1 day
2 S 3-4h (1 entry) Small change
1 XS 2h (1 entry) Trivial fix
None / bug S 2-4h (1 entry) Treat as ~2 SP (typically quick fixes)

How to calculate

  1. Assign default SP to tasks without SP (default: 2 SP — these are typically bugs/quick fixes)
  2. Sum all SP across all tasks in the pool (excluding recurring tasks)
  3. Calculate hours per SP unit: available_hours / total_SP
  4. Multiply: each task gets task_SP × hours_per_SP_unit hours
  5. Round to practical values (2h, 3h, 4h, 6h, 8h) — avoid 1h entries
  6. Adjust for date constraints — constrained tasks may need fewer hours if they have very few valid dates
  7. Balance total to exactly match working_days × hoursPerDay

Constraints

  • SP weighting is a guideline, not a strict formula
  • Date constraints override SP weighting (a 13SP task with only 1 valid day can't get 24h)
  • Minimum entry is 2h; avoid 1h entries (they don't feel natural)
  • A task CAN get a full 8h day if it's the only one available on that date (e.g., constrained tasks)
  • Present the weighted hour allocation to the user in a table before generating the script

Distribution Patterns per Day

Vary distribution across days — don't use the same pattern every day:

  • 4 tasks: 2h + 2h + 2h + 2h
  • 3 tasks: 3h + 3h + 2h
  • 2 tasks: 4h + 4h, or 6h + 2h
  • 1 task: full day (only for focused investigations or workshops)

Multi-day Tasks

When a task spans multiple days, vary the description:

  • Day 1: "analysis and implementation"
  • Day 2: "testing and fixes"
  • Day 3: "code review fixes"
  • Day 4: "QA feedback and final touches"

Script Safety Features

The generated script (from the template) includes:

Feature Description
--dry-run flag Fully offline preview — no API token, jq, or network required
Duplicate detection GET existing worklogs before each POST; skip if same issue+date+seconds already exists
Pre-flight validation Verify all issue keys exist via GET before logging anything
Error body output On failure, print the full API error response (not just HTTP code)
Retry on 5xx/429 One retry with 3s delay on 5xx; up to 3 retries with exponential backoff on 429
Dynamic summary Count entries, hours, and working days automatically at the end
Token from env var Reads $JIRA_API_TOKEN; refuses to run if not set

Common Errors

HTTP Code Cause Solution
401 Expired/invalid API token Generate new token at https://id.atlassian.com/manage-profile/security/api-tokens
400 Invalid date format or payload Check started format and JSON structure
403 No permission on issue Verify project access
404 Issue not found or wrong URL Verify issue key exists and URL construction
429 Rate limited Script retries with exponential backoff (up to 3 retries)

Checklist Before Generating Script

  • Config file loaded and validated
  • MCP connectivity verified
  • Existing worklogs fetched and presented to user
  • All tasks verified with user (including reassigned)
  • Assignment dates verified via changelog (v2 parser) — each task logged ONLY on dates when user was active assignee
  • Work verified via status changes — tasks where DEV DONE was before target month are removed (Step 4c)
  • SP-weighted distribution — hours per task proportional to story points
  • Date-lock enforced — every log_work call is on a date within the task's verified assignment range
  • Distribution table presented and approved by user — full table with date, task, hours, SP, description, assignment range
  • Skip days confirmed (holidays, vacation, sick days, manual days)
  • Cutoff date applied — NO future dates (only past working days as of today)
  • Recurring task days identified and confirmed for this month (within cutoff range)
  • Only generating entries for days NOT already fully logged
  • Hours balance to exactly hoursPerDay per working day
  • Task count matches available hours (tasks redistributed proportionally if fewer days than full month)
  • Script uses $JIRA_API_TOKEN env var (no hardcoded tokens)
  • MONTH_START_TS/MONTH_END_TS computed with python3 (NEVER manually)
  • Script executed (Step 7) — offered to run, dry-run shown, real execution completed (or user explicitly declined)