New: interactive configurator at cc.bruniaux.com/context/ that generates a personalized CLAUDE.md starter kit based on team size, stack, and current setup. Multi-step flow (profile, current state, stack, results) with maturity scoring (Level 1-5), copy-to-clipboard artifacts, localStorage persistence. Guide content: - guide/core/context-engineering.md (1,188 lines, 8 sections): context budget, 150-instruction ceiling, modular architecture, team assembly, ACE pipeline, quality measurement, context reduction techniques - examples/context-engineering/ (10 templates): assembler.ts, profile-template.yaml, skeleton-template.md, canary-check.sh, ci-drift-check.yml, eval-questions.yaml, context-budget-calculator.sh, rules/knowledge-feeding.md, rules/update-loop-retro.md - tools/context-audit-prompt.md (543 lines): 8-dimension scoring /100 Navigation: guide/README.md, machine-readable/reference.yaml (24 new entries) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
216 lines
8.5 KiB
YAML
216 lines
8.5 KiB
YAML
name: Context Drift Check
|
|
|
|
# Detects structural degradation in CLAUDE.md on a weekly schedule.
|
|
# Opens a GitHub issue automatically when problems are found.
|
|
#
|
|
# Setup:
|
|
# 1. Copy this file to .github/workflows/context-drift.yml
|
|
# 2. Make sure canary-check.sh is committed and executable
|
|
# 3. Adjust the cron schedule and threshold values as needed
|
|
#
|
|
# The workflow checks:
|
|
# - Structural validity (broken @imports, size, conflicts)
|
|
# - CLAUDE.md size trend (warns when exceeding threshold)
|
|
# - Broken @import references
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 9 * * 1' # Every Monday at 9:00 AM UTC
|
|
workflow_dispatch: # Allow manual trigger from GitHub UI
|
|
inputs:
|
|
project_dir:
|
|
description: 'Project directory to check (relative to repo root)'
|
|
required: false
|
|
default: '.'
|
|
|
|
env:
|
|
# Customize these thresholds for your project
|
|
CLAUDE_MD_MAX_LINES: 500
|
|
CLAUDE_MD_WARN_LINES: 400
|
|
|
|
jobs:
|
|
drift-check:
|
|
name: Check CLAUDE.md for drift
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
issues: write # Required to open issues on failure
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0 # Full history needed for git log checks
|
|
|
|
- name: Make canary script executable
|
|
run: chmod +x ./examples/context-engineering/canary-check.sh
|
|
|
|
- name: Run structural canary checks
|
|
id: canary
|
|
run: |
|
|
PROJECT_DIR="${{ github.event.inputs.project_dir || '.' }}"
|
|
./examples/context-engineering/canary-check.sh "$PROJECT_DIR"
|
|
continue-on-error: true # Capture exit code without stopping workflow
|
|
|
|
- name: Check CLAUDE.md size against thresholds
|
|
id: size_check
|
|
run: |
|
|
FILE="CLAUDE.md"
|
|
if [ ! -f "$FILE" ]; then
|
|
echo "::warning::CLAUDE.md not found at repo root"
|
|
exit 0
|
|
fi
|
|
|
|
CURRENT=$(wc -l < "$FILE" | tr -d ' ')
|
|
echo "current_lines=$CURRENT" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$CURRENT" -gt "$CLAUDE_MD_MAX_LINES" ]; then
|
|
echo "::error::CLAUDE.md has $CURRENT lines (max: $CLAUDE_MD_MAX_LINES). Consider modularizing with @imports."
|
|
echo "size_status=fail" >> "$GITHUB_OUTPUT"
|
|
elif [ "$CURRENT" -gt "$CLAUDE_MD_WARN_LINES" ]; then
|
|
echo "::warning::CLAUDE.md has $CURRENT lines (warn threshold: $CLAUDE_MD_WARN_LINES). Getting large."
|
|
echo "size_status=warn" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "CLAUDE.md size: $CURRENT lines (OK)"
|
|
echo "size_status=ok" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Check for stale @imports
|
|
id: import_check
|
|
run: |
|
|
FILE="CLAUDE.md"
|
|
if [ ! -f "$FILE" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
BROKEN=0
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^@([^[:space:]].+)$ ]]; then
|
|
IMPORT="${BASH_REMATCH[1]}"
|
|
if [ ! -f "$IMPORT" ]; then
|
|
echo "::error::Broken @import in CLAUDE.md: @$IMPORT"
|
|
BROKEN=$((BROKEN + 1))
|
|
fi
|
|
fi
|
|
done < "$FILE"
|
|
|
|
echo "broken_imports=$BROKEN" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$BROKEN" -gt 0 ]; then
|
|
echo "::error::Found $BROKEN broken @import(s) in CLAUDE.md"
|
|
else
|
|
echo "All @imports resolve correctly"
|
|
fi
|
|
|
|
- name: Check CLAUDE.md is git-tracked and recent
|
|
id: freshness_check
|
|
run: |
|
|
FILE="CLAUDE.md"
|
|
if [ ! -f "$FILE" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Check if tracked
|
|
if ! git ls-files --error-unmatch "$FILE" > /dev/null 2>&1; then
|
|
echo "::warning::CLAUDE.md is not tracked in git"
|
|
exit 0
|
|
fi
|
|
|
|
# Get days since last update
|
|
LAST_COMMIT_DATE=$(git log -1 --format="%cd" --date=format:"%Y-%m-%d" -- "$FILE")
|
|
TODAY=$(date +%Y-%m-%d)
|
|
DAYS_OLD=$(( ($(date -d "$TODAY" +%s 2>/dev/null || date -j -f "%Y-%m-%d" "$TODAY" +%s) - $(date -d "$LAST_COMMIT_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%d" "$LAST_COMMIT_DATE" +%s)) / 86400 ))
|
|
|
|
echo "days_old=$DAYS_OLD" >> "$GITHUB_OUTPUT"
|
|
echo "last_updated=$LAST_COMMIT_DATE" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$DAYS_OLD" -gt 90 ]; then
|
|
echo "::warning::CLAUDE.md was last updated $DAYS_OLD days ago ($LAST_COMMIT_DATE). Consider reviewing for stale rules."
|
|
else
|
|
echo "CLAUDE.md freshness: last updated $DAYS_OLD days ago ($LAST_COMMIT_DATE) — OK"
|
|
fi
|
|
|
|
- name: Collect check results
|
|
id: summary
|
|
if: always()
|
|
run: |
|
|
CANARY_STATUS="${{ steps.canary.outcome }}"
|
|
BROKEN_IMPORTS="${{ steps.import_check.outputs.broken_imports || 0 }}"
|
|
SIZE_STATUS="${{ steps.size_check.outputs.size_status || 'ok' }}"
|
|
CURRENT_LINES="${{ steps.size_check.outputs.current_lines || 0 }}"
|
|
DAYS_OLD="${{ steps.freshness_check.outputs.days_old || 0 }}"
|
|
LAST_UPDATED="${{ steps.freshness_check.outputs.last_updated || 'unknown' }}"
|
|
|
|
ISSUES=0
|
|
[ "$CANARY_STATUS" = "failure" ] && ISSUES=$((ISSUES + 1))
|
|
[ "$BROKEN_IMPORTS" -gt 0 ] && ISSUES=$((ISSUES + BROKEN_IMPORTS))
|
|
[ "$SIZE_STATUS" = "fail" ] && ISSUES=$((ISSUES + 1))
|
|
|
|
echo "total_issues=$ISSUES" >> "$GITHUB_OUTPUT"
|
|
echo "summary_lines=$CURRENT_LINES" >> "$GITHUB_OUTPUT"
|
|
echo "summary_days=$DAYS_OLD" >> "$GITHUB_OUTPUT"
|
|
echo "summary_updated=$LAST_UPDATED" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Open issue on failure
|
|
if: always() && steps.summary.outputs.total_issues > 0
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const issues = parseInt('${{ steps.summary.outputs.total_issues }}');
|
|
const lines = '${{ steps.summary.outputs.summary_lines }}';
|
|
const daysOld = '${{ steps.summary.outputs.summary_days }}';
|
|
const lastUpdated = '${{ steps.summary.outputs.summary_updated }}';
|
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
|
|
// Check if there's already an open issue for drift
|
|
const openIssues = await github.rest.issues.listForRepo({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
labels: 'ai-context,maintenance',
|
|
state: 'open',
|
|
});
|
|
|
|
if (openIssues.data.length > 0) {
|
|
// Add a comment to the existing issue instead of opening a new one
|
|
const existingIssue = openIssues.data[0];
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: existingIssue.number,
|
|
body: `Weekly check still finding issues (${issues} problem(s)).\n\nRun: ${runUrl}\nCLAUDE.md: ${lines} lines, last updated: ${lastUpdated} (${daysOld} days ago)`,
|
|
});
|
|
} else {
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: `Context Drift Detected — CLAUDE.md needs review`,
|
|
body: [
|
|
`The weekly context drift check found **${issues} problem(s)** in \`CLAUDE.md\`.`,
|
|
``,
|
|
`## Details`,
|
|
`- File size: ${lines} lines`,
|
|
`- Last updated: ${lastUpdated} (${daysOld} days ago)`,
|
|
`- Workflow run: ${runUrl}`,
|
|
``,
|
|
`## Action Required`,
|
|
`1. Review the workflow logs for specific issues`,
|
|
`2. Run \`./examples/context-engineering/canary-check.sh .\` locally for details`,
|
|
`3. Fix broken @imports, reduce file size, or update stale rules`,
|
|
`4. Close this issue once CLAUDE.md is back in good shape`,
|
|
``,
|
|
`_This issue was opened automatically by the context drift check workflow._`,
|
|
].join('\n'),
|
|
labels: ['ai-context', 'maintenance'],
|
|
});
|
|
}
|
|
|
|
- name: Final status
|
|
if: always()
|
|
run: |
|
|
ISSUES="${{ steps.summary.outputs.total_issues }}"
|
|
if [ "$ISSUES" -gt 0 ]; then
|
|
echo "Context drift check: FAILED ($ISSUES issues)"
|
|
exit 1
|
|
else
|
|
echo "Context drift check: PASSED"
|
|
fi
|