Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:17 +08:00
commit 8967d326a7
30 changed files with 5154 additions and 0 deletions

199
scripts/detect-project.sh Executable file
View File

@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# Detect project type, language, version, and build tools
set -euo pipefail
PROJECT_DIR="${1:-.}"
cd "$PROJECT_DIR"
# Initialize variables
LANGUAGE="unknown"
VERSION="unknown"
BUILD_TOOL="unknown"
FRAMEWORK="none"
PROJECT_TYPE="unknown"
QUALITY_TOOLS=()
TEST_FRAMEWORK="unknown"
HAS_DOCKER=false
CI="none"
# Detect language and version
detect_language() {
if [ -f "go.mod" ]; then
LANGUAGE="go"
VERSION=$(grep '^go ' go.mod | awk '{print $2}' || echo "unknown")
BUILD_TOOL="go"
TEST_FRAMEWORK="testing"
# Detect Go project type
if [ -d "cmd" ]; then
PROJECT_TYPE="go-cli"
elif grep -q "github.com/gofiber/fiber" go.mod 2>/dev/null; then
PROJECT_TYPE="go-web-app"
FRAMEWORK="fiber"
elif grep -q "github.com/labstack/echo" go.mod 2>/dev/null; then
PROJECT_TYPE="go-web-app"
FRAMEWORK="echo"
elif grep -q "github.com/gin-gonic/gin" go.mod 2>/dev/null; then
PROJECT_TYPE="go-web-app"
FRAMEWORK="gin"
else
PROJECT_TYPE="go-library"
fi
# Detect Go quality tools
[ -f ".golangci.yml" ] || [ -f ".golangci.yaml" ] && QUALITY_TOOLS+=("golangci-lint")
command -v gofmt &>/dev/null && QUALITY_TOOLS+=("gofmt")
elif [ -f "composer.json" ]; then
LANGUAGE="php"
VERSION=$(jq -r '.require.php // "unknown"' composer.json 2>/dev/null || echo "unknown")
BUILD_TOOL="composer"
# Detect PHP framework
if jq -e '.require."typo3/cms-core"' composer.json &>/dev/null; then
PROJECT_TYPE="php-typo3"
FRAMEWORK="typo3"
TYPO3_VERSION=$(jq -r '.require."typo3/cms-core"' composer.json 2>/dev/null || echo "unknown")
elif jq -e '.require."laravel/framework"' composer.json &>/dev/null; then
PROJECT_TYPE="php-laravel"
FRAMEWORK="laravel"
elif jq -e '.require."symfony/symfony"' composer.json &>/dev/null; then
PROJECT_TYPE="php-symfony"
FRAMEWORK="symfony"
else
PROJECT_TYPE="php-library"
fi
# Detect PHP quality tools
jq -e '.require."phpstan/phpstan"' composer.json &>/dev/null && QUALITY_TOOLS+=("phpstan")
jq -e '.require."friendsofphp/php-cs-fixer"' composer.json &>/dev/null && QUALITY_TOOLS+=("php-cs-fixer")
[ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] && TEST_FRAMEWORK="phpunit"
elif [ -f "package.json" ]; then
LANGUAGE="typescript"
VERSION=$(jq -r '.engines.node // "unknown"' package.json 2>/dev/null || echo "unknown")
# Detect JS/TS framework
if jq -e '.dependencies."next"' package.json &>/dev/null; then
PROJECT_TYPE="typescript-nextjs"
FRAMEWORK="next.js"
BUILD_TOOL="npm"
elif jq -e '.dependencies."react"' package.json &>/dev/null; then
PROJECT_TYPE="typescript-react"
FRAMEWORK="react"
BUILD_TOOL="npm"
elif jq -e '.dependencies."vue"' package.json &>/dev/null; then
PROJECT_TYPE="typescript-vue"
FRAMEWORK="vue"
BUILD_TOOL="npm"
elif jq -e '.dependencies."express"' package.json &>/dev/null; then
PROJECT_TYPE="typescript-node"
FRAMEWORK="express"
BUILD_TOOL="npm"
else
PROJECT_TYPE="typescript-library"
BUILD_TOOL="npm"
fi
# Check for yarn/pnpm
[ -f "yarn.lock" ] && BUILD_TOOL="yarn"
[ -f "pnpm-lock.yaml" ] && BUILD_TOOL="pnpm"
# Detect quality tools
jq -e '.devDependencies."eslint"' package.json &>/dev/null && QUALITY_TOOLS+=("eslint")
jq -e '.devDependencies."prettier"' package.json &>/dev/null && QUALITY_TOOLS+=("prettier")
jq -e '.devDependencies."typescript"' package.json &>/dev/null && QUALITY_TOOLS+=("tsc")
# Detect test framework
if jq -e '.devDependencies."jest"' package.json &>/dev/null; then
TEST_FRAMEWORK="jest"
elif jq -e '.devDependencies."vitest"' package.json &>/dev/null; then
TEST_FRAMEWORK="vitest"
fi
elif [ -f "pyproject.toml" ]; then
LANGUAGE="python"
VERSION=$(grep 'requires-python' pyproject.toml | cut -d'"' -f2 2>/dev/null || echo "unknown")
# Detect Python build tool
if grep -q '\[tool.poetry\]' pyproject.toml 2>/dev/null; then
BUILD_TOOL="poetry"
elif grep -q '\[tool.hatch\]' pyproject.toml 2>/dev/null; then
BUILD_TOOL="hatch"
else
BUILD_TOOL="pip"
fi
# Detect framework
if grep -q 'django' pyproject.toml 2>/dev/null; then
PROJECT_TYPE="python-django"
FRAMEWORK="django"
elif grep -q 'flask' pyproject.toml 2>/dev/null; then
PROJECT_TYPE="python-flask"
FRAMEWORK="flask"
elif grep -q 'fastapi' pyproject.toml 2>/dev/null; then
PROJECT_TYPE="python-fastapi"
FRAMEWORK="fastapi"
elif [ -d "scripts" ] && [ "$(find scripts -name '*.py' | wc -l)" -gt 3 ]; then
PROJECT_TYPE="python-cli"
else
PROJECT_TYPE="python-library"
fi
# Detect quality tools
grep -q 'ruff' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("ruff")
grep -q 'black' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("black")
grep -q 'mypy' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("mypy")
grep -q 'pytest' pyproject.toml 2>/dev/null && TEST_FRAMEWORK="pytest"
fi
}
# Detect if Makefile exists
if [ -f "Makefile" ]; then
BUILD_TOOL="make"
fi
# Detect Docker
[ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] && HAS_DOCKER=true
# Detect CI
if [ -d ".github/workflows" ]; then
CI="github-actions"
elif [ -f ".gitlab-ci.yml" ]; then
CI="gitlab-ci"
elif [ -f ".circleci/config.yml" ]; then
CI="circleci"
fi
# Run detection
detect_language
# Output JSON
# Handle empty quality_tools array
if [ ${#QUALITY_TOOLS[@]} -eq 0 ]; then
TOOLS_JSON="[]"
else
TOOLS_JSON="$(printf '%s\n' "${QUALITY_TOOLS[@]}" | jq -R . | jq -s .)"
fi
jq -n \
--arg type "$PROJECT_TYPE" \
--arg lang "$LANGUAGE" \
--arg ver "$VERSION" \
--arg build "$BUILD_TOOL" \
--arg framework "$FRAMEWORK" \
--argjson docker "$HAS_DOCKER" \
--argjson tools "$TOOLS_JSON" \
--arg test "$TEST_FRAMEWORK" \
--arg ci "$CI" \
'{
type: $type,
language: $lang,
version: $ver,
build_tool: $build,
framework: $framework,
has_docker: $docker,
quality_tools: $tools,
test_framework: $test,
ci: $ci
}'

178
scripts/detect-scopes.sh Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# Detect directories that should have scoped AGENTS.md files
set -euo pipefail
PROJECT_DIR="${1:-.}"
cd "$PROJECT_DIR"
MIN_FILES=5 # Minimum files to warrant scoped AGENTS.md
# Get project info
PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR")
LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
scopes=()
# Function to count source files in a directory
count_source_files() {
local dir="$1"
local pattern="$2"
find "$dir" -maxdepth 3 -type f -name "$pattern" 2>/dev/null | wc -l
}
# Function to add scope
add_scope() {
local path="$1"
local type="$2"
local count="$3"
scopes+=("{\"path\": \"$path\", \"type\": \"$type\", \"files\": $count}")
}
# Language-specific scope detection
case "$LANGUAGE" in
"go")
# Check common Go directories
[ -d "internal" ] && {
count=$(count_source_files "internal" "*.go")
[ "$count" -ge "$MIN_FILES" ] && add_scope "internal" "backend-go" "$count"
}
[ -d "pkg" ] && {
count=$(count_source_files "pkg" "*.go")
[ "$count" -ge "$MIN_FILES" ] && add_scope "pkg" "backend-go" "$count"
}
[ -d "cmd" ] && {
count=$(count_source_files "cmd" "*.go")
[ "$count" -ge 3 ] && add_scope "cmd" "cli" "$count"
}
[ -d "examples" ] && {
count=$(count_source_files "examples" "*.go")
[ "$count" -ge 3 ] && add_scope "examples" "examples" "$count"
}
[ -d "testutil" ] && {
count=$(count_source_files "testutil" "*.go")
[ "$count" -ge 3 ] && add_scope "testutil" "testing" "$count"
}
[ -d "docs" ] && {
count=$(find docs -type f \( -name "*.md" -o -name "*.rst" \) | wc -l)
[ "$count" -ge 3 ] && add_scope "docs" "documentation" "$count"
}
;;
"php")
# Check common PHP directories
[ -d "Classes" ] && {
count=$(count_source_files "Classes" "*.php")
[ "$count" -ge "$MIN_FILES" ] && add_scope "Classes" "backend-php" "$count"
}
[ -d "src" ] && {
count=$(count_source_files "src" "*.php")
[ "$count" -ge "$MIN_FILES" ] && add_scope "src" "backend-php" "$count"
}
[ -d "Tests" ] && {
count=$(count_source_files "Tests" "*.php")
[ "$count" -ge 3 ] && add_scope "Tests" "testing" "$count"
}
[ -d "tests" ] && {
count=$(count_source_files "tests" "*.php")
[ "$count" -ge 3 ] && add_scope "tests" "testing" "$count"
}
[ -d "Documentation" ] && {
count=$(find Documentation -type f \( -name "*.rst" -o -name "*.md" \) | wc -l)
[ "$count" -ge 3 ] && add_scope "Documentation" "documentation" "$count"
}
[ -d "Resources" ] && {
count=$(find Resources -type f | wc -l)
[ "$count" -ge 5 ] && add_scope "Resources" "resources" "$count"
}
;;
"typescript")
# Check common TypeScript/JavaScript directories
[ -d "src" ] && {
count=$(count_source_files "src" "*.ts")
ts_count=$count
count=$(count_source_files "src" "*.tsx")
tsx_count=$count
if [ "$tsx_count" -ge "$MIN_FILES" ]; then
add_scope "src" "frontend-typescript" "$tsx_count"
elif [ "$ts_count" -ge "$MIN_FILES" ]; then
add_scope "src" "backend-typescript" "$ts_count"
fi
}
[ -d "components" ] && {
count=$(count_source_files "components" "*.tsx")
[ "$count" -ge "$MIN_FILES" ] && add_scope "components" "frontend-typescript" "$count"
}
[ -d "pages" ] && {
count=$(count_source_files "pages" "*.tsx")
[ "$count" -ge 3 ] && add_scope "pages" "frontend-typescript" "$count"
}
[ -d "app" ] && {
count=$(count_source_files "app" "*.tsx")
[ "$count" -ge 3 ] && add_scope "app" "frontend-typescript" "$count"
}
[ -d "server" ] || [ -d "backend" ] && {
dir=$([ -d "server" ] && echo "server" || echo "backend")
count=$(count_source_files "$dir" "*.ts")
[ "$count" -ge "$MIN_FILES" ] && add_scope "$dir" "backend-typescript" "$count"
}
[ -d "__tests__" ] || [ -d "tests" ] && {
dir=$([ -d "__tests__" ] && echo "__tests__" || echo "tests")
count=$(count_source_files "$dir" "*.test.ts")
[ "$count" -ge 3 ] && add_scope "$dir" "testing" "$count"
}
;;
"python")
# Check common Python directories
[ -d "src" ] && {
count=$(count_source_files "src" "*.py")
[ "$count" -ge "$MIN_FILES" ] && add_scope "src" "backend-python" "$count"
}
[ -d "tests" ] && {
count=$(count_source_files "tests" "*.py")
[ "$count" -ge 3 ] && add_scope "tests" "testing" "$count"
}
[ -d "scripts" ] && {
count=$(count_source_files "scripts" "*.py")
[ "$count" -ge 3 ] && add_scope "scripts" "cli" "$count"
}
[ -d "docs" ] && {
count=$(find docs -type f \( -name "*.md" -o -name "*.rst" \) | wc -l)
[ "$count" -ge 3 ] && add_scope "docs" "documentation" "$count"
}
;;
esac
# Check for web subdirectories (cross-language)
if [ -d "internal/web" ]; then
count=$(find internal/web -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) | wc -l)
[ "$count" -ge "$MIN_FILES" ] && add_scope "internal/web" "frontend-typescript" "$count"
fi
# Output JSON
if [ ${#scopes[@]} -eq 0 ]; then
echo '{"scopes": []}'
else
echo "{\"scopes\": [$(IFS=,; echo "${scopes[*]}")]}"
fi

183
scripts/extract-commands.sh Executable file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# Extract build commands from various build tool files
set -euo pipefail
PROJECT_DIR="${1:-.}"
cd "$PROJECT_DIR"
# Get project info
PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR")
LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
BUILD_TOOL=$(echo "$PROJECT_INFO" | jq -r '.build_tool')
# Initialize command variables
TYPECHECK_CMD=""
LINT_CMD=""
FORMAT_CMD=""
TEST_CMD=""
BUILD_CMD=""
DEV_CMD=""
# Extract from Makefile
extract_from_makefile() {
[ ! -f "Makefile" ] && return
# Extract targets with ## comments
while IFS= read -r line; do
if [[ $line =~ ^([a-zA-Z_-]+):.*\#\#(.*)$ ]]; then
target="${BASH_REMATCH[1]}"
description="${BASH_REMATCH[2]}"
case "$target" in
lint|check) LINT_CMD="make $target" ;;
format|fmt) FORMAT_CMD="make $target" ;;
test|tests) TEST_CMD="make $target" ;;
build) BUILD_CMD="make $target" ;;
typecheck|types) TYPECHECK_CMD="make $target" ;;
dev|serve) DEV_CMD="make $target" ;;
esac
fi
done < Makefile
}
# Extract from package.json
extract_from_package_json() {
[ ! -f "package.json" ] && return
TYPECHECK_CMD=$(jq -r '.scripts.typecheck // .scripts["type-check"] // empty' package.json 2>/dev/null)
[ -n "$TYPECHECK_CMD" ] && TYPECHECK_CMD="npm run typecheck" || TYPECHECK_CMD="npx tsc --noEmit"
LINT_CMD=$(jq -r '.scripts.lint // empty' package.json 2>/dev/null)
[ -n "$LINT_CMD" ] && LINT_CMD="npm run lint" || LINT_CMD="npx eslint ."
FORMAT_CMD=$(jq -r '.scripts.format // empty' package.json 2>/dev/null)
[ -n "$FORMAT_CMD" ] && FORMAT_CMD="npm run format" || FORMAT_CMD="npx prettier --write ."
TEST_CMD=$(jq -r '.scripts.test // empty' package.json 2>/dev/null)
[ -n "$TEST_CMD" ] && TEST_CMD="npm test"
BUILD_CMD=$(jq -r '.scripts.build // empty' package.json 2>/dev/null)
[ -n "$BUILD_CMD" ] && BUILD_CMD="npm run build"
DEV_CMD=$(jq -r '.scripts.dev // .scripts.start // empty' package.json 2>/dev/null)
[ -n "$DEV_CMD" ] && DEV_CMD="npm run dev"
}
# Extract from composer.json
extract_from_composer_json() {
[ ! -f "composer.json" ] && return
LINT_CMD=$(jq -r '.scripts.lint // .scripts["cs:check"] // empty' composer.json 2>/dev/null)
[ -n "$LINT_CMD" ] && LINT_CMD="composer run lint"
FORMAT_CMD=$(jq -r '.scripts.format // .scripts["cs:fix"] // empty' composer.json 2>/dev/null)
[ -n "$FORMAT_CMD" ] && FORMAT_CMD="composer run format"
TEST_CMD=$(jq -r '.scripts.test // empty' composer.json 2>/dev/null)
[ -n "$TEST_CMD" ] && TEST_CMD="composer run test" || TEST_CMD="vendor/bin/phpunit"
TYPECHECK_CMD=$(jq -r '.scripts.phpstan // .scripts["stan"] // empty' composer.json 2>/dev/null)
[ -n "$TYPECHECK_CMD" ] && TYPECHECK_CMD="composer run phpstan" || {
if [ -f "phpstan.neon" ] || [ -f "Build/phpstan.neon" ]; then
TYPECHECK_CMD="vendor/bin/phpstan analyze"
fi
}
}
# Extract from pyproject.toml
extract_from_pyproject() {
[ ! -f "pyproject.toml" ] && return
# Check for ruff
if grep -q '\[tool.ruff\]' pyproject.toml; then
LINT_CMD="ruff check ."
FORMAT_CMD="ruff format ."
fi
# Check for black
if grep -q 'black' pyproject.toml; then
FORMAT_CMD="black ."
fi
# Check for mypy
if grep -q 'mypy' pyproject.toml; then
TYPECHECK_CMD="mypy ."
fi
# Check for pytest
if grep -q 'pytest' pyproject.toml; then
TEST_CMD="pytest"
fi
}
# Language-specific defaults
set_language_defaults() {
case "$LANGUAGE" in
"go")
[ -z "$TYPECHECK_CMD" ] && TYPECHECK_CMD="go build -v ./..."
[ -z "$LINT_CMD" ] && {
if [ -f ".golangci.yml" ] || [ -f ".golangci.yaml" ]; then
LINT_CMD="golangci-lint run ./..."
fi
}
[ -z "$FORMAT_CMD" ] && FORMAT_CMD="gofmt -w ."
[ -z "$TEST_CMD" ] && TEST_CMD="go test -v -race -short ./..."
[ -z "$BUILD_CMD" ] && BUILD_CMD="go build -v ./..."
;;
"php")
[ -z "$TYPECHECK_CMD" ] && {
if [ -f "phpstan.neon" ] || [ -f "Build/phpstan.neon" ]; then
TYPECHECK_CMD="vendor/bin/phpstan analyze"
fi
}
[ -z "$LINT_CMD" ] && LINT_CMD="vendor/bin/php-cs-fixer fix --dry-run"
[ -z "$FORMAT_CMD" ] && FORMAT_CMD="vendor/bin/php-cs-fixer fix"
[ -z "$TEST_CMD" ] && TEST_CMD="vendor/bin/phpunit"
;;
"typescript")
[ -z "$TYPECHECK_CMD" ] && TYPECHECK_CMD="npx tsc --noEmit"
[ -z "$LINT_CMD" ] && LINT_CMD="npx eslint ."
[ -z "$FORMAT_CMD" ] && FORMAT_CMD="npx prettier --write ."
[ -z "$TEST_CMD" ] && {
if [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
TEST_CMD="npm test"
elif grep -q 'vitest' package.json 2>/dev/null; then
TEST_CMD="npx vitest"
fi
}
;;
"python")
[ -z "$LINT_CMD" ] && LINT_CMD="ruff check ."
[ -z "$FORMAT_CMD" ] && FORMAT_CMD="ruff format ."
[ -z "$TYPECHECK_CMD" ] && TYPECHECK_CMD="mypy ."
[ -z "$TEST_CMD" ] && TEST_CMD="pytest"
;;
esac
}
# Run extraction
extract_from_makefile
extract_from_package_json
extract_from_composer_json
extract_from_pyproject
set_language_defaults
# Output JSON
jq -n \
--arg typecheck "$TYPECHECK_CMD" \
--arg lint "$LINT_CMD" \
--arg format "$FORMAT_CMD" \
--arg test "$TEST_CMD" \
--arg build "$BUILD_CMD" \
--arg dev "$DEV_CMD" \
'{
typecheck: $typecheck,
lint: $lint,
format: $format,
test: $test,
build: $build,
dev: $dev
}'

292
scripts/generate-agents.sh Executable file
View File

@@ -0,0 +1,292 @@
#!/usr/bin/env bash
# Main AGENTS.md generator script
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
TEMPLATE_DIR="$SKILL_DIR/templates"
# Source helper library
source "$SCRIPT_DIR/lib/template.sh"
# Default options
PROJECT_DIR="${1:-.}"
STYLE="${STYLE:-thin}"
DRY_RUN=false
UPDATE_ONLY=false
FORCE=false
VERBOSE=false
# Parse flags
while [[ $# -gt 0 ]]; do
case $1 in
--style=*)
STYLE="${1#*=}"
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--update)
UPDATE_ONLY=true
shift
;;
--force)
FORCE=true
shift
;;
--verbose|-v)
VERBOSE=true
shift
;;
--help|-h)
cat <<EOF
Usage: generate-agents.sh [PROJECT_DIR] [OPTIONS]
Generate AGENTS.md files for a project following the public agents.md convention.
Options:
--style=thin|verbose Template style (default: thin)
--dry-run Preview what will be created
--update Update existing files only
--force Force regeneration of existing files
--verbose, -v Verbose output
--help, -h Show this help message
Examples:
generate-agents.sh . # Generate thin root + scoped files
generate-agents.sh . --dry-run # Preview changes
generate-agents.sh . --style=verbose # Use verbose root template
generate-agents.sh . --update # Update existing files
EOF
exit 0
;;
*)
PROJECT_DIR="$1"
shift
;;
esac
done
cd "$PROJECT_DIR"
log() {
[ "$VERBOSE" = true ] && echo "[INFO] $*" >&2
}
error() {
echo "[ERROR] $*" >&2
exit 1
}
# Detect project
log "Detecting project type..."
PROJECT_INFO=$("$SCRIPT_DIR/detect-project.sh" "$PROJECT_DIR")
[ "$VERBOSE" = true ] && echo "$PROJECT_INFO" | jq . >&2
LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
VERSION=$(echo "$PROJECT_INFO" | jq -r '.version')
PROJECT_TYPE=$(echo "$PROJECT_INFO" | jq -r '.type')
[ "$LANGUAGE" = "unknown" ] && error "Could not detect project language"
# Detect scopes
log "Detecting scopes..."
SCOPES_INFO=$("$SCRIPT_DIR/detect-scopes.sh" "$PROJECT_DIR")
[ "$VERBOSE" = true ] && echo "$SCOPES_INFO" | jq . >&2
# Extract commands
log "Extracting build commands..."
COMMANDS=$("$SCRIPT_DIR/extract-commands.sh" "$PROJECT_DIR")
[ "$VERBOSE" = true ] && echo "$COMMANDS" | jq . >&2
# Generate root AGENTS.md
ROOT_FILE="$PROJECT_DIR/AGENTS.md"
if [ -f "$ROOT_FILE" ] && [ "$FORCE" = false ] && [ "$UPDATE_ONLY" = false ]; then
log "Root AGENTS.md already exists, skipping (use --force to regenerate)"
elif [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create/update: $ROOT_FILE"
else
log "Generating root AGENTS.md..."
# Select template
if [ "$STYLE" = "verbose" ]; then
TEMPLATE="$TEMPLATE_DIR/root-verbose.md"
else
TEMPLATE="$TEMPLATE_DIR/root-thin.md"
fi
# Prepare template variables
declare -A vars
vars[TIMESTAMP]=$(get_timestamp)
vars[LANGUAGE_CONVENTIONS]=$(get_language_conventions "$LANGUAGE" "$VERSION")
vars[TYPECHECK_CMD]=$(echo "$COMMANDS" | jq -r '.typecheck')
vars[LINT_CMD]=$(echo "$COMMANDS" | jq -r '.lint')
vars[FORMAT_CMD]=$(echo "$COMMANDS" | jq -r '.format' | sed 's/^/ (file scope): /')
vars[TEST_CMD]=$(echo "$COMMANDS" | jq -r '.test')
vars[SCOPE_INDEX]=$(build_scope_index "$SCOPES_INFO")
# Verbose template additional vars
if [ "$STYLE" = "verbose" ]; then
vars[PROJECT_DESCRIPTION]="TODO: Add project description"
vars[VERSION]="$VERSION"
vars[BUILD_TOOL]=$(echo "$PROJECT_INFO" | jq -r '.build_tool')
vars[FRAMEWORK]=$(echo "$PROJECT_INFO" | jq -r '.framework')
vars[PROJECT_TYPE]="$PROJECT_TYPE"
vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
vars[QUALITY_STANDARDS]="TODO: Add quality standards"
vars[SECURITY_SPECIFIC]="TODO: Add security-specific guidelines"
vars[TEST_COVERAGE]="40"
vars[TEST_FAST_CMD]=$(echo "$COMMANDS" | jq -r '.test')
vars[TEST_FULL_CMD]=$(echo "$COMMANDS" | jq -r '.test')
vars[ARCHITECTURE_DOC]="./docs/architecture.md"
vars[API_DOC]="./docs/api.md"
vars[CONTRIBUTING_DOC]="./CONTRIBUTING.md"
fi
# Language-specific conflict resolution
case "$LANGUAGE" in
"go")
vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]="- For Go-specific patterns, defer to language idioms and standard library conventions"
;;
*)
vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=""
;;
esac
# Render template
render_template "$TEMPLATE" "$ROOT_FILE" vars
echo "✅ Created: $ROOT_FILE"
fi
# Generate scoped AGENTS.md files
SCOPE_COUNT=$(echo "$SCOPES_INFO" | jq '.scopes | length')
if [ "$SCOPE_COUNT" -eq 0 ]; then
log "No scopes detected (directories with <$MIN_FILES source files)"
else
log "Generating $SCOPE_COUNT scoped AGENTS.md files..."
while read -r scope; do
SCOPE_PATH=$(echo "$scope" | jq -r '.path')
SCOPE_TYPE=$(echo "$scope" | jq -r '.type')
SCOPE_FILE="$PROJECT_DIR/$SCOPE_PATH/AGENTS.md"
if [ -f "$SCOPE_FILE" ] && [ "$FORCE" = false ] && [ "$UPDATE_ONLY" = false ]; then
log "Scoped AGENTS.md already exists: $SCOPE_PATH, skipping"
continue
fi
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create/update: $SCOPE_FILE"
continue
fi
# Select template based on scope type
SCOPE_TEMPLATE="$TEMPLATE_DIR/scoped/$SCOPE_TYPE.md"
if [ ! -f "$SCOPE_TEMPLATE" ]; then
log "No template for scope type: $SCOPE_TYPE, skipping $SCOPE_PATH"
continue
fi
# Prepare scoped template variables
declare -A scope_vars
scope_vars[TIMESTAMP]=$(get_timestamp)
scope_vars[SCOPE_NAME]=$(basename "$SCOPE_PATH")
scope_vars[SCOPE_DESCRIPTION]=$(get_scope_description "$SCOPE_TYPE")
scope_vars[FILE_PATH]="<file>"
scope_vars[HOUSE_RULES]=""
# Language-specific variables
case "$SCOPE_TYPE" in
"backend-go")
scope_vars[GO_VERSION]="$VERSION"
scope_vars[GO_MINOR_VERSION]=$(echo "$VERSION" | cut -d. -f2)
scope_vars[GO_TOOLS]="golangci-lint, gofmt"
scope_vars[ENV_VARS]="See .env.example"
scope_vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
;;
"backend-php")
scope_vars[PHP_VERSION]="$VERSION"
FRAMEWORK=$(echo "$PROJECT_INFO" | jq -r '.framework')
scope_vars[FRAMEWORK]="$FRAMEWORK"
scope_vars[PHP_EXTENSIONS]="json, mbstring, xml"
scope_vars[ENV_VARS]="See .env.example"
scope_vars[PHPSTAN_LEVEL]="10"
scope_vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
if [ "$FRAMEWORK" = "typo3" ]; then
scope_vars[FRAMEWORK_CONVENTIONS]="- TYPO3-specific: Use dependency injection, follow TYPO3 CGL"
scope_vars[FRAMEWORK_DOCS]="- TYPO3 documentation: https://docs.typo3.org"
else
scope_vars[FRAMEWORK_CONVENTIONS]=""
scope_vars[FRAMEWORK_DOCS]=""
fi
;;
"frontend-typescript")
scope_vars[NODE_VERSION]="$VERSION"
FRAMEWORK=$(echo "$PROJECT_INFO" | jq -r '.framework')
scope_vars[FRAMEWORK]="$FRAMEWORK"
scope_vars[PACKAGE_MANAGER]=$(echo "$PROJECT_INFO" | jq -r '.build_tool')
scope_vars[ENV_VARS]="See .env.example"
scope_vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
scope_vars[DEV_CMD]=$(echo "$COMMANDS" | jq -r '.dev')
scope_vars[CSS_APPROACH]="CSS Modules"
case "$FRAMEWORK" in
"react")
scope_vars[FRAMEWORK_CONVENTIONS]="- Use functional components with hooks\n- Avoid class components"
scope_vars[FRAMEWORK_DOCS]="https://react.dev"
;;
"next.js")
scope_vars[FRAMEWORK_CONVENTIONS]="- Use App Router (app/)\n- Server Components by default"
scope_vars[FRAMEWORK_DOCS]="https://nextjs.org/docs"
;;
"vue")
scope_vars[FRAMEWORK_CONVENTIONS]="- Use Composition API\n- Avoid Options API for new code"
scope_vars[FRAMEWORK_DOCS]="https://vuejs.org/guide"
;;
*)
scope_vars[FRAMEWORK_CONVENTIONS]=""
scope_vars[FRAMEWORK_DOCS]=""
;;
esac
;;
"cli")
scope_vars[LANGUAGE]="$LANGUAGE"
CLI_FRAMEWORK="standard"
[ -f "go.mod" ] && grep -q "github.com/spf13/cobra" go.mod 2>/dev/null && CLI_FRAMEWORK="cobra"
[ -f "go.mod" ] && grep -q "github.com/urfave/cli" go.mod 2>/dev/null && CLI_FRAMEWORK="urfave/cli"
scope_vars[CLI_FRAMEWORK]="$CLI_FRAMEWORK"
scope_vars[BUILD_OUTPUT_PATH]="./bin/"
scope_vars[SETUP_INSTRUCTIONS]="- Build: $(echo "$COMMANDS" | jq -r '.build')"
scope_vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
scope_vars[RUN_CMD]="./bin/$(basename "$PROJECT_DIR")"
scope_vars[TEST_CMD]=$(echo "$COMMANDS" | jq -r '.test')
scope_vars[LINT_CMD]=$(echo "$COMMANDS" | jq -r '.lint')
;;
esac
# Render template
render_template "$SCOPE_TEMPLATE" "$SCOPE_FILE" scope_vars
echo "✅ Created: $SCOPE_FILE"
done < <(echo "$SCOPES_INFO" | jq -c '.scopes[]')
fi
if [ "$DRY_RUN" = true ]; then
echo ""
echo "[DRY-RUN] No files were modified. Remove --dry-run to apply changes."
fi
echo ""
echo "✅ AGENTS.md generation complete!"
[ "$SCOPE_COUNT" -gt 0 ] && echo " Generated: 1 root + $SCOPE_COUNT scoped files"

178
scripts/validate-structure.sh Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# Validate AGENTS.md structure compliance
set -euo pipefail
PROJECT_DIR="${1:-.}"
cd "$PROJECT_DIR"
ERRORS=0
WARNINGS=0
error() {
echo "❌ ERROR: $*"
((ERRORS++))
}
warning() {
echo "⚠️ WARNING: $*"
((WARNINGS++))
}
success() {
echo "$*"
}
# Check if file has managed header
check_managed_header() {
local file="$1"
if grep -q "^<!-- Managed by agent:" "$file"; then
success "Managed header present: $file"
return 0
else
warning "Missing managed header: $file"
return 1
fi
}
# Check if root is thin (≤50 lines or has scope index)
check_root_is_thin() {
local file="$1"
local line_count=$(wc -l < "$file")
if [ "$line_count" -le 50 ]; then
success "Root is thin: $line_count lines"
return 0
elif grep -q "## Index of scoped AGENTS.md" "$file"; then
success "Root has scope index (verbose style acceptable)"
return 0
else
error "Root is bloated: $line_count lines and no scope index"
return 1
fi
}
# Check if root has precedence statement
check_precedence_statement() {
local file="$1"
if grep -qi "precedence" "$file" && grep -qi "closest.*AGENTS.md.*wins" "$file"; then
success "Precedence statement present"
return 0
else
error "Missing precedence statement in root"
return 1
fi
}
# Check if scoped file has all 9 sections
check_scoped_sections() {
local file="$1"
local required_sections=(
"## Overview"
"## Setup & environment"
"## Build & tests"
"## Code style & conventions"
"## Security & safety"
"## PR/commit checklist"
"## Good vs. bad examples"
"## When stuck"
)
local missing=()
for section in "${required_sections[@]}"; do
if ! grep -q "^$section" "$file"; then
missing+=("$section")
fi
done
if [ ${#missing[@]} -eq 0 ]; then
success "All required sections present: $file"
return 0
else
error "Missing sections in $file: ${missing[*]}"
return 1
fi
}
# Check if scope index links work
check_scope_links() {
local root_file="$1"
if ! grep -q "## Index of scoped AGENTS.md" "$root_file"; then
return 0 # No index, skip check
fi
# Extract links from scope index
local links=$(sed -n '/## Index of scoped AGENTS.md/,/^##/p' "$root_file" | grep -o '\./[^)]*AGENTS.md' || true)
if [ -z "$links" ]; then
warning "Scope index present but no links found"
return 1
fi
local broken=()
while read -r link; do
# Remove leading ./
local clean_link="${link#./}"
local full_path="$PROJECT_DIR/$clean_link"
if [ ! -f "$full_path" ]; then
broken+=("$link")
fi
done <<< "$links"
if [ ${#broken[@]} -eq 0 ]; then
success "All scope index links work"
return 0
else
error "Broken scope index links: ${broken[*]}"
return 1
fi
}
# Main validation
echo "Validating AGENTS.md structure in: $PROJECT_DIR"
echo ""
# Check root AGENTS.md
ROOT_FILE="$PROJECT_DIR/AGENTS.md"
if [ ! -f "$ROOT_FILE" ]; then
error "Root AGENTS.md not found"
else
echo "=== Root AGENTS.md ==="
check_managed_header "$ROOT_FILE"
check_root_is_thin "$ROOT_FILE"
check_precedence_statement "$ROOT_FILE"
check_scope_links "$ROOT_FILE"
echo ""
fi
# Check scoped AGENTS.md files
SCOPED_FILES=$(find "$PROJECT_DIR" -name "AGENTS.md" -not -path "$ROOT_FILE" 2>/dev/null || true)
if [ -n "$SCOPED_FILES" ]; then
echo "=== Scoped AGENTS.md Files ==="
while read -r file; do
rel_path="${file#$PROJECT_DIR/}"
echo "Checking: $rel_path"
check_managed_header "$file"
check_scoped_sections "$file"
echo ""
done <<< "$SCOPED_FILES"
fi
# Summary
echo "=== Validation Summary ==="
if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
echo "✅ All checks passed!"
exit 0
elif [ $ERRORS -eq 0 ]; then
echo "⚠️ Validation passed with $WARNINGS warning(s)"
exit 0
else
echo "❌ Validation failed with $ERRORS error(s) and $WARNINGS warning(s)"
exit 1
fi