| 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 changesci:- Unless CI/CD improvements- Merge commits - Don't create events for merges
- Revert commits - Could create
bug_fixedevent with note
Error Handling
If memory update fails:
- Log warning to stderr
- Don't block the commit
- 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 skilldev-memory-briefing- Reads events created by this skillproject-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
- Write descriptive commit messages - Better messages = better memory
- Use conventional commit format - Helps with type inference
- Link issues in commits - Use
Fixes #123format - Mention epic in commit - Include
epic-namein message body - Add TODO/FIXME - Extracted as next steps automatically
- One logical change per commit - Easier to categorize
Troubleshooting
Memory not updating?
- Check
.claude/config.yml- ensuredevMemory.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.jsonlpermissions
Wrong event types?
- Use conventional commit prefixes:
feat:,fix:, etc. - Review event type inference logic above
- Manually edit
events.jsonlif 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.jsonPostToolUse hooks - Remove duplicates manually (edit JSONL file)