diff --git a/README.md b/README.md index 73d50391f..25a9bca83 100644 --- a/README.md +++ b/README.md @@ -592,6 +592,11 @@ Agents are copied directly from the repo into `~/.claude/agents/` -- no conversi ./scripts/install.sh --tool claude-code ``` +**Windows:** +```cmd +scripts\install.bat -Tool claude-code +``` + Then activate in Claude Code: ``` Use the Frontend Developer agent to review this component. @@ -609,6 +614,11 @@ Agents are copied directly from the repo into `~/.github/agents/` and `~/.copilo ./scripts/install.sh --tool copilot ``` +**Windows:** +```cmd +scripts\install.bat -Tool copilot +``` + Then activate in GitHub Copilot: ``` Use the Frontend Developer agent to review this component. @@ -626,6 +636,11 @@ Each agent becomes a skill in `~/.gemini/antigravity/skills/agency-/`. ./scripts/install.sh --tool antigravity ``` +**Windows:** +```cmd +scripts\install.bat -Tool antigravity +``` + Activate in Gemini with Antigravity: ``` @agency-frontend-developer review this React component @@ -645,6 +660,12 @@ On a fresh clone, generate the Gemini extension files before running the install ./scripts/install.sh --tool gemini-cli ``` +**Windows:** +```cmd +scripts\convert.bat -Tool gemini-cli +scripts\install.bat -Tool gemini-cli +``` + See [integrations/gemini-cli/README.md](integrations/gemini-cli/README.md) for details. @@ -658,6 +679,12 @@ cd /your/project /path/to/agency-agents/scripts/install.sh --tool opencode ``` +**Windows:** +```cmd +cd /d C:\your\project +D:\path\to\agency-agents\scripts\install.bat -Tool opencode +``` + Or install globally: ```bash mkdir -p ~/.config/opencode/agents @@ -682,6 +709,12 @@ cd /your/project /path/to/agency-agents/scripts/install.sh --tool cursor ``` +**Windows:** +```cmd +cd /d C:\your\project +D:\path\to\agency-agents\scripts\install.bat -Tool cursor +``` + Rules are auto-applied when Cursor detects them in the project. Reference them explicitly: ``` Use the @security-engineer rules to review this code. @@ -700,6 +733,12 @@ cd /your/project /path/to/agency-agents/scripts/install.sh --tool aider ``` +**Windows:** +```cmd +cd /d C:\your\project +D:\path\to\agency-agents\scripts\install.bat -Tool aider +``` + Then reference agents in your Aider session: ``` Use the Frontend Developer agent to refactor this component. @@ -718,6 +757,12 @@ cd /your/project /path/to/agency-agents/scripts/install.sh --tool windsurf ``` +**Windows:** +```cmd +cd /d C:\your\project +D:\path\to\agency-agents\scripts\install.bat -Tool windsurf +``` + Reference agents in Windsurf's Cascade: ``` Use the Reality Checker agent to verify this is production ready. @@ -735,6 +780,11 @@ Each agent becomes a workspace with `SOUL.md`, `AGENTS.md`, and `IDENTITY.md` in ./scripts/install.sh --tool openclaw ``` +**Windows:** +```cmd +scripts\install.bat -Tool openclaw +``` + Agents are registered and available by `agentId` in OpenClaw sessions. See [integrations/openclaw/README.md](integrations/openclaw/README.md) for details. @@ -753,6 +803,13 @@ cd /your/project ./scripts/install.sh --tool qwen ``` +**Windows:** +```cmd +cd /d C:\your\project +D:\path\to\agency-agents\scripts\convert.bat -Tool qwen +D:\path\to\agency-agents\scripts\install.bat -Tool qwen +``` + **Usage in Qwen Code:** - Reference by name: `Use the frontend-developer agent to review this component` - Or let Qwen auto-delegate based on task context diff --git a/integrations/README.md b/integrations/README.md index c909700ff..64eefc734 100644 --- a/integrations/README.md +++ b/integrations/README.md @@ -14,6 +14,7 @@ supported agentic coding tools. - **[Cursor](#cursor)** — `.mdc` rule files in `cursor/` - **[Aider](#aider)** — `CONVENTIONS.md` in `aider/` - **[Windsurf](#windsurf)** — `.windsurfrules` in `windsurf/` +- **[Qwen Code](#qwen-code)** — SubAgent `.md` files in `qwen/` ## Quick Install @@ -32,7 +33,9 @@ supported agentic coding tools. ./scripts/install.sh --tool gemini-cli ``` -For project-scoped tools such as OpenCode, Cursor, Aider, and Windsurf, run +Windows examples are in the main README under [Tool-Specific Instructions](../README.md#tool-specific-instructions). + +For project-scoped tools such as OpenCode, Cursor, Aider, Windsurf, and Qwen, run the installer from your target project root as shown in the tool-specific sections below. @@ -172,3 +175,16 @@ cd /your/project && /path/to/agency-agents/scripts/install.sh --tool windsurf ``` See [windsurf/README.md](windsurf/README.md) for details. + +--- + +## Qwen Code + +SubAgents are generated as `.md` files and installed to `.qwen/agents/` in +your project root. + +```bash +cd /your/project && /path/to/agency-agents/scripts/convert.sh --tool qwen && /path/to/agency-agents/scripts/install.sh --tool qwen +``` + +See [../README.md#tool-specific-instructions](../README.md#tool-specific-instructions) for usage details. diff --git a/scripts/convert.bat b/scripts/convert.bat new file mode 100644 index 000000000..eebf0dbba --- /dev/null +++ b/scripts/convert.bat @@ -0,0 +1,6 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%convert.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/convert.ps1 b/scripts/convert.ps1 new file mode 100644 index 000000000..be5127c1b --- /dev/null +++ b/scripts/convert.ps1 @@ -0,0 +1,597 @@ +[CmdletBinding()] +param( + [string]$Tool = "all", + [string]$Out, + [Alias("h")] + [switch]$Help +) + +Set-StrictMode -Version 3.0 +$ErrorActionPreference = "Stop" + +$AgentDirs = @( + "academic", "design", "engineering", "game-development", "marketing", "paid-media", "sales", "product", "project-management", + "testing", "support", "spatial-computing", "specialized" +) + +$ValidTools = @("antigravity", "gemini-cli", "opencode", "cursor", "aider", "windsurf", "openclaw", "qwen", "all") +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir +$OutDir = if ($PSBoundParameters.ContainsKey("Out")) { $Out } else { Join-Path $RepoRoot "integrations" } +$Today = Get-Date -Format "yyyy-MM-dd" + +$Script:AiderAccumulator = New-Object System.Text.StringBuilder +$Script:WindsurfAccumulator = New-Object System.Text.StringBuilder + +function Get-Usage { + return @" +convert.ps1 -- Convert agency agent .md files into tool-specific formats. + +Reads all agent files from the standard category directories and outputs +converted files to integrations//. + +Usage: + ./scripts/convert.ps1 [-Tool ] [-Out ] [-Help] + +Tools: + antigravity -- Antigravity skill files (~/.gemini/antigravity/skills/) + gemini-cli -- Gemini CLI extension (skills/ + gemini-extension.json) + opencode -- OpenCode agent files (.opencode/agents/*.md) + cursor -- Cursor rule files (.cursor/rules/*.mdc) + aider -- Single CONVENTIONS.md for Aider + windsurf -- Single .windsurfrules for Windsurf + openclaw -- OpenClaw SOUL.md files (openclaw_workspace//SOUL.md) + qwen -- Qwen Code SubAgent files (~/.qwen/agents/*.md) + all -- All tools (default) +"@ +} + +function Write-Info { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-WarnMsg { + param([string]$Message) + Write-Host "[!!] $Message" -ForegroundColor Yellow +} + +function Write-ErrMsg { + param([string]$Message) + Write-Host "[ERR] $Message" -ForegroundColor Red +} + +function Write-Header { + param([string]$Message) + Write-Host "" + Write-Host $Message +} + +function Ensure-Directory { + param([string]$Path) + + if (-not (Test-Path -Path $Path -PathType Container)) { + $null = New-Item -ItemType Directory -Path $Path -Force + } +} + +function Write-Utf8File { + param( + [string]$Path, + [string]$Content + ) + + $parent = Split-Path -Parent $Path + if (-not [string]::IsNullOrWhiteSpace($parent)) { + Ensure-Directory -Path $parent + } + + if (-not $Content.EndsWith("`n")) { + $Content += "`n" + } + + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Write-ToolArtifact { + param( + [string]$RelativePath, + [string]$Content + ) + + $outPath = Join-Path $OutDir $RelativePath + Write-Utf8File -Path $outPath -Content $Content +} + +function Read-Utf8Lines { + param([string]$Path) + + $utf8 = New-Object System.Text.UTF8Encoding($false) + return [System.IO.File]::ReadAllLines($Path, $utf8) +} + +function Join-Lines { + param([string[]]$Lines) + + if ($null -eq $Lines -or $Lines.Count -eq 0) { + return "" + } + + return [string]::Join("`n", $Lines) +} + +function Get-AgentRecord { + param([string]$FilePath) + + $lines = Read-Utf8Lines -Path $FilePath + if ($lines.Count -lt 3) { + return $null + } + + if ($lines[0] -ne "---") { + return $null + } + + $endFrontmatter = -1 + for ($i = 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -eq "---") { + $endFrontmatter = $i + break + } + } + + if ($endFrontmatter -lt 1) { + return $null + } + + $frontmatterLines = @() + if ($endFrontmatter -gt 1) { + $frontmatterLines = $lines[1..($endFrontmatter - 1)] + } + + $bodyLines = @() + if ($endFrontmatter + 1 -le $lines.Count - 1) { + $bodyLines = $lines[($endFrontmatter + 1)..($lines.Count - 1)] + } + + $fields = @{} + foreach ($line in $frontmatterLines) { + if ($line -match '^\s*([A-Za-z0-9_-]+):\s*(.*)\s*$') { + $key = $matches[1].ToLowerInvariant() + if (-not $fields.ContainsKey($key)) { + $fields[$key] = $matches[2].Trim() + } + } + } + + $name = if ($fields.ContainsKey("name")) { $fields["name"] } else { "" } + if ([string]::IsNullOrWhiteSpace($name)) { + return $null + } + + $description = if ($fields.ContainsKey("description")) { $fields["description"] } else { "" } + $color = if ($fields.ContainsKey("color")) { $fields["color"] } else { "" } + $tools = if ($fields.ContainsKey("tools")) { $fields["tools"] } else { "" } + $emoji = if ($fields.ContainsKey("emoji")) { $fields["emoji"] } else { "" } + $vibe = if ($fields.ContainsKey("vibe")) { $fields["vibe"] } else { "" } + + return [pscustomobject]@{ + Path = $FilePath + Name = $name + Description = $description + Color = $color + Tools = $tools + Emoji = $emoji + Vibe = $vibe + BodyLines = $bodyLines + Body = (Join-Lines -Lines $bodyLines) + } +} + +function Slugify { + param([string]$Value) + + $slug = $Value.ToLowerInvariant() + $slug = [regex]::Replace($slug, "[^a-z0-9]", "-") + $slug = [regex]::Replace($slug, "-+", "-") + $slug = $slug.Trim('-') + return $slug +} + +function Resolve-OpenCodeColor { + param([string]$Color) + + $normalized = if ($null -eq $Color) { "" } else { $Color.Trim().ToLowerInvariant() } + + $namedMap = @{ + "cyan" = "#00FFFF" + "blue" = "#3498DB" + "green" = "#2ECC71" + "red" = "#E74C3C" + "purple" = "#9B59B6" + "orange" = "#F39C12" + "teal" = "#008080" + "indigo" = "#6366F1" + "pink" = "#E84393" + "gold" = "#EAB308" + "amber" = "#F59E0B" + "neon-green" = "#10B981" + "neon-cyan" = "#06B6D4" + "metallic-blue" = "#3B82F6" + "yellow" = "#EAB308" + "violet" = "#8B5CF6" + "rose" = "#F43F5E" + "lime" = "#84CC16" + "gray" = "#6B7280" + "fuchsia" = "#D946EF" + } + + $mapped = if ($namedMap.ContainsKey($normalized)) { $namedMap[$normalized] } else { $normalized } + + if ($mapped -match '^#[0-9a-fA-F]{6}$') { + return ("#{0}" -f $mapped.Substring(1).ToUpperInvariant()) + } + + if ($mapped -match '^[0-9a-fA-F]{6}$') { + return ("#{0}" -f $mapped.ToUpperInvariant()) + } + + return "#6B7280" +} + +function Initialize-Accumulators { + $Script:AiderAccumulator.Clear() | Out-Null + $Script:WindsurfAccumulator.Clear() | Out-Null + + $aiderHeader = @" +# The Agency -- AI Agent Conventions +# +# This file provides Aider with the full roster of specialized AI agents from +# The Agency (https://github.com/msitarzewski/agency-agents). +# +# To activate an agent, reference it by name in your Aider session prompt, e.g.: +# "Use the Frontend Developer agent to review this component." +# +# Generated by scripts/convert.ps1 -- do not edit manually. +"@ + + $windsurfHeader = @" +# The Agency -- AI Agent Rules for Windsurf +# +# Full roster of specialized AI agents from The Agency. +# To activate an agent, reference it by name in your Windsurf conversation. +# +# Generated by scripts/convert.ps1 -- do not edit manually. +"@ + + [void]$Script:AiderAccumulator.AppendLine($aiderHeader) + [void]$Script:WindsurfAccumulator.AppendLine($windsurfHeader) +} + +function Convert-Antigravity { + param([pscustomobject]$Agent) + + $slug = "agency-$(Slugify -Value $Agent.Name)" + $relativePath = Join-Path (Join-Path "antigravity" $slug) "SKILL.md" + + $content = @" +--- +name: $slug +description: $($Agent.Description) +risk: low +source: community +date_added: '$Today' +--- +$($Agent.Body) +"@ + + Write-ToolArtifact -RelativePath $relativePath -Content $content +} + +function Convert-GeminiCli { + param([pscustomobject]$Agent) + + $slug = Slugify -Value $Agent.Name + $relativePath = Join-Path (Join-Path (Join-Path "gemini-cli" "skills") $slug) "SKILL.md" + + $content = @" +--- +name: $slug +description: $($Agent.Description) +--- +$($Agent.Body) +"@ + + Write-ToolArtifact -RelativePath $relativePath -Content $content +} + +function Convert-OpenCode { + param([pscustomobject]$Agent) + + $slug = Slugify -Value $Agent.Name + $color = Resolve-OpenCodeColor -Color $Agent.Color + $relativePath = Join-Path (Join-Path "opencode" "agents") "$slug.md" + + $content = @" +--- +name: $($Agent.Name) +description: $($Agent.Description) +mode: subagent +color: '$color' +--- +$($Agent.Body) +"@ + + Write-ToolArtifact -RelativePath $relativePath -Content $content +} + +function Convert-Cursor { + param([pscustomobject]$Agent) + + $slug = Slugify -Value $Agent.Name + $relativePath = Join-Path (Join-Path "cursor" "rules") "$slug.mdc" + + $content = @" +--- +description: $($Agent.Description) +globs: "" +alwaysApply: false +--- +$($Agent.Body) +"@ + + Write-ToolArtifact -RelativePath $relativePath -Content $content +} + +function Split-OpenClawBody { + param([string[]]$BodyLines) + + $soul = New-Object System.Text.StringBuilder + $agents = New-Object System.Text.StringBuilder + $currentTarget = "agents" + $currentSection = New-Object System.Collections.Generic.List[string] + + $flushSection = { + if ($currentSection.Count -eq 0) { + return + } + + $sectionText = [string]::Join("`n", $currentSection) + "`n" + if ($currentTarget -eq "soul") { + [void]$soul.Append($sectionText) + } + else { + [void]$agents.Append($sectionText) + } + + $currentSection.Clear() + } + + foreach ($line in $BodyLines) { + if ($line -match '^##\s') { + & $flushSection + + $headerLower = $line.ToLowerInvariant() + if ( + $headerLower -match 'identity' -or + $headerLower -match 'communication' -or + $headerLower -match 'style' -or + $headerLower -match 'critical.rule' -or + $headerLower -match 'rules.you.must.follow' + ) { + $currentTarget = "soul" + } + else { + $currentTarget = "agents" + } + } + + [void]$currentSection.Add($line) + } + + & $flushSection + + return [pscustomobject]@{ + Soul = $soul.ToString() + Agents = $agents.ToString() + } +} + +function Convert-OpenClaw { + param([pscustomobject]$Agent) + + $slug = Slugify -Value $Agent.Name + $outDir = Join-Path (Join-Path $OutDir "openclaw") $slug + Ensure-Directory -Path $outDir + + $split = Split-OpenClawBody -BodyLines $Agent.BodyLines + Write-Utf8File -Path (Join-Path $outDir "SOUL.md") -Content $split.Soul + Write-Utf8File -Path (Join-Path $outDir "AGENTS.md") -Content $split.Agents + + $identity = "" + if (-not [string]::IsNullOrWhiteSpace($Agent.Emoji) -and -not [string]::IsNullOrWhiteSpace($Agent.Vibe)) { + $identity = @" +# $($Agent.Emoji) $($Agent.Name) +$($Agent.Vibe) +"@ + } + else { + $identity = @" +# $($Agent.Name) +$($Agent.Description) +"@ + } + + Write-Utf8File -Path (Join-Path $outDir "IDENTITY.md") -Content $identity +} + +function Convert-Qwen { + param([pscustomobject]$Agent) + + $slug = Slugify -Value $Agent.Name + $relativePath = Join-Path (Join-Path "qwen" "agents") "$slug.md" + + if (-not [string]::IsNullOrWhiteSpace($Agent.Tools)) { + $content = @" +--- +name: $slug +description: $($Agent.Description) +tools: $($Agent.Tools) +--- +$($Agent.Body) +"@ + } + else { + $content = @" +--- +name: $slug +description: $($Agent.Description) +--- +$($Agent.Body) +"@ + } + + Write-ToolArtifact -RelativePath $relativePath -Content $content +} + +function Accumulate-Aider { + param([pscustomobject]$Agent) + + $segment = @" + +--- + +## $($Agent.Name) + +> $($Agent.Description) + +$($Agent.Body) +"@ + + [void]$Script:AiderAccumulator.AppendLine($segment) +} + +function Accumulate-Windsurf { + param([pscustomobject]$Agent) + + $segment = @" + +================================================================================ +## $($Agent.Name) +$($Agent.Description) +================================================================================ + +$($Agent.Body) +"@ + + [void]$Script:WindsurfAccumulator.AppendLine($segment) +} + +function Get-AgentFilesInDir { + param([string]$DirectoryPath) + + if (-not (Test-Path -Path $DirectoryPath -PathType Container)) { + return @() + } + + return Get-ChildItem -Path $DirectoryPath -Filter "*.md" -File -Recurse | Sort-Object -Property FullName +} + +function Run-Conversions { + param([string]$TargetTool) + + $count = 0 + + foreach ($dir in $AgentDirs) { + $dirPath = Join-Path $RepoRoot $dir + $files = Get-AgentFilesInDir -DirectoryPath $dirPath + + foreach ($file in $files) { + $agent = Get-AgentRecord -FilePath $file.FullName + if ($null -eq $agent) { + continue + } + + switch ($TargetTool) { + "antigravity" { Convert-Antigravity -Agent $agent; break } + "gemini-cli" { Convert-GeminiCli -Agent $agent; break } + "opencode" { Convert-OpenCode -Agent $agent; break } + "cursor" { Convert-Cursor -Agent $agent; break } + "openclaw" { Convert-OpenClaw -Agent $agent; break } + "qwen" { Convert-Qwen -Agent $agent; break } + "aider" { Accumulate-Aider -Agent $agent; break } + "windsurf" { Accumulate-Windsurf -Agent $agent; break } + } + + $count++ + } + } + + return $count +} + +function Write-GeminiManifest { + $manifest = @' +{ + "name": "agency-agents", + "version": "1.0.0" +} +'@ + Write-ToolArtifact -RelativePath (Join-Path "gemini-cli" "gemini-extension.json") -Content $manifest +} + +function Write-SingleFileOutputs { + Write-ToolArtifact -RelativePath (Join-Path "aider" "CONVENTIONS.md") -Content $Script:AiderAccumulator.ToString() + Write-ToolArtifact -RelativePath (Join-Path "windsurf" ".windsurfrules") -Content $Script:WindsurfAccumulator.ToString() +} + +if ($Help) { + Write-Output (Get-Usage) + exit 0 +} + +$Tool = $Tool.ToLowerInvariant() +if (-not ($ValidTools -contains $Tool)) { + Write-ErrMsg "Unknown tool '$Tool'. Valid: $($ValidTools -join ' ')" + exit 1 +} + +Initialize-Accumulators + +Write-Header "The Agency -- Converting agents to tool-specific formats" +Write-Host (" Repo: {0}" -f $RepoRoot) +Write-Host (" Output: {0}" -f $OutDir) +Write-Host (" Tool: {0}" -f $Tool) +Write-Host (" Date: {0}" -f $Today) + +$toolsToRun = @() +if ($Tool -eq "all") { + $toolsToRun = @("antigravity", "gemini-cli", "opencode", "cursor", "aider", "windsurf", "openclaw", "qwen") +} +else { + $toolsToRun = @($Tool) +} + +$total = 0 +foreach ($targetTool in $toolsToRun) { + Write-Header "Converting: $targetTool" + $count = Run-Conversions -TargetTool $targetTool + $total += [int]$count + + if ($targetTool -eq "gemini-cli") { + Write-GeminiManifest + Write-Info "Wrote gemini-extension.json" + } + + Write-Info "Converted $count agents for $targetTool" +} + +if ($Tool -eq "all" -or $Tool -eq "aider" -or $Tool -eq "windsurf") { + Write-SingleFileOutputs + Write-Info "Wrote integrations/aider/CONVENTIONS.md" + Write-Info "Wrote integrations/windsurf/.windsurfrules" +} + +Write-Host "" +Write-Info "Done. Total conversions: $total" diff --git a/scripts/install.bat b/scripts/install.bat new file mode 100644 index 000000000..bd74b564d --- /dev/null +++ b/scripts/install.bat @@ -0,0 +1,6 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%install.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 000000000..f3ab19c40 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,658 @@ +[CmdletBinding()] +param( + [string]$Tool = "all", + [switch]$Interactive, + [Alias("no-interactive")] + [switch]$NoInteractive, + [Alias("h")] + [switch]$Help +) + +Set-StrictMode -Version 3.0 +$ErrorActionPreference = "Stop" + +$AllTools = @("claude-code", "copilot", "antigravity", "gemini-cli", "opencode", "openclaw", "cursor", "aider", "windsurf", "qwen") +$AgentDirs = @( + "academic", "design", "engineering", "game-development", "marketing", "paid-media", "sales", "product", "project-management", + "testing", "support", "spatial-computing", "specialized" +) + +function Get-Usage { + return @" +install.ps1 -- Install The Agency agents into your local agentic tool(s). + +Reads converted files from integrations/ and copies them to the appropriate +config directory for each tool. + +Usage: + ./scripts/install.ps1 [-Tool ] [-Interactive] [-NoInteractive] [-Help] + +Tools: + claude-code -- Copy agents to ~/.claude/agents/ + copilot -- Copy agents to ~/.github/agents/ and ~/.copilot/agents/ + antigravity -- Copy skills to ~/.gemini/antigravity/skills/ + gemini-cli -- Install extension to ~/.gemini/extensions/agency-agents/ + opencode -- Copy agents to .opencode/agents/ in current directory + cursor -- Copy rules to .cursor/rules/ in current directory + aider -- Copy CONVENTIONS.md to current directory + windsurf -- Copy .windsurfrules to current directory + openclaw -- Copy workspaces to ~/.openclaw/agency-agents/ + qwen -- Copy SubAgents to .qwen/agents/ in current directory + all -- Install for all detected tools (default) +"@ +} + +function Write-Ok { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-WarnMsg { + param([string]$Message) + Write-Host "[!!] $Message" -ForegroundColor Yellow +} + +function Write-ErrMsg { + param([string]$Message) + Write-Host "[ERR] $Message" -ForegroundColor Red +} + +function Write-Header { + param([string]$Message) + Write-Host "" + Write-Host $Message +} + +function Test-CommandAvailable { + param([string]$Name) + return $null -ne (Get-Command -Name $Name -ErrorAction SilentlyContinue) +} + +function Get-ToolLabel { + param([string]$ToolName) + + switch ($ToolName) { + "claude-code" { return "Claude Code (~/.claude/agents)" } + "copilot" { return "Copilot (~/.github + ~/.copilot)" } + "antigravity" { return "Antigravity (~/.gemini/antigravity)" } + "gemini-cli" { return "Gemini CLI (~/.gemini/extensions)" } + "opencode" { return "OpenCode (.opencode/agents)" } + "openclaw" { return "OpenClaw (~/.openclaw)" } + "cursor" { return "Cursor (.cursor/rules)" } + "aider" { return "Aider (CONVENTIONS.md)" } + "windsurf" { return "Windsurf (.windsurfrules)" } + "qwen" { return "Qwen Code (.qwen/agents)" } + default { return $ToolName } + } +} + +function Test-ToolDetected { + param([string]$ToolName) + + switch ($ToolName) { + "claude-code" { return (Test-Path -Path (Join-Path $HOME ".claude") -PathType Container) } + "copilot" { + return (Test-CommandAvailable "code") -or + (Test-Path -Path (Join-Path $HOME ".github") -PathType Container) -or + (Test-Path -Path (Join-Path $HOME ".copilot") -PathType Container) + } + "antigravity" { + $path = Join-Path (Join-Path (Join-Path $HOME ".gemini") "antigravity") "skills" + return (Test-Path -Path $path -PathType Container) + } + "gemini-cli" { return (Test-CommandAvailable "gemini") -or (Test-Path -Path (Join-Path $HOME ".gemini") -PathType Container) } + "opencode" { return (Test-CommandAvailable "opencode") -or (Test-Path -Path (Join-Path (Join-Path $HOME ".config") "opencode") -PathType Container) } + "openclaw" { return (Test-CommandAvailable "openclaw") -or (Test-Path -Path (Join-Path $HOME ".openclaw") -PathType Container) } + "cursor" { return (Test-CommandAvailable "cursor") -or (Test-Path -Path (Join-Path $HOME ".cursor") -PathType Container) } + "aider" { return (Test-CommandAvailable "aider") } + "windsurf" { return (Test-CommandAvailable "windsurf") -or (Test-Path -Path (Join-Path $HOME ".codeium") -PathType Container) } + "qwen" { return (Test-CommandAvailable "qwen") -or (Test-Path -Path (Join-Path $HOME ".qwen") -PathType Container) } + default { return $false } + } +} + +function Ensure-Directory { + param([string]$Path) + + if (-not (Test-Path -Path $Path -PathType Container)) { + $null = New-Item -ItemType Directory -Path $Path -Force + } +} + +function Copy-FlatFiles { + param( + [string]$Source, + [string]$Filter, + [string]$Destination + ) + + Ensure-Directory -Path $Destination + + $count = 0 + $files = Get-ChildItem -Path $Source -Filter $Filter -File | Sort-Object -Property Name + foreach ($file in $files) { + Copy-Item -Path $file.FullName -Destination $Destination -Force + $count++ + } + + return $count +} + +function Install-SingleProjectFile { + param( + [string]$Source, + [string]$MissingMessage, + [string]$Destination, + [string]$ExistsWarning, + [string]$SuccessMessage, + [string]$ScopeWarning + ) + + if (-not (Test-Path -Path $Source -PathType Leaf)) { + throw $MissingMessage + } + + if (Test-Path -Path $Destination -PathType Leaf) { + Write-WarnMsg $ExistsWarning + return + } + + Copy-Item -Path $Source -Destination $Destination -Force + Write-Ok $SuccessMessage + Write-WarnMsg $ScopeWarning +} + +function Read-Utf8FirstLine { + param([string]$Path) + + $utf8 = New-Object System.Text.UTF8Encoding($false) + $reader = [System.IO.StreamReader]::new($Path, $utf8) + try { + return $reader.ReadLine() + } + finally { + $reader.Dispose() + } +} + +function Get-AgentFiles { + param([string]$RepoRoot) + + $result = New-Object System.Collections.Generic.List[string] + + foreach ($dir in $AgentDirs) { + $dirPath = Join-Path $RepoRoot $dir + if (-not (Test-Path -Path $dirPath -PathType Container)) { + continue + } + + $files = Get-ChildItem -Path $dirPath -Filter "*.md" -File -Recurse | Sort-Object -Property FullName + foreach ($file in $files) { + $firstLine = Read-Utf8FirstLine -Path $file.FullName + if ($firstLine -eq "---") { + $result.Add($file.FullName) + } + } + } + + return $result.ToArray() +} + +function Copy-AgentMarkdown { + param( + [string]$RepoRoot, + [string]$Destination + ) + + Ensure-Directory -Path $Destination + $count = 0 + $files = Get-AgentFiles -RepoRoot $RepoRoot + + foreach ($file in $files) { + Copy-Item -Path $file -Destination $Destination -Force + $count++ + } + + return $count +} + +function Select-ToolsInteractive { + param([string[]]$Tools) + + $selected = @{} + $detected = @{} + + foreach ($toolName in $Tools) { + $isDetected = Test-ToolDetected -ToolName $toolName + $selected[$toolName] = $isDetected + $detected[$toolName] = $isDetected + } + + while ($true) { + Write-Host "" + Write-Host "The Agency -- Tool Installer" + Write-Host "System scan: [*] = detected on this machine" + Write-Host "" + + for ($i = 0; $i -lt $Tools.Count; $i++) { + $toolName = $Tools[$i] + $checked = if ($selected[$toolName]) { "[x]" } else { "[ ]" } + $scan = if ($detected[$toolName]) { "[*]" } else { "[ ]" } + $label = Get-ToolLabel -ToolName $toolName + Write-Host (" {0} {1,2}) {2} {3}" -f $checked, ($i + 1), $scan, $label) + } + + Write-Host "" + Write-Host (" [1-{0}] toggle [a] all [n] none [d] detected" -f $Tools.Count) + Write-Host " [Enter] install [q] quit" + $selectionInput = Read-Host ">>" + $selectionInput = $selectionInput.Trim() + + if ($selectionInput -eq "") { + $hasAny = $false + foreach ($toolName in $Tools) { + if ($selected[$toolName]) { + $hasAny = $true + break + } + } + + if ($hasAny) { + break + } + + Write-WarnMsg "Nothing selected -- pick a tool or press q to quit." + continue + } + + switch -Regex ($selectionInput.ToLowerInvariant()) { + "^q$" { + Write-Host "" + Write-Ok "Aborted." + exit 0 + } + "^a$" { + foreach ($toolName in $Tools) { + $selected[$toolName] = $true + } + continue + } + "^n$" { + foreach ($toolName in $Tools) { + $selected[$toolName] = $false + } + continue + } + "^d$" { + foreach ($toolName in $Tools) { + $selected[$toolName] = $detected[$toolName] + } + continue + } + default { + $tokens = $selectionInput -split "[ ,\t]+" | Where-Object { $_ -ne "" } + $toggled = $false + + foreach ($token in $tokens) { + $num = 0 + if ([int]::TryParse($token, [ref]$num)) { + $idx = $num - 1 + if ($idx -ge 0 -and $idx -lt $Tools.Count) { + $targetTool = $Tools[$idx] + $selected[$targetTool] = -not $selected[$targetTool] + $toggled = $true + } + } + } + + if (-not $toggled) { + Write-ErrMsg ("Invalid input. Enter a number 1-{0}, or a command." -f $Tools.Count) + } + } + } + } + + $chosen = New-Object System.Collections.Generic.List[string] + foreach ($toolName in $Tools) { + if ($selected[$toolName]) { + $chosen.Add($toolName) + } + } + + return $chosen.ToArray() +} + +function Install-ClaudeCode { + param([string]$RepoRoot) + + $dest = Join-Path (Join-Path $HOME ".claude") "agents" + $count = Copy-AgentMarkdown -RepoRoot $RepoRoot -Destination $dest + Write-Ok "Claude Code: $count agents -> $dest" +} + +function Install-Copilot { + param([string]$RepoRoot) + + $destGithub = Join-Path (Join-Path $HOME ".github") "agents" + $destCopilot = Join-Path (Join-Path $HOME ".copilot") "agents" + + $count = Copy-AgentMarkdown -RepoRoot $RepoRoot -Destination $destGithub + Ensure-Directory -Path $destCopilot + $files = Get-AgentFiles -RepoRoot $RepoRoot + foreach ($file in $files) { + Copy-Item -Path $file -Destination $destCopilot -Force + } + + Write-Ok "Copilot: $count agents -> $destGithub" + Write-Ok "Copilot: $count agents -> $destCopilot" +} + +function Install-Antigravity { + param([string]$Integrations) + + $src = Join-Path $Integrations "antigravity" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/antigravity missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Join-Path (Join-Path $HOME ".gemini") "antigravity") "skills" + Ensure-Directory -Path $dest + + $count = 0 + $folders = Get-ChildItem -Path $src -Directory | Sort-Object -Property Name + foreach ($folder in $folders) { + $target = Join-Path $dest $folder.Name + Ensure-Directory -Path $target + + $skillPath = Join-Path $folder.FullName "SKILL.md" + if (-not (Test-Path -Path $skillPath -PathType Leaf)) { + throw "Missing expected file: $skillPath" + } + + Copy-Item -Path $skillPath -Destination (Join-Path $target "SKILL.md") -Force + $count++ + } + + Write-Ok "Antigravity: $count skills -> $dest" +} + +function Install-GeminiCli { + param([string]$Integrations) + + $src = Join-Path $Integrations "gemini-cli" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/gemini-cli missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Join-Path (Join-Path $HOME ".gemini") "extensions") "agency-agents" + Ensure-Directory -Path (Join-Path $dest "skills") + + $manifest = Join-Path $src "gemini-extension.json" + if (-not (Test-Path -Path $manifest -PathType Leaf)) { + throw "Missing expected file: $manifest" + } + Copy-Item -Path $manifest -Destination (Join-Path $dest "gemini-extension.json") -Force + + $skillsRoot = Join-Path $src "skills" + if (-not (Test-Path -Path $skillsRoot -PathType Container)) { + throw "Missing expected directory: $skillsRoot" + } + + $count = 0 + $skillDirs = Get-ChildItem -Path $skillsRoot -Directory | Sort-Object -Property Name + foreach ($skillDir in $skillDirs) { + $target = Join-Path (Join-Path $dest "skills") $skillDir.Name + Ensure-Directory -Path $target + + $skillPath = Join-Path $skillDir.FullName "SKILL.md" + if (-not (Test-Path -Path $skillPath -PathType Leaf)) { + throw "Missing expected file: $skillPath" + } + + Copy-Item -Path $skillPath -Destination (Join-Path $target "SKILL.md") -Force + $count++ + } + + Write-Ok "Gemini CLI: $count skills -> $dest" +} + +function Install-OpenCode { + param([string]$Integrations) + + $src = Join-Path (Join-Path $Integrations "opencode") "agents" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/opencode missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Get-Location).Path ".opencode\agents" + $count = Copy-FlatFiles -Source $src -Filter "*.md" -Destination $dest + + Write-Ok "OpenCode: $count agents -> $dest" + Write-WarnMsg "OpenCode: project-scoped. Run from your project root to install there." +} + +function Install-OpenClaw { + param([string]$Integrations) + + $src = Join-Path $Integrations "openclaw" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/openclaw missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Join-Path $HOME ".openclaw") "agency-agents" + Ensure-Directory -Path $dest + + $count = 0 + $openclawInstalled = Test-CommandAvailable -Name "openclaw" + $dirs = Get-ChildItem -Path $src -Directory | Sort-Object -Property Name + + foreach ($dir in $dirs) { + $workspace = Join-Path $dest $dir.Name + Ensure-Directory -Path $workspace + + foreach ($name in @("SOUL.md", "AGENTS.md", "IDENTITY.md")) { + $from = Join-Path $dir.FullName $name + if (-not (Test-Path -Path $from -PathType Leaf)) { + throw "Missing expected file: $from" + } + Copy-Item -Path $from -Destination (Join-Path $workspace $name) -Force + } + + if ($openclawInstalled) { + try { + & openclaw agents add $dir.Name --workspace $workspace --non-interactive | Out-Null + } + catch { + } + } + + $count++ + } + + Write-Ok "OpenClaw: $count workspaces -> $dest" + if ($openclawInstalled) { + Write-WarnMsg "OpenClaw: run 'openclaw gateway restart' to activate new agents" + } +} + +function Install-Cursor { + param([string]$Integrations) + + $src = Join-Path (Join-Path $Integrations "cursor") "rules" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/cursor missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Get-Location).Path ".cursor\rules" + $count = Copy-FlatFiles -Source $src -Filter "*.mdc" -Destination $dest + + Write-Ok "Cursor: $count rules -> $dest" + Write-WarnMsg "Cursor: project-scoped. Run from your project root to install there." +} + +function Install-Aider { + param([string]$Integrations) + + $src = Join-Path (Join-Path $Integrations "aider") "CONVENTIONS.md" + $dest = Join-Path (Get-Location).Path "CONVENTIONS.md" + Install-SingleProjectFile ` + -Source $src ` + -MissingMessage "integrations/aider/CONVENTIONS.md missing. Run convert.sh or convert.ps1 first." ` + -Destination $dest ` + -ExistsWarning "Aider: CONVENTIONS.md already exists at $dest (remove to reinstall)." ` + -SuccessMessage "Aider: installed -> $dest" ` + -ScopeWarning "Aider: project-scoped. Run from your project root to install there." +} + +function Install-Windsurf { + param([string]$Integrations) + + $src = Join-Path (Join-Path $Integrations "windsurf") ".windsurfrules" + $dest = Join-Path (Get-Location).Path ".windsurfrules" + Install-SingleProjectFile ` + -Source $src ` + -MissingMessage "integrations/windsurf/.windsurfrules missing. Run convert.sh or convert.ps1 first." ` + -Destination $dest ` + -ExistsWarning "Windsurf: .windsurfrules already exists at $dest (remove to reinstall)." ` + -SuccessMessage "Windsurf: installed -> $dest" ` + -ScopeWarning "Windsurf: project-scoped. Run from your project root to install there." +} + +function Install-Qwen { + param([string]$Integrations) + + $src = Join-Path (Join-Path $Integrations "qwen") "agents" + if (-not (Test-Path -Path $src -PathType Container)) { + throw "integrations/qwen missing. Run convert.sh or convert.ps1 first." + } + + $dest = Join-Path (Get-Location).Path ".qwen\agents" + $count = Copy-FlatFiles -Source $src -Filter "*.md" -Destination $dest + + Write-Ok "Qwen Code: installed $count agents to $dest" + Write-WarnMsg "Qwen Code: project-scoped. Run from your project root to install there." + Write-WarnMsg "Tip: Run '/agents manage' in Qwen Code to refresh, or restart session" +} + +function Install-Tool { + param( + [string]$ToolName, + [string]$RepoRoot, + [string]$Integrations + ) + + switch ($ToolName) { + "claude-code" { Install-ClaudeCode -RepoRoot $RepoRoot; return } + "copilot" { Install-Copilot -RepoRoot $RepoRoot; return } + "antigravity" { Install-Antigravity -Integrations $Integrations; return } + "gemini-cli" { Install-GeminiCli -Integrations $Integrations; return } + "opencode" { Install-OpenCode -Integrations $Integrations; return } + "openclaw" { Install-OpenClaw -Integrations $Integrations; return } + "cursor" { Install-Cursor -Integrations $Integrations; return } + "aider" { Install-Aider -Integrations $Integrations; return } + "windsurf" { Install-Windsurf -Integrations $Integrations; return } + "qwen" { Install-Qwen -Integrations $Integrations; return } + default { throw "Unknown tool '$ToolName'" } + } +} + +if ($Help) { + Write-Output (Get-Usage) + exit 0 +} + +if ($Interactive -and $NoInteractive) { + Write-ErrMsg "Choose either -Interactive or -NoInteractive, not both." + exit 1 +} + +$Tool = $Tool.ToLowerInvariant() +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir +$Integrations = Join-Path $RepoRoot "integrations" + +if (-not (Test-Path -Path $Integrations -PathType Container)) { + Write-ErrMsg "integrations/ not found. Run ./scripts/convert.sh or ./scripts/convert.ps1 first." + exit 1 +} + +if ($Tool -ne "all" -and -not ($AllTools -contains $Tool)) { + Write-ErrMsg "Unknown tool '$Tool'. Valid: $($AllTools -join ' ')" + exit 1 +} + +$interactiveMode = "auto" +if ($Interactive) { + $interactiveMode = "yes" +} +elseif ($NoInteractive) { + $interactiveMode = "no" +} + +$useInteractive = $false +$canPrompt = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected + +if ($interactiveMode -eq "yes") { + $useInteractive = $true +} +elseif ($interactiveMode -eq "auto" -and $Tool -eq "all" -and $canPrompt) { + $useInteractive = $true +} + +$selectedTools = New-Object System.Collections.Generic.List[string] + +if ($useInteractive) { + $choices = Select-ToolsInteractive -Tools $AllTools + foreach ($choice in $choices) { + $selectedTools.Add($choice) + } +} +elseif ($Tool -ne "all") { + $selectedTools.Add($Tool) +} +else { + Write-Header "The Agency -- Scanning for installed tools..." + Write-Host "" + + foreach ($toolName in $AllTools) { + $detected = Test-ToolDetected -ToolName $toolName + if ($detected) { + $selectedTools.Add($toolName) + Write-Host (" [*] {0} detected" -f (Get-ToolLabel -ToolName $toolName)) + } + else { + Write-Host (" [ ] {0} not found" -f (Get-ToolLabel -ToolName $toolName)) + } + } +} + +if ($selectedTools.Count -eq 0) { + Write-WarnMsg "No tools selected or detected. Nothing to install." + Write-Host "" + Write-Host "Tip: use -Tool to force-install a specific tool." + Write-Host "Available: $($AllTools -join ' ')" + exit 0 +} + +Write-Header "The Agency -- Installing agents" +Write-Host (" Repo: {0}" -f $RepoRoot) +Write-Host (" Installing: {0}" -f ($selectedTools -join " ")) +Write-Host "" + +$installed = 0 +foreach ($toolName in $selectedTools) { + try { + Install-Tool -ToolName $toolName -RepoRoot $RepoRoot -Integrations $Integrations + $installed++ + } + catch { + Write-ErrMsg "$toolName install failed: $($_.Exception.Message)" + exit 1 + } +} + +Write-Host "" +Write-Ok "Done! Installed $installed tool(s)." +Write-Host "" +Write-Host "Run ./scripts/convert.sh (or .\scripts\convert.ps1) to regenerate after adding or editing agents." +Write-Host "" diff --git a/scripts/lint-agents.bat b/scripts/lint-agents.bat new file mode 100644 index 000000000..c8b794d48 --- /dev/null +++ b/scripts/lint-agents.bat @@ -0,0 +1,6 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%lint-agents.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/lint-agents.ps1 b/scripts/lint-agents.ps1 new file mode 100644 index 000000000..13ad0d586 --- /dev/null +++ b/scripts/lint-agents.ps1 @@ -0,0 +1,297 @@ +[CmdletBinding()] +param( + [string[]]$Paths, + [Alias("h")] + [switch]$Help +) + +Set-StrictMode -Version 3.0 +$ErrorActionPreference = "Stop" + +$AgentDirs = @( + "academic", + "design", + "engineering", + "game-development", + "marketing", + "paid-media", + "product", + "project-management", + "testing", + "support", + "spatial-computing", + "specialized" +) + +$RequiredFrontmatter = @("name", "description", "color") +$RecommendedSections = @("Identity", "Core Mission", "Critical Rules") + +$Script:ErrorCount = 0 +$Script:WarningCount = 0 + +function Get-Usage { + return @" +lint-agents.ps1 -- Validates agent markdown files. + +Checks: + 1. YAML frontmatter exists with name, description, color (ERROR) + 2. Recommended sections are present (WARN) + 3. Body has meaningful content (WARN) + +Usage: + ./scripts/lint-agents.ps1 [-Paths ] [-Help] + +Examples: + ./scripts/lint-agents.ps1 + ./scripts/lint-agents.ps1 -Paths engineering\engineering-frontend-developer.md +"@ +} + +function Write-ErrorMsg { + param([string]$Message) + Write-Host $Message -ForegroundColor Red +} + +function Write-WarnMsg { + param([string]$Message) + Write-Host $Message -ForegroundColor Yellow +} + +function Read-Utf8Lines { + param([string]$Path) + + $utf8 = New-Object System.Text.UTF8Encoding($false) + return [System.IO.File]::ReadAllLines($Path, $utf8) +} + +function Get-FirstLine { + param([string]$FilePath) + + $lines = Read-Utf8Lines -Path $FilePath + if ($lines.Count -eq 0) { + return "" + } + + return $lines[0] +} + +function Get-FrontmatterLines { + param([string]$FilePath) + + $lines = Read-Utf8Lines -Path $FilePath + if ($lines.Count -lt 3) { + return @() + } + + if ($lines[0] -ne "---") { + return @() + } + + $endIndex = -1 + for ($i = 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -eq "---") { + $endIndex = $i + break + } + } + + if ($endIndex -lt 1) { + return @() + } + + if ($endIndex -eq 1) { + return @() + } + + return $lines[1..($endIndex - 1)] +} + +function Get-BodyLines { + param([string]$FilePath) + + $lines = Read-Utf8Lines -Path $FilePath + if ($lines.Count -lt 3) { + return @() + } + + if ($lines[0] -ne "---") { + return @() + } + + $endIndex = -1 + for ($i = 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -eq "---") { + $endIndex = $i + break + } + } + + if ($endIndex -lt 1) { + return @() + } + + if ($endIndex -ge $lines.Count - 1) { + return @() + } + + return $lines[($endIndex + 1)..($lines.Count - 1)] +} + +function Increment-Error { + param([string]$Message) + Write-ErrorMsg -Message $Message + $Script:ErrorCount++ +} + +function Increment-Warn { + param([string]$Message) + Write-WarnMsg -Message $Message + $Script:WarningCount++ +} + +function Test-FrontmatterHasField { + param( + [string[]]$FrontmatterLines, + [string]$Field + ) + + foreach ($line in $FrontmatterLines) { + if ($line -match ("^" + [regex]::Escape($Field) + ":")) { + return $true + } + } + + return $false +} + +function Count-Words { + param([string[]]$BodyLines) + + if ($null -eq $BodyLines -or $BodyLines.Count -eq 0) { + return 0 + } + + $text = [string]::Join(" ", $BodyLines) + $matches = [regex]::Matches($text, "\S+") + return $matches.Count +} + +function Test-BodyHasSection { + param( + [string[]]$BodyLines, + [string]$Section + ) + + foreach ($line in $BodyLines) { + if ($line -match [regex]::Escape($Section)) { + return $true + } + } + + return $false +} + +function Lint-File { + param([string]$FilePath) + + $firstLine = Get-FirstLine -FilePath $FilePath + if ($firstLine -ne "---") { + Increment-Error -Message "ERROR ${FilePath}: missing frontmatter opening ---" + return + } + + $frontmatterLines = Get-FrontmatterLines -FilePath $FilePath + if ($frontmatterLines.Count -eq 0) { + Increment-Error -Message "ERROR ${FilePath}: empty or malformed frontmatter" + return + } + + foreach ($field in $RequiredFrontmatter) { + if (-not (Test-FrontmatterHasField -FrontmatterLines $frontmatterLines -Field $field)) { + Increment-Error -Message "ERROR ${FilePath}: missing frontmatter field '$field'" + } + } + + $bodyLines = Get-BodyLines -FilePath $FilePath + foreach ($section in $RecommendedSections) { + if (-not (Test-BodyHasSection -BodyLines $bodyLines -Section $section)) { + Increment-Warn -Message "WARN ${FilePath}: missing recommended section '$section'" + } + } + + $wordCount = Count-Words -BodyLines $bodyLines + if ($wordCount -lt 50) { + Increment-Warn -Message "WARN ${FilePath}: body seems very short (< 50 words)" + } +} + +function Get-DefaultFiles { + param([string]$RepoRoot) + + $files = New-Object System.Collections.Generic.List[string] + + foreach ($dir in $AgentDirs) { + $dirPath = Join-Path $RepoRoot $dir + if (-not (Test-Path -Path $dirPath -PathType Container)) { + continue + } + + $entries = Get-ChildItem -Path $dirPath -Filter "*.md" -File -Recurse | Sort-Object -Property FullName + foreach ($entry in $entries) { + $files.Add($entry.FullName) + } + } + + return $files.ToArray() +} + +if ($Help) { + Write-Output (Get-Usage) + exit 0 +} + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir + +$filesToLint = @() +if ($PSBoundParameters.ContainsKey("Paths") -and $Paths.Count -gt 0) { + foreach ($path in $Paths) { + $resolved = $path + if (-not [System.IO.Path]::IsPathRooted($resolved)) { + $resolved = Join-Path (Get-Location).Path $resolved + } + + if (-not (Test-Path -Path $resolved -PathType Leaf)) { + Increment-Error -Message "ERROR ${path}: file not found" + continue + } + + $filesToLint += (Resolve-Path -Path $resolved).Path + } +} +else { + $filesToLint = Get-DefaultFiles -RepoRoot $RepoRoot +} + +if ($filesToLint.Count -eq 0) { + Write-Host "No agent files found." + exit 1 +} + +Write-Host ("Linting {0} agent files..." -f $filesToLint.Count) +Write-Host "" + +foreach ($file in $filesToLint) { + Lint-File -FilePath $file +} + +Write-Host "" +Write-Host ("Results: {0} error(s), {1} warning(s) in {2} files." -f $Script:ErrorCount, $Script:WarningCount, $filesToLint.Count) + +if ($Script:ErrorCount -gt 0) { + Write-Host "FAILED: fix the errors above before merging." + exit 1 +} + +Write-Host "PASSED" +exit 0