diff --git a/distributables/github/workflows/orchestrator.yml b/distributables/github/workflows/orchestrator.yml index 71872f1..0081937 100644 --- a/distributables/github/workflows/orchestrator.yml +++ b/distributables/github/workflows/orchestrator.yml @@ -53,6 +53,8 @@ jobs: blocked_count: ${{ steps.monitor.outputs.blocked_count }} queued_count: ${{ steps.monitor.outputs.queued_count }} new_blocked: ${{ steps.monitor.outputs.new_blocked }} + transitions_details: ${{ steps.monitor.outputs.transitions_details }} + push_result: ${{ steps.commit.outputs.push_result }} steps: - name: Checkout repository @@ -92,6 +94,7 @@ jobs: """).fetchall() PLANNING_PLUS = ('planning', 'executing', 'reviewing', 'verifying', 'implemented', 'ready', 'released') + transition_details = [] # Store detailed transition info for story in blocked: prereqs = json.loads(story['prerequisites'] or '[]') @@ -107,6 +110,12 @@ jobs: break if all_ready: + # Get full story details before update + details = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE id = ? + """, (story['id'],)).fetchone() + conn.execute(""" UPDATE story_nodes SET hold_reason = 'queued', @@ -114,6 +123,13 @@ jobs: WHERE id = ? """, (story['id'],)) transitions.append(f"{story['id']}: blocked -> queued") + transition_details.append({ + 'id': story['id'], + 'user_story': details['user_story'][:80] + '...' if details['user_story'] and len(details['user_story']) > 80 else details['user_story'], + 'before': {'stage': details['stage'], 'hold': 'blocked', 'disposition': details['disposition']}, + 'after': {'stage': details['stage'], 'hold': 'queued', 'disposition': details['disposition']}, + 'change': 'blocked -> queued (all prereqs at planning+)' + }) # Check queued stories: transition to no hold when all prereqs reach implemented+ queued = conn.execute(""" @@ -129,6 +145,12 @@ jobs: for story in queued: prereqs = json.loads(story['prerequisites'] or '[]') if not prereqs: + # Get full story details before update + details = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE id = ? + """, (story['id'],)).fetchone() + # No prereqs, clear hold conn.execute(""" UPDATE story_nodes @@ -137,6 +159,13 @@ jobs: WHERE id = ? """, (story['id'],)) transitions.append(f"{story['id']}: queued -> no hold (no prereqs)") + transition_details.append({ + 'id': story['id'], + 'user_story': details['user_story'][:80] + '...' if details['user_story'] and len(details['user_story']) > 80 else details['user_story'], + 'before': {'stage': details['stage'], 'hold': 'queued', 'disposition': details['disposition']}, + 'after': {'stage': details['stage'], 'hold': None, 'disposition': details['disposition']}, + 'change': 'queued -> no hold (no prereqs)' + }) continue all_implemented = True @@ -147,6 +176,12 @@ jobs: break if all_implemented: + # Get full story details before update + details = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE id = ? + """, (story['id'],)).fetchone() + conn.execute(""" UPDATE story_nodes SET hold_reason = NULL, @@ -154,6 +189,13 @@ jobs: WHERE id = ? """, (story['id'],)) transitions.append(f"{story['id']}: queued -> no hold") + transition_details.append({ + 'id': story['id'], + 'user_story': details['user_story'][:80] + '...' if details['user_story'] and len(details['user_story']) > 80 else details['user_story'], + 'before': {'stage': details['stage'], 'hold': 'queued', 'disposition': details['disposition']}, + 'after': {'stage': details['stage'], 'hold': None, 'disposition': details['disposition']}, + 'change': 'queued -> no hold (all prereqs implemented+)' + }) conn.commit() @@ -188,6 +230,9 @@ jobs: f.write(f"blocked_count={blocked_count}\n") f.write(f"queued_count={queued_count}\n") f.write(f"new_blocked={new_blocked_ids}\n") + # Escape JSON for GitHub Actions output + details_json = json.dumps(transition_details).replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') + f.write(f"transitions_details={details_json}\n") print(f"Transitions made: {len(transitions)}") for t in transitions: @@ -196,9 +241,15 @@ jobs: print(f"Queued stories: {queued_count}") if new_blocked_ids: print(f"New blocked needing dep children: {new_blocked_ids}") + if transition_details: + print(f"\nTransition Details:") + for d in transition_details: + print(f" Story {d['id']}: {d['user_story']}") + print(f" {d['change']}") PYEOF - name: Commit transitions + id: commit if: steps.monitor.outputs.transitions_made != '0' run: | git config user.name "github-actions[bot]" @@ -211,6 +262,7 @@ jobs: for attempt in 1 2 3 4; do if git push origin main; then echo "Push successful" + echo "push_result=success" >> $GITHUB_OUTPUT exit 0 else echo "Push failed (attempt $attempt), retrying..." @@ -219,7 +271,10 @@ jobs: fi done echo "Push failed after 4 attempts" + echo "push_result=failed" >> $GITHUB_OUTPUT exit 1 + else + echo "push_result=no_changes" >> $GITHUB_OUTPUT fi # ════════════════════════════════════════════════════════════════════════════ @@ -251,6 +306,10 @@ jobs: if: always() && !cancelled() && needs.monitor-holds.result == 'success' runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + stories_examined: ${{ steps.check.outputs.stories_examined }} + concept_count: ${{ steps.check.outputs.concept_count }} + triggered: ${{ steps.trigger.outputs.triggered }} steps: - name: Checkout repository @@ -262,39 +321,72 @@ jobs: python3 << 'PYEOF' import sqlite3 import os + import json db_path = '.claude/data/story-tree.db' if not os.path.exists(db_path): with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("has_work=false\n") + f.write("stories_examined=[]\n") exit(0) conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row - # Priority: queued > polish > no hold - count = conn.execute(""" - SELECT COUNT(*) FROM story_nodes + # Get detailed info on concept stories + stories = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE stage = 'concept' AND disposition IS NULL AND (hold_reason IN ('queued', 'polish') OR hold_reason IS NULL) - """).fetchone()[0] + ORDER BY + CASE hold_reason + WHEN 'queued' THEN 1 + WHEN 'polish' THEN 2 + ELSE 3 + END, + updated_at ASC + LIMIT 5 + """).fetchall() + + count = len(stories) + stories_info = [] + for s in stories: + stories_info.append({ + 'id': s['id'], + 'user_story': (s['user_story'][:60] + '...') if s['user_story'] and len(s['user_story']) > 60 else s['user_story'], + 'stage': s['stage'], + 'hold': s['hold_reason'], + 'disposition': s['disposition'] + }) conn.close() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f"has_work={'true' if count > 0 else 'false'}\n") f.write(f"concept_count={count}\n") + stories_json = json.dumps(stories_info).replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') + f.write(f"stories_examined={stories_json}\n") print(f"Concepts to process: {count}") + for s in stories_info: + print(f" - {s['id']}: {s['user_story']} (hold: {s['hold']})") PYEOF - name: Trigger build-concepts + id: trigger if: steps.check.outputs.has_work == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Triggering build-concepts.yml for ${{ steps.check.outputs.concept_count }} concepts" - gh workflow run build-concepts.yml --repo ${{ github.repository }} || echo "Note: build-concepts also runs on its own schedule" + if gh workflow run build-concepts.yml --repo ${{ github.repository }}; then + echo "triggered=true" >> $GITHUB_OUTPUT + else + echo "triggered=false" >> $GITHUB_OUTPUT + echo "Note: build-concepts also runs on its own schedule" + fi # STEP 4: Create Plans — Trigger create-plan workflow create-plans: @@ -302,6 +394,10 @@ jobs: if: always() && !cancelled() && needs.monitor-holds.result == 'success' runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + stories_examined: ${{ steps.check.outputs.stories_examined }} + planning_count: ${{ steps.check.outputs.planning_count }} + triggered: ${{ steps.trigger.outputs.triggered }} steps: - name: Checkout repository @@ -313,39 +409,66 @@ jobs: python3 << 'PYEOF' import sqlite3 import os + import json db_path = '.claude/data/story-tree.db' if not os.path.exists(db_path): with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("has_work=false\n") + f.write("stories_examined=[]\n") exit(0) conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row - # Stories ready for planning (queued in planning stage) - count = conn.execute(""" - SELECT COUNT(*) FROM story_nodes + # Get detailed info on planning stories + stories = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE stage = 'planning' AND hold_reason = 'queued' AND disposition IS NULL - """).fetchone()[0] + ORDER BY updated_at ASC + LIMIT 5 + """).fetchall() + + count = len(stories) + stories_info = [] + for s in stories: + stories_info.append({ + 'id': s['id'], + 'user_story': (s['user_story'][:60] + '...') if s['user_story'] and len(s['user_story']) > 60 else s['user_story'], + 'stage': s['stage'], + 'hold': s['hold_reason'], + 'disposition': s['disposition'] + }) conn.close() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f"has_work={'true' if count > 0 else 'false'}\n") f.write(f"planning_count={count}\n") + stories_json = json.dumps(stories_info).replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') + f.write(f"stories_examined={stories_json}\n") print(f"Stories ready for planning: {count}") + for s in stories_info: + print(f" - {s['id']}: {s['user_story']} (hold: {s['hold']})") PYEOF - name: Trigger create-plan + id: trigger if: steps.check.outputs.has_work == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Triggering create-plan.yml for ${{ steps.check.outputs.planning_count }} stories" - gh workflow run create-plan.yml --repo ${{ github.repository }} || echo "Note: create-plan also runs on its own schedule" + if gh workflow run create-plan.yml --repo ${{ github.repository }}; then + echo "triggered=true" >> $GITHUB_OUTPUT + else + echo "triggered=false" >> $GITHUB_OUTPUT + echo "Note: create-plan also runs on its own schedule" + fi # STEP 5: Trigger Execute Story workflow trigger-execute: @@ -353,6 +476,10 @@ jobs: if: always() && !cancelled() && needs.monitor-holds.result == 'success' runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + stories_examined: ${{ steps.check.outputs.stories_examined }} + execute_count: ${{ steps.check.outputs.execute_count }} + triggered: ${{ steps.trigger.outputs.triggered }} steps: - name: Checkout repository @@ -364,38 +491,65 @@ jobs: python3 << 'PYEOF' import sqlite3 import os + import json db_path = '.claude/data/story-tree.db' if not os.path.exists(db_path): with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("has_work=false\n") + f.write("stories_examined=[]\n") exit(0) conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row - # Stories ready for execution (no hold in executing stage) - count = conn.execute(""" - SELECT COUNT(*) FROM story_nodes + # Get detailed info on executable stories + stories = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition + FROM story_nodes WHERE stage = 'executing' AND hold_reason IS NULL AND disposition IS NULL - """).fetchone()[0] + ORDER BY updated_at ASC + LIMIT 5 + """).fetchall() + + count = len(stories) + stories_info = [] + for s in stories: + stories_info.append({ + 'id': s['id'], + 'user_story': (s['user_story'][:60] + '...') if s['user_story'] and len(s['user_story']) > 60 else s['user_story'], + 'stage': s['stage'], + 'hold': s['hold_reason'], + 'disposition': s['disposition'] + }) conn.close() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f"has_work={'true' if count > 0 else 'false'}\n") + f.write(f"execute_count={count}\n") + stories_json = json.dumps(stories_info).replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') + f.write(f"stories_examined={stories_json}\n") print(f"Stories ready for execution: {count}") + for s in stories_info: + print(f" - {s['id']}: {s['user_story']}") PYEOF - name: Trigger execute-story + id: trigger if: steps.check.outputs.has_work == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Triggering execute-story.yml" - gh workflow run execute-story.yml --repo ${{ github.repository }} + if gh workflow run execute-story.yml --repo ${{ github.repository }}; then + echo "triggered=true" >> $GITHUB_OUTPUT + else + echo "triggered=false" >> $GITHUB_OUTPUT + fi # STEP 6: Trigger Debug Story workflow trigger-debug: @@ -403,6 +557,10 @@ jobs: if: always() && !cancelled() && needs.monitor-holds.result == 'success' runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + stories_examined: ${{ steps.check.outputs.stories_examined }} + debug_count: ${{ steps.check.outputs.debug_count }} + triggered: ${{ steps.trigger.outputs.triggered }} steps: - name: Checkout repository @@ -414,39 +572,67 @@ jobs: python3 << 'PYEOF' import sqlite3 import os + import json db_path = '.claude/data/story-tree.db' if not os.path.exists(db_path): with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write("has_work=false\n") + f.write("stories_examined=[]\n") exit(0) conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row - # Broken stories with debug attempts < 5 - count = conn.execute(""" - SELECT COUNT(*) FROM story_nodes + # Get detailed info on broken stories + stories = conn.execute(""" + SELECT id, user_story, stage, hold_reason, disposition, debug_attempts + FROM story_nodes WHERE stage = 'executing' AND hold_reason = 'broken' AND disposition IS NULL AND COALESCE(debug_attempts, 0) < 5 - """).fetchone()[0] + ORDER BY updated_at ASC + LIMIT 5 + """).fetchall() + + count = len(stories) + stories_info = [] + for s in stories: + stories_info.append({ + 'id': s['id'], + 'user_story': (s['user_story'][:60] + '...') if s['user_story'] and len(s['user_story']) > 60 else s['user_story'], + 'stage': s['stage'], + 'hold': s['hold_reason'], + 'disposition': s['disposition'], + 'debug_attempts': s['debug_attempts'] or 0 + }) conn.close() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f"has_work={'true' if count > 0 else 'false'}\n") + f.write(f"debug_count={count}\n") + stories_json = json.dumps(stories_info).replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') + f.write(f"stories_examined={stories_json}\n") print(f"Broken stories to debug: {count}") + for s in stories_info: + print(f" - {s['id']}: {s['user_story']} (attempts: {s['debug_attempts']})") PYEOF - name: Trigger debug-story + id: trigger if: steps.check.outputs.has_work == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Triggering debug-story.yml" - gh workflow run debug-story.yml --repo ${{ github.repository }} + if gh workflow run debug-story.yml --repo ${{ github.repository }}; then + echo "triggered=true" >> $GITHUB_OUTPUT + else + echo "triggered=false" >> $GITHUB_OUTPUT + fi # ════════════════════════════════════════════════════════════════════════════ # SUMMARY: Report cycle results @@ -459,16 +645,169 @@ jobs: steps: - name: Generate summary + env: + TRANSITIONS_DETAILS: ${{ needs.monitor-holds.outputs.transitions_details }} + CONCEPTS_STORIES: ${{ needs.process-concepts.outputs.stories_examined }} + PLANS_STORIES: ${{ needs.create-plans.outputs.stories_examined }} + EXECUTE_STORIES: ${{ needs.trigger-execute.outputs.stories_examined }} + DEBUG_STORIES: ${{ needs.trigger-debug.outputs.stories_examined }} + run: | + python3 << 'PYEOF' + import os + import json + + summary_file = os.environ.get('GITHUB_STEP_SUMMARY', '/dev/stdout') + + def write_summary(content): + with open(summary_file, 'a') as f: + f.write(content + '\n') + + def parse_json_safe(json_str, default=[]): + if not json_str or json_str == '': + return default + try: + return json.loads(json_str) + except: + return default + + write_summary("# Orchestrator Cycle Summary") + write_summary("") + + # Job Status Overview + write_summary("## Job Status") + write_summary("") + write_summary("| Job | Result | Details |") + write_summary("|-----|--------|---------|") + + monitor_result = "${{ needs.monitor-holds.result }}" + transitions = "${{ needs.monitor-holds.outputs.transitions_made || '0' }}" + push_result = "${{ needs.monitor-holds.outputs.push_result || 'n/a' }}" + push_icon = "OK" if push_result == "success" else ("FAILED" if push_result == "failed" else "—") + + write_summary(f"| Monitor Holds | {monitor_result} | {transitions} transitions, push: {push_icon} |") + write_summary(f"| Dep Children | ${{ needs.trigger-dep-children.result || 'skipped' }} | new_blocked: ${{ needs.monitor-holds.outputs.new_blocked || 'none' }} |") + + concepts_count = "${{ needs.process-concepts.outputs.concept_count || '0' }}" + concepts_triggered = "${{ needs.process-concepts.outputs.triggered || 'false' }}" + write_summary(f"| Process Concepts | ${{ needs.process-concepts.result || 'skipped' }} | {concepts_count} found, triggered: {concepts_triggered} |") + + plans_count = "${{ needs.create-plans.outputs.planning_count || '0' }}" + plans_triggered = "${{ needs.create-plans.outputs.triggered || 'false' }}" + write_summary(f"| Create Plans | ${{ needs.create-plans.result || 'skipped' }} | {plans_count} found, triggered: {plans_triggered} |") + + execute_count = "${{ needs.trigger-execute.outputs.execute_count || '0' }}" + execute_triggered = "${{ needs.trigger-execute.outputs.triggered || 'false' }}" + write_summary(f"| Execute Stories | ${{ needs.trigger-execute.result || 'skipped' }} | {execute_count} found, triggered: {execute_triggered} |") + + debug_count = "${{ needs.trigger-debug.outputs.debug_count || '0' }}" + debug_triggered = "${{ needs.trigger-debug.outputs.triggered || 'false' }}" + write_summary(f"| Debug Broken | ${{ needs.trigger-debug.result || 'skipped' }} | {debug_count} found, triggered: {debug_triggered} |") + + write_summary("") + + # Monitor Hold Transitions (if any) + transitions_details = parse_json_safe(os.environ.get('TRANSITIONS_DETAILS', '')) + if transitions_details: + write_summary("## Hold Transitions") + write_summary("") + write_summary("| Story ID | User Story | Change |") + write_summary("|----------|------------|--------|") + for t in transitions_details: + story = t.get('user_story', 'N/A') or 'N/A' + write_summary(f"| `{t['id']}` | {story} | {t['change']} |") + write_summary("") + + # Stories Examined by Each Job + any_stories = False + + concepts = parse_json_safe(os.environ.get('CONCEPTS_STORIES', '')) + if concepts: + any_stories = True + write_summary("## Concept Stories Examined") + write_summary("") + write_summary("| ID | User Story | Stage | Hold |") + write_summary("|----|------------|-------|------|") + for s in concepts: + write_summary(f"| `{s['id']}` | {s.get('user_story', 'N/A') or 'N/A'} | {s['stage']} | {s.get('hold') or '—'} |") + write_summary("") + + plans = parse_json_safe(os.environ.get('PLANS_STORIES', '')) + if plans: + any_stories = True + write_summary("## Planning Stories Examined") + write_summary("") + write_summary("| ID | User Story | Stage | Hold |") + write_summary("|----|------------|-------|------|") + for s in plans: + write_summary(f"| `{s['id']}` | {s.get('user_story', 'N/A') or 'N/A'} | {s['stage']} | {s.get('hold') or '—'} |") + write_summary("") + + execute = parse_json_safe(os.environ.get('EXECUTE_STORIES', '')) + if execute: + any_stories = True + write_summary("## Execution Stories Examined") + write_summary("") + write_summary("| ID | User Story | Stage | Hold |") + write_summary("|----|------------|-------|------|") + for s in execute: + write_summary(f"| `{s['id']}` | {s.get('user_story', 'N/A') or 'N/A'} | {s['stage']} | {s.get('hold') or '—'} |") + write_summary("") + + debug = parse_json_safe(os.environ.get('DEBUG_STORIES', '')) + if debug: + any_stories = True + write_summary("## Debug Stories Examined") + write_summary("") + write_summary("| ID | User Story | Stage | Hold | Attempts |") + write_summary("|----|------------|-------|------|----------|") + for s in debug: + write_summary(f"| `{s['id']}` | {s.get('user_story', 'N/A') or 'N/A'} | {s['stage']} | {s.get('hold') or '—'} | {s.get('debug_attempts', 0)} |") + write_summary("") + + if not any_stories and not transitions_details: + write_summary("## No Stories Processed") + write_summary("") + write_summary("No story nodes were transitioned or scheduled for processing in this cycle.") + write_summary("") + + # Push Status Summary + if push_result != "n/a": + write_summary("## Push Status") + write_summary("") + if push_result == "success": + write_summary("Changes successfully pushed to main branch.") + elif push_result == "failed": + write_summary("**Push failed** after 4 retry attempts. Changes may be on an orphaned commit.") + else: + write_summary("— No changes to push.") + write_summary("") + + PYEOF + + - name: Report failures + if: failure() run: | - echo "## Orchestrator Cycle Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Monitor Holds | Transitions: ${{ needs.monitor-holds.outputs.transitions_made || 0 }} |" >> $GITHUB_STEP_SUMMARY - echo "| Blocked Stories | ${{ needs.monitor-holds.outputs.blocked_count || 0 }} |" >> $GITHUB_STEP_SUMMARY - echo "| Queued Stories | ${{ needs.monitor-holds.outputs.queued_count || 0 }} |" >> $GITHUB_STEP_SUMMARY - echo "| Dep Children | ${{ needs.trigger-dep-children.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Process Concepts | ${{ needs.process-concepts.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Create Plans | ${{ needs.create-plans.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Execute Stories | ${{ needs.trigger-execute.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Debug Broken | ${{ needs.trigger-debug.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "## Job Failures" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "One or more jobs failed. Check the job logs above for details:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.monitor-holds.result }}" = "failure" ]; then + echo "- **Monitor Holds**: Failed during hold transition processing" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ needs.trigger-dep-children.result }}" = "failure" ]; then + echo "- **Dep Children**: Failed to trigger dependency children workflow" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ needs.process-concepts.result }}" = "failure" ]; then + echo "- **Process Concepts**: Failed during concept check or trigger" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ needs.create-plans.result }}" = "failure" ]; then + echo "- **Create Plans**: Failed during planning check or trigger" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ needs.trigger-execute.result }}" = "failure" ]; then + echo "- **Execute Stories**: Failed during execution check or trigger" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ needs.trigger-debug.result }}" = "failure" ]; then + echo "- **Debug Broken**: Failed during debug check or trigger" >> $GITHUB_STEP_SUMMARY + fi