Smart git commit with Linear integration and conventional commits
Smart git commit with Linear integration and conventional commits. Auto-detects issue from branch, generates commit messages from changes, and respects CLAUDE.md commit rules across all hierarchy levels.
/plugin marketplace add duongdev/ccpm/plugin install ccpm@duongdev-ccpm-marketplace[issue-id] [message]Auto-detects issue from git branch and creates conventional commits linked to Linear issues.
ALL Linear operations MUST use the Task tool with ccpm:linear-operations subagent.
// ✅ CORRECT - Use Task tool with subagent
Task({ subagent_type: "ccpm:linear-operations", prompt: `operation: get_issue\nparams:\n issueId: X` })
// ❌ WRONG - Direct MCP call
mcp__agent-mcp-gateway__execute_tool({ server: "linear", ... })
Linear is internal tracking. Execute immediately - NEVER ask for approval.
Before executing any git commit operations, this command MUST check ALL CLAUDE.md files in the hierarchy:
~/.claude/CLAUDE.md - User global rulesCLAUDE.mdCLAUDE.md.claude/CLAUDE.md in any of the aboveRules from more specific (closer) files take precedence over global ones.
Example rules to respect:
[PROJ-123] message)--gpg-sign, --signoff)Example hierarchy:
~/.claude/CLAUDE.md # Global: --signoff required
~/work/CLAUDE.md # Org: conventional commits
~/work/my-project/CLAUDE.md # Project: GPG signing
~/work/my-project/apps/web/CLAUDE.md # Subproject: custom prefix
Result: Command merges all rules, with subproject rules winning on conflicts.
COMMIT Mode Philosophy:
# Auto-detect issue from git branch
/ccpm:commit
# Explicit issue ID
/ccpm:commit PSN-29
# With custom message
/ccpm:commit PSN-29 "Completed JWT validation"
# Message only (auto-detect issue)
/ccpm:commit "Fixed login bug"
# Full conventional format
/ccpm:commit "fix(auth): resolve login button handler"
CRITICAL: Check ALL CLAUDE.md files in the hierarchy!
Claude Code loads CLAUDE.md files from multiple locations (in order of precedence):
~/.claude/CLAUDE.md (user global)/CLAUDE.md./CLAUDE.md.claude/CLAUDE.md in any of aboveThe command must respect rules from ALL these files, with more specific (closer) files taking precedence.
// Find all CLAUDE.md files in hierarchy
const cwd = process.cwd();
const gitRoot = await Bash('git rev-parse --show-toplevel 2>/dev/null || echo ""');
const homeDir = process.env.HOME;
// Collect all potential CLAUDE.md locations (order: global → local)
const claudeMdPaths = [];
// 1. User global
claudeMdPaths.push(`${homeDir}/.claude/CLAUDE.md`);
// 2. Walk up from git root (or cwd if no git) to find parent CLAUDE.md files
let searchDir = gitRoot.trim() || cwd;
let prevDir = '';
while (searchDir !== prevDir && searchDir !== '/') {
claudeMdPaths.push(`${searchDir}/CLAUDE.md`);
claudeMdPaths.push(`${searchDir}/.claude/CLAUDE.md`);
prevDir = searchDir;
searchDir = path.dirname(searchDir);
}
// 3. Current working directory (if different from git root)
if (cwd !== gitRoot.trim()) {
claudeMdPaths.push(`${cwd}/CLAUDE.md`);
claudeMdPaths.push(`${cwd}/.claude/CLAUDE.md`);
}
// Read all existing CLAUDE.md files and merge rules
let commitRules = {
format: 'conventional',
requireScope: false,
requireSignoff: false,
customPrefix: null,
prependIssueId: true,
additionalFlags: [],
sources: [] // Track which files contributed rules
};
for (const claudePath of claudeMdPaths) {
const content = await Read(claudePath).catch(() => null);
if (!content) continue;
// Check for commit-related rules
const hasCommitRules = content.match(/commit|git/i);
if (!hasCommitRules) continue;
commitRules.sources.push(claudePath);
// Parse rules (later files override earlier ones)
if (content.includes('--signoff') || content.match(/\b-s\b.*commit/i)) {
commitRules.requireSignoff = true;
if (!commitRules.additionalFlags.includes('--signoff')) {
commitRules.additionalFlags.push('--signoff');
}
}
if (content.includes('--gpg-sign') || content.match(/\b-S\b.*commit/i)) {
if (!commitRules.additionalFlags.includes('--gpg-sign')) {
commitRules.additionalFlags.push('--gpg-sign');
}
}
// Custom format (last one wins)
const formatMatch = content.match(/commit.*format[:\s]+([^\n]+)/i);
if (formatMatch) {
commitRules.customFormat = formatMatch[1].trim();
}
// Scope requirements
if (content.match(/must include.*scope|scope.*required/i)) {
commitRules.requireScope = true;
}
// Issue ID requirements
if (content.match(/must include.*issue|issue.*required/i)) {
commitRules.prependIssueId = true;
}
// Custom commit message patterns (e.g., "[PROJ-123] message" format)
const prefixMatch = content.match(/commit.*prefix[:\s]+([^\n]+)/i);
if (prefixMatch) {
commitRules.customPrefix = prefixMatch[1].trim();
}
}
// Display what was found
if (commitRules.sources.length > 0) {
console.log('📋 Found CLAUDE.md commit rules from:');
commitRules.sources.forEach(src => console.log(` • ${src}`));
if (commitRules.additionalFlags.length > 0) {
console.log(` Flags: ${commitRules.additionalFlags.join(' ')}`);
}
if (commitRules.customFormat) {
console.log(` Format: ${commitRules.customFormat}`);
}
}
Learn from project's commit history for consistency:
The session-init hook caches commit patterns in /tmp/ccpm-session-*.json. Read these patterns to:
// Read session state (cached by session-init hook)
const sessionFile = process.env.CCPM_SESSION_FILE || findLatestSessionFile();
let commitPatterns = {
format: 'conventional',
usesScope: true,
confidence: 80,
topTypes: ['feat', 'fix', 'docs', 'chore'],
topScopes: []
};
if (sessionFile) {
const session = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
if (session.commitPatterns) {
commitPatterns = session.commitPatterns;
}
}
// If session doesn't have patterns, analyze on the fly
if (!commitPatterns.topTypes?.length) {
const recentCommits = await Bash('git log --oneline -30 --format="%s"');
const lines = recentCommits.split('\n').filter(l => l.trim());
// Detect format
const conventionalRegex = /^(\w+)(?:\(([^)]+)\))?!?:\s*(.+)$/;
const types = {};
const scopes = {};
for (const line of lines) {
const match = line.match(conventionalRegex);
if (match) {
const [, type, scope] = match;
types[type] = (types[type] || 0) + 1;
if (scope) scopes[scope] = (scopes[scope] || 0) + 1;
}
}
commitPatterns.topTypes = Object.entries(types)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([t]) => t);
commitPatterns.topScopes = Object.entries(scopes)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([s]) => s);
}
// Display detected patterns
if (commitPatterns.confidence >= 70) {
console.log(`📊 Commit patterns (${commitPatterns.confidence}% confidence):`);
console.log(` Types: ${commitPatterns.topTypes.join(', ')}`);
if (commitPatterns.topScopes.length > 0) {
console.log(` Scopes: ${commitPatterns.topScopes.join(', ')}`);
}
}
If claude-mem is available, check for past commit-related decisions:
// Check if claude-mem is available (detected by session-init)
const claudeMemAvailable = session?.claudeMemAvailable || false;
if (claudeMemAvailable) {
// Use the mem-search skill to find relevant commit decisions
// This happens automatically via subagent context injection
console.log('🧠 claude-mem: Checking past commit decisions...');
// Past decisions about commit format, scopes, or conventions
// will be injected into context by subagent-context-injector
}
const args = process.argv.slice(2);
let issueId = args[0];
let userMessage = args[1];
const ISSUE_ID_PATTERN = /^[A-Z]+-\d+$/;
// If first arg doesn't look like issue ID, treat as message
if (args[0] && !ISSUE_ID_PATTERN.test(args[0])) {
userMessage = args[0];
issueId = null;
}
// Auto-detect issue ID from git branch
if (!issueId) {
const branch = await Bash('git rev-parse --abbrev-ref HEAD');
const match = branch.match(/([A-Z]+-\d+)/);
if (match) {
issueId = match[1];
console.log(`📌 Detected issue from branch: ${issueId}`);
}
}
git status --porcelain
If no changes:
✅ No changes to commit (working tree clean)
# Get file changes with stats
git status --porcelain && echo "---" && git diff --stat HEAD
Parse output:
const changes = {
modified: [],
added: [],
deleted: [],
hasTests: false,
hasSource: false,
hasDocs: false
};
// Parse git status
lines.forEach(line => {
const [status, file] = line.trim().split(/\s+/);
if (status === 'M') changes.modified.push(file);
else if (status === 'A' || status === '??') changes.added.push(file);
else if (status === 'D') changes.deleted.push(file);
// Detect file types
if (file.includes('test') || file.includes('spec')) changes.hasTests = true;
if (file.includes('src/') || file.includes('lib/')) changes.hasSource = true;
if (file.match(/\.(md|txt)$/)) changes.hasDocs = true;
});
// Show summary
console.log('\n📊 Changes to commit:\n');
console.log(` Modified: ${changes.modified.length}`);
console.log(` Added: ${changes.added.length}`);
console.log(` Deleted: ${changes.deleted.length}\n`);
If issue ID detected:
Use the Task tool:
Invoke ccpm:linear-operations:
operation: get_issue
params:
issueId: "{issue ID}"
context:
cache: true
command: "commit"
Extract:
console.log(`📋 Issue: ${issue.identifier} - ${issue.title}`);
console.log(`📊 Status: ${issue.state.name}\n`);
function suggestCommitType(changes, issueLabels, patterns) {
// From Linear issue labels (highest priority)
if (issueLabels.includes('bug') || issueLabels.includes('fix')) return 'fix';
if (issueLabels.includes('feature')) return 'feat';
// From file analysis
const files = [...changes.modified, ...changes.added, ...changes.deleted];
const hasCi = files.some(f => f.includes('.github/') || f.includes('ci') || f.includes('workflow'));
const hasTests = files.some(f => f.includes('test') || f.includes('spec'));
const hasDocs = files.some(f => f.match(/\.(md|txt|rst)$/) || f.includes('docs/'));
const hasConfig = files.some(f => f.match(/\.(json|yml|yaml|toml)$/) && !f.includes('package'));
// Check for specific patterns that match project history
if (hasCi && patterns.topTypes?.includes('ci')) return 'ci';
if (hasTests && !changes.hasSource && patterns.topTypes?.includes('test')) return 'test';
if (hasDocs && !changes.hasSource) return 'docs';
// From file analysis defaults
if (changes.hasSource && changes.added.length > 0) return 'feat';
if (hasConfig && !changes.hasSource) return 'chore';
// Default for modifications
if (changes.modified.length > 0 && changes.hasSource) return 'feat';
// Fall back to most common type in project, or 'chore'
return patterns.topTypes?.[0] || 'chore';
}
function suggestScope(changes, patterns) {
const files = [...changes.modified, ...changes.added, ...changes.deleted];
// Extract first-level directories
const directories = files
.map(f => f.split('/')[0])
.filter(d => d && !d.startsWith('.'));
// Count occurrences
const dirCounts = {};
for (const dir of directories) {
dirCounts[dir] = (dirCounts[dir] || 0) + 1;
}
// Find most common directory
const topDir = Object.entries(dirCounts)
.sort((a, b) => b[1] - a[1])
.map(([dir]) => dir)[0];
// Check if it matches existing scopes from history
if (topDir && patterns.topScopes?.includes(topDir)) {
return topDir;
}
// Check for common scope patterns
const commonScopes = ['hooks', 'commands', 'agents', 'helpers', 'skills', 'scripts', 'ci', 'docs'];
for (const scope of commonScopes) {
if (files.some(f => f.includes(`${scope}/`))) {
return scope;
}
}
return topDir || null;
}
const commitType = suggestCommitType(changes, issue?.labels || [], commitPatterns);
const suggestedScope = suggestScope(changes, commitPatterns);
let commitMessage;
// Determine scope: prefer detected scope from files, fall back to issueId
const scope = suggestedScope || (commitPatterns.usesScope ? issueId : null);
if (userMessage) {
// Check if already in conventional format
const conventionalMatch = userMessage.match(/^(\w+)(\([\w-]+\))?: (.+)$/);
if (conventionalMatch) {
// Use as-is
commitMessage = userMessage;
} else {
// Add conventional format with detected scope
commitMessage = `${commitType}${scope ? `(${scope})` : ''}: ${userMessage}`;
}
} else {
// Auto-generate from issue title or file changes
let description;
if (issue?.title) {
// Use issue title (lowercase first letter for conventional format)
description = issue.title.charAt(0).toLowerCase() + issue.title.slice(1);
} else {
// Generate from changes - be more descriptive
const files = [...changes.modified, ...changes.added];
if (changes.added.length > 0 && changes.hasSource) {
const mainFile = changes.added[0].split('/').pop().replace(/\.(ts|js|tsx|jsx)$/, '');
description = `add ${mainFile} module`;
} else if (suggestedScope && changes.modified.length > 0) {
// Use scope context for better description
description = `update ${suggestedScope} implementation`;
} else if (changes.modified.length > 0 && changes.hasSource) {
description = `update implementation`;
} else if (changes.hasDocs) {
description = `update documentation`;
} else {
description = `update ${changes.modified.length} file(s)`;
}
}
commitMessage = `${commitType}${scope ? `(${scope})` : ''}: ${description}`;
}
// Show pattern confidence if available
if (commitPatterns.confidence && commitPatterns.confidence < 70) {
console.log(`⚠️ Low pattern confidence (${commitPatterns.confidence}%) - commit format may vary`);
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 Proposed Commit Message
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
${commitMessage}
${issueId ? `🔗 Linked to: ${issueId}` : ''}
📊 Changes:
• Modified: ${changes.modified.length}
• Added: ${changes.added.length}
• Deleted: ${changes.deleted.length}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Use AskUserQuestion:
{
questions: [{
question: "Proceed with this commit?",
header: "Confirm",
multiSelect: false,
options: [
{
label: "Yes, commit",
description: "Create commit with this message"
},
{
label: "Edit message",
description: "Modify the commit message first"
},
{
label: "Cancel",
description: "Don't commit, go back"
}
]
}]
}
If "Yes, commit":
# Stage all changes
git add .
# Build commit command with flags from local CLAUDE.md rules
const commitFlags = commitRules.additionalFlags.join(' ');
# Create commit (use heredoc for proper formatting)
# Include any additional flags from local CLAUDE.md (e.g., --signoff, --gpg-sign)
git commit ${commitFlags} -m "$(cat <<'EOF'
${commitMessage}
EOF
)"
# Examples:
# No extra flags: git commit -m "feat(PSN-29): add auth"
# With signoff: git commit --signoff -m "feat(PSN-29): add auth"
# With GPG: git commit --gpg-sign -m "feat(PSN-29): add auth"
# Both: git commit --signoff --gpg-sign -m "feat(PSN-29): add auth"
Display success:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Commit Created Successfully!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📝 Commit: ${commitHash} - ${commitMessage}
🎯 Next Actions
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ⭐ Sync to Linear /ccpm:sync
2. 🚀 Push to remote git push
3. 🔄 Continue work /ccpm:work
4. ✅ Run verification /ccpm:verify
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
If "Edit message":
Display prompt:
Please provide your commit message (conventional format):
Format: <type>(<scope>): <description>
Examples:
• feat(auth): add JWT token validation
• fix(PSN-27): resolve login button handler
• docs: update API documentation
Your message:
Wait for user input, then repeat from Step 7.
If "Cancel":
⏸️ Commit cancelled. Changes remain staged.
✅ No changes to commit (working tree clean)
Run /ccpm:work to continue working or make changes first.
❌ Not a git repository
Initialize git first:
git init
⚠️ Could not fetch issue ${issueId} from Linear
Proceeding without issue context...
# Branch: feature/PSN-29-add-auth
# Made changes to src/auth/*.ts
/ccpm:commit
# Output:
# 📌 Detected issue from branch: PSN-29
# 📋 Issue: PSN-29 - Add user authentication
#
# 💬 Proposed: feat(PSN-29): add user authentication
#
# [Confirmation]
#
# ✅ Commit created!
# Branch: feature/PSN-29-add-auth
/ccpm:commit "Completed JWT validation"
# Output:
# 📌 Detected issue from branch: PSN-29
#
# 💬 Proposed: feat(PSN-29): Completed JWT validation
#
# ✅ Commit created!
/ccpm:commit PSN-29 "Fixed login bug"
# Output:
# 💬 Proposed: fix(PSN-29): Fixed login bug
#
# ✅ Commit created!
/ccpm:commit "fix(auth): resolve login button handler"
# Output:
# 💬 Proposed: fix(auth): resolve login button handler
#
# ✅ Commit created! (used as-is)
/ccpm:commit "update documentation"
# Output:
# 💬 Proposed: docs: update documentation
#
# ✅ Commit created!
Common types:
feat: New featurefix: Bug fixdocs: Documentation onlystyle: Formatting, whitespacerefactor: Code restructuringtest: Adding/updating testschore: Maintenance, dependenciesFormat:
<type>(<scope>): <description>
[optional body]
[optional footer]
Examples:
feat(auth): add JWT token validationfix(PSN-27): resolve race condition in logindocs: update API reference for auth endpointschore(deps): upgrade react to v19/ccpm:commit to save changes to git/ccpm:sync to update Lineargit log/ccpm:done to create PR