From 82cc10824a014724cdb7e6f1f486bb41d514fe23 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sun, 14 Dec 2025 16:17:30 -0700 Subject: [PATCH 1/2] fix(bmm): improve sprint-status validation and epic status handling (#1125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(bmm): improve sprint-status validation and epic status handling - Add status validation with interactive correction for unknown values - Update epic statuses to match state machine: backlog, in-progress, done - Map legacy "contexted" status to "in-progress" explicitly - Add retrospective status counting (optional, completed) - Rewrite risk detection rules for LLM clarity - Fix warnings vs risks naming inconsistency in data mode Closes #1106 Closes #1118 ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * style: fix prettier formatting in sprint-status instructions ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../sprint-status/instructions.md | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md b/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md index c31f0bea..dc5241c7 100644 --- a/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md +++ b/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md @@ -40,15 +40,50 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat - Epics: keys starting with "epic-" (and not ending with "-retrospective") - Retrospectives: keys ending with "-retrospective" - Stories: everything else (e.g., 1-2-login-form) - If any story has status `drafted`, treat as `ready-for-dev` (legacy status) + Map legacy story status "drafted" โ†’ "ready-for-dev" Count story statuses: backlog, ready-for-dev, in-progress, review, done - Count epic statuses: backlog, contexted - Detect risks: - - Stories in review but no reviewer assigned context โ†’ suggest `/bmad:bmm:workflows:code-review` - - Stories in in-progress with no ready-for-dev items behind them โ†’ keep focus on the active story - - All epics backlog/contexted but no stories ready-for-dev โ†’ prompt to run `/bmad:bmm:workflows:create-story` - - Stories in ready-for-dev may be unvalidated โ†’ suggest `/bmad:bmm:workflows:validate-create-story` before `dev-story` for quality check - + Map legacy epic status "contexted" โ†’ "in-progress" + Count epic statuses: backlog, in-progress, done + Count retrospective statuses: optional, completed + +Validate all statuses against known values: + +- Valid story statuses: backlog, ready-for-dev, in-progress, review, done, drafted (legacy) +- Valid epic statuses: backlog, in-progress, done, contexted (legacy) +- Valid retrospective statuses: optional, completed + + + +โš ๏ธ **Unknown status detected:** +{{#each invalid_entries}} + +- `{{key}}`: "{{status}}" (not recognized) + {{/each}} + +**Valid statuses:** + +- Stories: backlog, ready-for-dev, in-progress, review, done +- Epics: backlog, in-progress, done +- Retrospectives: optional, completed + + How should these be corrected? + {{#each invalid_entries}} + {{@index}}. {{key}}: "{{status}}" โ†’ [select valid status] + {{/each}} + +Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue without fixing: + +Update sprint-status.yaml with corrected values +Re-parse the file with corrected statuses + + + +Detect risks: + +- IF any story has status "review": suggest `/bmad:bmm:workflows:code-review` +- IF any story has status "in-progress" AND no stories have status "ready-for-dev": recommend staying focused on active story +- IF all epics have status "backlog" AND no stories have status "ready-for-dev": prompt `/bmad:bmm:workflows:create-story` + Pick the next recommended workflow using priority: @@ -71,7 +106,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat **Stories:** backlog {{count_backlog}}, ready-for-dev {{count_ready}}, in-progress {{count_in_progress}}, review {{count_review}}, done {{count_done}} -**Epics:** backlog {{epic_backlog}}, contexted {{epic_contexted}} +**Epics:** backlog {{epic_backlog}}, in-progress {{epic_in_progress}}, done {{epic_done}} **Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}}) @@ -141,8 +176,9 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted. count_review = {{count_review}} count_done = {{count_done}} epic_backlog = {{epic_backlog}} - epic_contexted = {{epic_contexted}} - warnings = {{risks}} + epic_in_progress = {{epic_in_progress}} + epic_done = {{epic_done}} + risks = {{risks}} Return to caller From ebb20f675f94bd0a91a2534cea4599f2786882ff Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sun, 14 Dec 2025 16:19:44 -0700 Subject: [PATCH 2/2] chore(discord): suppress link embeds and handle truncated URLs (#1126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(discord): suppress link embeds and handle truncated URLs - Add wrap_urls() to wrap URLs in <> to suppress Discord embeds - Add strip_trailing_url() to remove partial URLs from truncated text - Update all 6 workflow jobs with body text to use the new helpers - Partial URLs (from truncation) are removed since they are unusable - Complete URLs are wrapped to prevent large embed previews ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(discord): preserve URLs when escaping markdown - Replace sed-based esc() with awk version that skips content inside wrappers, preventing URL corruption from backslash escaping - Reorder pipeline: wrap_urls | esc (wrap first, then escape) - Update comment: "partial" โ†’ "incomplete" for clarity ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: Brian --- .github/scripts/discord-helpers.sh | 23 ++++++++++++++++-- .github/workflows/discord.yaml | 38 ++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/.github/scripts/discord-helpers.sh b/.github/scripts/discord-helpers.sh index 191b9037..dd323d30 100644 --- a/.github/scripts/discord-helpers.sh +++ b/.github/scripts/discord-helpers.sh @@ -2,8 +2,21 @@ # Discord notification helper functions # Escape markdown special chars and @mentions for safe Discord display -# Bracket expression: ] must be first, then other chars. In POSIX bracket expr, \ is literal. -esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; } +# Skips content inside wrappers to preserve URLs intact +esc() { + awk '{ + result = ""; in_url = 0; n = length($0) + for (i = 1; i <= n; i++) { + c = substr($0, i, 1) + if (c == "<" && substr($0, i, 8) ~ /^") in_url = 0 } + else if (c == "@") result = result "@ " + else if (index("[]\\*_()~`", c) > 0) result = result "\\" c + else result = result c + } + print result + }' +} # Truncate to $1 chars (or 80 if wall-of-text with <3 spaces) trunc() { @@ -13,3 +26,9 @@ trunc() { [ "$spaces" -lt 3 ] && [ ${#txt} -gt 80 ] && txt=$(printf '%s' "$txt" | cut -c1-80) printf '%s' "$txt" } + +# Remove incomplete URL at end of truncated text (incomplete URLs are useless) +strip_trailing_url() { sed -E 's~ to suppress Discord embeds (keeps links clickable) +wrap_urls() { sed -E 's~https?://[^[:space:]<>]+~<&>~g'; } diff --git a/.github/workflows/discord.yaml b/.github/workflows/discord.yaml index 109bbb16..9d5c44e6 100644 --- a/.github/workflows/discord.yaml +++ b/.github/workflows/discord.yaml @@ -53,7 +53,11 @@ jobs: TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." - BODY=$(printf '%s' "$PR_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$PR_BODY" | trunc $MAX_BODY) + if [ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$BODY" ] && BODY=" ยท $BODY" USER=$(printf '%s' "$PR_USER" | esc) @@ -91,7 +95,11 @@ jobs: TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc) [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." - BODY=$(printf '%s' "$ISSUE_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$ISSUE_BODY" | trunc $MAX_BODY) + if [ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$BODY" ] && BODY=" ยท $BODY" USER=$(printf '%s' "$USER" | esc) @@ -126,7 +134,11 @@ jobs: TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc) [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." - BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY) + if [ ${#COMMENT_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." USER=$(printf '%s' "$COMMENT_USER" | esc) @@ -162,7 +174,11 @@ jobs: TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." - BODY=$(printf '%s' "$REVIEW_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$REVIEW_BODY" | trunc $MAX_BODY) + if [ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$BODY" ] && BODY=": $BODY" USER=$(printf '%s' "$REVIEW_USER" | esc) @@ -194,7 +210,11 @@ jobs: TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc) [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..." - BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY) + if [ ${#COMMENT_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." USER=$(printf '%s' "$COMMENT_USER" | esc) @@ -224,7 +244,11 @@ jobs: REL_NAME=$(printf '%s' "$NAME" | trunc $MAX_TITLE | esc) [ ${#NAME} -gt $MAX_TITLE ] && REL_NAME="${REL_NAME}..." - BODY=$(printf '%s' "$RELEASE_BODY" | trunc $MAX_BODY | esc) + BODY=$(printf '%s' "$RELEASE_BODY" | trunc $MAX_BODY) + if [ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ]; then + BODY=$(printf '%s' "$BODY" | strip_trailing_url) + fi + BODY=$(printf '%s' "$BODY" | wrap_urls | esc) [ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..." [ -n "$BODY" ] && BODY=" ยท $BODY" TAG_ESC=$(printf '%s' "$TAG" | esc) @@ -275,7 +299,7 @@ jobs: run: | set -o pipefail [ -z "$WEBHOOK" ] && exit 0 - esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; } + esc() { sed -e 's/[][\*_()~`]/\\&/g' -e 's/@/@ /g'; } trunc() { tr '\n\r' ' ' | cut -c1-"$1"; } REF_TRUNC=$(printf '%s' "$REF" | trunc 100)