diff --git a/.ai-team-templates/constraint-tracking.md b/.ai-team-templates/constraint-tracking.md new file mode 100644 index 000000000..1936c3ff1 --- /dev/null +++ b/.ai-team-templates/constraint-tracking.md @@ -0,0 +1,38 @@ +# Constraint Budget Tracking + +When the user or system imposes constraints (question limits, revision limits, time budgets), maintain a visible counter in your responses and in the artifact. + +## Format + +``` +πŸ“Š Clarifying questions used: 2 / 3 +``` + +## Rules + +- Update the counter each time the constraint is consumed +- When a constraint is exhausted, state it: `πŸ“Š Question budget exhausted (3/3). Proceeding with current information.` +- If no constraints are active, do not display counters +- Include the final constraint status in multi-agent artifacts + +## Example Session + +``` +Coordinator: Spawning agents to analyze requirements... +πŸ“Š Clarifying questions used: 0 / 3 + +Agent asks clarification: "Should we support OAuth?" +Coordinator: Checking with user... +πŸ“Š Clarifying questions used: 1 / 3 + +Agent asks clarification: "What's the rate limit?" +Coordinator: Checking with user... +πŸ“Š Clarifying questions used: 2 / 3 + +Agent asks clarification: "Do we need RBAC?" +Coordinator: Checking with user... +πŸ“Š Clarifying questions used: 3 / 3 + +Agent asks clarification: "Should we cache responses?" +Coordinator: πŸ“Š Question budget exhausted (3/3). Proceeding without clarification. +``` diff --git a/.ai-team-templates/copilot-instructions.md b/.ai-team-templates/copilot-instructions.md new file mode 100644 index 000000000..ddc20f12c --- /dev/null +++ b/.ai-team-templates/copilot-instructions.md @@ -0,0 +1,46 @@ +# Copilot Coding Agent β€” Squad Instructions + +You are working on a project that uses **Squad**, an AI team framework. When picking up issues autonomously, follow these guidelines. + +## Team Context + +Before starting work on any issue: + +1. Read `.squad/team.md` for the team roster, member roles, and your capability profile. +2. Read `.squad/routing.md` for work routing rules. +3. If the issue has a `squad:{member}` label, read that member's charter at `.squad/agents/{member}/charter.md` to understand their domain expertise and coding style β€” work in their voice. + +## Capability Self-Check + +Before starting work, check your capability profile in `.squad/team.md` under the **Coding Agent β†’ Capabilities** section. + +- **🟒 Good fit** β€” proceed autonomously. +- **🟑 Needs review** β€” proceed, but note in the PR description that a squad member should review. +- **πŸ”΄ Not suitable** β€” do NOT start work. Instead, comment on the issue: + ``` + πŸ€– This issue doesn't match my capability profile (reason: {why}). Suggesting reassignment to a squad member. + ``` + +## Branch Naming + +Use the squad branch convention: +``` +squad/{issue-number}-{kebab-case-slug} +``` +Example: `squad/42-fix-login-validation` + +## PR Guidelines + +When opening a PR: +- Reference the issue: `Closes #{issue-number}` +- If the issue had a `squad:{member}` label, mention the member: `Working as {member} ({role})` +- If this is a 🟑 needs-review task, add to the PR description: `⚠️ This task was flagged as "needs review" β€” please have a squad member review before merging.` +- Follow any project conventions in `.squad/decisions.md` + +## Decisions + +If you make a decision that affects other team members, write it to: +``` +.squad/decisions/inbox/copilot-{brief-slug}.md +``` +The Scribe will merge it into the shared decisions file. diff --git a/.ai-team-templates/identity/now.md b/.ai-team-templates/identity/now.md new file mode 100644 index 000000000..04e1dfeeb --- /dev/null +++ b/.ai-team-templates/identity/now.md @@ -0,0 +1,9 @@ +--- +updated_at: {timestamp} +focus_area: {brief description} +active_issues: [] +--- + +# What We're Focused On + +{Narrative description of current focus β€” 1-3 sentences. Updated by coordinator at session start.} diff --git a/.ai-team-templates/identity/wisdom.md b/.ai-team-templates/identity/wisdom.md new file mode 100644 index 000000000..c3b978e4f --- /dev/null +++ b/.ai-team-templates/identity/wisdom.md @@ -0,0 +1,15 @@ +--- +last_updated: {timestamp} +--- + +# Team Wisdom + +Reusable patterns and heuristics learned through work. NOT transcripts β€” each entry is a distilled, actionable insight. + +## Patterns + + + +## Anti-Patterns + + diff --git a/.ai-team-templates/mcp-config.md b/.ai-team-templates/mcp-config.md new file mode 100644 index 000000000..2e361ee4b --- /dev/null +++ b/.ai-team-templates/mcp-config.md @@ -0,0 +1,90 @@ +# MCP Integration β€” Configuration and Samples + +MCP (Model Context Protocol) servers extend Squad with tools for external services β€” Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. + +> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, and graceful degradation. + +## Config File Locations + +Users configure MCP servers at these locations (checked in priority order): +1. **Repository-level:** `.copilot/mcp-config.json` (team-shared, committed to repo) +2. **Workspace-level:** `.vscode/mcp.json` (VS Code workspaces) +3. **User-level:** `~/.copilot/mcp-config.json` (personal) +4. **CLI override:** `--additional-mcp-config` flag (session-specific) + +## Sample Config β€” Trello + +```json +{ + "mcpServers": { + "trello": { + "command": "npx", + "args": ["-y", "@trello/mcp-server"], + "env": { + "TRELLO_API_KEY": "${TRELLO_API_KEY}", + "TRELLO_TOKEN": "${TRELLO_TOKEN}" + } + } + } +} +``` + +## Sample Config β€” GitHub + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}" + } + } + } +} +``` + +## Sample Config β€” Azure + +```json +{ + "mcpServers": { + "azure": { + "command": "npx", + "args": ["-y", "@azure/mcp-server"], + "env": { + "AZURE_SUBSCRIPTION_ID": "${AZURE_SUBSCRIPTION_ID}", + "AZURE_CLIENT_ID": "${AZURE_CLIENT_ID}", + "AZURE_CLIENT_SECRET": "${AZURE_CLIENT_SECRET}", + "AZURE_TENANT_ID": "${AZURE_TENANT_ID}" + } + } + } +} +``` + +## Sample Config β€” Aspire + +```json +{ + "mcpServers": { + "aspire": { + "command": "npx", + "args": ["-y", "@aspire/mcp-server"], + "env": { + "ASPIRE_DASHBOARD_URL": "${ASPIRE_DASHBOARD_URL}" + } + } + } +} +``` + +## Authentication Notes + +- **GitHub MCP requires a separate token** from the `gh` CLI auth. Generate at https://github.com/settings/tokens +- **Trello requires API key + token** from https://trello.com/power-ups/admin +- **Azure requires service principal credentials** β€” see Azure docs for setup +- **Aspire uses the dashboard URL** β€” typically `http://localhost:18888` during local dev + +Auth is a real blocker for some MCP servers. Users need separate tokens for GitHub MCP, Azure MCP, Trello MCP, etc. This is a documentation problem, not a code problem. diff --git a/.ai-team-templates/multi-agent-format.md b/.ai-team-templates/multi-agent-format.md new file mode 100644 index 000000000..b655ee942 --- /dev/null +++ b/.ai-team-templates/multi-agent-format.md @@ -0,0 +1,28 @@ +# Multi-Agent Artifact Format + +When multiple agents contribute to a final artifact (document, analysis, design), use this format. The assembled result must include: + +- Termination condition +- Constraint budgets (if active) +- Reviewer verdicts (if any) +- Raw agent outputs appendix + +## Assembly Structure + +The assembled result goes at the top. Below it, include: + +``` +## APPENDIX: RAW AGENT OUTPUTS + +### {Name} ({Role}) β€” Raw Output +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) β€” Raw Output +{Paste agent's verbatim response here, unedited} +``` + +## Appendix Rules + +This appendix is for diagnostic integrity. Do not edit, summarize, or polish the raw outputs. The Coordinator may not rewrite raw agent outputs; it may only paste them verbatim and assemble the final artifact above. + +See `.squad/templates/run-output.md` for the complete output format template. diff --git a/.ai-team-templates/plugin-marketplace.md b/.ai-team-templates/plugin-marketplace.md new file mode 100644 index 000000000..893632816 --- /dev/null +++ b/.ai-team-templates/plugin-marketplace.md @@ -0,0 +1,49 @@ +# Plugin Marketplace + +Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains β€” cloud platforms, frameworks, testing strategies, etc. + +## Marketplace State + +Registered marketplace sources are stored in `.squad/plugins/marketplaces.json`: + +```json +{ + "marketplaces": [ + { + "name": "awesome-copilot", + "source": "github/awesome-copilot", + "added_at": "2026-02-14T00:00:00Z" + } + ] +} +``` + +## CLI Commands + +Users manage marketplaces via the CLI: +- `squad plugin marketplace add {owner/repo}` β€” Register a GitHub repo as a marketplace source +- `squad plugin marketplace remove {name}` β€” Remove a registered marketplace +- `squad plugin marketplace list` β€” List registered marketplaces +- `squad plugin marketplace browse {name}` β€” List available plugins in a marketplace + +## When to Browse + +During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter: + +1. Read `.squad/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently. +2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords. +3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace β€” want me to install it as a skill for {CastName}?"* +4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it. + +## How to Install a Plugin + +1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent). +2. Copy it into the agent's skills directory: `.squad/skills/{plugin-name}/SKILL.md` +3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`. +4. Log the installation in the agent's `history.md`: *"πŸ“¦ Plugin '{plugin-name}' installed from {marketplace}."* + +## Graceful Degradation + +- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt. +- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} β€” continuing without it"*) and proceed with team member creation normally. +- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed. diff --git a/.ai-team-templates/workflows/squad-ci.yml b/.ai-team-templates/workflows/squad-ci.yml new file mode 100644 index 000000000..2f809d70f --- /dev/null +++ b/.ai-team-templates/workflows/squad-ci.yml @@ -0,0 +1,24 @@ +name: Squad CI + +on: + pull_request: + branches: [dev, preview, main, insider] + types: [opened, synchronize, reopened] + push: + branches: [dev, insider] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.js diff --git a/.ai-team-templates/workflows/squad-docs.yml b/.ai-team-templates/workflows/squad-docs.yml new file mode 100644 index 000000000..307d502c5 --- /dev/null +++ b/.ai-team-templates/workflows/squad-docs.yml @@ -0,0 +1,50 @@ +name: Squad Docs β€” Build & Deploy + +on: + workflow_dispatch: + push: + branches: [preview] + paths: + - 'docs/**' + - '.github/workflows/squad-docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install build dependencies + run: npm install --no-save markdown-it markdown-it-anchor + + - name: Build docs site + run: node docs/build.js --out _site --base /squad + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.ai-team-templates/workflows/squad-heartbeat.yml b/.ai-team-templates/workflows/squad-heartbeat.yml new file mode 100644 index 000000000..a3caa6ae7 --- /dev/null +++ b/.ai-team-templates/workflows/squad-heartbeat.yml @@ -0,0 +1,315 @@ +name: Squad Heartbeat (Ralph) + +on: + schedule: + # Every 30 minutes β€” adjust or remove if not needed + - cron: '*/30 * * * *' + + # React to completed work or new squad work + issues: + types: [closed, labeled] + pull_request: + types: [closed] + + # Manual trigger + workflow_dispatch: + +permissions: + issues: write + contents: read + pull-requests: read + +jobs: + heartbeat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Ralph β€” Check for squad work + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read team roster β€” check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.info('No .squad/team.md or .ai-team/team.md found β€” Ralph has nothing to monitor'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + + // Check if Ralph is on the roster + if (!content.includes('Ralph') || !content.includes('πŸ”„')) { + core.info('Ralph not on roster β€” heartbeat disabled'); + return; + } + + // Parse members from roster + const lines = content.split('\n'); + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) break; + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && !['Scribe', 'Ralph'].includes(cells[0])) { + members.push({ + name: cells[0], + role: cells[1], + label: `squad:${cells[0].toLowerCase()}` + }); + } + } + } + + if (members.length === 0) { + core.info('No squad members found β€” nothing to monitor'); + return; + } + + // 1. Find untriaged issues (labeled "squad" but no "squad:{member}" label) + const { data: squadIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'squad', + state: 'open', + per_page: 20 + }); + + const memberLabels = members.map(m => m.label); + const untriaged = squadIssues.filter(issue => { + const issueLabels = issue.labels.map(l => l.name); + return !memberLabels.some(ml => issueLabels.includes(ml)); + }); + + // 2. Find assigned but unstarted issues (has squad:{member} label, no assignee) + const unstarted = []; + for (const member of members) { + try { + const { data: memberIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: member.label, + state: 'open', + per_page: 10 + }); + for (const issue of memberIssues) { + if (!issue.assignees || issue.assignees.length === 0) { + unstarted.push({ issue, member }); + } + } + } catch (e) { + // Label may not exist yet + } + } + + // 3. Find squad issues missing triage verdict (no go:* label) + const missingVerdict = squadIssues.filter(issue => { + const labels = issue.labels.map(l => l.name); + return !labels.some(l => l.startsWith('go:')); + }); + + // 4. Find go:yes issues missing release target + const goYesIssues = squadIssues.filter(issue => { + const labels = issue.labels.map(l => l.name); + return labels.includes('go:yes') && !labels.some(l => l.startsWith('release:')); + }); + + // 4b. Find issues missing type: label + const missingType = squadIssues.filter(issue => { + const labels = issue.labels.map(l => l.name); + return !labels.some(l => l.startsWith('type:')); + }); + + // 5. Find open PRs that need attention + const { data: openPRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 20 + }); + + const squadPRs = openPRs.filter(pr => + pr.labels.some(l => l.name.startsWith('squad')) + ); + + // Build status summary + const summary = []; + if (untriaged.length > 0) { + summary.push(`πŸ”΄ **${untriaged.length} untriaged issue(s)** need triage`); + } + if (unstarted.length > 0) { + summary.push(`🟑 **${unstarted.length} assigned issue(s)** have no assignee`); + } + if (missingVerdict.length > 0) { + summary.push(`βšͺ **${missingVerdict.length} issue(s)** missing triage verdict (no \`go:\` label)`); + } + if (goYesIssues.length > 0) { + summary.push(`βšͺ **${goYesIssues.length} approved issue(s)** missing release target (no \`release:\` label)`); + } + if (missingType.length > 0) { + summary.push(`βšͺ **${missingType.length} issue(s)** missing \`type:\` label`); + } + if (squadPRs.length > 0) { + const drafts = squadPRs.filter(pr => pr.draft).length; + const ready = squadPRs.length - drafts; + if (drafts > 0) summary.push(`🟑 **${drafts} draft PR(s)** in progress`); + if (ready > 0) summary.push(`🟒 **${ready} PR(s)** open for review/merge`); + } + + if (summary.length === 0) { + core.info('πŸ“‹ Board is clear β€” Ralph found no pending work'); + return; + } + + core.info(`πŸ”„ Ralph found work:\n${summary.join('\n')}`); + + // Auto-triage untriaged issues + for (const issue of untriaged) { + const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); + let assignedMember = null; + let reason = ''; + + // Simple keyword-based routing + for (const member of members) { + const role = member.role.toLowerCase(); + if ((role.includes('frontend') || role.includes('ui')) && + (issueText.includes('ui') || issueText.includes('frontend') || + issueText.includes('css') || issueText.includes('component'))) { + assignedMember = member; + reason = 'Matches frontend/UI domain'; + break; + } + if ((role.includes('backend') || role.includes('api') || role.includes('server')) && + (issueText.includes('api') || issueText.includes('backend') || + issueText.includes('database') || issueText.includes('endpoint'))) { + assignedMember = member; + reason = 'Matches backend/API domain'; + break; + } + if ((role.includes('test') || role.includes('qa')) && + (issueText.includes('test') || issueText.includes('bug') || + issueText.includes('fix') || issueText.includes('regression'))) { + assignedMember = member; + reason = 'Matches testing/QA domain'; + break; + } + } + + // Default to Lead + if (!assignedMember) { + const lead = members.find(m => + m.role.toLowerCase().includes('lead') || + m.role.toLowerCase().includes('architect') + ); + if (lead) { + assignedMember = lead; + reason = 'No domain match β€” routed to Lead'; + } + } + + if (assignedMember) { + // Add member label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [assignedMember.label] + }); + + // Post triage comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: [ + `### πŸ”„ Ralph β€” Auto-Triage`, + '', + `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`, + `**Reason:** ${reason}`, + '', + `> Ralph auto-triaged this issue via the squad heartbeat. To reassign, swap the \`squad:*\` label.` + ].join('\n') + }); + + core.info(`Auto-triaged #${issue.number} β†’ ${assignedMember.name}`); + } + } + + # Copilot auto-assign step (uses PAT if available) + - name: Ralph β€” Assign @copilot issues + if: success() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) return; + + const content = fs.readFileSync(teamFile, 'utf8'); + + // Check if @copilot is on the team with auto-assign + const hasCopilot = content.includes('πŸ€– Coding Agent') || content.includes('@copilot'); + const autoAssign = content.includes(''); + if (!hasCopilot || !autoAssign) return; + + // Find issues labeled squad:copilot with no assignee + try { + const { data: copilotIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'squad:copilot', + state: 'open', + per_page: 5 + }); + + const unassigned = copilotIssues.filter(i => + !i.assignees || i.assignees.length === 0 + ); + + if (unassigned.length === 0) { + core.info('No unassigned squad:copilot issues'); + return; + } + + // Get repo default branch + const { data: repoData } = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + for (const issue of unassigned) { + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${context.repo.owner}/${context.repo.repo}`, + base_branch: repoData.default_branch, + custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.` + } + }); + core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`); + } catch (e) { + core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`); + } + } + } catch (e) { + core.info(`No squad:copilot label found or error: ${e.message}`); + } diff --git a/.ai-team-templates/workflows/squad-insider-release.yml b/.ai-team-templates/workflows/squad-insider-release.yml new file mode 100644 index 000000000..a3124d194 --- /dev/null +++ b/.ai-team-templates/workflows/squad-insider-release.yml @@ -0,0 +1,61 @@ +name: Squad Insider Release + +on: + push: + branches: [insider] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.js + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + SHORT_SHA=$(git rev-parse --short HEAD) + INSIDER_VERSION="${VERSION}-insider+${SHORT_SHA}" + INSIDER_TAG="v${INSIDER_VERSION}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "insider_version=$INSIDER_VERSION" >> "$GITHUB_OUTPUT" + echo "insider_tag=$INSIDER_TAG" >> "$GITHUB_OUTPUT" + echo "πŸ“¦ Base Version: $VERSION (Short SHA: $SHORT_SHA)" + echo "🏷️ Insider Version: $INSIDER_VERSION" + echo "πŸ”– Insider Tag: $INSIDER_TAG" + + - name: Create git tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.version.outputs.insider_tag }}" -m "Insider Release ${{ steps.version.outputs.insider_tag }}" + git push origin "${{ steps.version.outputs.insider_tag }}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.insider_tag }}" \ + --title "${{ steps.version.outputs.insider_tag }}" \ + --notes "This is an insider/development build of Squad. Install with:\`\`\`bash\nnpx github:bradygaster/squad#${{ steps.version.outputs.insider_tag }}\n\`\`\`\n\n**Note:** Insider builds may be unstable and are intended for early adopters and testing only." \ + --prerelease + + - name: Verify release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${{ steps.version.outputs.insider_tag }}" + echo "βœ… Insider Release ${{ steps.version.outputs.insider_tag }} created and verified." diff --git a/.ai-team-templates/workflows/squad-issue-assign.yml b/.ai-team-templates/workflows/squad-issue-assign.yml new file mode 100644 index 000000000..ad140f42d --- /dev/null +++ b/.ai-team-templates/workflows/squad-issue-assign.yml @@ -0,0 +1,161 @@ +name: Squad Issue Assign + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + assign-work: + # Only trigger on squad:{member} labels (not the base "squad" label) + if: startsWith(github.event.label.name, 'squad:') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Identify assigned member and trigger work + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const label = context.payload.label.name; + + // Extract member name from label (e.g., "squad:ripley" β†’ "ripley") + const memberName = label.replace('squad:', '').toLowerCase(); + + // Read team roster β€” check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found β€” cannot assign work'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if this is a coding agent assignment + const isCopilotAssignment = memberName === 'copilot'; + + let assignedMember = null; + if (isCopilotAssignment) { + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + } else { + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0].toLowerCase() === memberName) { + assignedMember = { name: cells[0], role: cells[1] }; + break; + } + } + } + } + + if (!assignedMember) { + core.warning(`No member found matching label "${label}"`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `⚠️ No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.` + }); + return; + } + + // Post assignment acknowledgment + let comment; + if (isCopilotAssignment) { + comment = [ + `### πŸ€– Routed to @copilot (Coding Agent)`, + '', + `**Issue:** #${issue.number} β€” ${issue.title}`, + '', + `@copilot has been assigned and will pick this up automatically.`, + '', + `> The coding agent will create a \`copilot/*\` branch and open a draft PR.`, + `> Review the PR as you would any team member's work.`, + ].join('\n'); + } else { + comment = [ + `### πŸ“‹ Assigned to ${assignedMember.name} (${assignedMember.role})`, + '', + `**Issue:** #${issue.number} β€” ${issue.title}`, + '', + `${assignedMember.name} will pick this up in the next Copilot session.`, + '', + `> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`, + `> Otherwise, start a Copilot session and say:`, + `> \`${assignedMember.name}, work on issue #${issue.number}\``, + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`); + + # Separate step: assign @copilot using PAT (required for coding agent) + - name: Assign @copilot coding agent + if: github.event.label.name == 'squad:copilot' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + // Get the default branch name (main, master, etc.) + const { data: repoData } = await github.rest.repos.get({ owner, repo }); + const baseBranch = repoData.default_branch; + + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner, + repo, + issue_number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${owner}/${repo}`, + base_branch: baseBranch, + custom_instructions: '', + custom_agent: '', + model: '' + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`); + } catch (err) { + core.warning(`Assignment with agent_assignment failed: ${err.message}`); + // Fallback: try without agent_assignment + try { + await github.rest.issues.addAssignees({ + owner, repo, issue_number, + assignees: ['copilot-swe-agent'] + }); + core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`); + } catch (err2) { + core.warning(`Fallback also failed: ${err2.message}`); + } + } diff --git a/.ai-team-templates/workflows/squad-label-enforce.yml b/.ai-team-templates/workflows/squad-label-enforce.yml new file mode 100644 index 000000000..633d220df --- /dev/null +++ b/.ai-team-templates/workflows/squad-label-enforce.yml @@ -0,0 +1,181 @@ +name: Squad Label Enforce + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + enforce: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enforce mutual exclusivity + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const appliedLabel = context.payload.label.name; + + // Namespaces with mutual exclusivity rules + const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; + + // Skip if not a managed namespace label + if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { + core.info(`Label ${appliedLabel} is not in a managed namespace β€” skipping`); + return; + } + + const allLabels = issue.labels.map(l => l.name); + + // Handle go: namespace (mutual exclusivity) + if (appliedLabel.startsWith('go:')) { + const otherGoLabels = allLabels.filter(l => + l.startsWith('go:') && l !== appliedLabel + ); + + if (otherGoLabels.length > 0) { + // Remove conflicting go: labels + for (const label of otherGoLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🏷️ Triage verdict updated β†’ \`${appliedLabel}\`` + }); + } + + // Auto-apply release:backlog if go:yes and no release target + if (appliedLabel === 'go:yes') { + const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); + if (!hasReleaseLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['release:backlog'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `πŸ“‹ Marked as \`release:backlog\` β€” assign a release target when ready.` + }); + + core.info('Applied release:backlog for go:yes issue'); + } + } + + // Remove release: labels if go:no + if (appliedLabel === 'go:no') { + const releaseLabels = allLabels.filter(l => l.startsWith('release:')); + if (releaseLabels.length > 0) { + for (const label of releaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed release label from go:no issue: ${label}`); + } + } + } + } + + // Handle release: namespace (mutual exclusivity) + if (appliedLabel.startsWith('release:')) { + const otherReleaseLabels = allLabels.filter(l => + l.startsWith('release:') && l !== appliedLabel + ); + + if (otherReleaseLabels.length > 0) { + // Remove conflicting release: labels + for (const label of otherReleaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🏷️ Release target updated β†’ \`${appliedLabel}\`` + }); + } + } + + // Handle type: namespace (mutual exclusivity) + if (appliedLabel.startsWith('type:')) { + const otherTypeLabels = allLabels.filter(l => + l.startsWith('type:') && l !== appliedLabel + ); + + if (otherTypeLabels.length > 0) { + for (const label of otherTypeLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🏷️ Issue type updated β†’ \`${appliedLabel}\`` + }); + } + } + + // Handle priority: namespace (mutual exclusivity) + if (appliedLabel.startsWith('priority:')) { + const otherPriorityLabels = allLabels.filter(l => + l.startsWith('priority:') && l !== appliedLabel + ); + + if (otherPriorityLabels.length > 0) { + for (const label of otherPriorityLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🏷️ Priority updated β†’ \`${appliedLabel}\`` + }); + } + } + + core.info(`Label enforcement complete for ${appliedLabel}`); diff --git a/.ai-team-templates/workflows/squad-main-guard.yml b/.ai-team-templates/workflows/squad-main-guard.yml new file mode 100644 index 000000000..7ea0dbe17 --- /dev/null +++ b/.ai-team-templates/workflows/squad-main-guard.yml @@ -0,0 +1,129 @@ +name: Squad Protected Branch Guard + +on: + pull_request: + branches: [main, preview, insider] + types: [opened, synchronize, reopened] + push: + branches: [main, preview, insider] + +permissions: + contents: read + pull-requests: read + +jobs: + guard: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check for forbidden paths + uses: actions/github-script@v7 + with: + script: | + // Fetch all files changed - handles both PR and push events + let files = []; + + if (context.eventName === 'pull_request') { + // PR event: use pulls.listFiles API + let page = 1; + while (true) { + const resp = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + per_page: 100, + page + }); + files.push(...resp.data); + if (resp.data.length < 100) break; + page++; + } + } else if (context.eventName === 'push') { + // Push event: compare against base branch + const base = context.payload.before; + const head = context.payload.after; + + // If this is not a force push and base exists, compare commits + if (base && base !== '0000000000000000000000000000000000000000') { + const comparison = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base, + head + }); + files = comparison.data.files || []; + } else { + // Force push or initial commit: list all files in the current tree + core.info('Force push detected or initial commit, checking tree state'); + const { data: tree } = await github.rest.git.getTree({ + owner: context.repo.owner, + repo: context.repo.repo, + tree_sha: head, + recursive: 'true' + }); + files = tree.tree + .filter(item => item.type === 'blob') + .map(item => ({ filename: item.path, status: 'added' })); + } + } + + // Check each file against forbidden path rules + // Allow removals ΓÇâ deleting forbidden files from protected branches is fine + const forbidden = files + .filter(f => f.status !== 'removed') + .map(f => f.filename) + .filter(f => { + // .ai-team/** and .squad/** ΓÇâ ALL team state files, zero exceptions + if (f === '.ai-team' || f.startsWith('.ai-team/') || f === '.squad' || f.startsWith('.squad/')) return true; + // .ai-team-templates/** ΓÇâ Squad's own templates, stay on dev + if (f === '.ai-team-templates' || f.startsWith('.ai-team-templates/')) return true; + // team-docs/** ΓÇâ ALL internal team docs, zero exceptions + if (f.startsWith('team-docs/')) return true; + // docs/proposals/** ΓÇâ internal design proposals, stay on dev + if (f.startsWith('docs/proposals/')) return true; + return false; + }); + + if (forbidden.length === 0) { + core.info('✅ No forbidden paths found in PR ΓÇâ all clear.'); + return; + } + + // Build a clear, actionable error message + const lines = [ + '## β‰‘Ζ’ΓœΒ½ Forbidden files detected in PR to main', + '', + 'The following files must NOT be merged into `main`.', + '`.ai-team/` and `.squad/` are runtime team state ΓÇâ they belong on dev branches only.', + '`.ai-team-templates/` is Squad\'s internal planning ΓÇâ it belongs on dev branches only.', + '`team-docs/` is internal team content ΓÇâ it belongs on dev branches only.', + '`docs/proposals/` is internal design proposals ΓÇâ it belongs on dev branches only.', + '', + '### Forbidden files found:', + '', + ...forbidden.map(f => `- \`${f}\``), + '', + '### How to fix:', + '', + '```bash', + '# Remove tracked .ai-team/ files (keeps local copies):', + 'git rm --cached -r .ai-team/', + '', + '# Remove tracked .squad/ files (keeps local copies):', + 'git rm --cached -r .squad/', + '', + '# Remove tracked team-docs/ files:', + 'git rm --cached -r team-docs/', + '', + '# Commit the removal and push:', + 'git commit -m "chore: remove forbidden paths from PR"', + 'git push', + '```', + '', + '> Ξ“ΓœΓ‘βˆ©β••Γ… `.ai-team/` and `.squad/` are committed on `dev` and feature branches by design.', + '> The guard workflow is the enforcement mechanism that keeps these files off `main` and `preview`.', + '> `git rm --cached` untracks them from this PR without deleting your local copies.', + ]; + + core.setFailed(lines.join('\n')); diff --git a/.ai-team-templates/workflows/squad-preview.yml b/.ai-team-templates/workflows/squad-preview.yml new file mode 100644 index 000000000..9298c364e --- /dev/null +++ b/.ai-team-templates/workflows/squad-preview.yml @@ -0,0 +1,55 @@ +name: Squad Preview Validation + +on: + push: + branches: [preview] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Validate version consistency + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md β€” update CHANGELOG.md before release" + exit 1 + fi + echo "βœ… Version $VERSION validated in CHANGELOG.md" + + - name: Run tests + run: node --test test/*.test.js + + - name: Check no .ai-team/ or .squad/ files are tracked + run: | + FOUND_FORBIDDEN=0 + if git ls-files --error-unmatch .ai-team/ 2>/dev/null; then + echo "::error::❌ .ai-team/ files are tracked on preview β€” this must not ship." + FOUND_FORBIDDEN=1 + fi + if git ls-files --error-unmatch .squad/ 2>/dev/null; then + echo "::error::❌ .squad/ files are tracked on preview β€” this must not ship." + FOUND_FORBIDDEN=1 + fi + if [ $FOUND_FORBIDDEN -eq 1 ]; then + exit 1 + fi + echo "βœ… No .ai-team/ or .squad/ files tracked β€” clean for release." + + - name: Validate package.json version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if [ -z "$VERSION" ]; then + echo "::error::❌ No version field found in package.json." + exit 1 + fi + echo "βœ… package.json version: $VERSION" diff --git a/.ai-team-templates/workflows/squad-promote.yml b/.ai-team-templates/workflows/squad-promote.yml new file mode 100644 index 000000000..07bac3261 --- /dev/null +++ b/.ai-team-templates/workflows/squad-promote.yml @@ -0,0 +1,121 @@ +name: Squad Promote + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run β€” show what would happen without pushing' + required: false + default: 'false' + type: choice + options: ['false', 'true'] + +permissions: + contents: write + +jobs: + dev-to-preview: + name: Promote dev β†’ preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state (dry run info) + run: | + echo "=== dev HEAD ===" && git log origin/dev -1 --oneline + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== Files that would be stripped ===" + git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates|squad-templates)|team-docs/|docs/proposals/)" || echo "(none)" + + - name: Merge dev β†’ preview (strip forbidden paths) + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout preview + git merge origin/dev --no-commit --no-ff -X theirs || true + + # Strip forbidden paths from merge commit + git rm -rf --cached --ignore-unmatch \ + .ai-team/ \ + .squad/ \ + .ai-team-templates/ \ + .squad-templates/ \ + team-docs/ \ + "docs/proposals/" || true + + # Commit if there are staged changes + if ! git diff --cached --quiet; then + git commit -m "chore: promote dev β†’ preview (v$(node -e "console.log(require('./package.json').version)"))" + git push origin preview + echo "βœ… Pushed preview branch" + else + echo "ℹ️ Nothing to commit β€” preview is already up to date" + fi + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "πŸ” Dry run complete β€” no changes pushed." + + preview-to-main: + name: Promote preview β†’ main (release) + needs: dev-to-preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state + run: | + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== main HEAD ===" && git log origin/main -1 --oneline + echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)" + + - name: Validate preview is release-ready + run: | + git checkout preview + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md β€” update before releasing" + exit 1 + fi + echo "βœ… Version $VERSION has CHANGELOG entry" + + # Verify no forbidden files on preview + FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates|squad-templates)/|team-docs/|docs/proposals/)" || true) + if [ -n "$FORBIDDEN" ]; then + echo "::error::Forbidden files found on preview: $FORBIDDEN" + exit 1 + fi + echo "βœ… No forbidden files on preview" + + - name: Merge preview β†’ main + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout main + git merge origin/preview --no-ff -m "chore: promote preview β†’ main (v$(node -e "console.log(require('./package.json').version)"))" + git push origin main + echo "βœ… Pushed main β€” squad-release.yml will tag and publish the release" + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "πŸ” Dry run complete β€” no changes pushed." diff --git a/.ai-team-templates/workflows/squad-release.yml b/.ai-team-templates/workflows/squad-release.yml new file mode 100644 index 000000000..bbd5de793 --- /dev/null +++ b/.ai-team-templates/workflows/squad-release.yml @@ -0,0 +1,77 @@ +name: Squad Release + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.js + + - name: Validate version consistency + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md β€” update CHANGELOG.md before release" + exit 1 + fi + echo "βœ… Version $VERSION validated in CHANGELOG.md" + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "πŸ“¦ Version: $VERSION (tag: v$VERSION)" + + - name: Check if tag already exists + id: check_tag + run: | + if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "⏭️ Tag ${{ steps.version.outputs.tag }} already exists β€” skipping release." + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "πŸ†• Tag ${{ steps.version.outputs.tag }} does not exist β€” creating release." + fi + + - name: Create git tag + if: steps.check_tag.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.tag }}" + + - name: Create GitHub Release + if: steps.check_tag.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "${{ steps.version.outputs.tag }}" \ + --generate-notes \ + --latest + + - name: Verify release + if: steps.check_tag.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${{ steps.version.outputs.tag }}" + echo "βœ… Release ${{ steps.version.outputs.tag }} created and verified." diff --git a/.ai-team-templates/workflows/squad-triage.yml b/.ai-team-templates/workflows/squad-triage.yml new file mode 100644 index 000000000..a58be9b29 --- /dev/null +++ b/.ai-team-templates/workflows/squad-triage.yml @@ -0,0 +1,260 @@ +name: Squad Triage + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + triage: + if: github.event.label.name == 'squad' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Triage issue via Lead agent + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + + // Read team roster β€” check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found β€” cannot triage'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if @copilot is on the team + const hasCopilot = content.includes('πŸ€– Coding Agent'); + const copilotAutoAssign = content.includes(''); + + // Parse @copilot capability profile + let goodFitKeywords = []; + let needsReviewKeywords = []; + let notSuitableKeywords = []; + + if (hasCopilot) { + // Extract capability tiers from team.md + const goodFitMatch = content.match(/🟒\s*Good fit[^:]*:\s*(.+)/i); + const needsReviewMatch = content.match(/🟑\s*Needs review[^:]*:\s*(.+)/i); + const notSuitableMatch = content.match(/πŸ”΄\s*Not suitable[^:]*:\s*(.+)/i); + + if (goodFitMatch) { + goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation']; + } + if (needsReviewMatch) { + needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration']; + } + if (notSuitableMatch) { + notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance']; + } + } + + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + // Read routing rules β€” check .squad/ first, fall back to .ai-team/ + let routingFile = '.squad/routing.md'; + if (!fs.existsSync(routingFile)) { + routingFile = '.ai-team/routing.md'; + } + let routingContent = ''; + if (fs.existsSync(routingFile)) { + routingContent = fs.readFileSync(routingFile, 'utf8'); + } + + // Find the Lead + const lead = members.find(m => + m.role.toLowerCase().includes('lead') || + m.role.toLowerCase().includes('architect') || + m.role.toLowerCase().includes('coordinator') + ); + + if (!lead) { + core.warning('No Lead role found in team roster β€” cannot triage'); + return; + } + + // Build triage context + const memberList = members.map(m => + `- **${m.name}** (${m.role}) β†’ label: \`squad:${m.name.toLowerCase()}\`` + ).join('\n'); + + // Determine best assignee based on issue content and routing + const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); + + let assignedMember = null; + let triageReason = ''; + let copilotTier = null; + + // First, evaluate @copilot fit if enabled + if (hasCopilot) { + const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw)); + const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw)); + const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw)); + + if (isGoodFit) { + copilotTier = 'good-fit'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟒 Good fit for @copilot β€” matches capability profile'; + } else if (isNeedsReview) { + copilotTier = 'needs-review'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟑 Routing to @copilot (needs review) β€” a squad member should review the PR'; + } else if (isNotSuitable) { + copilotTier = 'not-suitable'; + // Fall through to normal routing + } + } + + // If not routed to @copilot, use keyword-based routing + if (!assignedMember) { + for (const member of members) { + const role = member.role.toLowerCase(); + if ((role.includes('frontend') || role.includes('ui')) && + (issueText.includes('ui') || issueText.includes('frontend') || + issueText.includes('css') || issueText.includes('component') || + issueText.includes('button') || issueText.includes('page') || + issueText.includes('layout') || issueText.includes('design'))) { + assignedMember = member; + triageReason = 'Issue relates to frontend/UI work'; + break; + } + if ((role.includes('backend') || role.includes('api') || role.includes('server')) && + (issueText.includes('api') || issueText.includes('backend') || + issueText.includes('database') || issueText.includes('endpoint') || + issueText.includes('server') || issueText.includes('auth'))) { + assignedMember = member; + triageReason = 'Issue relates to backend/API work'; + break; + } + if ((role.includes('test') || role.includes('qa') || role.includes('quality')) && + (issueText.includes('test') || issueText.includes('bug') || + issueText.includes('fix') || issueText.includes('regression') || + issueText.includes('coverage'))) { + assignedMember = member; + triageReason = 'Issue relates to testing/quality work'; + break; + } + if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) && + (issueText.includes('deploy') || issueText.includes('ci') || + issueText.includes('pipeline') || issueText.includes('docker') || + issueText.includes('infrastructure'))) { + assignedMember = member; + triageReason = 'Issue relates to DevOps/infrastructure work'; + break; + } + } + } + + // Default to Lead if no routing match + if (!assignedMember) { + assignedMember = lead; + triageReason = 'No specific domain match β€” assigned to Lead for further analysis'; + } + + const isCopilot = assignedMember.name === '@copilot'; + const assignLabel = isCopilot ? 'squad:copilot' : `squad:${assignedMember.name.toLowerCase()}`; + + // Add the member-specific label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [assignLabel] + }); + + // Apply default triage verdict + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['go:needs-research'] + }); + + // Auto-assign @copilot if enabled + if (isCopilot && copilotAutoAssign) { + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot'] + }); + } catch (err) { + core.warning(`Could not auto-assign @copilot: ${err.message}`); + } + } + + // Build copilot evaluation note + let copilotNote = ''; + if (hasCopilot && !isCopilot) { + if (copilotTier === 'not-suitable') { + copilotNote = `\n\n**@copilot evaluation:** πŸ”΄ Not suitable β€” issue involves work outside the coding agent's capability profile.`; + } else { + copilotNote = `\n\n**@copilot evaluation:** No strong capability match β€” routed to squad member.`; + } + } + + // Post triage comment + const comment = [ + `### πŸ—οΈ Squad Triage β€” ${lead.name} (${lead.role})`, + '', + `**Issue:** #${issue.number} β€” ${issue.title}`, + `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`, + `**Reason:** ${triageReason}`, + copilotTier === 'needs-review' ? `\n⚠️ **PR review recommended** β€” a squad member should review @copilot's work on this one.` : '', + copilotNote, + '', + `---`, + '', + `**Team roster:**`, + memberList, + hasCopilot ? `- **@copilot** (Coding Agent) β†’ label: \`squad:copilot\`` : '', + '', + `> To reassign, remove the current \`squad:*\` label and add the correct one.`, + ].filter(Boolean).join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Triaged issue #${issue.number} β†’ ${assignedMember.name} (${assignLabel})`); diff --git a/.ai-team-templates/workflows/sync-squad-labels.yml b/.ai-team-templates/workflows/sync-squad-labels.yml new file mode 100644 index 000000000..fbcfd9cc2 --- /dev/null +++ b/.ai-team-templates/workflows/sync-squad-labels.yml @@ -0,0 +1,169 @@ +name: Sync Squad Labels + +on: + push: + paths: + - '.squad/team.md' + - '.ai-team/team.md' + workflow_dispatch: + +permissions: + issues: write + contents: read + +jobs: + sync-labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse roster and sync labels + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + + if (!fs.existsSync(teamFile)) { + core.info('No .squad/team.md or .ai-team/team.md found β€” skipping label sync'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Parse the Members table for agent names + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`); + + // Check if @copilot is on the team + const hasCopilot = content.includes('πŸ€– Coding Agent'); + + // Define label color palette for squad labels + const SQUAD_COLOR = '9B8FCC'; + const MEMBER_COLOR = '9B8FCC'; + const COPILOT_COLOR = '10b981'; + + // Define go: and release: labels (static) + const GO_LABELS = [ + { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' }, + { name: 'go:no', color: 'B60205', description: 'Not pursuing' }, + { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' } + ]; + + const RELEASE_LABELS = [ + { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' }, + { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' }, + { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' }, + { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' }, + { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' } + ]; + + const TYPE_LABELS = [ + { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, + { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, + { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation β€” produces a plan, not code' }, + { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' }, + { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' }, + { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } + ]; + + // High-signal labels β€” these MUST visually dominate all others + const SIGNAL_LABELS = [ + { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' }, + { name: 'feedback', color: '00E5FF', description: 'User feedback β€” high signal, needs attention' } + ]; + + const PRIORITY_LABELS = [ + { name: 'priority:p0', color: 'B60205', description: 'Blocking release' }, + { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' }, + { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' } + ]; + + // Ensure the base "squad" triage label exists + const labels = [ + { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox β€” Lead will assign to a member' } + ]; + + for (const member of members) { + labels.push({ + name: `squad:${member.name.toLowerCase()}`, + color: MEMBER_COLOR, + description: `Assigned to ${member.name} (${member.role})` + }); + } + + // Add @copilot label if coding agent is on the team + if (hasCopilot) { + labels.push({ + name: 'squad:copilot', + color: COPILOT_COLOR, + description: 'Assigned to @copilot (Coding Agent) for autonomous work' + }); + } + + // Add go:, release:, type:, priority:, and high-signal labels + labels.push(...GO_LABELS); + labels.push(...RELEASE_LABELS); + labels.push(...TYPE_LABELS); + labels.push(...PRIORITY_LABELS); + labels.push(...SIGNAL_LABELS); + + // Sync labels (create or update) + for (const label of labels) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name + }); + // Label exists β€” update it + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Updated label: ${label.name}`); + } catch (err) { + if (err.status === 404) { + // Label doesn't exist β€” create it + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Created label: ${label.name}`); + } else { + throw err; + } + } + } + + core.info(`Label sync complete: ${labels.length} labels synced`); diff --git a/.ai-team/agents/beast/history.md b/.ai-team/agents/beast/history.md index c7e07b68d..2cd9919f4 100644 --- a/.ai-team/agents/beast/history.md +++ b/.ai-team/agents/beast/history.md @@ -1,60 +1,20 @@ # Project Context -- **Owner:** Jeffrey T. Fritz (csharpfritz@users.noreply.github.com) +- **Owner:** Jeffrey T. Fritz - **Project:** BlazorWebFormsComponents β€” Blazor components emulating ASP.NET Web Forms controls for migration - **Stack:** C#, Blazor, .NET, ASP.NET Web Forms, bUnit, xUnit, MkDocs, Playwright - **Created:** 2026-02-10 ## Learnings - - -- **Doc structure pattern:** Each component doc follows a consistent structure: title β†’ intro paragraph with MS docs link β†’ Features Supported β†’ Features NOT Supported β†’ Web Forms Declarative Syntax β†’ Blazor Razor Syntax (with examples) β†’ HTML Output β†’ Migration Notes (Before/After) β†’ Examples β†’ See Also. Admonitions (`!!! note`, `!!! warning`, `!!! tip`) are used for gotchas and important notes. -- **mkdocs.yml nav is alphabetical:** Components are listed alphabetically within their category sections (Editor Controls, Data Controls, Validation Controls, Navigation Controls, Login Controls, Utility Features). -- **Calendar doc already existed:** The Calendar component doc was already present at `docs/EditorControls/Calendar.md` and in the mkdocs nav β€” likely created alongside the component PR. No changes needed. -- **PageService doc existed on PR branch but not on dev:** The basepage services branch (`copilot/create-basepage-for-services`) already had a comprehensive `docs/UtilityFeatures/PageService.md`. I created a fresh version on dev that matches the project doc conventions. -- **ImageMap is in Navigation Controls, not Editor Controls:** Despite being image-related, ImageMap is categorized under Navigation Controls in the mkdocs nav, alongside HyperLink, Menu, SiteMapPath, and TreeView. -- **Style migration pattern:** Web Forms used `TableItemStyle` child elements (e.g., ``). The Blazor components use CSS class name string parameters (e.g., `TitleStyleCss="my-class"`). This is a key migration note for Calendar, and should be documented for any future components with similar style patterns. -- **Branch naming varies:** PR branches on upstream use `copilot/create-*` naming (not `copilot/fix-*` as referenced in some task descriptions). Always verify branch names via `git ls-remote` or GitHub API. -- **Deferred controls doc pattern:** For controls permanently excluded from the library, use `docs/Migration/DeferredControls.md` with per-control sections: What It Did β†’ Why It's Not Implemented β†’ Recommended Alternatives β†’ Migration Example (Before/After). Include a summary table at the end. This is distinct from the component doc pattern β€” deferred controls don't have Features Supported/Not Supported sections since they have zero Blazor implementation. -- **Migration section nav is semi-alphabetical:** The Migration section in mkdocs.yml keeps "Getting started" and "Migration Strategies" at the top, then remaining entries in alphabetical order. - -πŸ“Œ Team update (2026-02-10): Docs and samples must ship in the same sprint as the component β€” decided by Jeffrey T. Fritz -πŸ“Œ Team update (2026-02-10): PRs #328 (ASCX CLI) and #309 (VS Snippets) shelved indefinitely β€” decided by Jeffrey T. Fritz -πŸ“Œ Team update (2026-02-10): Sprint 1 gate review β€” ImageMap (#337) APPROVED, PageService (#327) APPROVED, ready to merge β€” decided by Forge -πŸ“Œ Team update (2026-02-10): Sprint 2 complete β€” Localize, MultiView+View, ChangePassword, CreateUserWizard shipped with docs, samples, tests. 709 tests passing. 41/53 components done. β€” decided by Squad -πŸ“Œ Team update (2026-02-11): Sprint 3 scope: DetailsView + PasswordRecovery. Chart/Substitution/Xml deferred. 48/53 β†’ target 50/53. β€” decided by Forge -πŸ“Œ Team update (2026-02-11): Colossus added as dedicated integration test engineer. Rogue retains bUnit unit tests. β€” decided by Jeffrey T. Fritz -- **PasswordRecovery doc pattern follows ChangePassword:** The PasswordRecovery doc mirrors the ChangePassword.md structure β€” same "Authentication Integration" warning admonition, same style migration guidance (TableItemStyle β†’ CSS classes via cascading parameters), same emphasis on event-driven architecture. This three-step wizard pattern (UserName β†’ Question β†’ Success) with `@ref` for calling component methods (SetQuestion, SkipToSuccess) is unique among login controls and should be noted for any future wizard-style components. -- **DetailsView doc covers generic component:** DetailsView is generic (`DetailsView`), unlike most other data controls. The doc explicitly calls out the `ItemType` requirement and the reflection-based auto-field generation. The Fields child content pattern with CascadingValue registration is worth noting for any future components that use child component registration. -- **Sprint 3 docs delivered:** DetailsView and PasswordRecovery documentation created with full structure (features, Web Forms syntax, Blazor syntax, HTML output, migration notes, examples, See Also). Added to mkdocs.yml nav (alphabetical) and linked in README.md. - -πŸ“Œ Team update (2026-02-12): Sprint 3 gate review β€” DetailsView and PasswordRecovery APPROVED. Action item: fix DetailsView docs to replace `DataSource=` with `Items=` in Blazor code samples. β€” decided by Forge - - Team update (2026-02-12): Milestone 4 planned Chart component with Chart.js via JS interop. 8 work items, design review required before implementation. decided by Forge + Squad - -- **Chart doc is first JS interop component:** The Chart component is unique in the library β€” it's the first to use JavaScript interop (Chart.js via ES module import). The doc template needed a new "HTML Output Exception" admonition pattern to explain why `` replaces ``. This pattern should be reused for any future components that deviate from identical HTML output. -- **DeferredControls.md updated for partial implementation:** Chart moved from fully-deferred to partially-implemented. The DeferredControls page now has a dual role: documenting controls not implemented at all (Substitution, Xml) AND documenting unsupported sub-features of implemented controls (27 unsupported chart types). This "partially implemented" pattern may apply to future controls. -- **Child component docs pattern:** Chart introduces a multi-component documentation pattern (Chart, ChartSeries, ChartArea, ChartLegend, ChartTitle) with separate parameter tables for each. This nested-component doc approach should be used for any future components with required child components. -- **Chart Type Gallery added:** Added a "Chart Type Gallery" section to `docs/DataControls/Chart.md` between "Chart Palettes" and "Web Forms Features NOT Supported". Contains 8 subsections (Column, Line, Bar, Pie, Doughnut, Area, Scatter, Stacked Column) each with a screenshot, `SeriesChartType` enum value, and 1-2 sentence usage guidance. Includes `!!! warning` admonitions on Pie and Doughnut for the Phase 1 palette limitation (single series color instead of per-segment colors). -- **Chart image path convention:** Chart screenshots live at `docs/images/chart/chart-{type}.png` (lowercase, hyphenated). Referenced from Chart.md using relative paths: `../images/chart/chart-{type}.png`. This `docs/images/{component}/` pattern should be used for any future component screenshots. -- **AccessKey and ToolTip missing across all WebControl-based components:** Neither `BaseWebFormsComponent` nor `BaseStyledComponent` defines `AccessKey` or `ToolTip` parameters. Every control inheriting from WebControl in Web Forms has these, so they are universally πŸ”΄ Missing. Adding them to `BaseStyledComponent` would fix all styled controls in one shot. -- **Label uses wrong base class for style support:** `Label` inherits `BaseWebFormsComponent` (no style) instead of `BaseStyledComponent`. Web Forms `Label` inherits from `WebControl` and supports all style properties (CssClass, BackColor, Font, etc.). This is the biggest gap for Label β€” 11 style properties are missing. -- **ListControl-derived components share common gaps:** ListBox, RadioButtonList (and CheckBoxList, DropDownList) all have the same missing properties: `AppendDataBoundItems`, `DataTextFormatString`, `CausesValidation`, `ValidationGroup`, and `TextChanged` event. These could be fixed once in a shared base. -- **Literal/Localize/PlaceHolder/View/MultiView are near-complete:** Controls inheriting from `Control` (not `WebControl`) have no style properties by design. The Blazor implementations are essentially feature-complete β€” matching all relevant properties and events. -- **Substitution and Xml are permanently deferral candidates:** Both controls are tightly coupled to server-side ASP.NET infrastructure (output caching and XSLT transformation respectively). Neither concept maps to Blazor's component model. Recommend documenting migration alternatives rather than implementing. -- **Style property computed but not directly settable:** Across all `BaseStyledComponent`-derived controls, the `Style` property is computed from BackColor/ForeColor/Font/etc. via `IStyle.ToStyle()`. Web Forms allowed direct `Style["property"] = "value"` assignment. This pattern difference is consistent but worth noting in migration guides. -- **Panel is the most feature-complete styled control:** Panel implements 6 out of 7 specific Web Forms properties (only BackImageUrl missing). Combined with full BaseStyledComponent inheritance, it has the highest coverage of any editor control. - - - Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent decided by Beast, Cyclops - Team update (2026-02-23): Chart implementation architecture consolidated (10 decisions) decided by Cyclops, Forge - Team update (2026-02-23): DetailsView/PasswordRecovery branch (sprint3) must be merged forward decided by Forge - - Team update (2026-02-23): BaseListControl introduced docs should reflect shared base for list controls decided by Cyclops - Team update (2026-02-23): Label AssociatedControlID switches rendered element document accessibility benefit decided by Cyclops - Team update (2026-02-23): Login controls now inherit BaseStyledComponent update docs for outer style support decided by Rogue, Cyclops - Team update (2026-02-23): Milestone 6 Work Plan ratified 54 WIs, Beast assigned branding (UI-11) and docs (UI-12) decided by Forge + + +### Core Context (2026-02-10 through 2026-02-25) + +Established doc structure: title β†’ intro (MS docs link) β†’ Features Supported β†’ NOT Supported β†’ Web Forms syntax β†’ Blazor syntax β†’ HTML Output β†’ Migration Notes β†’ Examples β†’ See Also. mkdocs.yml nav alphabetical within categories. Branch naming: `copilot/create-*`. Chart doc introduced JS interop "HTML Output Exception" pattern and multi-component (child) doc pattern. Chart Type Gallery at `docs/images/chart/chart-{type}.png`. Created PasswordRecovery doc (3-step wizard pattern), DetailsView doc (generic component pattern). Deferred controls use `docs/Migration/DeferredControls.md`. Feature audit: AccessKey/ToolTip base class gap, Substitution/Xml deferred, Style is computed. + +**Key patterns:** Style migration: TableItemStyle β†’ CSS class string parameters. DeferredControls.md has dual role (fully deferred + partially implemented). Chart screenshots at `docs/images/{component}/`. Shared sub-component docs linked from parent control docs. + Team update (2026-02-23): Menu Orientation requires Razor local variable workaround document this pattern decided by Jubilee - **Milestone 8 release-readiness docs polish:** Formally deferred Substitution and Xml controls in `status.md` (changed from πŸ”΄ Not Started to ⏸️ Deferred with rationale). Added Deferred column to summary table. Updated `docs/Migration/DeferredControls.md` to mark Chart as fully implemented (removed "Phase 1"/"Partial" hedging). Removed all "Phase 1"/"Phase 2/3" hedging from `docs/DataControls/Chart.md`. Fixed duplicate `DeferredControls.md` entry in `mkdocs.yml` and re-alphabetized Migration nav. Fixed broken `ImageMap` link in `README.md` (pointed to EditorControls, should be NavigationControls). Added missing doc links in README for MultiView, View, ChangePassword, CreateUserWizard. Marked Xml as deferred in README component list. @@ -62,3 +22,69 @@ Team update (2026-02-24): Menu auto-ID pattern components with JS interop should auto-generate IDs decided by Cyclops Team update (2026-02-24): M8 scope excludes version bump to 1.0 and release decided by Jeffrey T. Fritz Team update (2026-02-24): PagerSettings shared sub-component created update docs when component stabilizes decided by Cyclops + +- **M9 Doc Gap Audit (WI-09):** Audited all docs against M6-M8 features. GridView, TreeView, Menu, Validators (ControlToValidate), and Login are fully documented. Gaps found in: FormView (ItemCommand event, styles, PagerSettings not in Blazor sections), DetailsView (Caption missing, styles/PagerSettings listed as unsupported but may be stale), DataGrid (paging listed as unsupported, needs verification), ChangePassword (Orientation and TextLayout not documented despite Login having them), and PagerSettings (no dedicated doc page exists). Full report in `.ai-team/decisions/inbox/beast-m9-doc-audit.md`. +- **M9 Planning-Docs Historical Headers (WI-10):** Added `> ⚠️ Historical Snapshot (Pre-Milestone 6)` header to all 54 per-control audit files and SUMMARY.md in `planning-docs/`. Excluded README.md and MILESTONE*-PLAN.md files (still current/active). This prevents future contributors from treating pre-M6 gap data as current. +- **ChangePassword/Login parity gap:** Login.md documents Orientation and TextLayout with full reference tables and migration examples, but ChangePassword.md has neither. Both controls should have identical coverage for these shared layout properties. +- **ToolTip universality documented (WI-04):** Added ToolTip to Features Supported in Label.md, TextBox.md, and GridView.md (Button.md already had it). Added "Common Properties on All Styled Controls" section to Migration/readme.md explaining that ToolTip (renders as `title` attribute) is universally available on all BaseStyledComponent-derived controls. Used `!!! tip` admonition and code examples. No dedicated Common Properties page created β€” kept it inline in the migration guide for minimal footprint. + + Team update (2026-02-25): ToolTip moved to BaseStyledComponent (28+ controls), ValidationSummary comma-split fixed, SkinID boolstring fixed decided by Cyclops + Team update (2026-02-25): M9 plan ratified 12 WIs across P0/P1/P2, migration fidelity theme decided by Forge + +- **M9 Consolidated Audit Report:** Created `planning-docs/AUDIT-REPORT-M9.md` combining findings from three M9 audits: Doc Gap Audit (5 findings β†’ #359), Integration Test Coverage Audit (5 findings β†’ #358), and Sample Navigation Audit (19 findings β†’ #350). All 29 findings mapped to M10 GitHub Issues with 100% coverage. Report includes 6 additional post-M9 findings (component gaps and TreeView bug). Used the planning-docs historical snapshot header convention (`> ⚠️ Historical Snapshot (Milestone 9)`). +- **Audit report convention established:** Consolidated audit reports should live at `planning-docs/AUDIT-REPORT-M{N}.md` with the standard historical snapshot header, summary table, per-audit sections with findings + resolution status, and an appendix issue tracker. This pattern can be reused for future milestone audits. + + Team update (2026-02-25): TreeView NodeImage now checks ShowExpandCollapse independently of ShowLines; ExpandCollapseImage() helper added (#361) decided by Cyclops + + + Team update (2026-02-25): M12 introduces Migration Analysis Tool PoC (`bwfc-migrate` CLI, regex-based ASPX parsing, 3-phase roadmap) decided by Forge + +- **Issue #359 doc updates (M6-M8 features):** Updated 4 existing doc pages and created 1 new page to close gaps identified in the M9 Doc Gap Audit: + 1. **ChangePassword.md** β€” Added Orientation and TextLayout documentation with reference tables, enum usage examples, and migration Before/After, mirroring the Login.md pattern. + 2. **PagerSettings.md** (NEW) β€” Created dedicated doc page in DataControls/ covering all properties, PagerButtons/PagerPosition enums, usage with FormView/DetailsView/GridView, and migration notes. Added to mkdocs.yml nav alphabetically. + 3. **FormView.md** β€” Added ItemCommand, ItemCreated, PageIndexChanging/PageIndexChanged events to features and Blazor syntax. Added Caption/CaptionAlign, PagerSettings child element, PagerTemplate, and 7 style sub-components (RowStyle, EditRowStyle, InsertRowStyle, HeaderStyle, FooterStyle, EmptyDataRowStyle, PagerStyle) with reference table. + 4. **DetailsView.md** β€” Added Caption/CaptionAlign with reference table. Moved styles and PagerSettings from "NOT Supported" to "Supported" (were stale). Added 10-row style sub-components table and PagerSettings child element to Blazor syntax. Updated migration notes to reflect style child elements. + 5. **DataGrid.md** β€” Moved Paging, Sorting, Selection, and Editing from "NOT Supported" to "Supported" (all now implemented). Added AllowPaging/PageSize/CurrentPageIndex/PageIndexChanged, AllowSorting/SortCommand, SelectedIndex/EditItemIndex to Blazor syntax. Added paging and sorting examples with event handlers. +- **Documentation pattern: PagerSettings as shared sub-component doc:** PagerSettings is the first shared (non-control) sub-component to get its own dedicated doc page. It's referenced via `[PagerSettings](PagerSettings.md)` links from FormView, DetailsView, and GridView docs. Future shared sub-components (e.g., TableItemStyle if ever documented separately) should follow this pattern. + + + Team update (2026-02-25): All login controls (Login, LoginView, ChangePassword, PasswordRecovery, CreateUserWizard) now inherit from BaseStyledComponent decided by Cyclops + + Team update (2026-02-25): ComponentCatalog.cs now links all sample pages; new samples must be registered there decided by Jubilee + + + Team update (2026-02-25): Future milestone work should include a doc review pass to catch stale 'NOT Supported' entries decided by Beast + + Team update (2026-02-25): Shared sub-components of sufficient complexity get their own doc page (e.g., PagerSettings) decided by Beast + + Team update (2026-02-25): ListView now has full CRUD event parity (7 new events) docs may need updating decided by Cyclops + Team update (2026-02-25): Menu styles use MenuItemStyle pattern with IMenuStyleContainer docs may need updating decided by Cyclops + + Team update (2026-02-25): All new work MUST use feature branches pushed to origin with PR to upstream/dev. Never commit directly to dev. decided by Jeffrey T. Fritz + + + Team update (2026-02-25): Theme core types (#364) use nullable properties for StyleSheetTheme semantics, case-insensitive keys, empty-string default skin key. ThemeProvider is infrastructure, not a WebForms control. GetSkin returns null for missing entries. decided by Cyclops + + + Team update (2026-02-25): SkinID defaults to empty string, EnableTheming defaults to true. [Obsolete] removed these are now functional [Parameter] properties. decided by Cyclops + + + Team update (2026-02-25): ThemeConfiguration CascadingParameter wired into BaseStyledComponent (not BaseWebFormsComponent). ApplySkin runs in OnParametersSet with StyleSheetTheme semantics. Font properties checked individually. decided by Cyclops + +- **ThemesAndSkins.md updated for M10 PoC:** Updated `docs/Migration/ThemesAndSkins.md` to reflect the actual PoC implementation. Key changes: (1) Replaced "Current Status" admonition β€” removed stale warnings about `[Obsolete]` and `bool` SkinID, replaced with "PoC Implemented (M10)" status. (2) Updated Approach 2 code examples to use real class names (`ThemeConfiguration`, `ControlSkin`, `ThemeProvider`) and actual API (`AddSkin`/`GetSkin` with string control type name). (3) Updated "Recommended Approach" β†’ "Implemented Approach" with present tense. (4) Updated Implementation Roadmap β€” Phase 1 marked βœ… Complete, Phase 2 deferred items listed for M11. (5) Updated migration Before/After example to use real API with `using BlazorWebFormsComponents.Theming`. (6) Added "PoC Decisions" section documenting 7 design decisions (StyleSheetTheme default, missing SkinID handling, namespace, string keys, ControlSkin mirroring, BaseStyledComponent placement, .skin parser deferral). (7) Added disambiguation note to Approach 4 (DI) since its hypothetical class has the same name as the real implementation. All alternative approaches (1, 3, 4, 5) preserved as reference context. + + + + Team update (2026-02-25): HTML audit strategy approved decided by Forge + + Team update (2026-02-25): HTML audit milestones M11-M13 defined, existing M12M14, Skins/ThemesM15+ decided by Forge per Jeff's directive + + Team update (2026-02-26): Menu RenderingMode=Table added docs may need updating for dual rendering modes decided by Cyclops + + Team update (2026-02-26): Login+Identity strategy defined handler delegates, separate Identity package docs needed when implemented decided by Forge + +- **NamingContainer.md created:** New doc page at `docs/UtilityFeatures/NamingContainer.md` covering the NamingContainer component β€” a structural (no-HTML) component that establishes naming scopes for child component ID generation, equivalent to Web Forms `INamingContainer`. Documented UseCtl00Prefix parameter, nesting behavior, migration before/after, and relationship to WebFormsPage (which inherits NamingContainer). Added to mkdocs.yml nav alphabetically. Updated IDRendering.md with cross-references to NamingContainer and WebFormsPage in both the Naming Containers section and Related Documentation. +- **Structural component doc pattern:** Components that render no HTML of their own (purely structural) should lead with that fact prominently, since developers expect Blazor components to produce markup. The "renders no HTML" callout and the relationship comparison table (NamingContainer vs WebFormsPage) are reusable patterns for future structural/infrastructure component docs. + + Team update (2026-02-26): WebFormsPage unified wrapper inherits NamingContainer, adds Theme cascading, replaces separate wrappers decided by Jeffrey T. Fritz, Forge + Team update (2026-02-26): Login+Identity controls deferred to future milestone do not schedule docs decided by Jeffrey T. Fritz diff --git a/.ai-team/agents/colossus/history.md b/.ai-team/agents/colossus/history.md index c0a4355f4..020f01191 100644 --- a/.ai-team/agents/colossus/history.md +++ b/.ai-team/agents/colossus/history.md @@ -34,3 +34,78 @@ Added 9 smoke tests and 9 interaction tests for M7 sample pages: GridView Select Team update (2026-02-24): Menu auto-ID pattern Menu now auto-generates IDs, JS interop crash fixed decided by Cyclops Team update (2026-02-24): M8 scope excludes version bump to 1.0 and release decided by Jeffrey T. Fritz + + Team update (2026-02-25): Deployment pipeline patterns established compute Docker version with nbgv before build, gate on secrets, dual NuGet publishing decided by Forge + +## Summary: M9 Integration Test Coverage Audit (WI-11) + +Audited all sample page `@page` routes against ControlSampleTests.cs and InteractiveComponentTests.cs. Found 105 sample routes total; 100 covered by smoke tests, 57 interaction tests exist. Identified **5 pages without any smoke test**: ListView/CrudOperations (M7 β€” highest priority), Label, Panel/BackImageUrl, LoginControls/Orientation, and DataGrid/Styles. All other M7 features (GridView Selection/DisplayProperties, TreeView Selection/ExpandCollapse, Menu Selection, FormView Events/Styles, DetailsView Styles/Caption) have full smoke + interaction test coverage. Report written to `.ai-team/decisions/inbox/colossus-m9-test-audit.md`. + + Team update (2026-02-25): ToolTip moved to BaseStyledComponent (28+ controls), ValidationSummary comma-split fixed, SkinID boolstring fixed decided by Cyclops + Team update (2026-02-25): M9 plan ratified 12 WIs, migration fidelity decided by Forge + Team update (2026-02-25): Test coverage audit merged 5 gaps identified, P0: ListView CrudOperations decided by Colossus + + Team update (2026-02-25): Consolidated audit reports now use `planning-docs/AUDIT-REPORT-M{N}.md` pattern for all milestone audits decided by Beast + + + Team update (2026-02-25): M12 introduces Migration Analysis Tool PoC (`bwfc-migrate` CLI, regex-based ASPX parsing, 3-phase roadmap) decided by Forge + +## Summary: Issue #358 β€” 5 Missing Smoke Tests (2026-02-25) + +Added 5 missing smoke test InlineData entries to ControlSampleTests.cs covering all gaps identified in M9 audit: ListView/CrudOperations, Label, Panel/BackImageUrl, LoginControls/Orientation, DataGrid/Styles. All 5 sample pages verified to exist. Tests added as InlineData to existing Theory methods (EditorControl, DataControl, LoginControl). Build verified green (0 errors). + +## Learnings + +- Panel/BackImageUrl sample page uses external placeholder URLs (`via.placeholder.com`). The existing `VerifyPageLoadsWithoutErrors` filter for "Failed to load resource" handles this, so the smoke test works despite the team convention against external image URLs. +- LoginControls/Orientation is at `/ControlSamples/LoginControls/Orientation` (not under `/ControlSamples/Login` or `/ControlSamples/ChangePassword` as initially suggested in the issue). + + + + Team update (2026-02-25): Future milestone work should include a doc review pass to catch stale 'NOT Supported' entries decided by Beast + + Team update (2026-02-25): Shared sub-components of sufficient complexity get their own doc page (e.g., PagerSettings) decided by Beast + + Team update (2026-02-25): All login controls (Login, LoginView, ChangePassword, PasswordRecovery, CreateUserWizard) now inherit from BaseStyledComponent decided by Cyclops + + Team update (2026-02-25): ComponentCatalog.cs now links all sample pages; new samples must be registered there decided by Jubilee + + Team update (2026-02-25): ListView now has full CRUD event parity (7 new events) interaction tests may be needed decided by Cyclops + Team update (2026-02-25): Menu styles use MenuItemStyle with IMenuStyleContainer interaction tests may be needed decided by Cyclops + + Team update (2026-02-25): All new work MUST use feature branches pushed to origin with PR to upstream/dev. Never commit directly to dev. decided by Jeffrey T. Fritz + + + Team update (2026-02-25): Theme core types (#364) use nullable properties for StyleSheetTheme semantics, case-insensitive keys, empty-string default skin key. ThemeProvider is infrastructure, not a WebForms control. GetSkin returns null for missing entries. decided by Cyclops + + + Team update (2026-02-25): SkinID defaults to empty string, EnableTheming defaults to true. [Obsolete] removed these are now functional [Parameter] properties. decided by Cyclops + + + Team update (2026-02-25): ThemeConfiguration CascadingParameter wired into BaseStyledComponent (not BaseWebFormsComponent). ApplySkin runs in OnParametersSet with StyleSheetTheme semantics. Font properties checked individually. decided by Cyclops + + + Team update (2026-02-25): Calendar selection behavior review found 7 issues (1 P0: external SelectedDate sync, 4 P1: SelectWeekText default, SelectedDates sorting/mutability, style layering, 2 P2: test gaps, allocation) decided by Forge + + + Team update (2026-02-25): HTML audit strategy approved decided by Forge + + Team update (2026-02-25): HTML audit milestones M11-M13 defined, existing M12M14, Skins/ThemesM15+ decided by Forge per Jeff's directive + + Team update (2026-02-26): Menu RenderingMode=Table integration tests may need table-mode variants decided by Cyclops + + Team update (2026-02-26): Login+Identity strategy defined integration tests needed when handlers implemented decided by Forge + + Team update (2026-02-26): Data control divergence: normalization pipeline needs stripping and Blazor data control normalization decided by Forge + + Team update (2026-02-26): Post-fix capture: normalizer needs GUID ID stripping and empty style="" removal decided by Rogue + + Team update (2026-02-26): WebFormsPage unified wrapper inherits NamingContainer, adds Theme cascading, replaces separate wrappers decided by Jeffrey T. Fritz, Forge + Team update (2026-02-26): SharedSampleObjects is the single source for sample data parity between Blazor and WebForms decided by Jeffrey T. Fritz + +## Summary: PR #377 DetailsView Integration Test Fix (2026-02-26) + +Fixed 5 stale Customerβ†’Product assertions in InteractiveComponentTests.cs after DetailsView sample pages migrated from Customer to Product model (SharedSampleObjects.Models.Product). Changes: "Customer Details"β†’"Product Details" (Styles), "Customer Record"β†’"Product Record" (Caption), "No customers found."β†’"No products found." (EmptyData), Customer field namesβ†’Product field names in EditMode assertion message. All 7 DetailsView integration tests passing. + +## Learnings + +- When sample data models change (e.g., Customerβ†’Product), integration test assertions referencing model-specific text (header text, empty data messages, caption text, field name lists in assertion messages) must be updated in lockstep. Smoke tests won't catch these because they only verify page loads without errors β€” interactive tests with text-matching assertions are the ones that break. diff --git a/.ai-team/agents/cyclops/history.md b/.ai-team/agents/cyclops/history.md index b984ddbbb..aef9dd0f0 100644 --- a/.ai-team/agents/cyclops/history.md +++ b/.ai-team/agents/cyclops/history.md @@ -1,74 +1,142 @@ # Project Context -- **Owner:** Jeffrey T. Fritz (csharpfritz@users.noreply.github.com) +- **Owner:** Jeffrey T. Fritz - **Project:** BlazorWebFormsComponents β€” Blazor components emulating ASP.NET Web Forms controls for migration - **Stack:** C#, Blazor, .NET, ASP.NET Web Forms, bUnit, xUnit, MkDocs, Playwright - **Created:** 2026-02-10 ## Learnings - - - -### Summary: Milestones 1–3 Implementation (2026-02-10 through 2026-02-12) - -Built Calendar (enum fix, async events), ImageMap (BaseStyledComponent, Guid IDs, Enabled propagation), FileUpload (InputFile integration, path sanitization), PasswordRecovery (3-step wizard, per-step EditForm, SubmitButtonStyleβ†’LoginButtonStyle cascading), DetailsView (DataBoundComponent, auto-field reflection, mode switching, 10 events, paging). Image and Label upgraded to BaseStyledComponent (WI-15/WI-17). - -**Key patterns:** Enum files in `Enums/` with explicit int values. Instance-based Guid IDs (not static). `_ = callback.InvokeAsync()` for render-time events. `Path.GetFileName()` for file save security. Login controls inherit BaseWebFormsComponent with CascadingParameter styles. - -### Summary: Milestone 4 Chart Component (2026-02-12) - -Chart uses BaseStyledComponent, CascadingValue `"ParentChart"` for child registration. JS interop via separate `ChartJsInterop` (not shared service). `ChartConfigBuilder` is pure static class for testability. ChartWidth/ChartHeight as strings (avoid base Width/Height conflict). SeriesChartType.Point β†’ Chart.js "scatter". 8 Phase 1 types; unsupported throw NotSupportedException. ChartSeries data binding via reflection on Items/XValueMember/YValueMembers. - -### Summary: Feature Audit β€” Editor Controls A–I (2026-02-23) - -Audited 13 controls. Found: AccessKey/ToolTip missing from base class (universal gap), Image needs BaseStyledComponent, HyperLink.NavigateUrl naming mismatch, list controls missing DataTextFormatString/AppendDataBoundItems/CausesValidation/ValidationGroup, Calendar styles use CSS strings instead of TableItemStyle objects. - -πŸ“Œ Team update (2026-02-12): DetailsView auto-generated fields must render in Edit/Insert mode β€” decided by Cyclops - -- **DataBoundComponent style inheritance (WI-07):** Changed `BaseDataBoundComponent` to inherit `BaseStyledComponent` instead of `BaseWebFormsComponent`. This gives ALL data controls (GridView, DetailsView, FormView, ListView, DataGrid, DataList, Repeater, TreeView, AdRotator, BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList) the full IStyle property set (BackColor, CssClass, ForeColor, Font, etc.) from the base class. Removed duplicate IStyle implementations and CssClass properties from: GridView, DetailsView, DataGrid, DataList, TreeView, AdRotator, BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList. DataList kept its `new string Style` parameter (user-supplied CSS) but removed its IStyle declaration and 9 duplicate style properties. ListView kept its obsolete `new string Style` parameter. FormView and Repeater needed no changes. -- **CausesValidation pattern for non-button controls (WI-49):** CheckBox, RadioButton, and TextBox now have `CausesValidation` (bool, default true), `ValidationGroup` (string), and a `[CascadingParameter(Name = "ValidationGroupCoordinator")] ValidationGroupCoordinator Coordinator` β€” identical to the pattern in `ButtonBaseComponent`. Validation is triggered in the existing `HandleChange` method for CheckBox/RadioButton. TextBox has the parameters but no handler wiring because the component has no `@onchange` binding in its template. -- **BaseListControl base class (WI-47/48):** Created `DataBinding/BaseListControl.cs` inheriting `DataBoundComponent`. Consolidates `StaticItems`, `DataTextField`, `DataValueField`, `GetItems()`, and `GetPropertyValue()` from all 5 list controls (BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList). All 5 now inherit `BaseListControl`. This mirrors Web Forms `ListControl` as the shared base. -- **DataTextFormatString (WI-47):** `[Parameter] public string DataTextFormatString` on `BaseListControl`. Applied via `string.Format(DataTextFormatString, text)` at render time in `GetItems()`, not at bind time. Affects both static and data-bound items. Static items get a cloned `ListItem` to avoid mutating the source collection. -- **AppendDataBoundItems (WI-48):** `[Parameter] public bool AppendDataBoundItems` on `BaseListControl`, default `false`. When `false` and `Items != null`, static items are skipped in `GetItems()`. When `true`, static items always render before data-bound items. Matches Web Forms semantics where `DataBind()` clears `Items` by default. -- **List control inheritance chain:** `BaseListControl` β†’ `DataBoundComponent` β†’ `BaseDataBoundComponent` β†’ `BaseStyledComponent` β†’ `BaseWebFormsComponent`. All list controls get full style property set via this chain. -- **Orientation enum and Menu orientation (WI-50):** Created `Enums/Orientation.cs` (Horizontal=0, Vertical=1) using file-scoped namespace. Menu.razor.cs gets `[Parameter] public Orientation Orientation { get; set; } = Orientation.Vertical;`. Horizontal layout achieved via CSS class `horizontal` on the top-level `
    `, with `display: inline-block` on direct `
  • ` children. JS interop orientation string made dynamic from enum value. -- **Label AssociatedControlID (WI-51):** When `AssociatedControlID` is set (non-null/non-empty), Label renders `