diff --git a/.github/adoption/reports/.gitkeep b/.github/adoption/reports/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.github/adoption/tracking.md b/.github/adoption/tracking.md new file mode 100644 index 000000000..e2a1bf867 --- /dev/null +++ b/.github/adoption/tracking.md @@ -0,0 +1,21 @@ +# Squad Adoption Tracking + +This file tracks Squad adoption at an aggregate level. Individual repository information is not collected or published. + +## Aggregate Metrics + +**Last updated:** 2026-03-09 + +- **Total repositories:** 78+ (discovered via GitHub code search) +- **Combined discovery methods:** package.json dependencies + squad.agent.md configuration files + +## Discovery Methods + +- **Code search: package.json** — Repositories with `@bradygaster/squad-cli` or `@bradygaster/squad-sdk` in dependencies +- **Code search: squad.agent.md** — Repositories with `.squad/agents/*/squad.agent.md` files (Squad agent configuration) + +These metrics are collected automatically via GitHub's public code search API. No individual repository information is stored or displayed. + +--- + +**Last audit:** 2026-03-09 by automation diff --git a/.github/workflows/adoption-report.yml b/.github/workflows/adoption-report.yml new file mode 100644 index 000000000..af2d28b99 --- /dev/null +++ b/.github/workflows/adoption-report.yml @@ -0,0 +1,53 @@ +name: Squad Adoption Report + +# Tier 1 automation from Flight's adoption monitoring strategy +# Runs daily to track Squad adoption metrics + +on: + schedule: + # Daily at 00:00 UTC + - cron: '0 0 * * *' + workflow_dispatch: + # Allow manual trigger + +permissions: + contents: write # To commit report back to repo + +jobs: + adoption-monitor: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Run adoption monitor + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/adoption-monitor.mjs + + - name: Commit adoption report + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + # Check if there are changes to commit + if [[ -n $(git status --porcelain .github/adoption/reports/) ]]; then + git add .github/adoption/reports/ + git commit -m "chore: daily adoption report $(date +%Y-%m-%d) + + Automated adoption metrics collection. + + Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" + git push + echo "✅ Adoption report committed" + else + echo "ℹ️ No changes to commit" + fi diff --git a/docs/src/content/docs/community/built-with-squad.md b/docs/src/content/docs/community/built-with-squad.md new file mode 100644 index 000000000..49119a907 --- /dev/null +++ b/docs/src/content/docs/community/built-with-squad.md @@ -0,0 +1,86 @@ +--- +title: Built with Squad +description: Projects using Squad in production — from enterprise job schedulers to learning platforms and real-world samples. +--- + +**78+ repositories** across organizations and individual developers use Squad. + +## Featured projects + +These projects are using Squad to build AI-powered development workflows: + +| Project | Description | Link | +|---------|-------------|------| +| microsoft/Generative-AI-for-beginners-dotnet | Microsoft's GenAI learning path for .NET | [GitHub](https://github.com/microsoft/Generative-AI-for-beginners-dotnet) | +| quartznet/quartznet | Enterprise job scheduler for .NET | [GitHub](https://github.com/quartznet/quartznet) | +| bradygaster/AspireSquad | AI agent teams for Aspire applications | [GitHub](https://github.com/bradygaster/AspireSquad) | +| bradygaster/ACCES | Aspire Community Content Engine | [GitHub](https://github.com/bradygaster/ACCES) | +| tamirdresher/squad-tetris | Squad-powered Tetris game | [GitHub](https://github.com/tamirdresher/squad-tetris) | +| csharpfritz/SquadUI | Squad UI components | [GitHub](https://github.com/csharpfritz/SquadUI) | + +## Real-world scenarios + +The [Squad-IRL repository](https://github.com/bradygaster/Squad-IRL) showcases 19 practical scenarios demonstrating Squad's versatility — from developer workflows to business operations to personal productivity tools. + +### Developer workflows + +These scenarios automate common development and technical tasks: + +- [**Bug triage**](https://github.com/bradygaster/Squad-IRL/tree/main/bug-triage) — Automatically categorize and prioritize incoming bugs +- [**Compliance checker**](https://github.com/bradygaster/Squad-IRL/tree/main/compliance-checker) — Validate code and documentation against compliance requirements +- [**A/B test orchestrator**](https://github.com/bradygaster/Squad-IRL/tree/main/ab-test-orchestrator) — Coordinate and analyze A/B testing workflows + +### Business and operations + +These scenarios handle business processes and operational workflows: + +- [**Contract reviewer**](https://github.com/bradygaster/Squad-IRL/tree/main/contract-reviewer) — Review contracts for key terms and risks +- [**Support ticket router**](https://github.com/bradygaster/Squad-IRL/tree/main/support-ticket-router) — Route support tickets to the right team members +- [**Inventory manager**](https://github.com/bradygaster/Squad-IRL/tree/main/inventory-manager) — Track and manage inventory levels +- [**Price monitor**](https://github.com/bradygaster/Squad-IRL/tree/main/price-monitor) — Monitor competitor pricing and market trends +- [**Real estate analyzer**](https://github.com/bradygaster/Squad-IRL/tree/main/real-estate-analyzer) — Analyze real estate listings and market data +- [**Realtor sales package**](https://github.com/bradygaster/Squad-IRL/tree/main/realtor-sales-package) — Generate sales materials for real estate listings +- [**Appointment scheduler**](https://github.com/bradygaster/Squad-IRL/tree/main/appointment-scheduler) — Coordinate and schedule appointments across multiple calendars + +### Personal productivity + +These scenarios help you manage personal workflows and information: + +- [**Gmail integration**](https://github.com/bradygaster/Squad-IRL/tree/main/gmail) — Automate email processing and responses +- [**LinkedIn monitor**](https://github.com/bradygaster/Squad-IRL/tree/main/linkedin-monitor) — Track LinkedIn activity and opportunities +- [**Meeting recap**](https://github.com/bradygaster/Squad-IRL/tree/main/meeting-recap) — Generate summaries and action items from meetings +- [**Job application tracker**](https://github.com/bradygaster/Squad-IRL/tree/main/job-application-tracker) — Track job applications and follow-ups +- [**Receipt scanner**](https://github.com/bradygaster/Squad-IRL/tree/main/receipt-scanner) — Extract and organize data from receipts +- [**Travel planner**](https://github.com/bradygaster/Squad-IRL/tree/main/travel-planner) — Plan and organize travel itineraries + +### Creative and fun + +These scenarios showcase Squad's flexibility for creative projects: + +- [**Content creation**](https://github.com/bradygaster/Squad-IRL/tree/main/content-creation) — Generate and manage content across multiple channels +- [**Social media manager**](https://github.com/bradygaster/Squad-IRL/tree/main/social-media-manager) — Schedule and coordinate social media posts +- [**MTG Commander deck builder**](https://github.com/bradygaster/Squad-IRL/tree/main/mtg-commander-deck-builder) — Build Magic: The Gathering Commander decks based on strategy preferences + +## Add your project + +Using Squad in your project? We'd love to feature it here. + +**Share your project:** Post about your Squad project on LinkedIn using the `#BuiltWithSquad` hashtag to help others discover what's possible with Squad. Community projects inspire innovation. + +**How to get listed:** +1. Open a pull request with the title prefix `showcase:` (e.g., `showcase: Add My Project`) + - **Note:** The `showcase:` prefix is required for PR triage and review +2. Add your project to this page + +**What to include:** +- Project name and repository link +- One-sentence description +- Confirmation that you're actively using Squad + +**Review time:** We typically review submissions within a few days. + +**About submissions:** PAO, the team's DevRel agent, reviews all submissions to ensure projects align with Squad's community standards and are actively maintained. + +--- + +*These metrics are updated automatically. Individual projects are listed with owner permission.* diff --git a/scripts/adoption-monitor.mjs b/scripts/adoption-monitor.mjs new file mode 100644 index 000000000..0a89496fd --- /dev/null +++ b/scripts/adoption-monitor.mjs @@ -0,0 +1,266 @@ +#!/usr/bin/env node +/** + * Squad Adoption Monitor + * Daily automated tracking of Squad adoption metrics (Tier 1 automation) + * + * Collects: + * - GitHub repo metrics (stars, forks, watchers) + * - Code search results (package.json imports, squad.agent.md files) + * - npm download stats (weekly downloads) + * - Recent forks and stargazers + * + * Generates markdown reports in .github/adoption/reports/{YYYY-MM-DD}.md + */ + +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(__dirname, '..'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const REPO_OWNER = 'bradygaster'; +const REPO_NAME = 'squad'; + +async function fetchWithRetry(url, options = {}, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(url, options); + + // Check rate limit + const remaining = response.headers.get('x-ratelimit-remaining'); + if (remaining && parseInt(remaining) < 10) { + console.warn(`⚠️ GitHub API rate limit low: ${remaining} requests remaining`); + } + + if (!response.ok && response.status !== 422) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response; + } catch (error) { + if (i === retries - 1) throw error; + console.log(`Retry ${i + 1}/${retries} for ${url}`); + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } + } + throw new Error('Max retries exceeded'); +} + +async function getRepoMetrics() { + const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`; + const response = await fetchWithRetry(url, { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'Squad-Adoption-Monitor' + } + }); + + const data = await response.json(); + return { + stars: data.stargazers_count || 0, + forks: data.forks_count || 0, + watchers: data.subscribers_count || 0 + }; +} + +async function searchCode(query) { + const url = `https://api.github.com/search/code?q=${encodeURIComponent(query)}&per_page=1`; + + try { + const response = await fetchWithRetry(url, { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'Squad-Adoption-Monitor' + } + }); + + const data = await response.json(); + return data.total_count || 0; + } catch (error) { + console.warn(`⚠️ Code search failed for "${query}": ${error && error.message ? error.message : 'Unknown error'}`); + return 0; + } +} + +async function getSearchResults() { + // Search for repos using Squad in package.json + const packageJsonCount = await searchCode('"@bradygaster/squad" filename:package.json'); + + // Search for repos with squad.agent.md files + const agentMdCount = await searchCode('path:.github/agents filename:squad.agent.md'); + + return { packageJsonCount, agentMdCount }; +} + +async function getNpmDownloads() { + try { + const [sdkResponse, cliResponse] = await Promise.all([ + fetch('https://api.npmjs.org/downloads/point/last-week/@bradygaster/squad-sdk'), + fetch('https://api.npmjs.org/downloads/point/last-week/@bradygaster/squad-cli') + ]); + + const sdkData = sdkResponse.ok ? await sdkResponse.json() : { downloads: 0 }; + const cliData = cliResponse.ok ? await cliResponse.json() : { downloads: 0 }; + + return { + sdk: sdkData.downloads || 0, + cli: cliData.downloads || 0 + }; + } catch (error) { + console.warn(`⚠️ npm API failed: ${error && error.message ? error.message : 'Unknown error'}`); + return { sdk: 0, cli: 0 }; + } +} + +async function getRecentForks() { + const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/forks?sort=newest&per_page=30`; + + try { + const response = await fetchWithRetry(url, { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'Squad-Adoption-Monitor' + } + }); + + const forks = await response.json(); + + // Filter to last 7 days + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + return forks + .filter(fork => new Date(fork.created_at) > sevenDaysAgo) + .map(fork => ({ + full_name: fork.full_name, + owner: fork.owner.login, + created_at: fork.created_at.split('T')[0], // Format as YYYY-MM-DD + profile: `https://github.com/${fork.owner.login}` + })); + } catch (error) { + console.warn(`⚠️ Failed to fetch recent forks: ${error && error.message ? error.message : 'Unknown error'}`); + return []; + } +} + +async function getPreviousReport() { + const reportsDir = join(REPO_ROOT, '.github/adoption/reports'); + + try { + const files = await readdir(reportsDir); + const reportFiles = files + .filter(f => f.endsWith('.md') && f !== '.gitkeep') + .sort() + .reverse(); + + if (reportFiles.length === 0) return null; + + const previousReportPath = join(reportsDir, reportFiles[0]); + const content = await readFile(previousReportPath, 'utf-8'); + + // Parse previous metrics from markdown + const starsMatch = content.match(/\*\*Stars:\*\* (\d+)/); + const forksMatch = content.match(/\*\*Forks:\*\* (\d+)/); + const sdkMatch = content.match(/squad-sdk: (\d+)/); + const cliMatch = content.match(/squad-cli: (\d+)/); + const packageMatch = content.match(/\*\*Repos using Squad:\*\* ~?(\d+)/); + const agentMatch = content.match(/\*\*Repos with squad\.agent\.md:\*\* ~?(\d+)/); + + return { + date: reportFiles[0].replace('.md', ''), + stars: starsMatch ? parseInt(starsMatch[1]) : 0, + forks: forksMatch ? parseInt(forksMatch[1]) : 0, + npmSdk: sdkMatch ? parseInt(sdkMatch[1]) : 0, + npmCli: cliMatch ? parseInt(cliMatch[1]) : 0, + packageJsonCount: packageMatch ? parseInt(packageMatch[1]) : 0, + agentMdCount: agentMatch ? parseInt(agentMatch[1]) : 0 + }; + } catch (error) { + return null; + } +} + +function calculateDelta(current, previous) { + const delta = current - previous; + if (delta === 0) return '(no change)'; + return delta > 0 ? `(+${delta} this week)` : `(${delta} this week)`; +} + +function calculatePercentage(current, previous) { + if (previous === 0) return 'N/A'; + const percentage = ((current - previous) / previous * 100).toFixed(1); + return `${percentage}%`; +} + +async function generateReport() { + console.log('🚀 Collecting adoption metrics...\n'); + + if (!GITHUB_TOKEN) { + throw new Error('GITHUB_TOKEN environment variable is required'); + } + + const [repoMetrics, searchResults, npmDownloads, recentForks, previousReport] = await Promise.all([ + getRepoMetrics(), + getSearchResults(), + getNpmDownloads(), + getRecentForks(), + getPreviousReport() + ]); + + const today = new Date().toISOString().split('T')[0]; + + console.log('📊 Current metrics:'); + console.log(` Stars: ${repoMetrics.stars}`); + console.log(` Forks: ${repoMetrics.forks}`); + console.log(` Watchers: ${repoMetrics.watchers}`); + console.log(` npm downloads (7d): SDK ${npmDownloads.sdk}, CLI ${npmDownloads.cli}`); + console.log(` Repos using Squad: ~${searchResults.packageJsonCount}`); + console.log(` Repos with squad.agent.md: ~${searchResults.agentMdCount}`); + console.log(` Recent forks (7d): ${recentForks.length}\n`); + + const report = `# Squad adoption report — ${today} + +## Momentum metrics + +- **Stars:** ${repoMetrics.stars} ${previousReport ? calculateDelta(repoMetrics.stars, previousReport.stars) : ''} +- **Forks:** ${repoMetrics.forks} ${previousReport ? calculateDelta(repoMetrics.forks, previousReport.forks) : ''} +- **Watchers:** ${repoMetrics.watchers} +- **npm downloads (7d):** squad-sdk: ${npmDownloads.sdk}${previousReport ? ` ${calculateDelta(npmDownloads.sdk, previousReport.npmSdk)}` : ''}, squad-cli: ${npmDownloads.cli}${previousReport ? ` ${calculateDelta(npmDownloads.cli, previousReport.npmCli)}` : ''} +- **Repos using Squad:** ~${searchResults.packageJsonCount} ${previousReport ? calculateDelta(searchResults.packageJsonCount, previousReport.packageJsonCount) : '(code search)'} +- **Repos with squad.agent.md:** ~${searchResults.agentMdCount} ${previousReport ? calculateDelta(searchResults.agentMdCount, previousReport.agentMdCount) : '(code search)'} + +## New adopters (this week) + +${recentForks.length > 0 ? `| Fork | Owner | Forked | Profile | +|------|-------|--------|---------| +${recentForks.map(fork => `| ${fork.full_name} | ${fork.owner} | ${fork.created_at} | ${fork.profile} |`).join('\n')}` : '_No new forks this week_'} + +## Trend + +${previousReport ? `- **Week-over-week star growth:** ${calculatePercentage(repoMetrics.stars, previousReport.stars)} +- **New forks this week:** ${recentForks.length} +- **npm download trend (SDK):** ${npmDownloads.sdk > previousReport.npmSdk ? '📈 Growing' : npmDownloads.sdk < previousReport.npmSdk ? '📉 Declining' : '➡️ Stable'} +- **Adoption velocity:** ${searchResults.packageJsonCount > previousReport.packageJsonCount ? '🚀 Accelerating' : searchResults.packageJsonCount < previousReport.packageJsonCount ? '🐌 Slowing' : '➡️ Steady'}` : '_First report — no trend data yet_'} + +--- + +*Generated by [Squad Adoption Monitor](../../scripts/adoption-monitor.mjs)* +*Next report: ${new Date(Date.now() + 86400000).toISOString().split('T')[0]}* +`; + + const reportPath = join(REPO_ROOT, '.github/adoption/reports', `${today}.md`); + await writeFile(reportPath, report, 'utf-8'); + + console.log(`✅ Report generated: ${reportPath}`); +} + +// Run +generateReport().catch(error => { + console.error('❌ Adoption monitor failed:', error); + process.exit(1); +}); diff --git a/test/docs-build.test.ts b/test/docs-build.test.ts index 5a14ce65f..8d638d58c 100644 --- a/test/docs-build.test.ts +++ b/test/docs-build.test.ts @@ -25,6 +25,8 @@ const EXPECTED_SCENARIOS = [ 'issue-driven-dev', 'existing-repo', 'ci-cd-integration', 'solo-dev', 'monorepo', 'team-of-humans', ]; +const EXPECTED_COMMUNITY = ['built-with-squad']; + // Blog posts are discovered dynamically to avoid breaking tests when posts change const EXPECTED_BLOG = existsSync(BLOG_CONTENT_DIR) ? readdirSync(BLOG_CONTENT_DIR) @@ -169,6 +171,7 @@ describe('Docs Build Script (Astro)', () => { ...EXPECTED_GUIDES.map(n => ({ dir: 'guide', name: n })), ...EXPECTED_REFERENCE.map(n => ({ dir: 'reference', name: n })), ...EXPECTED_SCENARIOS.map(n => ({ dir: 'scenarios', name: n })), + ...EXPECTED_COMMUNITY.map(n => ({ dir: 'community', name: n })), ]; for (const { dir, name } of allExpected) { const htmlPath = join(DIST_DIR, 'docs', dir, name, 'index.html');