Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"name": "edge-scrum",
"source": "./plugins/edge-scrum",
"description": "Agents, skills, and workflows relevant to scrum process management for the OpenShift Edge Team",
"version": "1.0.0"
"version": "1.2.0"
},
{
"name": "git-commits",
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/edge-scrum-release-health
1 change: 1 addition & 0 deletions .claude/skills/edge-scrum-release-planning
2 changes: 1 addition & 1 deletion plugins/edge-scrum/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "edge-scrum",
"description": "Agents, skills, and workflows relevant to scrum process management for the OpenShift Edge Team",
"version": "1.1.0",
"version": "1.2.0",
"author": { "name": "jeff-roche" },
"homepage": "https://github.com/openshift-eng/edge-tooling",
"license": "Apache-2.0"
Expand Down
32 changes: 32 additions & 0 deletions plugins/edge-scrum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,35 @@ Analyzes the health of an OCP release cycle. Traverses the full Jira hierarchy
Output is saved to `.reports/release_health_{version}_{YYYY-MM-DD}.md`.

See [`skills/release-health/README.md`](skills/release-health/README.md) for full usage details.

#### `release-planning`

Assesses whether the team can deliver planned scope within remaining time. Runs a data-quality gate followed by six planning risk checks — capacity, timeline, assignment, bug load, sizing, and composite progress — to surface risks per person and per feature before they become execution problems.

**Usage:**

```shell
/release-planning [version] [sprint-range] [bc:branch-cut-sprint] [pd:pencils-down-sprint] [--component <component>]
```

| Example | Description |
|---------|-------------|
| `/release-planning` | Interactive mode — prompts for all parameters |
| `/release-planning 5.0 287-292 bc:292` | Analyze OCP 5.0, sprints 287–292, branch cut at 292 |
| `/release-planning 5.0 287-292 bc:292 pd:291` | Same, with pencils down at sprint 291 |
| `/release-planning 5.0 287-292 bc:292 --component TNA` | Filtered to TNA features only |

**Pencils down** is when all feature code must be merged. **Branch cut** is when the release branch is created. Feature timeline risk is measured against pencils down. If `pd:` is omitted, it defaults to the branch cut sprint.

**What it produces:**

1. **Data Quality** — validates story-level breakdown before running checks
2. **Capacity** — per-person assigned SP vs remaining capacity
3. **Timeline** — per-feature remaining work vs time left
4. **Assignment** — unassigned work and single points of failure
5. **Bug Load** — unassigned Blocker/Critical bugs
6. **Sizing** — T-shirt size vs actual scope mismatches
7. **Composite Risk** — multi-signal risk assessment per feature (LOW/MEDIUM/HIGH)
8. **Recommendations** — actionable per-person, per-feature, and team-level actions

Output is saved to `.reports/release_planning_{version}_{YYYY-MM-DD}.md` and `.reports/release_planning_{version}_{YYYY-MM-DD}.docx`.
30 changes: 25 additions & 5 deletions plugins/edge-scrum/bin/_jira_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,28 @@ def extract_issue_type(issue):


def extract_sp(issue, issue_type=None):
"""Extract story points. Bugs always return 0."""
"""Extract story points. Bugs always return 0. Returns 0 on unparseable input."""
if issue_type is None:
issue_type = extract_issue_type(issue)
if issue_type == "Bug":
return 0
raw = issue.get("customfield_10028")
if raw is None:
return 0
if isinstance(raw, dict):
val = raw.get("value")
return int(val) if val is not None else 0
return int(float(raw)) # truncates fractional SP intentionally
try:
if isinstance(raw, bool):
return 0
if isinstance(raw, dict):
val = raw.get("value")
if val is None:
return 0
val = float(val)
else:
val = float(raw)
result = max(0, round(val))
return result
except (ValueError, TypeError):
return 0


def extract_epic_key(issue):
Expand Down Expand Up @@ -176,6 +186,16 @@ def format_date(d):
return d.isoformat()


def safe_format_date(d, fallback):
"""Format date, returning fallback on None or parse failure."""
if d is None:
return fallback
try:
return format_date(d)
except (ValueError, TypeError, AttributeError):
return fallback


def days_between(start, end, inclusive=True):
"""Calendar days between two dates."""
s = parse_date(start)
Expand Down
Loading