issue-compliance

star 0

Enforce that every commit is linked to a GitHub issue. Detects related issues, creates one if none exists, and blocks non-compliant commits. Use when committing, checking commit compliance, enforcing issue traceability, or validating commit-issue linkage.

Norman-Norman-Norman By Norman-Norman-Norman schedule Updated 3/27/2026

name: issue-compliance description: 'Enforce that every commit is linked to a GitHub issue. Detects related issues, creates one if none exists, and blocks non-compliant commits. Use when committing, checking commit compliance, enforcing issue traceability, or validating commit-issue linkage.'

Issue Compliance — Every Commit Must Have an Issue

This skill enforces a strict policy: no commit may be pushed without a linked GitHub issue. It detects related issues from branch names, changed files, and recent history. If no issue exists, it creates one automatically. If the user declines issue creation, the commit is blocked and the user is informed of the compliance requirement.

This skill is designed to run before the commit is finalized. It can be invoked standalone or composed with the commit-and-push skill (insert between staging and committing).


Prerequisites

  1. Git is initialized with at least one remote configured.
  2. GitHub MCP Server tools are available. The procedure needs mcp_github or mcp_github2 prefixed tools for issue search and creation.
  3. Repository identity is known. Owner and repo name must be resolvable from the git remote or provided by the user. Default upstream: Norman-Norman-Norman/future-of-pm.

Procedure

Step 1: Identify the Repository

Tool: run_in_terminal

Determine the GitHub owner and repo from the git remote.

Action:

run_in_terminal with:
  command: "git remote get-url upstream 2>$null || git remote get-url origin"
  explanation: "Getting the remote URL to identify the GitHub repository."
  goal: "Identify repo owner and name"
  isBackground: false
  timeout: 5000

Decision logic:

  • Parse the remote URL to extract <owner> and <repo>.
  • If no remote is found, ask the user for the owner/repo and stop if not provided.
  • Record OWNER and REPO for use in subsequent steps.

Step 2: Gather Change Context

Collect signals that may reference existing GitHub issues.

2a. Get the current branch name

Tool: run_in_terminal

run_in_terminal with:
  command: "git branch --show-current"
  explanation: "Getting current branch name to check for issue references."
  goal: "Identify branch"
  isBackground: false
  timeout: 5000

Look for issue number patterns: feature/123-description, fix/456, issue-789, gh-42.

2b. Get the list of changed files

Tool: get_changed_files or run_in_terminal

run_in_terminal with:
  command: "git diff --name-only HEAD"
  explanation: "Listing changed files to search for issue references."
  goal: "Get changed files"
  isBackground: false
  timeout: 5000

If no HEAD exists yet (initial commit), use git diff --cached --name-only.

2c. Search changed files for issue references

Tool: grep_search

grep_search with:
  query: "#[0-9]+"
  isRegexp: true
  includePattern: "<changed files from 2b>"

Extract any #123, GH-456, Closes #789, Refs #42 patterns.

2d. Check recent commit messages

Tool: run_in_terminal

run_in_terminal with:
  command: "git log --oneline -10"
  explanation: "Checking recent commits for issue references."
  goal: "Find related issues in history"
  isBackground: false
  timeout: 5000

Record: Collect all unique issue numbers found across 2a–2d into CANDIDATE_ISSUES.


Step 3: Verify Candidate Issues on GitHub

Tool: mcp_github_search_issues or mcp_github2_search_issues

For each issue number in CANDIDATE_ISSUES, verify it exists and is open on the target repository.

Action:

For each candidate issue number N:
  Call search_issues with:
    query: "repo:<OWNER>/<REPO> is:issue <N>"
    perPage: 5

Decision logic:

  • If at least one verified open issue is found, record it as LINKED_ISSUE and proceed to Step 6 (compliance confirmed).
  • If candidate issues exist but are all closed, inform the user and ask: "Issue #N is closed. Should I reference the closed issue, reopen it, or create a new one?"
  • If no candidates were found at all, proceed to Step 4.

Step 4: Search for Related Issues by Content

Tool: mcp_github_search_issues or mcp_github2_search_issues

If no issue number was detected, search by keywords derived from the changes.

Action:

  1. Summarize the changes into 3–5 keywords based on the changed files, branch name, and diff content.
  2. Search GitHub:
Call search_issues with:
  query: "repo:<OWNER>/<REPO> is:issue is:open <keywords>"
  perPage: 10

Decision logic:

  • If a matching open issue is found that clearly relates to the current changes, present it to the user: "I found issue #N: ''. Should I link your commit to this issue?"<ul> <li>If the user confirms, record it as <code>LINKED_ISSUE</code> and proceed to <strong>Step 6</strong>.</li> <li>If the user declines, proceed to <strong>Step 5</strong>.</li> </ul> </li> <li>If no matching issues are found, proceed to <strong>Step 5</strong>.</li> </ul> <hr> <h3>Step 5: Create a New Issue</h3> <p><strong>⚠️ COMPLIANCE GATE — No issue exists for this commit. The user must either create one or acknowledge non-compliance.</strong></p> <p>Inform the user:</p> <blockquote> <p><strong>⚠️ Issue Compliance Check Failed</strong></p> <p>No GitHub issue is linked to this commit. Repository policy requires every commit to reference an issue for traceability.</p> <p>I can create an issue automatically based on your changes. Shall I proceed?</p> </blockquote> <p><strong>If the user agrees (or does not object):</strong></p> <h4>5a. Compose the Issue</h4> <p>Build an issue from the change context gathered in Step 2:</p> <ul> <li><strong>Title:</strong> Derive from the branch name or a summary of the changes. Use a category prefix: <code>[Feature]</code>, <code>[Bug]</code>, <code>[Chore]</code>, <code>[Docs]</code>, <code>[Refactor]</code>.</li> <li><strong>Body:</strong> Use the Task/Chore template:</li> </ul> <pre><code class="language-markdown">## Summary <1–2 sentences describing the work being committed.> ## Details <What was changed and why, based on the diff and changed files.> ## Checklist - [x] Implementation complete - [ ] Tests verified - [ ] Documentation updated (if applicable) ## Notes This issue was auto-created by the issue-compliance skill to maintain commit traceability. </code></pre> <ul> <li><strong>Labels:</strong> Use <code>compliance</code> if it exists; otherwise omit.</li> </ul> <h4>5b. Check for Duplicates</h4> <p><strong>Tool:</strong> <code>mcp_github_search_issues</code> or <code>mcp_github2_search_issues</code></p> <pre><code>Call search_issues with: query: "repo:<OWNER>/<REPO> is:issue <title keywords>" perPage: 5 </code></pre> <p>If a duplicate exists, link to it instead of creating a new one.</p> <h4>5c. Create the Issue</h4> <p><strong>Tool:</strong> <code>mcp_github_issue_write</code> or <code>mcp_github2_issue_write</code></p> <pre><code>Call issue_write with: method: "create" owner: "<OWNER>" repo: "<REPO>" title: "<composed title>" body: "<composed body>" </code></pre> <p>Record the returned issue number as <code>LINKED_ISSUE</code>.</p> <p><strong>If the user declines issue creation:</strong></p> <p>Report non-compliance and block:</p> <blockquote> <p><strong>❌ Commit Blocked — Non-Compliant</strong></p> <p>Repository policy requires every commit to reference a GitHub issue. No issue was found and you declined to create one.</p> <p>To proceed, either:</p> <ol> <li>Create an issue manually and re-run the commit.</li> <li>Provide an existing issue number to link to.</li> <li>Re-run this check and allow automatic issue creation.</li> </ol> </blockquote> <p><strong>STOP. Do not proceed to Step 6. Do not allow the commit.</strong></p> <hr> <h3>Step 6: Report Compliance and Return Issue Reference</h3> <p>The commit has a linked issue. Report the compliance status to the user and return the issue reference for use in the commit message footer.</p> <p><strong>Output to user:</strong></p> <blockquote> <p><strong>✅ Issue Compliance — Passed</strong></p> <p>Commit is linked to issue <strong>#<LINKED_ISSUE></strong>: <em><issue title></em></p> <p>The following footer should be included in the commit message:</p> <pre><code>Refs #<LINKED_ISSUE> </code></pre> <p><em>(Use <code>Closes #<LINKED_ISSUE></code> if this commit fully resolves the issue.)</em></p> </blockquote> <p><strong>Return values</strong> (for composability with other skills):</p> <table> <thead> <tr> <th>Variable</th> <th>Value</th> <th>Description</th> </tr> </thead> <tbody><tr> <td><code>LINKED_ISSUE</code></td> <td><code><number></code></td> <td>The GitHub issue number</td> </tr> <tr> <td><code>ISSUE_TITLE</code></td> <td><code><string></code></td> <td>The issue title</td> </tr> <tr> <td><code>COMMIT_FOOTER</code></td> <td><code>Refs #<number></code></td> <td>Ready-to-use commit footer line</td> </tr> <tr> <td><code>COMPLIANCE_STATUS</code></td> <td><code>PASSED</code> or <code>BLOCKED</code></td> <td>Whether the commit may proceed</td> </tr> </tbody></table> <hr> <h2>Integration with commit-and-push</h2> <p>This skill is designed to compose with the existing <code>commit-and-push</code> skill. Insert it <strong>between Step 4 (Stage Changes) and Step 5 (Identify Related Issues)</strong> of commit-and-push:</p> <ol> <li>After staging, call <strong>issue-compliance</strong> Steps 1–6.</li> <li>If <code>COMPLIANCE_STATUS = PASSED</code>, use the returned <code>COMMIT_FOOTER</code> in the commit message footer.</li> <li>If <code>COMPLIANCE_STATUS = BLOCKED</code>, abort the commit (same as a test failure).</li> </ol> <p>The commit-and-push skill's own Step 5 (Identify Related Issues) becomes redundant when issue-compliance is active — issue-compliance is the authoritative source for issue linkage.</p> <hr> <h2>Edge Cases</h2> <table> <thead> <tr> <th>Scenario</th> <th>Behavior</th> </tr> </thead> <tbody><tr> <td>Multiple issues found</td> <td>Present all candidates; let the user choose which to link.</td> </tr> <tr> <td>Branch is <code>main</code> or <code>master</code></td> <td>Still enforce compliance — direct commits to default branch especially need traceability.</td> </tr> <tr> <td>Merge commits</td> <td>Skip compliance check for merge commits (detected via <code>git log -1 --format=%P</code> having two parents).</td> </tr> <tr> <td>Amend commits</td> <td>Re-run compliance on the amended content. The original issue link carries forward.</td> </tr> <tr> <td>No internet / GitHub API failure</td> <td>Warn the user that compliance could not be verified. Do NOT silently allow the commit. Ask the user to provide an issue number manually.</td> </tr> <tr> <td>User provides issue number directly</td> <td>Accept it, verify it exists (Step 3), and skip to Step 6.</td> </tr> </tbody></table> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/Norman-Norman-Norman/future-of-pm --skill issue-compliance </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>0</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>0</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/244782354?v=4" alt="Norman-Norman-Norman" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>Norman-Norman-Norman</span> <a href="/?creator=Norman-Norman-Norman" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>