---
roadcrew_template_name: "enrich-project-github.md"
roadcrew_template_type: "command"
execution_mode: "auto-execute"
roadcrew_template_version: "v1.0"
roadcrew_last_updated: "2025-11-03"
roadcrew_min_version: "1.6.0"
roadcrew_license: "See LICENSE file in .roadcrew folder"
roadcrew_copyright: "Copyright (c) 2025 North Star Holdings, LLC"
spdx_license_identifier: "LicenseRef-RoadcrewLicense-1.0"
---

# Enrich Project (GitHub)

Transform GitHub repository activity into activeContext documentation.

## What This Command Does

Fetches recent GitHub activity and converts it into an AI-optimized activeContext file by:

1. Querying GitHub API for:
   - Recent pull requests (open, merged, under review)
   - Open issues and their status
   - Active discussions and decisions
   - Team activity (last 30 days by default)
2. Parsing activity to extract current work context:
   - What's being actively worked on (PRs, in-progress issues)
   - Recent completions (merged PRs, closed issues)
   - Blocked items (issues labeled "blocked")
   - Active decisions (issues/discussions with decision markers)
3. Applying word constraints (1000 words max)
4. Generating optimized activeContext.md

**Output file:**
- ✅ `memory-bank/activeContext.md` (1000 words max)

## Usage

```bash
# Fetch recent GitHub activity and update activeContext
/enrich-project --github

# Last 7 days instead of default 30
/enrich-project --github --days 7

# Specific repository (auto-detected if not specified)
/enrich-project --github --repo tailwind-ai/roadcrew

# Dry run to preview
/enrich-project --github --dry-run

# Force overwrite existing file
/enrich-project --github --force
```

## Prerequisites

- `GITHUB_TOKEN` environment variable must be set
- Repository must be accessible (public or authorized private)
- GitHub API rate limits apply (5000/hour)

## Process

### 1. Authenticate and Connect

```typescript
async function connectGitHub(repo?: string): Promise<{ owner: string; repo: string }> {
  const token = process.env.GITHUB_TOKEN;
  if (!token) {
    throw new Error('GITHUB_TOKEN environment variable not set');
  }
  
  const octokit = new Octokit({ auth: token });
  
  // Auto-detect repo if not specified
  let repoInfo = repo;
  if (!repoInfo) {
    // Try to get from git remote
    const remote = await getGitRemote();
    repoInfo = parseRepoFromRemote(remote);
  }
  
  const [owner, repoName] = repoInfo.split('/');
  
  // Verify access
  await octokit.repos.get({ owner, repo: repoName });
  
  return { owner, repo: repoName };
}
```

### 2. Fetch Recent Activity

```typescript
interface GitHubActivity {
  openPRs: PRData[];
  mergedPRs: PRData[];
  openIssues: IssueData[];
  closedIssues: IssueData[];
  blockedIssues: IssueData[];
  recentActivity: ActivityEntry[];
}

async function fetchGitHubActivity(
  owner: string,
  repo: string,
  days: number = 30
): Promise<GitHubActivity> {
  const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
  const since = new Date();
  since.setDate(since.getDate() - days);
  
  // Fetch open PRs
  const openPRs = await octokit.paginate(octokit.pulls.list, {
    owner,
    repo,
    state: 'open',
    per_page: 100
  });
  
  // Fetch recently merged PRs
  const mergedPRs = await octokit.paginate(octokit.pulls.list, {
    owner,
    repo,
    state: 'closed',
    per_page: 100
  }).then(prs => prs.filter(pr => pr.merged_at && 
    new Date(pr.merged_at) > since
  ));
  
  // Fetch open issues
  const openIssues = await octokit.paginate(octokit.issues.list, {
    owner,
    repo,
    state: 'open',
    labels: '',
    per_page: 100
  });
  
  // Fetch blocked issues
  const blockedIssues = await octokit.paginate(octokit.issues.list, {
    owner,
    repo,
    state: 'open',
    labels: 'blocked',
    per_page: 100
  });
  
  // Fetch closed issues (last 30 days)
  const closedIssues = await octokit.paginate(octokit.issues.list, {
    owner,
    repo,
    state: 'closed',
    per_page: 100
  }).then(issues => issues.filter(i => 
    new Date(i.closed_at) > since
  ));
  
  return {
    openPRs: openPRs.slice(0, 5), // Top 5
    mergedPRs: mergedPRs.slice(0, 3),
    openIssues: openIssues.slice(0, 5),
    closedIssues: closedIssues.slice(0, 3),
    blockedIssues,
    recentActivity: []
  };
}
```

### 3. Extract Context Sections

```typescript
interface ActiveContextExtraction {
  currentFocus: string;
  recentChanges: string;
  nextSteps: string;
  blockedItems: string;
  activeDecisions: string;
}

async function extractActiveContext(
  activity: GitHubActivity
): Promise<ActiveContextExtraction> {
  return {
    currentFocus: generateCurrentFocus(activity.openPRs, activity.openIssues),
    recentChanges: generateRecentChanges(activity.mergedPRs, activity.closedIssues),
    nextSteps: generateNextSteps(activity.openPRs, activity.openIssues),
    blockedItems: generateBlockedItems(activity.blockedIssues),
    activeDecisions: generateActiveDecisions(activity.openPRs, activity.openIssues)
  };
}

function generateCurrentFocus(openPRs: PRData[], openIssues: IssueData[]): string {
  const focus = [];
  
  for (const pr of openPRs.slice(0, 3)) {
    focus.push(`- PR #${pr.number}: ${pr.title} (${pr.state})`);
  }
  
  for (const issue of openIssues.slice(0, 2)) {
    if (issue.assignees?.length > 0) {
      focus.push(`- Issue #${issue.number}: ${issue.title} (in progress)`);
    }
  }
  
  return focus.join('\n');
}

function generateRecentChanges(mergedPRs: PRData[], closedIssues: IssueData[]): string {
  const changes = [];
  
  for (const pr of mergedPRs) {
    const daysAgo = Math.floor((Date.now() - new Date(pr.merged_at).getTime()) / (1000 * 60 * 60 * 24));
    changes.push(`- Merged PR #${pr.number}: ${pr.title} (${daysAgo} day${daysAgo > 1 ? 's' : ''} ago)`);
  }
  
  for (const issue of closedIssues) {
    const daysAgo = Math.floor((Date.now() - new Date(issue.closed_at).getTime()) / (1000 * 60 * 60 * 24));
    changes.push(`- Closed issue #${issue.number}: ${issue.title} (${daysAgo} day${daysAgo > 1 ? 's' : ''} ago)`);
  }
  
  return changes.join('\n');
}

function generateBlockedItems(blockedIssues: IssueData[]): string {
  if (blockedIssues.length === 0) {
    return 'None currently blocked.';
  }
  
  return blockedIssues
    .slice(0, 3)
    .map(issue => {
      const blockedSince = extractBlockedDate(issue.body);
      return `- Issue #${issue.number}: ${issue.title}${blockedSince ? ` (blocked since ${blockedSince})` : ''}`;
    })
    .join('\n');
}
```

### 4. Apply Constraints

```typescript
const GITHUB_CONSTRAINTS = {
  activeContext: {
    maxTotalWords: 1000,
    maxSectionWords: {
      'Current Focus': 200,
      'Recent Changes': 300,
      'Next Steps': 250,
      'Blocked Items': 150,
      'Active Decisions': 100
    },
    requiredSections: ['Current Focus', 'Next Steps']
  }
};

async function applyGitHubConstraints(
  extraction: ActiveContextExtraction
): Promise<{ processed: ActiveContextExtraction; report: ConstraintReport }> {
  const constraints = GITHUB_CONSTRAINTS.activeContext;
  const report: ConstraintReport = {
    sections: {},
    warnings: [],
    ready: true
  };
  
  let totalWords = 0;
  
  for (const [section, content] of Object.entries(extraction)) {
    const maxWords = constraints.maxSectionWords[section];
    const wordCount = countWords(content);
    
    if (wordCount > maxWords) {
      extraction[section] = await trimToWordCount(content, maxWords);
      report.sections[section] = {
        trimmed: true,
        from: wordCount,
        to: maxWords,
        percentOver: Math.round((wordCount - maxWords) / maxWords * 100)
      };
    }
    
    totalWords += wordCount;
  }
  
  if (totalWords > constraints.maxTotalWords) {
    report.warnings.push(
      `Total exceeds limit: ${totalWords}/${constraints.maxTotalWords} words`
    );
  }
  
  return { processed: extraction, report };
}
```

### 5. Format and Write

```typescript
async function formatGitHubOutput(
  extraction: ActiveContextExtraction,
  repo: string
): Promise<string> {
  const template = await loadTemplate('activecontext');
  
  const today = new Date().toISOString().split('T')[0];
  
  let output = template
    .replace('{{LAST_UPDATED}}', today)
    .replace('{{CURRENT_PHASE}}', 'Active Development')
    .replace('{{ACTIVE_SPRINT}}', getCurrentSprintId())
    .replace('{{CURRENT_FEATURE_NAME}}', '(from GitHub activity)')
    .replace(/Current Focus[\s\S]*?---/m, 
      `## Current Focus\n${extraction.currentFocus}\n\n---`)
    .replace(/Recent Changes[\s\S]*?---/m,
      `## Recent Changes\n${extraction.recentChanges}\n\n---`)
    .replace(/Next Steps[\s\S]*?---/m,
      `## Next Steps\n${extraction.nextSteps}\n\n---`)
    .replace(/Blocked Items[\s\S]*?---/m,
      `## Blocked Items\n${extraction.blockedItems}\n\n---`);
  
  return output;
}
```

## Display Report

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔗 FETCHING GITHUB ACTIVITY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Repository: tailwind-ai/roadcrew
Time window: Last 30 days
Authentication: ✅ GITHUB_TOKEN

ACTIVITY FETCHED:
  ✅ Open PRs: 5 found
    - PR #205: Add dark mode support (in review)
    - PR #203: Performance optimization (draft)
    - PR #201: Documentation update (changes requested)

  ✅ Recent Merges: 3 found
    - PR #199: Fix auth bug (merged 2 days ago)
    - PR #195: Add caching layer (merged 5 days ago)
    - PR #187: Refactor templates (merged 8 days ago)

  ✅ Open Issues: 12 found
    - Issue #208: Add feature X (assigned)
    - Issue #206: Investigate bug Y (no assignee)
    - Issue #204: Improve docs (assigned)

  ✅ Blocked Issues: 1 found
    - Issue #200: Waiting on external API (blocked since 2025-10-28)

  ✅ Closed Recently: 4 found
    - Issue #197: Completed (closed 3 days ago)
    - Issue #194: Fixed (closed 7 days ago)

EXTRACTING SECTIONS:

activeContext.md:
  ✅ Current Focus: 180 words
  ✅ Recent Changes: 220 words
  ✅ Next Steps: 190 words
  ✅ Blocked Items: 85 words
  ✅ Active Decisions: 95 words
  
  📊 Total: 770/1000 words (77% of limit) ✅

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ READY TO WRITE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

File ready for output:
  📄 memory-bank/activeContext.md (770 words)

Run again without --dry-run to create file.
```

## Error Handling

**Authentication failed:**
```
❌ Error: GitHub authentication failed
   
   Ensure GITHUB_TOKEN is set and has 'repo' scope:
   export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
```

**Repository not found:**
```
❌ Error: Repository not found or access denied
   
   Repo: tailwind-ai/roadcrew
   
   Check that the repo exists and you have access.
```

**No activity found:**
```
⚠️  Warning: No activity found in last 30 days
   
   Repository may be inactive or you specified wrong time window.
   Try: /enrich-project --github --days 60
```

**Rate limit exceeded:**
```
⚠️  Warning: GitHub API rate limit exceeded
   Remaining: 0/5000 requests
   Reset at: 2025-11-04 14:30:00 UTC
   
   Wait until reset to try again.
```

## Flags Reference

| Flag | Purpose |
|------|---------|
| `--days <n>` | Look back N days (default: 30) |
| `--repo <owner/name>` | Specific repository (auto-detected if not set) |
| `--dry-run` | Preview without writing file |
| `--force` | Overwrite existing activeContext.md |
| `--verbose` | Show detailed API calls and data |

## Next Steps

After enrichment:

```bash
# Review the generated file
cat memory-bank/activeContext.md

# Then enrich business context
/enrich-project --business

# Or enrich technical context
/enrich-project --technical

# Or do all at once
/enrich-project --all
```

## Rate Limiting

This command respects GitHub API rate limits:

- **Limit:** 5,000 requests/hour per token
- **Current usage:** ~20-25 requests per run
- **Retry logic:** Automatic delay and retry on 429 errors

For typical usage, you can run this command many times per day without hitting limits.
