Skip to main content

Jira Sprint Distribution Rules

Companion to ROADMAP.md. Automation rules and manual procedures for distributing GEHR epics and stories into Jira sprints based on the roadmap slice mapping.

Jira Project: GEHR on ghasitech.atlassian.net


1. Sprint Structure

Sprint Naming Convention

GEHR-S{slice}-Sprint-{N}

Examples:

  • GEHR-S0-Sprint-1 through GEHR-S0-Sprint-6 (M0: Platform Foundation)
  • GEHR-S1-Sprint-1 through GEHR-S1-Sprint-6 (M1: Core Clinical)
  • GEHR-S2-Sprint-1 through GEHR-S2-Sprint-6 (M2: Orders & Billing)
  • GEHR-S3-Sprint-1 through GEHR-S3-Sprint-6 (M3: Integrated Care)
  • GEHR-S4-Sprint-1 through GEHR-S4-Sprint-6 (M4: Full Platform)
  • GEHR-S5-Sprint-1 through GEHR-S5-Sprint-6 (M5: National Scale)

Sprint Duration

  • 2 weeks per sprint
  • 6 sprints per milestone (3 months)
  • 36 total sprints across M0–M5

Jira Board Configuration

  • Board type: Scrum
  • Estimation: Story Points
  • Sprint capacity: configurable per team (see team-capacity-model.md)

2. Label Taxonomy for Automation

Required Labels on All Issues

Every epic and story in GEHR must have these labels for automation to work:

LabelFormatExamplePurpose
sliceslice-S{N}slice-S0, slice-S1Maps issue to milestone slice
modulemod-{name}mod-iam, mod-registrationMaps to owning microservice
tracktrack-{type}track-baseline, track-enhancementBaseline vs enhancement
priority-grouppg-{N}pg-1, pg-2, pg-3Sprint ordering within slice
teamteam-{name}team-platform, team-clinicalOwning team
gategate-{code}gate-offline, gate-ai, gate-securityCross-cutting concern

Priority Groups (Sprint Assignment Order)

Priority GroupLabelDescriptionTypical Sprint
PG-1pg-1Foundation / blockers for other storiesSprint 1–2
PG-2pg-2Core features / main vertical sliceSprint 2–4
PG-3pg-3Depth, polish, integration, testingSprint 4–6

3. Module → Slice → Sprint Mapping Table

Use this table to bulk-label existing backlog issues. This is the authoritative mapping for Jira automation.

S0 — Platform Foundation (Sprints 1–6)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
iamIAM-EPICslice-S0pg-1Sprint 1team-platform
tenantTEN-EPICslice-S0pg-1Sprint 1–2team-platform
hierarchyHIER-EPICslice-S0pg-1Sprint 1–2team-platform
licensingLICN-EPICslice-S0pg-2Sprint 2–3team-platform
access-policyACPOL-EPIC-01–04slice-S0pg-2Sprint 2–3team-platform
auditAUD-EPICslice-S0pg-1Sprint 1–3team-security
config-resolverCFG-EPICslice-S0pg-2Sprint 2–3team-platform
fhir-gatewayFHIR-EPIC-01slice-S0pg-2Sprint 2–3team-platform
terminologyTERM-EPIC-01slice-S0pg-2Sprint 3team-platform
ai-orchestratorAIO-EPIC-01slice-S0pg-2Sprint 3–4team-ai
platform-adminPADM-EPIC-01–03slice-S0pg-3Sprint 3–5team-platform
desktop-electronDESK-EPIC-01slice-S0pg-1Sprint 1–2team-desktop
desktop-electronDESK-EPIC-02slice-S0pg-2Sprint 2–4team-desktop

S1 — Core Clinical (Sprints 7–12)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
registrationREG-EPICslice-S1pg-1Sprint 7–8team-clinical
provider-directoryPROV-EPICslice-S1pg-1Sprint 7team-clinical
facility-managementFAC-EPICslice-S1pg-1Sprint 7team-clinical
schedulingSCHED-EPICslice-S1pg-2Sprint 8–9team-clinical
patient-chartPCHART-EPICslice-S1pg-2Sprint 8–9team-clinical
clinical-notesCNOTE-EPICslice-S1pg-2Sprint 9–10team-clinical
vitalsVIT-EPICslice-S1pg-2Sprint 8–9team-clinical
problem-listPROB-EPICslice-S1pg-2Sprint 9team-clinical
allergiesALG-EPICslice-S1pg-2Sprint 9team-clinical
medication-managementMED-EPICslice-S1pg-2Sprint 9–10team-clinical
desktop-electronDESK-EPIC-03slice-S1pg-2Sprint 8–11team-desktop
ai-orchestratorAIO-EPIC-02slice-S1pg-3Sprint 10–11team-ai

S2 — Orders & Billing (Sprints 13–18)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
orders-cpoeORD-EPICslice-S2pg-1Sprint 13–15team-diagnostics
resultsRES-EPICslice-S2pg-2Sprint 14–16team-diagnostics
terminologyTERM-EPIC-02–06slice-S2pg-1Sprint 13–14team-platform
billingBILL-EPIC-01–02slice-S2pg-2Sprint 15–17team-finance
document-managementDOC-EPIC-01–08slice-S2pg-2Sprint 14–17team-clinical

S3 — Integrated Care (Sprints 19–24)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
laboratory-lisLAB-EPICslice-S3pg-1Sprint 19–22team-diagnostics
pharmacyPHARM-EPICslice-S3pg-1Sprint 19–21team-pharmacy
ghasi-e-prescribing-gatewayEPRESC-EPICslice-S3pg-2Sprint 20–22team-interop
digital-communicationDCOM-EPICslice-S3pg-2Sprint 19–22team-engagement
patient-portal-apiPortal EPICsslice-S3pg-2Sprint 20–23team-engagement
insuranceINS-EPIC-01–04slice-S3pg-3Sprint 21–23team-finance
desktop-electronDESK-EPIC-04,05slice-S3pg-3Sprint 22–24team-desktop

S4 — Full Platform (Sprints 25–30)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
radiology-pacsRAD-EPICslice-S4pg-1Sprint 25–28team-diagnostics
health-populationHPOP-EPICslice-S4pg-1Sprint 25–28team-pophealth
billingBILL-EPIC-03–05slice-S4pg-2Sprint 25–28team-finance
claimsCLM-EPICslice-S4pg-2Sprint 26–29team-finance
insuranceINS-EPIC-05–08slice-S4pg-2Sprint 27–29team-finance
immunizationsIMMZ-EPICslice-S4pg-2Sprint 26–29team-clinical
care-plansCP-EPICslice-S4pg-3Sprint 27–29team-clinical
hl7v2-interopHL7-EPICslice-S4pg-3Sprint 27–29team-interop
ai-orchestratorAIO-EPIC-03slice-S4pg-3Sprint 28–30team-ai
access-policyACPOL-EPIC-05–07slice-S4pg-3Sprint 28–29team-platform
desktop-electronDESK-EPIC-06,08,09,10slice-S4pg-3Sprint 27–30team-desktop

S5 — National Scale (Sprints 31–36)

ModuleEpic PrefixSlice LabelPriority GroupTarget SprintTeam Label
All (hardening)Variousslice-S5pg-2Sprint 33–36Various
platform-adminPADM-EPIC-04–07slice-S5pg-2Sprint 31–34team-platform
desktop-electronDESK-EPIC-11slice-S5pg-3Sprint 34–36team-desktop

4. Jira Automation Rules

Rule 1: Auto-assign sprint based on slice + priority group labels

Trigger: Issue label changed (or issue created with labels) Condition: Issue has both slice-S{N} and pg-{M} labels Action: Move issue to the correct sprint

{
"name": "GEHR: Auto-assign sprint from slice + priority group",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "slice-S\\d+"
},
{
"type": "label_matches",
"pattern": "pg-\\d+"
}
],
"actions": [
{
"type": "move_to_sprint",
"sprint_mapping": {
"slice-S0 + pg-1": "GEHR-S0-Sprint-1",
"slice-S0 + pg-2": "GEHR-S0-Sprint-3",
"slice-S0 + pg-3": "GEHR-S0-Sprint-5",
"slice-S1 + pg-1": "GEHR-S1-Sprint-1",
"slice-S1 + pg-2": "GEHR-S1-Sprint-3",
"slice-S1 + pg-3": "GEHR-S1-Sprint-5",
"slice-S2 + pg-1": "GEHR-S2-Sprint-1",
"slice-S2 + pg-2": "GEHR-S2-Sprint-3",
"slice-S2 + pg-3": "GEHR-S2-Sprint-5",
"slice-S3 + pg-1": "GEHR-S3-Sprint-1",
"slice-S3 + pg-2": "GEHR-S3-Sprint-3",
"slice-S3 + pg-3": "GEHR-S3-Sprint-5",
"slice-S4 + pg-1": "GEHR-S4-Sprint-1",
"slice-S4 + pg-2": "GEHR-S4-Sprint-3",
"slice-S4 + pg-3": "GEHR-S4-Sprint-5",
"slice-S5 + pg-1": "GEHR-S5-Sprint-1",
"slice-S5 + pg-2": "GEHR-S5-Sprint-3",
"slice-S5 + pg-3": "GEHR-S5-Sprint-5"
}
}
]
}

Rule 2: Auto-label stories from parent epic

Trigger: Issue linked to epic (parent link set) Condition: Parent epic has slice-S{N} label Action: Copy slice, module, team, and track labels to child story

{
"name": "GEHR: Inherit labels from parent epic",
"trigger": "issue_linked",
"conditions": [
{
"type": "link_type",
"value": "is child of"
},
{
"type": "parent_has_label",
"pattern": "slice-S\\d+"
}
],
"actions": [
{
"type": "copy_labels_from_parent",
"labels": ["slice-*", "mod-*", "team-*", "track-*"]
}
]
}

Rule 3: Auto-set component from module label

Trigger: Label mod-{name} added to issue Action: Set Jira component to matching module name

{
"name": "GEHR: Set component from module label",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "mod-.*"
}
],
"actions": [
{
"type": "set_component",
"component_from_label": "mod-{name} → {name}"
}
]
}

Rule 4: Milestone gate reminder

Trigger: Sprint completed Condition: Sprint name matches GEHR-S{N}-Sprint-6 (last sprint of slice) Action: Create "Milestone Gate Review" task and notify stakeholders

{
"name": "GEHR: Milestone gate review at slice end",
"trigger": "sprint_completed",
"conditions": [
{
"type": "sprint_name_matches",
"pattern": "GEHR-S\\d+-Sprint-6"
}
],
"actions": [
{
"type": "create_issue",
"summary": "Milestone M{N} Gate Review",
"issue_type": "Task",
"labels": ["gate-review", "milestone-M{N}"],
"description": "Review all readiness gates per slice-release-readiness.md. Verify F/N/A/O/S/T/V/D dimensions."
},
{
"type": "send_notification",
"to": ["architecture-team", "security-team", "product-team"],
"message": "Milestone M{N} gate review is due. Please complete readiness checklist."
}
]
}

Rule 5: Flag stories without required labels

Trigger: Issue created or moved to sprint Condition: Issue is Story type AND missing any of: slice-*, mod-*, team-*, pg-* Action: Flag issue + add comment

{
"name": "GEHR: Flag unlabeled stories",
"trigger": "issue_moved_to_sprint",
"conditions": [
{
"type": "issue_type",
"value": "Story"
},
{
"type": "missing_labels",
"required_patterns": ["slice-S\\d+", "mod-.*", "team-.*", "pg-\\d+"]
}
],
"actions": [
{
"type": "flag_issue"
},
{
"type": "add_comment",
"body": "⚠️ This story is missing required labels for sprint automation. Please add: slice-S{N}, mod-{module}, team-{team}, pg-{priority}. See docs/roadmap/jira-sprint-distribution-rules.md for the mapping table."
}
]
}

Rule 6: Cross-cutting gate label triggers checklist

Trigger: Label gate-offline or gate-ai or gate-security added Action: Add relevant checklist items to the issue

{
"name": "GEHR: Add gate checklist on label",
"trigger": "label_changed",
"conditions": [
{
"type": "label_matches",
"pattern": "gate-.*"
}
],
"actions": [
{
"type": "add_checklist",
"mapping": {
"gate-offline": [
"Offline E2E test passes (airplane mode)",
"Sync round-trip verified",
"Conflict resolution tested",
"Stale-data indicator visible",
"Idempotency verified",
"SQLite encryption verified"
],
"gate-ai": [
"Provenance tag on AI outputs",
"Safety classifier active",
"PII redaction before cloud calls",
"Per-tenant budget respected",
"Graceful degradation tested",
"Prompt regression tests pass"
],
"gate-security": [
"Tenant isolation test green",
"PHI encryption verified",
"Audit events emitting",
"No hardcoded secrets",
"Rate limiting configured",
"ModuleEntitlementGuard active"
]
}
}
]
}

5. Bulk Labeling Script (PowerShell)

Use this script to bulk-apply labels to existing backlog issues based on the mapping table above. Run after creating sprints in Jira.

# bulk-label-gehr-issues.ps1
# Prerequisites: Jira REST API token, jq installed
# Usage: ./bulk-label-gehr-issues.ps1

$JiraBase = "https://ghasitech.atlassian.net"
$Project = "GEHR"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))

# Module → Slice → Priority Group → Team mapping
$Mappings = @(
# S0 - Platform Foundation
@{ Module = "iam"; EpicPrefix = "IAM-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "tenant"; EpicPrefix = "TEN-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "hierarchy"; EpicPrefix = "HIER-EPIC"; Slice = "S0"; PG = "1"; Team = "platform" }
@{ Module = "licensing"; EpicPrefix = "LICN-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "access-policy"; EpicPrefix = "ACPOL-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "audit"; EpicPrefix = "AUD-EPIC"; Slice = "S0"; PG = "1"; Team = "security" }
@{ Module = "config-resolver"; EpicPrefix = "CFG-EPIC"; Slice = "S0"; PG = "2"; Team = "platform" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-01"; Slice = "S0"; PG = "2"; Team = "ai" }
@{ Module = "platform-admin"; EpicPrefix = "PADM-EPIC"; Slice = "S0"; PG = "3"; Team = "platform" }

# S1 - Core Clinical
@{ Module = "registration"; EpicPrefix = "REG-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "provider-directory"; EpicPrefix = "PROV-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "facility-management"; EpicPrefix = "FAC-EPIC"; Slice = "S1"; PG = "1"; Team = "clinical" }
@{ Module = "scheduling"; EpicPrefix = "SCHED-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "patient-chart"; EpicPrefix = "PCHART-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "clinical-notes"; EpicPrefix = "CNOTE-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "vitals"; EpicPrefix = "VIT-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "problem-list"; EpicPrefix = "PROB-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "allergies"; EpicPrefix = "ALG-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "medication-management"; EpicPrefix = "MED-EPIC"; Slice = "S1"; PG = "2"; Team = "clinical" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-02"; Slice = "S1"; PG = "3"; Team = "ai" }

# S2 - Orders & Billing
@{ Module = "orders-cpoe"; EpicPrefix = "ORD-EPIC"; Slice = "S2"; PG = "1"; Team = "diagnostics" }
@{ Module = "results"; EpicPrefix = "RES-EPIC"; Slice = "S2"; PG = "2"; Team = "diagnostics" }
@{ Module = "terminology"; EpicPrefix = "TERM-EPIC"; Slice = "S2"; PG = "1"; Team = "platform" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-01"; Slice = "S2"; PG = "2"; Team = "finance" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-02"; Slice = "S2"; PG = "2"; Team = "finance" }
@{ Module = "document-management"; EpicPrefix = "DOC-EPIC"; Slice = "S2"; PG = "2"; Team = "clinical" }

# S3 - Integrated Care
@{ Module = "laboratory-lis"; EpicPrefix = "LAB-EPIC"; Slice = "S3"; PG = "1"; Team = "diagnostics" }
@{ Module = "pharmacy"; EpicPrefix = "PHARM-EPIC"; Slice = "S3"; PG = "1"; Team = "pharmacy" }
@{ Module = "ghasi-e-prescribing-gateway"; EpicPrefix = "EPRESC-EPIC"; Slice = "S3"; PG = "2"; Team = "interop" }
@{ Module = "digital-communication"; EpicPrefix = "DCOM-EPIC"; Slice = "S3"; PG = "2"; Team = "engagement" }
@{ Module = "insurance"; EpicPrefix = "INS-EPIC"; Slice = "S3"; PG = "3"; Team = "finance" }

# S4 - Full Platform
@{ Module = "radiology-pacs"; EpicPrefix = "RAD-EPIC"; Slice = "S4"; PG = "1"; Team = "diagnostics" }
@{ Module = "health-population"; EpicPrefix = "HPOP-EPIC"; Slice = "S4"; PG = "1"; Team = "pophealth" }
@{ Module = "billing"; EpicPrefix = "BILL-EPIC-03"; Slice = "S4"; PG = "2"; Team = "finance" }
@{ Module = "claims"; EpicPrefix = "CLM-EPIC"; Slice = "S4"; PG = "2"; Team = "finance" }
@{ Module = "immunizations"; EpicPrefix = "IMMZ-EPIC"; Slice = "S4"; PG = "2"; Team = "clinical" }
@{ Module = "care-plans"; EpicPrefix = "CP-EPIC"; Slice = "S4"; PG = "3"; Team = "clinical" }
@{ Module = "ai-orchestrator"; EpicPrefix = "AIO-EPIC-03"; Slice = "S4"; PG = "3"; Team = "ai" }
)

foreach ($m in $Mappings) {
$labels = @("slice-$($m.Slice)", "mod-$($m.Module)", "team-$($m.Team)", "pg-$($m.PG)", "track-baseline")

# Find epics matching prefix in summary
$jql = "project = $Project AND issuetype = Epic AND summary ~ `"$($m.EpicPrefix)`""
$searchUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($jql))&maxResults=100"

$response = Invoke-RestMethod -Uri $searchUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}

foreach ($issue in $response.issues) {
$existingLabels = $issue.fields.labels
$newLabels = ($existingLabels + $labels) | Sort-Object -Unique

$body = @{
fields = @{
labels = $newLabels
}
} | ConvertTo-Json -Depth 3

$updateUrl = "$JiraBase/rest/api/3/issue/$($issue.key)"
Invoke-RestMethod -Uri $updateUrl -Method Put -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $body

Write-Host "Labeled $($issue.key) ($($issue.fields.summary)) with: $($labels -join ', ')"

# Also label child stories
$childJql = "project = $Project AND issuetype = Story AND parent = $($issue.key)"
$childUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($childJql))&maxResults=100"
$childResponse = Invoke-RestMethod -Uri $childUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}

foreach ($child in $childResponse.issues) {
$childExisting = $child.fields.labels
$childNew = ($childExisting + $labels) | Sort-Object -Unique

$childBody = @{
fields = @{
labels = $childNew
}
} | ConvertTo-Json -Depth 3

$childUpdateUrl = "$JiraBase/rest/api/3/issue/$($child.key)"
Invoke-RestMethod -Uri $childUpdateUrl -Method Put -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $childBody

Write-Host " Labeled child $($child.key) with: $($labels -join ', ')"
}

Start-Sleep -Milliseconds 200 # Rate limiting
}
}

6. Sprint Creation Script (PowerShell)

Create all 36 sprints in Jira before running the labeling script.

# create-gehr-sprints.ps1
# Prerequisites: Jira REST API token, board ID known
# Usage: ./create-gehr-sprints.ps1 -BoardId <your-board-id>

param(
[Parameter(Mandatory=$true)]
[int]$BoardId
)

$JiraBase = "https://ghasitech.atlassian.net"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))

# Define start date (adjust to your actual project start)
$ProjectStart = Get-Date "2026-05-01"
$SprintDuration = 14 # days

$Slices = @(
@{ Name = "S0"; Label = "Platform Foundation"; Milestone = "M0" }
@{ Name = "S1"; Label = "Core Clinical"; Milestone = "M1" }
@{ Name = "S2"; Label = "Orders & Billing"; Milestone = "M2" }
@{ Name = "S3"; Label = "Integrated Care"; Milestone = "M3" }
@{ Name = "S4"; Label = "Full Platform"; Milestone = "M4" }
@{ Name = "S5"; Label = "National Scale"; Milestone = "M5" }
)

$sprintIndex = 0
foreach ($slice in $Slices) {
for ($i = 1; $i -le 6; $i++) {
$startDate = $ProjectStart.AddDays($sprintIndex * $SprintDuration)
$endDate = $startDate.AddDays($SprintDuration)

$sprintName = "GEHR-$($slice.Name)-Sprint-$i"
$goal = "$($slice.Milestone): $($slice.Label) - Sprint $i of 6"

$body = @{
name = $sprintName
startDate = $startDate.ToString("yyyy-MM-dd")
endDate = $endDate.ToString("yyyy-MM-dd")
originBoardId = $BoardId
goal = $goal
} | ConvertTo-Json

$createUrl = "$JiraBase/rest/agile/1.0/sprint"
$response = Invoke-RestMethod -Uri $createUrl -Method Post -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $body

Write-Host "Created sprint: $sprintName (ID: $($response.id)) [$($startDate.ToString('MMM dd')) - $($endDate.ToString('MMM dd'))]"

$sprintIndex++
Start-Sleep -Milliseconds 200
}
}

7. Move Issues to Sprints Script (PowerShell)

After labeling and sprint creation, move issues to their target sprints.

# move-to-sprints.ps1
# Moves labeled issues to their target sprints

$JiraBase = "https://ghasitech.atlassian.net"
$Project = "GEHR"
$Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${env:JIRA_EMAIL}:${env:JIRA_API_TOKEN}"))

# First, get all sprint IDs
param(
[Parameter(Mandatory=$true)]
[int]$BoardId
)

$sprintsUrl = "$JiraBase/rest/agile/1.0/board/$BoardId/sprint?maxResults=50"
$sprints = (Invoke-RestMethod -Uri $sprintsUrl -Headers @{
Authorization = "Basic $Auth"
}).values

# Build sprint name → ID lookup
$sprintLookup = @{}
foreach ($s in $sprints) {
$sprintLookup[$s.name] = $s.id
}

# Slice + PG → Sprint name mapping
$SprintMap = @{
"slice-S0:pg-1" = "GEHR-S0-Sprint-1"
"slice-S0:pg-2" = "GEHR-S0-Sprint-3"
"slice-S0:pg-3" = "GEHR-S0-Sprint-5"
"slice-S1:pg-1" = "GEHR-S1-Sprint-1"
"slice-S1:pg-2" = "GEHR-S1-Sprint-3"
"slice-S1:pg-3" = "GEHR-S1-Sprint-5"
"slice-S2:pg-1" = "GEHR-S2-Sprint-1"
"slice-S2:pg-2" = "GEHR-S2-Sprint-3"
"slice-S2:pg-3" = "GEHR-S2-Sprint-5"
"slice-S3:pg-1" = "GEHR-S3-Sprint-1"
"slice-S3:pg-2" = "GEHR-S3-Sprint-3"
"slice-S3:pg-3" = "GEHR-S3-Sprint-5"
"slice-S4:pg-1" = "GEHR-S4-Sprint-1"
"slice-S4:pg-2" = "GEHR-S4-Sprint-3"
"slice-S4:pg-3" = "GEHR-S4-Sprint-5"
"slice-S5:pg-1" = "GEHR-S5-Sprint-1"
"slice-S5:pg-2" = "GEHR-S5-Sprint-3"
"slice-S5:pg-3" = "GEHR-S5-Sprint-5"
}

foreach ($entry in $SprintMap.GetEnumerator()) {
$parts = $entry.Key -split ":"
$sliceLabel = $parts[0]
$pgLabel = $parts[1]
$sprintName = $entry.Value

$sprintId = $sprintLookup[$sprintName]
if (-not $sprintId) {
Write-Warning "Sprint not found: $sprintName"
continue
}

# Find issues with both labels
$jql = "project = $Project AND labels = `"$sliceLabel`" AND labels = `"$pgLabel`" AND sprint is EMPTY"
$searchUrl = "$JiraBase/rest/api/3/search?jql=$([uri]::EscapeDataString($jql))&maxResults=200&fields=key,summary"
$response = Invoke-RestMethod -Uri $searchUrl -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
}

if ($response.issues.Count -eq 0) {
Write-Host "No unassigned issues for $sliceLabel + $pgLabel"
continue
}

$issueKeys = $response.issues | ForEach-Object { $_.key }

# Move to sprint
$moveBody = @{
issues = $issueKeys
} | ConvertTo-Json

$moveUrl = "$JiraBase/rest/agile/1.0/sprint/$sprintId/issue"
Invoke-RestMethod -Uri $moveUrl -Method Post -Headers @{
Authorization = "Basic $Auth"
"Content-Type" = "application/json"
} -Body $moveBody

Write-Host "Moved $($issueKeys.Count) issues to $sprintName : $($issueKeys -join ', ')"
Start-Sleep -Milliseconds 300
}

8. Jira Automation Rules (Import-Ready JSON)

These rules can be imported directly into Jira Automation (Project Settings → Automation → Import).

Rule: Sprint overflow redistribution

When a sprint is overfull (>80% of capacity), move lowest-priority stories to the next sprint.

{
"name": "GEHR: Sprint overflow redistribution",
"trigger": {
"type": "scheduled",
"cron": "0 0 9 * * MON"
},
"conditions": [
{
"type": "jql",
"jql": "project = GEHR AND sprint in openSprints() AND issuetype = Story"
}
],
"actions": [
{
"type": "comment",
"body": "Sprint capacity check: review if this sprint exceeds team capacity. Move overflow items to next sprint."
}
]
}

Rule: Slice completion notification

{
"name": "GEHR: Slice completion notification",
"trigger": {
"type": "jql_condition_met",
"jql": "project = GEHR AND labels = 'slice-S{N}' AND status != Done",
"condition": "count = 0"
},
"actions": [
{
"type": "send_email",
"to": "asmar.momand@gmail.com",
"subject": "GEHR: Slice S{N} complete!",
"body": "All issues in slice S{N} are resolved. Ready for milestone gate review."
}
]
}

9. Execution Checklist

Run these steps in order to set up Jira for the roadmap:

  1. [ ] Create sprints — Run create-gehr-sprints.ps1 with your board ID
  2. [ ] Verify sprints — Check Jira board shows 36 sprints in correct order
  3. [ ] Bulk-label epics — Run bulk-label-gehr-issues.ps1 to apply slice/module/team/pg labels
  4. [ ] Verify labels — Spot-check 10 issues across different modules
  5. [ ] Move to sprints — Run move-to-sprints.ps1 to distribute labeled issues
  6. [ ] Import automation rules — Import the JSON rules into Jira Automation
  7. [ ] Remaining modules — Push the 12 unpushed modules (allergies, billing, care-plans, claims, clinical-notes, iam, insurance, licensing, orders-cpoe, radiology-pacs, results, registration enhancement) first, then label and assign
  8. [ ] Set sprint capacities — Configure per-team sprint capacity based on team-capacity-model.md
  9. [ ] Review board — Walk through sprint 1 with team leads; adjust assignments
  10. [ ] Start S0-Sprint-1 — Activate the first sprint

10. Handling Special Cases

Stories spanning multiple sprints

  • Split into sub-tasks, each fitting within a single sprint.
  • Parent story stays in the start sprint; sub-tasks distribute across sprints.

Cross-cutting stories (offline, security, AI)

  • Add gate-offline, gate-ai, or gate-security labels.
  • These stories may span teams — assign to the primary team and add watchers for secondary teams.

Enhancement track vs baseline track

  • All track-enhancement stories are assigned to S4 or S5 by default.
  • Exceptions require product owner approval and explicit sprint assignment.

Stories not yet in Jira (12 unpushed modules)

  • Push remaining modules using the existing push-unified-to-jira-rest.mjs script.
  • Then run the bulk-labeling script to assign labels.
  • Then run the sprint-move script.

Desktop-electron stories

  • DESK-EPIC-01, 02 → S0 (Desktop team)
  • DESK-EPIC-03 → S1 (Desktop team)
  • DESK-EPIC-04, 05 → S3 (Desktop team)
  • DESK-EPIC-06, 08, 09, 10 → S4 (Desktop team)
  • DESK-EPIC-11 → S5 (Desktop team)
  • Label each with both mod-desktop-electron and the corresponding slice-S{N}.