Claude Code Plugins

Community-maintained marketplace

Feedback

Update development memory (events.jsonl) based on commit metadata and diff analysis. Automatically tracks features, fixes, refactorings, and decisions.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name dev-memory-update
description Update development memory (events.jsonl) based on commit metadata and diff analysis. Automatically tracks features, fixes, refactorings, and decisions.

Dev Memory Update Skill

Automatically extract and store development events from git commits into ai_memory/events.jsonl.

Purpose

This skill analyzes commit metadata (message, diff, branch, timestamp) and creates structured event records that build an automated development timeline for the project.

When to Use

Automatic Invocation

  • Post-commit hook: Runs automatically after every git commit
  • Batch mode: Process multiple commits at once

Manual Invocation

  • When reviewing past commits to backfill memory
  • When commit hook was disabled and you want to catch up
  • When you want to add events for non-commit activities (meetings, decisions)

Input Requirements

Required

{
  repo: string;              // Repository name (not full path)
  branch: string;            // Current git branch
  commit_hash: string;       // Git SHA (short or long form)
  commit_message: string;    // Full commit message
  commit_timestamp: string;  // ISO 8601 timestamp
}

Optional

{
  diff_summary?: string;     // Output of `git diff --stat`
  files_changed?: number;    // Count of modified files
  related_issues?: string[]; // Extracted issue numbers
  related_prs?: string[];    // Extracted PR numbers
  author?: string;           // Commit author
}

Behavior

Step 1: Initialize Memory Directory

# Ensure ai_memory/ exists
MEMORY_DIR="ai_memory"
mkdir -p "$MEMORY_DIR"

# Create .gitkeep if first time
if [ ! -f "$MEMORY_DIR/.gitkeep" ]; then
  echo "# AI Development Memory" > "$MEMORY_DIR/.gitkeep"
  echo "# Auto-generated by dev-memory-update skill" >> "$MEMORY_DIR/.gitkeep"
fi

Step 2: Extract Metadata

// Parse commit message for patterns
const parseCommitMessage = (message: string) => {
  const lines = message.split('\n');
  const subject = lines[0];
  const body = lines.slice(1).join('\n').trim();

  // Extract type from conventional commit format
  const typeMatch = subject.match(/^(feat|fix|refactor|test|docs|chore|perf|style|build|ci)(\(.+\))?:/);
  const type = typeMatch ? typeMatch[1] : null;

  // Extract issue/PR numbers
  const issueMatches = message.match(/#(\d+)/g) || [];
  const issues = issueMatches.map(m => m);

  const prMatches = message.match(/\bPR #(\d+)\b/gi) || [];
  const prs = prMatches.map(m => '#' + m.match(/\d+/)[0]);

  // Extract epic ID if present
  const epicMatch = message.match(/epic[:-]\s*([a-z0-9-]+)/i);
  const epicId = epicMatch ? epicMatch[1] : null;

  return { subject, body, type, issues, prs, epicId };
};

Step 3: Infer Event Type

Map commit type to event type:

Commit Type Event Type Notes
feat feature_implemented New functionality
fix bug_fixed Bug resolution
refactor refactor Code restructuring
test test_added Test coverage
docs docs_updated Documentation
perf feature_implemented Performance improvement treated as feature
build, ci, chore No event Skip unless significant
No type prefix feature_implemented Default assumption

Decision logic:

const inferEventType = (commitType: string | null, message: string): EventType => {
  if (commitType === 'feat' || commitType === 'perf') return 'feature_implemented';
  if (commitType === 'fix') return 'bug_fixed';
  if (commitType === 'refactor') return 'refactor';
  if (commitType === 'test') return 'test_added';
  if (commitType === 'docs') return 'docs_updated';

  // Check for decision keywords in message
  if (/\b(decided|chose|selected|adopted)\b/i.test(message)) {
    return 'decision';
  }

  // Check for breaking change keywords
  if (/BREAKING CHANGE|breaking:/i.test(message)) {
    return 'breaking_change';
  }

  // Default to feature
  return 'feature_implemented';
};

Step 4: Generate Event ID

Performance Note: For large files, this implementation reads the entire file. Consider optimizing by reading backwards (using tac command or a reverse-reading library) to find the last event ID more efficiently:

# Alternative: Read last matching event backwards (shell example)
LAST_EVENT=$(tac ai_memory/events.jsonl 2>/dev/null | grep -m1 "\"id\":\"evt-$TODAY_DATE" | jq -r '.id')
const generateEventId = (): string => {
  const date = new Date().toISOString().split('T')[0].replace(/-/g, '');

  // Read existing events to find next sequence number
  const eventsFile = 'ai_memory/events.jsonl';
  const todayPrefix = `evt-${date}`;

  let maxSeq = 0;
  if (fs.existsSync(eventsFile)) {
    const lines = fs.readFileSync(eventsFile, 'utf-8').split('\n').filter(l => l.trim());
    for (const line of lines) {
      try {
        const event = JSON.parse(line);
        if (event.id?.startsWith(todayPrefix)) {
          const seqMatch = event.id.match(/-(\d{3})$/);
          if (seqMatch) {
            maxSeq = Math.max(maxSeq, parseInt(seqMatch[1], 10));
          }
        }
      } catch (e) {
        // Skip malformed lines
      }
    }
  }

  const nextSeq = (maxSeq + 1).toString().padStart(3, '0');
  return `${todayPrefix}-${nextSeq}`;
};

Step 5: Extract Open Questions and Next Steps

const extractActions = (message: string) => {
  const openQuestions: string[] = [];
  const nextSteps: string[] = [];

  const lines = message.split('\n');

  for (const line of lines) {
    const trimmed = line.trim();

    // Look for questions
    if (trimmed.endsWith('?') && trimmed.length > 10) {
      openQuestions.push(trimmed);
    }

    // Look for TODO, FIXME, action items
    if (/^(TODO|FIXME|Next|Action|Follow-up):/i.test(trimmed)) {
      const action = trimmed.replace(/^[^:]+:\s*/, '');
      nextSteps.push(action);
    }

    // Look for bullet points that look like next steps
    if (/^[-*]\s+(Add|Create|Update|Fix|Test|Implement)/i.test(trimmed)) {
      nextSteps.push(trimmed.replace(/^[-*]\s+/, ''));
    }
  }

  return { openQuestions, nextSteps };
};

Step 6: Create Event Object

const createEvent = (input: CommitInput): DevEvent => {
  const { subject, body, type, issues, prs, epicId } = parseCommitMessage(input.commit_message);
  const eventType = inferEventType(type, input.commit_message);
  const { openQuestions, nextSteps } = extractActions(input.commit_message);

  const event: DevEvent = {
    id: generateEventId(),
    timestamp: input.commit_timestamp,
    repo: input.repo,
    branch: input.branch,
    type: eventType,
    title: subject.substring(0, 100), // Truncate if needed
    summary: body.substring(0, 500) || subject,
    commit_hash: input.commit_hash.substring(0, 7), // Short SHA
  };

  // Add optional fields only if present
  if (issues.length > 0) event.related_issues = issues;
  if (prs.length > 0) event.related_prs = prs;
  if (epicId) event.epic_id = epicId;
  if (openQuestions.length > 0) event.open_questions = openQuestions;
  if (nextSteps.length > 0) event.next_steps = nextSteps;
  if (input.files_changed) event.files_changed = input.files_changed;

  // Set confidence based on how much we could extract
  event.confidence = type && issues.length > 0 ? 'high' :
                     type ? 'medium' : 'low';

  return event;
};

Step 7: Append to events.jsonl

# Write event as single-line JSON
echo "$EVENT_JSON" >> ai_memory/events.jsonl

IMPORTANT:

  • No trailing comma
  • No pretty-printing (single line)
  • UTF-8 encoding
  • Append mode (>> not >)

Step 8: Update or Create Session

Performance Note: This implementation reads the entire sessions file. For better performance, consider reading backwards to find the most recent session for the current branch and day:

# Alternative: Read sessions backwards (shell example)
LAST_SESSION=$(tac ai_memory/sessions.jsonl 2>/dev/null | grep -m1 "\"branch\":\"$BRANCH\"" | jq -r 'select(.timestamp_start | startswith("'$TODAY'"))')
const updateSession = (event: DevEvent, commitInput: CommitInput) => {
  const sessionFile = 'ai_memory/sessions.jsonl';
  const sessionId = `sess-${event.timestamp.split('T')[0].replace(/-/g, '')}-${event.timestamp.split('T')[1].substring(0, 6).replace(/:/g, '')}`;

  // Try to find existing session for today on this branch
  let existingSession: DevSession | null = null;
  const today = event.timestamp.split('T')[0];

  if (fs.existsSync(sessionFile)) {
    const lines = fs.readFileSync(sessionFile, 'utf-8').split('\n').filter(l => l.trim());
    for (const line of lines) {
      try {
        const session = JSON.parse(line);
        if (session.timestamp_start.startswith(today) && session.branch === event.branch) {
          existingSession = session;
          break;
        }
      } catch (e) {
        // Skip malformed lines
      }
    }
  }

  if (existingSession) {
    // Update existing session
    existingSession.timestamp_end = event.timestamp;
    existingSession.created_events.push(event.id);
    if (event.commit_hash) {
      existingSession.commits = existingSession.commits || [];
      existingSession.commits.push(event.commit_hash);
    }

    // Re-write updated session (append-only: add new version, old one ignored when reading latest)
    fs.appendFileSync(sessionFile, JSON.stringify(existingSession) + '\n', 'utf-8');
  } else {
    // Create new session
    const newSession: DevSession = {
      id: sessionId,
      timestamp_start: event.timestamp,
      timestamp_end: event.timestamp,
      repo: event.repo,
      branch: event.branch,
      agent: 'Claude Code',
      summary: `Working on ${event.title}`,
      created_events: [event.id],
      commits: event.commit_hash ? [event.commit_hash] : [],
    };

    fs.appendFileSync(sessionFile, JSON.stringify(newSession) + '\n', 'utf-8');
  }
};

Example Usage

From Post-Commit Hook

#!/bin/bash
# .claude/hooks/post-commit-memory.sh

# Extract commit info
REPO=$(basename "$(git rev-parse --show-toplevel)")
BRANCH=$(git branch --show-current)
COMMIT_HASH=$(git rev-parse HEAD)
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
COMMIT_TIMESTAMP=$(git log -1 --format=%aI)
FILES_CHANGED=$(git diff-tree --no-commit-id --name-only -r HEAD | wc -l)

# Call dev-memory-update skill (Claude invokes this)
echo "→ Updating dev memory for commit ${COMMIT_HASH:0:7}..."

# Claude would execute this skill with the extracted data

Manual Backfill

# Process last 10 commits
git log -10 --pretty=format:'%H|%aI|%s' | while IFS='|' read hash timestamp subject; do
  # Extract and process each commit
  echo "Processing: $subject"
done

Constraints

Max Events Per Commit

Default: 3 events maximum per commit

Rationale: Most commits should represent 1 logical change. If more than 3 events, commit is likely too large.

Override: Can be configured in .claude/config.yml:

devMemory:
  maxEventsPerCommit: 3

Skipped Commits

Skip these commit types:

  • chore: - Unless significant (dependency upgrades)
  • build: - Unless build system changes
  • ci: - Unless CI/CD improvements
  • Merge commits - Don't create events for merges
  • Revert commits - Could create bug_fixed event with note

Error Handling

If memory update fails:

  1. Log warning to stderr
  2. Don't block the commit
  3. Continue gracefully

Never:

  • Fail the commit because memory update failed
  • Throw errors that stop the workflow
  • Corrupt existing JSONL files

Output

Success

{
  "status": "success",
  "events_created": 1,
  "event_ids": ["evt-20251210-001"],
  "session_updated": true,
  "session_id": "sess-20251210-210500"
}

Skipped

{
  "status": "skipped",
  "reason": "Commit type 'chore' not significant enough",
  "commit_type": "chore"
}

Error

{
  "status": "error",
  "error": "Failed to parse commit message",
  "graceful": true
}

Integration with Workflows

Conductor Workflow

  • After Phase 4, Step 2 (commit-with-validation)
  • Post-commit hook runs automatically
  • Memory updated with feature/fix details

Manual Commit Workflow

  • Hook runs on every commit
  • No user intervention needed
  • Silent unless errors

Related Skills

  • commit-with-validation - Creates the commit that triggers this skill
  • dev-memory-briefing - Reads events created by this skill
  • project-memory - Complementary long-term memory (MCP-based)

Configuration

In .claude/config.yml:

devMemory:
  enabled: true
  autoUpdateOnCommit: true
  maxEventsPerCommit: 3
  skipCommitTypes: ['chore', 'build', 'ci']
  confidenceThreshold: 'low'  # Include all events, even low confidence

Best Practices

  1. Write descriptive commit messages - Better messages = better memory
  2. Use conventional commit format - Helps with type inference
  3. Link issues in commits - Use Fixes #123 format
  4. Mention epic in commit - Include epic-name in message body
  5. Add TODO/FIXME - Extracted as next steps automatically
  6. One logical change per commit - Easier to categorize

Troubleshooting

Memory not updating?

  • Check .claude/config.yml - ensure devMemory.enabled: true
  • Check hook is configured in .claude/settings.json
  • Run ls -la .claude/hooks/post-commit-memory.sh - ensure executable
  • Check ai_memory/events.jsonl permissions

Wrong event types?

  • Use conventional commit prefixes: feat:, fix:, etc.
  • Review event type inference logic above
  • Manually edit events.jsonl if needed (it's just JSON)

Events.jsonl growing too large?

  • Check file size: wc -l ai_memory/events.jsonl
  • If > 10,000 lines, consider archiving old events
  • Compress: gzip ai_memory/events-2024.jsonl

Duplicate events?

  • Check if hook running multiple times
  • Review .claude/settings.json PostToolUse hooks
  • Remove duplicates manually (edit JSONL file)