# Submit Sign-off - Record Your Approval Decision The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml You MUST have already loaded and processed: {installed_path}/workflow.yaml ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✍️ SUBMIT SIGN-OFF ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Call: mcp__github__get_me() current_user = response.login ❌ GitHub MCP not accessible HALT Which document are you signing off on? Enter key: document_key = response if (document_type === 'prd') { doc_path = `${docs_dir}/prd/${document_key}.md` doc_label = `prd:${document_key}` review_label = 'type:prd-review' } else { doc_path = `${docs_dir}/epics/epic-${document_key}.md` doc_label = `epic:${document_key}` review_label = 'type:epic-review' } Call: mcp__github__search_issues({ query: "repo:{{github_owner}}/{{github_repo}} label:{{review_label}} label:{{doc_label}} label:review-status:signoff is:open" }) ❌ No active sign-off request found for {{doc_label}} The document may be: - Still in feedback stage - Already approved - Not yet created Use [MT] My Tasks to see what's pending for you. HALT review_issue = response.items[0] 📋 Found sign-off request: #{{review_issue.number}} {{review_issue.title}} Read doc_path ❌ Document not found: {{doc_path}} HALT doc_content = file_content title = extract_title(doc_content) version = extract_version(doc_content) // Check if user already signed off signoff_label_prefix = `signoff-${current_user}-` existing_signoff = review_issue.labels.some(l => l.name.startsWith(signoff_label_prefix) ) ⚠️ You have already submitted a sign-off decision for this document. Would you like to: [1] Change your decision [2] View current status [3] Cancel Choice: HALT Proceeding to update your sign-off decision... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📄 DOCUMENT SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ **Title:** {{title}} **Version:** v{{version}} **Key:** {{doc_label}} Would you like to view the full document before deciding? View document? (y/n): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📄 DOCUMENT CONTENT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ {{doc_content}} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🗳️ YOUR DECISION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Please select your sign-off decision: [1] ✅ APPROVE - I approve this document [2] ✅📝 APPROVE WITH NOTE - I approve with a minor note/observation [3] 🚫 BLOCK - I cannot approve, there is a blocking issue Decision (1-3): decision_map = { '1': { key: 'approved', emoji: '✅', text: 'Approved' }, '2': { key: 'approved_with_note', emoji: '✅📝', text: 'Approved with Note' }, '3': { key: 'blocked', emoji: '🚫', text: 'Blocked' } } decision = decision_map[response] || decision_map['1'] Enter your note (this will be visible to all stakeholders): note = response ⚠️ Blocking a document requires a clear reason. This will: 1. Prevent the document from being approved 2. Notify the PO and stakeholders 3. May trigger a new feedback round Enter your blocking reason: note = response Would you like to create a formal feedback issue for this blocking concern? Create feedback issue? (y/n): create_feedback_issue = true note = null // Build sign-off comment signoff_comment = `### ${decision.emoji} Sign-off from @${current_user} **Decision:** ${decision.text} **Date:** ${new Date().toISOString().split('T')[0]}` if (note) { signoff_comment += ` **Note:** ${note}` } Call: mcp__github__add_issue_comment({ owner: "{{github_owner}}", repo: "{{github_repo}}", issue_number: review_issue.number, body: signoff_comment }) // Get current labels current_labels = review_issue.labels.map(l => l.name) // Remove any existing signoff label for this user new_labels = current_labels.filter(l => !l.startsWith(`signoff-${current_user}-`) ) // Add new signoff label decision_label = decision.key.replace(/_/g, '-') new_labels.push(`signoff-${current_user}-${decision_label}`) Call: mcp__github__issue_write({ method: 'update', owner: "{{github_owner}}", repo: "{{github_repo}}", issue_number: review_issue.number, labels: new_labels }) feedback_body = `# 🚫 Blocking Concern **Document:** \`${doc_label}\` **Review:** #${review_issue.number} **Type:** Blocking concern requiring resolution --- ## Concern ${note} --- _Submitted by @${current_user} as part of sign-off for v${version}_` Call: mcp__github__issue_write({ method: 'create', owner: "{{github_owner}}", repo: "{{github_repo}}", title: "🚫 Blocking: {{title}}", body: feedback_body, labels: ['type:{{document_type}}-feedback', doc_label, 'feedback-type:concern', 'priority:high', 'feedback-status:new', `linked-review:${review_issue.number}`] }) ✅ Created feedback issue: #{{response.number}} ✅ Sign-off submitted: {{decision.emoji}} {{decision.text}} Checking if all required sign-offs are complete... // Refresh issue to get updated labels Call: mcp__github__issue_read({ method: 'get', owner: github_owner, repo: github_repo, issue_number: review_issue.number }) // Count sign-offs labels = response.labels.map(l => l.name) approved_count = labels.filter(l => l.includes('-approved') || l.includes('-approved-with-note') ).length blocked_count = labels.filter(l => l.includes('-blocked')).length // Get stakeholder count from document stakeholder_count = extract_stakeholders(doc_content).length ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 SIGN-OFF STATUS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ **Approved:** {{approved_count}} / {{stakeholder_count}} **Blocked:** {{blocked_count}} **Pending:** {{stakeholder_count - approved_count - blocked_count}} ⚠️ Document has {{blocked_count}} blocking concern(s). Cannot be approved until resolved. 🎉 ALL SIGN-OFFS RECEIVED! The document is ready to be marked as APPROVED. Mark document as approved? (y/n): // Update document status updated_content = doc_content.replace(/\*\*Status:\*\* .+/, '**Status:** Approved') Write updated_content to doc_path // Update review issue final_labels = labels .filter(l => !l.startsWith('review-status:')) .concat(['review-status:approved']) Call: mcp__github__issue_write({ method: 'update', owner: "{{github_owner}}", repo: "{{github_repo}}", issue_number: review_issue.number, labels: final_labels, state: 'closed', state_reason: 'completed' }) Call: mcp__github__add_issue_comment({ owner: "{{github_owner}}", repo: "{{github_repo}}", issue_number: review_issue.number, body: `## ✅ DOCUMENT APPROVED All stakeholders have signed off. This document is now approved and ready for implementation. **Final Version:** v${version} **Approved:** ${new Date().toISOString().split('T')[0]} **Approvals:** ${approved_count} / ${stakeholder_count}` }) ✅ Document marked as APPROVED! Review issue #{{review_issue.number}} closed. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ SIGN-OFF COMPLETE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ **Your Decision:** {{decision.emoji}} {{decision.text}} **Document:** {{title}} v{{version}} **Review Issue:** #{{review_issue.number}} Thank you for your review! 🙏 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ## Helper Functions ```javascript function extract_title(content) { const match = content.match(/^#\s+(PRD|Epic):\s*(.+)$/m); return match ? match[2].trim() : 'Untitled'; } function extract_version(content) { const match = content.match(/\*\*Version:\*\*\s*(\d+)/); return match ? match[1] : '1'; } function extract_stakeholders(content) { const field = content.match(/\|\s*Stakeholders\s*\|\s*(.+?)\s*\|/); if (!field) return []; return field[1] .split(/[,\s]+/) .filter(s => s.startsWith('@')) .map(s => s.replace('@', '')); } ``` ## Natural Language Triggers This workflow responds to: - "Sign off on [document]" - "Submit my sign-off" - "Approve the PRD" - "I approve [document]" - Menu trigger: `SO`