From 8714186eff9470ca8d7c7fcb53123f64b0255e98 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 10:27:04 +0200 Subject: [PATCH 01/24] =?UTF-8?q?feat(edge-ic):=20add=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20config=20setup=20and=20frontmatter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/MORNING_CONFIG_FORMAT.md | 59 +++++++++++++ plugins/edge-ic/skills/morning/SKILL.md | 85 +++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md create mode 100644 plugins/edge-ic/skills/morning/SKILL.md diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md new file mode 100644 index 00000000..a5ec5760 --- /dev/null +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -0,0 +1,59 @@ +# Morning Config Format + +Configuration for the `edge-ic:morning` skill, stored at `$HOME/.config/edge-ic/morning.yaml`. + +## Structure + +```yaml +daily_notes: + enabled: true + path: "$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md" + format: auto # auto | todo | freeform + +jira: + username: "user@example.com" + watch_statuses: ["ON_QE"] + board_id: "11479" + +github: + username: "ghuser" + +rhel_verification: + enabled: false + project: "RHEL" + summary_filter: "[TNF]" + component: "resource-agents" + +sections: + qa_tasks: true + sprint_backlog: true + carry_over: true + open_prs: true + rhel_queue: false + quarterly_reminders: true +``` + +## Fields + +| Field | Required | Default | Purpose | +|-------|----------|---------|---------| +| `daily_notes.enabled` | yes | `true` | Whether to check daily notes for carry-over | +| `daily_notes.path` | if enabled | `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md` | Path template with date placeholders | +| `daily_notes.format` | no | `auto` | `auto` detects format; `todo` for checkbox-based; `freeform` for keyword-based | +| `jira.username` | yes | inferred from MCP config | JIRA email for assignee queries | +| `jira.watch_statuses` | yes | `["ON_QE"]` | Statuses that surface as "ready for you" | +| `jira.board_id` | yes | auto-discovered | Agile board ID for sprint queries | +| `github.username` | yes | inferred from git/gh | GitHub username for PR matching | +| `rhel_verification.enabled` | yes | `false` | Whether to check RHEL verification queue | +| `rhel_verification.project` | if enabled | `RHEL` | JIRA project key | +| `rhel_verification.summary_filter` | if enabled | `[TNF]` | Summary search string | +| `rhel_verification.component` | if enabled | `resource-agents` | Component filter | +| `sections.*` | no | all `true` | Toggle individual sections on/off | + +## Date Placeholders + +In `daily_notes.path`, these are replaced at runtime: +- `{YYYY}` — 4-digit year +- `{MM}` — 2-digit month (zero-padded) +- `{DD}` — 2-digit day (zero-padded) +- `{YYYY-MM-DD}` — full ISO date diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md new file mode 100644 index 00000000..a117f8e5 --- /dev/null +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -0,0 +1,85 @@ +--- +name: edge-ic:morning +description: Surface your daily task list — QA-ready tickets, sprint backlog, carry-over items, open PRs, RHEL verification queue, and quarterly reminders. Use at the start of your day to see what needs attention. +allowed-tools: + - Read + - Bash + - WebFetch + - AskUserQuestion + - mcp__mcp-atlassian__jira_search + - mcp__mcp-atlassian__jira_get_agile_boards + - mcp__mcp-atlassian__jira_get_sprints_from_board + - mcp__mcp-atlassian__jira_get_sprint_issues + - mcp__mcp-atlassian__jira_get_issue +user-invocable: true +argument-hint: "[--setup]" +--- + +# IC: Morning Briefing + +Aggregate your daily task surface from JIRA, daily notes, PR dashboards, and calendar into a single prioritized morning briefing. + +## Step 0: Parse Arguments + +Check `$ARGUMENTS`: +- `--setup` → jump to Step 1 (force re-run setup even if config exists) +- Empty → proceed to config check + +## Step 1: Config Check and First-Run Setup + +**Check for config file:** + +```bash +cat "$HOME/.config/edge-ic/morning.yaml" 2>/dev/null +``` + +If the file exists and `--setup` was NOT passed, read it and proceed to Step 2. + +If the file does not exist OR `--setup` was passed, run the setup wizard: + +**Read the config format reference** from `$PLUGIN_DIR/references/MORNING_CONFIG_FORMAT.md` for field definitions. + +**Question 1:** Ask the user: +> "Do you use daily TODO or standup note files?" +- Yes → ask follow-up: "Where are they stored?" (default: `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md`) +- No → set `daily_notes.enabled: false` + +**Question 2:** Ask the user: +> "Which JIRA statuses should surface as 'ready for you'? These are tasks that need your attention." +- Present options: `ON_QE`, `Code Review`, `In Progress`, `POST` +- Allow multi-select +- QA engineers typically pick `ON_QE`; developers might pick `Code Review` or `In Progress` + +**Question 3:** Infer GitHub username: + +```bash +gh api user --jq '.login' 2>/dev/null || git config user.name 2>/dev/null +``` + +Ask user to confirm or correct: "Your GitHub username appears to be `{inferred}`. Is that correct?" + +**Question 4:** Infer JIRA email from the MCP config environment. Ask user to confirm: "Your JIRA email appears to be `{inferred}`. Is that correct?" + +**Question 5:** Auto-discover the board ID. Query boards for the user's projects: + +Use `jira_get_agile_boards` to search boards. Present the results and ask the user to pick their primary board. Store the `board_id`. + +**Question 6:** Ask the user: +> "Do you track RHEL bug verification? (e.g., TNF resource-agents tickets)" +- Yes → ask for project key (default: `RHEL`), summary filter (default: `[TNF]`), component (default: `resource-agents`) +- No → set `rhel_verification.enabled: false` + +**Write the config file:** + +```bash +mkdir -p "$HOME/.config/edge-ic" +``` + +Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collected values. Then proceed to Step 2. + +## Usage + +```text +/edge-ic:morning # Run morning briefing (setup on first run) +/edge-ic:morning --setup # Force re-run setup wizard +``` From 6516a9dbdb3c576c85fddb99db5cbf34be6bc22c Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 10:37:19 +0200 Subject: [PATCH 02/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20concrete=20JIRA=20inference,=20fix=20sections=20def?= =?UTF-8?q?ault=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md | 2 +- plugins/edge-ic/skills/morning/SKILL.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md index a5ec5760..ed71025b 100644 --- a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -48,7 +48,7 @@ sections: | `rhel_verification.project` | if enabled | `RHEL` | JIRA project key | | `rhel_verification.summary_filter` | if enabled | `[TNF]` | Summary search string | | `rhel_verification.component` | if enabled | `resource-agents` | Component filter | -| `sections.*` | no | all `true` | Toggle individual sections on/off | +| `sections.*` | no | `true` (except `rhel_queue`: `false` when RHEL verification is disabled) | Toggle individual sections on/off | ## Date Placeholders diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index a117f8e5..4aefcc02 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -58,7 +58,13 @@ gh api user --jq '.login' 2>/dev/null || git config user.name 2>/dev/null Ask user to confirm or correct: "Your GitHub username appears to be `{inferred}`. Is that correct?" -**Question 4:** Infer JIRA email from the MCP config environment. Ask user to confirm: "Your JIRA email appears to be `{inferred}`. Is that correct?" +**Question 4:** Infer JIRA email from the MCP config environment: + +```bash +echo "${JIRA_USERNAME:-}" 2>/dev/null +``` + +Ask user to confirm: "Your JIRA email appears to be `{inferred}`. Is that correct?" **Question 5:** Auto-discover the board ID. Query boards for the user's projects: From 4127a29fc420d69dc9f8c906c7ea410a816c9733 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 10:51:16 +0200 Subject: [PATCH 03/24] =?UTF-8?q?feat(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20add=20data=20gathering=20steps=202-7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/skills/morning/SKILL.md | 173 ++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 4aefcc02..96f35174 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -83,6 +83,179 @@ mkdir -p "$HOME/.config/edge-ic" Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collected values. Then proceed to Step 2. +## Step 2: Gather QA/Watch-Status Tasks + +Skip if `sections.qa_tasks` is `false` in config. + +Query JIRA for tickets in watch statuses assigned to the current user: + +``` +jira_search with JQL: assignee = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC +``` + +Replace `{status1}`, `{status2}` etc. with values from `jira.watch_statuses` in config. + +Use `fields: "status,assignee,issuetype,summary,priority,comment"` and `comment_limit: 2`. + +For each result, scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. + +Store results as a list of: +- `key`: ticket key (e.g., `OCPEDGE-2710`) +- `summary`: ticket summary +- `requester`: comment author who requested QA (or null) +- `link`: `https://redhat.atlassian.net/browse/{key}` + +## Step 3: Gather Sprint Backlog + +Skip if `sections.sprint_backlog` is `false` in config. + +**Discover active sprint:** + +Use `jira_get_sprints_from_board` with `board_id` from config and `state: "active"`. Extract: +- Sprint name +- Sprint start date +- Sprint end date +- Sprint ID + +**Calculate sprint metadata:** +- Days remaining = sprint end date minus today +- Total sprint days = sprint end date minus sprint start date +- Sprint is urgent if days remaining <= 3 + +**Fetch sprint issues:** + +Use `jira_get_sprint_issues` with the sprint ID. Set `limit: 50` and `fields: "status,assignee,issuetype,summary,priority,story_points,customfield_10016"`. + +**Note:** Story points may be in `customfield_10016` (story_points) — check which field contains the numeric value. + +**Filter and compute:** +- Filter to issues where assignee matches `jira.username` from config +- Separate into: completed (status category = Done) and not-done (everything else) +- Sum story points: completed points vs total points +- Group not-done issues by status in workflow order: In Progress, Code Review, POST, To Do, New + +Store results as: +- `sprint_name`: e.g., "Sprint 26" +- `days_remaining`: integer +- `total_days`: integer +- `points_completed`: integer +- `points_total`: integer +- `is_urgent`: boolean (days_remaining <= 3) +- `issues`: list grouped by status, each with key, summary, status, link + +## Step 4: Gather Yesterday's Carry-Over + +Skip if `sections.carry_over` is `false` in config or `daily_notes.enabled` is `false`. + +**Determine yesterday's workday:** + +```bash +if [ "$(date +%u)" -eq 1 ]; then + # Monday — look back to Friday + yesterday=$(date -v-3d +%Y-%m-%d) +else + yesterday=$(date -v-1d +%Y-%m-%d) +fi +echo "$yesterday" +``` + +**Resolve the file path** by replacing placeholders in `daily_notes.path`: +- `{YYYY}` → year from yesterday +- `{MM}` → month from yesterday (zero-padded) +- `{DD}` → day from yesterday (zero-padded) +- `{YYYY-MM-DD}` → full date + +**Read the file.** If it does not exist, skip this section silently (no warning). + +**Parse based on format:** + +If `daily_notes.format` is `auto`, detect: +- File contains `## Priority` or `## In Progress` or `- [ ]` → TODO format +- Otherwise → freeform format + +**TODO format:** Extract all unchecked items (`- [ ]`) from all sections. These are incomplete tasks. + +**Freeform format:** Extract lines that: +- Start with `in-progress` or `in progress` (case-insensitive) +- Do NOT start with `done`, `completed`, `finished`, `verified` (case-insensitive) + +Store results as a list of raw text strings. + +## Step 5: Gather Open PRs + +Skip if `sections.open_prs` is `false` in config. + +**Fetch the latest run ID:** + +```bash +curl -sf "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/latest-build.txt" +``` + +If this fails, skip this section with a note: "Could not fetch PR dashboard — skipping open PRs section." + +**Fetch the PR summary page:** + +Use WebFetch on: +``` +https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/{run_id}/artifacts/pr-notifier/openshift-edge-tooling-gh-notifier/artifacts/edge-tooling-pr-summary.html +``` + +With prompt: "Extract all PRs where the Author column matches '{github_username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels." + +Store results as a list of: +- `repo`: e.g., `openshift/origin` +- `pr_number`: integer +- `title`: string +- `days_open`: string (e.g., "12d") +- `days_idle`: string (e.g., "2d") +- `missing_labels`: string (e.g., "lgtm") +- `link`: full GitHub PR URL + +## Step 6: Check Quarterly Reminders + +Skip if `sections.quarterly_reminders` is `false` in config. + +**Determine quarter end date:** + +```bash +month=$(date +%-m) +year=$(date +%Y) +if [ "$month" -le 3 ]; then + quarter_end="${year}-03-31" +elif [ "$month" -le 6 ]; then + quarter_end="${year}-06-30" +elif [ "$month" -le 9 ]; then + quarter_end="${year}-09-30" +else + quarter_end="${year}-12-31" +fi +days_left=$(( ( $(date -j -f "%Y-%m-%d" "$quarter_end" +%s) - $(date +%s) ) / 86400 )) +echo "$quarter_end $days_left" +``` + +If `days_left <= 14`, store a reminder with: +- `quarter_end_date`: formatted date (e.g., "Jun 30") +- `days_left`: integer +- Two action items: + - "Complete Quarterly Connection in Workday" + - "Submit RewardZone points: https://rewardzone.redhat.com/" + +## Step 7: Check RHEL Verification Queue + +Skip if `sections.rhel_queue` is `false` in config or `rhel_verification.enabled` is `false`. + +Query JIRA: + +``` +jira_search with JQL: project = "{rhel_verification.project}" AND summary ~ "{rhel_verification.summary_filter}" AND component = "{rhel_verification.component}" AND "Preliminary Testing" = Requested AND "Test Coverage" is EMPTY AND (fixVersion in unreleasedVersions() OR fixVersion is EMPTY) +``` + +Use `fields: "summary,status,fixVersions"` and `limit: 20`. + +Store results as: +- `count`: number of tickets found +- `tickets`: list of key + summary + ## Usage ```text From bfdf03ca18232ab3aae535a197baefb5e6804ee2 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 11:05:59 +0200 Subject: [PATCH 04/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20clarify=20parallel=20execution=20and=20config=20ref?= =?UTF-8?q?erences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/skills/morning/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 96f35174..2150cfce 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -83,6 +83,8 @@ mkdir -p "$HOME/.config/edge-ic" Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collected values. Then proceed to Step 2. +Steps 2-7 are independent and can be run in parallel. Skip any step whose corresponding section is disabled in config. + ## Step 2: Gather QA/Watch-Status Tasks Skip if `sections.qa_tasks` is `false` in config. @@ -200,7 +202,7 @@ Use WebFetch on: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/{run_id}/artifacts/pr-notifier/openshift-edge-tooling-gh-notifier/artifacts/edge-tooling-pr-summary.html ``` -With prompt: "Extract all PRs where the Author column matches '{github_username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels." +With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels." Store results as a list of: - `repo`: e.g., `openshift/origin` From 50cfeaf87de659b367a6ed21439ead989449f50a Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 11:09:40 +0200 Subject: [PATCH 05/24] =?UTF-8?q?feat(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20output=20rendering,=20deduplication,=20and=20format?= =?UTF-8?q?=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/MORNING_OUTPUT_FORMAT.md | 64 +++++++++++++++++++ plugins/edge-ic/skills/morning/SKILL.md | 52 +++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md new file mode 100644 index 00000000..82de4d5a --- /dev/null +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -0,0 +1,64 @@ +# Morning Briefing Output Format + +Reference template for the `edge-ic:morning` skill output. + +## Template + +``` +☀ Morning Briefing — {month} {day}, {year} + + {sprint_name} — {days_remaining} of {total_days} days remaining + Story Points: {points_completed} / {points_total} completed ({percentage}%) + {progress_bar} + + {summary_line} + +── QA Ready ────────────────────────────────────────────── + {KEY} {summary} ({requester_note}) + {KEY} {summary} {link} + +── Sprint Backlog (not Done) ───────────────────────────── + {Status}: + {KEY} {summary} {link} + +── Carry-over from Yesterday ───────────────────────────── + - {item_text} + +── Your Open PRs ───────────────────────────────────────── + {repo}#{pr_number} "{title}" open {days_open} · idle {days_idle} · missing: {labels} + +── RHEL Verification Queue ─────────────────────────────── + {count} RHEL tickets awaiting verification + {KEY} {summary} + → Run /two-node:create-rhel-stories to create tracking stories + +── Reminders ───────────────────────────────────────────── + ⚠ {sprint_name} ends in {days} days — prepare tasks for next sprint + ⚠ Quarter ends in {days_left} days ({quarter_end_date}) + → Complete Quarterly Connection in Workday + → Submit RewardZone points: https://rewardzone.redhat.com/ +``` + +## Rules + +- **Empty sections**: omit entirely (no header, no blank lines) +- **All sections empty**: print "Nothing on your plate — enjoy the quiet morning" +- **Summary line**: count items per non-empty section, join with ` · ` (e.g., "5 QA tasks ready · 3 sprint items") +- **Links**: plain URLs, not markdown — terminal-friendly +- **Progress bar**: 20 characters wide, filled proportionally: `████████████░░░░░░░░` + - Filled char: `█` (U+2588) + - Empty char: `░` (U+2591) +- **Requester note**: if a QA request was found in comments, show `(requested by @{author} in comment)`, otherwise show the JIRA link +- **Sprint urgency**: last 3 days → reminder in Reminders section; last day → use 🔴 prefix instead of ⚠ + +## Summary Line Labels + +| Section | Label format | +|---------|-------------| +| QA Ready | `{n} QA task(s) ready` | +| Sprint Backlog | `{n} sprint item(s)` | +| Carry-over | `{n} carry-over(s)` | +| Open PRs | `{n} PR(s)` | +| RHEL Queue | `{n} RHEL ticket(s)` | +| Quarterly | `⚠ quarterly reminder` (no count) | +| Sprint Urgency | `⚠ sprint ending soon` (no count) | diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 2150cfce..ffae52a2 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -258,6 +258,58 @@ Store results as: - `count`: number of tickets found - `tickets`: list of key + summary +## Step 8: Deduplicate + +Before rendering, remove duplicate tickets across sections. A ticket that appears in multiple data sources is shown only in the highest-priority section. + +**Priority order** (highest first): +1. QA Ready (Step 2) +2. Sprint Backlog (Step 3) +3. Carry-over (Step 4) +4. RHEL Queue (Step 7) + +For each ticket key found in a higher-priority section, remove it from all lower-priority sections. Carry-over items are matched by JIRA key if they contain one (e.g., a line like `OCPEDGE-2700: some task` matches key `OCPEDGE-2700`). + +## Step 9: Render Output + +Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`. + +**Render the sprint header** (always shown if sprint data is available): +- Sprint name, days remaining of total days +- Story points completed vs total with percentage +- Progress bar: 20 chars, filled proportionally with `█` and `░` + +**Render the summary line:** +- Count items in each non-empty section +- Join with ` · ` separator +- Append `⚠ quarterly reminder` if quarterly reminder is active +- Append `⚠ sprint ending soon` if sprint is urgent (days_remaining <= 3) + +**Render each section** in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, RHEL Queue, Reminders. +- Skip any section that has zero items +- Use the exact formatting from the output format reference +- All JIRA links: `https://redhat.atlassian.net/browse/{KEY}` + +**Reminders section** collects: +- Sprint urgency reminder (if days_remaining <= 3): "⚠ {sprint_name} ends in {N} days — prepare tasks for next sprint" (or "🔴 {sprint_name} ends today — finalize work and groom next sprint") +- Quarterly reminder (if within 14 days of quarter end) + +**If all sections are empty** (no QA tasks, no sprint items, no carry-over, no PRs, no RHEL tickets, no reminders), output: + +``` +☀ Morning Briefing — {date} + + Nothing on your plate — enjoy the quiet morning +``` + +**Error notes:** If any data source failed during gathering, append a note at the bottom: + +``` +────────────────────────────────────────────────────────── +⚠ Could not reach JIRA — QA tasks, sprint backlog, and RHEL queue skipped +⚠ Could not fetch PR dashboard — open PRs section skipped +``` + ## Usage ```text From 6bd6cb1f70b6ba5470f3d9fc52b42e8bd3f3ef77 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 11:21:42 +0200 Subject: [PATCH 06/24] =?UTF-8?q?feat(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20edge=20cases=20and=20gotchas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/skills/morning/SKILL.md | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index ffae52a2..dfc85f17 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -310,6 +310,34 @@ Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FOR ⚠ Could not fetch PR dashboard — open PRs section skipped ``` +## Edge Cases + +- **No active sprint:** If `jira_get_sprints_from_board` returns no active sprint, skip the sprint header and sprint backlog section entirely. Show a note in the output: "No active sprint found." +- **JIRA MCP unavailable:** If any JIRA MCP call fails, skip all JIRA-dependent sections (QA tasks, sprint backlog, RHEL queue). Append error note at bottom. +- **PR dashboard unavailable:** If `latest-build.txt` fetch or the HTML fetch fails, skip the open PRs section. Append error note. +- **No daily notes file:** Skip carry-over silently — no warning, no empty section. +- **Monday carry-over:** Look back to Friday (3 days) for carry-over, not Saturday/Sunday. +- **Config file exists but is malformed:** If YAML parsing fails, warn user and offer to re-run setup (`/morning --setup`). +- **Story points field varies:** Try `customfield_10016` first, then `story_points`. If neither has data, show "Story Points: N/A" in sprint header. +- **Board ID not set in config:** If `board_id` is missing, attempt auto-discovery via `jira_get_agile_boards`. If that fails, skip sprint section. + +## Gotchas + +- **`currentUser()` in JQL:** Works only when authenticated via MCP. If the MCP config uses a different email than expected, queries return nothing. Verify during setup (Question 4). +- **PR dashboard run ID:** The `latest-build.txt` file contains just the numeric run ID with no trailing newline. Strip whitespace before constructing the URL. +- **RHEL "Preliminary Testing" field:** This is a custom field. The JQL syntax `"Preliminary Testing" = Requested` works on Jira Cloud but the field ID may vary by project. If the query fails, log the error and skip. +- **macOS vs Linux date commands:** The `date -v-3d` syntax is macOS-specific. For portability, use: `date -d "3 days ago"` on Linux. Detect platform first: + +```bash +if date -v-1d +%Y-%m-%d 2>/dev/null; then + # macOS + yesterday=$(date -v-1d +%Y-%m-%d) +else + # Linux + yesterday=$(date -d "yesterday" +%Y-%m-%d) +fi +``` + ## Usage ```text From 9b8cc8d6a190e49978c498505d98a21e143476c8 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 11:36:10 +0200 Subject: [PATCH 07/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20address=20final=20review=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/skills/morning/SKILL.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index dfc85f17..3cebff4d 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -3,6 +3,7 @@ name: edge-ic:morning description: Surface your daily task list — QA-ready tickets, sprint backlog, carry-over items, open PRs, RHEL verification queue, and quarterly reminders. Use at the start of your day to see what needs attention. allowed-tools: - Read + - Write - Bash - WebFetch - AskUserQuestion @@ -17,7 +18,7 @@ argument-hint: "[--setup]" # IC: Morning Briefing -Aggregate your daily task surface from JIRA, daily notes, PR dashboards, and calendar into a single prioritized morning briefing. +Aggregate your daily task surface from JIRA, daily notes, PR dashboards, and date-based reminders into a single prioritized morning briefing. ## Step 0: Parse Arguments @@ -97,13 +98,14 @@ jira_search with JQL: assignee = currentUser() AND status in ("{status1}", "{sta Replace `{status1}`, `{status2}` etc. with values from `jira.watch_statuses` in config. -Use `fields: "status,assignee,issuetype,summary,priority,comment"` and `comment_limit: 2`. +Use `fields: "status,assignee,issuetype,summary,priority"` and `limit: 50`. -For each result, scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. +For each result with watch status, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. Store results as a list of: - `key`: ticket key (e.g., `OCPEDGE-2710`) - `summary`: ticket summary +- `status`: ticket status - `requester`: comment author who requested QA (or null) - `link`: `https://redhat.atlassian.net/browse/{key}` From 2084704deec81983e215ae5cf131ab2bc76f6ea5 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Fri, 19 Jun 2026 14:46:20 +0200 Subject: [PATCH 08/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20use=20QA=20Contact=20field,=20fix=20sprint=20query?= =?UTF-8?q?=20and=20story=20points?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from test-run findings: 1. QA section: use "QA Contact" (customfield_10470) instead of assignee to find tickets where user is the tester, not the developer. Remove the watch_statuses setup question — sprint backlog always shows non-Done tasks, no status filter needed. 2. Sprint backlog: use JQL search (assignee = currentUser() AND sprint = {id}) instead of jira_get_sprint_issues, which silently fails to paginate on large boards (100+ issues). 3. Story points: try customfield_10028 ("Story Points") first, then customfield_10016 ("Story point estimate"). The board uses 10028. Co-Authored-By: Claude Opus 4.6 --- .../references/MORNING_CONFIG_FORMAT.md | 2 +- plugins/edge-ic/skills/morning/SKILL.md | 41 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md index ed71025b..5d1f6390 100644 --- a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -41,7 +41,7 @@ sections: | `daily_notes.path` | if enabled | `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md` | Path template with date placeholders | | `daily_notes.format` | no | `auto` | `auto` detects format; `todo` for checkbox-based; `freeform` for keyword-based | | `jira.username` | yes | inferred from MCP config | JIRA email for assignee queries | -| `jira.watch_statuses` | yes | `["ON_QE"]` | Statuses that surface as "ready for you" | +| `jira.watch_statuses` | no | `["ON_QE"]` | Statuses for QA Contact section (advanced — most users keep default) | | `jira.board_id` | yes | auto-discovered | Agile board ID for sprint queries | | `github.username` | yes | inferred from git/gh | GitHub username for PR matching | | `rhel_verification.enabled` | yes | `false` | Whether to check RHEL verification queue | diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 3cebff4d..41d55d67 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -45,13 +45,7 @@ If the file does not exist OR `--setup` was passed, run the setup wizard: - Yes → ask follow-up: "Where are they stored?" (default: `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md`) - No → set `daily_notes.enabled: false` -**Question 2:** Ask the user: -> "Which JIRA statuses should surface as 'ready for you'? These are tasks that need your attention." -- Present options: `ON_QE`, `Code Review`, `In Progress`, `POST` -- Allow multi-select -- QA engineers typically pick `ON_QE`; developers might pick `Code Review` or `In Progress` - -**Question 3:** Infer GitHub username: +**Question 2:** Infer GitHub username: ```bash gh api user --jq '.login' 2>/dev/null || git config user.name 2>/dev/null @@ -59,7 +53,7 @@ gh api user --jq '.login' 2>/dev/null || git config user.name 2>/dev/null Ask user to confirm or correct: "Your GitHub username appears to be `{inferred}`. Is that correct?" -**Question 4:** Infer JIRA email from the MCP config environment: +**Question 3:** Infer JIRA email from the MCP config environment: ```bash echo "${JIRA_USERNAME:-}" 2>/dev/null @@ -67,11 +61,11 @@ echo "${JIRA_USERNAME:-}" 2>/dev/null Ask user to confirm: "Your JIRA email appears to be `{inferred}`. Is that correct?" -**Question 5:** Auto-discover the board ID. Query boards for the user's projects: +**Question 4:** Auto-discover the board ID. Query boards for the user's projects: Use `jira_get_agile_boards` to search boards. Present the results and ask the user to pick their primary board. Store the `board_id`. -**Question 6:** Ask the user: +**Question 5:** Ask the user: > "Do you track RHEL bug verification? (e.g., TNF resource-agents tickets)" - Yes → ask for project key (default: `RHEL`), summary filter (default: `[TNF]`), component (default: `resource-agents`) - No → set `rhel_verification.enabled: false` @@ -86,21 +80,21 @@ Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collecte Steps 2-7 are independent and can be run in parallel. Skip any step whose corresponding section is disabled in config. -## Step 2: Gather QA/Watch-Status Tasks +## Step 2: Gather QA Tasks Skip if `sections.qa_tasks` is `false` in config. -Query JIRA for tickets in watch statuses assigned to the current user: +Query JIRA for tickets where the current user is the **QA Contact** and status indicates QA is needed: ``` -jira_search with JQL: assignee = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC +jira_search with JQL: "QA Contact" = currentUser() AND status = "ON_QE" ORDER BY priority DESC ``` -Replace `{status1}`, `{status2}` etc. with values from `jira.watch_statuses` in config. - Use `fields: "status,assignee,issuetype,summary,priority"` and `limit: 50`. -For each result with watch status, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. +**Note:** The QA Contact field is `customfield_10470` (user picker). The JQL clause name is `"QA Contact"`. This is separate from the `assignee` field — a ticket's assignee is the developer; the QA Contact is the person responsible for testing. + +For each result, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. Store results as a list of: - `key`: ticket key (e.g., `OCPEDGE-2710`) @@ -126,14 +120,17 @@ Use `jira_get_sprints_from_board` with `board_id` from config and `state: "activ - Total sprint days = sprint end date minus sprint start date - Sprint is urgent if days remaining <= 3 -**Fetch sprint issues:** +**Fetch the user's sprint issues via JQL** (avoids pagination issues with large boards): + +``` +jira_search with JQL: assignee = currentUser() AND sprint = {sprint_id} ORDER BY status ASC, priority DESC +``` -Use `jira_get_sprint_issues` with the sprint ID. Set `limit: 50` and `fields: "status,assignee,issuetype,summary,priority,story_points,customfield_10016"`. +Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,customfield_10016"` and `limit: 50`. -**Note:** Story points may be in `customfield_10016` (story_points) — check which field contains the numeric value. +**Note:** Story points are typically in `customfield_10028` ("Story Points"). Fall back to `customfield_10016` ("Story point estimate") if `customfield_10028` is null. If neither has data, show "Story Points: N/A". -**Filter and compute:** -- Filter to issues where assignee matches `jira.username` from config +**Compute:** - Separate into: completed (status category = Done) and not-done (everything else) - Sum story points: completed points vs total points - Group not-done issues by status in workflow order: In Progress, Code Review, POST, To Do, New @@ -320,7 +317,7 @@ Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FOR - **No daily notes file:** Skip carry-over silently — no warning, no empty section. - **Monday carry-over:** Look back to Friday (3 days) for carry-over, not Saturday/Sunday. - **Config file exists but is malformed:** If YAML parsing fails, warn user and offer to re-run setup (`/morning --setup`). -- **Story points field varies:** Try `customfield_10016` first, then `story_points`. If neither has data, show "Story Points: N/A" in sprint header. +- **Story points field varies:** Try `customfield_10028` ("Story Points") first, then `customfield_10016` ("Story point estimate"). If neither has data, show "Story Points: N/A" in sprint header. - **Board ID not set in config:** If `board_id` is missing, attempt auto-discovery via `jira_get_agile_boards`. If that fails, skip sprint section. ## Gotchas From a5edb3e1925d4a9c5fe1a7765fcf789ef688606b Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 10:49:04 +0200 Subject: [PATCH 09/24] =?UTF-8?q?style(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20block=20pixel=20title,=20=C2=BB=20section=20markers?= =?UTF-8?q?,=20fix=20I=20glyph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Calvin S box-drawing title with block pixel font (▀▄█ half-blocks) - Replace emoji section markers (🟣🔵🔄📋👀🔧⏰) with ASCII » prefix - Simplify border alignment: all borders now uniformly 60 raw chars - Fix I glyph (was identical to T with ▀█▀ serifs, now single █ column) - Update config format docs to match new title style Co-Authored-By: Claude Opus 4.6 --- .../references/MORNING_CONFIG_FORMAT.md | 16 +- .../references/MORNING_OUTPUT_FORMAT.md | 172 ++++++++++++++---- plugins/edge-ic/skills/morning/SKILL.md | 167 ++++++++++++----- 3 files changed, 273 insertions(+), 82 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md index 5d1f6390..738479a2 100644 --- a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -5,6 +5,8 @@ Configuration for the `edge-ic:morning` skill, stored at `$HOME/.config/edge-ic/ ## Structure ```yaml +title: "Morning Edge" # text rendered as block pixel title banner + daily_notes: enabled: true path: "$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md" @@ -12,8 +14,9 @@ daily_notes: jira: username: "user@example.com" - watch_statuses: ["ON_QE"] - board_id: "11479" + qa_statuses: ["ON_QA"] + qa_components: [] # empty = all components; e.g. ["Two Node Fencing", "LVMS"] + board_ids: ["11479"] github: username: "ghuser" @@ -29,6 +32,7 @@ sections: sprint_backlog: true carry_over: true open_prs: true + review_queue: true rhel_queue: false quarterly_reminders: true ``` @@ -37,18 +41,20 @@ sections: | Field | Required | Default | Purpose | |-------|----------|---------|---------| +| `title` | no | `Morning Edge` | Text rendered as block pixel title banner (▀▄█). Multi-word titles stack vertically. | | `daily_notes.enabled` | yes | `true` | Whether to check daily notes for carry-over | | `daily_notes.path` | if enabled | `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md` | Path template with date placeholders | | `daily_notes.format` | no | `auto` | `auto` detects format; `todo` for checkbox-based; `freeform` for keyword-based | | `jira.username` | yes | inferred from MCP config | JIRA email for assignee queries | -| `jira.watch_statuses` | no | `["ON_QE"]` | Statuses for QA Contact section (advanced — most users keep default) | -| `jira.board_id` | yes | auto-discovered | Agile board ID for sprint queries | +| `jira.qa_statuses` | no | `["ON_QA"]` | Statuses that mean "ready for QA" in the QA Contact section | +| `jira.qa_components` | no | `[]` (all) | Filter QA tasks to specific components (e.g., `["Two Node Fencing", "LVMS"]`) | +| `jira.board_ids` | yes | auto-discovered | List of agile board IDs for sprint queries (e.g., `["11479", "12345"]`) | | `github.username` | yes | inferred from git/gh | GitHub username for PR matching | | `rhel_verification.enabled` | yes | `false` | Whether to check RHEL verification queue | | `rhel_verification.project` | if enabled | `RHEL` | JIRA project key | | `rhel_verification.summary_filter` | if enabled | `[TNF]` | Summary search string | | `rhel_verification.component` | if enabled | `resource-agents` | Component filter | -| `sections.*` | no | `true` (except `rhel_queue`: `false` when RHEL verification is disabled) | Toggle individual sections on/off | +| `sections.*` | no | `true` (except `rhel_queue`: `false` when RHEL verification is disabled) | Toggle individual sections on/off. Available: `qa_tasks`, `sprint_backlog`, `carry_over`, `open_prs`, `review_queue`, `rhel_queue`, `quarterly_reminders` | ## Date Placeholders diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md index 82de4d5a..3b3cd1df 100644 --- a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -2,54 +2,150 @@ Reference template for the `edge-ic:morning` skill output. +All panels use rounded box-drawing characters (`╭╮╰╯│─`). The header panel contains the sprint info and summary line. Each data section gets its own panel. Empty sections are omitted entirely — no empty box. + +## Title Banner + +The output begins with the configured title rendered in block pixel characters (▀ ▄ █), centered above the panels. Default title: `Morning Edge` + +``` + █▄ ▄█ █▀▀█ █▀▀▄ █▄ █ █ █▄ █ █▀▀▀ + █ ▀ █ █ █ █▄▄▀ █ ▀█ █ █ ▀█ █ ▀█ + ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ + █▀▀▀ █▀▀▄ █▀▀▀ █▀▀▀ + █▀▀ █ █ █ ▀█ █▀▀ + ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ +``` + +### Title Construction Rules + +- **Font**: block pixel using `▀ ▄ █` — each letter is 3 rows tall, 3-5 columns wide +- **Layout**: two-word titles stack vertically, left-aligned at the same indent; single-word titles use one block +- **Centering**: indent so the longer word is centered relative to the 60-char panel width +- **Long titles (> 45 chars rendered)**: fall back to spaced capital letters (`M O R N I N G E D G E`) +- **Blank line** after the title, before the header panel + +### Block Pixel Character Map + +Each letter is 3 rows tall. Most are 4 columns wide; M and W are 5; I is 1; T is 3. Letters separated by 1 space. + +``` +A:█▀▀█ B:█▀▀▄ C:█▀▀▀ D:█▀▀▄ E:█▀▀▀ F:█▀▀▀ G:█▀▀▀ + █▀▀█ █▀▀▄ █ █ █ █▀▀ █▀▀ █ ▀█ + ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ + +H:█ █ I:█ J: ▀▀█ K:█ ▄▀ L:█ M:█▄ ▄█ N:█▄ █ + █▀▀█ █ █ █▀▄ █ █ ▀ █ █ ▀█ + ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ + +O:█▀▀█ P:█▀▀▄ Q:█▀▀█ R:█▀▀▄ S:█▀▀▀ T:▀█▀ U:█ █ + █ █ █▀▀ █ ▀█ █▄▄▀ ▀▀▀█ █ █ █ + ▀▀▀▀ ▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀▀▀▀ + +V:█ █ W:█ █ X:▀▄ ▄▀ Y:█ █ Z:▀▀▀█ + ▀▄▄▀ █ █ █ ▀█▀ ▀▄▄▀ ▄▀▀ + ▀▀ ▀▀ ▀▀ █ █ ▀▀ ▀▀▀▀ +``` + ## Template ``` -☀ Morning Briefing — {month} {day}, {year} + █▄ ▄█ █▀▀█ █▀▀▄ █▄ █ █ █▄ █ █▀▀▀ + █ ▀ █ █ █ █▄▄▀ █ ▀█ █ █ ▀█ █ ▀█ + ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ + █▀▀▀ █▀▀▄ █▀▀▀ █▀▀▀ + █▀▀ █ █ █ ▀█ █▀▀ + ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ - {sprint_name} — {days_remaining} of {total_days} days remaining - Story Points: {points_completed} / {points_total} completed ({percentage}%) - {progress_bar} +╭──────────────────────────────────────────────────────────╮ +│ ☀ Morning Briefing — {month} {day}, {year} │ +│ │ +│ **{sprint_name}** — {days_remaining} of {total_days} days remaining +│ **Story Points:** {completed} / {total} ({pct}%) │ +│ ▐{progress_bar}▌ {pct}% │ +│ │ +│ > {summary_line} │ +╰──────────────────────────────────────────────────────────╯ - {summary_line} +╭─ » QA Ready ─────────────────────────────────────────────╮ +│ **{KEY}** {summary} ({requester_note}) │ +│ **{KEY}** {summary} {link} │ +╰──────────────────────────────────────────────────────────╯ -── QA Ready ────────────────────────────────────────────── - {KEY} {summary} ({requester_note}) - {KEY} {summary} {link} +╭─ » Sprint Backlog ───────────────────────────────────────╮ +│ {Status}: │ +│ **{KEY}** {summary} [{sp} SP] {link} │ +╰──────────────────────────────────────────────────────────╯ -── Sprint Backlog (not Done) ───────────────────────────── - {Status}: - {KEY} {summary} {link} +╭─ » Carry-over from Yesterday ────────────────────────────╮ +│ - {item_text} │ +╰──────────────────────────────────────────────────────────╯ -── Carry-over from Yesterday ───────────────────────────── - - {item_text} +╭─ » Your Open PRs ────────────────────────────────────────╮ +│ **{repo}#{number}** "{title}" │ +│ open {days} · idle {days} · {n} unresolved · missing: {labels} +╰──────────────────────────────────────────────────────────╯ -── Your Open PRs ───────────────────────────────────────── - {repo}#{pr_number} "{title}" open {days_open} · idle {days_idle} · missing: {labels} +╭─ » PRs Awaiting Your Review ─────────────────────────────╮ +│ **{repo}#{number}** "{title}" open {days} │ +│ {link} │ +╰──────────────────────────────────────────────────────────╯ -── RHEL Verification Queue ─────────────────────────────── - {count} RHEL tickets awaiting verification - {KEY} {summary} - → Run /two-node:create-rhel-stories to create tracking stories +╭─ » RHEL Verification Queue ──────────────────────────────╮ +│ {count} RHEL tickets awaiting verification │ +│ **{KEY}** {summary} │ +│ → Run /two-node:create-rhel-stories to create stories │ +╰──────────────────────────────────────────────────────────╯ -── Reminders ───────────────────────────────────────────── - ⚠ {sprint_name} ends in {days} days — prepare tasks for next sprint - ⚠ Quarter ends in {days_left} days ({quarter_end_date}) - → Complete Quarterly Connection in Workday - → Submit RewardZone points: https://rewardzone.redhat.com/ +╭─ » Reminders ────────────────────────────────────────────╮ +│ ⚠ **{sprint_name}** ends in {days} days — prepare next │ +│ ⚠ Quarter ends in {days_left} days ({quarter_end_date}) │ +│ → Complete Quarterly Connection in Workday │ +│ → Submit RewardZone points: https://rewardzone.redhat.com/ +╰──────────────────────────────────────────────────────────╯ ``` +## Panel Construction + +All borders target **60 visual columns** wide (outer `╭`/`╰` to outer `╮`/`╯`). + +- **All borders**: **60 raw chars** (`╭` + 58×`─` + `╮`). Section markers use `»` (narrow, 1 visual column) so all borders are uniformly 60 raw = 60 visual +- **Content lines**: left `│` + 2-space indent; right `│` aligned at column 60 where content fits; long content (URLs, summaries) can overflow past the right border +- **Corners**: `╭` top-left, `╮` top-right, `╰` bottom-left, `╯` bottom-right +- **Section title**: embedded in top border: `╭─ » {title} ─...─╮` + +## Section Markers + +All sections use `»` as a prefix in the top border. The `⚠` symbol is reserved for warning content inside panels (reminders, error notes) — not used as a section marker. + +## Progress Bar + +10 colored squares wide with bracket borders `▐...▌`, gradient fill from red to green: + +- Positions 1-3: 🟥 (red) +- Positions 4-5: 🟠 (orange) +- Positions 6-7: 🟡 (yellow) +- Positions 8-10: 🟢 (green) +- Unfilled positions use `░` (U+2591) +- Append ` {percentage}%` after the closing bracket + +Examples: +- 0%: `▐░░░░░░░░░░▌ 0%` +- 20%: `▐🟥🟥░░░░░░░░▌ 20%` +- 50%: `▐🟥🟥🟥🟠🟠░░░░░▌ 50%` +- 80%: `▐🟥🟥🟥🟠🟠🟡🟡🟢░░▌ 80%` +- 100%: `▐🟥🟥🟥🟠🟠🟡🟡🟢🟢🟢▌ 100%` + ## Rules -- **Empty sections**: omit entirely (no header, no blank lines) -- **All sections empty**: print "Nothing on your plate — enjoy the quiet morning" -- **Summary line**: count items per non-empty section, join with ` · ` (e.g., "5 QA tasks ready · 3 sprint items") +- **Empty sections**: omit the entire panel (no empty box) +- **All sections empty**: show only the header panel with "Nothing on your plate — enjoy the quiet morning" +- **Summary line**: count items per non-empty section, join with ` · `. Prefix with `>` +- **Ticket keys**: always **bold** (`**KEY**`) +- **Sprint name**: always **bold** in header and reminders - **Links**: plain URLs, not markdown — terminal-friendly -- **Progress bar**: 20 characters wide, filled proportionally: `████████████░░░░░░░░` - - Filled char: `█` (U+2588) - - Empty char: `░` (U+2591) - **Requester note**: if a QA request was found in comments, show `(requested by @{author} in comment)`, otherwise show the JIRA link -- **Sprint urgency**: last 3 days → reminder in Reminders section; last day → use 🔴 prefix instead of ⚠ +- **Sprint urgency**: last 3 days → reminder in Reminders panel; last day → use 🔴 prefix instead of ⚠ ## Summary Line Labels @@ -58,7 +154,19 @@ Reference template for the `edge-ic:morning` skill output. | QA Ready | `{n} QA task(s) ready` | | Sprint Backlog | `{n} sprint item(s)` | | Carry-over | `{n} carry-over(s)` | -| Open PRs | `{n} PR(s)` | +| Open PRs | `{n} open PR(s)` | +| Review Queue | `{n} PR(s) to review` | | RHEL Queue | `{n} RHEL ticket(s)` | | Quarterly | `⚠ quarterly reminder` (no count) | | Sprint Urgency | `⚠ sprint ending soon` (no count) | + +## Error Notes + +Errors go in their own panel at the bottom (uses `⚠` instead of `»`): + +``` +╭─ ⚠ Notes ────────────────────────────────────────────────╮ +│ ⚠ Could not reach JIRA — QA tasks, sprint, RHEL skipped │ +│ ⚠ Could not fetch PR dashboard — open PRs skipped │ +╰──────────────────────────────────────────────────────────╯ +``` diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 41d55d67..b14804b3 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -43,6 +43,7 @@ If the file does not exist OR `--setup` was passed, run the setup wizard: **Question 1:** Ask the user: > "Do you use daily TODO or standup note files?" - Yes → ask follow-up: "Where are they stored?" (default: `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md`) + - **Validate immediately:** resolve the path template for today and yesterday, then check if either file (or the parent directory) exists. If nothing is found, warn the user: "No files found at that path — double-check and try again?" Allow them to correct or confirm. - No → set `daily_notes.enabled: false` **Question 2:** Infer GitHub username: @@ -61,15 +62,34 @@ echo "${JIRA_USERNAME:-}" 2>/dev/null Ask user to confirm: "Your JIRA email appears to be `{inferred}`. Is that correct?" -**Question 4:** Auto-discover the board ID. Query boards for the user's projects: +**Question 4:** Auto-discover boards. Query boards for the user's projects: -Use `jira_get_agile_boards` to search boards. Present the results and ask the user to pick their primary board. Store the `board_id`. +Use `jira_get_agile_boards` to search boards. Present the results and ask the user to pick one or more boards they want to track sprints from. Store the selected IDs in `board_ids` (list). + +Follow-up: "Do you want to add boards from other projects?" If yes, ask for the project key, search its boards, and let them pick. Repeat until they're done. **Question 5:** Ask the user: +> "Do you want to track QA tasks (tickets where you are the QA Contact)?" +- Yes → ask two follow-ups: + - "Which statuses mean a ticket is ready for your QA work? (comma-separated)" — default: `ON_QA`. This is a free-text field since different projects use different workflows (e.g., OCPBUGS uses `ON_QA`, other projects may use `Code Review` or `Review`). + - "Filter by specific components? Enter component names comma-separated, or leave blank for all." (e.g., `Two Node Fencing, LVMS`; default: empty = no filter) +- No → set `sections.qa_tasks: false` + +**Question 6:** Ask the user: +> "Do you want to see PRs assigned to you for review? (useful for cherrypick PRs and code reviews)" +- Yes → set `sections.review_queue: true` +- No → set `sections.review_queue: false` + +**Question 7:** Ask the user: > "Do you track RHEL bug verification? (e.g., TNF resource-agents tickets)" - Yes → ask for project key (default: `RHEL`), summary filter (default: `[TNF]`), component (default: `resource-agents`) - No → set `rhel_verification.enabled: false` +**Question 8:** Ask the user: +> "What title do you want in the banner? (default: `Morning Edge`)" +- Accept a custom title (any text, 1-3 words recommended) +- Default: `Morning Edge` + **Write the config file:** ```bash @@ -78,21 +98,28 @@ mkdir -p "$HOME/.config/edge-ic" Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collected values. Then proceed to Step 2. -Steps 2-7 are independent and can be run in parallel. Skip any step whose corresponding section is disabled in config. +Steps 2-8 are independent and can be run in parallel. Skip any step whose corresponding section is disabled in config. ## Step 2: Gather QA Tasks Skip if `sections.qa_tasks` is `false` in config. -Query JIRA for tickets where the current user is the **QA Contact** and status indicates QA is needed: +Query JIRA for tickets where the current user is the **QA Contact** and status matches the configured watch statuses. This searches across all projects, not just the sprint board: ``` -jira_search with JQL: "QA Contact" = currentUser() AND status = "ON_QE" ORDER BY priority DESC +jira_search with JQL: "QA Contact" = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC ``` -Use `fields: "status,assignee,issuetype,summary,priority"` and `limit: 50`. +Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). + +If `jira.qa_components` is set in config, append a component filter: +``` +AND component in ("{comp1}", "{comp2}") +``` -**Note:** The QA Contact field is `customfield_10470` (user picker). The JQL clause name is `"QA Contact"`. This is separate from the `assignee` field — a ticket's assignee is the developer; the QA Contact is the person responsible for testing. +Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit: 50`. + +**Note:** The QA Contact field is `customfield_10470` (user picker). The JQL clause name is `"QA Contact"`. This is separate from the `assignee` field — a ticket's assignee is the developer; the QA Contact is the person responsible for testing. The `ON_QA` status exists on projects like OCPBUGS and CNV but not on OCPEDGE/USHIFT, so this query searches cross-project. For each result, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. @@ -107,15 +134,17 @@ Store results as a list of: Skip if `sections.sprint_backlog` is `false` in config. +**For each board in `jira.board_ids`**, discover and gather sprint data: + **Discover active sprint:** -Use `jira_get_sprints_from_board` with `board_id` from config and `state: "active"`. Extract: +Use `jira_get_sprints_from_board` with each board ID and `state: "active"`. Extract per sprint: - Sprint name - Sprint start date - Sprint end date - Sprint ID -**Calculate sprint metadata:** +**Calculate sprint metadata** (per sprint): - Days remaining = sprint end date minus today - Total sprint days = sprint end date minus sprint start date - Sprint is urgent if days remaining <= 3 @@ -130,13 +159,14 @@ Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,custo **Note:** Story points are typically in `customfield_10028` ("Story Points"). Fall back to `customfield_10016` ("Story point estimate") if `customfield_10028` is null. If neither has data, show "Story Points: N/A". -**Compute:** +**Compute** (per sprint): - Separate into: completed (status category = Done) and not-done (everything else) - Sum story points: completed points vs total points - Group not-done issues by status in workflow order: In Progress, Code Review, POST, To Do, New -Store results as: +Store results as a list of sprints, each with: - `sprint_name`: e.g., "Sprint 26" +- `board_name`: e.g., "OpenShift Edge Scrum" - `days_remaining`: integer - `total_days`: integer - `points_completed`: integer @@ -144,6 +174,8 @@ Store results as: - `is_urgent`: boolean (days_remaining <= 3) - `issues`: list grouped by status, each with key, summary, status, link +**Rendering:** If multiple boards have active sprints, render a separate sprint header and backlog section for each. Show the board name in the header to distinguish them. + ## Step 4: Gather Yesterday's Carry-Over Skip if `sections.carry_over` is `false` in config or `daily_notes.enabled` is `false`. @@ -171,9 +203,16 @@ echo "$yesterday" **Parse based on format:** If `daily_notes.format` is `auto`, detect: +- File contains `* TODO` or `** TODO` or `SCHEDULED:` → org-mode format - File contains `## Priority` or `## In Progress` or `- [ ]` → TODO format - Otherwise → freeform format +**Org-mode format:** Extract all headings with a `TODO` keyword (not `DONE`): +- Lines matching `^\*+ TODO (.*)` → extract the heading text after `TODO` +- Lines matching `^\*+ IN-PROGRESS (.*)` or `^\*+ NEXT (.*)` → also extract as incomplete +- Ignore headings with `DONE`, `CANCELLED`, or `WAITING` keywords +- If a `TODO` heading has a `SCHEDULED: ` line beneath it, include the date in the carry-over item + **TODO format:** Extract all unchecked items (`- [ ]`) from all sections. These are incomplete tasks. **Freeform format:** Extract lines that: @@ -201,7 +240,15 @@ Use WebFetch on: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/{run_id}/artifacts/pr-notifier/openshift-edge-tooling-gh-notifier/artifacts/edge-tooling-pr-summary.html ``` -With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels." +With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels, and unresolved conversations count if available." + +For each PR found, also fetch unresolved review comments via `gh`: + +```bash +gh pr view {pr_number} --repo {repo} --json reviewDecision,reviews,comments --jq '{reviewDecision, unresolved: [.reviews[].comments[]? | select(.isMinimized == false and .resolvedAt == null)] | length}' +``` + +If `gh` is not available or the command fails, skip unresolved comments for that PR (show "?" instead of a count). Store results as a list of: - `repo`: e.g., `openshift/origin` @@ -210,9 +257,35 @@ Store results as a list of: - `days_open`: string (e.g., "12d") - `days_idle`: string (e.g., "2d") - `missing_labels`: string (e.g., "lgtm") +- `unresolved`: integer or "?" if unavailable +- `link`: full GitHub PR URL + +## Step 6: Gather PRs Awaiting Your Review + +Skip if `sections.review_queue` is `false` in config. + +**Fetch PRs where the user is a requested reviewer:** + +```bash +gh search prs --review-requested=@me --state=open --json repository,number,title,createdAt,url --limit 20 +``` + +If `gh` is not available or the command fails, skip this section with a note: "Could not fetch review queue — skipping." + +**Filter out stale PRs:** Discard any PR where `createdAt` is older than 200 days. This avoids surfacing abandoned review requests from old projects. + +**For each remaining PR**, compute: +- `days_open`: days since `createdAt` +- `repo`: repository full name (e.g., `openshift/origin`) + +Store results as a list of: +- `repo`: e.g., `openshift/origin` +- `pr_number`: integer +- `title`: string +- `days_open`: string (e.g., "5d") - `link`: full GitHub PR URL -## Step 6: Check Quarterly Reminders +## Step 7: Check Quarterly Reminders Skip if `sections.quarterly_reminders` is `false` in config. @@ -241,7 +314,7 @@ If `days_left <= 14`, store a reminder with: - "Complete Quarterly Connection in Workday" - "Submit RewardZone points: https://rewardzone.redhat.com/" -## Step 7: Check RHEL Verification Queue +## Step 8: Check RHEL Verification Queue Skip if `sections.rhel_queue` is `false` in config or `rhel_verification.enabled` is `false`. @@ -257,7 +330,7 @@ Store results as: - `count`: number of tickets found - `tickets`: list of key + summary -## Step 8: Deduplicate +## Step 9: Deduplicate Before rendering, remove duplicate tickets across sections. A ticket that appears in multiple data sources is shown only in the highest-priority section. @@ -265,48 +338,51 @@ Before rendering, remove duplicate tickets across sections. A ticket that appear 1. QA Ready (Step 2) 2. Sprint Backlog (Step 3) 3. Carry-over (Step 4) -4. RHEL Queue (Step 7) +4. RHEL Queue (Step 8) For each ticket key found in a higher-priority section, remove it from all lower-priority sections. Carry-over items are matched by JIRA key if they contain one (e.g., a line like `OCPEDGE-2700: some task` matches key `OCPEDGE-2700`). -## Step 9: Render Output +## Step 10: Render Output Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`. -**Render the sprint header** (always shown if sprint data is available): -- Sprint name, days remaining of total days -- Story points completed vs total with percentage -- Progress bar: 20 chars, filled proportionally with `█` and `░` - -**Render the summary line:** -- Count items in each non-empty section -- Join with ` · ` separator -- Append `⚠ quarterly reminder` if quarterly reminder is active -- Append `⚠ sprint ending soon` if sprint is urgent (days_remaining <= 3) - -**Render each section** in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, RHEL Queue, Reminders. -- Skip any section that has zero items -- Use the exact formatting from the output format reference +Use the **panel layout** from the output format reference (`$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`). All output uses rounded box-drawing characters (`╭╮╰╯│─`). + +**Title banner** (always rendered, before everything else): +- Read the `title` field from config (default: `Morning Edge`) +- Render the title in block pixel style using ▀▄█ half-block characters, following the character map from the output format reference +- Center the text horizontally relative to the 60-char panel width +- Multi-word titles: stack vertically (one word per block row), left-aligned at the same indent +- If the rendered text exceeds 45 chars wide, fall back to spaced capital letters +- One blank line after the title, before the header panel + +**Header panel** (always rendered): +- Title line: `☀ Morning Briefing — {date}` +- Sprint info: **bold** sprint name, days remaining, story points +- Progress bar: 10 colored squares wide with bracket borders `▐...▌`, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`) +- Summary line: count items per non-empty section, join with ` · `, prefix with `>` + +**Section panels** — render in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. +- Each section is its own panel: `╭─ » {title} ─...╮ ... ╰─...╯` (all sections use `»` as prefix) +- Skip any section that has zero items (no empty panels) +- **Ticket keys**: always **bold** (`**KEY**`) +- **Sprint name**: always **bold** in header and reminders - All JIRA links: `https://redhat.atlassian.net/browse/{KEY}` +- Content that overflows the box width (long URLs, summaries) can extend past the right `│` — readability over alignment -**Reminders section** collects: -- Sprint urgency reminder (if days_remaining <= 3): "⚠ {sprint_name} ends in {N} days — prepare tasks for next sprint" (or "🔴 {sprint_name} ends today — finalize work and groom next sprint") +**Reminders panel** collects: +- Sprint urgency reminder (if days_remaining <= 3): "⚠ **{sprint_name}** ends in {N} days — prepare tasks for next sprint" (or "🔴 **{sprint_name}** ends today — finalize work and groom next sprint") - Quarterly reminder (if within 14 days of quarter end) -**If all sections are empty** (no QA tasks, no sprint items, no carry-over, no PRs, no RHEL tickets, no reminders), output: - -``` -☀ Morning Briefing — {date} - - Nothing on your plate — enjoy the quiet morning -``` +**If all sections are empty**, show only the header panel with "Nothing on your plate — enjoy the quiet morning" -**Error notes:** If any data source failed during gathering, append a note at the bottom: +**Error notes:** If any data source failed, append an error panel: ``` -────────────────────────────────────────────────────────── -⚠ Could not reach JIRA — QA tasks, sprint backlog, and RHEL queue skipped -⚠ Could not fetch PR dashboard — open PRs section skipped +╭─ ⚠ Notes ────────────────────────────────────────────────╮ +│ ⚠ Could not reach JIRA — QA tasks, sprint, RHEL skipped │ +│ ⚠ Could not fetch PR dashboard — open PRs skipped │ +╰──────────────────────────────────────────────────────────╯ ``` ## Edge Cases @@ -318,7 +394,8 @@ Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FOR - **Monday carry-over:** Look back to Friday (3 days) for carry-over, not Saturday/Sunday. - **Config file exists but is malformed:** If YAML parsing fails, warn user and offer to re-run setup (`/morning --setup`). - **Story points field varies:** Try `customfield_10028` ("Story Points") first, then `customfield_10016` ("Story point estimate"). If neither has data, show "Story Points: N/A" in sprint header. -- **Board ID not set in config:** If `board_id` is missing, attempt auto-discovery via `jira_get_agile_boards`. If that fails, skip sprint section. +- **Board IDs not set in config:** If `board_ids` is missing or empty, attempt auto-discovery via `jira_get_agile_boards`. If that fails, skip sprint section. +- **Legacy `board_id` field:** If config has `board_id` (string) instead of `board_ids` (list), treat it as a single-element list for backwards compatibility. ## Gotchas From e401d0c31a9d303748fb4ac695e64161145003df Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 11:31:34 +0200 Subject: [PATCH 10/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20address=20CodeRabbit=20review=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MORNING_CONFIG_FORMAT.md: mark rhel_verification.enabled as optional (no) - MORNING_OUTPUT_FORMAT.md: remove double progress-bar wrapper (▐▐…▌▌ → single) - MORNING_OUTPUT_FORMAT.md: clarify overflow rule — summaries wrap, URLs may extend - SKILL.md: add JQL escaping note at both interpolation sites (QA + RHEL steps) - SKILL.md: replace macOS-only date syntax with portable OS-detect at both call sites - SKILL.md: replace REST resolvedAt with GraphQL reviewThreads.isResolved for unresolved PR count - SKILL.md: inline Edge Cases and Gotchas into their respective workflow steps; remove separate sections Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../references/MORNING_CONFIG_FORMAT.md | 2 +- .../references/MORNING_OUTPUT_FORMAT.md | 4 +- plugins/edge-ic/skills/morning/SKILL.md | 93 ++++++++++--------- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md index 738479a2..a4e8b249 100644 --- a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -50,7 +50,7 @@ sections: | `jira.qa_components` | no | `[]` (all) | Filter QA tasks to specific components (e.g., `["Two Node Fencing", "LVMS"]`) | | `jira.board_ids` | yes | auto-discovered | List of agile board IDs for sprint queries (e.g., `["11479", "12345"]`) | | `github.username` | yes | inferred from git/gh | GitHub username for PR matching | -| `rhel_verification.enabled` | yes | `false` | Whether to check RHEL verification queue | +| `rhel_verification.enabled` | no | `false` | Whether to check RHEL verification queue | | `rhel_verification.project` | if enabled | `RHEL` | JIRA project key | | `rhel_verification.summary_filter` | if enabled | `[TNF]` | Summary search string | | `rhel_verification.component` | if enabled | `resource-agents` | Component filter | diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md index 3b3cd1df..53aa0ab5 100644 --- a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -62,7 +62,7 @@ V:█ █ W:█ █ X:▀▄ ▄▀ Y:█ █ Z:▀▀▀█ │ │ │ **{sprint_name}** — {days_remaining} of {total_days} days remaining │ **Story Points:** {completed} / {total} ({pct}%) │ -│ ▐{progress_bar}▌ {pct}% │ +│ {progress_bar} │ │ │ │ > {summary_line} │ ╰──────────────────────────────────────────────────────────╯ @@ -110,7 +110,7 @@ V:█ █ W:█ █ X:▀▄ ▄▀ Y:█ █ Z:▀▀▀█ All borders target **60 visual columns** wide (outer `╭`/`╰` to outer `╮`/`╯`). - **All borders**: **60 raw chars** (`╭` + 58×`─` + `╮`). Section markers use `»` (narrow, 1 visual column) so all borders are uniformly 60 raw = 60 visual -- **Content lines**: left `│` + 2-space indent; right `│` aligned at column 60 where content fits; long content (URLs, summaries) can overflow past the right border +- **Content lines**: left `│` + 2-space indent; right `│` aligned at column 60 where content fits; long summaries wrap to a continuation line; bare URLs may extend past the border for usability - **Corners**: `╭` top-left, `╮` top-right, `╰` bottom-left, `╯` bottom-right - **Section title**: embedded in top border: `╭─ » {title} ─...─╮` diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index b14804b3..4d4649ea 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -34,7 +34,7 @@ Check `$ARGUMENTS`: cat "$HOME/.config/edge-ic/morning.yaml" 2>/dev/null ``` -If the file exists and `--setup` was NOT passed, read it and proceed to Step 2. +If the file exists and `--setup` was NOT passed, read it and proceed to Step 2. If YAML parsing fails, warn the user and offer to re-run setup (`/morning --setup`), then stop. If the file does not exist OR `--setup` was passed, run the setup wizard: @@ -104,13 +104,15 @@ Steps 2-8 are independent and can be run in parallel. Skip any step whose corres Skip if `sections.qa_tasks` is `false` in config. +**If any JIRA MCP call fails in this step**, skip QA tasks and record an error note: "Could not reach JIRA — QA tasks skipped." Note: `currentUser()` in JQL only works when the MCP session is authenticated with the correct email; if queries return empty unexpectedly, verify JIRA auth. + Query JIRA for tickets where the current user is the **QA Contact** and status matches the configured watch statuses. This searches across all projects, not just the sprint board: ``` jira_search with JQL: "QA Contact" = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC ``` -Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). +Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. If `jira.qa_components` is set in config, append a component filter: ``` @@ -134,11 +136,15 @@ Store results as a list of: Skip if `sections.sprint_backlog` is `false` in config. +**If any JIRA MCP call fails in this step**, skip sprint backlog and record an error note: "Could not reach JIRA — sprint backlog skipped." + +**Board IDs:** If `board_ids` is missing or empty in config, attempt auto-discovery via `jira_get_agile_boards`. If that fails too, skip sprint section. If config has a legacy `board_id` (string) instead of `board_ids` (list), treat it as a single-element list. + **For each board in `jira.board_ids`**, discover and gather sprint data: **Discover active sprint:** -Use `jira_get_sprints_from_board` with each board ID and `state: "active"`. Extract per sprint: +Use `jira_get_sprints_from_board` with each board ID and `state: "active"`. If no active sprint is found, skip this board's sprint header and backlog silently. Extract per sprint: - Sprint name - Sprint start date - Sprint end date @@ -161,7 +167,7 @@ Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,custo **Compute** (per sprint): - Separate into: completed (status category = Done) and not-done (everything else) -- Sum story points: completed points vs total points +- Sum story points: try `customfield_10028` ("Story Points") first, then `customfield_10016` ("Story point estimate"). If neither has data, show "Story Points: N/A" in the sprint header - Group not-done issues by status in workflow order: In Progress, Code Review, POST, To Do, New Store results as a list of sprints, each with: @@ -183,11 +189,20 @@ Skip if `sections.carry_over` is `false` in config or `daily_notes.enabled` is ` **Determine yesterday's workday:** ```bash -if [ "$(date +%u)" -eq 1 ]; then - # Monday — look back to Friday - yesterday=$(date -v-3d +%Y-%m-%d) +if date -v-1d +%Y-%m-%d 2>/dev/null; then + # macOS/BSD + if [ "$(date +%u)" -eq 1 ]; then + yesterday=$(date -v-3d +%Y-%m-%d) + else + yesterday=$(date -v-1d +%Y-%m-%d) + fi else - yesterday=$(date -v-1d +%Y-%m-%d) + # Linux/GNU + if [ "$(date +%u)" -eq 1 ]; then + yesterday=$(date -d "3 days ago" +%Y-%m-%d) + else + yesterday=$(date -d "yesterday" +%Y-%m-%d) + fi fi echo "$yesterday" ``` @@ -231,7 +246,7 @@ Skip if `sections.open_prs` is `false` in config. curl -sf "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/latest-build.txt" ``` -If this fails, skip this section with a note: "Could not fetch PR dashboard — skipping open PRs section." +If this fails, skip this section with a note: "Could not fetch PR dashboard — skipping open PRs section." The file contains just the numeric run ID with no trailing newline — strip whitespace before constructing the URL. **Fetch the PR summary page:** @@ -242,10 +257,23 @@ https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/lo With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels, and unresolved conversations count if available." -For each PR found, also fetch unresolved review comments via `gh`: +For each PR found, also fetch unresolved review thread count via GraphQL (the REST API does not expose thread resolution state): ```bash -gh pr view {pr_number} --repo {repo} --json reviewDecision,reviews,comments --jq '{reviewDecision, unresolved: [.reviews[].comments[]? | select(.isMinimized == false and .resolvedAt == null)] | length}' +gh api graphql \ + -F owner="{owner}" -F repo="{repo}" -F pr={pr_number} \ + -f query='query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){ + reviewDecision + reviewThreads(first:100){ nodes{ isResolved } } + } + } + }' \ + --jq '{ + reviewDecision: .data.repository.pullRequest.reviewDecision, + unresolved: [.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length + }' ``` If `gh` is not available or the command fails, skip unresolved comments for that PR (show "?" instead of a count). @@ -303,7 +331,13 @@ elif [ "$month" -le 9 ]; then else quarter_end="${year}-12-31" fi -days_left=$(( ( $(date -j -f "%Y-%m-%d" "$quarter_end" +%s) - $(date +%s) ) / 86400 )) +if date -j -f "%Y-%m-%d" "$quarter_end" +%s 2>/dev/null; then + # macOS/BSD + days_left=$(( ( $(date -j -f "%Y-%m-%d" "$quarter_end" +%s) - $(date +%s) ) / 86400 )) +else + # Linux/GNU + days_left=$(( ( $(date -d "$quarter_end" +%s) - $(date +%s) ) / 86400 )) +fi echo "$quarter_end $days_left" ``` @@ -318,12 +352,16 @@ If `days_left <= 14`, store a reminder with: Skip if `sections.rhel_queue` is `false` in config or `rhel_verification.enabled` is `false`. +**If the JIRA MCP call fails**, skip this section and record an error note: "Could not reach JIRA — RHEL queue skipped." The `"Preliminary Testing" = Requested` clause uses a custom field that works on Jira Cloud; if the query fails with a field-not-found error, log it and skip. + Query JIRA: ``` jira_search with JQL: project = "{rhel_verification.project}" AND summary ~ "{rhel_verification.summary_filter}" AND component = "{rhel_verification.component}" AND "Preliminary Testing" = Requested AND "Test Coverage" is EMPTY AND (fixVersion in unreleasedVersions() OR fixVersion is EMPTY) ``` +Before interpolating `rhel_verification.*` config values, escape backslashes as `\\` and double-quotes as `\"`. + Use `fields: "summary,status,fixVersions"` and `limit: 20`. Store results as: @@ -368,7 +406,7 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc - **Ticket keys**: always **bold** (`**KEY**`) - **Sprint name**: always **bold** in header and reminders - All JIRA links: `https://redhat.atlassian.net/browse/{KEY}` -- Content that overflows the box width (long URLs, summaries) can extend past the right `│` — readability over alignment +- Long summaries wrap to a continuation line; bare URLs may extend past the right `│` for usability **Reminders panel** collects: - Sprint urgency reminder (if days_remaining <= 3): "⚠ **{sprint_name}** ends in {N} days — prepare tasks for next sprint" (or "🔴 **{sprint_name}** ends today — finalize work and groom next sprint") @@ -385,35 +423,6 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc ╰──────────────────────────────────────────────────────────╯ ``` -## Edge Cases - -- **No active sprint:** If `jira_get_sprints_from_board` returns no active sprint, skip the sprint header and sprint backlog section entirely. Show a note in the output: "No active sprint found." -- **JIRA MCP unavailable:** If any JIRA MCP call fails, skip all JIRA-dependent sections (QA tasks, sprint backlog, RHEL queue). Append error note at bottom. -- **PR dashboard unavailable:** If `latest-build.txt` fetch or the HTML fetch fails, skip the open PRs section. Append error note. -- **No daily notes file:** Skip carry-over silently — no warning, no empty section. -- **Monday carry-over:** Look back to Friday (3 days) for carry-over, not Saturday/Sunday. -- **Config file exists but is malformed:** If YAML parsing fails, warn user and offer to re-run setup (`/morning --setup`). -- **Story points field varies:** Try `customfield_10028` ("Story Points") first, then `customfield_10016` ("Story point estimate"). If neither has data, show "Story Points: N/A" in sprint header. -- **Board IDs not set in config:** If `board_ids` is missing or empty, attempt auto-discovery via `jira_get_agile_boards`. If that fails, skip sprint section. -- **Legacy `board_id` field:** If config has `board_id` (string) instead of `board_ids` (list), treat it as a single-element list for backwards compatibility. - -## Gotchas - -- **`currentUser()` in JQL:** Works only when authenticated via MCP. If the MCP config uses a different email than expected, queries return nothing. Verify during setup (Question 4). -- **PR dashboard run ID:** The `latest-build.txt` file contains just the numeric run ID with no trailing newline. Strip whitespace before constructing the URL. -- **RHEL "Preliminary Testing" field:** This is a custom field. The JQL syntax `"Preliminary Testing" = Requested` works on Jira Cloud but the field ID may vary by project. If the query fails, log the error and skip. -- **macOS vs Linux date commands:** The `date -v-3d` syntax is macOS-specific. For portability, use: `date -d "3 days ago"` on Linux. Detect platform first: - -```bash -if date -v-1d +%Y-%m-%d 2>/dev/null; then - # macOS - yesterday=$(date -v-1d +%Y-%m-%d) -else - # Linux - yesterday=$(date -d "yesterday" +%Y-%m-%d) -fi -``` - ## Usage ```text From cb8277177c236a1e07f16bbd6b6319a0d373f2fa Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 11:42:17 +0200 Subject: [PATCH 11/24] =?UTF-8?q?style(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20fix=20block=20pixel=20glyphs=20J,=20L,=20Q,=20X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - J: add bottom hook (▀▄▄▀) - L: trim base to 3-wide (▀▀▀) - Q: add tail (▀▀▀▄) to distinguish from O - X: fix bottom spread (▄▀ ▀▄) to mirror top Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md index 53aa0ab5..c01c6260 100644 --- a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -34,17 +34,17 @@ A:█▀▀█ B:█▀▀▄ C:█▀▀▀ D:█▀▀▄ E:█▀▀▀ █▀▀█ █▀▀▄ █ █ █ █▀▀ █▀▀ █ ▀█ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ -H:█ █ I:█ J: ▀▀█ K:█ ▄▀ L:█ M:█▄ ▄█ N:█▄ █ - █▀▀█ █ █ █▀▄ █ █ ▀ █ █ ▀█ - ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ +H:█ █ I:█ J: ▀█ K:█ ▄▀ L:█ M:█▄ ▄█ N:█▄ █ + █▀▀█ █ █ █▀▄ █ █ ▀ █ █ ▀█ + ▀ ▀ ▀ ▀▄▄▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ O:█▀▀█ P:█▀▀▄ Q:█▀▀█ R:█▀▀▄ S:█▀▀▀ T:▀█▀ U:█ █ - █ █ █▀▀ █ ▀█ █▄▄▀ ▀▀▀█ █ █ █ - ▀▀▀▀ ▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀▀▀▀ + █ █ █▀▀ █ █ █▄▄▀ ▀▀▀█ █ █ █ + ▀▀▀▀ ▀ ▀▀▀▄ ▀ ▀ ▀▀▀▀ ▀ ▀▀▀▀ V:█ █ W:█ █ X:▀▄ ▄▀ Y:█ █ Z:▀▀▀█ ▀▄▄▀ █ █ █ ▀█▀ ▀▄▄▀ ▄▀▀ - ▀▀ ▀▀ ▀▀ █ █ ▀▀ ▀▀▀▀ + ▀▀ ▀▀ ▀▀ ▄▀ ▀▄ ▀▀ ▀▀▀▀ ``` ## Template From b574aa93ee8b61ff489ceac04a0f3e3fe9b4c8c6 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 11:49:24 +0200 Subject: [PATCH 12/24] fix(edge-ic): suppress stdout in date capability probes (>/dev/null 2>&1) --- plugins/edge-ic/skills/morning/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 4d4649ea..1ad9cdf9 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -189,7 +189,7 @@ Skip if `sections.carry_over` is `false` in config or `daily_notes.enabled` is ` **Determine yesterday's workday:** ```bash -if date -v-1d +%Y-%m-%d 2>/dev/null; then +if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then # macOS/BSD if [ "$(date +%u)" -eq 1 ]; then yesterday=$(date -v-3d +%Y-%m-%d) @@ -331,7 +331,7 @@ elif [ "$month" -le 9 ]; then else quarter_end="${year}-12-31" fi -if date -j -f "%Y-%m-%d" "$quarter_end" +%s 2>/dev/null; then +if date -j -f "%Y-%m-%d" "$quarter_end" +%s >/dev/null 2>&1; then # macOS/BSD days_left=$(( ( $(date -j -f "%Y-%m-%d" "$quarter_end" +%s) - $(date +%s) ) / 86400 )) else From c46a85177fa53fd7ad4df0b6a3496c0ee1566bf5 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 13:07:46 +0200 Subject: [PATCH 13/24] fix(edge-ic): fix markdownlint errors (MD031/MD032/MD038/MD040) --- .../references/MORNING_CONFIG_FORMAT.md | 1 + .../references/MORNING_OUTPUT_FORMAT.md | 11 +++--- plugins/edge-ic/skills/morning/SKILL.md | 39 ++++++++++++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md index a4e8b249..242f7c2c 100644 --- a/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_CONFIG_FORMAT.md @@ -59,6 +59,7 @@ sections: ## Date Placeholders In `daily_notes.path`, these are replaced at runtime: + - `{YYYY}` — 4-digit year - `{MM}` — 2-digit month (zero-padded) - `{DD}` — 2-digit day (zero-padded) diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md index c01c6260..cb34456b 100644 --- a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -8,7 +8,7 @@ All panels use rounded box-drawing characters (`╭╮╰╯│─`). The header The output begins with the configured title rendered in block pixel characters (▀ ▄ █), centered above the panels. Default title: `Morning Edge` -``` +```text █▄ ▄█ █▀▀█ █▀▀▄ █▄ █ █ █▄ █ █▀▀▀ █ ▀ █ █ █ █▄▄▀ █ ▀█ █ █ ▀█ █ ▀█ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ @@ -29,7 +29,7 @@ The output begins with the configured title rendered in block pixel characters ( Each letter is 3 rows tall. Most are 4 columns wide; M and W are 5; I is 1; T is 3. Letters separated by 1 space. -``` +```text A:█▀▀█ B:█▀▀▄ C:█▀▀▀ D:█▀▀▄ E:█▀▀▀ F:█▀▀▀ G:█▀▀▀ █▀▀█ █▀▀▄ █ █ █ █▀▀ █▀▀ █ ▀█ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀ ▀▀▀▀ @@ -49,7 +49,7 @@ V:█ █ W:█ █ X:▀▄ ▄▀ Y:█ █ Z:▀▀▀█ ## Template -``` +```text █▄ ▄█ █▀▀█ █▀▀▄ █▄ █ █ █▄ █ █▀▀▀ █ ▀ █ █ █ █▄▄▀ █ ▀█ █ █ ▀█ █ ▀█ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ @@ -127,9 +127,10 @@ All sections use `»` as a prefix in the top border. The `⚠` symbol is reserve - Positions 6-7: 🟡 (yellow) - Positions 8-10: 🟢 (green) - Unfilled positions use `░` (U+2591) -- Append ` {percentage}%` after the closing bracket +- Append `{percentage}%` after the closing bracket Examples: + - 0%: `▐░░░░░░░░░░▌ 0%` - 20%: `▐🟥🟥░░░░░░░░▌ 20%` - 50%: `▐🟥🟥🟥🟠🟠░░░░░▌ 50%` @@ -164,7 +165,7 @@ Examples: Errors go in their own panel at the bottom (uses `⚠` instead of `»`): -``` +```text ╭─ ⚠ Notes ────────────────────────────────────────────────╮ │ ⚠ Could not reach JIRA — QA tasks, sprint, RHEL skipped │ │ ⚠ Could not fetch PR dashboard — open PRs skipped │ diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 1ad9cdf9..fded6646 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -23,6 +23,7 @@ Aggregate your daily task surface from JIRA, daily notes, PR dashboards, and dat ## Step 0: Parse Arguments Check `$ARGUMENTS`: + - `--setup` → jump to Step 1 (force re-run setup even if config exists) - Empty → proceed to config check @@ -42,6 +43,7 @@ If the file does not exist OR `--setup` was passed, run the setup wizard: **Question 1:** Ask the user: > "Do you use daily TODO or standup note files?" + - Yes → ask follow-up: "Where are they stored?" (default: `$HOME/.daily/{YYYY}/{MM}/{YYYY-MM-DD}.md`) - **Validate immediately:** resolve the path template for today and yesterday, then check if either file (or the parent directory) exists. If nothing is found, warn the user: "No files found at that path — double-check and try again?" Allow them to correct or confirm. - No → set `daily_notes.enabled: false` @@ -70,6 +72,7 @@ Follow-up: "Do you want to add boards from other projects?" If yes, ask for the **Question 5:** Ask the user: > "Do you want to track QA tasks (tickets where you are the QA Contact)?" + - Yes → ask two follow-ups: - "Which statuses mean a ticket is ready for your QA work? (comma-separated)" — default: `ON_QA`. This is a free-text field since different projects use different workflows (e.g., OCPBUGS uses `ON_QA`, other projects may use `Code Review` or `Review`). - "Filter by specific components? Enter component names comma-separated, or leave blank for all." (e.g., `Two Node Fencing, LVMS`; default: empty = no filter) @@ -77,16 +80,19 @@ Follow-up: "Do you want to add boards from other projects?" If yes, ask for the **Question 6:** Ask the user: > "Do you want to see PRs assigned to you for review? (useful for cherrypick PRs and code reviews)" + - Yes → set `sections.review_queue: true` - No → set `sections.review_queue: false` **Question 7:** Ask the user: > "Do you track RHEL bug verification? (e.g., TNF resource-agents tickets)" + - Yes → ask for project key (default: `RHEL`), summary filter (default: `[TNF]`), component (default: `resource-agents`) - No → set `rhel_verification.enabled: false` **Question 8:** Ask the user: > "What title do you want in the banner? (default: `Morning Edge`)" + - Accept a custom title (any text, 1-3 words recommended) - Default: `Morning Edge` @@ -108,14 +114,15 @@ Skip if `sections.qa_tasks` is `false` in config. Query JIRA for tickets where the current user is the **QA Contact** and status matches the configured watch statuses. This searches across all projects, not just the sprint board: -``` +```text jira_search with JQL: "QA Contact" = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC ``` Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. If `jira.qa_components` is set in config, append a component filter: -``` + +```text AND component in ("{comp1}", "{comp2}") ``` @@ -126,6 +133,7 @@ Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit For each result, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. Store results as a list of: + - `key`: ticket key (e.g., `OCPEDGE-2710`) - `summary`: ticket summary - `status`: ticket status @@ -145,19 +153,21 @@ Skip if `sections.sprint_backlog` is `false` in config. **Discover active sprint:** Use `jira_get_sprints_from_board` with each board ID and `state: "active"`. If no active sprint is found, skip this board's sprint header and backlog silently. Extract per sprint: + - Sprint name - Sprint start date - Sprint end date - Sprint ID **Calculate sprint metadata** (per sprint): + - Days remaining = sprint end date minus today - Total sprint days = sprint end date minus sprint start date - Sprint is urgent if days remaining <= 3 **Fetch the user's sprint issues via JQL** (avoids pagination issues with large boards): -``` +```text jira_search with JQL: assignee = currentUser() AND sprint = {sprint_id} ORDER BY status ASC, priority DESC ``` @@ -166,11 +176,13 @@ Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,custo **Note:** Story points are typically in `customfield_10028` ("Story Points"). Fall back to `customfield_10016` ("Story point estimate") if `customfield_10028` is null. If neither has data, show "Story Points: N/A". **Compute** (per sprint): + - Separate into: completed (status category = Done) and not-done (everything else) - Sum story points: try `customfield_10028` ("Story Points") first, then `customfield_10016` ("Story point estimate"). If neither has data, show "Story Points: N/A" in the sprint header - Group not-done issues by status in workflow order: In Progress, Code Review, POST, To Do, New Store results as a list of sprints, each with: + - `sprint_name`: e.g., "Sprint 26" - `board_name`: e.g., "OpenShift Edge Scrum" - `days_remaining`: integer @@ -208,6 +220,7 @@ echo "$yesterday" ``` **Resolve the file path** by replacing placeholders in `daily_notes.path`: + - `{YYYY}` → year from yesterday - `{MM}` → month from yesterday (zero-padded) - `{DD}` → day from yesterday (zero-padded) @@ -218,11 +231,13 @@ echo "$yesterday" **Parse based on format:** If `daily_notes.format` is `auto`, detect: + - File contains `* TODO` or `** TODO` or `SCHEDULED:` → org-mode format - File contains `## Priority` or `## In Progress` or `- [ ]` → TODO format - Otherwise → freeform format **Org-mode format:** Extract all headings with a `TODO` keyword (not `DONE`): + - Lines matching `^\*+ TODO (.*)` → extract the heading text after `TODO` - Lines matching `^\*+ IN-PROGRESS (.*)` or `^\*+ NEXT (.*)` → also extract as incomplete - Ignore headings with `DONE`, `CANCELLED`, or `WAITING` keywords @@ -231,6 +246,7 @@ If `daily_notes.format` is `auto`, detect: **TODO format:** Extract all unchecked items (`- [ ]`) from all sections. These are incomplete tasks. **Freeform format:** Extract lines that: + - Start with `in-progress` or `in progress` (case-insensitive) - Do NOT start with `done`, `completed`, `finished`, `verified` (case-insensitive) @@ -251,7 +267,8 @@ If this fails, skip this section with a note: "Could not fetch PR dashboard — **Fetch the PR summary page:** Use WebFetch on: -``` + +```text https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/{run_id}/artifacts/pr-notifier/openshift-edge-tooling-gh-notifier/artifacts/edge-tooling-pr-summary.html ``` @@ -279,6 +296,7 @@ gh api graphql \ If `gh` is not available or the command fails, skip unresolved comments for that PR (show "?" instead of a count). Store results as a list of: + - `repo`: e.g., `openshift/origin` - `pr_number`: integer - `title`: string @@ -303,10 +321,12 @@ If `gh` is not available or the command fails, skip this section with a note: "C **Filter out stale PRs:** Discard any PR where `createdAt` is older than 200 days. This avoids surfacing abandoned review requests from old projects. **For each remaining PR**, compute: + - `days_open`: days since `createdAt` - `repo`: repository full name (e.g., `openshift/origin`) Store results as a list of: + - `repo`: e.g., `openshift/origin` - `pr_number`: integer - `title`: string @@ -342,6 +362,7 @@ echo "$quarter_end $days_left" ``` If `days_left <= 14`, store a reminder with: + - `quarter_end_date`: formatted date (e.g., "Jun 30") - `days_left`: integer - Two action items: @@ -356,7 +377,7 @@ Skip if `sections.rhel_queue` is `false` in config or `rhel_verification.enabled Query JIRA: -``` +```text jira_search with JQL: project = "{rhel_verification.project}" AND summary ~ "{rhel_verification.summary_filter}" AND component = "{rhel_verification.component}" AND "Preliminary Testing" = Requested AND "Test Coverage" is EMPTY AND (fixVersion in unreleasedVersions() OR fixVersion is EMPTY) ``` @@ -365,6 +386,7 @@ Before interpolating `rhel_verification.*` config values, escape backslashes as Use `fields: "summary,status,fixVersions"` and `limit: 20`. Store results as: + - `count`: number of tickets found - `tickets`: list of key + summary @@ -373,6 +395,7 @@ Store results as: Before rendering, remove duplicate tickets across sections. A ticket that appears in multiple data sources is shown only in the highest-priority section. **Priority order** (highest first): + 1. QA Ready (Step 2) 2. Sprint Backlog (Step 3) 3. Carry-over (Step 4) @@ -387,6 +410,7 @@ Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FOR Use the **panel layout** from the output format reference (`$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`). All output uses rounded box-drawing characters (`╭╮╰╯│─`). **Title banner** (always rendered, before everything else): + - Read the `title` field from config (default: `Morning Edge`) - Render the title in block pixel style using ▀▄█ half-block characters, following the character map from the output format reference - Center the text horizontally relative to the 60-char panel width @@ -395,12 +419,14 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc - One blank line after the title, before the header panel **Header panel** (always rendered): + - Title line: `☀ Morning Briefing — {date}` - Sprint info: **bold** sprint name, days remaining, story points - Progress bar: 10 colored squares wide with bracket borders `▐...▌`, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`) - Summary line: count items per non-empty section, join with ` · `, prefix with `>` **Section panels** — render in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. + - Each section is its own panel: `╭─ » {title} ─...╮ ... ╰─...╯` (all sections use `»` as prefix) - Skip any section that has zero items (no empty panels) - **Ticket keys**: always **bold** (`**KEY**`) @@ -409,6 +435,7 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc - Long summaries wrap to a continuation line; bare URLs may extend past the right `│` for usability **Reminders panel** collects: + - Sprint urgency reminder (if days_remaining <= 3): "⚠ **{sprint_name}** ends in {N} days — prepare tasks for next sprint" (or "🔴 **{sprint_name}** ends today — finalize work and groom next sprint") - Quarterly reminder (if within 14 days of quarter end) @@ -416,7 +443,7 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc **Error notes:** If any data source failed, append an error panel: -``` +```text ╭─ ⚠ Notes ────────────────────────────────────────────────╮ │ ⚠ Could not reach JIRA — QA tasks, sprint, RHEL skipped │ │ ⚠ Could not fetch PR dashboard — open PRs skipped │ From b50d7f8bebae77f6e4f47519cdcf126a0cbf1520 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 14:55:18 +0200 Subject: [PATCH 14/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20progress=20bar=20uses=20story=20points,=20open=20PR?= =?UTF-8?q?s=20uses=20gh=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Progress bar now explicitly represents story points completion, not sprint days elapsed - Step 5 supplements CI dashboard with `gh search prs --author=@me` to catch recent PRs not yet indexed by the periodic notifier job; results are merged and deduplicated Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md | 2 +- plugins/edge-ic/skills/morning/SKILL.md | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md index cb34456b..95001d51 100644 --- a/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md +++ b/plugins/edge-ic/references/MORNING_OUTPUT_FORMAT.md @@ -120,7 +120,7 @@ All sections use `»` as a prefix in the top border. The `⚠` symbol is reserve ## Progress Bar -10 colored squares wide with bracket borders `▐...▌`, gradient fill from red to green: +Represents **story points completion** (`points_completed / points_total`). Do NOT use sprint days elapsed. 10 colored squares wide with bracket borders `▐...▌`, gradient fill from red to green: - Positions 1-3: 🟥 (red) - Positions 4-5: 🟠 (orange) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index fded6646..30716382 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -262,7 +262,7 @@ Skip if `sections.open_prs` is `false` in config. curl -sf "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/latest-build.txt" ``` -If this fails, skip this section with a note: "Could not fetch PR dashboard — skipping open PRs section." The file contains just the numeric run ID with no trailing newline — strip whitespace before constructing the URL. +If this fails, skip the dashboard and rely solely on the `gh` fallback below. The file contains just the numeric run ID with no trailing newline — strip whitespace before constructing the URL. **Fetch the PR summary page:** @@ -274,6 +274,14 @@ https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/lo With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels, and unresolved conversations count if available." +**Also fetch open PRs directly via `gh`** (catches recent PRs not yet in the dashboard): + +```bash +gh search prs --author=@me --state=open --json repository,number,title,createdAt,url --limit 50 +``` + +Merge the two sources, deduplicating by `repo + pr_number`. For PRs found only in the `gh` results (not in the dashboard), compute `days_open` from `createdAt`, set `days_idle` to "?", and set `missing_labels` to "?" — the dashboard has richer metadata. PRs found in the dashboard take precedence for those fields. + For each PR found, also fetch unresolved review thread count via GraphQL (the REST API does not expose thread resolution state): ```bash @@ -422,7 +430,7 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc - Title line: `☀ Morning Briefing — {date}` - Sprint info: **bold** sprint name, days remaining, story points -- Progress bar: 10 colored squares wide with bracket borders `▐...▌`, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`) +- Progress bar: represents **story points completion** (`points_completed / points_total`). 10 colored squares wide, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`). If story points are N/A, omit the bar entirely. Do NOT use sprint days elapsed — use story points only. - Summary line: count items per non-empty section, join with ` · `, prefix with `>` **Section panels** — render in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. From 6a700cdc264f671f73c8a318673c9512da48d51e Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 15:17:14 +0200 Subject: [PATCH 15/24] =?UTF-8?q?perf(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20parallel=20data=20gathering,=20drop=20CI=20dashboar?= =?UTF-8?q?d,=20lazy=20comment=20scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Instruct agent to batch all Steps 2-8 tool calls in a single turn for true parallelism - Remove CI dashboard WebFetch (slow, periodic lag); gh search prs is now the sole PR source - Skip per-ticket jira_get_issue comment scan when >3 QA tickets to avoid serial roundtrips Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 28 +++++-------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 30716382..808a9f99 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -104,7 +104,7 @@ mkdir -p "$HOME/.config/edge-ic" Write the YAML config to `$HOME/.config/edge-ic/morning.yaml` using the collected values. Then proceed to Step 2. -Steps 2-8 are independent and can be run in parallel. Skip any step whose corresponding section is disabled in config. +Steps 2-8 are fully independent. **Issue all their tool calls in a single response turn so they execute in parallel** — do not wait for one step to finish before starting the next. In Claude Code this means batching all MCP and Bash calls together. Only render output (Step 10) after all steps complete. Skip any step whose corresponding section is disabled in config. ## Step 2: Gather QA Tasks @@ -130,7 +130,7 @@ Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit **Note:** The QA Contact field is `customfield_10470` (user picker). The JQL clause name is `"QA Contact"`. This is separate from the `assignee` field — a ticket's assignee is the developer; the QA Contact is the person responsible for testing. The `ON_QA` status exists on projects like OCPBUGS and CNV but not on OCPEDGE/USHIFT, so this query searches cross-project. -For each result, fetch its details using `jira_get_issue` with `comment_limit: 2` to scan the last 2 comments for QA request keywords: "ready for QA", "please test", "QA needed", "please verify". If found, note the comment author as the requester. +Skip the per-ticket comment scan by default — set `requester: null` for all results. Only scan comments if there are 3 or fewer QA tickets (to avoid serial roundtrips slowing the briefing). When scanning, fetch all tickets in parallel using `jira_get_issue` with `comment_limit: 2`, checking for keywords: "ready for QA", "please test", "QA needed", "please verify". Store results as a list of: @@ -256,33 +256,15 @@ Store results as a list of raw text strings. Skip if `sections.open_prs` is `false` in config. -**Fetch the latest run ID:** - -```bash -curl -sf "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/latest-build.txt" -``` - -If this fails, skip the dashboard and rely solely on the `gh` fallback below. The file contains just the numeric run ID with no trailing newline — strip whitespace before constructing the URL. - -**Fetch the PR summary page:** - -Use WebFetch on: - -```text -https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-eng-edge-tooling-main-pr-notifier/{run_id}/artifacts/pr-notifier/openshift-edge-tooling-gh-notifier/artifacts/edge-tooling-pr-summary.html -``` - -With prompt: "Extract all PRs where the Author column matches '{config.github.username}'. For each PR return: repo (e.g. openshift/origin), PR number, title, days open, days idle, missing labels, and unresolved conversations count if available." - -**Also fetch open PRs directly via `gh`** (catches recent PRs not yet in the dashboard): +**Fetch open PRs directly via `gh`** (primary source — always up to date): ```bash gh search prs --author=@me --state=open --json repository,number,title,createdAt,url --limit 50 ``` -Merge the two sources, deduplicating by `repo + pr_number`. For PRs found only in the `gh` results (not in the dashboard), compute `days_open` from `createdAt`, set `days_idle` to "?", and set `missing_labels` to "?" — the dashboard has richer metadata. PRs found in the dashboard take precedence for those fields. +Compute `days_open` from `createdAt`. Set `days_idle` and `missing_labels` to "?" (the CI dashboard is no longer fetched — it added latency for marginal benefit). If `gh` is not available, skip this section with a note. -For each PR found, also fetch unresolved review thread count via GraphQL (the REST API does not expose thread resolution state): +For each PR, fetch unresolved review thread count via GraphQL in parallel (the REST API does not expose thread resolution state): ```bash gh api graphql \ From f2b703d0486c4d60003eda6866426d7275250ba1 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Mon, 22 Jun 2026 15:21:06 +0200 Subject: [PATCH 16/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20filter=20open=20PRs=20older=20than=20200=20days,=20?= =?UTF-8?q?require=20precise=20days=5Fopen=20calc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/edge-ic/skills/morning/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 808a9f99..cceb9441 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -262,7 +262,7 @@ Skip if `sections.open_prs` is `false` in config. gh search prs --author=@me --state=open --json repository,number,title,createdAt,url --limit 50 ``` -Compute `days_open` from `createdAt`. Set `days_idle` and `missing_labels` to "?" (the CI dashboard is no longer fetched — it added latency for marginal benefit). If `gh` is not available, skip this section with a note. +Compute `days_open` from `createdAt` (today's date minus `createdAt` in days — be precise, do not eyeball). **Discard any PR where `days_open` > 200** — these are stale/abandoned PRs that add noise. Set `days_idle` and `missing_labels` to "?" (the CI dashboard is no longer fetched). If `gh` is not available, skip this section with a note. For each PR, fetch unresolved review thread count via GraphQL in parallel (the REST API does not expose thread resolution state): From c5d5c7777d4743734b6c905e454bd9db636ec2ea Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 09:40:16 +0200 Subject: [PATCH 17/24] =?UTF-8?q?feat(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20include=20unassigned=20QA=20tasks,=20split=20QA=20p?= =?UTF-8?q?anel=20by=20assignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 12 +- plugins/lvms/.claude-plugin/plugin.json | 2 +- plugins/lvms/README.md | 8 + .../skills/run-integration-tests/SKILL.md | 360 ++++++++++++++++++ 4 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 plugins/lvms/skills/run-integration-tests/SKILL.md diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index cceb9441..968045a5 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -112,10 +112,10 @@ Skip if `sections.qa_tasks` is `false` in config. **If any JIRA MCP call fails in this step**, skip QA tasks and record an error note: "Could not reach JIRA — QA tasks skipped." Note: `currentUser()` in JQL only works when the MCP session is authenticated with the correct email; if queries return empty unexpectedly, verify JIRA auth. -Query JIRA for tickets where the current user is the **QA Contact** and status matches the configured watch statuses. This searches across all projects, not just the sprint board: +Query JIRA for tickets where either the current user is the **QA Contact** or no QA Contact is assigned, and status matches the configured watch statuses. This searches across all projects, not just the sprint board: ```text -jira_search with JQL: "QA Contact" = currentUser() AND status in ("{status1}", "{status2}") ORDER BY priority DESC +jira_search with JQL: ("QA Contact" = currentUser() OR "QA Contact" is EMPTY) AND status in ("{status1}", "{status2}") ORDER BY priority DESC ``` Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. @@ -139,6 +139,7 @@ Store results as a list of: - `status`: ticket status - `requester`: comment author who requested QA (or null) - `link`: `https://redhat.atlassian.net/browse/{key}` +- `qa_assigned`: `true` if QA Contact = currentUser(), `false` if QA Contact is EMPTY ## Step 3: Gather Sprint Backlog @@ -415,6 +416,13 @@ Use the **panel layout** from the output format reference (`$PLUGIN_DIR/referenc - Progress bar: represents **story points completion** (`points_completed / points_total`). 10 colored squares wide, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`). If story points are N/A, omit the bar entirely. Do NOT use sprint days elapsed — use story points only. - Summary line: count items per non-empty section, join with ` · `, prefix with `>` +**QA Ready panel** — split into two sub-groups within the same panel: + +1. **Your QA** (`qa_assigned: true`) — tickets where you are the QA Contact. Label this group with `▸ Your QA`. +2. **Unassigned QA** (`qa_assigned: false`) — tickets with no QA Contact set. Label this group with `▸ Unassigned`. These are candidates to pick up or assign. + +Omit a sub-group header if that group is empty. If both groups are empty, skip the panel entirely. + **Section panels** — render in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. - Each section is its own panel: `╭─ » {title} ─...╮ ... ╰─...╯` (all sections use `»` as prefix) diff --git a/plugins/lvms/.claude-plugin/plugin.json b/plugins/lvms/.claude-plugin/plugin.json index 8350db56..4b7717a8 100644 --- a/plugins/lvms/.claude-plugin/plugin.json +++ b/plugins/lvms/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "lvms", "description": "LVMS (Logical Volume Manager Storage) release, QE, operational workflows, and troubleshooting", - "version": "1.1.0", + "version": "1.2.0", "author": { "name": "sakbas" }, "homepage": "https://github.com/openshift-eng/edge-tooling", "license": "Apache-2.0" diff --git a/plugins/lvms/README.md b/plugins/lvms/README.md index b59151b8..267d96cb 100644 --- a/plugins/lvms/README.md +++ b/plugins/lvms/README.md @@ -14,6 +14,7 @@ LVMS (Logical Volume Manager Storage) release, QE, and operational workflows. | Skill | Description | |---|---| | `/lvms:analyze` | Troubleshoot LVMS storage issues on live clusters or must-gather data | +| `/lvms:run-integration-tests` | Run QE integration tests from lvm-operator repo (deploy from source, run, report) | | `/lvms:check-release-readiness` | Verify branches, dependencies, and configuration for an LVMS release | | `/lvms:z-stream-report` | Generate z-stream release urgency report for all supported versions | | `/lvms:setup-prereq` | Set up prerequisites to test unreleased LVMS operator builds | @@ -53,6 +54,13 @@ LVMS (Logical Volume Manager Storage) release, QE, and operational workflows. /lvms:setup-prereq disconnected ``` +### Run integration tests (RC/EC builds) + +```text +/lvms:run-integration-tests +/lvms:run-integration-tests OCPEDGE-1995 +``` + ## Requirements - `oc` CLI (authenticated with cluster-admin) diff --git a/plugins/lvms/skills/run-integration-tests/SKILL.md b/plugins/lvms/skills/run-integration-tests/SKILL.md new file mode 100644 index 00000000..440b5c68 --- /dev/null +++ b/plugins/lvms/skills/run-integration-tests/SKILL.md @@ -0,0 +1,360 @@ +--- +name: lvms:run-integration-tests +argument-hint: "[JIRA-ID]" +description: Run LVMS QE integration tests on a TNF cluster via SSH — deploys operator from source, runs tests, parses results, posts to JIRA. Use for RC/EC builds where OLM catalog is unavailable. +user-invocable: true +allowed-tools: Bash, Read, AskUserQuestion +--- + +# lvms:run-integration-tests + +## Synopsis + +```bash +/lvms:run-integration-tests +/lvms:run-integration-tests OCPEDGE-1995 +``` + +## Description + +Automates the full LVMS QE integration test pipeline on a TNF cluster. Designed for RC/EC builds +where the `redhat-operators` catalog does not include the `lvms-operator` package. For released +builds, use `/lvms:setup-prereq` instead. + +The skill is **re-entrant**: invoke it once to start the run, then invoke it again when tests +complete to collect results. State is detected automatically from the hypervisor. + +## Prerequisites + +| Requirement | Details | +|-------------|---------| +| TNF cluster | Fresh cluster with extra disks (`VM_EXTRADISKS_LIST="vda vdb vdc"`) | +| SSH access | Hypervisor reachable by SSH with `oc` CLI and cluster-admin kubeconfig | +| Go 1.24+ | Installed on the hypervisor (for building the test binary) | + +## Implementation + +### Step 1: Gather Inputs + +Parse `$ARGUMENTS` for an optional JIRA ticket ID (e.g. `OCPEDGE-1995`). + +Ask for the hypervisor SSH host: + +``` +Hypervisor SSH host? (default: ec2-user@52.29.221.136) +``` + +Set: +``` +SSH_HOST = user input or default +KUBECONFIG = /home/ec2-user/openshift-metal3/dev-scripts/ocp/ostest/auth/kubeconfig +``` + +### Step 2: Detect State + +Check process and log state on the hypervisor to decide which phase to enter: + +```bash +ssh "$SSH_HOST" ' + pgrep -f "integration-test run-suite" >/dev/null 2>&1 && echo RUNNING + ls ~/lvms-mno.log ~/lvms-sno.log 2>/dev/null | head -1 +' 2>/dev/null +``` + +| Result | Phase | +|--------|-------| +| Output contains `RUNNING` | → Phase 2: Tests In Progress | +| Output contains a log path (no RUNNING) | → Phase 3: Results Ready | +| No output | → Phase 1: Fresh Run | + +--- + +## Phase 1: Fresh Run + +### Step 1a: Ask for suite + +``` +Which test suite? +- mno: Multi-Node OpenShift (36 tests) — use for TNF clusters +- sno: Single-Node OpenShift (30 tests) — use for SNO clusters +- both: Run MNO then SNO sequentially (~3-4 hours) +``` + +Default to `mno` for TNF topology. + +### Step 1b: Check existing LVMS deployment + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ + oc get deployment/lvms-operator -n openshift-lvm-storage 2>/dev/null" +``` + +- **Found**: Ask: "LVMS already deployed. Redeploy from source or skip to test run?" + - Redeploy: `ssh "$SSH_HOST" "cd ~/lvm-operator && make undeploy"` then continue + - Skip: jump to Step 1d +- **Not found**: Continue + +### Step 1c: Deploy LVMS from source + +Patch image registry to Managed (some tests need it): + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ + oc patch configs.imageregistry.operator.openshift.io cluster \ + --type merge --patch '{\"spec\":{\"managementState\":\"Managed\",\"storage\":{\"emptyDir\":{}}}}'" +``` + +Clone or update lvm-operator: + +```bash +ssh "$SSH_HOST" ' + if [ -d ~/lvm-operator ]; then + cd ~/lvm-operator && git fetch origin && git checkout main && git pull origin main + else + git clone https://github.com/openshift/lvm-operator.git ~/lvm-operator + fi + echo "Commit: $(cd ~/lvm-operator && git rev-parse --short HEAD)" +' +``` + +Deploy (creates namespace, CRDs, RBAC, operator via kustomize — no image build required): + +```bash +ssh "$SSH_HOST" "cd ~/lvm-operator && make deploy" +``` + +Wait for operator: + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ + oc -n openshift-lvm-storage wait deployment/lvms-operator \ + --for=condition=Available --timeout=120s" +``` + +Apply LVMCluster CR (required for CSI driver registration): + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ + oc apply -n openshift-lvm-storage \ + -f ~/lvm-operator/config/samples/lvm_v1alpha1_lvmcluster.yaml" +``` + +Wait for Ready (poll every 10s, timeout 3m): + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ + for i in \$(seq 1 18); do + STATE=\$(oc -n openshift-lvm-storage get lvmcluster my-lvmcluster \ + -o jsonpath='{.status.state}' 2>/dev/null) + echo \"[\$i] \$STATE\" + [ \"\$STATE\" = \"Ready\" ] && break + sleep 10 + done" +``` + +Verify CSI driver — if `topolvm.io` is not listed, do not proceed (tests will silently skip everything): + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; oc get csidrivers | grep topolvm" +``` + +### Step 1d: Build test binary and start tests + +```bash +ssh "$SSH_HOST" "cd ~/lvm-operator/test/integration && make integration-build" +``` + +Remove stale logs, then start the run with `nohup`: + +```bash +# For mno: +ssh "$SSH_HOST" ' + rm -f ~/lvms-mno.log + cd ~/lvm-operator/test/integration + nohup bash -c "./integration-test run-suite -c 1 \ + openshift/lvm-operator/test/integration/qe_tests/mno > ~/lvms-mno.log 2>&1" & + echo "PID: $!" +' + +# For sno: +ssh "$SSH_HOST" ' + rm -f ~/lvms-sno.log + cd ~/lvm-operator/test/integration + nohup bash -c "./integration-test run-suite -c 1 \ + openshift/lvm-operator/test/integration/qe_tests/sno > ~/lvms-sno.log 2>&1" & + echo "PID: $!" +' + +# For both (sequential): +ssh "$SSH_HOST" ' + rm -f ~/lvms-mno.log ~/lvms-sno.log + cd ~/lvm-operator/test/integration + nohup bash -c " + ./integration-test run-suite -c 1 \ + openshift/lvm-operator/test/integration/qe_tests/mno > ~/lvms-mno.log 2>&1 + ./integration-test run-suite -c 1 \ + openshift/lvm-operator/test/integration/qe_tests/sno > ~/lvms-sno.log 2>&1 + " & + echo "PID: $!" +' +``` + +### Step 1e: Exit with monitoring instructions + +``` +Tests started on (PID above). Suite: — ~1-2 hours per suite. + +The OTE framework buffers output per-test, so the log stays empty while tests run. + +Monitor: + ssh 'ps aux | grep integration-test' + ssh 'tail -f ~/lvms-.log' + +When complete, re-invoke: + /lvms:run-integration-tests +``` + +**Stop here.** Do not proceed to Phase 2 or 3 in the same invocation. + +--- + +## Phase 2: Tests In Progress + +Count completed tests from the partial log: + +```bash +ssh "$SSH_HOST" "python3 -c \" +import json, sys +try: + raw = open('/root/lvms-mno.log').read() + # partial JSON — count result fields + passed = raw.count('\"result\": \"passed\"') + failed = raw.count('\"result\": \"failed\"') + print(f'passed: {passed}, failed: {failed}') +except: print('log not yet written') +\" 2>/dev/null || echo 'log not yet written'" +``` + +Report to the user: +``` +Tests still running on . +Progress: X passed, Y failed so far. + +Monitor: ssh 'tail -f ~/lvms-.log' + +Re-invoke /lvms:run-integration-tests when complete. +``` + +**Stop here.** + +--- + +## Phase 3: Results Ready + +### Step 3a: Determine which logs exist + +```bash +ssh "$SSH_HOST" "ls ~/lvms-mno.log ~/lvms-sno.log 2>/dev/null" +``` + +### Step 3b: Parse each log + +For each log file, use Python to parse the OTE JSON output (the log is a JSON array with a trailing +`Error: N tests failed` line): + +```bash +ssh "$SSH_HOST" "python3 -c \" +import json, re +raw = open('/root/lvms-.log').read() +data = json.loads(raw[:raw.rfind(']')+1]) +passed = [t for t in data if t['result'] == 'passed'] +failed = [t for t in data if t['result'] == 'failed'] +print(f'TOTAL:{len(data)} PASSED:{len(passed)} FAILED:{len(failed)}') +for t in failed: + m = re.search(r'[A-Z]+-\d+', t['name']) + tid = m.group(0) if m else 'unknown' + print(f'FAIL|{tid}|{t[\"name\"][:80]}|{t.get(\"output\",\"\").strip().splitlines()[-1][:120]}') +\"" +``` + +### Step 3c: Get cluster version + +```bash +ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; oc version --short 2>/dev/null || oc version" +``` + +### Step 3d: Generate Markdown report + +Known flakes (flag but do not count as failures): + +| Test ID | Known Issue | +|---------|-------------| +| OCP-86156 | `pvmove` fails with `No data to move` — VG state sensitivity between tests | +| OCP-71012 | ForceWipe — virtio partition naming on libvirt VMs | +| OCP-69772 | RAID test picks `/dev/sr0` (virtual CD-ROM) | + +Report format: + +```markdown +### LVMS Integration Test Results + +**OCP Version:** +**lvm-operator:** main @ +**Suite:** +**Date:** + +#### Summary + +| Suite | Passed | Failed | Total | Pass Rate | +|-------|--------|--------|-------|-----------| +| MNO | X | Y | Z | XX.X% | + +#### Failures + +| Test ID | Test Name | Failure Reason | Known Flake? | +|---------|-----------|----------------|--------------| + +#### Conclusion +**PASS** / **BLOCKED** — +``` + +Use **PASS** if pass rate ≥ 90% and no unexpected failures. Use **BLOCKED** otherwise. + +### Step 3e: Post to JIRA + +If a JIRA ticket was provided (from `$ARGUMENTS`), ask before posting: +``` +Post results to ? (yes/no) +``` + +If yes, post using the `mcp__mcp-atlassian__jira_add_comment` MCP tool. + +### Step 3f: Offer log cleanup + +Ask: "Remove log files from the hypervisor? (yes/no)" + +If yes: +```bash +ssh "$SSH_HOST" "rm -f ~/lvms-mno.log ~/lvms-sno.log" +``` + +## Error Handling + +| Error | Action | +|-------|--------| +| SSH connection refused | Hypervisor down — start it and retry | +| Go not found | Install Go 1.24+ on the hypervisor | +| `make deploy` fails | Check `oc get events -n openshift-lvm-storage` | +| LVMCluster not Ready after 3m | Check `oc describe lvmcluster -n openshift-lvm-storage` | +| `topolvm.io` CSI driver missing | Check vg-manager logs: `oc logs -n openshift-lvm-storage -l app.kubernetes.io/name=vg-manager` | +| `make integration-build` fails | Verify Go version — must be 1.24+ | +| Log empty after process exits | SSH disconnect killed the run — restart with nohup | +| JIRA post fails | Display report for manual copy | + +## Notes + +- **`-c 1` is mandatory** — Serial/Disruptive tests modify the LVMCluster CR; higher concurrency causes interference +- **`nohup` is mandatory** — SSH disconnect kills the run otherwise +- **Run from the hypervisor only** — test binary resolves cluster-internal DNS +- **OTE log buffering** — log stays empty until each test completes; monitor via `ps aux` +- **Free disks required** — check worker nodes with `lsblk`; disks with partitions/filesystems are skipped From 2e0bb366dbc4520264dd21e6ede05a4a91c1354b Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 09:42:54 +0200 Subject: [PATCH 18/24] chore: bump edge-ic to 1.3.0, sync lvms marketplace version to 1.2.0 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude-plugin/marketplace.json | 4 ++-- plugins/edge-ic/.claude-plugin/plugin.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 96790cdb..2af43929 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -16,7 +16,7 @@ "name": "edge-ic", "source": "./plugins/edge-ic", "description": "Individual contributor workflow automation for TODO management, status reporting, and Jira updates", - "version": "1.2.0" + "version": "1.3.0" }, { "name": "edge-ocp-ci", @@ -52,7 +52,7 @@ "name": "lvms", "source": "./plugins/lvms", "description": "LVMS (Logical Volume Manager Storage) release, QE, operational workflows, and troubleshooting", - "version": "1.1.0" + "version": "1.2.0" }, { "name": "lvms-ci", diff --git a/plugins/edge-ic/.claude-plugin/plugin.json b/plugins/edge-ic/.claude-plugin/plugin.json index 0044d302..48f45a9b 100644 --- a/plugins/edge-ic/.claude-plugin/plugin.json +++ b/plugins/edge-ic/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "edge-ic", "description": "Individual contributor workflow automation for TODO management, status reporting, and Jira updates", - "version": "1.2.0", + "version": "1.3.0", "author": { "name": "jaypoulz" }, "homepage": "https://github.com/openshift-eng/edge-tooling", "license": "Apache-2.0", From c31a7bbe3c9179c4257c0d16ec1c51d1b21c8b6e Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 09:51:15 +0200 Subject: [PATCH 19/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20filter=20QA=20tasks=20to=20OCPBUGS/OCPEDGE=20projec?= =?UTF-8?q?ts,=20make=20configurable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 968045a5..f8e9ea0d 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -75,6 +75,7 @@ Follow-up: "Do you want to add boards from other projects?" If yes, ask for the - Yes → ask two follow-ups: - "Which statuses mean a ticket is ready for your QA work? (comma-separated)" — default: `ON_QA`. This is a free-text field since different projects use different workflows (e.g., OCPBUGS uses `ON_QA`, other projects may use `Code Review` or `Review`). + - "Which Jira projects should be searched for QA tasks? (comma-separated project keys)" — default: `OCPBUGS, OCPEDGE`. Store as `jira.qa_projects`. - "Filter by specific components? Enter component names comma-separated, or leave blank for all." (e.g., `Two Node Fencing, LVMS`; default: empty = no filter) - No → set `sections.qa_tasks: false` @@ -115,9 +116,11 @@ Skip if `sections.qa_tasks` is `false` in config. Query JIRA for tickets where either the current user is the **QA Contact** or no QA Contact is assigned, and status matches the configured watch statuses. This searches across all projects, not just the sprint board: ```text -jira_search with JQL: ("QA Contact" = currentUser() OR "QA Contact" is EMPTY) AND status in ("{status1}", "{status2}") ORDER BY priority DESC +jira_search with JQL: ("QA Contact" = currentUser() OR "QA Contact" is EMPTY) AND status in ("{status1}", "{status2}") AND project in ({proj1}, {proj2}, ...) ORDER BY priority DESC ``` +Replace `{proj1}`, `{proj2}` etc. with values from `jira.qa_projects` in config (default: `["OCPBUGS", "OCPEDGE"]`). Project keys do not need quoting in JQL. + Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. If `jira.qa_components` is set in config, append a component filter: From c0af3a1e9f5377a615f206b5702999efa02c3e5a Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 09:54:53 +0200 Subject: [PATCH 20/24] =?UTF-8?q?perf(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20batch=20GraphQL,=20combine=20gh=20searches,=20singl?= =?UTF-8?q?e-query=20sprint=20fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Batch all PR review thread GraphQL calls into one query (N round-trips → 1) - Combine authored + review-requested gh search into one Bash call - Replace per-board sprint discovery with single openSprints() JQL query Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 61 +++++++++++-------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index f8e9ea0d..d0415bdd 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -152,30 +152,22 @@ Skip if `sections.sprint_backlog` is `false` in config. **Board IDs:** If `board_ids` is missing or empty in config, attempt auto-discovery via `jira_get_agile_boards`. If that fails too, skip sprint section. If config has a legacy `board_id` (string) instead of `board_ids` (list), treat it as a single-element list. -**For each board in `jira.board_ids`**, discover and gather sprint data: +**Fetch all active sprint issues in a single JQL query** (eliminates per-board sprint discovery calls): -**Discover active sprint:** - -Use `jira_get_sprints_from_board` with each board ID and `state: "active"`. If no active sprint is found, skip this board's sprint header and backlog silently. Extract per sprint: +```text +jira_search with JQL: assignee = currentUser() AND sprint in openSprints() ORDER BY status ASC, priority DESC +``` -- Sprint name -- Sprint start date -- Sprint end date -- Sprint ID +Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,customfield_10016,sprint"` and `limit: 50`. -**Calculate sprint metadata** (per sprint): +**Extract sprint metadata from the results:** The `sprint` field on each issue contains the sprint name, start date, end date, and board. Group issues by sprint name and derive: +- Sprint name, start date, end date from the sprint field - Days remaining = sprint end date minus today - Total sprint days = sprint end date minus sprint start date - Sprint is urgent if days remaining <= 3 -**Fetch the user's sprint issues via JQL** (avoids pagination issues with large boards): - -```text -jira_search with JQL: assignee = currentUser() AND sprint = {sprint_id} ORDER BY status ASC, priority DESC -``` - -Use `fields: "status,assignee,issuetype,summary,priority,customfield_10028,customfield_10016"` and `limit: 50`. +If the query returns no results, skip the sprint section silently. If the `sprint` field is unavailable or null on any issue, fall back to `jira_get_sprints_from_board` for the configured board IDs. **Note:** Story points are typically in `customfield_10028` ("Story Points"). Fall back to `customfield_10016` ("Story point estimate") if `customfield_10028` is null. If neither has data, show "Story Points: N/A". @@ -268,26 +260,25 @@ gh search prs --author=@me --state=open --json repository,number,title,createdAt Compute `days_open` from `createdAt` (today's date minus `createdAt` in days — be precise, do not eyeball). **Discard any PR where `days_open` > 200** — these are stale/abandoned PRs that add noise. Set `days_idle` and `missing_labels` to "?" (the CI dashboard is no longer fetched). If `gh` is not available, skip this section with a note. -For each PR, fetch unresolved review thread count via GraphQL in parallel (the REST API does not expose thread resolution state): +Fetch unresolved review thread counts for **all PRs in a single GraphQL query** using aliases (one round-trip instead of N): ```bash -gh api graphql \ - -F owner="{owner}" -F repo="{repo}" -F pr={pr_number} \ - -f query='query($owner:String!,$repo:String!,$pr:Int!){ - repository(owner:$owner,name:$repo){ - pullRequest(number:$pr){ - reviewDecision - reviewThreads(first:100){ nodes{ isResolved } } - } - } - }' \ - --jq '{ - reviewDecision: .data.repository.pullRequest.reviewDecision, - unresolved: [.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length - }' +# Build a single batched query with one alias per PR +# Example for 3 PRs: +gh api graphql -f query='query { + pr0: repository(owner:"openshift-eng",name:"edge-tooling") { + pullRequest(number:198) { reviewDecision reviewThreads(first:100){ nodes{ isResolved } } } + } + pr1: repository(owner:"openshift",name:"origin") { + pullRequest(number:42) { reviewDecision reviewThreads(first:100){ nodes{ isResolved } } } + } +}' --jq '{ + pr0: { reviewDecision: .data.pr0.pullRequest.reviewDecision, unresolved: [.data.pr0.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length }, + pr1: { reviewDecision: .data.pr1.pullRequest.reviewDecision, unresolved: [.data.pr1.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length } +}' ``` -If `gh` is not available or the command fails, skip unresolved comments for that PR (show "?" instead of a count). +Dynamically generate the query string and jq filter from the PR list. Map each alias (`pr0`, `pr1`, ...) back to the corresponding PR. If the query fails, set `unresolved: "?"` for all PRs. Store results as a list of: @@ -304,12 +295,14 @@ Store results as a list of: Skip if `sections.review_queue` is `false` in config. -**Fetch PRs where the user is a requested reviewer:** +**Combine with Step 5 when both are enabled:** If both `sections.open_prs` and `sections.review_queue` are enabled, run both `gh search` commands in a single Bash call to save a tool round-trip: ```bash -gh search prs --review-requested=@me --state=open --json repository,number,title,createdAt,url --limit 20 +echo '{"authored":' && gh search prs --author=@me --state=open --json repository,number,title,createdAt,url --limit 50 && echo ',"review":' && gh search prs --review-requested=@me --state=open --json repository,number,title,createdAt,url --limit 20 && echo '}' ``` +Parse the combined output and split into Step 5 and Step 6 data. If only one section is enabled, run its query alone. + If `gh` is not available or the command fails, skip this section with a note: "Could not fetch review queue — skipping." **Filter out stale PRs:** Discard any PR where `createdAt` is older than 200 days. This avoids surfacing abandoned review requests from old projects. From a303c4dbf845dbba86cebb340a24c2a40232417a Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 10:21:54 +0200 Subject: [PATCH 21/24] =?UTF-8?q?perf(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20split=20QA=20queries,=20require=20qa=5Fcomponents?= =?UTF-8?q?=20for=20unassigned,=20inline=20rendering,=20drop=20GraphQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split QA into two queries: your QA (limit 20) and unassigned (limit 10, requires qa_components) - Without component filter, unassigned query returned 50+ tickets (~12k tokens) causing 6+ min render time - Inline all rendering rules in Step 10, pre-render default title — no more reading output format reference - Drop per-PR GraphQL review thread fetch entirely Total execution time reduced from ~9min to ~2.5min. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 122 ++++++++++-------------- 1 file changed, 50 insertions(+), 72 deletions(-) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index d0415bdd..6ee38519 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -113,23 +113,27 @@ Skip if `sections.qa_tasks` is `false` in config. **If any JIRA MCP call fails in this step**, skip QA tasks and record an error note: "Could not reach JIRA — QA tasks skipped." Note: `currentUser()` in JQL only works when the MCP session is authenticated with the correct email; if queries return empty unexpectedly, verify JIRA auth. -Query JIRA for tickets where either the current user is the **QA Contact** or no QA Contact is assigned, and status matches the configured watch statuses. This searches across all projects, not just the sprint board: +Run **two JQL queries** to separate assigned vs. unassigned QA tickets: + +**Query 1 — Your QA** (tickets assigned to you): ```text -jira_search with JQL: ("QA Contact" = currentUser() OR "QA Contact" is EMPTY) AND status in ("{status1}", "{status2}") AND project in ({proj1}, {proj2}, ...) ORDER BY priority DESC +jira_search with JQL: "QA Contact" = currentUser() AND status in ("{status1}", "{status2}") AND project in ({proj1}, {proj2}, ...) ORDER BY priority DESC ``` -Replace `{proj1}`, `{proj2}` etc. with values from `jira.qa_projects` in config (default: `["OCPBUGS", "OCPEDGE"]`). Project keys do not need quoting in JQL. - -Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. +Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit: 20`. -If `jira.qa_components` is set in config, append a component filter: +**Query 2 — Unassigned QA** (only if `jira.qa_components` is set in config — skip otherwise to avoid returning hundreds of irrelevant tickets): ```text -AND component in ("{comp1}", "{comp2}") +jira_search with JQL: "QA Contact" is EMPTY AND status in ("{status1}", "{status2}") AND project in ({proj1}, {proj2}, ...) AND component in ("{comp1}", "{comp2}") ORDER BY priority DESC ``` -Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit: 50`. +Use `fields: "status,assignee,issuetype,summary,priority,components"` and `limit: 10`. + +If `jira.qa_components` is NOT set, skip Query 2 and add a note in the output: "Configure `qa_components` in `~/.config/edge-ic/morning.yaml` to see unassigned QA tickets for your team." + +Replace `{proj1}`, `{proj2}` etc. with values from `jira.qa_projects` in config (default: `["OCPBUGS", "OCPEDGE"]`). Replace `{status1}`, `{status2}` etc. with values from `jira.qa_statuses` in config (default: `["ON_QA"]`). Before interpolating any config value into JQL, escape backslashes as `\\` and double-quotes as `\"` to prevent query breakage. **Note:** The QA Contact field is `customfield_10470` (user picker). The JQL clause name is `"QA Contact"`. This is separate from the `assignee` field — a ticket's assignee is the developer; the QA Contact is the person responsible for testing. The `ON_QA` status exists on projects like OCPBUGS and CNV but not on OCPEDGE/USHIFT, so this query searches cross-project. @@ -260,25 +264,7 @@ gh search prs --author=@me --state=open --json repository,number,title,createdAt Compute `days_open` from `createdAt` (today's date minus `createdAt` in days — be precise, do not eyeball). **Discard any PR where `days_open` > 200** — these are stale/abandoned PRs that add noise. Set `days_idle` and `missing_labels` to "?" (the CI dashboard is no longer fetched). If `gh` is not available, skip this section with a note. -Fetch unresolved review thread counts for **all PRs in a single GraphQL query** using aliases (one round-trip instead of N): - -```bash -# Build a single batched query with one alias per PR -# Example for 3 PRs: -gh api graphql -f query='query { - pr0: repository(owner:"openshift-eng",name:"edge-tooling") { - pullRequest(number:198) { reviewDecision reviewThreads(first:100){ nodes{ isResolved } } } - } - pr1: repository(owner:"openshift",name:"origin") { - pullRequest(number:42) { reviewDecision reviewThreads(first:100){ nodes{ isResolved } } } - } -}' --jq '{ - pr0: { reviewDecision: .data.pr0.pullRequest.reviewDecision, unresolved: [.data.pr0.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length }, - pr1: { reviewDecision: .data.pr1.pullRequest.reviewDecision, unresolved: [.data.pr1.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length } -}' -``` - -Dynamically generate the query string and jq filter from the PR list. Map each alias (`pr0`, `pr1`, ...) back to the corresponding PR. If the query fails, set `unresolved: "?"` for all PRs. +**Skip the per-PR review thread fetch entirely** — it adds N sequential round-trips and the briefing doesn't need it. Set `unresolved: null` for all PRs. The PR link is enough to check threads on demand. Store results as a list of: @@ -286,23 +272,18 @@ Store results as a list of: - `pr_number`: integer - `title`: string - `days_open`: string (e.g., "12d") -- `days_idle`: string (e.g., "2d") -- `missing_labels`: string (e.g., "lgtm") -- `unresolved`: integer or "?" if unavailable - `link`: full GitHub PR URL ## Step 6: Gather PRs Awaiting Your Review Skip if `sections.review_queue` is `false` in config. -**Combine with Step 5 when both are enabled:** If both `sections.open_prs` and `sections.review_queue` are enabled, run both `gh search` commands in a single Bash call to save a tool round-trip: +**Fetch PRs where the user is a requested reviewer:** ```bash -echo '{"authored":' && gh search prs --author=@me --state=open --json repository,number,title,createdAt,url --limit 50 && echo ',"review":' && gh search prs --review-requested=@me --state=open --json repository,number,title,createdAt,url --limit 20 && echo '}' +gh search prs --review-requested=@me --state=open --json repository,number,title,createdAt,url --limit 20 ``` -Parse the combined output and split into Step 5 and Step 6 data. If only one section is enabled, run its query alone. - If `gh` is not available or the command fails, skip this section with a note: "Could not fetch review queue — skipping." **Filter out stale PRs:** Discard any PR where `createdAt` is older than 200 days. This avoids surfacing abandoned review requests from old projects. @@ -392,57 +373,54 @@ For each ticket key found in a higher-priority section, remove it from all lower ## Step 10: Render Output -Read the output format reference from `$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`. - -Use the **panel layout** from the output format reference (`$PLUGIN_DIR/references/MORNING_OUTPUT_FORMAT.md`). All output uses rounded box-drawing characters (`╭╮╰╯│─`). +**Do NOT read the output format reference file.** All rendering rules are inline below for speed. -**Title banner** (always rendered, before everything else): +**Title banner** — copy-paste the pre-rendered default title verbatim (do not compute it): -- Read the `title` field from config (default: `Morning Edge`) -- Render the title in block pixel style using ▀▄█ half-block characters, following the character map from the output format reference -- Center the text horizontally relative to the 60-char panel width -- Multi-word titles: stack vertically (one word per block row), left-aligned at the same indent -- If the rendered text exceeds 45 chars wide, fall back to spaced capital letters -- One blank line after the title, before the header panel - -**Header panel** (always rendered): +```text + █▄ ▄█ █▀▀█ █▀▀▄ █▄ █ █ █▄ █ █▀▀▀ + █ ▀ █ █ █ █▄▄▀ █ ▀█ █ █ ▀█ █ ▀█ + ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ + █▀▀▀ █▀▀▄ █▀▀▀ █▀▀▀ + █▀▀ █ █ █ ▀█ █▀▀ + ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ +``` -- Title line: `☀ Morning Briefing — {date}` -- Sprint info: **bold** sprint name, days remaining, story points -- Progress bar: represents **story points completion** (`points_completed / points_total`). 10 colored squares wide, gradient fill (positions 1-3 🟥, 4-5 🟠, 6-7 🟡, 8-10 🟢, unfilled `░`). If story points are N/A, omit the bar entirely. Do NOT use sprint days elapsed — use story points only. -- Summary line: count items per non-empty section, join with ` · `, prefix with `>` +If the user configured a custom title, fall back to spaced capital letters (e.g., `D A I L Y`). Do not attempt pixel rendering for custom titles. -**QA Ready panel** — split into two sub-groups within the same panel: +**All panels** use `╭╮╰╯│─` box-drawing, 60 chars wide. Section header: `╭─ » {title} ─...─╮`. Skip empty sections. -1. **Your QA** (`qa_assigned: true`) — tickets where you are the QA Contact. Label this group with `▸ Your QA`. -2. **Unassigned QA** (`qa_assigned: false`) — tickets with no QA Contact set. Label this group with `▸ Unassigned`. These are candidates to pick up or assign. +**Header panel:** -Omit a sub-group header if that group is empty. If both groups are empty, skip the panel entirely. +```text +╭──────────────────────────────────────────────────────────╮ +│ ☀ Morning Briefing — {date} │ +│ **{sprint}** — {days_left}/{total} days · {pts_done}/{pts_total} SP +│ ▐{bar}▌ {pct}% │ +│ > {summary} │ +╰──────────────────────────────────────────────────────────╯ +``` -**Section panels** — render in order: QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. +Progress bar: 10 slots, filled by story points ratio. Positions 1-3: 🟥, 4-5: 🟠, 6-7: 🟡, 8-10: 🟢, unfilled: `░`. Omit bar if story points N/A. -- Each section is its own panel: `╭─ » {title} ─...╮ ... ╰─...╯` (all sections use `»` as prefix) -- Skip any section that has zero items (no empty panels) -- **Ticket keys**: always **bold** (`**KEY**`) -- **Sprint name**: always **bold** in header and reminders -- All JIRA links: `https://redhat.atlassian.net/browse/{KEY}` -- Long summaries wrap to a continuation line; bare URLs may extend past the right `│` for usability +**Section order:** QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. -**Reminders panel** collects: +**QA Ready panel** — split into two sub-groups: +- `▸ Your QA` (`qa_assigned: true`) — tickets where you are the QA Contact +- `▸ Unassigned` (`qa_assigned: false`) — no QA Contact set +Omit a sub-group if empty. Skip panel if both empty. -- Sprint urgency reminder (if days_remaining <= 3): "⚠ **{sprint_name}** ends in {N} days — prepare tasks for next sprint" (or "🔴 **{sprint_name}** ends today — finalize work and groom next sprint") -- Quarterly reminder (if within 14 days of quarter end) +**Formatting rules:** +- Ticket keys: **bold** (`**KEY**`) +- JIRA links: `https://redhat.atlassian.net/browse/{KEY}` +- Sprint name: always **bold** +- URLs may extend past the right `│` -**If all sections are empty**, show only the header panel with "Nothing on your plate — enjoy the quiet morning" +**Reminders:** Sprint urgency if days_remaining <= 3 (⚠ or 🔴 if last day). Quarterly reminder if within 14 days. -**Error notes:** If any data source failed, append an error panel: +**All empty:** "Nothing on your plate — enjoy the quiet morning" -```text -╭─ ⚠ Notes ────────────────────────────────────────────────╮ -│ ⚠ Could not reach JIRA — QA tasks, sprint, RHEL skipped │ -│ ⚠ Could not fetch PR dashboard — open PRs skipped │ -╰──────────────────────────────────────────────────────────╯ -``` +**Errors:** Append `╭─ ⚠ Notes ─...╮` panel with `⚠` per failed source. ## Usage From 6c1ab7179d2e1f8f51e05ca0652bad0df5fbaa92 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 10:34:15 +0200 Subject: [PATCH 22/24] =?UTF-8?q?fix(edge-ic):=20morning=20skill=20?= =?UTF-8?q?=E2=80=94=20fix=20markdownlint=20MD032=20blank=20lines=20around?= =?UTF-8?q?=20lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugins/edge-ic/skills/morning/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/edge-ic/skills/morning/SKILL.md b/plugins/edge-ic/skills/morning/SKILL.md index 6ee38519..e2aa1e31 100644 --- a/plugins/edge-ic/skills/morning/SKILL.md +++ b/plugins/edge-ic/skills/morning/SKILL.md @@ -406,11 +406,14 @@ Progress bar: 10 slots, filled by story points ratio. Positions 1-3: 🟥, 4-5: **Section order:** QA Ready, Sprint Backlog, Carry-over, Open PRs, Review Queue, RHEL Queue, Reminders. **QA Ready panel** — split into two sub-groups: + - `▸ Your QA` (`qa_assigned: true`) — tickets where you are the QA Contact - `▸ Unassigned` (`qa_assigned: false`) — no QA Contact set + Omit a sub-group if empty. Skip panel if both empty. **Formatting rules:** + - Ticket keys: **bold** (`**KEY**`) - JIRA links: `https://redhat.atlassian.net/browse/{KEY}` - Sprint name: always **bold** From a95ae9e6ac535bbb484465f41ac667a8afd477fe Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 10:40:28 +0200 Subject: [PATCH 23/24] fix(edge-ic): remove unrelated LVMS changes from morning skill PR LVMS plugin.json bump, README update, and run-integration-tests skill were accidentally included. Revert them to match origin/main. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude-plugin/marketplace.json | 2 +- plugins/lvms/.claude-plugin/plugin.json | 2 +- plugins/lvms/README.md | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2af43929..9e3c3908 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -52,7 +52,7 @@ "name": "lvms", "source": "./plugins/lvms", "description": "LVMS (Logical Volume Manager Storage) release, QE, operational workflows, and troubleshooting", - "version": "1.2.0" + "version": "1.1.0" }, { "name": "lvms-ci", diff --git a/plugins/lvms/.claude-plugin/plugin.json b/plugins/lvms/.claude-plugin/plugin.json index 4b7717a8..8350db56 100644 --- a/plugins/lvms/.claude-plugin/plugin.json +++ b/plugins/lvms/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "lvms", "description": "LVMS (Logical Volume Manager Storage) release, QE, operational workflows, and troubleshooting", - "version": "1.2.0", + "version": "1.1.0", "author": { "name": "sakbas" }, "homepage": "https://github.com/openshift-eng/edge-tooling", "license": "Apache-2.0" diff --git a/plugins/lvms/README.md b/plugins/lvms/README.md index 267d96cb..b59151b8 100644 --- a/plugins/lvms/README.md +++ b/plugins/lvms/README.md @@ -14,7 +14,6 @@ LVMS (Logical Volume Manager Storage) release, QE, and operational workflows. | Skill | Description | |---|---| | `/lvms:analyze` | Troubleshoot LVMS storage issues on live clusters or must-gather data | -| `/lvms:run-integration-tests` | Run QE integration tests from lvm-operator repo (deploy from source, run, report) | | `/lvms:check-release-readiness` | Verify branches, dependencies, and configuration for an LVMS release | | `/lvms:z-stream-report` | Generate z-stream release urgency report for all supported versions | | `/lvms:setup-prereq` | Set up prerequisites to test unreleased LVMS operator builds | @@ -54,13 +53,6 @@ LVMS (Logical Volume Manager Storage) release, QE, and operational workflows. /lvms:setup-prereq disconnected ``` -### Run integration tests (RC/EC builds) - -```text -/lvms:run-integration-tests -/lvms:run-integration-tests OCPEDGE-1995 -``` - ## Requirements - `oc` CLI (authenticated with cluster-admin) From fb31cc630c5d2269a2b0e51f47a23dbc1aa5f697 Mon Sep 17 00:00:00 2001 From: Luca Consalvi Date: Thu, 25 Jun 2026 10:41:02 +0200 Subject: [PATCH 24/24] fix: remove LVMS run-integration-tests skill from morning PR Accidentally included in earlier commit, belongs in a separate PR. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../skills/run-integration-tests/SKILL.md | 360 ------------------ 1 file changed, 360 deletions(-) delete mode 100644 plugins/lvms/skills/run-integration-tests/SKILL.md diff --git a/plugins/lvms/skills/run-integration-tests/SKILL.md b/plugins/lvms/skills/run-integration-tests/SKILL.md deleted file mode 100644 index 440b5c68..00000000 --- a/plugins/lvms/skills/run-integration-tests/SKILL.md +++ /dev/null @@ -1,360 +0,0 @@ ---- -name: lvms:run-integration-tests -argument-hint: "[JIRA-ID]" -description: Run LVMS QE integration tests on a TNF cluster via SSH — deploys operator from source, runs tests, parses results, posts to JIRA. Use for RC/EC builds where OLM catalog is unavailable. -user-invocable: true -allowed-tools: Bash, Read, AskUserQuestion ---- - -# lvms:run-integration-tests - -## Synopsis - -```bash -/lvms:run-integration-tests -/lvms:run-integration-tests OCPEDGE-1995 -``` - -## Description - -Automates the full LVMS QE integration test pipeline on a TNF cluster. Designed for RC/EC builds -where the `redhat-operators` catalog does not include the `lvms-operator` package. For released -builds, use `/lvms:setup-prereq` instead. - -The skill is **re-entrant**: invoke it once to start the run, then invoke it again when tests -complete to collect results. State is detected automatically from the hypervisor. - -## Prerequisites - -| Requirement | Details | -|-------------|---------| -| TNF cluster | Fresh cluster with extra disks (`VM_EXTRADISKS_LIST="vda vdb vdc"`) | -| SSH access | Hypervisor reachable by SSH with `oc` CLI and cluster-admin kubeconfig | -| Go 1.24+ | Installed on the hypervisor (for building the test binary) | - -## Implementation - -### Step 1: Gather Inputs - -Parse `$ARGUMENTS` for an optional JIRA ticket ID (e.g. `OCPEDGE-1995`). - -Ask for the hypervisor SSH host: - -``` -Hypervisor SSH host? (default: ec2-user@52.29.221.136) -``` - -Set: -``` -SSH_HOST = user input or default -KUBECONFIG = /home/ec2-user/openshift-metal3/dev-scripts/ocp/ostest/auth/kubeconfig -``` - -### Step 2: Detect State - -Check process and log state on the hypervisor to decide which phase to enter: - -```bash -ssh "$SSH_HOST" ' - pgrep -f "integration-test run-suite" >/dev/null 2>&1 && echo RUNNING - ls ~/lvms-mno.log ~/lvms-sno.log 2>/dev/null | head -1 -' 2>/dev/null -``` - -| Result | Phase | -|--------|-------| -| Output contains `RUNNING` | → Phase 2: Tests In Progress | -| Output contains a log path (no RUNNING) | → Phase 3: Results Ready | -| No output | → Phase 1: Fresh Run | - ---- - -## Phase 1: Fresh Run - -### Step 1a: Ask for suite - -``` -Which test suite? -- mno: Multi-Node OpenShift (36 tests) — use for TNF clusters -- sno: Single-Node OpenShift (30 tests) — use for SNO clusters -- both: Run MNO then SNO sequentially (~3-4 hours) -``` - -Default to `mno` for TNF topology. - -### Step 1b: Check existing LVMS deployment - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ - oc get deployment/lvms-operator -n openshift-lvm-storage 2>/dev/null" -``` - -- **Found**: Ask: "LVMS already deployed. Redeploy from source or skip to test run?" - - Redeploy: `ssh "$SSH_HOST" "cd ~/lvm-operator && make undeploy"` then continue - - Skip: jump to Step 1d -- **Not found**: Continue - -### Step 1c: Deploy LVMS from source - -Patch image registry to Managed (some tests need it): - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ - oc patch configs.imageregistry.operator.openshift.io cluster \ - --type merge --patch '{\"spec\":{\"managementState\":\"Managed\",\"storage\":{\"emptyDir\":{}}}}'" -``` - -Clone or update lvm-operator: - -```bash -ssh "$SSH_HOST" ' - if [ -d ~/lvm-operator ]; then - cd ~/lvm-operator && git fetch origin && git checkout main && git pull origin main - else - git clone https://github.com/openshift/lvm-operator.git ~/lvm-operator - fi - echo "Commit: $(cd ~/lvm-operator && git rev-parse --short HEAD)" -' -``` - -Deploy (creates namespace, CRDs, RBAC, operator via kustomize — no image build required): - -```bash -ssh "$SSH_HOST" "cd ~/lvm-operator && make deploy" -``` - -Wait for operator: - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ - oc -n openshift-lvm-storage wait deployment/lvms-operator \ - --for=condition=Available --timeout=120s" -``` - -Apply LVMCluster CR (required for CSI driver registration): - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ - oc apply -n openshift-lvm-storage \ - -f ~/lvm-operator/config/samples/lvm_v1alpha1_lvmcluster.yaml" -``` - -Wait for Ready (poll every 10s, timeout 3m): - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; \ - for i in \$(seq 1 18); do - STATE=\$(oc -n openshift-lvm-storage get lvmcluster my-lvmcluster \ - -o jsonpath='{.status.state}' 2>/dev/null) - echo \"[\$i] \$STATE\" - [ \"\$STATE\" = \"Ready\" ] && break - sleep 10 - done" -``` - -Verify CSI driver — if `topolvm.io` is not listed, do not proceed (tests will silently skip everything): - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; oc get csidrivers | grep topolvm" -``` - -### Step 1d: Build test binary and start tests - -```bash -ssh "$SSH_HOST" "cd ~/lvm-operator/test/integration && make integration-build" -``` - -Remove stale logs, then start the run with `nohup`: - -```bash -# For mno: -ssh "$SSH_HOST" ' - rm -f ~/lvms-mno.log - cd ~/lvm-operator/test/integration - nohup bash -c "./integration-test run-suite -c 1 \ - openshift/lvm-operator/test/integration/qe_tests/mno > ~/lvms-mno.log 2>&1" & - echo "PID: $!" -' - -# For sno: -ssh "$SSH_HOST" ' - rm -f ~/lvms-sno.log - cd ~/lvm-operator/test/integration - nohup bash -c "./integration-test run-suite -c 1 \ - openshift/lvm-operator/test/integration/qe_tests/sno > ~/lvms-sno.log 2>&1" & - echo "PID: $!" -' - -# For both (sequential): -ssh "$SSH_HOST" ' - rm -f ~/lvms-mno.log ~/lvms-sno.log - cd ~/lvm-operator/test/integration - nohup bash -c " - ./integration-test run-suite -c 1 \ - openshift/lvm-operator/test/integration/qe_tests/mno > ~/lvms-mno.log 2>&1 - ./integration-test run-suite -c 1 \ - openshift/lvm-operator/test/integration/qe_tests/sno > ~/lvms-sno.log 2>&1 - " & - echo "PID: $!" -' -``` - -### Step 1e: Exit with monitoring instructions - -``` -Tests started on (PID above). Suite: — ~1-2 hours per suite. - -The OTE framework buffers output per-test, so the log stays empty while tests run. - -Monitor: - ssh 'ps aux | grep integration-test' - ssh 'tail -f ~/lvms-.log' - -When complete, re-invoke: - /lvms:run-integration-tests -``` - -**Stop here.** Do not proceed to Phase 2 or 3 in the same invocation. - ---- - -## Phase 2: Tests In Progress - -Count completed tests from the partial log: - -```bash -ssh "$SSH_HOST" "python3 -c \" -import json, sys -try: - raw = open('/root/lvms-mno.log').read() - # partial JSON — count result fields - passed = raw.count('\"result\": \"passed\"') - failed = raw.count('\"result\": \"failed\"') - print(f'passed: {passed}, failed: {failed}') -except: print('log not yet written') -\" 2>/dev/null || echo 'log not yet written'" -``` - -Report to the user: -``` -Tests still running on . -Progress: X passed, Y failed so far. - -Monitor: ssh 'tail -f ~/lvms-.log' - -Re-invoke /lvms:run-integration-tests when complete. -``` - -**Stop here.** - ---- - -## Phase 3: Results Ready - -### Step 3a: Determine which logs exist - -```bash -ssh "$SSH_HOST" "ls ~/lvms-mno.log ~/lvms-sno.log 2>/dev/null" -``` - -### Step 3b: Parse each log - -For each log file, use Python to parse the OTE JSON output (the log is a JSON array with a trailing -`Error: N tests failed` line): - -```bash -ssh "$SSH_HOST" "python3 -c \" -import json, re -raw = open('/root/lvms-.log').read() -data = json.loads(raw[:raw.rfind(']')+1]) -passed = [t for t in data if t['result'] == 'passed'] -failed = [t for t in data if t['result'] == 'failed'] -print(f'TOTAL:{len(data)} PASSED:{len(passed)} FAILED:{len(failed)}') -for t in failed: - m = re.search(r'[A-Z]+-\d+', t['name']) - tid = m.group(0) if m else 'unknown' - print(f'FAIL|{tid}|{t[\"name\"][:80]}|{t.get(\"output\",\"\").strip().splitlines()[-1][:120]}') -\"" -``` - -### Step 3c: Get cluster version - -```bash -ssh "$SSH_HOST" "export KUBECONFIG=$KUBECONFIG; oc version --short 2>/dev/null || oc version" -``` - -### Step 3d: Generate Markdown report - -Known flakes (flag but do not count as failures): - -| Test ID | Known Issue | -|---------|-------------| -| OCP-86156 | `pvmove` fails with `No data to move` — VG state sensitivity between tests | -| OCP-71012 | ForceWipe — virtio partition naming on libvirt VMs | -| OCP-69772 | RAID test picks `/dev/sr0` (virtual CD-ROM) | - -Report format: - -```markdown -### LVMS Integration Test Results - -**OCP Version:** -**lvm-operator:** main @ -**Suite:** -**Date:** - -#### Summary - -| Suite | Passed | Failed | Total | Pass Rate | -|-------|--------|--------|-------|-----------| -| MNO | X | Y | Z | XX.X% | - -#### Failures - -| Test ID | Test Name | Failure Reason | Known Flake? | -|---------|-----------|----------------|--------------| - -#### Conclusion -**PASS** / **BLOCKED** — -``` - -Use **PASS** if pass rate ≥ 90% and no unexpected failures. Use **BLOCKED** otherwise. - -### Step 3e: Post to JIRA - -If a JIRA ticket was provided (from `$ARGUMENTS`), ask before posting: -``` -Post results to ? (yes/no) -``` - -If yes, post using the `mcp__mcp-atlassian__jira_add_comment` MCP tool. - -### Step 3f: Offer log cleanup - -Ask: "Remove log files from the hypervisor? (yes/no)" - -If yes: -```bash -ssh "$SSH_HOST" "rm -f ~/lvms-mno.log ~/lvms-sno.log" -``` - -## Error Handling - -| Error | Action | -|-------|--------| -| SSH connection refused | Hypervisor down — start it and retry | -| Go not found | Install Go 1.24+ on the hypervisor | -| `make deploy` fails | Check `oc get events -n openshift-lvm-storage` | -| LVMCluster not Ready after 3m | Check `oc describe lvmcluster -n openshift-lvm-storage` | -| `topolvm.io` CSI driver missing | Check vg-manager logs: `oc logs -n openshift-lvm-storage -l app.kubernetes.io/name=vg-manager` | -| `make integration-build` fails | Verify Go version — must be 1.24+ | -| Log empty after process exits | SSH disconnect killed the run — restart with nohup | -| JIRA post fails | Display report for manual copy | - -## Notes - -- **`-c 1` is mandatory** — Serial/Disruptive tests modify the LVMCluster CR; higher concurrency causes interference -- **`nohup` is mandatory** — SSH disconnect kills the run otherwise -- **Run from the hypervisor only** — test binary resolves cluster-internal DNS -- **OTE log buffering** — log stays empty until each test completes; monitor via `ps aux` -- **Free disks required** — check worker nodes with `lsblk`; disks with partitions/filesystems are skipped