| 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;
branch: string;
commit_hash: string;
commit_message: string;
commit_timestamp: string;
}
Optional
{
diff_summary?: string;
files_changed?: number;
related_issues?: string[];
related_prs?: string[];
author?: string;
}
Behavior
Step 1: Initialize Memory Directory
MEMORY_DIR="ai_memory"
mkdir -p "$MEMORY_DIR"
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
const parseCommitMessage = (message: string) => {
const lines = message.split('\n');
const subject = lines[0];
const body = lines.slice(1).join('\n').trim();
const typeMatch = subject.match(/^(feat|fix|refactor|test|docs|chore|perf|style|build|ci)(\(.+\))?:/);
const type = typeMatch ? typeMatch[1] : null;
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]);
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';
if (/\b(decided|chose|selected|adopted)\b/i.test(message)) {
return 'decision';
}
if (/BREAKING CHANGE|breaking:/i.test(message)) {
return 'breaking_change';
}
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:
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, '');
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) {
}
}
}
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();
if (trimmed.endsWith('?') && trimmed.length > 10) {
openQuestions.push(trimmed);
}
if (/^(TODO|FIXME|Next|Action|Follow-up):/i.test(trimmed)) {
const action = trimmed.replace(/^[^:]+:\s*/, '');
nextSteps.push(action);
}
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),
summary: body.substring(0, 500) || subject,
commit_hash: input.commit_hash.substring(0, 7),
};
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;
event.confidence = type && issues.length > 0 ? 'high' :
type ? 'medium' : 'low';
return event;
};
Step 7: Append to events.jsonl
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:
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, '')}`;
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) {
}
}
}
if (existingSession) {
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);
}
fs.appendFileSync(sessionFile, JSON.stringify(existingSession) + '\n', 'utf-8');
} else {
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
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)
echo "→ Updating dev memory for commit ${COMMIT_HASH:0:7}..."
Manual Backfill
git log -10 --pretty=format:'%H|%aI|%s' | while IFS='|' read hash timestamp subject; do
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:
- 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 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'
Best Practices
- Write descriptive commit messages - Better messages = better memory
- Use conventional commit format - Helps with type inference
- Link issues in commits - Use
Fixes #123 format
- Mention epic in commit - Include
epic-name in 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 - 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)