AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\"$squash_log\")\n\n if [ \"$squash_patterns\" -gt $((total_commits / 3)) ]; then\n strategy=\"squash-and-merge\"\n fi\n\n jq -n \\\n --arg strategy \"$strategy\" \\\n --arg merge_pct \"$merge_pct\" \\\n --arg merge_commits \"$merge_commits\" \\\n --arg total \"$total_commits\" \\\n '{\n strategy: $strategy,\n merge_commit_percentage: ($merge_pct | tonumber),\n stats: {\n merge_commits: ($merge_commits | tonumber),\n total_commits: ($total | tonumber)\n }\n }'\n}\n\n# Analyze release tagging\nanalyze_releases() {\n local tags\n tags=$(project_git tag -l 2>/dev/null | tail -20 || echo \"\")\n\n if [ -z \"$tags\" ]; then\n echo '{\"pattern\": \"none\", \"total_tags\": 0}'\n return\n fi\n\n local total_tags\n total_tags=$(echo \"$tags\" | wc -l)\n\n # Check for semver pattern (v1.2.3 or 1.2.3)\n local semver_count\n semver_count=$(count_matches '^v?[0-9]+\\.[0-9]+\\.[0-9]+' \"$tags\")\n\n # Check for calver pattern (2024.01.15 or similar)\n local calver_count\n calver_count=$(count_matches '^[0-9]{4}\\.[0-9]{2}' \"$tags\")\n\n local pattern=\"custom\"\n if [ \"$semver_count\" -gt $((total_tags / 2)) ]; then\n pattern=\"semver\"\n elif [ \"$calver_count\" -gt $((total_tags / 2)) ]; then\n pattern=\"calver\"\n fi\n\n # Check for v prefix\n local v_prefix_count has_v_prefix\n v_prefix_count=$(count_matches '^v' \"$tags\")\n has_v_prefix=$([[ \"$v_prefix_count\" -gt $((total_tags / 2)) ]] && echo \"true\" || echo \"false\")\n\n # Get latest tag\n local latest_tag\n latest_tag=$(project_git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n\n jq -n \\\n --arg pattern \"$pattern\" \\\n --argjson has_v_prefix \"$has_v_prefix\" \\\n --arg latest \"$latest_tag\" \\\n --arg total \"$total_tags\" \\\n --arg semver \"$semver_count\" \\\n '{\n pattern: $pattern,\n uses_v_prefix: $has_v_prefix,\n latest_tag: $latest,\n stats: {\n total_tags: ($total | tonumber),\n semver_tags: ($semver | tonumber)\n }\n }'\n}\n\n# Analyze default branch\nanalyze_default_branch() {\n local default_branch\n default_branch=$(project_git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo \"\")\n\n if [ -z \"$default_branch\" ]; then\n # Try to detect from common names\n if project_git show-ref --verify --quiet refs/heads/main 2>/dev/null; then\n default_branch=\"main\"\n elif project_git show-ref --verify --quiet refs/heads/master 2>/dev/null; then\n default_branch=\"master\"\n fi\n fi\n\n jq -n --arg branch \"$default_branch\" '{default_branch: $branch}'\n}\n\n# Run all analyses\nCOMMIT_CONVENTION=$(analyze_commit_convention)\nBRANCH_NAMING=$(analyze_branch_naming)\nMERGE_STRATEGY=$(analyze_merge_strategy)\nRELEASES=$(analyze_releases)\nDEFAULT_BRANCH=$(analyze_default_branch)\n\n# Build final JSON output\njq -n \\\n --argjson commits \"$COMMIT_CONVENTION\" \\\n --argjson branches \"$BRANCH_NAMING\" \\\n --argjson merge \"$MERGE_STRATEGY\" \\\n --argjson releases \"$RELEASES\" \\\n --argjson default \"$DEFAULT_BRANCH\" \\\n '{\n commit_convention: $commits,\n branch_naming: $branches,\n merge_strategy: $merge,\n releases: $releases,\n default_branch: $default.default_branch\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":10491,"content_sha256":"af31c9c0816150dd64c02f9e9f1d8e7f61a5c2a55d3d1c4fabb02adcb7ea9931"},{"filename":"scripts/check-freshness.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Check if AGENTS.md files are up to date with recent git commits\n# Compares the \"Last updated\" date in each AGENTS.md with commits affecting that scope\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Options\nVERBOSE=false\nJSON=false\nDAYS_THRESHOLD=7 # Warn if commits are older than this many days after last update (used below)\n\n# Parse flags\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --verbose|-v)\n VERBOSE=true\n shift\n ;;\n --json)\n JSON=true\n shift\n ;;\n --threshold=*)\n # shellcheck disable=SC2034 # Reserved for future threshold-based staleness check\n DAYS_THRESHOLD=\"${1#*=}\"\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: check-freshness.sh [PROJECT_DIR] [OPTIONS]\n\nCheck if AGENTS.md files are up to date with recent git commits.\n\nOptions:\n --verbose, -v Show detailed output including commit lists\n --json Emit machine-readable JSON on stdout (human output suppressed)\n --threshold=DAYS Days after last update to consider stale (default: 7)\n --help, -h Show this help message\n\nExamples:\n check-freshness.sh . # Check all AGENTS.md freshness\n check-freshness.sh . --verbose # Show commit details\n check-freshness.sh . --threshold=14 # Use 14-day threshold\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\n# Ensure we're in a git repository\nif ! project_git rev-parse --git-dir > /dev/null 2>&1; then\n echo \"Error: Not a git repository\"\n exit 1\nfi\n\n# In JSON mode, route all human-readable output to /dev/null and reserve the\n# original stdout (fd 3) for the single JSON document emitted at the end. This\n# keeps --json strictly additive: the default (no-flag) path is untouched.\nJSON_FILES=()\nif [[ \"$JSON\" = true ]]; then\n exec 3>&1 1>/dev/null\nfi\n\nSTALE_COUNT=0\nFRESH_COUNT=0\nUNKNOWN_COUNT=0\n\nlog() {\n if [ \"$VERBOSE\" = true ]; then\n echo \"[INFO] $*\" >&2\n fi\n}\n\n# Extract the \"Last updated\" date from AGENTS.md header\n# Format: \u003c!-- Managed by agent: ... Last updated: YYYY-MM-DD -->\nextract_last_updated() {\n local file=\"$1\"\n local date_str\n\n # Try to extract date from header comment\n date_str=$(grep -o 'Last updated: [0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}' \"$file\" 2>/dev/null | head -1 | sed 's/Last updated: //')\n\n if [ -n \"$date_str\" ]; then\n echo \"$date_str\"\n return 0\n fi\n\n # Try alternative formats\n # \"Created: YYYY-MM-DD\" or \"Updated: YYYY-MM-DD\"\n date_str=$(grep -oE '(Created|Updated): [0-9]{4}-[0-9]{2}-[0-9]{2}' \"$file\" 2>/dev/null | head -1 | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')\n\n if [ -n \"$date_str\" ]; then\n echo \"$date_str\"\n return 0\n fi\n\n return 1\n}\n\n# Get the scope directory for an AGENTS.md file\n# Root AGENTS.md covers the whole repo, scoped files cover their directory\nget_scope_path() {\n local agents_file=\"$1\"\n local dir\n\n dir=$(dirname \"$agents_file\")\n\n if [ \"$dir\" = \".\" ]; then\n echo \".\" # Root covers everything\n else\n echo \"$dir\"\n fi\n}\n\n# Count commits in a scope since a given date\ncount_commits_since() {\n local scope_path=\"$1\"\n local since_date=\"$2\"\n local count\n\n if [ \"$scope_path\" = \".\" ]; then\n # Root: check all commits except AGENTS.md files themselves\n count=$(project_git log --oneline --since=\"$since_date\" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | wc -l)\n else\n # Scoped: check commits in that directory\n count=$(project_git log --oneline --since=\"$since_date\" -- \"$scope_path\" ':(exclude)**/AGENTS.md' 2>/dev/null | wc -l)\n fi\n\n echo \"$count\"\n}\n\n# Get commit summary for a scope since a given date\nget_commits_since() {\n local scope_path=\"$1\"\n local since_date=\"$2\"\n\n if [ \"$scope_path\" = \".\" ]; then\n project_git log --oneline --since=\"$since_date\" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | head -10\n else\n project_git log --oneline --since=\"$since_date\" -- \"$scope_path\" ':(exclude)**/AGENTS.md' 2>/dev/null | head -10\n fi\n}\n\n# Get files changed in a scope since a given date\nget_changed_files_since() {\n local scope_path=\"$1\"\n local since_date=\"$2\"\n\n if [ \"$scope_path\" = \".\" ]; then\n project_git log --name-only --pretty=format: --since=\"$since_date\" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | sort -u | grep -v '^

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| head -20\n else\n project_git log --name-only --pretty=format: --since=\"$since_date\" -- \"$scope_path\" ':(exclude)**/AGENTS.md' 2>/dev/null | sort -u | grep -v '^

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| head -20\n fi\n}\n\n# Check freshness of a single AGENTS.md file\ncheck_file_freshness() {\n local agents_file=\"$1\"\n local rel_path=\"${agents_file#\"$PROJECT_DIR\"/}\"\n local last_updated\n local scope_path\n local commit_count\n\n echo \"Checking: $rel_path\"\n\n # Extract last updated date\n if ! last_updated=$(extract_last_updated \"$agents_file\"); then\n echo \" ⚠️ No 'Last updated' date found in header\"\n ((UNKNOWN_COUNT++)) || true\n if [[ \"$JSON\" = true ]]; then\n JSON_FILES+=(\"$(jq -nc --arg path \"$rel_path\" \\\n '{path:$path,status:\"unknown\",last_updated:null,commits_since:null}')\")\n fi\n return 1\n fi\n\n log \" Last updated: $last_updated\"\n\n # Get scope path\n scope_path=$(get_scope_path \"$agents_file\")\n log \" Scope: $scope_path\"\n\n # Count commits since last update\n commit_count=$(count_commits_since \"$scope_path\" \"$last_updated\")\n\n if [ \"$commit_count\" -eq 0 ]; then\n echo \" ✅ Up to date (no commits since $last_updated)\"\n ((FRESH_COUNT++)) || true\n if [[ \"$JSON\" = true ]]; then\n JSON_FILES+=(\"$(jq -nc --arg path \"$rel_path\" --arg lu \"$last_updated\" \\\n '{path:$path,status:\"fresh\",last_updated:$lu,commits_since:0}')\")\n fi\n return 0\n fi\n\n # Check if commits are significant\n echo \" ⚠️ Potentially stale: $commit_count commit(s) since $last_updated\"\n ((STALE_COUNT++)) || true\n if [[ \"$JSON\" = true ]]; then\n JSON_FILES+=(\"$(jq -nc --arg path \"$rel_path\" --arg lu \"$last_updated\" --argjson cs \"$commit_count\" \\\n '{path:$path,status:\"stale\",last_updated:$lu,commits_since:$cs}')\")\n fi\n\n if [ \"$VERBOSE\" = true ]; then\n echo \" Recent commits:\"\n get_commits_since \"$scope_path\" \"$last_updated\" | while read -r line; do\n echo \" - $line\"\n done\n\n echo \" Changed files:\"\n get_changed_files_since \"$scope_path\" \"$last_updated\" | while read -r line; do\n echo \" - $line\"\n done\n fi\n\n return 1\n}\n\n# Main\necho \"Checking AGENTS.md freshness in: $PROJECT_DIR\"\necho \"\"\n\n# Find all AGENTS.md files. Dependency trees are excluded, as in\n# validate-structure.sh (#84): a third-party AGENTS.md is not this project's.\nAGENTS_FILES=$(find \"$PROJECT_DIR\" -name \"AGENTS.md\" -type f \\\n -not -path \"*/.git/*\" \\\n -not -path \"*/vendor/*\" \\\n -not -path \"*/node_modules/*\" \\\n -not -path \"*/.Build/*\" \\\n 2>/dev/null | sort)\n\nif [ -z \"$AGENTS_FILES\" ]; then\n echo \"No AGENTS.md files found\"\n exit 0\nfi\n\n# Check each file.\n# `|| true`: check_file_freshness returns non-zero for stale/unknown files; without\n# this guard `set -e` aborts the loop on the first such file, processing only one\n# file and defeating the summary below (and forcing the validate-structure caller to\n# always warn). Tolerating the non-zero lets every file be counted.\nwhile read -r file; do\n check_file_freshness \"$file\" || true\n echo \"\"\ndone \u003c\u003c\u003c \"$AGENTS_FILES\"\n\n# Emit JSON document (machine-readable) and exit before the human summary.\nif [[ \"$JSON\" = true ]]; then\n if [[ \"${#JSON_FILES[@]}\" -eq 0 ]]; then\n files_json='[]'\n else\n files_json=$(printf '%s\\n' \"${JSON_FILES[@]}\" | jq -s '.')\n fi\n jq -nc \\\n --argjson files \"$files_json\" \\\n --argjson fresh \"$FRESH_COUNT\" \\\n --argjson stale \"$STALE_COUNT\" \\\n --argjson unknown \"$UNKNOWN_COUNT\" \\\n '{script:\"check-freshness\",schema:1,summary:{fresh:$fresh,stale:$stale,unknown:$unknown},files:$files}' >&3\n if [[ \"$STALE_COUNT\" -gt 0 ]]; then exit 1; fi\n exit 0\nfi\n\n# Summary\necho \"=== Freshness Summary ===\"\necho \"✅ Up to date: $FRESH_COUNT\"\necho \"⚠️ Potentially stale: $STALE_COUNT\"\n[ \"$UNKNOWN_COUNT\" -gt 0 ] && echo \"❓ Unknown (no date): $UNKNOWN_COUNT\"\n\nif [ \"$STALE_COUNT\" -gt 0 ]; then\n echo \"\"\n echo \"Recommendation: Review the stale AGENTS.md files and update if needed.\"\n echo \"Use --verbose to see which commits and files have changed.\"\n exit 1\nfi\n\nexit 0\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9090,"content_sha256":"9f5531e152f1ca6b4c3587c6c74e6bc50df99b028994ff32c227c504dce76282"},{"filename":"scripts/detect-golden-samples.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Detect golden sample files (canonical patterns to follow)\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Check if git is available\nif ! project_git rev-parse --git-dir > /dev/null 2>&1; then\n echo \"\"\n exit 0\nfi\n\n# Get project language\nPROJECT_INFO=$(bash \"$(dirname \"$0\")/detect-project.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{\"language\":\"unknown\"}')\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\n\n# Find high-churn files (frequently modified = important)\nget_high_churn_files() {\n project_git log --name-only --pretty=format: --since=\"6 months ago\" 2>/dev/null | \\\n grep -v '^

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| \\\n sort | uniq -c | sort -rn | head -20\n}\n\n# Find entrypoint files\nfind_entrypoints() {\n case \"$LANGUAGE\" in\n \"go\")\n find . -name \"main.go\" -type f 2>/dev/null | head -5\n ;;\n \"typescript\")\n for f in src/index.ts src/index.tsx src/main.ts src/main.tsx app/page.tsx pages/index.tsx; do\n [ -f \"$f\" ] && echo \"$f\"\n done\n ;;\n \"php\")\n for f in public/index.php src/Kernel.php ext_localconf.php; do\n [ -f \"$f\" ] && echo \"$f\"\n done\n ;;\n \"python\")\n for f in src/main.py main.py app/main.py __main__.py; do\n [ -f \"$f\" ] && echo \"$f\"\n done\n ;;\n esac\n}\n\n# Find component/model examples\nfind_examples() {\n case \"$LANGUAGE\" in\n \"go\")\n # Find a service or handler file\n find . -path ./vendor -prune -o -name \"*_service.go\" -type f -print 2>/dev/null | head -1\n find . -path ./vendor -prune -o -name \"*_handler.go\" -type f -print 2>/dev/null | head -1\n ;;\n \"typescript\")\n # Find a component\n find . -path ./node_modules -prune -o -name \"*.tsx\" -type f -print 2>/dev/null | \\\n grep -E \"(Button|Card|Modal|Form)\" | head -1\n # Find an API route\n find . -path ./node_modules -prune -o -path \"*/api/*\" -name \"*.ts\" -type f -print 2>/dev/null | head -1\n ;;\n \"php\")\n # Find a controller\n find . -path ./vendor -prune -o -name \"*Controller.php\" -type f -print 2>/dev/null | head -1\n # Find a service\n find . -path ./vendor -prune -o -name \"*Service.php\" -type f -print 2>/dev/null | head -1\n ;;\n \"python\")\n # Find a service or model\n find . -path ./.venv -prune -o -name \"*_service.py\" -type f -print 2>/dev/null | head -1\n find . -path ./.venv -prune -o -name \"models.py\" -type f -print 2>/dev/null | head -1\n ;;\n esac\n}\n\n# Find test examples\nfind_test_examples() {\n case \"$LANGUAGE\" in\n \"go\")\n find . -path ./vendor -prune -o -name \"*_test.go\" -type f -print 2>/dev/null | head -1\n ;;\n \"typescript\")\n find . -path ./node_modules -prune -o -name \"*.test.ts\" -o -name \"*.test.tsx\" -type f -print 2>/dev/null | head -1\n ;;\n \"php\")\n find . -path ./vendor -prune -o -name \"*Test.php\" -type f -print 2>/dev/null | head -1\n ;;\n \"python\")\n find . -path ./.venv -prune -o -name \"test_*.py\" -type f -print 2>/dev/null | head -1\n ;;\n esac\n}\n\n# Describe file based on name/path\ndescribe_file() {\n local file=\"$1\"\n case \"$file\" in\n *main.go|*main.ts|*main.py|*/index.ts*) echo \"Entrypoint\" ;;\n *Controller*) echo \"Controller\" ;;\n *Service*|*_service*) echo \"Service\" ;;\n *Handler*|*_handler*) echo \"Handler\" ;;\n *Model*|*models*) echo \"Model\" ;;\n *Test*|*_test*) echo \"Test\" ;;\n *Button*|*Card*|*Modal*) echo \"Component\" ;;\n */api/*) echo \"API route\" ;;\n *) echo \"Reference\" ;;\n esac\n}\n\n# Describe key patterns in file\ndescribe_patterns() {\n local file=\"$1\"\n local patterns=\"\"\n\n if [ -f \"$file\" ]; then\n # Check for common patterns\n grep -l \"interface\" \"$file\" > /dev/null 2>&1 && patterns=\"$patterns, interface\"\n grep -l \"async\" \"$file\" > /dev/null 2>&1 && patterns=\"$patterns, async\"\n grep -l \"class\" \"$file\" > /dev/null 2>&1 && patterns=\"$patterns, class\"\n grep -l \"test\\|describe\\|it(\" \"$file\" > /dev/null 2>&1 && patterns=\"$patterns, tests\"\n grep -l \"func.*error\" \"$file\" > /dev/null 2>&1 && patterns=\"$patterns, error handling\"\n fi\n\n # Remove leading \", \"\n echo \"${patterns#, }\"\n}\n\n# Generate output in table row format\noutput=\"\"\n\n# Add entrypoints\nwhile IFS= read -r file; do\n [ -z \"$file\" ] && continue\n file=\"${file#./}\"\n desc=$(describe_file \"$file\")\n patterns=$(describe_patterns \"$file\")\n [ -n \"$patterns\" ] && patterns=\" ($patterns)\"\n output=\"$output| $desc | \\`$file\\` | ${patterns:-standard patterns} |\\n\"\ndone \u003c \u003c(find_entrypoints)\n\n# Add examples\nwhile IFS= read -r file; do\n [ -z \"$file\" ] && continue\n file=\"${file#./}\"\n desc=$(describe_file \"$file\")\n patterns=$(describe_patterns \"$file\")\n [ -n \"$patterns\" ] && patterns=\" ($patterns)\"\n output=\"$output| $desc | \\`$file\\` | ${patterns:-standard patterns} |\\n\"\ndone \u003c \u003c(find_examples)\n\n# Add test example\nwhile IFS= read -r file; do\n [ -z \"$file\" ] && continue\n file=\"${file#./}\"\n output=\"$output| Test | \\`$file\\` | test structure |\\n\"\ndone \u003c \u003c(find_test_examples)\n\n# Output (remove empty lines, duplicates)\necho -e \"$output\" | sed '/^$/d' | sort -u | head -10\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5804,"content_sha256":"32469029339c2817661ad3dc4299c195fb41293dd44509d24903f897019ad489"},{"filename":"scripts/detect-heuristics.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Detect project heuristics (When X → Do Y decisions)\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Get project language\nPROJECT_INFO=$(bash \"$(dirname \"$0\")/detect-project.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{\"language\":\"unknown\"}')\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\n\noutput=\"\"\n\n# Check for env file patterns\nif [ -f \".env.example\" ] || [ -f \".env.sample\" ]; then\n output=\"$output| Adding env var | Add to \\`.env.example\\` first |\\n\"\nfi\n\n# Check for TypeScript env types\nif [ -f \"src/env.d.ts\" ] || [ -f \"types/env.d.ts\" ]; then\n output=\"$output| Adding env var | Also update \\`types/env.d.ts\\` |\\n\"\nfi\n\n# Check for Next.js App Router\nif [ -d \"app\" ] && { [ -f \"next.config.js\" ] || [ -f \"next.config.ts\" ] || [ -f \"next.config.mjs\" ]; }; then\n output=\"$output| Adding new page | Create in \\`app/\\` (App Router) |\\n\"\nfi\n\n# Check for old Next.js Pages Router\nif [ -d \"pages\" ] && [ ! -d \"app\" ]; then\n output=\"$output| Adding new page | Create in \\`pages/\\` directory |\\n\"\nfi\n\n# Check for state management\nif grep -q \"zustand\" package.json 2>/dev/null; then\n output=\"$output| Adding state | Use Zustand store in \\`stores/\\` |\\n\"\nelif grep -q \"redux\" package.json 2>/dev/null; then\n output=\"$output| Adding state | Use Redux slice pattern |\\n\"\nelif grep -q \"mobx\" package.json 2>/dev/null; then\n output=\"$output| Adding state | Use MobX observable pattern |\\n\"\nfi\n\n# Check for API directory patterns\nif [ -d \"src/api\" ] || [ -d \"api\" ]; then\n api_dir=$([ -d \"src/api\" ] && echo \"src/api\" || echo \"api\")\n output=\"$output| Adding API endpoint | Create in \\`$api_dir/\\` |\\n\"\nfi\n\n# Check for test patterns\nif [ -d \"__tests__\" ]; then\n output=\"$output| Adding tests | Create in \\`__tests__/\\` directory |\\n\"\nelif [ -d \"tests\" ] || [ -d \"test\" ]; then\n test_dir=$([ -d \"tests\" ] && echo \"tests\" || echo \"test\")\n output=\"$output| Adding tests | Create in \\`$test_dir/\\` directory |\\n\"\nfi\n\n# Check for TYPO3\nif [ -f \"ext_emconf.php\" ]; then\n output=\"$output| Adding controller | Create in \\`Classes/Controller/\\` |\\n\"\n output=\"$output| Adding service | Create in \\`Classes/Service/\\` |\\n\"\nfi\n\n# Check for Makefile\nif [ -f \"Makefile\" ]; then\n output=\"$output| Running tasks | Check \\`make help\\` for available commands |\\n\"\nfi\n\n# Check for Docker\nif [ -f \"docker-compose.yml\" ] || [ -f \"docker-compose.yaml\" ] || [ -f \"compose.yml\" ]; then\n output=\"$output| Running locally | Use \\`docker compose up\\` |\\n\"\nfi\n\n# Check for DDEV\nif [ -d \".ddev\" ]; then\n output=\"$output| Running locally | Use \\`ddev start\\` then \\`ddev ssh\\` |\\n\"\nfi\n\n# Language-specific heuristics\ncase \"$LANGUAGE\" in\n \"go\")\n output=\"$output| Adding package | Internal → \\`internal/\\`, Public → \\`pkg/\\` |\\n\"\n [ -f \"go.work\" ] && output=\"$output| Multi-module | Use \\`go.work\\` for workspace |\\n\"\n ;;\n \"php\")\n output=\"$output| Adding class | Follow PSR-4 in \\`Classes/\\` or \\`src/\\` |\\n\"\n ;;\n \"python\")\n [ -f \"pyproject.toml\" ] && output=\"$output| Adding dependency | Update \\`pyproject.toml\\` |\\n\"\n ;;\nesac\n\n# Output (remove empty lines, duplicates, limit to 10)\necho -e \"$output\" | sed '/^$/d' | sort -u | head -10\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3326,"content_sha256":"48c60e659e32a83d660e3778ebfe48090ee32b9090788955054ef073104f74b1"},{"filename":"scripts/detect-project.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Detect project type, language, version, and build tools\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # pre-commit runs shellcheck without -x, so\n# it cannot follow this source however the path is written.\nsource \"$SCRIPT_DIR/lib/config-root.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Initialize variables\nLANGUAGE=\"unknown\"\nVERSION=\"unknown\"\nBUILD_TOOL=\"unknown\" # Deprecated: use package_manager or task_runner\nPACKAGE_MANAGER=\"unknown\" # Package manager: npm/yarn/pnpm/bun, composer, poetry/uv/pip, go\nTASK_RUNNER=\"none\" # Task runner: make/just/mage/bazel, or none\nFRAMEWORK=\"none\"\nPROJECT_TYPE=\"unknown\"\nQUALITY_TOOLS=()\nTEST_FRAMEWORK=\"unknown\"\nHAS_DOCKER=false\nCI=\"none\"\nIDE_CONFIGS=()\nAGENT_CONFIGS=()\nSTACKS=() # Multi-stack detection: (go, php, python, typescript, docker)\n\n# Detect language and version\ndetect_language() {\n if [ -f \"go.mod\" ]; then\n LANGUAGE=\"go\"\n VERSION=$(grep '^go ' go.mod | awk '{print $2}' || echo \"unknown\")\n BUILD_TOOL=\"go\"\n PACKAGE_MANAGER=\"go\"\n TEST_FRAMEWORK=\"testing\"\n\n # Detect Go project type\n if [ -d \"cmd\" ]; then\n PROJECT_TYPE=\"go-cli\"\n elif grep -q \"github.com/gofiber/fiber\" go.mod 2>/dev/null; then\n PROJECT_TYPE=\"go-web-app\"\n FRAMEWORK=\"fiber\"\n elif grep -q \"github.com/labstack/echo\" go.mod 2>/dev/null; then\n PROJECT_TYPE=\"go-web-app\"\n FRAMEWORK=\"echo\"\n elif grep -q \"github.com/gin-gonic/gin\" go.mod 2>/dev/null; then\n PROJECT_TYPE=\"go-web-app\"\n FRAMEWORK=\"gin\"\n else\n PROJECT_TYPE=\"go-library\"\n fi\n\n # Detect Go quality tools\n { [ -f \".golangci.yml\" ] || [ -f \".golangci.yaml\" ]; } && QUALITY_TOOLS+=(\"golangci-lint\") || true\n command -v gofmt &>/dev/null && QUALITY_TOOLS+=(\"gofmt\") || true\n\n elif [ -f \"composer.json\" ]; then\n # Check if this is actually a PHP project:\n # 1. Has PHP version requirement, OR\n # 2. Has PHP source files, OR\n # 3. Has PHP framework dependencies\n local has_php_requirement=false\n local has_php_files=false\n local has_php_deps=false\n\n # Check for PHP version requirement\n if jq -e '.require.php // .[\"require-dev\"].php' composer.json &>/dev/null; then\n has_php_requirement=true\n fi\n\n # Check for PHP source files (common locations)\n if [[ -d \"src\" ]] && [[ -n \"$(find src -name \"*.php\" -type f -print -quit 2>/dev/null)\" ]]; then\n has_php_files=true\n elif [[ -d \"Classes\" ]] && [[ -n \"$(find Classes -name \"*.php\" -type f -print -quit 2>/dev/null)\" ]]; then\n has_php_files=true\n elif [[ -d \"lib\" ]] && [[ -n \"$(find lib -name \"*.php\" -type f -print -quit 2>/dev/null)\" ]]; then\n has_php_files=true\n elif [ -f \"ext_emconf.php\" ]; then\n has_php_files=true\n elif [[ -n \"$(find . -maxdepth 2 -name \"*.php\" -type f -print -quit 2>/dev/null)\" ]]; then\n has_php_files=true\n fi\n\n # Check for PHP framework dependencies\n if jq -e '.require.\"typo3/cms-core\" // .require.\"laravel/framework\" // .require.\"symfony/framework-bundle\" // .require.\"oro/platform\"' composer.json &>/dev/null; then\n has_php_deps=true\n fi\n\n # Only treat as PHP if we have evidence it's a PHP project\n if [ \"$has_php_requirement\" = true ] || [ \"$has_php_files\" = true ] || [ \"$has_php_deps\" = true ]; then\n LANGUAGE=\"php\"\n VERSION=$(jq -r '.require.php // \"unknown\"' composer.json 2>/dev/null || echo \"unknown\")\n BUILD_TOOL=\"composer\"\n PACKAGE_MANAGER=\"composer\"\n else\n # composer.json exists but this isn't a PHP project\n # Continue to check other languages\n :\n fi\n\n # Detect PHP framework (only if we determined this is PHP)\n if [ \"$LANGUAGE\" = \"php\" ]; then\n # TYPO3 extension detection (ext_emconf.php is definitive)\n if [ -f \"ext_emconf.php\" ]; then\n PROJECT_TYPE=\"php-typo3-extension\"\n FRAMEWORK=\"typo3\"\n elif jq -e '.require.\"typo3/cms-core\"' composer.json &>/dev/null; then\n PROJECT_TYPE=\"php-typo3\"\n FRAMEWORK=\"typo3\"\n # Oro detection (OroCommerce, OroPlatform, OroCRM)\n # Differentiate between full Oro project and standalone bundle\n elif jq -e '.require.\"oro/platform\"' composer.json &>/dev/null || \\\n jq -e '.require.\"oro/commerce\"' composer.json &>/dev/null || \\\n jq -e '.require.\"oro/crm\"' composer.json &>/dev/null; then\n FRAMEWORK=\"oro\"\n # Check if it's a standalone bundle (has type: oro-bundle, or no bin/console)\n composer_type=$(jq -r '.type // \"\"' composer.json 2>/dev/null)\n if [ \"$composer_type\" = \"oro-bundle\" ] || [ \"$composer_type\" = \"symfony-bundle\" ]; then\n PROJECT_TYPE=\"php-oro-bundle\"\n elif [ -f \"bin/console\" ] || [ -f \"public/index.php\" ]; then\n PROJECT_TYPE=\"php-oro\"\n else\n # Likely a standalone bundle without explicit type\n PROJECT_TYPE=\"php-oro-bundle\"\n fi\n elif [ -f \"config/oro/bundles.yml\" ]; then\n PROJECT_TYPE=\"php-oro-bundle\"\n FRAMEWORK=\"oro\"\n elif jq -e '.require.\"laravel/framework\"' composer.json &>/dev/null; then\n PROJECT_TYPE=\"php-laravel\"\n FRAMEWORK=\"laravel\"\n elif jq -e '.require.\"symfony/symfony\"' composer.json &>/dev/null || \\\n jq -e '.require.\"symfony/framework-bundle\"' composer.json &>/dev/null; then\n PROJECT_TYPE=\"php-symfony\"\n FRAMEWORK=\"symfony\"\n else\n PROJECT_TYPE=\"php-library\"\n fi\n\n # Detect PHP quality tools\n jq -e '.require.\"phpstan/phpstan\" // .[\"require-dev\"].\"phpstan/phpstan\"' composer.json &>/dev/null && QUALITY_TOOLS+=(\"phpstan\") || true\n jq -e '.require.\"friendsofphp/php-cs-fixer\" // .[\"require-dev\"].\"friendsofphp/php-cs-fixer\"' composer.json &>/dev/null && QUALITY_TOOLS+=(\"php-cs-fixer\") || true\n { [ -f \"phpunit.xml\" ] || [ -f \"phpunit.xml.dist\" ]; } && TEST_FRAMEWORK=\"phpunit\" || true\n fi\n # If composer.json exists but it's not a PHP project, fall through to other checks\n\n fi\n\n # Check for package.json (might also exist alongside composer.json)\n if [ \"$LANGUAGE\" = \"unknown\" ] && [ -f \"package.json\" ]; then\n LANGUAGE=\"typescript\"\n VERSION=$(jq -r '.engines.node // \"unknown\"' package.json 2>/dev/null || echo \"unknown\")\n\n # Detect package manager from lockfile (workspace-aware, default npm)\n PACKAGE_MANAGER=\"npm\"\n local ws_root\n ws_root=$(find_node_workspace_root \"$(pwd)\" || true)\n if [ -n \"$ws_root\" ]; then\n # Check workspace root for lockfiles\n [ -f \"$ws_root/pnpm-lock.yaml\" ] && PACKAGE_MANAGER=\"pnpm\"\n [ -f \"$ws_root/yarn.lock\" ] && PACKAGE_MANAGER=\"yarn\"\n [ -f \"$ws_root/bun.lockb\" ] && PACKAGE_MANAGER=\"bun\"\n [ -f \"$ws_root/package-lock.json\" ] && PACKAGE_MANAGER=\"npm\"\n else\n # Fallback to local lockfiles\n [ -f \"yarn.lock\" ] && PACKAGE_MANAGER=\"yarn\"\n [ -f \"pnpm-lock.yaml\" ] && PACKAGE_MANAGER=\"pnpm\"\n [ -f \"bun.lockb\" ] && PACKAGE_MANAGER=\"bun\"\n fi\n BUILD_TOOL=\"$PACKAGE_MANAGER\"\n\n # Detect JS/TS framework\n if jq -e '.dependencies.\"next\"' package.json &>/dev/null; then\n PROJECT_TYPE=\"typescript-nextjs\"\n FRAMEWORK=\"next.js\"\n elif jq -e '.dependencies.\"react\"' package.json &>/dev/null; then\n PROJECT_TYPE=\"typescript-react\"\n FRAMEWORK=\"react\"\n elif jq -e '.dependencies.\"vue\"' package.json &>/dev/null; then\n PROJECT_TYPE=\"typescript-vue\"\n FRAMEWORK=\"vue\"\n elif jq -e '.dependencies.\"express\"' package.json &>/dev/null; then\n PROJECT_TYPE=\"typescript-node\"\n FRAMEWORK=\"express\"\n else\n PROJECT_TYPE=\"typescript-library\"\n fi\n\n # Detect quality tools\n jq -e '.devDependencies.\"eslint\"' package.json &>/dev/null && QUALITY_TOOLS+=(\"eslint\") || true\n jq -e '.devDependencies.\"prettier\"' package.json &>/dev/null && QUALITY_TOOLS+=(\"prettier\") || true\n jq -e '.devDependencies.\"typescript\"' package.json &>/dev/null && QUALITY_TOOLS+=(\"tsc\") || true\n\n # Detect test framework\n if jq -e '.devDependencies.\"jest\"' package.json &>/dev/null; then\n TEST_FRAMEWORK=\"jest\"\n elif jq -e '.devDependencies.\"vitest\"' package.json &>/dev/null; then\n TEST_FRAMEWORK=\"vitest\"\n fi || true\n\n fi\n\n # Check for pyproject.toml\n if [ \"$LANGUAGE\" = \"unknown\" ] && [ -f \"pyproject.toml\" ]; then\n LANGUAGE=\"python\"\n VERSION=$(grep 'requires-python' pyproject.toml | cut -d'\"' -f2 2>/dev/null || echo \"unknown\")\n\n # Detect Python package manager\n if grep -q '\\[tool.poetry\\]' pyproject.toml 2>/dev/null; then\n PACKAGE_MANAGER=\"poetry\"\n elif grep -q '\\[tool.uv\\]' pyproject.toml 2>/dev/null || [ -f \"uv.lock\" ]; then\n PACKAGE_MANAGER=\"uv\"\n elif grep -q '\\[tool.hatch\\]' pyproject.toml 2>/dev/null; then\n PACKAGE_MANAGER=\"hatch\"\n else\n PACKAGE_MANAGER=\"pip\"\n fi\n BUILD_TOOL=\"$PACKAGE_MANAGER\"\n\n # Detect framework\n if grep -q 'django' pyproject.toml 2>/dev/null; then\n PROJECT_TYPE=\"python-django\"\n FRAMEWORK=\"django\"\n elif grep -q 'flask' pyproject.toml 2>/dev/null; then\n PROJECT_TYPE=\"python-flask\"\n FRAMEWORK=\"flask\"\n elif grep -q 'fastapi' pyproject.toml 2>/dev/null; then\n PROJECT_TYPE=\"python-fastapi\"\n FRAMEWORK=\"fastapi\"\n elif [ -d \"scripts\" ] && [ \"$(find scripts -name '*.py' | wc -l)\" -gt 3 ]; then\n PROJECT_TYPE=\"python-cli\"\n else\n PROJECT_TYPE=\"python-library\"\n fi\n\n # Detect quality tools\n grep -q 'ruff' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=(\"ruff\")\n grep -q 'black' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=(\"black\")\n grep -q 'mypy' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=(\"mypy\")\n grep -q 'pytest' pyproject.toml 2>/dev/null && TEST_FRAMEWORK=\"pytest\"\n fi\n\n # Check for Claude Code plugin/skill repos\n # Can be: plugin-only, skill-only, plugin+skills, or any of these with bash\n if [ \"$LANGUAGE\" = \"unknown\" ]; then\n local has_plugin=false\n local has_skills=false\n local skill_count=0\n\n [ -f \".claude-plugin/plugin.json\" ] && has_plugin=true\n [ -d \"skills\" ] && skill_count=$(find skills -maxdepth 2 -name \"SKILL.md\" -type f 2>/dev/null | wc -l)\n [ \"$skill_count\" -gt 0 ] && has_skills=true\n\n if [ \"$has_plugin\" = true ] || [ \"$has_skills\" = true ]; then\n FRAMEWORK=\"claude-code\"\n PACKAGE_MANAGER=\"none\"\n\n # Determine project type based on combination\n if [ \"$has_plugin\" = true ] && [ \"$has_skills\" = true ]; then\n if [ \"$skill_count\" -gt 1 ]; then\n PROJECT_TYPE=\"claude-code-plugin-monorepo\"\n else\n PROJECT_TYPE=\"claude-code-plugin\"\n fi\n LANGUAGE=\"claude-code-plugin\"\n elif [ \"$has_plugin\" = true ]; then\n PROJECT_TYPE=\"claude-code-plugin\"\n LANGUAGE=\"claude-code-plugin\"\n else\n # Skills without plugin.json (standalone skill repo)\n if [ \"$skill_count\" -gt 1 ]; then\n PROJECT_TYPE=\"claude-code-skill-monorepo\"\n else\n PROJECT_TYPE=\"claude-code-skill\"\n fi\n LANGUAGE=\"claude-code-skill\"\n fi\n\n # Check for shell scripts (common in skills)\n local sh_count\n sh_count=$(find . -maxdepth 5 -name \"*.sh\" -type f 2>/dev/null | wc -l)\n if [ \"$sh_count\" -gt 3 ]; then\n BUILD_TOOL=\"bash\"\n [ -f \".shellcheckrc\" ] && QUALITY_TOOLS+=(\"shellcheck\") || true\n fi\n fi\n fi\n\n # Container-primary detection (Dockerfile-only repos)\n # Must come before bash fallback - some container repos have helper scripts\n if [ \"$LANGUAGE\" = \"unknown\" ]; then\n if [ -f \"Dockerfile\" ]; then\n # Check if this is primarily a container image project\n # Indicators: no source code, or only build scripts\n local has_source=false\n # Check for significant source files\n [ \"$(find . -maxdepth 3 -name '*.go' -o -name '*.py' -o -name '*.php' -o -name '*.ts' -o -name '*.js' 2>/dev/null | head -5 | wc -l)\" -gt 3 ] && has_source=true\n\n if [ \"$has_source\" = false ]; then\n LANGUAGE=\"docker\"\n PROJECT_TYPE=\"container-image\"\n BUILD_TOOL=\"docker\"\n PACKAGE_MANAGER=\"none\"\n FRAMEWORK=\"docker\"\n\n # Detect if it's a compose-based project\n if [ -f \"docker-compose.yml\" ] || [ -f \"compose.yml\" ] || [ -f \"compose.yaml\" ]; then\n PROJECT_TYPE=\"container-stack\"\n fi\n fi\n fi\n fi\n\n # Fallback: Check for bash/shell projects\n if [ \"$LANGUAGE\" = \"unknown\" ]; then\n local sh_count\n sh_count=$(find . -maxdepth 5 -name \"*.sh\" -type f 2>/dev/null | wc -l)\n if [ \"$sh_count\" -gt 3 ]; then\n LANGUAGE=\"bash\"\n PROJECT_TYPE=\"bash-scripts\"\n BUILD_TOOL=\"bash\"\n PACKAGE_MANAGER=\"none\"\n # Check for shellcheck\n [ -f \".shellcheckrc\" ] && QUALITY_TOOLS+=(\"shellcheck\") || true\n fi\n fi\n}\n\n# Detect task runner (does NOT override package_manager)\nif [ -f \"Makefile\" ]; then\n TASK_RUNNER=\"make\"\nelif [ -f \"justfile\" ] || [ -f \"Justfile\" ]; then\n TASK_RUNNER=\"just\"\nelif [ -f \"Taskfile.yml\" ] || [ -f \"Taskfile.yaml\" ]; then\n TASK_RUNNER=\"task\"\nfi\n\n# Detect Docker (check both old and new compose naming)\n[ -f \"Dockerfile\" ] || [ -f \"docker-compose.yml\" ] || [ -f \"compose.yml\" ] || [ -f \"compose.yaml\" ] && HAS_DOCKER=true\n\n# Detect CI\nif [ -d \".github/workflows\" ]; then\n CI=\"github-actions\"\nelif [ -f \".gitlab-ci.yml\" ]; then\n CI=\"gitlab-ci\"\nelif [ -f \".circleci/config.yml\" ]; then\n CI=\"circleci\"\nfi\n\n# Detect IDE configurations\n[ -f \".editorconfig\" ] && IDE_CONFIGS+=(\"editorconfig\") || true\n[ -d \".vscode\" ] && IDE_CONFIGS+=(\"vscode\") || true\n[ -d \".idea\" ] && IDE_CONFIGS+=(\"idea\") || true\n[ -d \".phpstorm\" ] && IDE_CONFIGS+=(\"phpstorm\") || true\n[ -d \".fleet\" ] && IDE_CONFIGS+=(\"fleet\") || true\n[ -f \".sublime-project\" ] && IDE_CONFIGS+=(\"sublime\") || true\n{ [ -d \".vim\" ] || [ -f \".vimrc\" ]; } && IDE_CONFIGS+=(\"vim\") || true\n{ [ -d \".nvim\" ] || [ -f \".nvimrc\" ]; } && IDE_CONFIGS+=(\"neovim\") || true\n\n# Detect AI coding agent configurations\n[ -d \".cursor\" ] && AGENT_CONFIGS+=(\"cursor\") || true\n{ [ -d \".claude\" ] || [ -f \"CLAUDE.md\" ] || [ -f \".claude/CLAUDE.md\" ]; } && AGENT_CONFIGS+=(\"claude\") || true\n[ -d \".windsurf\" ] && AGENT_CONFIGS+=(\"windsurf\") || true\n{ [ -d \".aider\" ] || [ -f \".aider.conf.yml\" ]; } && AGENT_CONFIGS+=(\"aider\") || true\n[ -d \".continue\" ] && AGENT_CONFIGS+=(\"continue\") || true\n{ [ -f \"copilot-instructions.md\" ] || [ -f \".github/copilot-instructions.md\" ]; } && AGENT_CONFIGS+=(\"copilot\") || true\n[ -d \".codeium\" ] && AGENT_CONFIGS+=(\"codeium\") || true\n[ -d \".tabnine\" ] && AGENT_CONFIGS+=(\"tabnine\") || true\n{ [ -d \".sourcegraph\" ] || [ -f \".sourcegraph/cody.json\" ]; } && AGENT_CONFIGS+=(\"cody\") || true\n\n# Run detection\ndetect_language\n\n# Multi-stack detection: detect secondary languages/technologies\n# STACKS array is populated here after primary language detection\ndetect_stacks() {\n # Always add primary language to stacks (if known)\n [ \"$LANGUAGE\" != \"unknown\" ] && STACKS+=(\"$LANGUAGE\")\n\n # Detect Docker as secondary stack (if not already primary)\n if [ \"$LANGUAGE\" != \"docker\" ] && [ \"$HAS_DOCKER\" = true ]; then\n STACKS+=(\"docker\")\n fi\n\n # Detect frontend stack in backend projects\n if [[ \"$LANGUAGE\" =~ ^(php|go|python)$ ]]; then\n # Check for Node.js frontend\n if [ -f \"package.json\" ]; then\n # Check what kind of frontend\n if jq -e '.dependencies.\"react\" // .dependencies.\"next\"' package.json &>/dev/null; then\n STACKS+=(\"react\")\n elif jq -e '.dependencies.\"vue\"' package.json &>/dev/null; then\n STACKS+=(\"vue\")\n elif jq -e '.dependencies.\"svelte\"' package.json &>/dev/null; then\n STACKS+=(\"svelte\")\n else\n STACKS+=(\"typescript\")\n fi\n fi\n # Check for frontend in subdirectories\n for frontend_dir in web frontend client ui internal/web; do\n if [ -d \"$frontend_dir\" ] && [ -f \"$frontend_dir/package.json\" ]; then\n if jq -e '.dependencies.\"react\" // .dependencies.\"next\"' \"$frontend_dir/package.json\" &>/dev/null; then\n [[ ! \" ${STACKS[*]} \" =~ \" react \" ]] && STACKS+=(\"react\")\n elif jq -e '.dependencies.\"vue\"' \"$frontend_dir/package.json\" &>/dev/null; then\n [[ ! \" ${STACKS[*]} \" =~ \" vue \" ]] && STACKS+=(\"vue\")\n fi\n fi\n done\n fi\n\n # Detect backend in frontend projects (monorepo patterns)\n if [[ \"$LANGUAGE\" == \"typescript\" ]]; then\n # Check for Go backend\n if [ -f \"go.mod\" ] || [ -d \"server\" ] && [ -f \"server/go.mod\" ]; then\n STACKS+=(\"go\")\n fi\n # Check for Python backend\n if [ -f \"pyproject.toml\" ] || { [ -d \"server\" ] && [ -f \"server/pyproject.toml\" ]; }; then\n STACKS+=(\"python\")\n fi\n # Check for PHP backend\n if [ -f \"composer.json\" ] || { [ -d \"api\" ] && [ -f \"api/composer.json\" ]; }; then\n STACKS+=(\"php\")\n fi\n fi\n}\n\ndetect_stacks\n\n# Output JSON\n# Handle empty arrays\nif [ ${#QUALITY_TOOLS[@]} -eq 0 ]; then\n TOOLS_JSON=\"[]\"\nelse\n TOOLS_JSON=\"$(printf '%s\\n' \"${QUALITY_TOOLS[@]}\" | jq -R . | jq -s .)\"\nfi\n\nif [ ${#IDE_CONFIGS[@]} -eq 0 ]; then\n IDE_JSON=\"[]\"\nelse\n IDE_JSON=\"$(printf '%s\\n' \"${IDE_CONFIGS[@]}\" | jq -R . | jq -s .)\"\nfi\n\nif [ ${#AGENT_CONFIGS[@]} -eq 0 ]; then\n AGENT_JSON=\"[]\"\nelse\n AGENT_JSON=\"$(printf '%s\\n' \"${AGENT_CONFIGS[@]}\" | jq -R . | jq -s .)\"\nfi\n\nif [ ${#STACKS[@]} -eq 0 ]; then\n STACKS_JSON=\"[]\"\nelse\n STACKS_JSON=\"$(printf '%s\\n' \"${STACKS[@]}\" | jq -R . | jq -s .)\"\nfi\n\njq -n \\\n --arg type \"$PROJECT_TYPE\" \\\n --arg lang \"$LANGUAGE\" \\\n --arg ver \"$VERSION\" \\\n --arg build \"$BUILD_TOOL\" \\\n --arg pkg_mgr \"$PACKAGE_MANAGER\" \\\n --arg task_runner \"$TASK_RUNNER\" \\\n --arg framework \"$FRAMEWORK\" \\\n --argjson docker \"$HAS_DOCKER\" \\\n --argjson tools \"$TOOLS_JSON\" \\\n --arg test \"$TEST_FRAMEWORK\" \\\n --arg ci \"$CI\" \\\n --argjson ide_configs \"$IDE_JSON\" \\\n --argjson agent_configs \"$AGENT_JSON\" \\\n --argjson stacks \"$STACKS_JSON\" \\\n '{\n type: $type,\n language: $lang,\n version: $ver,\n build_tool: $build,\n package_manager: $pkg_mgr,\n task_runner: $task_runner,\n framework: $framework,\n has_docker: $docker,\n quality_tools: $tools,\n test_framework: $test,\n ci: $ci,\n ide_configs: $ide_configs,\n agent_configs: $agent_configs,\n stacks: $stacks\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":20055,"content_sha256":"3910118bcb31ef2827c8d08cb49ebb8f5ba32da3c0af6e95725579ccef9739cf"},{"filename":"scripts/detect-scopes.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Detect directories that should have scoped AGENTS.md files\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\nMIN_FILES=5 # Minimum files to warrant scoped AGENTS.md\n\n# Get project info\nPROJECT_INFO=$(bash \"$(dirname \"$0\")/detect-project.sh\" \"$PROJECT_DIR\")\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\n\nscopes=()\n\n# Function to count source files in a directory\ncount_source_files() {\n local dir=\"$1\"\n local pattern=\"$2\"\n find \"$dir\" -maxdepth 3 -type f -name \"$pattern\" 2>/dev/null | wc -l\n}\n\n# Function to add scope (uses jq for proper JSON escaping)\nadd_scope() {\n local path=\"$1\"\n local type=\"$2\"\n local count=\"$3\"\n\n scopes+=(\"$(jq -n --arg p \"$path\" --arg t \"$type\" --argjson c \"$count\" \\\n '{path: $p, type: $t, files: $c}')\")\n}\n\n# Language-specific scope detection\ncase \"$LANGUAGE\" in\n \"go\")\n # Check common Go directories\n [ -d \"internal\" ] && {\n count=$(count_source_files \"internal\" \"*.go\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"internal\" \"backend-go\" \"$count\"\n }\n\n [ -d \"pkg\" ] && {\n count=$(count_source_files \"pkg\" \"*.go\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"pkg\" \"backend-go\" \"$count\"\n }\n\n [ -d \"cmd\" ] && {\n count=$(count_source_files \"cmd\" \"*.go\")\n [ \"$count\" -ge 3 ] && add_scope \"cmd\" \"cli\" \"$count\"\n }\n\n [ -d \"examples\" ] && {\n count=$(count_source_files \"examples\" \"*.go\")\n [ \"$count\" -ge 3 ] && add_scope \"examples\" \"examples\" \"$count\"\n }\n\n [ -d \"testutil\" ] && {\n count=$(count_source_files \"testutil\" \"*.go\")\n [ \"$count\" -ge 3 ] && add_scope \"testutil\" \"testing\" \"$count\"\n }\n\n [ -d \"docs\" ] && {\n count=$(find docs -type f \\( -name \"*.md\" -o -name \"*.rst\" \\) | wc -l)\n [ \"$count\" -ge 3 ] && add_scope \"docs\" \"documentation\" \"$count\"\n }\n ;;\n\n \"php\")\n # Determine PHP backend type based on framework\n FRAMEWORK=$(echo \"$PROJECT_INFO\" | jq -r '.framework')\n PROJECT_TYPE=$(echo \"$PROJECT_INFO\" | jq -r '.type')\n\n # Select appropriate backend template\n # Differentiate between extension/bundle (standalone package) vs project (full installation)\n if [ \"$PROJECT_TYPE\" = \"php-typo3-extension\" ]; then\n PHP_BACKEND_TYPE=\"typo3-extension\"\n elif [ \"$PROJECT_TYPE\" = \"php-typo3\" ]; then\n PHP_BACKEND_TYPE=\"typo3-project\"\n elif [ \"$PROJECT_TYPE\" = \"php-oro-bundle\" ]; then\n PHP_BACKEND_TYPE=\"oro-bundle\"\n elif [ \"$PROJECT_TYPE\" = \"php-oro\" ]; then\n PHP_BACKEND_TYPE=\"oro-project\"\n elif [ \"$FRAMEWORK\" = \"symfony\" ] || [ \"$PROJECT_TYPE\" = \"php-symfony\" ]; then\n PHP_BACKEND_TYPE=\"symfony\"\n else\n PHP_BACKEND_TYPE=\"backend-php\"\n fi\n\n # Check common PHP directories\n [ -d \"Classes\" ] && {\n count=$(count_source_files \"Classes\" \"*.php\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"Classes\" \"$PHP_BACKEND_TYPE\" \"$count\"\n }\n\n [ -d \"src\" ] && {\n count=$(count_source_files \"src\" \"*.php\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"src\" \"$PHP_BACKEND_TYPE\" \"$count\"\n }\n\n [ -d \"Tests\" ] && {\n count=$(count_source_files \"Tests\" \"*.php\")\n if [ \"$count\" -ge 3 ]; then\n # Use TYPO3-specific testing template for TYPO3 extensions\n if [ \"$PROJECT_TYPE\" = \"php-typo3-extension\" ]; then\n add_scope \"Tests\" \"typo3-testing\" \"$count\"\n else\n add_scope \"Tests\" \"testing\" \"$count\"\n fi\n fi\n }\n\n [ -d \"tests\" ] && {\n count=$(count_source_files \"tests\" \"*.php\")\n [ \"$count\" -ge 3 ] && add_scope \"tests\" \"testing\" \"$count\"\n }\n\n [ -d \"Documentation\" ] && {\n count=$(find Documentation -type f \\( -name \"*.rst\" -o -name \"*.md\" \\) | wc -l)\n if [ \"$count\" -ge 3 ]; then\n # Use TYPO3-specific docs template for TYPO3 extensions\n if [ \"$PROJECT_TYPE\" = \"php-typo3-extension\" ]; then\n add_scope \"Documentation\" \"typo3-docs\" \"$count\"\n else\n add_scope \"Documentation\" \"documentation\" \"$count\"\n fi\n fi\n }\n\n [ -d \"Resources\" ] && {\n count=$(find Resources -type f | wc -l)\n [ \"$count\" -ge 5 ] && add_scope \"Resources\" \"resources\" \"$count\"\n }\n\n # Oro-specific: check for Bundle directories within projects\n if [ \"$PROJECT_TYPE\" = \"php-oro\" ]; then\n for bundle_dir in src/*/Bundle/*/; do\n [ -d \"$bundle_dir\" ] && {\n count=$(count_source_files \"$bundle_dir\" \"*.php\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"${bundle_dir%/}\" \"oro-bundle\" \"$count\"\n }\n done\n fi\n ;;\n\n \"typescript\")\n # Check common TypeScript/JavaScript directories\n [ -d \"src\" ] && {\n count=$(count_source_files \"src\" \"*.ts\")\n ts_count=$count\n count=$(count_source_files \"src\" \"*.tsx\")\n tsx_count=$count\n\n if [ \"$tsx_count\" -ge \"$MIN_FILES\" ]; then\n add_scope \"src\" \"frontend-typescript\" \"$tsx_count\"\n elif [ \"$ts_count\" -ge \"$MIN_FILES\" ]; then\n add_scope \"src\" \"backend-typescript\" \"$ts_count\"\n fi\n }\n\n [ -d \"components\" ] && {\n count=$(count_source_files \"components\" \"*.tsx\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"components\" \"frontend-typescript\" \"$count\"\n }\n\n [ -d \"pages\" ] && {\n count=$(count_source_files \"pages\" \"*.tsx\")\n [ \"$count\" -ge 3 ] && add_scope \"pages\" \"frontend-typescript\" \"$count\"\n }\n\n [ -d \"app\" ] && {\n count=$(count_source_files \"app\" \"*.tsx\")\n [ \"$count\" -ge 3 ] && add_scope \"app\" \"frontend-typescript\" \"$count\"\n }\n\n if [ -d \"server\" ] || [ -d \"backend\" ]; then\n dir=$([ -d \"server\" ] && echo \"server\" || echo \"backend\")\n count=$(count_source_files \"$dir\" \"*.ts\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"$dir\" \"backend-typescript\" \"$count\"\n fi\n\n if [ -d \"__tests__\" ] || [ -d \"tests\" ]; then\n dir=$([ -d \"__tests__\" ] && echo \"__tests__\" || echo \"tests\")\n count=$(count_source_files \"$dir\" \"*.test.ts\")\n [ \"$count\" -ge 3 ] && add_scope \"$dir\" \"testing\" \"$count\"\n fi\n ;;\n\n \"python\")\n # Determine Python template: use python-modern if pyproject.toml with ruff/mypy\n PYTHON_TEMPLATE=\"backend-python\"\n if [ -f \"pyproject.toml\" ]; then\n if grep -q 'ruff' pyproject.toml 2>/dev/null || grep -q 'mypy' pyproject.toml 2>/dev/null; then\n PYTHON_TEMPLATE=\"python-modern\"\n fi\n fi\n\n # Check common Python directories\n [ -d \"src\" ] && {\n count=$(count_source_files \"src\" \"*.py\")\n [ \"$count\" -ge \"$MIN_FILES\" ] && add_scope \"src\" \"$PYTHON_TEMPLATE\" \"$count\"\n }\n\n [ -d \"tests\" ] && {\n count=$(count_source_files \"tests\" \"*.py\")\n [ \"$count\" -ge 3 ] && add_scope \"tests\" \"testing\" \"$count\"\n }\n\n [ -d \"scripts\" ] && {\n count=$(count_source_files \"scripts\" \"*.py\")\n [ \"$count\" -ge 3 ] && add_scope \"scripts\" \"cli\" \"$count\"\n }\n\n [ -d \"docs\" ] && {\n count=$(find docs -type f \\( -name \"*.md\" -o -name \"*.rst\" \\) | wc -l)\n [ \"$count\" -ge 3 ] && add_scope \"docs\" \"documentation\" \"$count\"\n }\n ;;\nesac\n\n# Check for web subdirectories (cross-language)\n# IMPORTANT: Only create frontend-typescript scopes if Node toolchain exists\n# Either: package.json in scope dir, package.json at root, or lockfile at root\nhas_node_toolchain() {\n local scope_dir=\"$1\"\n [ -f \"$scope_dir/package.json\" ] || \\\n [ -f \"package.json\" ] || \\\n [ -f \"pnpm-lock.yaml\" ] || \\\n [ -f \"yarn.lock\" ] || \\\n [ -f \"bun.lockb\" ] || \\\n [ -f \"package-lock.json\" ]\n}\n\nfor web_dir in internal/web web frontend client ui; do\n if [ -d \"$web_dir\" ]; then\n count=$(find \"$web_dir\" -type f \\( -name \"*.ts\" -o -name \"*.tsx\" -o -name \"*.js\" -o -name \"*.jsx\" \\) 2>/dev/null | wc -l)\n if [ \"$count\" -ge \"$MIN_FILES\" ] && has_node_toolchain \"$web_dir\"; then\n add_scope \"$web_dir\" \"frontend-typescript\" \"$count\"\n fi\n fi\ndone\n\n# Check for Claude Code skills (each skill gets its own scope)\nif [ -d \"skills\" ]; then\n for skill_dir in skills/*/; do\n if [ -f \"${skill_dir}SKILL.md\" ]; then\n # Count files in skill (sh, md, yaml)\n count=$(find \"$skill_dir\" -type f \\( -name \"*.sh\" -o -name \"*.md\" -o -name \"*.yaml\" -o -name \"*.yml\" \\) 2>/dev/null | wc -l)\n add_scope \"${skill_dir%/}\" \"claude-code-skill\" \"$count\"\n fi\n done\nfi\n\n# Check for DDEV local development environment (cross-language)\nif [ -d \".ddev\" ]; then\n count=$(find .ddev -type f \\( -name \"*.yaml\" -o -name \"*.yml\" \\) 2>/dev/null | wc -l)\n [ \"$count\" -ge 1 ] && add_scope \".ddev\" \"ddev\" \"$count\"\nfi\n\n# Check for Docker/container directories (cross-language)\n# These get \"docker\" scope type for container-focused documentation\nfor docker_dir in docker deploy .docker infrastructure infra; do\n if [ -d \"$docker_dir\" ]; then\n # Count Docker-related files\n count=$(find \"$docker_dir\" -type f \\( -name \"Dockerfile*\" -o -name \"*.dockerfile\" -o -name \"docker-compose*.yml\" -o -name \"compose*.yml\" -o -name \"*.yaml\" -o -name \"*.sh\" \\) 2>/dev/null | wc -l)\n [ \"$count\" -ge 2 ] && add_scope \"$docker_dir\" \"docker\" \"$count\"\n fi\ndone\n\n# Check for CI/CD configurations (cross-language)\n# GitHub Actions\nif [ -d \".github/workflows\" ]; then\n count=$(find .github/workflows -type f -name \"*.yml\" -o -name \"*.yaml\" 2>/dev/null | wc -l)\n [ \"$count\" -ge 1 ] && add_scope \".github/workflows\" \"github-actions\" \"$count\"\nfi\n\n# GitLab CI - use .gitlab directory if exists, otherwise skip (root AGENTS.md covers it)\nif [ -f \".gitlab-ci.yml\" ]; then\n if [ -d \".gitlab\" ]; then\n count=$(find .gitlab -type f -name \"*.yml\" 2>/dev/null | wc -l)\n [ \"$count\" -ge 1 ] && add_scope \".gitlab\" \"gitlab-ci\" \"$count\"\n fi\n # Note: If no .gitlab/ directory, the root AGENTS.md will mention gitlab-ci.yml\nfi\n\n# Concourse CI\nconcourse_detected=false\nconcourse_dir=\"\"\nfor concourse_pattern in \"ci/pipeline.yml\" \"ci/pipeline.yaml\" \"concourse/pipeline.yml\"; do\n if [ -f \"$concourse_pattern\" ]; then\n concourse_dir=$(dirname \"$concourse_pattern\")\n concourse_detected=true\n break\n fi\ndone\n# Check for pipeline.yml at root - only create scope if ci/ directory exists\nif [ \"$concourse_detected\" = false ]; then\n if [ -f \"pipeline.yml\" ] || [ -f \"pipeline.yaml\" ]; then\n if [ -d \"ci\" ]; then\n concourse_dir=\"ci\"\n concourse_detected=true\n fi\n # If no ci/ directory, root AGENTS.md will cover the pipeline file\n fi\nfi\n# Also check for *-pipeline.yml at root - only create scope if ci/ directory exists\nif [ \"$concourse_detected\" = false ]; then\n pipeline_files=$(find . -maxdepth 1 -name \"*-pipeline.yml\" -o -name \"*-pipeline.yaml\" 2>/dev/null | wc -l)\n if [ \"$pipeline_files\" -gt 0 ] && [ -d \"ci\" ]; then\n concourse_dir=\"ci\"\n concourse_detected=true\n fi\nfi\nif [ \"$concourse_detected\" = true ] && [ -n \"$concourse_dir\" ] && [ -d \"$concourse_dir\" ]; then\n count=$(find \"$concourse_dir\" -type f \\( -name \"*.yml\" -o -name \"*.yaml\" -o -name \"*.sh\" \\) 2>/dev/null | wc -l)\n [ \"$count\" -ge 1 ] && add_scope \"$concourse_dir\" \"concourse\" \"$count\"\nfi\n\n# Output JSON\n# A directory that already carries an AGENTS.md IS a scope, whatever the\n# language heuristics above concluded. Detection knows a fixed set of\n# directory names; a project may reasonably keep a scoped file somewhere else\n# (TYPO3 `Configuration/`, for one). Leaving such a file out of the root index\n# means an agent reading the root never learns it exists — the precedence rule\n# the index exists to serve silently stops working, and nothing reports it.\nwhile IFS= read -r existing; do\n [ -n \"$existing\" ] || continue\n dir=\"${existing%/AGENTS.md}\"\n dir=\"${dir#./}\"\n case \"$dir\" in\n \"\" | \".\" | \"AGENTS.md\") continue ;; # the root file itself\n esac\n # `${scopes[@]}` on an empty array counts as unset under `set -u` before\n # bash 4.4, and this loop is reached with nothing detected yet whenever a\n # project's only scoped file sits in an unrecognised directory.\n for known in ${scopes[@]+\"${scopes[@]}\"}; do\n [ \"$(printf '%s' \"$known\" | jq -r '.path')\" = \"$dir\" ] && continue 2\n done\n count=$(find \"$dir\" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')\n add_scope \"$dir\" \"existing\" \"$count\"\ndone \u003c \u003c(find . -name AGENTS.md -not -path './.git/*' -not -path './node_modules/*' \\\n -not -path './vendor/*' -not -path './.Build/*' 2>/dev/null | sort)\n\nif [ ${#scopes[@]} -eq 0 ]; then\n echo '{\"scopes\": []}'\nelse\n # Join scopes with commas via explicit loop — avoids IFS= entirely\n # (the opengrep bash.lang.security.ifs-tampering rule flags any\n # IFS= assignment, even inside subshells).\n joined=\"${scopes[0]}\"\n for ((i=1; i\u003c${#scopes[@]}; i++)); do\n joined+=\",${scopes[i]}\"\n done\n echo \"{\\\"scopes\\\": [$joined]}\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":13747,"content_sha256":"43f2fe6a574b55fe071524507aa2857e4c407b603df9496ac2442b09fb2c30cd"},{"filename":"scripts/detect-utilities.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Detect utility files and libraries to prevent reinvention\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Check if git is available\nif ! project_git rev-parse --git-dir > /dev/null 2>&1; then\n echo \"\"\n exit 0\nfi\n\n# Get project language\nPROJECT_INFO=$(bash \"$(dirname \"$0\")/detect-project.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{\"language\":\"unknown\"}')\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\n\n# Common utility directory names\nUTIL_DIRS=(\"utils\" \"util\" \"helpers\" \"helper\" \"lib\" \"shared\" \"common\" \"pkg\")\n\n# Find utility directories\nfind_util_dirs() {\n for dir in \"${UTIL_DIRS[@]}\"; do\n [ -d \"$dir\" ] && echo \"$dir\"\n [ -d \"src/$dir\" ] && echo \"src/$dir\"\n [ -d \"internal/$dir\" ] && echo \"internal/$dir\"\n done\n}\n\n# Extract exports from TypeScript index file\nextract_ts_exports() {\n local file=\"$1\"\n grep -oE \"export \\{ [^}]+ \\}\" \"$file\" 2>/dev/null | \\\n sed 's/export { //; s/ }//; s/,/\\n/g' | \\\n tr -d ' ' | head -10\n}\n\n# Extract function names from Go file\nextract_go_exports() {\n local dir=\"$1\"\n find \"$dir\" -name \"*.go\" -type f 2>/dev/null | while read -r file; do\n grep -oE \"^func [A-Z][a-zA-Z0-9_]*\" \"$file\" 2>/dev/null | \\\n sed 's/func //' | head -5\n done | sort -u | head -10\n}\n\n# Extract function names from Python file\nextract_py_exports() {\n local dir=\"$1\"\n find \"$dir\" -name \"*.py\" ! -name \"__*\" -type f 2>/dev/null | while read -r file; do\n grep -oE \"^def [a-z_][a-z0-9_]*\" \"$file\" 2>/dev/null | \\\n sed 's/def //' | head -5\n done | sort -u | head -10\n}\n\n# Infer utility purpose from name\ninfer_purpose() {\n local name=\"$1\"\n case \"$name\" in\n *date*|*time*|*format*Date*) echo \"date/time formatting\" ;;\n *http*|*fetch*|*api*|*client*) echo \"HTTP requests\" ;;\n *log*) echo \"logging\" ;;\n *valid*|*check*) echo \"validation\" ;;\n *parse*) echo \"parsing\" ;;\n *format*) echo \"formatting\" ;;\n *error*) echo \"error handling\" ;;\n *auth*|*token*) echo \"authentication\" ;;\n *cache*) echo \"caching\" ;;\n *config*) echo \"configuration\" ;;\n *string*|*str*) echo \"string manipulation\" ;;\n *file*|*path*) echo \"file operations\" ;;\n *) echo \"utility\" ;;\n esac\n}\n\n# Generate output\noutput=\"\"\n\nwhile IFS= read -r dir; do\n [ -z \"$dir\" ] && continue\n\n case \"$LANGUAGE\" in\n \"typescript\")\n # Check for index.ts barrel export\n if [ -f \"$dir/index.ts\" ]; then\n while IFS= read -r export; do\n [ -z \"$export\" ] && continue\n purpose=$(infer_purpose \"$export\")\n output=\"$output| $purpose | \\`$export\\` | \\`$dir/\\` |\\n\"\n done \u003c \u003c(extract_ts_exports \"$dir/index.ts\")\n fi\n ;;\n\n \"go\")\n while IFS= read -r func; do\n [ -z \"$func\" ] && continue\n purpose=$(infer_purpose \"$func\")\n output=\"$output| $purpose | \\`$func\\` | \\`$dir/\\` |\\n\"\n done \u003c \u003c(extract_go_exports \"$dir\")\n ;;\n\n \"python\")\n while IFS= read -r func; do\n [ -z \"$func\" ] && continue\n purpose=$(infer_purpose \"$func\")\n output=\"$output| $purpose | \\`$func\\` | \\`$dir/\\` |\\n\"\n done \u003c \u003c(extract_py_exports \"$dir\")\n ;;\n\n \"php\")\n # List PHP utility classes\n while read -r file; do\n class=$(grep -oE \"class [A-Z][a-zA-Z0-9_]+\" \"$file\" 2>/dev/null | head -1 | sed 's/class //')\n [ -n \"$class\" ] && {\n purpose=$(infer_purpose \"$class\")\n relpath=\"${file#./}\"\n output=\"$output| $purpose | \\`$class\\` | \\`$relpath\\` |\\n\"\n }\n done \u003c \u003c(find \"$dir\" -name \"*.php\" -type f 2>/dev/null | head -20)\n ;;\n esac\ndone \u003c \u003c(find_util_dirs)\n\n# Check for common utility files at root or src\nfor util_file in \"utils.ts\" \"utils.js\" \"helpers.ts\" \"helpers.js\" \"utils.py\" \"helpers.py\"; do\n for prefix in \"\" \"src/\"; do\n file=\"${prefix}${util_file}\"\n [ -f \"$file\" ] && output=\"$output| utilities | \\`$file\\` | \\`$file\\` |\\n\"\n done\ndone\n\n# Output (remove empty lines, duplicates)\necho -e \"$output\" | sed '/^$/d' | sort -u | head -15\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4687,"content_sha256":"2314c4adc86ffbf68dde019019bdceda41825609f6eb5f7903da16f545f859a2"},{"filename":"scripts/extract-adrs.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract Architectural Decision Records (ADRs) from common locations\n# Returns JSON with ADR metadata for AGENTS.md generation\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Common ADR directory locations\nADR_DIRS=(\n \"docs/adr\" \"docs/adrs\" \"docs/decisions\" \"docs/architecture/decisions\"\n \"Documentation/ADR\" \"Documentation/Decisions\"\n \"adr\" \"ADR\"\n)\n\n# Find the first existing ADR directory\nADR_DIR=\"\"\nfor dir in \"${ADR_DIRS[@]}\"; do\n if [ -d \"$dir\" ]; then\n ADR_DIR=\"$dir\"\n break\n fi\ndone\n\n# If no ADR directory found, check for standalone ADR files in docs/\nif [ -z \"$ADR_DIR\" ]; then\n # Look for ADR-named files anywhere in docs/\n if [ -d \"docs\" ]; then\n adr_file=$(find docs -maxdepth 2 -type f \\( -name \"ADR-*.md\" -o -name \"adr-*.md\" -o -name \"*-adr-*.md\" -o -name \"ADR*.rst\" \\) 2>/dev/null | head -1)\n if [ -n \"$adr_file\" ]; then\n ADR_DIR=$(dirname \"$adr_file\")\n fi\n fi\nfi\n\n# No ADRs found\nif [ -z \"$ADR_DIR\" ]; then\n jq -n '{adr_count: 0, adr_directory: null, adrs: []}'\n exit 0\nfi\n\n# Collect ADR files\nADR_FILES=()\nwhile IFS= read -r -d '' file; do\n ADR_FILES+=(\"$file\")\ndone \u003c \u003c(find \"$ADR_DIR\" -maxdepth 1 -type f \\( -name \"ADR-*.md\" -o -name \"adr-*.md\" -o -name \"*-adr-*.md\" -o -name \"ADR*.rst\" -o -name \"adr*.rst\" -o -name \"*.md\" \\) -print0 2>/dev/null | sort -z)\n\n# Deduplicate and filter — only include files that look like ADRs\nFILTERED_FILES=()\nfor file in \"${ADR_FILES[@]}\"; do\n basename=$(basename \"$file\")\n # Accept files with ADR in the name, or numbered files (common ADR pattern like 0001-*.md)\n if [[ \"$basename\" =~ ^[Aa][Dd][Rr][-_] ]] || [[ \"$basename\" =~ ^[0-9]{3,4}[-_] ]] || [[ \"$basename\" =~ -[Aa][Dd][Rr][-_] ]]; then\n FILTERED_FILES+=(\"$file\")\n fi\ndone\n\nif [ ${#FILTERED_FILES[@]} -eq 0 ]; then\n jq -n --arg dir \"$ADR_DIR\" '{adr_count: 0, adr_directory: $dir, adrs: []}'\n exit 0\nfi\n\n# Extract metadata from each ADR\nADRS_JSON=\"[]\"\nfor file in \"${FILTERED_FILES[@]}\"; do\n filename=$(basename \"$file\")\n\n # Read file content\n content=$(cat \"$file\" 2>/dev/null) || continue\n [ -z \"$content\" ] && continue\n\n # Extract title: first heading (# or ##)\n title=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^#{1,2}[[:space:]]+(.*) ]]; then\n title=\"${BASH_REMATCH[1]}\"\n # Strip leading ADR number prefix like \"ADR-001:\" or \"ADR 1:\"\n title=$(echo \"$title\" | sed -E 's/^ADR[-_ ]?[0-9]+:?\\s*//i')\n break\n fi\n done \u003c\u003c\u003c \"$content\"\n [ -z \"$title\" ] && title=\"$filename\"\n\n # Extract status: look for \"Status:\" line or **Status** pattern\n status=\"Unknown\"\n while IFS= read -r line; do\n # Match patterns like \"## Status\", then read next non-empty line\n if [[ \"$line\" =~ ^#{1,3}[[:space:]]+[Ss]tatus ]]; then\n # Read lines after the status heading to find the actual status\n found_heading=false\n while IFS= read -r sline; do\n if [ \"$found_heading\" = false ]; then\n found_heading=true\n continue\n fi\n # Skip empty lines\n [[ -z \"${sline// /}\" ]] && continue\n # Extract the status value\n # shellcheck disable=SC2016 # sed script: the backticks and\n # asterisks are literal regex, not shell expansions.\n sline=$(echo \"$sline\" | sed -E 's/^\\*\\*//;s/\\*\\*.*$//;s/^- //;s/^`//;s/`$//')\n if [[ \"$sline\" =~ ^(Accepted|Proposed|Deprecated|Superseded|Rejected|Draft|Approved) ]]; then\n status=\"${BASH_REMATCH[1]}\"\n fi\n break\n done\n break\n fi\n # Match inline patterns: \"**Status**: Accepted\", \"**Status:** Accepted\", \"Status: Accepted\"\n if [[ \"$line\" =~ \\*?\\*?[Ss]tatus\\*?\\*?:?[[:space:]]*(Accepted|Proposed|Deprecated|Superseded|Rejected|Draft|Approved) ]]; then\n status=\"${BASH_REMATCH[1]}\"\n break\n fi\n done \u003c\u003c\u003c \"$content\"\n\n # Extract summary: first paragraph after Context heading, or first paragraph after title\n summary=\"\"\n # Try \"Context\" section first\n in_context=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^#{1,3}[[:space:]]+(Context|Background) ]]; then\n in_context=true\n continue\n fi\n if [ \"$in_context\" = true ]; then\n [[ -z \"${line// /}\" ]] && continue\n # Stop at next heading\n [[ \"$line\" =~ ^# ]] && break\n # Take the first non-empty, non-heading line\n summary=$(echo \"$line\" | sed -E 's/^\\*\\*//;s/\\*\\*$//;s/^- //')\n # Truncate to ~120 chars\n if [ ${#summary} -gt 120 ]; then\n summary=\"${summary:0:117}...\"\n fi\n break\n fi\n done \u003c\u003c\u003c \"$content\"\n\n # Fallback: first paragraph after title\n if [ -z \"$summary\" ]; then\n past_title=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^# ]] && [ \"$past_title\" = false ]; then\n past_title=true\n continue\n fi\n if [ \"$past_title\" = true ]; then\n [[ -z \"${line// /}\" ]] && continue\n [[ \"$line\" =~ ^# ]] && break\n [[ \"$line\" =~ ^[*-][[:space:]] ]] && continue\n summary=$(echo \"$line\" | sed -E 's/^\\*\\*//;s/\\*\\*$//;s/^- //')\n if [ ${#summary} -gt 120 ]; then\n summary=\"${summary:0:117}...\"\n fi\n break\n fi\n done \u003c\u003c\u003c \"$content\"\n fi\n\n # Extract decision: look for \"Decision\" section\n decision=\"\"\n in_decision=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^#{1,3}[[:space:]]+(Decision|Resolution) ]]; then\n in_decision=true\n continue\n fi\n if [ \"$in_decision\" = true ]; then\n [[ -z \"${line// /}\" ]] && continue\n [[ \"$line\" =~ ^# ]] && break\n [[ \"$line\" =~ ^\\`\\`\\` ]] && break\n decision=$(echo \"$line\" | sed -E 's/^\\*\\*//;s/\\*\\*$//;s/^- //')\n if [ ${#decision} -gt 150 ]; then\n decision=\"${decision:0:147}...\"\n fi\n break\n fi\n done \u003c\u003c\u003c \"$content\"\n\n # Build JSON entry\n ADRS_JSON=$(echo \"$ADRS_JSON\" | jq \\\n --arg file \"$filename\" \\\n --arg title \"$title\" \\\n --arg status \"$status\" \\\n --arg summary \"$summary\" \\\n --arg decision \"$decision\" \\\n '. + [{file: $file, title: $title, status: $status, summary: $summary, decision: $decision}]')\ndone\n\n# Output final JSON\nADR_COUNT=$(echo \"$ADRS_JSON\" | jq 'length')\njq -n \\\n --argjson count \"$ADR_COUNT\" \\\n --arg dir \"$ADR_DIR\" \\\n --argjson adrs \"$ADRS_JSON\" \\\n '{adr_count: $count, adr_directory: $dir, adrs: $adrs}'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7004,"content_sha256":"9489096253b0b0fb97b863bbd4f46984e408b8e46f4b0ad3d4a73846968cb24b"},{"filename":"scripts/extract-agent-configs.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract information from AI coding agent configuration files\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Extract content from markdown-based instructions\nextract_md_instructions() {\n local file=\"$1\"\n local max_lines=\"${2:-50}\"\n\n if [ ! -f \"$file\" ]; then\n echo \"\"\n return\n fi\n\n # Get content, skip frontmatter if present\n awk '\n BEGIN { in_frontmatter=0; started=0 }\n /^---$/ && !started { in_frontmatter=!in_frontmatter; next }\n !in_frontmatter { started=1; print }\n ' \"$file\" | head -\"$max_lines\"\n}\n\n# Extract rules from various formats\nextract_rules() {\n local file=\"$1\"\n local rules=()\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Extract bullet points or lines that look like rules\n while IFS= read -r line; do\n # Clean up the line\n line=$(echo \"$line\" | sed 's/^[[:space:]]*[-*][[:space:]]*//' | sed 's/[[:space:]]*$//')\n # Keep lines that look like rules (not empty, reasonable length)\n if [ -n \"$line\" ] && [ ${#line} -gt 5 ] && [ ${#line} -lt 500 ]; then\n rules+=(\"$line\")\n fi\n done \u003c \u003c(grep -E '^[[:space:]]*[-*][[:space:]]' \"$file\" 2>/dev/null | head -30 || true)\n\n if [ ${#rules[@]} -eq 0 ]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${rules[@]}\" | jq -R . | jq -s .\n fi\n}\n\n# Parse Cursor settings/rules\nparse_cursor() {\n local cursor_dir=\".cursor\"\n\n if [ ! -d \"$cursor_dir\" ]; then\n echo \"{}\"\n return\n fi\n\n local rules_file=\"\"\n local rules_content=\"[]\"\n local settings_file=\"\"\n local model=\"\"\n # Note: context_files would be used for extracting @file references but not yet implemented\n\n # Check for rules files\n for f in \"$cursor_dir/rules\" \"$cursor_dir/rules.md\" \"$cursor_dir/.cursorrules\"; do\n if [ -f \"$f\" ]; then\n rules_file=\"$f\"\n rules_content=$(extract_rules \"$f\")\n break\n fi\n done\n\n # Also check root .cursorrules\n if [ -z \"$rules_file\" ] && [ -f \".cursorrules\" ]; then\n rules_file=\".cursorrules\"\n rules_content=$(extract_rules \".cursorrules\")\n fi\n\n # Check for settings\n if [ -f \"$cursor_dir/settings.json\" ]; then\n settings_file=\"$cursor_dir/settings.json\"\n model=$(jq -r '.model // \"\"' \"$settings_file\" 2>/dev/null || echo \"\")\n fi\n\n jq -n \\\n --arg rules_file \"$rules_file\" \\\n --argjson rules \"$rules_content\" \\\n --arg settings_file \"$settings_file\" \\\n --arg model \"$model\" \\\n '{\n rules_file: $rules_file,\n rules: $rules,\n settings_file: $settings_file,\n model: $model\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse Claude Code config\nparse_claude() {\n local claude_dir=\".claude\"\n local instructions_file=\"\"\n local instructions=\"\"\n local settings_file=\"\"\n local model=\"\"\n\n # Check for CLAUDE.md in various locations\n for f in \"CLAUDE.md\" \"$claude_dir/CLAUDE.md\" \"$claude_dir/settings/CLAUDE.md\"; do\n if [ -f \"$f\" ]; then\n instructions_file=\"$f\"\n instructions=$(extract_md_instructions \"$f\" 30)\n break\n fi\n done\n\n # Check for settings.json\n if [ -f \"$claude_dir/settings.json\" ]; then\n settings_file=\"$claude_dir/settings.json\"\n model=$(jq -r '.model // \"\"' \"$settings_file\" 2>/dev/null || echo \"\")\n fi\n\n # Check for .claude.json in root\n if [ -z \"$settings_file\" ] && [ -f \".claude.json\" ]; then\n settings_file=\".claude.json\"\n model=$(jq -r '.model // \"\"' \"$settings_file\" 2>/dev/null || echo \"\")\n fi\n\n jq -n \\\n --arg instructions_file \"$instructions_file\" \\\n --arg instructions \"$instructions\" \\\n --arg settings_file \"$settings_file\" \\\n --arg model \"$model\" \\\n '{\n instructions_file: $instructions_file,\n instructions_preview: $instructions,\n settings_file: $settings_file,\n model: $model\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse GitHub Copilot config\nparse_copilot() {\n local instructions_file=\"\"\n local instructions=\"\"\n\n # Check for copilot instructions\n for f in \".github/copilot-instructions.md\" \"copilot-instructions.md\"; do\n if [ -f \"$f\" ]; then\n instructions_file=\"$f\"\n instructions=$(extract_md_instructions \"$f\" 30)\n break\n fi\n done\n\n jq -n \\\n --arg instructions_file \"$instructions_file\" \\\n --arg instructions \"$instructions\" \\\n '{\n instructions_file: $instructions_file,\n instructions_preview: $instructions\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse Windsurf config\nparse_windsurf() {\n local windsurf_dir=\".windsurf\"\n\n if [ ! -d \"$windsurf_dir\" ]; then\n echo \"{}\"\n return\n fi\n\n local rules_file=\"\"\n local rules_content=\"[]\"\n\n # Check for rules/instructions\n for f in \"$windsurf_dir/rules.md\" \"$windsurf_dir/instructions.md\" \"$windsurf_dir/.windsurfrules\"; do\n if [ -f \"$f\" ]; then\n rules_file=\"$f\"\n rules_content=$(extract_rules \"$f\")\n break\n fi\n done\n\n jq -n \\\n --arg rules_file \"$rules_file\" \\\n --argjson rules \"$rules_content\" \\\n '{\n rules_file: $rules_file,\n rules: $rules\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse Aider config\nparse_aider() {\n local config_file=\"\"\n local model=\"\"\n local conventions=\"\"\n\n # Check for aider config files\n for f in \".aider.conf.yml\" \".aider.conf.yaml\" \".aider/config.yml\"; do\n if [ -f \"$f\" ]; then\n config_file=\"$f\"\n # Extract model if present\n model=$(grep -E '^model:' \"$f\" 2>/dev/null | sed 's/model:[[:space:]]*//' | head -1 || echo \"\")\n break\n fi\n done\n\n # Check for conventions file\n if [ -f \".aider/CONVENTIONS.md\" ]; then\n conventions=\".aider/CONVENTIONS.md\"\n fi\n\n jq -n \\\n --arg config_file \"$config_file\" \\\n --arg model \"$model\" \\\n --arg conventions \"$conventions\" \\\n '{\n config_file: $config_file,\n model: $model,\n conventions_file: $conventions\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse Continue config\nparse_continue() {\n local continue_dir=\".continue\"\n\n if [ ! -d \"$continue_dir\" ]; then\n echo \"{}\"\n return\n fi\n\n local config_file=\"\"\n local models=\"[]\"\n\n # Check for config files\n for f in \"$continue_dir/config.json\" \"$continue_dir/config.yaml\"; do\n if [ -f \"$f\" ]; then\n config_file=\"$f\"\n if [[ \"$f\" == *.json ]]; then\n models=$(jq '[.models[]? | .title // .model] // []' \"$f\" 2>/dev/null || echo \"[]\")\n fi\n break\n fi\n done\n\n jq -n \\\n --arg config_file \"$config_file\" \\\n --argjson models \"$models\" \\\n '{\n config_file: $config_file,\n configured_models: $models\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse Cody (Sourcegraph) config\nparse_cody() {\n local cody_file=\"\"\n local model=\"\"\n\n # Check for cody config\n for f in \".sourcegraph/cody.json\" \".cody/config.json\"; do\n if [ -f \"$f\" ]; then\n cody_file=\"$f\"\n model=$(jq -r '.model // \"\"' \"$f\" 2>/dev/null || echo \"\")\n break\n fi\n done\n\n jq -n \\\n --arg config_file \"$cody_file\" \\\n --arg model \"$model\" \\\n '{\n config_file: $config_file,\n model: $model\n } | with_entries(select(.value != \"\"))'\n}\n\n# Detect which agents are configured\nAGENTS=()\n{ [ -d \".cursor\" ] || [ -f \".cursorrules\" ]; } && AGENTS+=(\"cursor\")\n{ [ -d \".claude\" ] || [ -f \"CLAUDE.md\" ]; } && AGENTS+=(\"claude\")\n{ [ -f \".github/copilot-instructions.md\" ] || [ -f \"copilot-instructions.md\" ]; } && AGENTS+=(\"copilot\")\n[ -d \".windsurf\" ] && AGENTS+=(\"windsurf\")\n{ [ -d \".aider\" ] || [ -f \".aider.conf.yml\" ] || [ -f \".aider.conf.yaml\" ]; } && AGENTS+=(\"aider\")\n[ -d \".continue\" ] && AGENTS+=(\"continue\")\n{ [ -d \".codeium\" ]; } && AGENTS+=(\"codeium\")\n{ [ -d \".tabnine\" ]; } && AGENTS+=(\"tabnine\")\n{ [ -d \".sourcegraph\" ] || [ -f \".sourcegraph/cody.json\" ]; } && AGENTS+=(\"cody\")\n\n# Build agents list JSON\nif [ ${#AGENTS[@]} -eq 0 ]; then\n AGENTS_JSON=\"[]\"\nelse\n AGENTS_JSON=$(printf '%s\\n' \"${AGENTS[@]}\" | jq -R . | jq -s .)\nfi\n\n# Extract configs for detected agents\nCURSOR_CONFIG=$(parse_cursor)\nCLAUDE_CONFIG=$(parse_claude)\nCOPILOT_CONFIG=$(parse_copilot)\nWINDSURF_CONFIG=$(parse_windsurf)\nAIDER_CONFIG=$(parse_aider)\nCONTINUE_CONFIG=$(parse_continue)\nCODY_CONFIG=$(parse_cody)\n\n# Build final JSON output\njq -n \\\n --argjson detected \"$AGENTS_JSON\" \\\n --argjson cursor \"$CURSOR_CONFIG\" \\\n --argjson claude \"$CLAUDE_CONFIG\" \\\n --argjson copilot \"$COPILOT_CONFIG\" \\\n --argjson windsurf \"$WINDSURF_CONFIG\" \\\n --argjson aider \"$AIDER_CONFIG\" \\\n --argjson continue_config \"$CONTINUE_CONFIG\" \\\n --argjson cody \"$CODY_CONFIG\" \\\n '{\n detected_agents: $detected,\n cursor: $cursor,\n claude: $claude,\n copilot: $copilot,\n windsurf: $windsurf,\n aider: $aider,\n continue: $continue_config,\n cody: $cody\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9518,"content_sha256":"7d3533722d950e39b214c5e5f8226b7deccb81b785ee3ede94bbf3fb9e53d1ac"},{"filename":"scripts/extract-architecture-rules.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract module boundary rules from architecture test frameworks\n# Supports: phpat, deptrac, Go conventions, ESLint import rules, golangci-lint depguard\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Collect results into arrays/objects, then assemble JSON at the end\nFRAMEWORK=\"\"\nRULES=\"[]\"\nINTERNAL_PACKAGES=\"[]\"\nLAYER_DEFINITIONS=\"{}\"\n\n# --- phpat (PHP Architecture Tester) ---\nparse_phpat() {\n local found=false\n\n # Find phpat test files\n local phpat_files=()\n for dir in tests/Architecture Tests/Architecture tests/architecture; do\n if [ -d \"$dir\" ]; then\n while IFS= read -r f; do\n phpat_files+=(\"$f\")\n done \u003c \u003c(find \"$dir\" -name \"*.php\" -type f 2>/dev/null)\n fi\n done\n\n # Also check phpat config files\n local phpat_config=\"\"\n for f in phpat.yaml phpat.neon phpat.yml; do\n [ -f \"$f\" ] && phpat_config=\"$f\" && break\n done\n\n if [ ${#phpat_files[@]} -eq 0 ] && [ -z \"$phpat_config\" ]; then\n return 1\n fi\n\n found=true\n FRAMEWORK=\"phpat\"\n local rules_arr=\"[]\"\n\n # Parse PHP test files for rule patterns\n for file in \"${phpat_files[@]}\"; do\n # Match shouldNotDependOn patterns\n # e.g. classesThat(Selector::haveClassName('*Controller*'))->shouldNotDependOn(Selector::haveClassName('*Repository*'))\n while IFS= read -r line; do\n local source=\"\"\n local target=\"\"\n\n # Extract source from classesThat / classesInNamespace / haveClassName\n if [[ \"$line\" =~ classesThat\\(.*haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\) ]]; then\n source=\"${BASH_REMATCH[1]}\"\n elif [[ \"$line\" =~ classesInNamespace\\([\\'\\\"](.*)[\\'\\\"]\\) ]]; then\n source=\"${BASH_REMATCH[1]}\"\n elif [[ \"$line\" =~ classesThat\\(.*classesInNamespace\\([\\'\\\"](.*)[\\'\\\"]\\) ]]; then\n source=\"${BASH_REMATCH[1]}\"\n fi\n\n # Determine rule type and target\n local rule_type=\"\"\n if [[ \"$line\" =~ shouldNotDependOn ]]; then\n rule_type=\"must_not_depend\"\n if [[ \"$line\" =~ shouldNotDependOn\\(.*haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\) ]]; then\n target=\"${BASH_REMATCH[1]}\"\n elif [[ \"$line\" =~ shouldNotDependOn\\(.*classesInNamespace\\([\\'\\\"](.*)[\\'\\\"]\\) ]]; then\n target=\"${BASH_REMATCH[1]}\"\n fi\n elif [[ \"$line\" =~ shouldOnlyDependOn ]]; then\n rule_type=\"must_only_depend\"\n if [[ \"$line\" =~ shouldOnlyDependOn\\(.*haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\) ]]; then\n target=\"${BASH_REMATCH[1]}\"\n elif [[ \"$line\" =~ shouldOnlyDependOn\\(.*classesInNamespace\\([\\'\\\"](.*)[\\'\\\"]\\) ]]; then\n target=\"${BASH_REMATCH[1]}\"\n fi\n elif [[ \"$line\" =~ mustNotDependOn ]]; then\n rule_type=\"must_not_depend\"\n if [[ \"$line\" =~ mustNotDependOn\\(.*haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\) ]]; then\n target=\"${BASH_REMATCH[1]}\"\n fi\n fi\n\n if [ -n \"$source\" ] && [ -n \"$target\" ] && [ -n \"$rule_type\" ]; then\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"$source\" --arg t \"$target\" --arg rt \"$rule_type\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": $rt}]')\n fi\n done \u003c \u003c(grep -E 'shouldNotDependOn|shouldOnlyDependOn|mustNotDependOn' \"$file\" 2>/dev/null || true)\n\n # Match canOnlyBeAccessedBy patterns\n while IFS= read -r line; do\n local target=\"\"\n local source=\"\"\n\n if [[ \"$line\" =~ haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\).*canOnlyBeAccessedBy ]]; then\n target=\"${BASH_REMATCH[1]}\"\n fi\n if [[ \"$line\" =~ canOnlyBeAccessedBy\\(.*haveClassName\\([\\'\\\"]\\*?([A-Za-z]+)\\*?[\\'\\\"]\\) ]]; then\n source=\"${BASH_REMATCH[1]}\"\n fi\n\n if [ -n \"$source\" ] && [ -n \"$target\" ]; then\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"$source\" --arg t \"$target\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": \"can_only_access\"}]')\n fi\n done \u003c \u003c(grep -E 'canOnlyBeAccessedBy' \"$file\" 2>/dev/null || true)\n done\n\n # Parse phpat YAML/NEON config\n if [ -n \"$phpat_config\" ]; then\n # Extract rules from YAML config (simplified parsing)\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+)[[:space:]]+should_not_depend_on[[:space:]]+(.+) ]]; then\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"${BASH_REMATCH[1]}\" --arg t \"${BASH_REMATCH[2]}\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": \"must_not_depend\"}]')\n fi\n done \u003c \"$phpat_config\"\n fi\n\n RULES=\"$rules_arr\"\n return 0\n}\n\n# --- deptrac (PHP dependency checker) ---\nparse_deptrac() {\n local config_file=\"\"\n for f in deptrac.yaml depfile.yaml deptrac.yml depfile.yml; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n [ -z \"$config_file\" ] && return 1\n\n FRAMEWORK=\"deptrac\"\n local rules_arr=\"[]\"\n local layers_obj=\"{}\"\n\n # Parse layer definitions\n local in_layers=false\n local current_layer=\"\"\n local layer_collectors=()\n while IFS= read -r line; do\n # Detect layers section\n if [[ \"$line\" =~ ^layers: ]]; then\n in_layers=true\n continue\n fi\n\n # End of layers section (next top-level key)\n if $in_layers && [[ \"$line\" =~ ^[a-z] && ! \"$line\" =~ ^[[:space:]] ]]; then\n in_layers=false\n # Save last layer\n if [ -n \"$current_layer\" ] && [ ${#layer_collectors[@]} -gt 0 ]; then\n local collectors_json\n collectors_json=$(printf '%s\\n' \"${layer_collectors[@]}\" | jq -R . | jq -s .)\n layers_obj=$(echo \"$layers_obj\" | jq --arg l \"$current_layer\" --argjson c \"$collectors_json\" '. + {($l): $c}')\n fi\n continue\n fi\n\n if $in_layers; then\n # Layer name\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then\n # Save previous layer\n if [ -n \"$current_layer\" ] && [ ${#layer_collectors[@]} -gt 0 ]; then\n local collectors_json\n collectors_json=$(printf '%s\\n' \"${layer_collectors[@]}\" | jq -R . | jq -s .)\n layers_obj=$(echo \"$layers_obj\" | jq --arg l \"$current_layer\" --argjson c \"$collectors_json\" '. + {($l): $c}')\n fi\n current_layer=$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\n layer_collectors=()\n fi\n # Collector value (directory or className)\n if [[ \"$line\" =~ value:[[:space:]]*(.+) ]]; then\n layer_collectors+=(\"$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\")\n fi\n fi\n done \u003c \"$config_file\"\n\n # Save last layer\n if [ -n \"$current_layer\" ] && [ ${#layer_collectors[@]} -gt 0 ]; then\n local collectors_json\n collectors_json=$(printf '%s\\n' \"${layer_collectors[@]}\" | jq -R . | jq -s .)\n layers_obj=$(echo \"$layers_obj\" | jq --arg l \"$current_layer\" --argjson c \"$collectors_json\" '. + {($l): $c}')\n fi\n\n LAYER_DEFINITIONS=\"$layers_obj\"\n\n # Parse ruleset (which layers can depend on which)\n local in_ruleset=false\n local current_source=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^ruleset: ]]; then\n in_ruleset=true\n continue\n fi\n\n if $in_ruleset && [[ \"$line\" =~ ^[a-z] && ! \"$line\" =~ ^[[:space:]] ]]; then\n in_ruleset=false\n continue\n fi\n\n if $in_ruleset; then\n # Source layer\n if [[ \"$line\" =~ ^[[:space:]]+([A-Za-z_]+): ]]; then\n current_source=\"${BASH_REMATCH[1]}\"\n fi\n # Allowed target\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*([A-Za-z_]+) ]] && [ -n \"$current_source\" ]; then\n local target=\"${BASH_REMATCH[1]}\"\n if [ \"$target\" != \"$current_source\" ]; then\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"$current_source\" --arg t \"$target\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": \"may_depend\"}]')\n fi\n fi\n fi\n done \u003c \"$config_file\"\n\n RULES=\"$rules_arr\"\n return 0\n}\n\n# --- Go architecture conventions ---\nparse_go_architecture() {\n # Only for Go projects\n [ ! -f \"go.mod\" ] && return 1\n\n local found_anything=false\n local internal_pkgs=\"[]\"\n local rules_arr=\"[]\"\n\n # Check for internal/ directories (Go compiler-enforced boundaries)\n if [ -d \"internal\" ]; then\n found_anything=true\n while IFS= read -r dir; do\n local rel_path=\"${dir#./}\"\n internal_pkgs=$(echo \"$internal_pkgs\" | jq --arg p \"$rel_path\" '. + [$p]')\n done \u003c \u003c(find ./internal -mindepth 1 -maxdepth 2 -type d 2>/dev/null)\n\n # If no subdirs, just list internal itself\n if [ \"$(echo \"$internal_pkgs\" | jq 'length')\" = \"0\" ]; then\n internal_pkgs='[\"internal\"]'\n fi\n fi\n\n # Check for depguard rules in golangci-lint config\n local golangci_config=\"\"\n for f in .golangci.yml .golangci.yaml; do\n [ -f \"$f\" ] && golangci_config=\"$f\" && break\n done\n\n if [ -n \"$golangci_config\" ]; then\n # Parse depguard deny rules\n local in_depguard=false\n local in_deny=false\n local current_pkg=\"\"\n local current_desc=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*depguard: ]]; then\n in_depguard=true\n continue\n fi\n\n # Exit depguard section on next top-level setting\n if $in_depguard && [[ \"$line\" =~ ^[[:space:]]{4}[a-z] && ! \"$line\" =~ ^[[:space:]]{6} && ! \"$line\" =~ deny && ! \"$line\" =~ rules && ! \"$line\" =~ main ]]; then\n in_depguard=false\n in_deny=false\n continue\n fi\n\n if $in_depguard && [[ \"$line\" =~ deny: ]]; then\n in_deny=true\n continue\n fi\n\n if $in_depguard && $in_deny; then\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*pkg:[[:space:]]*(.+) ]]; then\n current_pkg=$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\n found_anything=true\n fi\n if [[ \"$line\" =~ desc:[[:space:]]*(.+) ]]; then\n current_desc=$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\n if [ -n \"$current_pkg\" ]; then\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"*\" --arg t \"$current_pkg\" --arg rt \"must_not_depend\" --arg d \"$current_desc\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": $rt, \"reason\": $d}]')\n current_pkg=\"\"\n current_desc=\"\"\n fi\n fi\n fi\n done \u003c \"$golangci_config\"\n\n # Parse gomodguard if present\n local in_gomodguard=false\n local in_blocked=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*gomodguard: ]]; then\n in_gomodguard=true\n continue\n fi\n\n if $in_gomodguard && [[ \"$line\" =~ blocked: ]]; then\n in_blocked=true\n continue\n fi\n\n if $in_gomodguard && $in_blocked; then\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*pkg:[[:space:]]*(.+) ]]; then\n local blocked_pkg\n blocked_pkg=$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"*\" --arg t \"$blocked_pkg\" --arg rt \"must_not_depend\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": $rt}]')\n found_anything=true\n fi\n fi\n\n # Exit gomodguard on next top-level setting\n if $in_gomodguard && [[ \"$line\" =~ ^[[:space:]]{4}[a-z] && ! \"$line\" =~ blocked && ! \"$line\" =~ modules && ! \"$line\" =~ versions ]]; then\n in_gomodguard=false\n in_blocked=false\n fi\n done \u003c \"$golangci_config\"\n\n # Parse forbidigo patterns (banned function calls)\n local in_forbidigo=false\n local in_forbid=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*forbidigo: ]]; then\n in_forbidigo=true\n continue\n fi\n if $in_forbidigo && [[ \"$line\" =~ forbid: ]]; then\n in_forbid=true\n continue\n fi\n if $in_forbidigo && $in_forbid; then\n if [[ \"$line\" =~ pattern:[[:space:]]*(.+) ]]; then\n local pattern\n pattern=$(echo \"${BASH_REMATCH[1]}\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[\\^$]//g')\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg s \"*\" --arg t \"$pattern\" --arg rt \"forbidden_call\" \\\n '. + [{\"source\": $s, \"target\": $t, \"type\": $rt}]')\n found_anything=true\n fi\n fi\n if $in_forbidigo && [[ \"$line\" =~ ^[[:space:]]{4}[a-z] && ! \"$line\" =~ forbid && ! \"$line\" =~ pattern ]]; then\n in_forbidigo=false\n in_forbid=false\n fi\n done \u003c \"$golangci_config\"\n fi\n\n # Detect directory-based architecture patterns\n local arch_pattern=\"\"\n if [ -d \"ports\" ] && [ -d \"adapters\" ]; then\n arch_pattern=\"hexagonal\"\n found_anything=true\n elif [ -d \"domain\" ] && [ -d \"infrastructure\" ]; then\n arch_pattern=\"clean-architecture\"\n found_anything=true\n elif [ -d \"cmd\" ] && [ -d \"pkg\" ]; then\n arch_pattern=\"go-standard-layout\"\n found_anything=true\n fi\n\n if [ -n \"$arch_pattern\" ]; then\n LAYER_DEFINITIONS=$(echo \"$LAYER_DEFINITIONS\" | jq --arg p \"$arch_pattern\" '. + {\"architecture_pattern\": $p}')\n fi\n\n $found_anything || return 1\n\n [ -z \"$FRAMEWORK\" ] && FRAMEWORK=\"go-conventions\"\n RULES=\"$rules_arr\"\n INTERNAL_PACKAGES=\"$internal_pkgs\"\n return 0\n}\n\n# --- ESLint import restrictions ---\nparse_eslint_imports() {\n local config_file=\"\"\n for f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n [ -z \"$config_file\" ] && return 1\n\n local rules_arr=\"[]\"\n local found=false\n\n # For JSON eslint configs, extract import/no-restricted-paths or no-restricted-imports\n if [[ \"$config_file\" == *.json ]] || [[ \"$config_file\" == .eslintrc ]]; then\n # Check for no-restricted-imports\n local restricted\n restricted=$(jq -r '.rules[\"no-restricted-imports\"] // .rules[\"import/no-restricted-paths\"] // empty' \"$config_file\" 2>/dev/null || echo \"\")\n if [ -n \"$restricted\" ] && [ \"$restricted\" != \"null\" ]; then\n found=true\n # Extract patterns from the restriction config\n local patterns\n patterns=$(echo \"$restricted\" | jq -r '\n if type == \"array\" then\n .[1].patterns // .[1].zones // []\n elif type == \"object\" then\n .patterns // .zones // []\n else [] end | .[] |\n if type == \"object\" then\n \"from=\" + (.from // .target // \"?\") + \" import=\" + (.import // .message // \"restricted\")\n elif type == \"string\" then\n \"pattern=\" + .\n else empty end\n ' 2>/dev/null || echo \"\")\n\n while IFS= read -r pat; do\n [ -z \"$pat\" ] && continue\n rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg desc \"$pat\" \\\n '. + [{\"source\": \"*\", \"target\": $desc, \"type\": \"import_restriction\"}]')\n done \u003c\u003c\u003c \"$patterns\"\n fi\n fi\n\n # For JS configs, do a best-effort grep for no-restricted-imports\n if [[ \"$config_file\" == *.js ]] || [[ \"$config_file\" == *.mjs ]] || [[ \"$config_file\" == *.cjs ]]; then\n if grep -q 'no-restricted-imports\\|import/no-restricted-paths' \"$config_file\" 2>/dev/null; then\n found=true\n # Extract string patterns from the config (simplified)\n while IFS= read -r pattern; do\n [ -z \"$pattern\" ] && continue\n local clean_pattern\n clean_pattern=$(echo \"$pattern\" | sed \"s/['\\\"]//g;s/,//g;s/^[[:space:]]*//\" | head -c 100)\n [ -n \"$clean_pattern\" ] && rules_arr=$(echo \"$rules_arr\" | jq \\\n --arg t \"$clean_pattern\" \\\n '. + [{\"source\": \"*\", \"target\": $t, \"type\": \"import_restriction\"}]')\n done \u003c \u003c(grep -A1 'no-restricted-imports\\|import/no-restricted-paths' \"$config_file\" 2>/dev/null | grep -oE \"'[^']+'\" | head -10)\n fi\n fi\n\n $found || return 1\n\n [ -z \"$FRAMEWORK\" ] && FRAMEWORK=\"eslint-imports\"\n RULES=\"$rules_arr\"\n return 0\n}\n\n# --- Detect architecture from directory conventions ---\nparse_directory_architecture() {\n local rules_arr=\"[]\"\n local layers_obj=\"{}\"\n local found=false\n\n # Hexagonal / Ports & Adapters\n if [ -d \"ports\" ] && [ -d \"adapters\" ]; then\n found=true\n layers_obj=$(echo \"$layers_obj\" | jq '. + {\"architecture_pattern\": \"hexagonal\"}')\n rules_arr=$(echo \"$rules_arr\" | jq '. + [\n {\"source\": \"adapters\", \"target\": \"ports\", \"type\": \"may_depend\"},\n {\"source\": \"ports\", \"target\": \"adapters\", \"type\": \"must_not_depend\"}\n ]')\n fi\n\n # Clean Architecture\n if [ -d \"domain\" ] && { [ -d \"infrastructure\" ] || [ -d \"application\" ]; }; then\n found=true\n layers_obj=$(echo \"$layers_obj\" | jq '. + {\"architecture_pattern\": \"clean-architecture\"}')\n rules_arr=$(echo \"$rules_arr\" | jq '. + [\n {\"source\": \"domain\", \"target\": \"infrastructure\", \"type\": \"must_not_depend\"},\n {\"source\": \"domain\", \"target\": \"application\", \"type\": \"must_not_depend\"}\n ]')\n fi\n\n # Onion Architecture\n if [ -d \"Domain\" ] && [ -d \"Application\" ] && [ -d \"Infrastructure\" ]; then\n found=true\n layers_obj=$(echo \"$layers_obj\" | jq '. + {\"architecture_pattern\": \"onion\"}')\n rules_arr=$(echo \"$rules_arr\" | jq '. + [\n {\"source\": \"Domain\", \"target\": \"Infrastructure\", \"type\": \"must_not_depend\"},\n {\"source\": \"Domain\", \"target\": \"Application\", \"type\": \"must_not_depend\"},\n {\"source\": \"Application\", \"target\": \"Infrastructure\", \"type\": \"must_not_depend\"}\n ]')\n fi\n\n $found || return 1\n\n [ -z \"$FRAMEWORK\" ] && FRAMEWORK=\"directory-conventions\"\n RULES=\"$rules_arr\"\n LAYER_DEFINITIONS=\"$layers_obj\"\n return 0\n}\n\n# --- Main: try each parser in priority order ---\n# Try specific frameworks first, then fall back to conventions\nparse_phpat || true\nif [ \"$FRAMEWORK\" = \"\" ]; then\n parse_deptrac || true\nfi\nif [ \"$FRAMEWORK\" = \"\" ]; then\n parse_go_architecture || true\nfi\nif [ \"$FRAMEWORK\" = \"\" ]; then\n parse_eslint_imports || true\nfi\nif [ \"$FRAMEWORK\" = \"\" ]; then\n parse_directory_architecture || true\nfi\n\n# Output final JSON\njq -n \\\n --arg framework \"$FRAMEWORK\" \\\n --argjson rules \"$RULES\" \\\n --argjson internal_packages \"$INTERNAL_PACKAGES\" \\\n --argjson layer_definitions \"$LAYER_DEFINITIONS\" \\\n '{\n framework: $framework,\n rules: $rules,\n internal_packages: $internal_packages,\n layer_definitions: $layer_definitions\n } | with_entries(select(\n .value != \"\" and .value != [] and .value != {} and .value != null\n ))'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":20415,"content_sha256":"611127dbd35353ff1cf09d32b2ad9d271af9179052fdef0ddaaa627cbc86a9f1"},{"filename":"scripts/extract-ci-commands.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract commands and configuration from CI/CD workflow files\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Parse GitHub Actions workflows\nparse_github_actions() {\n local workflows_dir=\".github/workflows\"\n\n if [ ! -d \"$workflows_dir\" ]; then\n echo \"{}\"\n return\n fi\n\n local workflow_files=()\n local all_commands=()\n local jobs=()\n local triggers=()\n\n # Find all workflow files\n for f in \"$workflows_dir\"/*.yml \"$workflows_dir\"/*.yaml; do\n [ -f \"$f\" ] || continue\n workflow_files+=(\"$(basename \"$f\")\")\n\n # Extract triggers (on: section)\n local file_triggers\n file_triggers=$(grep -A10 \"^on:\" \"$f\" 2>/dev/null | grep -E \"^\\s+(push|pull_request|workflow_dispatch|schedule|release):\" | sed 's/[[:space:]]*\\([a-z_]*\\):.*/\\1/' | tr '\\n' ',' | sed 's/,$//' || echo \"\")\n [ -n \"$file_triggers\" ] && triggers+=(\"$file_triggers\")\n\n # Extract job names\n local in_jobs=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^jobs: ]]; then\n in_jobs=true\n continue\n fi\n if $in_jobs && [[ \"$line\" =~ ^[[:space:]]{2}([a-zA-Z0-9_-]+): ]]; then\n jobs+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \"$f\"\n\n # Extract run commands (handles both single-line and multiline YAML blocks)\n local in_multiline=false\n local run_indent=0\n while IFS= read -r line; do\n if $in_multiline; then\n # Calculate current line's indent (count leading spaces)\n local current_indent=0\n if [[ \"$line\" =~ ^([[:space:]]*) ]]; then\n current_indent=${#BASH_REMATCH[1]}\n fi\n # Check if we've exited the multiline block (same or less indent, non-empty line)\n if [[ -n \"${line//[[:space:]]/}\" ]] && (( current_indent \u003c= run_indent )); then\n in_multiline=false\n # Re-check this line in case it contains a new run: command\n else\n # Inside multiline block - capture first meaningful command line\n local trimmed=\"${line#\"${line%%[![:space:]]*}\"}\" # Trim leading whitespace\n # Skip empty lines and comments\n if [[ -n \"$trimmed\" ]] && [[ ! \"$trimmed\" =~ ^# ]]; then\n all_commands+=(\"$trimmed\")\n in_multiline=false # We only want the first command\n continue\n fi\n continue\n fi\n fi\n\n if [[ \"$line\" =~ ^([[:space:]]*)(-[[:space:]]+)?run:[[:space:]]*(.*) ]]; then\n run_indent=${#BASH_REMATCH[1]}\n local cmd=\"${BASH_REMATCH[3]}\"\n # Check if it's a multi-line indicator\n if [[ \"$cmd\" == \"|\" ]] || [[ \"$cmd\" == \"|-\" ]] || [[ \"$cmd\" == \"|+\" ]]; then\n in_multiline=true\n continue\n fi\n # Single-line command - clean and add\n cmd=$(echo \"$cmd\" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')\n [ -n \"$cmd\" ] && all_commands+=(\"$cmd\")\n fi\n done \u003c \"$f\"\n done\n\n # Deduplicate\n local unique_commands=()\n local unique_triggers=()\n local unique_jobs=()\n\n if [ ${#all_commands[@]} -gt 0 ]; then\n mapfile -t unique_commands \u003c \u003c(printf '%s\\n' \"${all_commands[@]}\" | sort -u | head -20)\n fi\n if [ ${#triggers[@]} -gt 0 ]; then\n mapfile -t unique_triggers \u003c \u003c(printf '%s\\n' \"${triggers[@]}\" | tr ',' '\\n' | sort -u)\n fi\n if [ ${#jobs[@]} -gt 0 ]; then\n mapfile -t unique_jobs \u003c \u003c(printf '%s\\n' \"${jobs[@]}\" | sort -u)\n fi\n\n # Build JSON arrays\n local files_json=\"[]\"\n local commands_json=\"[]\"\n local triggers_json=\"[]\"\n local jobs_json=\"[]\"\n\n [ ${#workflow_files[@]} -gt 0 ] && files_json=$(printf '%s\\n' \"${workflow_files[@]}\" | jq -R . | jq -s .)\n [ ${#unique_commands[@]} -gt 0 ] && commands_json=$(printf '%s\\n' \"${unique_commands[@]}\" | jq -R . | jq -s .)\n [ ${#unique_triggers[@]} -gt 0 ] && triggers_json=$(printf '%s\\n' \"${unique_triggers[@]}\" | jq -R . | jq -s .)\n [ ${#unique_jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\\n' \"${unique_jobs[@]}\" | jq -R . | jq -s .)\n\n jq -n \\\n --argjson files \"$files_json\" \\\n --argjson commands \"$commands_json\" \\\n --argjson triggers \"$triggers_json\" \\\n --argjson jobs \"$jobs_json\" \\\n '{\n workflow_files: $files,\n triggers: $triggers,\n jobs: $jobs,\n run_commands: $commands\n }'\n}\n\n# Parse GitLab CI\nparse_gitlab_ci() {\n if [ ! -f \".gitlab-ci.yml\" ]; then\n echo \"{}\"\n return\n fi\n\n local stages=()\n local jobs=()\n local commands=()\n\n # Extract stages\n local in_stages=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^stages: ]]; then\n in_stages=true\n continue\n fi\n if $in_stages && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then\n stages+=(\"${BASH_REMATCH[1]}\")\n elif $in_stages && [[ ! \"$line\" =~ ^[[:space:]] ]]; then\n in_stages=false\n fi\n done \u003c \".gitlab-ci.yml\"\n\n # Extract job names (lines that start with a name and have a colon, not indented, not keywords)\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^([a-zA-Z0-9_-]+): ]] && [[ ! \"$line\" =~ ^(stages|variables|default|include|workflow|image|services|before_script|after_script|cache): ]]; then\n jobs+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \".gitlab-ci.yml\"\n\n # Extract script commands\n local in_script=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]+script: ]]; then\n in_script=true\n continue\n fi\n if $in_script && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then\n local cmd=\"${BASH_REMATCH[1]}\"\n cmd=$(echo \"$cmd\" | sed \"s/^['\\\"]//;s/['\\\"]$//\")\n [ -n \"$cmd\" ] && commands+=(\"$cmd\")\n elif $in_script && [[ ! \"$line\" =~ ^[[:space:]] ]]; then\n in_script=false\n fi\n done \u003c \".gitlab-ci.yml\"\n\n # Build JSON arrays\n local stages_json=\"[]\"\n local jobs_json=\"[]\"\n local commands_json=\"[]\"\n\n [ ${#stages[@]} -gt 0 ] && stages_json=$(printf '%s\\n' \"${stages[@]}\" | jq -R . | jq -s .)\n [ ${#jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\\n' \"${jobs[@]}\" | jq -R . | jq -s .)\n [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\\n' \"${commands[@]}\" | sort -u | head -20 | jq -R . | jq -s .)\n\n jq -n \\\n --argjson stages \"$stages_json\" \\\n --argjson jobs \"$jobs_json\" \\\n --argjson commands \"$commands_json\" \\\n '{\n stages: $stages,\n jobs: $jobs,\n script_commands: $commands\n }'\n}\n\n# Parse CircleCI\nparse_circleci() {\n if [ ! -f \".circleci/config.yml\" ]; then\n echo \"{}\"\n return\n fi\n\n local jobs=()\n local commands=()\n local orbs=()\n\n # Extract orbs\n local in_orbs=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^orbs: ]]; then\n in_orbs=true\n continue\n fi\n if $in_orbs && [[ \"$line\" =~ ^[[:space:]]+([a-zA-Z0-9_-]+):[[:space:]]* ]]; then\n orbs+=(\"${BASH_REMATCH[1]}\")\n elif $in_orbs && [[ ! \"$line\" =~ ^[[:space:]] ]]; then\n in_orbs=false\n fi\n done \u003c \".circleci/config.yml\"\n\n # Extract job names\n local in_jobs=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^jobs: ]]; then\n in_jobs=true\n continue\n fi\n if $in_jobs && [[ \"$line\" =~ ^[[:space:]]{2}([a-zA-Z0-9_-]+): ]]; then\n jobs+=(\"${BASH_REMATCH[1]}\")\n elif $in_jobs && [[ ! \"$line\" =~ ^[[:space:]] ]]; then\n in_jobs=false\n fi\n done \u003c \".circleci/config.yml\"\n\n # Extract run commands (handles both single-line and multiline YAML blocks)\n local in_multiline=false\n local cmd_indent=0\n while IFS= read -r line; do\n if $in_multiline; then\n # Calculate current line's indent\n local current_indent=0\n if [[ \"$line\" =~ ^([[:space:]]*) ]]; then\n current_indent=${#BASH_REMATCH[1]}\n fi\n # Check if we've exited the multiline block\n if [[ -n \"${line//[[:space:]]/}\" ]] && (( current_indent \u003c= cmd_indent )); then\n in_multiline=false\n else\n # Inside multiline block - capture first meaningful command line\n local trimmed=\"${line#\"${line%%[![:space:]]*}\"}\"\n if [[ -n \"$trimmed\" ]] && [[ ! \"$trimmed\" =~ ^# ]]; then\n commands+=(\"$trimmed\")\n in_multiline=false\n continue\n fi\n continue\n fi\n fi\n\n if [[ \"$line\" =~ ^([[:space:]]*)command:[[:space:]]*(.*) ]]; then\n cmd_indent=${#BASH_REMATCH[1]}\n local cmd=\"${BASH_REMATCH[2]}\"\n if [[ \"$cmd\" == \"|\" ]] || [[ \"$cmd\" == \"|-\" ]] || [[ \"$cmd\" == \"|+\" ]]; then\n in_multiline=true\n continue\n fi\n cmd=$(echo \"$cmd\" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')\n [ -n \"$cmd\" ] && commands+=(\"$cmd\")\n fi\n done \u003c \".circleci/config.yml\"\n\n # Build JSON arrays\n local jobs_json=\"[]\"\n local commands_json=\"[]\"\n local orbs_json=\"[]\"\n\n [ ${#jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\\n' \"${jobs[@]}\" | jq -R . | jq -s .)\n [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\\n' \"${commands[@]}\" | sort -u | head -20 | jq -R . | jq -s .)\n [ ${#orbs[@]} -gt 0 ] && orbs_json=$(printf '%s\\n' \"${orbs[@]}\" | jq -R . | jq -s .)\n\n jq -n \\\n --argjson jobs \"$jobs_json\" \\\n --argjson commands \"$commands_json\" \\\n --argjson orbs \"$orbs_json\" \\\n '{\n orbs: $orbs,\n jobs: $jobs,\n run_commands: $commands\n }'\n}\n\n# Parse Travis CI\nparse_travis() {\n if [ ! -f \".travis.yml\" ]; then\n echo \"{}\"\n return\n fi\n\n local commands=()\n local language=\"\"\n\n # Extract language\n language=$(grep -E \"^language:\" \".travis.yml\" 2>/dev/null | sed 's/language:[[:space:]]*//' | head -1 || echo \"\")\n\n # Extract script commands\n local in_script=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^script: ]]; then\n in_script=true\n # Check if single-line script\n if [[ \"$line\" =~ ^script:[[:space:]]+(.+) ]]; then\n commands+=(\"${BASH_REMATCH[1]}\")\n in_script=false\n fi\n continue\n fi\n if $in_script && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then\n commands+=(\"${BASH_REMATCH[1]}\")\n elif $in_script && [[ ! \"$line\" =~ ^[[:space:]] ]]; then\n in_script=false\n fi\n done \u003c \".travis.yml\"\n\n local commands_json=\"[]\"\n [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\\n' \"${commands[@]}\" | jq -R . | jq -s .)\n\n jq -n \\\n --arg language \"$language\" \\\n --argjson commands \"$commands_json\" \\\n '{\n language: $language,\n script_commands: $commands\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Detect CI system\nCI_SYSTEM=\"none\"\nif [ -d \".github/workflows\" ]; then\n CI_SYSTEM=\"github-actions\"\nelif [ -f \".gitlab-ci.yml\" ]; then\n CI_SYSTEM=\"gitlab-ci\"\nelif [ -f \".circleci/config.yml\" ]; then\n CI_SYSTEM=\"circleci\"\nelif [ -f \".travis.yml\" ]; then\n CI_SYSTEM=\"travis-ci\"\nelif [ -f \"Jenkinsfile\" ]; then\n CI_SYSTEM=\"jenkins\"\nelif [ -f \"azure-pipelines.yml\" ]; then\n CI_SYSTEM=\"azure-pipelines\"\nelif [ -f \"bitbucket-pipelines.yml\" ]; then\n CI_SYSTEM=\"bitbucket-pipelines\"\nfi\n\n# Parse the detected CI system\nGITHUB_ACTIONS=\"{}\"\nGITLAB_CI=\"{}\"\nCIRCLECI=\"{}\"\nTRAVIS=\"{}\"\n\ncase \"$CI_SYSTEM\" in\n \"github-actions\")\n GITHUB_ACTIONS=$(parse_github_actions)\n ;;\n \"gitlab-ci\")\n GITLAB_CI=$(parse_gitlab_ci)\n ;;\n \"circleci\")\n CIRCLECI=$(parse_circleci)\n ;;\n \"travis-ci\")\n TRAVIS=$(parse_travis)\n ;;\nesac\n\n# Build final JSON output\njq -n \\\n --arg ci_system \"$CI_SYSTEM\" \\\n --argjson github_actions \"$GITHUB_ACTIONS\" \\\n --argjson gitlab_ci \"$GITLAB_CI\" \\\n --argjson circleci \"$CIRCLECI\" \\\n --argjson travis \"$TRAVIS\" \\\n '{\n ci_system: $ci_system,\n github_actions: $github_actions,\n gitlab_ci: $gitlab_ci,\n circleci: $circleci,\n travis: $travis\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":12846,"content_sha256":"4c7273188ae4806c1eb85b9948f53db410fbb30ab1b37cd988026ed9a1fcaa69"},{"filename":"scripts/extract-ci-rules.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract CI workflow rules that agents need to follow:\n# version matrices, quality gates, coverage thresholds, security workflows, linter configs, pinned actions\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n# Collect unique values into a bash array via a nameref\n# Usage: collect_unique arr_name \"value\"\ncollect_unique() {\n local -n _arr=\"$1\"\n local val=\"$2\"\n for existing in \"${_arr[@]+\"${_arr[@]}\"}\"; do\n [[ \"$existing\" == \"$val\" ]] && return\n done\n _arr+=(\"$val\")\n}\n\n# Turn a bash array into a JSON array via jq\narr_to_json() {\n local -n _a=\"$1\"\n if [[ ${#_a[@]} -eq 0 ]]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${_a[@]}\" | jq -R . | jq -s .\n fi\n}\n\n# Try yq first, fall back to grep/sed parsing\nhas_yq() { command -v yq &>/dev/null; }\n\n# ---------------------------------------------------------------------------\n# GitHub Actions parsing\n# ---------------------------------------------------------------------------\n# shellcheck disable=SC2034 # the arrays below are read by NAME through\n# collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow\n# that and reports every one of them as unused.\nparse_github_actions() {\n local wf_dir=\".github/workflows\"\n [[ -d \"$wf_dir\" ]] || { echo \"{}\"; return; }\n\n local php_versions=()\n local node_versions=()\n local go_version=\"\"\n local python_versions=()\n local quality_gates=()\n local required_checks=()\n local linter_configs=()\n local security_workflows=()\n local coverage_threshold=\"\"\n local pinned_actions=true\n local permissions=()\n\n for wf in \"$wf_dir\"/*.yml \"$wf_dir\"/*.yaml; do\n [[ -f \"$wf\" ]] || continue\n local basename\n basename=$(basename \"$wf\")\n\n # --- Security workflows ---\n case \"$basename\" in\n codeql*|scorecard*|dependency-review*|trivy*|snyk*|semgrep*)\n collect_unique security_workflows \"$basename\"\n ;;\n esac\n\n local content\n content=$(\u003c\"$wf\")\n\n # --- Pinned actions check ---\n # SHA-pinned: uses: foo/bar@\u003c40-hex-chars> (may have # vX.Y.Z comment)\n # NOT pinned: uses: foo/[email protected] or @main\n # Exclude reusable workflow calls (.github/workflows/) — those often can't be SHA-pinned\n # Strategy: find all uses: lines, exclude reusable workflows, then check if any lack a 40-char hex SHA\n local unpinned_count\n unpinned_count=$(echo \"$content\" \\\n | grep -E '^[[:space:]]*uses:' \\\n | grep -vE '\\.github/workflows/' \\\n | { grep -vE '@[0-9a-f]{40}' || true; } \\\n | wc -l)\n if [[ \"$unpinned_count\" -gt 0 ]]; then\n pinned_actions=false\n fi\n\n # --- Permissions block ---\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]+(contents|pull-requests|security-events|id-token|packages|actions|checks|issues|statuses|deployments):[[:space:]]+(read|write|none) ]]; then\n local perm=\"${BASH_REMATCH[1]}: ${BASH_REMATCH[2]}\"\n collect_unique permissions \"$perm\"\n fi\n done \u003c\u003c\u003c \"$content\"\n\n # --- PHP version matrix ---\n # Matches patterns like: php: ['8.2', '8.3'] or php-version: ['8.2']\n # Also matrix entries like php: [8.2, 8.3]\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique php_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E '(php|php-version|php_version).*\\[' | grep -oE \"[0-9]+\\.[0-9]+\" || true)\n\n # Single php-version value\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique php_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E 'php-version:[[:space:]]*[0-9]' | grep -oE \"[0-9]+\\.[0-9]+\" || true)\n\n # --- Node version matrix ---\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique node_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E '(node|node-version|node_version).*\\[' | grep -oE \"[0-9]+\" || true)\n\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique node_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E 'node-version:[[:space:]]*[0-9]' | grep -oE \"[0-9]+\" || true)\n\n # --- Go version ---\n # go-version-file: go.mod → read from go.mod\n if echo \"$content\" | grep -qE 'go-version-file:'; then\n if [[ -f \"go.mod\" ]]; then\n local gv\n gv=$(grep -E '^go ' go.mod | head -1 | awk '{print $2}')\n [[ -n \"$gv\" ]] && go_version=\"$gv\"\n fi\n fi\n # Explicit go-version: 1.x\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && go_version=\"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E 'go-version:[[:space:]]*[0-9]' | grep -oE \"[0-9]+\\.[0-9]+[0-9.]*\" | head -1 || true)\n\n # --- Python version matrix ---\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique python_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E '(python|python-version).*\\[' | grep -oE \"[0-9]+\\.[0-9]+\" || true)\n\n while IFS= read -r ver; do\n ver=$(echo \"$ver\" | tr -d \"\\\"' \")\n [[ -n \"$ver\" ]] && collect_unique python_versions \"$ver\"\n done \u003c \u003c(echo \"$content\" | grep -E 'python-version:[[:space:]]*[0-9]' | grep -oE \"[0-9]+\\.[0-9]+\" || true)\n\n # --- Quality gates (detect from job names and run commands) ---\n # PHPStan\n if echo \"$content\" | grep -qiE 'phpstan|phpstan\\.neon'; then\n # Try to extract level from the command line\n local phpstan_level\n phpstan_level=$(echo \"$content\" | grep -oE 'phpstan[[:space:]]+analyse.*--level[=[:space:]]*[0-9]+' | grep -oE '[0-9]+

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| head -1 || true)\n if [[ -z \"$phpstan_level\" ]] && [[ -f \"phpstan.neon\" || -f \"phpstan.neon.dist\" || -f \"phpstan.dist.neon\" ]]; then\n local neon_file\n for neon_file in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do\n [[ -f \"$neon_file\" ]] || continue\n phpstan_level=$(grep -E '^[[:space:]]*level:' \"$neon_file\" | head -1 | grep -oE '[0-9]+' || true)\n [[ -n \"$phpstan_level\" ]] && break\n done\n fi\n if [[ -n \"$phpstan_level\" ]]; then\n collect_unique quality_gates \"phpstan-level-${phpstan_level}\"\n else\n collect_unique quality_gates \"phpstan\"\n fi\n fi\n\n # PHPUnit / phpunit\n echo \"$content\" | grep -qiE 'phpunit|ci:test:php:unit' && collect_unique quality_gates \"phpunit\"\n\n # Rector\n echo \"$content\" | grep -qiE 'rector.*--dry-run|ci:test:php:rector' && collect_unique quality_gates \"rector-dry-run\"\n\n # PHP-CS-Fixer\n echo \"$content\" | grep -qiE 'php-cs-fixer|ci:test:php:cgl' && collect_unique quality_gates \"php-cs-fixer\"\n\n # golangci-lint\n echo \"$content\" | grep -qiE 'golangci-lint' && collect_unique quality_gates \"golangci-lint\"\n\n # ESLint\n echo \"$content\" | grep -qiE 'eslint' && collect_unique quality_gates \"eslint\"\n\n # Jest / Vitest\n echo \"$content\" | grep -qiE '\\bjest\\b|\\bvitest\\b' && collect_unique quality_gates \"jest\"\n\n # pytest\n echo \"$content\" | grep -qiE '\\bpytest\\b' && collect_unique quality_gates \"pytest\"\n\n # go test\n echo \"$content\" | grep -qE 'go test' && collect_unique quality_gates \"go-test\"\n\n # go vet\n echo \"$content\" | grep -qE 'go vet' && collect_unique quality_gates \"go-vet\"\n\n # govulncheck\n echo \"$content\" | grep -qiE 'govulncheck' && collect_unique quality_gates \"govulncheck\"\n\n # fuzz tests\n echo \"$content\" | grep -qE '\\-fuzz=' && collect_unique quality_gates \"fuzz-tests\"\n\n # mutation testing\n echo \"$content\" | grep -qiE 'mutation|infection' && collect_unique quality_gates \"mutation-testing\"\n\n # CodeQL (also a security workflow)\n if echo \"$content\" | grep -qiE 'codeql'; then\n collect_unique quality_gates \"codeql\"\n collect_unique security_workflows \"$basename\"\n fi\n\n # Trivy\n if echo \"$content\" | grep -qiE 'trivy'; then\n collect_unique quality_gates \"trivy\"\n collect_unique security_workflows \"$basename\"\n fi\n\n # --- Coverage threshold ---\n # Patterns: THRESHOLD=60, --min-coverage=80, coverage-threshold: 80, fail_ci_if_error\n local threshold\n threshold=$(echo \"$content\" | grep -oE 'THRESHOLD=[0-9]+\\.?[0-9]*' | head -1 | grep -oE '[0-9]+\\.?[0-9]*' || true)\n [[ -n \"$threshold\" && -z \"$coverage_threshold\" ]] && coverage_threshold=\"${threshold}%\"\n\n threshold=$(echo \"$content\" | grep -oE '(--min-coverage|coverage-threshold|minimum_coverage)[=:[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+' || true)\n [[ -n \"$threshold\" && -z \"$coverage_threshold\" ]] && coverage_threshold=\"${threshold}%\"\n\n # --- Required checks (from workflow names) ---\n local wf_name\n wf_name=$(echo \"$content\" | grep -E '^name:' | head -1 | sed 's/^name:[[:space:]]*//')\n [[ -n \"$wf_name\" ]] && collect_unique required_checks \"$wf_name\"\n\n # --- Linter config references ---\n for cfg in .php-cs-fixer.php .php-cs-fixer.dist.php phpstan.neon phpstan.neon.dist phpstan.dist.neon \\\n .eslintrc .eslintrc.js .eslintrc.json eslint.config.js eslint.config.mjs \\\n .golangci.yml .golangci.yaml .prettierrc .prettierrc.json \\\n ruff.toml .flake8 .pylintrc mypy.ini .mypy.ini \\\n .markdownlint.json .markdownlint-cli2.jsonc .yamllint.yml; do\n if [[ -f \"$cfg\" ]]; then\n collect_unique linter_configs \"$cfg\"\n fi\n done\n done\n\n # Build JSON\n local php_json node_json python_json gates_json checks_json linters_json security_json perms_json\n php_json=$(arr_to_json php_versions)\n node_json=$(arr_to_json node_versions)\n python_json=$(arr_to_json python_versions)\n gates_json=$(arr_to_json quality_gates)\n checks_json=$(arr_to_json required_checks)\n linters_json=$(arr_to_json linter_configs)\n security_json=$(arr_to_json security_workflows)\n perms_json=$(arr_to_json permissions)\n\n jq -n \\\n --arg ci_platform \"github-actions\" \\\n --argjson php_versions \"$php_json\" \\\n --argjson node_versions \"$node_json\" \\\n --arg go_version \"$go_version\" \\\n --argjson python_versions \"$python_json\" \\\n --argjson quality_gates \"$gates_json\" \\\n --arg coverage_threshold \"$coverage_threshold\" \\\n --argjson required_checks \"$checks_json\" \\\n --argjson pinned_actions \"$pinned_actions\" \\\n --argjson linter_configs \"$linters_json\" \\\n --argjson security_workflows \"$security_json\" \\\n --argjson permissions \"$perms_json\" \\\n '{\n ci_platform: $ci_platform,\n php_versions: $php_versions,\n node_versions: $node_versions,\n go_version: $go_version,\n python_versions: $python_versions,\n quality_gates: $quality_gates,\n coverage_threshold: $coverage_threshold,\n required_checks: $required_checks,\n pinned_actions: $pinned_actions,\n linter_configs: $linter_configs,\n security_workflows: $security_workflows,\n permissions: $permissions\n } | with_entries(select(.value != \"\" and .value != [] and .value != null))'\n}\n\n# ---------------------------------------------------------------------------\n# GitLab CI parsing\n# ---------------------------------------------------------------------------\n# shellcheck disable=SC2034 # the arrays below are read by NAME through\n# collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow\n# that and reports every one of them as unused.\nparse_gitlab_ci() {\n [[ -f \".gitlab-ci.yml\" ]] || { echo \"{}\"; return; }\n\n local content\n content=$(\u003c\".gitlab-ci.yml\")\n\n local quality_gates=()\n local stages=()\n\n # Extract stages\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then\n local stage=\"${BASH_REMATCH[1]}\"\n stage=$(echo \"$stage\" | tr -d \"\\\"' \")\n collect_unique stages \"$stage\"\n fi\n done \u003c \u003c(sed -n '/^stages:/,/^[^[:space:]-]/p' \".gitlab-ci.yml\" | tail -n +2)\n\n # Detect quality gates from script blocks\n echo \"$content\" | grep -qiE 'phpstan' && collect_unique quality_gates \"phpstan\"\n echo \"$content\" | grep -qiE 'phpunit' && collect_unique quality_gates \"phpunit\"\n echo \"$content\" | grep -qiE 'eslint' && collect_unique quality_gates \"eslint\"\n echo \"$content\" | grep -qiE 'jest|vitest' && collect_unique quality_gates \"jest\"\n echo \"$content\" | grep -qiE 'pytest' && collect_unique quality_gates \"pytest\"\n echo \"$content\" | grep -qiE 'golangci-lint' && collect_unique quality_gates \"golangci-lint\"\n echo \"$content\" | grep -qE 'go test' && collect_unique quality_gates \"go-test\"\n\n local stages_json gates_json\n stages_json=$(arr_to_json stages)\n gates_json=$(arr_to_json quality_gates)\n\n jq -n \\\n --arg ci_platform \"gitlab-ci\" \\\n --argjson stages \"$stages_json\" \\\n --argjson quality_gates \"$gates_json\" \\\n '{\n ci_platform: $ci_platform,\n stages: $stages,\n quality_gates: $quality_gates\n } | with_entries(select(.value != \"\" and .value != [] and .value != null))'\n}\n\n# ---------------------------------------------------------------------------\n# Concourse CI parsing\n# ---------------------------------------------------------------------------\n# shellcheck disable=SC2034 # the arrays below are read by NAME through\n# collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow\n# that and reports every one of them as unused.\nparse_concourse() {\n local pipeline_file=\"\"\n for f in ci/pipeline.yml ci/pipeline.yaml pipeline.yml pipeline.yaml ci/*.yml; do\n [[ -f \"$f\" ]] && pipeline_file=\"$f\" && break\n done\n [[ -z \"$pipeline_file\" ]] && { echo \"{}\"; return; }\n\n local content\n content=$(\u003c\"$pipeline_file\")\n\n local resource_types=()\n local jobs=()\n\n # Extract resource types\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then\n collect_unique resource_types \"${BASH_REMATCH[1]}\"\n fi\n done \u003c \u003c(sed -n '/^resource_types:/,/^[a-z]/p' \"$pipeline_file\" | tail -n +2)\n\n # Extract job names\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then\n collect_unique jobs \"${BASH_REMATCH[1]}\"\n fi\n done \u003c \u003c(sed -n '/^jobs:/,/^[a-z]/p' \"$pipeline_file\" | tail -n +2)\n\n local rt_json jobs_json\n rt_json=$(arr_to_json resource_types)\n jobs_json=$(arr_to_json jobs)\n\n jq -n \\\n --arg ci_platform \"concourse\" \\\n --arg pipeline_file \"$pipeline_file\" \\\n --argjson resource_types \"$rt_json\" \\\n --argjson jobs \"$jobs_json\" \\\n '{\n ci_platform: $ci_platform,\n pipeline_file: $pipeline_file,\n resource_types: $resource_types,\n jobs: $jobs\n } | with_entries(select(.value != \"\" and .value != [] and .value != null))'\n}\n\n# ---------------------------------------------------------------------------\n# Detect CI platform and parse\n# ---------------------------------------------------------------------------\nif [[ -d \".github/workflows\" ]]; then\n parse_github_actions\nelif [[ -f \".gitlab-ci.yml\" ]]; then\n parse_gitlab_ci\nelif [[ -f \"ci/pipeline.yml\" || -f \"ci/pipeline.yaml\" || -f \"pipeline.yml\" || -f \"pipeline.yaml\" ]]; then\n parse_concourse\nelse\n # No CI detected\n echo '{}'\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":16437,"content_sha256":"61d4870aabcd4fbb421a6fdca01cdf396f91258a8f34eb7805ce518266905395"},{"filename":"scripts/extract-commands.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract build commands from various build tool files\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$SCRIPT_DIR/lib/config-root.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Stack filter: node, php, go, python, or auto (default)\nSTACK_FILTER=\"${2:-auto}\"\n\n# Skip extraction functions based on stack\nshould_extract_node() {\n [[ \"$STACK_FILTER\" == \"auto\" || \"$STACK_FILTER\" == \"node\" ]]\n}\n\nshould_extract_php() {\n [[ \"$STACK_FILTER\" == \"auto\" || \"$STACK_FILTER\" == \"php\" ]]\n}\n\nshould_extract_go() {\n [[ \"$STACK_FILTER\" == \"auto\" || \"$STACK_FILTER\" == \"go\" ]]\n}\n\nshould_extract_python() {\n [[ \"$STACK_FILTER\" == \"auto\" || \"$STACK_FILTER\" == \"python\" ]]\n}\n\n# Get project info (use . since we already cd'd to PROJECT_DIR)\nPROJECT_INFO=$(bash \"$SCRIPT_DIR/detect-project.sh\" \".\")\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\n# shellcheck disable=SC2034 # BUILD_TOOL reserved for future build-tool-specific detection\nBUILD_TOOL=$(echo \"$PROJECT_INFO\" | jq -r '.build_tool')\n\n# Initialize command variables\nINSTALL_CMD=\"\"\nTYPECHECK_CMD=\"\"\nLINT_CMD=\"\"\nFORMAT_CMD=\"\"\nTEST_CMD=\"\"\nTEST_SINGLE_CMD=\"\"\nBUILD_CMD=\"\"\nDEV_CMD=\"\"\n\n# Detect package manager for Node.js projects\n# Workspace-aware: checks workspace root for lockfiles if in a monorepo\nPACKAGE_MANAGER=\"npm\"\ndetect_package_manager() {\n # First, check if we're in a workspace - lockfile will be at workspace root\n local workspace_root\n workspace_root=$(find_node_workspace_root \"$(pwd)\" || true)\n if [ -n \"$workspace_root\" ]; then\n if [ -f \"$workspace_root/pnpm-lock.yaml\" ]; then\n PACKAGE_MANAGER=\"pnpm\"\n return\n elif [ -f \"$workspace_root/yarn.lock\" ]; then\n PACKAGE_MANAGER=\"yarn\"\n return\n elif [ -f \"$workspace_root/bun.lockb\" ]; then\n PACKAGE_MANAGER=\"bun\"\n return\n elif [ -f \"$workspace_root/package-lock.json\" ]; then\n PACKAGE_MANAGER=\"npm\"\n return\n fi\n fi\n\n # Fallback: check local directory\n if [ -f \"pnpm-lock.yaml\" ]; then\n PACKAGE_MANAGER=\"pnpm\"\n elif [ -f \"yarn.lock\" ]; then\n PACKAGE_MANAGER=\"yarn\"\n elif [ -f \"bun.lockb\" ]; then\n PACKAGE_MANAGER=\"bun\"\n elif [ -f \"package.json\" ]; then\n # Check packageManager field\n local pm_field\n pm_field=$(jq -r '.packageManager // empty' package.json 2>/dev/null | cut -d@ -f1)\n if [ -n \"$pm_field\" ]; then\n PACKAGE_MANAGER=\"$pm_field\"\n fi\n fi\n}\n\n# Get the run command for package manager\nget_pm_run() {\n case \"$PACKAGE_MANAGER\" in\n pnpm) echo \"pnpm\" ;;\n yarn) echo \"yarn\" ;;\n bun) echo \"bun run\" ;;\n *) echo \"npm run\" ;;\n esac\n}\n\n# Get the npx-equivalent for package manager\nget_pm_dlx() {\n case \"$PACKAGE_MANAGER\" in\n pnpm) echo \"pnpm dlx\" ;;\n yarn) echo \"yarn dlx\" ;;\n bun) echo \"bunx\" ;;\n *) echo \"npx\" ;;\n esac\n}\n\n# Extract from Makefile\nextract_from_makefile() {\n [ ! -f \"Makefile\" ] && return 0\n\n # Extract targets with ## comments\n while IFS= read -r line; do\n if [[ $line =~ ^([a-zA-Z_-]+):.*\\#\\#(.*)$ ]]; then\n target=\"${BASH_REMATCH[1]}\"\n # description captured but not yet used (for future enhanced output)\n # description=\"${BASH_REMATCH[2]}\"\n\n case \"$target\" in\n lint|check) LINT_CMD=\"make $target\" ;;\n format|fmt) FORMAT_CMD=\"make $target\" ;;\n test|tests) TEST_CMD=\"make $target\" ;;\n build) BUILD_CMD=\"make $target\" ;;\n typecheck|types) TYPECHECK_CMD=\"make $target\" ;;\n dev|serve) DEV_CMD=\"make $target\" ;;\n esac\n fi\n done \u003c Makefile\n}\n\n# Extract from package.json\nextract_from_package_json() {\n [ ! -f \"package.json\" ] && return 0\n\n # Detect package manager first\n detect_package_manager\n\n local pm_run pm_dlx\n pm_run=$(get_pm_run)\n pm_dlx=$(get_pm_dlx)\n\n # Set install command based on package manager\n INSTALL_CMD=\"$PACKAGE_MANAGER install\"\n\n local typecheck_key has_lint has_format has_test has_build dev_key\n typecheck_key=$(package_script_key typecheck \"type-check\")\n has_lint=$(jq -r '.scripts.lint // empty' package.json 2>/dev/null)\n has_format=$(jq -r '.scripts.format // empty' package.json 2>/dev/null)\n has_test=$(jq -r '.scripts.test // empty' package.json 2>/dev/null)\n has_build=$(jq -r '.scripts.build // empty' package.json 2>/dev/null)\n dev_key=$(package_script_key dev start)\n\n if [ -n \"$typecheck_key\" ]; then\n TYPECHECK_CMD=\"$pm_run $typecheck_key\"\n else\n TYPECHECK_CMD=\"$pm_dlx tsc --noEmit\"\n fi\n\n if [ -n \"$has_lint\" ]; then\n LINT_CMD=\"$pm_run lint\"\n else\n LINT_CMD=\"$pm_dlx eslint .\"\n fi\n\n if [ -n \"$has_format\" ]; then\n FORMAT_CMD=\"$pm_run format\"\n else\n FORMAT_CMD=\"$pm_dlx prettier --write .\"\n fi\n\n if [ -n \"$has_test\" ]; then\n TEST_CMD=\"$PACKAGE_MANAGER test\"\n # Single file test command\n if grep -q 'vitest' package.json 2>/dev/null; then\n TEST_SINGLE_CMD=\"$pm_dlx vitest run\"\n elif grep -q 'jest' package.json 2>/dev/null; then\n TEST_SINGLE_CMD=\"$pm_dlx jest\"\n else\n TEST_SINGLE_CMD=\"$PACKAGE_MANAGER test --\"\n fi\n fi\n\n if [ -n \"$has_build\" ]; then\n BUILD_CMD=\"$pm_run build\"\n fi\n\n if [ -n \"$dev_key\" ]; then\n DEV_CMD=\"$pm_run $dev_key\"\n fi\n}\n\n# Print the first script key that the given manifest actually defines, or\n# nothing. Callers emit `\u003crunner> \u003ckey>`, so the key has to be the one that\n# exists — accepting `cs:fix` as evidence and then printing `format` writes a\n# command the project does not have.\nmanifest_script_key() {\n local manifest=\"$1\"\n shift\n local key\n for key in \"$@\"; do\n if jq -e --arg k \"$key\" '.scripts[$k] // empty' \"$manifest\" >/dev/null 2>&1; then\n printf '%s' \"$key\"\n return 0\n fi\n done\n return 0\n}\n\ncomposer_script_key() { manifest_script_key composer.json \"$@\"; }\npackage_script_key() { manifest_script_key package.json \"$@\"; }\n\n# Extract from composer.json\nextract_from_composer_json() {\n [ ! -f \"composer.json\" ] && return 0\n\n local lint_key format_key test_key phpstan_key\n\n # Emit the script that EXISTS, not the first name we looked for. Accepting\n # `cs:fix` as evidence and then printing `composer run format` produces a\n # command the project does not define, and AGENTS.md presents it as fact.\n lint_key=$(composer_script_key lint \"cs:check\")\n format_key=$(composer_script_key format \"cs:fix\")\n test_key=$(composer_script_key test)\n phpstan_key=$(composer_script_key phpstan stan)\n\n if [ -n \"$lint_key\" ]; then\n LINT_CMD=\"composer run $lint_key\"\n fi\n\n if [ -n \"$format_key\" ]; then\n FORMAT_CMD=\"composer run $format_key\"\n fi\n\n if [ -n \"$test_key\" ]; then\n TEST_CMD=\"composer run $test_key\"\n else\n TEST_CMD=\"vendor/bin/phpunit\"\n fi\n\n if [ -n \"$phpstan_key\" ]; then\n TYPECHECK_CMD=\"composer run $phpstan_key\"\n elif [ -f \"phpstan.neon\" ] || [ -f \"Build/phpstan.neon\" ]; then\n TYPECHECK_CMD=\"vendor/bin/phpstan analyze\"\n fi\n}\n\n# Extract from pyproject.toml\nextract_from_pyproject() {\n [ ! -f \"pyproject.toml\" ] && return 0\n\n # Check for ruff\n if grep -q '\\[tool.ruff\\]' pyproject.toml; then\n LINT_CMD=\"ruff check .\"\n FORMAT_CMD=\"ruff format .\"\n fi\n\n # Check for black\n if grep -q 'black' pyproject.toml; then\n FORMAT_CMD=\"black .\"\n fi\n\n # Check for mypy\n if grep -q 'mypy' pyproject.toml; then\n TYPECHECK_CMD=\"mypy .\"\n fi\n\n # Check for pytest\n if grep -q 'pytest' pyproject.toml; then\n TEST_CMD=\"pytest\"\n fi\n}\n\n# Language-specific defaults\nset_language_defaults() {\n case \"$LANGUAGE\" in\n \"go\")\n : \"${INSTALL_CMD:=go mod download}\"\n : \"${TYPECHECK_CMD:=go build -v ./...}\"\n if [ -z \"$LINT_CMD\" ]; then\n if [ -f \".golangci.yml\" ] || [ -f \".golangci.yaml\" ]; then\n LINT_CMD=\"golangci-lint run ./...\"\n fi\n fi\n : \"${FORMAT_CMD:=gofmt -w .}\"\n : \"${TEST_CMD:=go test -v -race -short ./...}\"\n : \"${TEST_SINGLE_CMD:=go test -v -race}\"\n : \"${BUILD_CMD:=go build -v ./...}\"\n ;;\n\n \"php\")\n : \"${INSTALL_CMD:=composer install}\"\n if [ -z \"$TYPECHECK_CMD\" ]; then\n if [ -f \"phpstan.neon\" ] || [ -f \"Build/phpstan.neon\" ]; then\n TYPECHECK_CMD=\"vendor/bin/phpstan analyze\"\n fi\n fi\n : \"${LINT_CMD:=vendor/bin/php-cs-fixer fix --dry-run}\"\n : \"${FORMAT_CMD:=vendor/bin/php-cs-fixer fix}\"\n : \"${TEST_CMD:=vendor/bin/phpunit}\"\n : \"${TEST_SINGLE_CMD:=vendor/bin/phpunit}\"\n ;;\n\n \"typescript\")\n local pm_dlx\n pm_dlx=$(get_pm_dlx)\n : \"${INSTALL_CMD:=$PACKAGE_MANAGER install}\"\n : \"${TYPECHECK_CMD:=$pm_dlx tsc --noEmit}\"\n : \"${LINT_CMD:=$pm_dlx eslint .}\"\n : \"${FORMAT_CMD:=$pm_dlx prettier --write .}\"\n if [ -z \"$TEST_CMD\" ]; then\n if [ -f \"jest.config.js\" ] || [ -f \"jest.config.ts\" ]; then\n TEST_CMD=\"$PACKAGE_MANAGER test\"\n TEST_SINGLE_CMD=\"$pm_dlx jest\"\n elif grep -q 'vitest' package.json 2>/dev/null; then\n TEST_CMD=\"$pm_dlx vitest run\"\n TEST_SINGLE_CMD=\"$pm_dlx vitest run\"\n fi\n fi\n ;;\n\n \"python\")\n if [ -f \"pyproject.toml\" ]; then\n if grep -q '\\[tool.poetry\\]' pyproject.toml 2>/dev/null; then\n INSTALL_CMD=\"poetry install\"\n elif grep -q '\\[tool.uv\\]' pyproject.toml 2>/dev/null; then\n INSTALL_CMD=\"uv sync\"\n else\n INSTALL_CMD=\"pip install -e .\"\n fi\n else\n INSTALL_CMD=\"pip install -r requirements.txt\"\n fi\n : \"${LINT_CMD:=ruff check .}\"\n : \"${FORMAT_CMD:=ruff format .}\"\n : \"${TYPECHECK_CMD:=mypy .}\"\n : \"${TEST_CMD:=pytest}\"\n : \"${TEST_SINGLE_CMD:=pytest}\"\n ;;\n esac\n}\n\n# Run extraction (stack-filtered)\nextract_from_makefile # Always extract Makefile (cross-stack)\nshould_extract_node && extract_from_package_json\nshould_extract_php && extract_from_composer_json\nshould_extract_python && extract_from_pyproject\n\n# Only set language defaults if matching stack or auto\ncase \"$STACK_FILTER\" in\n \"auto\") set_language_defaults ;;\n \"node\") [[ \"$LANGUAGE\" == \"typescript\" || \"$LANGUAGE\" == \"javascript\" ]] && set_language_defaults ;;\n \"php\") [[ \"$LANGUAGE\" == \"php\" ]] && set_language_defaults ;;\n \"go\") [[ \"$LANGUAGE\" == \"go\" ]] && set_language_defaults ;;\n \"python\") [[ \"$LANGUAGE\" == \"python\" ]] && set_language_defaults ;;\n *)\n echo \"ERROR: Invalid STACK_FILTER value: $STACK_FILTER\" >&2\n echo \"Valid options: auto, node, php, go, python\" >&2\n exit 1\n ;;\nesac\n\n# Output JSON\njq -n \\\n --arg install \"$INSTALL_CMD\" \\\n --arg typecheck \"$TYPECHECK_CMD\" \\\n --arg lint \"$LINT_CMD\" \\\n --arg format \"$FORMAT_CMD\" \\\n --arg test \"$TEST_CMD\" \\\n --arg test_single \"$TEST_SINGLE_CMD\" \\\n --arg build \"$BUILD_CMD\" \\\n --arg dev \"$DEV_CMD\" \\\n --arg package_manager \"$PACKAGE_MANAGER\" \\\n '{\n install: $install,\n typecheck: $typecheck,\n lint: $lint,\n format: $format,\n test: $test,\n test_single: $test_single,\n build: $build,\n dev: $dev,\n package_manager: $package_manager\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":12189,"content_sha256":"6f82f24b2bd48ad50c802dc37d4dc97deee178616e2f359f65577c564ae48135"},{"filename":"scripts/extract-documentation.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract information from documentation files (README, CONTRIBUTING, SECURITY, etc.)\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Initialize outputs\nPROJECT_DESCRIPTION=\"\"\nARCHITECTURE_SECTION=\"\"\nPR_PROCESS=\"\"\nSECURITY_POLICY=\"\"\nVULNERABILITY_REPORTING=\"\"\nCHANGELOG_FORMAT=\"\"\n\n# Helper to extract section from markdown\n# Usage: extract_section \"file\" \"heading\" [max_lines]\nextract_section() {\n local file=\"$1\"\n local heading=\"$2\"\n local max_lines=\"${3:-20}\"\n\n if [ ! -f \"$file\" ]; then\n echo \"\"\n return\n fi\n\n # Find section and extract content until next heading\n awk -v heading=\"$heading\" -v max=\"$max_lines\" '\n BEGIN { found=0; count=0 }\n /^##?#?[[:space:]]/ {\n if (found) exit\n if (tolower($0) ~ tolower(heading)) { found=1; next }\n }\n found && count \u003c max { print; count++ }\n ' \"$file\"\n}\n\n# Extract first paragraph after title (project description)\nextract_description() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"\"\n return\n fi\n\n # Skip title and badges, get first paragraph\n awk '\n BEGIN { started=0; in_para=0 }\n /^#[^#]/ { started=1; next }\n started && /^\\[!\\[/ { next } # Skip badges\n started && /^[![]/ { next } # Skip images\n started && /^>/ { next } # Skip blockquotes initially\n started && !in_para && /^[A-Za-z]/ { in_para=1 }\n in_para && /^$/ { exit }\n in_para { print }\n ' \"$file\" | head -5\n}\n\n# Extract badges from README\nextract_badges() {\n local file=\"$1\"\n local badges=()\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Find badge patterns: [![text](url)](link) or ![text](url)\n while IFS= read -r line; do\n if [[ \"$line\" =~ \\[!\\[([^\\]]+)\\] ]]; then\n badges+=(\"${BASH_REMATCH[1]}\")\n elif [[ \"$line\" =~ !\\[([^\\]]+)\\] ]]; then\n badges+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \u003c(grep -E '^\\[?!\\[' \"$file\" 2>/dev/null || true)\n\n if [ ${#badges[@]} -eq 0 ]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${badges[@]}\" | jq -R . | jq -s .\n fi\n}\n\n# Extract key rules from CONTRIBUTING.md\nextract_contributing_rules() {\n local file=\"$1\"\n local rules=()\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Extract bullet points from key sections\n while IFS= read -r line; do\n # Clean up the line\n line=$(echo \"$line\" | sed 's/^[[:space:]]*[-*][[:space:]]*//' | sed 's/[[:space:]]*$//')\n if [ -n \"$line\" ] && [ ${#line} -gt 10 ] && [ ${#line} -lt 200 ]; then\n rules+=(\"$line\")\n fi\n done \u003c \u003c(grep -E '^[[:space:]]*[-*][[:space:]]' \"$file\" 2>/dev/null | head -20 || true)\n\n if [ ${#rules[@]} -eq 0 ]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${rules[@]}\" | jq -R . | jq -s .\n fi\n}\n\n# Check for PR process documentation\nextract_pr_process() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"\"\n return\n fi\n\n # Look for PR-related sections\n local section\n section=$(extract_section \"$file\" \"pull request\" 15)\n if [ -z \"$section\" ]; then\n section=$(extract_section \"$file\" \"submitting\" 15)\n fi\n if [ -z \"$section\" ]; then\n section=$(extract_section \"$file\" \"how to contribute\" 15)\n fi\n\n echo \"$section\" | head -10\n}\n\n# Extract security policy\nextract_security_policy() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"\"\n return\n fi\n\n # Get main content\n awk '\n BEGIN { started=0 }\n /^#/ { started=1; next }\n started { print }\n ' \"$file\" | head -20\n}\n\n# Detect changelog format\ndetect_changelog_format() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"none\"\n return\n fi\n\n # Check for Keep a Changelog format\n if grep -qi \"keep a changelog\" \"$file\" 2>/dev/null; then\n echo \"keepachangelog\"\n return\n fi\n\n # Check for conventional changelog\n if grep -qE '^\\s*###?\\s*(Added|Changed|Deprecated|Removed|Fixed|Security)' \"$file\" 2>/dev/null; then\n echo \"keepachangelog\"\n return\n fi\n\n # Check for date-based entries\n if grep -qE '^\\s*##\\s*\\[?[0-9]+\\.[0-9]+' \"$file\" 2>/dev/null; then\n echo \"semver-sections\"\n return\n fi\n\n # Check for simple list format\n if grep -qE '^[-*]\\s+' \"$file\" 2>/dev/null; then\n echo \"simple-list\"\n return\n fi\n\n echo \"custom\"\n}\n\n# Main extraction\n\n# README.md\nif [ -f \"README.md\" ]; then\n PROJECT_DESCRIPTION=$(extract_description \"README.md\")\n BADGES_JSON=$(extract_badges \"README.md\")\n ARCHITECTURE_SECTION=$(extract_section \"README.md\" \"architecture\" 30)\n if [ -z \"$ARCHITECTURE_SECTION\" ]; then\n ARCHITECTURE_SECTION=$(extract_section \"README.md\" \"structure\" 30)\n fi\nfi\n\n# CONTRIBUTING.md (check multiple locations)\nCONTRIBUTING_FILE=\"\"\nfor f in CONTRIBUTING.md .github/CONTRIBUTING.md docs/CONTRIBUTING.md; do\n if [ -f \"$f\" ]; then\n CONTRIBUTING_FILE=\"$f\"\n break\n fi\ndone\n\nif [ -n \"$CONTRIBUTING_FILE\" ]; then\n CONTRIBUTING_RULES_JSON=$(extract_contributing_rules \"$CONTRIBUTING_FILE\")\n PR_PROCESS=$(extract_pr_process \"$CONTRIBUTING_FILE\")\n CODE_STYLE_SECTION=$(extract_section \"$CONTRIBUTING_FILE\" \"code style\" 20)\n if [ -z \"$CODE_STYLE_SECTION\" ]; then\n CODE_STYLE_SECTION=$(extract_section \"$CONTRIBUTING_FILE\" \"style guide\" 20)\n fi\nelse\n CONTRIBUTING_RULES_JSON=\"[]\"\n CODE_STYLE_SECTION=\"\"\nfi\n\n# SECURITY.md (check multiple locations)\nSECURITY_FILE=\"\"\nfor f in SECURITY.md .github/SECURITY.md docs/SECURITY.md; do\n if [ -f \"$f\" ]; then\n SECURITY_FILE=\"$f\"\n break\n fi\ndone\n\nif [ -n \"$SECURITY_FILE\" ]; then\n SECURITY_POLICY=$(extract_security_policy \"$SECURITY_FILE\")\n VULNERABILITY_REPORTING=$(extract_section \"$SECURITY_FILE\" \"reporting\" 15)\nelse\n SECURITY_POLICY=\"\"\n VULNERABILITY_REPORTING=\"\"\nfi\n\n# CHANGELOG.md\nCHANGELOG_FILE=\"\"\nfor f in CHANGELOG.md HISTORY.md CHANGES.md; do\n if [ -f \"$f\" ]; then\n CHANGELOG_FILE=\"$f\"\n break\n fi\ndone\n\nif [ -n \"$CHANGELOG_FILE\" ]; then\n CHANGELOG_FORMAT=$(detect_changelog_format \"$CHANGELOG_FILE\")\nelse\n CHANGELOG_FORMAT=\"none\"\nfi\n\n# CODE_OF_CONDUCT.md\nCOC_FILE=\"\"\nfor f in CODE_OF_CONDUCT.md .github/CODE_OF_CONDUCT.md; do\n if [ -f \"$f\" ]; then\n COC_FILE=\"$f\"\n break\n fi\ndone\n\nHAS_CODE_OF_CONDUCT=false\nif [ -n \"$COC_FILE\" ]; then\n HAS_CODE_OF_CONDUCT=true\nfi\n\n# Build JSON output\njq -n \\\n --arg desc \"$PROJECT_DESCRIPTION\" \\\n --argjson badges \"${BADGES_JSON:-[]}\" \\\n --arg arch \"$ARCHITECTURE_SECTION\" \\\n --arg contributing_file \"$CONTRIBUTING_FILE\" \\\n --argjson contributing_rules \"${CONTRIBUTING_RULES_JSON:-[]}\" \\\n --arg pr_process \"$PR_PROCESS\" \\\n --arg code_style \"$CODE_STYLE_SECTION\" \\\n --arg security_file \"$SECURITY_FILE\" \\\n --arg security_policy \"$SECURITY_POLICY\" \\\n --arg vuln_reporting \"$VULNERABILITY_REPORTING\" \\\n --arg changelog_file \"$CHANGELOG_FILE\" \\\n --arg changelog_format \"$CHANGELOG_FORMAT\" \\\n --argjson has_coc \"$HAS_CODE_OF_CONDUCT\" \\\n --arg coc_file \"$COC_FILE\" \\\n '{\n readme: {\n description: $desc,\n badges: $badges,\n architecture_section: $arch\n },\n contributing: {\n file: $contributing_file,\n rules: $contributing_rules,\n pr_process: $pr_process,\n code_style: $code_style\n },\n security: {\n file: $security_file,\n policy: $security_policy,\n vulnerability_reporting: $vuln_reporting\n },\n changelog: {\n file: $changelog_file,\n format: $changelog_format\n },\n code_of_conduct: {\n exists: $has_coc,\n file: $coc_file\n }\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7990,"content_sha256":"73d425298a3d2a52737d0e311f6608ac5767064304aecf87fff3b2376b7cac8a"},{"filename":"scripts/extract-github-rulesets.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract GitHub repository rulesets (newer API, not just branch protection)\n# Returns JSON with ruleset details for AGENTS.md generation\n# Falls back gracefully if unavailable (no auth, not GitHub, no rulesets)\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Silent exit with empty JSON if prerequisites not met\nbail() {\n jq -n '{rulesets: [], merge_queue: false, required_checks: [], signed_commits: false}'\n exit 0\n}\n\n# Check gh CLI available\ncommand -v gh &>/dev/null || bail\n\n# Check authenticated\ngh auth status &>/dev/null 2>&1 || bail\n\n# Check this is a git repo with a remote\nREMOTE_URL=$(project_git remote get-url origin 2>/dev/null) || bail\n\n# Check it's a GitHub repo\n[[ \"$REMOTE_URL\" =~ github\\.com ]] || bail\n\n# Extract owner/repo from URL (handles both HTTPS and SSH)\nOWNER_REPO=$(echo \"$REMOTE_URL\" | sed -E 's|.*github\\.com[:/]([^/]+/[^/.]+)(\\.git)?.*|\\1|')\n[[ -n \"$OWNER_REPO\" ]] || bail\n\n# Fetch rulesets — may fail if no permission or no rulesets\nRULESETS_RAW=$(gh api \"repos/$OWNER_REPO/rulesets\" 2>/dev/null) || bail\n\n# Check if response is a valid array\necho \"$RULESETS_RAW\" | jq -e 'type == \"array\"' &>/dev/null || bail\n\nRULESET_COUNT=$(echo \"$RULESETS_RAW\" | jq 'length')\nif [ \"$RULESET_COUNT\" -eq 0 ]; then\n bail\nfi\n\n# Track aggregate settings\nHAS_MERGE_QUEUE=false\nHAS_SIGNED_COMMITS=false\nALL_REQUIRED_CHECKS=\"[]\"\nRULESETS_JSON=\"[]\"\n\nfor i in $(seq 0 $((RULESET_COUNT - 1))); do\n RULESET_ID=$(echo \"$RULESETS_RAW\" | jq -r \".[$i].id\")\n RULESET_NAME=$(echo \"$RULESETS_RAW\" | jq -r \".[$i].name // \\\"unnamed\\\"\")\n RULESET_TARGET=$(echo \"$RULESETS_RAW\" | jq -r \".[$i].target // \\\"branch\\\"\")\n RULESET_ENFORCEMENT=$(echo \"$RULESETS_RAW\" | jq -r \".[$i].enforcement // \\\"disabled\\\"\")\n\n # Skip disabled rulesets\n [ \"$RULESET_ENFORCEMENT\" = \"disabled\" ] && continue\n\n # Fetch detailed ruleset info (includes rules array)\n DETAIL=$(gh api \"repos/$OWNER_REPO/rulesets/$RULESET_ID\" 2>/dev/null) || continue\n\n # Extract rule types from the rules array\n RULE_TYPES=$(echo \"$DETAIL\" | jq -r '[.rules[]?.type // empty] | unique' 2>/dev/null) || RULE_TYPES=\"[]\"\n\n # Check for specific rule types\n if echo \"$RULE_TYPES\" | jq -e 'index(\"merge_queue\")' &>/dev/null; then\n HAS_MERGE_QUEUE=true\n fi\n if echo \"$RULE_TYPES\" | jq -e 'index(\"required_signatures\")' &>/dev/null; then\n HAS_SIGNED_COMMITS=true\n fi\n\n # Extract required status checks\n STATUS_CHECKS=$(echo \"$DETAIL\" | jq -r '\n [.rules[]? | select(.type == \"required_status_checks\") |\n .parameters.required_status_checks[]?.context // empty] | unique' 2>/dev/null) || STATUS_CHECKS=\"[]\"\n\n if [ \"$STATUS_CHECKS\" != \"[]\" ] && [ \"$STATUS_CHECKS\" != \"null\" ]; then\n ALL_REQUIRED_CHECKS=$(echo \"$ALL_REQUIRED_CHECKS\" \"$STATUS_CHECKS\" | jq -s 'add | unique')\n fi\n\n # Add to rulesets array\n RULESETS_JSON=$(echo \"$RULESETS_JSON\" | jq \\\n --arg name \"$RULESET_NAME\" \\\n --arg target \"$RULESET_TARGET\" \\\n --arg enforcement \"$RULESET_ENFORCEMENT\" \\\n --argjson rules \"$RULE_TYPES\" \\\n '. + [{name: $name, target: $target, enforcement: $enforcement, rules: $rules}]')\ndone\n\n# Output final JSON\njq -n \\\n --argjson rulesets \"$RULESETS_JSON\" \\\n --argjson merge_queue \"$HAS_MERGE_QUEUE\" \\\n --argjson required_checks \"$ALL_REQUIRED_CHECKS\" \\\n --argjson signed_commits \"$HAS_SIGNED_COMMITS\" \\\n '{rulesets: $rulesets, merge_queue: $merge_queue, required_checks: $required_checks, signed_commits: $signed_commits}'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3860,"content_sha256":"1522abc70ea18ebd2da3d284118b1de0f10e70d31bee32b6b511ec9051fdf631"},{"filename":"scripts/extract-github-settings.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract GitHub repository settings via gh CLI\n# Returns {} silently if gh unavailable, not authenticated, or not a GitHub repo\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Silent exit with empty JSON if prerequisites not met\nbail() { echo \"{}\"; exit 0; }\n\n# Check gh CLI available\ncommand -v gh &>/dev/null || bail\n\n# Check authenticated\ngh auth status &>/dev/null 2>&1 || bail\n\n# Check this is a git repo with a remote\nREMOTE_URL=$(project_git remote get-url origin 2>/dev/null) || bail\n\n# Check it's a GitHub repo\n[[ \"$REMOTE_URL\" =~ github\\.com ]] || bail\n\n# Extract owner/repo from URL (handles both HTTPS and SSH)\nOWNER_REPO=$(echo \"$REMOTE_URL\" | sed -E 's|.*github\\.com[:/]([^/]+/[^/.]+)(\\.git)?.*|\\1|')\n[[ -n \"$OWNER_REPO\" ]] || bail\n\n# Fetch repo settings\nREPO_INFO=$(gh api \"repos/$OWNER_REPO\" 2>/dev/null) || bail\n\n# Extract merge strategies\nALLOW_SQUASH=$(echo \"$REPO_INFO\" | jq -r '.allow_squash_merge // false')\nALLOW_MERGE=$(echo \"$REPO_INFO\" | jq -r '.allow_merge_commit // false')\nALLOW_REBASE=$(echo \"$REPO_INFO\" | jq -r '.allow_rebase_merge // false')\nDEFAULT_BRANCH=$(echo \"$REPO_INFO\" | jq -r '.default_branch // \"main\"')\nDELETE_BRANCH=$(echo \"$REPO_INFO\" | jq -r '.delete_branch_on_merge // false')\n\n# Build merge strategies array\nSTRATEGIES=\"[]\"\n[ \"$ALLOW_SQUASH\" = \"true\" ] && STRATEGIES=$(echo \"$STRATEGIES\" | jq '. + [\"squash\"]')\n[ \"$ALLOW_MERGE\" = \"true\" ] && STRATEGIES=$(echo \"$STRATEGIES\" | jq '. + [\"merge\"]')\n[ \"$ALLOW_REBASE\" = \"true\" ] && STRATEGIES=$(echo \"$STRATEGIES\" | jq '. + [\"rebase\"]')\n\n# Fetch branch protection (may fail if not configured or no access)\n# Note: gh api returns error JSON to stdout on 404, so we check for error message\nPROTECTION=$(gh api \"repos/$OWNER_REPO/branches/$DEFAULT_BRANCH/protection\" 2>/dev/null || true)\nif echo \"$PROTECTION\" | jq -e '.message' &>/dev/null; then\n # API returned an error (e.g., \"Branch not protected\")\n PROTECTION=\"{}\"\nfi\n\n# Extract protection settings\nREQUIRED_APPROVALS=0\nREQUIRED_CHECKS=\"[]\"\nREQUIRE_UP_TO_DATE=false\nDISMISS_STALE=false\n\nif [ \"$PROTECTION\" != \"{}\" ]; then\n # Required approving reviews\n REQUIRED_APPROVALS=$(echo \"$PROTECTION\" | jq -r '.required_pull_request_reviews.required_approving_review_count // 0')\n DISMISS_STALE=$(echo \"$PROTECTION\" | jq -r '.required_pull_request_reviews.dismiss_stale_reviews // false')\n\n # Required status checks\n REQUIRED_CHECKS=$(echo \"$PROTECTION\" | jq -r '.required_status_checks.contexts // []')\n\n # Require up-to-date branch\n REQUIRE_UP_TO_DATE=$(echo \"$PROTECTION\" | jq -r '.required_status_checks.strict // false')\nfi\n\n# Output JSON\njq -n \\\n --arg default_branch \"$DEFAULT_BRANCH\" \\\n --argjson merge_strategies \"$STRATEGIES\" \\\n --argjson required_approvals \"$REQUIRED_APPROVALS\" \\\n --argjson required_checks \"$REQUIRED_CHECKS\" \\\n --argjson require_up_to_date \"$REQUIRE_UP_TO_DATE\" \\\n --argjson dismiss_stale \"$DISMISS_STALE\" \\\n --argjson delete_branch \"$DELETE_BRANCH\" \\\n '{\n available: true,\n default_branch: $default_branch,\n merge_strategies: $merge_strategies,\n required_approvals: $required_approvals,\n required_checks: $required_checks,\n require_up_to_date: $require_up_to_date,\n dismiss_stale_reviews: $dismiss_stale,\n delete_branch_on_merge: $delete_branch\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3676,"content_sha256":"b582a2d1ed80bbba250f594744e9efcbdee8c81576f167bdad7c2e4eba268e8b"},{"filename":"scripts/extract-ide-settings.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract information from IDE and editor configuration files\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Parse .editorconfig\nparse_editorconfig() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"{}\"\n return\n fi\n\n local indent_style=\"\"\n local indent_size=\"\"\n local tab_width=\"\"\n local end_of_line=\"\"\n local charset=\"\"\n local trim_trailing=\"\"\n local insert_final=\"\"\n local max_line_length=\"\"\n\n # Parse INI-style format (simplified)\n while IFS= read -r line; do\n # Skip comments and section headers\n [[ \"$line\" =~ ^[[:space:]]*[#\\;] ]] && continue\n [[ \"$line\" =~ ^\\[.*\\] ]] && continue\n\n # Parse key=value\n if [[ \"$line\" =~ ^([a-z_]+)[[:space:]]*=[[:space:]]*(.+)$ ]]; then\n key=\"${BASH_REMATCH[1]}\"\n value=\"${BASH_REMATCH[2]}\"\n\n case \"$key\" in\n indent_style) indent_style=\"$value\" ;;\n indent_size) indent_size=\"$value\" ;;\n tab_width) tab_width=\"$value\" ;;\n end_of_line) end_of_line=\"$value\" ;;\n charset) charset=\"$value\" ;;\n trim_trailing_whitespace) trim_trailing=\"$value\" ;;\n insert_final_newline) insert_final=\"$value\" ;;\n max_line_length) max_line_length=\"$value\" ;;\n esac\n fi\n done \u003c \"$file\"\n\n jq -n \\\n --arg indent_style \"$indent_style\" \\\n --arg indent_size \"$indent_size\" \\\n --arg tab_width \"$tab_width\" \\\n --arg end_of_line \"$end_of_line\" \\\n --arg charset \"$charset\" \\\n --arg trim_trailing \"$trim_trailing\" \\\n --arg insert_final \"$insert_final\" \\\n --arg max_line_length \"$max_line_length\" \\\n '{\n indent_style: $indent_style,\n indent_size: $indent_size,\n tab_width: $tab_width,\n end_of_line: $end_of_line,\n charset: $charset,\n trim_trailing_whitespace: $trim_trailing,\n insert_final_newline: $insert_final,\n max_line_length: $max_line_length\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse VSCode settings.json\nparse_vscode_settings() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"{}\"\n return\n fi\n\n # Extract key settings (remove comments first)\n sed 's|//.*||g' \"$file\" 2>/dev/null | jq '{\n formatter: (.\"editor.defaultFormatter\" // null),\n format_on_save: (.\"editor.formatOnSave\" // null),\n tab_size: (.\"editor.tabSize\" // null),\n insert_spaces: (.\"editor.insertSpaces\" // null),\n eol: (.\"files.eol\" // null),\n trailing_whitespace: (.\"files.trimTrailingWhitespace\" // null),\n final_newline: (.\"files.insertFinalNewline\" // null),\n rulers: (.\"editor.rulers\" // null),\n python_linting: (.\"python.linting.enabled\" // null),\n python_formatter: (.\"python.formatting.provider\" // null),\n typescript_preferences: (.\"typescript.preferences.quoteStyle\" // null),\n eslint_enable: (.\"eslint.enable\" // null),\n prettier_enable: (.\"prettier.enable\" // null)\n } | with_entries(select(.value != null))' 2>/dev/null || echo \"{}\"\n}\n\n# Parse VSCode extensions.json\nparse_vscode_extensions() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Extract recommendations\n jq '.recommendations // []' \"$file\" 2>/dev/null || echo \"[]\"\n}\n\n# Parse VSCode launch.json\nparse_vscode_launch() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Extract configuration names and types\n jq '[.configurations[] | {name: .name, type: .type, request: .request}]' \"$file\" 2>/dev/null || echo \"[]\"\n}\n\n# Check for JetBrains IDE settings\nparse_jetbrains_settings() {\n local idea_dir=\"$1\"\n\n if [ ! -d \"$idea_dir\" ]; then\n echo \"{}\"\n return\n fi\n\n local code_style_file=\"\"\n local inspection_profile=\"\"\n local project_settings=()\n\n # Find code style settings\n if [ -d \"$idea_dir/codeStyles\" ]; then\n code_style_file=$(find \"$idea_dir/codeStyles\" -name \"*.xml\" -type f 2>/dev/null | head -1)\n fi\n\n # Find inspection profile\n if [ -d \"$idea_dir/inspectionProfiles\" ]; then\n inspection_profile=$(find \"$idea_dir/inspectionProfiles\" -name \"*.xml\" -type f 2>/dev/null | head -1)\n fi\n\n # Check for common settings files\n [ -f \"$idea_dir/misc.xml\" ] && project_settings+=(\"misc.xml\")\n [ -f \"$idea_dir/modules.xml\" ] && project_settings+=(\"modules.xml\")\n [ -f \"$idea_dir/vcs.xml\" ] && project_settings+=(\"vcs.xml\")\n [ -f \"$idea_dir/php.xml\" ] && project_settings+=(\"php.xml\")\n [ -f \"$idea_dir/jsLibraryMappings.xml\" ] && project_settings+=(\"jsLibraryMappings.xml\")\n\n local settings_json=\"[]\"\n if [ ${#project_settings[@]} -gt 0 ]; then\n settings_json=$(printf '%s\\n' \"${project_settings[@]}\" | jq -R . | jq -s .)\n fi\n\n jq -n \\\n --arg code_style \"$code_style_file\" \\\n --arg inspection \"$inspection_profile\" \\\n --argjson settings \"$settings_json\" \\\n '{\n code_style_file: $code_style,\n inspection_profile: $inspection,\n project_settings: $settings\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Detect IDEs present\nIDES=()\n[ -f \".editorconfig\" ] && IDES+=(\"editorconfig\")\n[ -d \".vscode\" ] && IDES+=(\"vscode\")\n[ -d \".idea\" ] && IDES+=(\"idea\")\n[ -d \".phpstorm\" ] && IDES+=(\"phpstorm\")\n[ -d \".fleet\" ] && IDES+=(\"fleet\")\n{ [ -d \".vim\" ] || [ -f \".vimrc\" ]; } && IDES+=(\"vim\")\n{ [ -d \".nvim\" ] || [ -f \".nvimrc\" ]; } && IDES+=(\"neovim\")\n[ -f \".sublime-project\" ] && IDES+=(\"sublime\")\n\n# Build IDE list JSON\nif [ ${#IDES[@]} -eq 0 ]; then\n IDES_JSON=\"[]\"\nelse\n IDES_JSON=$(printf '%s\\n' \"${IDES[@]}\" | jq -R . | jq -s .)\nfi\n\n# Extract specific settings\nEDITORCONFIG_SETTINGS=\"{}\"\nVSCODE_SETTINGS=\"{}\"\nVSCODE_EXTENSIONS=\"[]\"\nVSCODE_LAUNCH=\"[]\"\nJETBRAINS_SETTINGS=\"{}\"\n\nif [ -f \".editorconfig\" ]; then\n EDITORCONFIG_SETTINGS=$(parse_editorconfig \".editorconfig\")\nfi\n\nif [ -d \".vscode\" ]; then\n VSCODE_SETTINGS=$(parse_vscode_settings \".vscode/settings.json\")\n VSCODE_EXTENSIONS=$(parse_vscode_extensions \".vscode/extensions.json\")\n VSCODE_LAUNCH=$(parse_vscode_launch \".vscode/launch.json\")\nfi\n\nif [ -d \".idea\" ]; then\n JETBRAINS_SETTINGS=$(parse_jetbrains_settings \".idea\")\nelif [ -d \".phpstorm\" ]; then\n JETBRAINS_SETTINGS=$(parse_jetbrains_settings \".phpstorm\")\nfi\n\n# Build final JSON output\njq -n \\\n --argjson ides \"$IDES_JSON\" \\\n --argjson editorconfig \"$EDITORCONFIG_SETTINGS\" \\\n --argjson vscode_settings \"$VSCODE_SETTINGS\" \\\n --argjson vscode_extensions \"$VSCODE_EXTENSIONS\" \\\n --argjson vscode_launch \"$VSCODE_LAUNCH\" \\\n --argjson jetbrains \"$JETBRAINS_SETTINGS\" \\\n '{\n detected_ides: $ides,\n editorconfig: $editorconfig,\n vscode: {\n settings: $vscode_settings,\n recommended_extensions: $vscode_extensions,\n launch_configurations: $vscode_launch\n },\n jetbrains: $jetbrains\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7214,"content_sha256":"5a58e2edd9d663f02beea5014b64f6ff0361eda483f8c4e365d3614e7dc9899c"},{"filename":"scripts/extract-platform-files.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract information from platform-specific files (.github/, .gitlab/, etc.)\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Helper to extract checklist items from markdown\nextract_checklist() {\n local file=\"$1\"\n local items=()\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Find checkbox lines: - [ ] text or - [x] text\n while IFS= read -r line; do\n # Extract text after checkbox\n text=$(echo \"$line\" | sed -E 's/^[[:space:]]*[-*][[:space:]]*\\[[[:space:]x]\\][[:space:]]*//')\n if [ -n \"$text\" ] && [ ${#text} -gt 3 ]; then\n items+=(\"$text\")\n fi\n done \u003c \u003c(grep -E '^\\s*[-*]\\s*\\[[[:space:]x]\\]' \"$file\" 2>/dev/null || true)\n\n if [ ${#items[@]} -eq 0 ]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${items[@]}\" | jq -R . | jq -s .\n fi\n}\n\n# Extract required fields from issue templates (YAML front matter)\nextract_issue_template_fields() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Check for YAML form-based template\n if grep -q \"^type:\" \"$file\" 2>/dev/null; then\n # Extract field labels marked as required\n grep -B2 \"required: true\" \"$file\" 2>/dev/null | grep \"label:\" | sed 's/.*label:[[:space:]]*//' | jq -R . | jq -s .\n else\n echo \"[]\"\n fi\n}\n\n# Parse CODEOWNERS file\nparse_codeowners() {\n local file=\"$1\"\n local owners=()\n\n if [ ! -f \"$file\" ]; then\n echo \"[]\"\n return\n fi\n\n # Extract pattern and owners (skip comments and empty lines)\n while IFS= read -r line; do\n # Skip comments and empty lines\n [[ \"$line\" =~ ^[[:space:]]*# ]] && continue\n [[ -z \"${line// }\" ]] && continue\n\n # Parse pattern and owners\n pattern=$(echo \"$line\" | awk '{print $1}')\n owner=$(echo \"$line\" | awk '{$1=\"\"; print}' | sed 's/^[[:space:]]*//')\n\n if [ -n \"$pattern\" ] && [ -n \"$owner\" ]; then\n owners+=(\"{\\\"pattern\\\": \\\"$pattern\\\", \\\"owners\\\": \\\"$owner\\\"}\")\n fi\n done \u003c \"$file\"\n\n if [ ${#owners[@]} -eq 0 ]; then\n echo \"[]\"\n else\n printf '%s\\n' \"${owners[@]}\" | jq -s .\n fi\n}\n\n# Parse dependabot.yml\nparse_dependabot() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"{}\"\n return\n fi\n\n # Extract package ecosystems and schedules\n local ecosystems=()\n local current_eco=\"\"\n local current_schedule=\"\"\n\n while IFS= read -r line; do\n if [[ \"$line\" =~ package-ecosystem:[[:space:]]*\\\"?([^\\\"]+)\\\"? ]]; then\n current_eco=\"${BASH_REMATCH[1]}\"\n elif [[ \"$line\" =~ interval:[[:space:]]*\\\"?([^\\\"]+)\\\"? ]]; then\n current_schedule=\"${BASH_REMATCH[1]}\"\n if [ -n \"$current_eco\" ]; then\n ecosystems+=(\"{\\\"ecosystem\\\": \\\"$current_eco\\\", \\\"schedule\\\": \\\"$current_schedule\\\"}\")\n current_eco=\"\"\n current_schedule=\"\"\n fi\n fi\n done \u003c \"$file\"\n\n if [ ${#ecosystems[@]} -eq 0 ]; then\n echo \"{\\\"updates\\\": []}\"\n else\n printf '%s\\n' \"${ecosystems[@]}\" | jq -s '{updates: .}'\n fi\n}\n\n# Parse renovate.json\nparse_renovate() {\n local file=\"$1\"\n\n if [ ! -f \"$file\" ]; then\n echo \"{}\"\n return\n fi\n\n # Extract key configuration\n jq '{\n extends: .extends,\n schedule: .schedule,\n automerge: .automerge,\n labels: .labels\n }' \"$file\" 2>/dev/null || echo \"{}\"\n}\n\n# Detect platform type\nPLATFORM=\"none\"\nif [ -d \".github\" ]; then\n PLATFORM=\"github\"\nelif [ -d \".gitlab\" ]; then\n PLATFORM=\"gitlab\"\nelif [ -d \".bitbucket\" ]; then\n PLATFORM=\"bitbucket\"\nfi\n\n# Initialize result variables\nPR_TEMPLATE_FILE=\"\"\nPR_CHECKLIST=\"[]\"\nISSUE_TEMPLATES=()\nCODEOWNERS_FILE=\"\"\nCODEOWNERS_RULES=\"[]\"\nDEPENDABOT_FILE=\"\"\nDEPENDABOT_CONFIG=\"{}\"\nRENOVATE_FILE=\"\"\nRENOVATE_CONFIG=\"{}\"\nFUNDING_FILE=\"\"\nFUNDING_SPONSORS=\"[]\"\n\n# GitHub-specific extraction\nif [ \"$PLATFORM\" = \"github\" ]; then\n # PR template\n for f in .github/PULL_REQUEST_TEMPLATE.md .github/pull_request_template.md PULL_REQUEST_TEMPLATE.md; do\n if [ -f \"$f\" ]; then\n PR_TEMPLATE_FILE=\"$f\"\n PR_CHECKLIST=$(extract_checklist \"$f\")\n break\n fi\n done\n\n # Issue templates\n if [ -d \".github/ISSUE_TEMPLATE\" ]; then\n for template in .github/ISSUE_TEMPLATE/*.md .github/ISSUE_TEMPLATE/*.yml .github/ISSUE_TEMPLATE/*.yaml; do\n if [ -f \"$template\" ]; then\n name=$(basename \"$template\" | sed 's/\\.[^.]*$//')\n fields=$(extract_issue_template_fields \"$template\")\n ISSUE_TEMPLATES+=(\"{\\\"name\\\": \\\"$name\\\", \\\"file\\\": \\\"$template\\\", \\\"required_fields\\\": $fields}\")\n fi\n done\n fi\n\n # CODEOWNERS\n for f in .github/CODEOWNERS CODEOWNERS docs/CODEOWNERS; do\n if [ -f \"$f\" ]; then\n CODEOWNERS_FILE=\"$f\"\n CODEOWNERS_RULES=$(parse_codeowners \"$f\")\n break\n fi\n done\n\n # Dependabot\n for f in .github/dependabot.yml .github/dependabot.yaml; do\n if [ -f \"$f\" ]; then\n DEPENDABOT_FILE=\"$f\"\n DEPENDABOT_CONFIG=$(parse_dependabot \"$f\")\n break\n fi\n done\n\n # Renovate (can be in root or .github)\n for f in renovate.json .github/renovate.json renovate.json5 .renovaterc .renovaterc.json; do\n if [ -f \"$f\" ]; then\n RENOVATE_FILE=\"$f\"\n RENOVATE_CONFIG=$(parse_renovate \"$f\")\n break\n fi\n done\n\n # Funding\n if [ -f \".github/FUNDING.yml\" ]; then\n FUNDING_FILE=\".github/FUNDING.yml\"\n # Extract sponsor platforms\n FUNDING_SPONSORS=$(grep -E \"^[a-z_]+:\" \"$FUNDING_FILE\" 2>/dev/null | cut -d: -f1 | jq -R . | jq -s . || echo \"[]\")\n fi\nfi\n\n# GitLab-specific extraction\nif [ \"$PLATFORM\" = \"gitlab\" ]; then\n # MR template\n for f in .gitlab/merge_request_templates/Default.md .gitlab/merge_request_templates/*.md; do\n if [ -f \"$f\" ]; then\n PR_TEMPLATE_FILE=\"$f\"\n PR_CHECKLIST=$(extract_checklist \"$f\")\n break\n fi\n done\n\n # Issue templates\n if [ -d \".gitlab/issue_templates\" ]; then\n for template in .gitlab/issue_templates/*.md; do\n if [ -f \"$template\" ]; then\n name=$(basename \"$template\" .md)\n ISSUE_TEMPLATES+=(\"{\\\"name\\\": \\\"$name\\\", \\\"file\\\": \\\"$template\\\", \\\"required_fields\\\": []}\")\n fi\n done\n fi\n\n # CODEOWNERS\n if [ -f \"CODEOWNERS\" ]; then\n CODEOWNERS_FILE=\"CODEOWNERS\"\n CODEOWNERS_RULES=$(parse_codeowners \"CODEOWNERS\")\n fi\nfi\n\n# Build issue templates JSON\nif [ ${#ISSUE_TEMPLATES[@]} -eq 0 ]; then\n ISSUE_TEMPLATES_JSON=\"[]\"\nelse\n ISSUE_TEMPLATES_JSON=$(printf '%s\\n' \"${ISSUE_TEMPLATES[@]}\" | jq -s .)\nfi\n\n# Build final JSON output\njq -n \\\n --arg platform \"$PLATFORM\" \\\n --arg pr_template \"$PR_TEMPLATE_FILE\" \\\n --argjson pr_checklist \"$PR_CHECKLIST\" \\\n --argjson issue_templates \"$ISSUE_TEMPLATES_JSON\" \\\n --arg codeowners_file \"$CODEOWNERS_FILE\" \\\n --argjson codeowners_rules \"$CODEOWNERS_RULES\" \\\n --arg dependabot_file \"$DEPENDABOT_FILE\" \\\n --argjson dependabot \"$DEPENDABOT_CONFIG\" \\\n --arg renovate_file \"$RENOVATE_FILE\" \\\n --argjson renovate \"$RENOVATE_CONFIG\" \\\n --arg funding_file \"$FUNDING_FILE\" \\\n --argjson funding_sponsors \"$FUNDING_SPONSORS\" \\\n '{\n platform: $platform,\n pull_request: {\n template_file: $pr_template,\n checklist_items: $pr_checklist\n },\n issue_templates: $issue_templates,\n codeowners: {\n file: $codeowners_file,\n rules: $codeowners_rules\n },\n dependency_updates: {\n dependabot: {\n file: $dependabot_file,\n config: $dependabot\n },\n renovate: {\n file: $renovate_file,\n config: $renovate\n }\n },\n funding: {\n file: $funding_file,\n sponsors: $funding_sponsors\n }\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":8192,"content_sha256":"15f43771e032996ef030f90f85b7d3444c969dd2be5855c0ffa5d4f7f7a316ce"},{"filename":"scripts/extract-quality-configs.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Extract detailed settings from quality tool configuration files\n# (linters, formatters, type checkers, etc.)\nset -euo pipefail\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Helper to safely parse YAML values\nyaml_get() {\n local file=\"$1\"\n local key=\"$2\"\n local default=\"${3:-}\"\n\n if [ ! -f \"$file\" ]; then\n echo \"$default\"\n return\n fi\n\n # Simple YAML extraction (handles basic cases)\n grep -E \"^${key}:\" \"$file\" 2>/dev/null | sed \"s/^${key}:[[:space:]]*//\" | head -1 || echo \"$default\"\n}\n\n# Parse golangci-lint config (.golangci.yml or .golangci.yaml)\nparse_golangci() {\n local config_file=\"\"\n for f in .golangci.yml .golangci.yaml; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n local line_length=\"\"\n local enabled_linters=()\n local disabled_linters=()\n\n # Extract line-length from linters-settings.lll\n line_length=$(grep -A5 \"lll:\" \"$config_file\" 2>/dev/null | grep \"line-length:\" | sed 's/.*line-length:[[:space:]]*//' | head -1 || echo \"\")\n\n # Extract enabled linters\n local in_enable=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*enable: ]]; then\n in_enable=true\n continue\n fi\n if [[ \"$line\" =~ ^[[:space:]]*disable: ]] || [[ \"$line\" =~ ^[[:space:]]*[a-z]+: && ! \"$line\" =~ ^[[:space:]]*- ]]; then\n in_enable=false\n fi\n if $in_enable && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*([a-z0-9_-]+) ]]; then\n enabled_linters+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \"$config_file\"\n\n # Extract disabled linters\n local in_disable=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*disable: ]]; then\n in_disable=true\n continue\n fi\n if [[ \"$line\" =~ ^[[:space:]]*enable: ]] || [[ \"$line\" =~ ^[[:space:]]*[a-z]+: && ! \"$line\" =~ ^[[:space:]]*- ]]; then\n in_disable=false\n fi\n if $in_disable && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*([a-z0-9_-]+) ]]; then\n disabled_linters+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \"$config_file\"\n\n # Build JSON arrays\n local enabled_json=\"[]\"\n local disabled_json=\"[]\"\n [ ${#enabled_linters[@]} -gt 0 ] && enabled_json=$(printf '%s\\n' \"${enabled_linters[@]}\" | jq -R . | jq -s .)\n [ ${#disabled_linters[@]} -gt 0 ] && disabled_json=$(printf '%s\\n' \"${disabled_linters[@]}\" | jq -R . | jq -s .)\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg line_length \"$line_length\" \\\n --argjson enabled \"$enabled_json\" \\\n --argjson disabled \"$disabled_json\" \\\n '{\n file: $file,\n line_length: $line_length,\n enabled_linters: $enabled,\n disabled_linters: $disabled\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse PHPStan config (phpstan.neon or phpstan.neon.dist)\nparse_phpstan() {\n local config_file=\"\"\n for f in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n local level=\"\"\n local paths=()\n\n # Extract level\n level=$(grep -E \"^[[:space:]]*level:\" \"$config_file\" 2>/dev/null | sed 's/.*level:[[:space:]]*//' | head -1 || echo \"\")\n\n # Extract paths\n local in_paths=false\n while IFS= read -r line; do\n if [[ \"$line\" =~ ^[[:space:]]*paths: ]]; then\n in_paths=true\n continue\n fi\n if [[ \"$line\" =~ ^[[:space:]]*[a-z]+: && ! \"$line\" =~ ^[[:space:]]*- ]]; then\n in_paths=false\n fi\n if $in_paths && [[ \"$line\" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then\n paths+=(\"${BASH_REMATCH[1]}\")\n fi\n done \u003c \"$config_file\"\n\n local paths_json=\"[]\"\n [ ${#paths[@]} -gt 0 ] && paths_json=$(printf '%s\\n' \"${paths[@]}\" | jq -R . | jq -s .)\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg level \"$level\" \\\n --argjson paths \"$paths_json\" \\\n '{\n file: $file,\n level: $level,\n paths: $paths\n } | with_entries(select(.value != \"\" and .value != []))'\n}\n\n# Parse ESLint config\nparse_eslint() {\n local config_file=\"\"\n for f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n local extends=\"\"\n local plugins=\"\"\n\n # For JSON config files\n if [[ \"$config_file\" == *.json ]] || [[ \"$config_file\" == .eslintrc ]]; then\n extends=$(jq -r '.extends // [] | if type == \"array\" then . else [.] end | join(\", \")' \"$config_file\" 2>/dev/null || echo \"\")\n plugins=$(jq -r '.plugins // [] | join(\", \")' \"$config_file\" 2>/dev/null || echo \"\")\n fi\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg extends \"$extends\" \\\n --arg plugins \"$plugins\" \\\n '{\n file: $file,\n extends: $extends,\n plugins: $plugins\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse Prettier config\nparse_prettier() {\n local config_file=\"\"\n for f in .prettierrc .prettierrc.json .prettierrc.yml .prettierrc.yaml .prettierrc.js .prettierrc.cjs prettier.config.js prettier.config.cjs; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n local print_width=\"\"\n local tab_width=\"\"\n local use_tabs=\"\"\n local semi=\"\"\n local single_quote=\"\"\n\n # For JSON config files\n if [[ \"$config_file\" == *.json ]] || [[ \"$config_file\" == .prettierrc ]]; then\n print_width=$(jq -r '.printWidth // \"\"' \"$config_file\" 2>/dev/null || echo \"\")\n tab_width=$(jq -r '.tabWidth // \"\"' \"$config_file\" 2>/dev/null || echo \"\")\n use_tabs=$(jq -r '.useTabs // \"\"' \"$config_file\" 2>/dev/null || echo \"\")\n semi=$(jq -r '.semi // \"\"' \"$config_file\" 2>/dev/null || echo \"\")\n single_quote=$(jq -r '.singleQuote // \"\"' \"$config_file\" 2>/dev/null || echo \"\")\n fi\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg print_width \"$print_width\" \\\n --arg tab_width \"$tab_width\" \\\n --arg use_tabs \"$use_tabs\" \\\n --arg semi \"$semi\" \\\n --arg single_quote \"$single_quote\" \\\n '{\n file: $file,\n print_width: $print_width,\n tab_width: $tab_width,\n use_tabs: $use_tabs,\n semi: $semi,\n single_quote: $single_quote\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse TypeScript config (tsconfig.json)\nparse_tsconfig() {\n if [ ! -f \"tsconfig.json\" ]; then\n echo \"{}\"\n return\n fi\n\n local strict=\"\"\n local target=\"\"\n local module=\"\"\n local strict_null=\"\"\n\n strict=$(jq -r '.compilerOptions.strict // \"\"' tsconfig.json 2>/dev/null || echo \"\")\n target=$(jq -r '.compilerOptions.target // \"\"' tsconfig.json 2>/dev/null || echo \"\")\n module=$(jq -r '.compilerOptions.module // \"\"' tsconfig.json 2>/dev/null || echo \"\")\n strict_null=$(jq -r '.compilerOptions.strictNullChecks // \"\"' tsconfig.json 2>/dev/null || echo \"\")\n\n jq -n \\\n --arg file \"tsconfig.json\" \\\n --arg strict \"$strict\" \\\n --arg target \"$target\" \\\n --arg module \"$module\" \\\n --arg strict_null \"$strict_null\" \\\n '{\n file: $file,\n strict: $strict,\n target: $target,\n module: $module,\n strict_null_checks: $strict_null\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse Ruff config (ruff.toml or pyproject.toml)\nparse_ruff() {\n local config_file=\"\"\n local line_length=\"\"\n local select_rules=\"\"\n local ignore_rules=\"\"\n\n # Check for standalone ruff.toml\n if [ -f \"ruff.toml\" ]; then\n config_file=\"ruff.toml\"\n line_length=$(grep -E \"^line-length\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n select_rules=$(grep -E \"^select\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n ignore_rules=$(grep -E \"^ignore\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n elif [ -f \"pyproject.toml\" ] && grep -q '\\[tool.ruff\\]' pyproject.toml 2>/dev/null; then\n config_file=\"pyproject.toml\"\n # Extract from [tool.ruff] section\n line_length=$(sed -n '/\\[tool.ruff\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^line-length\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n select_rules=$(sed -n '/\\[tool.ruff\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^select\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n fi\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg line_length \"$line_length\" \\\n --arg select \"$select_rules\" \\\n --arg ignore \"$ignore_rules\" \\\n '{\n file: $file,\n line_length: $line_length,\n select: $select,\n ignore: $ignore\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse mypy config\nparse_mypy() {\n local config_file=\"\"\n local strict=\"\"\n local python_version=\"\"\n\n # Check various config locations\n if [ -f \"mypy.ini\" ]; then\n config_file=\"mypy.ini\"\n strict=$(grep -E \"^strict[[:space:]]*=\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n python_version=$(grep -E \"^python_version[[:space:]]*=\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n elif [ -f \".mypy.ini\" ]; then\n config_file=\".mypy.ini\"\n strict=$(grep -E \"^strict[[:space:]]*=\" \"$config_file\" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n elif [ -f \"pyproject.toml\" ] && grep -q '\\[tool.mypy\\]' pyproject.toml 2>/dev/null; then\n config_file=\"pyproject.toml\"\n strict=$(sed -n '/\\[tool.mypy\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^strict\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n python_version=$(sed -n '/\\[tool.mypy\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^python_version\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n elif [ -f \"setup.cfg\" ] && grep -q '\\[mypy\\]' setup.cfg 2>/dev/null; then\n config_file=\"setup.cfg\"\n strict=$(sed -n '/\\[mypy\\]/,/^\\[/p' setup.cfg 2>/dev/null | grep -E \"^strict[[:space:]]*=\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n fi\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg strict \"$strict\" \\\n --arg python_version \"$python_version\" \\\n '{\n file: $file,\n strict: $strict,\n python_version: $python_version\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse Black config\nparse_black() {\n local config_file=\"\"\n local line_length=\"\"\n local target_version=\"\"\n\n if [ -f \"pyproject.toml\" ] && grep -q '\\[tool.black\\]' pyproject.toml 2>/dev/null; then\n config_file=\"pyproject.toml\"\n line_length=$(sed -n '/\\[tool.black\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^line-length\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n target_version=$(sed -n '/\\[tool.black\\]/,/^\\[/p' pyproject.toml 2>/dev/null | grep -E \"^target-version\" | sed 's/.*=[[:space:]]*//' | head -1 || echo \"\")\n fi\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg line_length \"$line_length\" \\\n --arg target_version \"$target_version\" \\\n '{\n file: $file,\n line_length: $line_length,\n target_version: $target_version\n } | with_entries(select(.value != \"\"))'\n}\n\n# Parse PHP-CS-Fixer config\nparse_php_cs_fixer() {\n local config_file=\"\"\n for f in .php-cs-fixer.php .php-cs-fixer.dist.php .php_cs .php_cs.dist; do\n [ -f \"$f\" ] && config_file=\"$f\" && break\n done\n\n if [ -z \"$config_file\" ]; then\n echo \"{}\"\n return\n fi\n\n # Check for risky rules\n local has_risky=\"false\"\n grep -q \"setRiskyAllowed(true)\" \"$config_file\" 2>/dev/null && has_risky=\"true\"\n\n # Try to extract rule set\n local rule_set=\"\"\n rule_set=$(grep -oE \"@(PSR12|PSR2|Symfony|PhpCsFixer)\" \"$config_file\" 2>/dev/null | head -1 || echo \"\")\n\n jq -n \\\n --arg file \"$config_file\" \\\n --arg rule_set \"$rule_set\" \\\n --argjson risky \"$has_risky\" \\\n '{\n file: $file,\n rule_set: $rule_set,\n risky_allowed: $risky\n } | with_entries(select(.value != \"\" and .value != false))'\n}\n\n# Detect which tools are configured\nTOOLS=()\n[ -f \".golangci.yml\" ] || [ -f \".golangci.yaml\" ] && TOOLS+=(\"golangci-lint\")\n[ -f \"phpstan.neon\" ] || [ -f \"phpstan.neon.dist\" ] || [ -f \"phpstan.dist.neon\" ] && TOOLS+=(\"phpstan\")\nfor f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do\n [ -f \"$f\" ] && TOOLS+=(\"eslint\") && break\ndone\nfor f in .prettierrc .prettierrc.json .prettierrc.yml prettier.config.js; do\n [ -f \"$f\" ] && TOOLS+=(\"prettier\") && break\ndone\n[ -f \"tsconfig.json\" ] && TOOLS+=(\"typescript\")\n{ [ -f \"ruff.toml\" ] || grep -q '\\[tool.ruff\\]' pyproject.toml 2>/dev/null; } && TOOLS+=(\"ruff\")\n{ [ -f \"mypy.ini\" ] || [ -f \".mypy.ini\" ] || grep -q '\\[tool.mypy\\]' pyproject.toml 2>/dev/null || grep -q '\\[mypy\\]' setup.cfg 2>/dev/null; } && TOOLS+=(\"mypy\")\ngrep -q '\\[tool.black\\]' pyproject.toml 2>/dev/null && TOOLS+=(\"black\")\nfor f in .php-cs-fixer.php .php-cs-fixer.dist.php .php_cs .php_cs.dist; do\n [ -f \"$f\" ] && TOOLS+=(\"php-cs-fixer\") && break\ndone\n\n# Build tools list JSON\nif [ ${#TOOLS[@]} -eq 0 ]; then\n TOOLS_JSON=\"[]\"\nelse\n TOOLS_JSON=$(printf '%s\\n' \"${TOOLS[@]}\" | jq -R . | jq -s .)\nfi\n\n# Parse each tool config\nGOLANGCI_CONFIG=$(parse_golangci)\nPHPSTAN_CONFIG=$(parse_phpstan)\nESLINT_CONFIG=$(parse_eslint)\nPRETTIER_CONFIG=$(parse_prettier)\nTSCONFIG=$(parse_tsconfig)\nRUFF_CONFIG=$(parse_ruff)\nMYPY_CONFIG=$(parse_mypy)\nBLACK_CONFIG=$(parse_black)\nPHP_CS_FIXER_CONFIG=$(parse_php_cs_fixer)\n\n# Build final JSON output\njq -n \\\n --argjson tools \"$TOOLS_JSON\" \\\n --argjson golangci \"$GOLANGCI_CONFIG\" \\\n --argjson phpstan \"$PHPSTAN_CONFIG\" \\\n --argjson eslint \"$ESLINT_CONFIG\" \\\n --argjson prettier \"$PRETTIER_CONFIG\" \\\n --argjson typescript \"$TSCONFIG\" \\\n --argjson ruff \"$RUFF_CONFIG\" \\\n --argjson mypy \"$MYPY_CONFIG\" \\\n --argjson black \"$BLACK_CONFIG\" \\\n --argjson php_cs_fixer \"$PHP_CS_FIXER_CONFIG\" \\\n '{\n detected_tools: $tools,\n golangci_lint: $golangci,\n phpstan: $phpstan,\n eslint: $eslint,\n prettier: $prettier,\n typescript: $typescript,\n ruff: $ruff,\n mypy: $mypy,\n black: $black,\n php_cs_fixer: $php_cs_fixer\n }'\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":15223,"content_sha256":"c23bd54b4216b380627e8ca3c1a8e962227f9f9333b91748c82879c377945b9a"},{"filename":"scripts/generate-agents.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Main AGENTS.md generator script\n# Requires: Bash 4.3+ (for nameref variables)\n# shellcheck disable=SC2034 # vars/scope_vars are used via nameref in template functions\nset -euo pipefail\n\n# Check Bash version - we need 4.3+ for nameref variables (local -n)\nif ((BASH_VERSINFO[0] \u003c 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] \u003c 3))); then\n echo \"Error: Bash 4.3+ required (found ${BASH_VERSION}).\" >&2\n echo \"On macOS: brew install bash\" >&2\n echo \"Then run with: /opt/homebrew/bin/bash $0 $*\" >&2\n exit 1\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSKILL_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nTEMPLATE_DIR=\"$SKILL_DIR/assets\"\n\n# Source helper libraries\n# shellcheck disable=SC1091 # libs are shellchecked individually by the same hook\nsource \"$SCRIPT_DIR/lib/template.sh\"\n# shellcheck disable=SC1091\nsource \"$SCRIPT_DIR/lib/summary.sh\"\n# shellcheck disable=SC1091\nsource \"$SCRIPT_DIR/lib/config-root.sh\"\n\n# Default options\nPROJECT_DIR=\"${1:-.}\"\nSTYLE=\"${STYLE:-thin}\"\nDRY_RUN=false\nUPDATE_ONLY=false\nFORCE=false\nVERBOSE=false\nCLAUDE_SHIM=false\nCREATE_SYMLINKS=true\nJSON=false\n\n# Parse flags\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --style=*)\n STYLE=\"${1#*=}\"\n shift\n ;;\n --dry-run)\n DRY_RUN=true\n shift\n ;;\n --json)\n JSON=true\n shift\n ;;\n --update)\n UPDATE_ONLY=true\n shift\n ;;\n --force)\n FORCE=true\n shift\n ;;\n --verbose|-v)\n VERBOSE=true\n shift\n ;;\n --claude-shim)\n CLAUDE_SHIM=true\n shift\n ;;\n --no-symlinks)\n CREATE_SYMLINKS=false\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: generate-agents.sh [PROJECT_DIR] [OPTIONS]\n\nGenerate AGENTS.md files for a project following the public agents.md convention.\n\nOptions:\n --style=thin|verbose Template style (default: thin)\n --dry-run Preview what will be created\n --json Emit the write manifest as JSON (human output suppressed)\n --update Update existing files only\n --force Force regeneration of existing files\n --claude-shim Generate CLAUDE.md that imports AGENTS.md (root only, legacy)\n --no-symlinks Skip creating CLAUDE.md/GEMINI.md symlinks (default: create them)\n --verbose, -v Verbose output\n --help, -h Show this help message\n\nExamples:\n generate-agents.sh . # Generate thin root + scoped files\n generate-agents.sh . --dry-run # Preview changes\n generate-agents.sh . --dry-run --json # Machine-readable plan, nothing written\n generate-agents.sh . --style=verbose # Use verbose root template\n generate-agents.sh . --update # Update existing files\n generate-agents.sh . --no-symlinks # Skip CLAUDE.md/GEMINI.md symlinks\n generate-agents.sh . --claude-shim # Generate CLAUDE.md @import shim instead of symlink\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\n# Validate PROJECT_DIR exists\nif [[ ! -d \"$PROJECT_DIR\" ]]; then\n echo \"Error: Project directory not found: $PROJECT_DIR\" >&2\n exit 1\nfi\n\n# Convert to absolute path before cd (so subsequent script calls work)\nPROJECT_DIR=\"$(cd \"$PROJECT_DIR\" && pwd)\"\n# Physical path of the project, for keeping scoped writes inside it.\nPROJECT_REAL=\"$(cd \"$PROJECT_DIR\" && pwd -P)\"\ncd \"$PROJECT_DIR\"\n\n# --json emits the write manifest instead of the prose, the same way the\n# read-only scripts do it: stdout is parked on fd 3 and the human lines go\n# nowhere, so every existing echo stays untouched.\nif [ \"$JSON\" = true ]; then\n exec 3>&1 1>/dev/null\nfi\n\n# One entry per path the run touches. With --dry-run it is a plan, without it a\n# receipt of what was written -- the dry_run field in the output says which.\nMANIFEST=()\n# emit_op OP KIND PATH [KEY VALUE]\n# op: write | symlink | keep\n# kind: agents-file | compat-file | shim\n# Paths are recorded relative to the project root, so a manifest can be\n# reviewed and compared without carrying one machine's directory layout.\nemit_op() {\n local op=\"$1\" kind=\"$2\" path=\"$3\" key=\"${4:-}\" value=\"${5:-}\"\n local rel=\"${path#\"$PROJECT_DIR\"/}\"\n MANIFEST+=(\"$(jq -nc \\\n --arg op \"$op\" --arg kind \"$kind\" --arg path \"$rel\" \\\n --arg key \"$key\" --arg value \"$value\" \\\n '{op: $op, kind: $kind, path: $path}\n + (if $key == \"\" then {} else {($key): $value} end)')\")\n}\n\n# Initialize summary tracking\ninit_summary\n\nlog() {\n if [ \"$VERBOSE\" = true ]; then\n echo \"[INFO] $*\" >&2\n fi\n}\n\n# A CLAUDE.md/GEMINI.md compatibility file we are allowed to write:\n# absent, a symlink we created (pointing at AGENTS.md), or our own generated\n# \"@AGENTS.md\" import file. Anything else — a regular file, or a symlink\n# pointing at rules the user maintains elsewhere — belongs to the user and is\n# kept unless --force. Without this check \"is a symlink\" was treated the same\n# as \"does not exist\" and a foreign link was silently repointed (#103).\ncompat_file_is_ours() {\n local file=\"$1\"\n if [ -L \"$file\" ]; then\n [ \"$(readlink \"$file\")\" = \"AGENTS.md\" ]\n return\n fi\n [ ! -e \"$file\" ] && return 0\n grep -qxF '@AGENTS.md' \"$file\" 2>/dev/null\n}\n\n# One line saying what was left alone and how to override it. Printed\n# unconditionally: a skip that only shows under --verbose reads as \"nothing\n# happened here\" to everyone else.\nreport_kept_file() {\n local file=\"$1\" label=\"$2\" what\n if [ -L \"$file\" ]; then\n what=\"symlink → $(readlink \"$file\")\"\n else\n what=\"regular file\"\n fi\n emit_op keep compat-file \"$file\" reason \"$what\"\n echo \"⏭️ Kept: $label ($what) — use --force to replace\"\n}\n\nerror() {\n echo \"[ERROR] $*\" >&2\n exit 1\n}\n\n# Detect Node.js package manager for a scope directory\n# Walks up from scope dir to find package.json with lockfile\n# Workspace-aware: checks workspace root for lockfiles if in a monorepo\ndetect_node_package_manager() {\n local scope_dir=\"$1\"\n local search_dir=\"$scope_dir\"\n\n # First, check if we're in a workspace - lockfile will be at workspace root\n local workspace_root\n workspace_root=$(find_node_workspace_root \"$scope_dir\" || true)\n if [ -n \"$workspace_root\" ]; then\n [ -f \"$workspace_root/pnpm-lock.yaml\" ] && echo \"pnpm\" && return\n [ -f \"$workspace_root/yarn.lock\" ] && echo \"yarn\" && return\n [ -f \"$workspace_root/bun.lockb\" ] && echo \"bun\" && return\n [ -f \"$workspace_root/package-lock.json\" ] && echo \"npm\" && return\n fi\n\n # Walk up to find package.json with lockfile\n while [ \"$search_dir\" != \".\" ] && [ \"$search_dir\" != \"/\" ]; do\n if [ -f \"$search_dir/package.json\" ]; then\n # Found package.json, check for lockfiles\n [ -f \"$search_dir/pnpm-lock.yaml\" ] && echo \"pnpm\" && return\n [ -f \"$search_dir/yarn.lock\" ] && echo \"yarn\" && return\n [ -f \"$search_dir/bun.lockb\" ] && echo \"bun\" && return\n [ -f \"$search_dir/package-lock.json\" ] && echo \"npm\" && return\n # No lockfile, default to npm\n echo \"npm\"\n return\n fi\n search_dir=$(dirname \"$search_dir\")\n done\n\n # Check root as last resort\n if [ -f \"package.json\" ]; then\n [ -f \"pnpm-lock.yaml\" ] && echo \"pnpm\" && return\n [ -f \"yarn.lock\" ] && echo \"yarn\" && return\n [ -f \"bun.lockb\" ] && echo \"bun\" && return\n echo \"npm\"\n return\n fi\n\n # Fallback: use from PROJECT_INFO if available\n if [ -n \"${PROJECT_INFO:-}\" ]; then\n local pkg_mgr\n pkg_mgr=$(echo \"$PROJECT_INFO\" | jq -r '.package_manager // \"unknown\"')\n if [ \"$pkg_mgr\" != \"unknown\" ] && [ \"$pkg_mgr\" != \"go\" ] && [ \"$pkg_mgr\" != \"composer\" ]; then\n echo \"$pkg_mgr\"\n return\n fi\n fi\n\n echo \"npm\" # Ultimate fallback\n}\n\n# Detect CSS approach/framework used in project\n# Checks package.json dependencies and config files\ndetect_css_approach() {\n local config_root=\"$1\"\n local pkg_json=\"$config_root/package.json\"\n local approaches=()\n\n # Check package.json dependencies\n if [ -f \"$pkg_json\" ]; then\n local deps\n deps=$(jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' \"$pkg_json\" 2>/dev/null || echo \"\")\n\n # Tailwind CSS\n if echo \"$deps\" | grep -q \"^tailwindcss$\"; then\n approaches+=(\"Tailwind CSS\")\n fi\n\n # styled-components\n if echo \"$deps\" | grep -q \"^styled-components$\"; then\n approaches+=(\"styled-components\")\n fi\n\n # Emotion\n if echo \"$deps\" | grep -q \"^@emotion/\"; then\n approaches+=(\"Emotion\")\n fi\n\n # CSS Modules (indicated by config or common patterns)\n if [ -f \"$config_root/postcss.config.js\" ] || [ -f \"$config_root/postcss.config.mjs\" ]; then\n if ! printf '%s\\n' \"${approaches[@]}\" | grep -q \"Tailwind\"; then\n approaches+=(\"CSS Modules\")\n fi\n fi\n\n # Sass/SCSS\n if echo \"$deps\" | grep -q \"^sass$\"; then\n approaches+=(\"Sass/SCSS\")\n fi\n fi\n\n # Default to CSS Modules if nothing detected but tsconfig exists\n if [ ${#approaches[@]} -eq 0 ]; then\n [ -f \"$config_root/tsconfig.json\" ] && approaches+=(\"CSS Modules\")\n fi\n\n # Join approaches with comma. `local IFS` is function-scoped and does\n # not tamper with the caller's IFS — safe under strict SC2030/SC2031\n # semantics. Suppress opengrep's false positive on the `IFS=` pattern.\n local IFS=', ' # nosemgrep: bash.lang.security.ifs-tampering.ifs-tampering\n echo \"${approaches[*]}\"\n}\n\n# Byte budget enforcement (OpenAI Codex default: 32 KiB for combined instructions)\nBYTE_BUDGET=\"${BYTE_BUDGET:-32768}\"\n\nenforce_byte_budget() {\n local file=\"$1\"\n local budget=\"$2\"\n\n [ ! -f \"$file\" ] && return 0\n\n local size\n size=$(wc -c \u003c \"$file\")\n\n if [ \"$size\" -le \"$budget\" ]; then\n log \"File size: $size bytes (within $budget budget)\"\n return 0\n fi\n\n log \"File exceeds byte budget ($size > $budget), pruning...\"\n\n local content\n content=$(cat \"$file\")\n local pruned=false\n\n # Strategy 1: Reduce golden samples to 5\n if echo \"$content\" | grep -q \"AGENTS-GENERATED:START golden-samples\"; then\n local sample_count\n sample_count=$(echo \"$content\" | sed -n '/AGENTS-GENERATED:START golden-samples/,/AGENTS-GENERATED:END golden-samples/p' | grep -c \"^|\") || sample_count=0\n if [ \"$sample_count\" -gt 7 ]; then # header + separator + 5 data rows = 7\n content=$(echo \"$content\" | awk '\n /AGENTS-GENERATED:START golden-samples/ { in_section=1; count=0; print; next }\n /AGENTS-GENERATED:END golden-samples/ { in_section=0; print; next }\n in_section && /^\\|/ { count++; if (count \u003c= 7) print; next }\n { print }\n ')\n pruned=true\n log \" - Reduced golden samples to 5\"\n fi\n fi\n\n # Strategy 2: Reduce heuristics to 5\n if echo \"$content\" | grep -q \"AGENTS-GENERATED:START heuristics\"; then\n local heuristic_count\n heuristic_count=$(echo \"$content\" | sed -n '/AGENTS-GENERATED:START heuristics/,/AGENTS-GENERATED:END heuristics/p' | grep -c \"^|\") || heuristic_count=0\n if [ \"$heuristic_count\" -gt 7 ]; then # header + separator + 5 data rows = 7\n content=$(echo \"$content\" | awk '\n /AGENTS-GENERATED:START heuristics/ { in_section=1; count=0; print; next }\n /AGENTS-GENERATED:END heuristics/ { in_section=0; print; next }\n in_section && /^\\|/ { count++; if (count \u003c= 7) print; next }\n { print }\n ')\n pruned=true\n log \" - Reduced heuristics to 5\"\n fi\n fi\n\n # Strategy 3: Reduce utilities to 5\n if echo \"$content\" | grep -q \"AGENTS-GENERATED:START utilities\"; then\n local utility_count\n utility_count=$(echo \"$content\" | sed -n '/AGENTS-GENERATED:START utilities/,/AGENTS-GENERATED:END utilities/p' | grep -c \"^|\") || utility_count=0\n if [ \"$utility_count\" -gt 7 ]; then # header + separator + 5 data rows = 7\n content=$(echo \"$content\" | awk '\n /AGENTS-GENERATED:START utilities/ { in_section=1; count=0; print; next }\n /AGENTS-GENERATED:END utilities/ { in_section=0; print; next }\n in_section && /^\\|/ { count++; if (count \u003c= 7) print; next }\n { print }\n ')\n pruned=true\n log \" - Reduced utilities to 5\"\n fi\n fi\n\n # Write pruned content\n if [ \"$pruned\" = true ]; then\n printf '%s\\n' \"$content\" | replace_file \"$file\"\n local new_size\n new_size=$(wc -c \u003c \"$file\")\n echo \"⚠️ Pruned due to size budget ($size → $new_size bytes)\"\n fi\n\n return 0\n}\n\n# Detect project\nlog \"Detecting project type...\"\nPROJECT_INFO=$(\"$SCRIPT_DIR/detect-project.sh\" \"$PROJECT_DIR\")\n[ \"$VERBOSE\" = true ] && echo \"$PROJECT_INFO\" | jq . >&2\n\nLANGUAGE=$(echo \"$PROJECT_INFO\" | jq -r '.language')\nVERSION=$(echo \"$PROJECT_INFO\" | jq -r '.version')\nPROJECT_TYPE=$(echo \"$PROJECT_INFO\" | jq -r '.type')\n\n[ \"$LANGUAGE\" = \"unknown\" ] && error \"Could not detect project language\"\n\n# Detect scopes\nlog \"Detecting scopes...\"\nSCOPES_INFO=$(\"$SCRIPT_DIR/detect-scopes.sh\" \"$PROJECT_DIR\")\n# A scope directory that resolves outside the project (through a symlink) is\n# dropped before anything uses the list: its AGENTS.md and compatibility links\n# would be written there, and the root index would point at it.\noutside_scopes=()\nwhile IFS= read -r scope_path; do\n [ -n \"$scope_path\" ] || continue\n scope_real=\"$(cd \"$PROJECT_DIR/$scope_path\" 2>/dev/null && pwd -P)\" || scope_real=\"\"\n if [[ \"$scope_real\" != \"$PROJECT_REAL\" && \"$scope_real\" != \"$PROJECT_REAL\"/* ]]; then\n outside_scopes+=(\"$scope_path\")\n emit_op keep agents-file \"$PROJECT_DIR/$scope_path/AGENTS.md\" reason \"resolves outside the project\"\n echo \"⚠️ Skipped: $scope_path resolves outside the project\"\n fi\ndone \u003c \u003c(echo \"$SCOPES_INFO\" | jq -r '.scopes[]?.path')\nif [ \"${#outside_scopes[@]}\" -gt 0 ]; then\n SCOPES_INFO=$(echo \"$SCOPES_INFO\" | jq --args '.scopes |= map(select(.path as $p | $ARGS.positional | index($p) | not))' \"${outside_scopes[@]}\")\nfi\n[ \"$VERBOSE\" = true ] && echo \"$SCOPES_INFO\" | jq . >&2\n\n# Map language to stack filter for extract-commands.sh\nget_stack_filter() {\n local lang=\"$1\"\n case \"$lang\" in\n go) echo \"go\" ;;\n php) echo \"php\" ;;\n python) echo \"python\" ;;\n typescript|javascript) echo \"node\" ;;\n *) echo \"auto\" ;;\n esac\n}\n\n# Extract commands\nlog \"Extracting build commands...\"\nPRIMARY_STACK=$(get_stack_filter \"$LANGUAGE\")\nCOMMANDS=$(\"$SCRIPT_DIR/extract-commands.sh\" \"$PROJECT_DIR\" \"$PRIMARY_STACK\")\n[ \"$VERBOSE\" = true ] && echo \"$COMMANDS\" | jq . >&2\n\n# Extract documentation (README, CONTRIBUTING, SECURITY, etc.)\nlog \"Extracting documentation...\"\nDOCS_INFO=$(\"$SCRIPT_DIR/extract-documentation.sh\" \"$PROJECT_DIR\")\n[ \"$VERBOSE\" = true ] && echo \"$DOCS_INFO\" | jq . >&2\n\n# Extract platform files (.github/, .gitlab/, etc.)\nlog \"Extracting platform files...\"\nPLATFORM_INFO=$(\"$SCRIPT_DIR/extract-platform-files.sh\" \"$PROJECT_DIR\")\n[ \"$VERBOSE\" = true ] && echo \"$PLATFORM_INFO\" | jq . >&2\n\n# Extract IDE settings (.editorconfig, .vscode/, etc.)\nlog \"Extracting IDE settings...\"\nIDE_INFO=$(\"$SCRIPT_DIR/extract-ide-settings.sh\" \"$PROJECT_DIR\")\n[ \"$VERBOSE\" = true ] && echo \"$IDE_INFO\" | jq . >&2\n\n# Extract AI agent configs (.cursor/, .claude/, etc.)\nlog \"Extracting AI agent configs...\"\nAGENT_INFO=$(\"$SCRIPT_DIR/extract-agent-configs.sh\" \"$PROJECT_DIR\")\n[ \"$VERBOSE\" = true ] && echo \"$AGENT_INFO\" | jq . >&2\n\n# Generate file map\nlog \"Generating file map...\"\nFILE_MAP=$(\"$SCRIPT_DIR/generate-file-map.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo \"\")\n\n# Detect golden samples\nlog \"Detecting golden samples...\"\nGOLDEN_SAMPLES=$(\"$SCRIPT_DIR/detect-golden-samples.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo \"\")\n\n# Detect utilities\nlog \"Detecting utilities...\"\nUTILITIES_LIST=$(\"$SCRIPT_DIR/detect-utilities.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo \"\")\n\n# Detect heuristics\nlog \"Detecting heuristics...\"\nHEURISTICS=$(\"$SCRIPT_DIR/detect-heuristics.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo \"\")\n\n# Extract quality configs (detailed linter/formatter settings)\nlog \"Extracting quality configs...\"\nQUALITY_CONFIG=$(\"$SCRIPT_DIR/extract-quality-configs.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$QUALITY_CONFIG\" | jq . >&2\n\n# Extract CI commands\nlog \"Extracting CI commands...\"\nCI_INFO=$(\"$SCRIPT_DIR/extract-ci-commands.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$CI_INFO\" | jq . >&2\n\n# Extract CI rules (version matrices, quality gates, coverage thresholds)\nlog \"Extracting CI rules...\"\nCI_RULES=$(\"$SCRIPT_DIR/extract-ci-rules.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$CI_RULES\" | jq . >&2\n\n# Extract GitHub repository settings\nlog \"Extracting GitHub settings...\"\nGITHUB_SETTINGS=$(\"$SCRIPT_DIR/extract-github-settings.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$GITHUB_SETTINGS\" | jq . >&2\n\n# Extract GitHub rulesets\nlog \"Extracting GitHub rulesets...\"\nGITHUB_RULESETS=$(\"$SCRIPT_DIR/extract-github-rulesets.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{\"rulesets\":[],\"merge_queue\":false,\"required_checks\":[],\"signed_commits\":false}')\n[ \"$VERBOSE\" = true ] && echo \"$GITHUB_RULESETS\" | jq . >&2\n\n# Extract ADRs\nlog \"Extracting ADRs...\"\nADR_INFO=$(\"$SCRIPT_DIR/extract-adrs.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{\"adr_count\":0,\"adr_directory\":null,\"adrs\":[]}')\n[ \"$VERBOSE\" = true ] && echo \"$ADR_INFO\" | jq . >&2\n\n# Extract architecture / module boundary rules\nlog \"Extracting architecture rules...\"\nARCH_RULES=$(\"$SCRIPT_DIR/extract-architecture-rules.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$ARCH_RULES\" | jq . >&2\n\n# Determine command source confidence\n# Priority: CI > Makefile > package.json/composer.json > fallback defaults\nget_command_source() {\n local ci_system\n ci_system=$(echo \"$CI_INFO\" | jq -r '.ci_system // \"none\"')\n\n if [ \"$ci_system\" != \"none\" ] && [ \"$ci_system\" != \"null\" ]; then\n local ci_commands\n ci_commands=$(echo \"$CI_INFO\" | jq -r '.github_actions.run_commands // .gitlab_ci.script_commands // [] | length')\n if [ \"$ci_commands\" -gt 0 ]; then\n echo \"CI ($ci_system)\"\n return 0\n fi\n fi\n\n if [ -f \"Makefile\" ]; then\n echo \"Makefile\"\n return 0\n fi\n\n case \"$LANGUAGE\" in\n \"typescript\"|\"javascript\")\n [ -f \"package.json\" ] && echo \"package.json\" && return 0\n ;;\n \"php\")\n [ -f \"composer.json\" ] && echo \"composer.json\" && return 0\n ;;\n \"python\")\n [ -f \"pyproject.toml\" ] && echo \"pyproject.toml\" && return 0\n [ -f \"setup.py\" ] && echo \"setup.py\" && return 0\n ;;\n \"go\")\n [ -f \"go.mod\" ] && echo \"go.mod\" && return 0\n ;;\n esac\n\n echo \"defaults\"\n}\n\nCOMMAND_SOURCE=$(get_command_source)\nlog \"Command source: $COMMAND_SOURCE\"\n\n# Analyze git history for conventions\nlog \"Analyzing git history...\"\nGIT_HISTORY=$(\"$SCRIPT_DIR/analyze-git-history.sh\" \"$PROJECT_DIR\" 2>/dev/null || echo '{}')\n[ \"$VERBOSE\" = true ] && echo \"$GIT_HISTORY\" | jq . >&2\n\n# Helper: Safe jq extraction that filters null values\n# Usage: jq_safe \"$json\" '.path.to.value'\njq_safe() {\n local json=\"$1\"\n local path=\"$2\"\n local result\n result=$(echo \"$json\" | jq -r \"$path // empty | select(. != null and . != \\\"null\\\" and . != \\\"\\\")\")\n echo \"$result\"\n}\n\n# Helper: Build quality standards from quality config\nbuild_quality_standards() {\n local quality_json=\"$1\"\n local standards=\"\"\n\n # Get detected tools\n local tools\n tools=$(echo \"$quality_json\" | jq -r '.detected_tools | if . and . != null then join(\", \") else \"\" end | select(. != \"\")')\n [ -n \"$tools\" ] && standards=\"$standards- Quality tools: $tools\\n\"\n\n # PHPStan level\n local phpstan_level\n phpstan_level=$(jq_safe \"$quality_json\" '.phpstan.level')\n [ -n \"$phpstan_level\" ] && standards=\"$standards- PHPStan level: $phpstan_level (do not lower)\\n\"\n\n # TypeScript strict mode\n local ts_strict\n ts_strict=$(jq_safe \"$quality_json\" '.typescript.strict')\n [ \"$ts_strict\" = \"true\" ] && standards=\"$standards- TypeScript: strict mode enabled\\n\"\n\n # Line length settings\n local line_length\n line_length=$(jq_safe \"$quality_json\" '.golangci_lint.line_length // .prettier.print_width // .black.line_length // .ruff.line_length')\n [ -n \"$line_length\" ] && standards=\"$standards- Line length: $line_length\\n\"\n\n # ESLint extends\n local eslint_extends\n eslint_extends=$(jq_safe \"$quality_json\" '.eslint.extends')\n [ -n \"$eslint_extends\" ] && standards=\"$standards- ESLint: extends $eslint_extends\\n\"\n\n # PHP-CS-Fixer ruleset\n local php_cs_ruleset\n php_cs_ruleset=$(jq_safe \"$quality_json\" '.php_cs_fixer.rule_set')\n [ -n \"$php_cs_ruleset\" ] && standards=\"$standards- PHP-CS-Fixer: $php_cs_ruleset rules\\n\"\n\n # Mypy strict\n local mypy_strict\n mypy_strict=$(jq_safe \"$quality_json\" '.mypy.strict')\n [ \"$mypy_strict\" = \"True\" ] || [ \"$mypy_strict\" = \"true\" ] && standards=\"$standards- Mypy: strict mode enabled\\n\"\n\n # Ruff select rules\n local ruff_select\n ruff_select=$(jq_safe \"$quality_json\" '.ruff.select')\n [ -n \"$ruff_select\" ] && standards=\"$standards- Ruff: $ruff_select\\n\"\n\n # Default if nothing found\n [ -z \"$standards\" ] && standards=\"- Follow project linting and formatting rules\\n- Write tests for new functionality\\n- Keep functions focused and well-documented\\n\"\n\n echo -e \"$standards\"\n}\n\n# Helper: Build CI/Quality Gates section from CI rules\nbuild_ci_rules_section() {\n local ci_json=\"$1\"\n local section=\"\"\n\n local platform\n platform=$(echo \"$ci_json\" | jq -r '.ci_platform // empty')\n [[ -z \"$platform\" ]] && return 0\n\n section=\"## CI/Quality Gates\\n\"\n section=\"${section}> Platform: ${platform}\\n\\n\"\n\n # Version matrix\n local versions_block=\"\"\n local php_versions\n php_versions=$(echo \"$ci_json\" | jq -r '.php_versions // [] | if length > 0 then \"PHP \" + join(\", \") else \"\" end')\n [[ -n \"$php_versions\" ]] && versions_block=\"${versions_block}- ${php_versions}\\n\"\n\n local node_versions\n node_versions=$(echo \"$ci_json\" | jq -r '.node_versions // [] | if length > 0 then \"Node \" + join(\", \") else \"\" end')\n [[ -n \"$node_versions\" ]] && versions_block=\"${versions_block}- ${node_versions}\\n\"\n\n local go_version\n go_version=$(echo \"$ci_json\" | jq -r '.go_version // empty')\n [[ -n \"$go_version\" ]] && versions_block=\"${versions_block}- Go ${go_version}\\n\"\n\n local python_versions\n python_versions=$(echo \"$ci_json\" | jq -r '.python_versions // [] | if length > 0 then \"Python \" + join(\", \") else \"\" end')\n [[ -n \"$python_versions\" ]] && versions_block=\"${versions_block}- ${python_versions}\\n\"\n\n if [[ -n \"$versions_block\" ]]; then\n section=\"${section}### Version Matrix\\n${versions_block}\\n\"\n fi\n\n # Quality gates\n local gates\n gates=$(echo \"$ci_json\" | jq -r '.quality_gates // [] | if length > 0 then .[] else empty end')\n if [[ -n \"$gates\" ]]; then\n section=\"${section}### Quality Gates (must pass before merge)\\n\"\n while IFS= read -r gate; do\n section=\"${section}- \\`${gate}\\`\\n\"\n done \u003c\u003c\u003c \"$gates\"\n section=\"${section}\\n\"\n fi\n\n # Coverage threshold\n local coverage\n coverage=$(echo \"$ci_json\" | jq -r '.coverage_threshold // empty')\n [[ -n \"$coverage\" ]] && section=\"${section}### Coverage\\n- Minimum threshold: ${coverage}\\n\\n\"\n\n # Security workflows\n local sec_wfs\n sec_wfs=$(echo \"$ci_json\" | jq -r '.security_workflows // [] | if length > 0 then .[] else empty end')\n if [[ -n \"$sec_wfs\" ]]; then\n section=\"${section}### Security Workflows\\n\"\n while IFS= read -r wf; do\n section=\"${section}- \\`${wf}\\`\\n\"\n done \u003c\u003c\u003c \"$sec_wfs\"\n section=\"${section}\\n\"\n fi\n\n # Pinned actions\n local pinned\n pinned=$(echo \"$ci_json\" | jq -r '.pinned_actions // empty')\n if [[ \"$pinned\" == \"true\" ]]; then\n section=\"${section}### Constraints\\n- Actions are SHA-pinned — maintain pinning when updating\\n\"\n fi\n\n # Linter configs\n local linters\n linters=$(echo \"$ci_json\" | jq -r '.linter_configs // [] | if length > 0 then .[] else empty end')\n if [[ -n \"$linters\" ]]; then\n section=\"${section}- Linter configs: \"\n local first=true\n while IFS= read -r cfg; do\n if $first; then\n section=\"${section}\\`${cfg}\\`\"\n first=false\n else\n section=\"${section}, \\`${cfg}\\`\"\n fi\n done \u003c\u003c\u003c \"$linters\"\n section=\"${section}\\n\"\n fi\n\n echo -e \"$section\"\n}\n\n# Helper: Build module boundaries section from architecture rules\nbuild_module_boundaries() {\n local arch_json=\"$1\"\n local section=\"\"\n\n # Check if we have any data\n local framework\n framework=$(echo \"$arch_json\" | jq -r '.framework // empty' 2>/dev/null)\n [ -z \"$framework\" ] && return 0\n\n local rule_count\n rule_count=$(echo \"$arch_json\" | jq -r '.rules | length // 0' 2>/dev/null)\n local internal_count\n internal_count=$(echo \"$arch_json\" | jq -r '.internal_packages | length // 0' 2>/dev/null)\n local has_layers\n has_layers=$(echo \"$arch_json\" | jq -r '.layer_definitions | length > 0' 2>/dev/null)\n\n [ \"$rule_count\" = \"0\" ] && [ \"$internal_count\" = \"0\" ] && [ \"$has_layers\" != \"true\" ] && return 0\n\n section=\"## Module Boundaries\\n\"\n section=\"${section}> Source: ${framework}\\n\\n\"\n\n # Internal packages (Go)\n if [ \"$internal_count\" -gt 0 ]; then\n section=\"${section}### Internal Packages (compiler-enforced)\\n\"\n while IFS= read -r pkg; do\n section=\"${section}- \\`${pkg}\\`\\n\"\n done \u003c \u003c(echo \"$arch_json\" | jq -r '.internal_packages[]' 2>/dev/null)\n section=\"${section}\\n\"\n fi\n\n # Layer definitions\n if [ \"$has_layers\" = \"true\" ]; then\n local arch_pattern\n arch_pattern=$(echo \"$arch_json\" | jq -r '.layer_definitions.architecture_pattern // empty' 2>/dev/null)\n if [ -n \"$arch_pattern\" ]; then\n section=\"${section}### Architecture Pattern\\n\"\n section=\"${section}- ${arch_pattern}\\n\\n\"\n fi\n\n # Non-pattern layer definitions (deptrac etc.)\n local layer_keys\n layer_keys=$(echo \"$arch_json\" | jq -r '.layer_definitions | keys[] | select(. != \"architecture_pattern\")' 2>/dev/null)\n if [ -n \"$layer_keys\" ]; then\n section=\"${section}### Layers\\n\"\n while IFS= read -r layer; do\n local values\n values=$(echo \"$arch_json\" | jq -r --arg l \"$layer\" '.layer_definitions[$l] | if type == \"array\" then join(\", \") else tostring end' 2>/dev/null)\n section=\"${section}- **${layer}**: ${values}\\n\"\n done \u003c\u003c\u003c \"$layer_keys\"\n section=\"${section}\\n\"\n fi\n fi\n\n # Dependency rules\n if [ \"$rule_count\" -gt 0 ]; then\n section=\"${section}### Dependency Rules\\n\"\n section=\"${section}| Source | Target | Rule | Reason |\\n\"\n section=\"${section}|--------|--------|------|--------|\\n\"\n while IFS= read -r rule_line; do\n section=\"${section}${rule_line}\\n\"\n done \u003c \u003c(echo \"$arch_json\" | jq -r '.rules[] |\n \"| \" + .source + \" | `\" + .target + \"` | \" + .type + \" | \" + (.reason // \"-\") + \" |\"' 2>/dev/null)\n fi\n\n echo -e \"$section\"\n}\n\n# Helper: Build workflow section from git history\n# build_workflow_info \u003cgit history json> [github settings json]\nbuild_workflow_info() {\n local git_json=\"$1\"\n local workflow=\"\"\n\n # Commit convention\n local convention\n convention=$(echo \"$git_json\" | jq -r '.commit_convention.convention // \"unknown\" | select(. != null)')\n [ -z \"$convention\" ] && convention=\"unknown\"\n local confidence\n confidence=$(echo \"$git_json\" | jq -r '.commit_convention.confidence // 0 | select(. != null)')\n [ -z \"$confidence\" ] && confidence=0\n\n if [ \"$convention\" != \"unknown\" ] && [ \"$confidence\" -gt 30 ]; then\n case \"$convention\" in\n \"conventional-commits\")\n workflow=\"$workflow- Commits: Use Conventional Commits (feat:, fix:, docs:, etc.)\\n\"\n ;;\n \"tag-prefix\")\n workflow=\"$workflow- Commits: Use [TAG] prefix style\\n\"\n ;;\n \"emoji\")\n workflow=\"$workflow- Commits: Use emoji prefixes\\n\"\n ;;\n \"ticket-reference\")\n workflow=\"$workflow- Commits: Include ticket references (JIRA-123, #123)\\n\"\n ;;\n esac\n fi\n\n # Merge strategy. History says what was done; the repository settings, when\n # known, say what is allowed now. A method the settings do not allow is not\n # advice, and when they allow exactly one, that one is.\n local merge_strategy method allowed\n merge_strategy=$(echo \"$git_json\" | jq -r '.merge_strategy.strategy // \"unknown\"')\n case \"$merge_strategy\" in\n \"squash-and-merge\") method=squash ;;\n \"merge-commits\") method=merge ;;\n *) method=\"\" ;;\n esac\n allowed=$(echo \"${2:-{\\}}\" | jq -r '(.merge_strategies // []) | join(\" \")' 2>/dev/null || true)\n if [ -n \"$allowed\" ]; then\n [[ -n \"$method\" && \" $allowed \" != *\" $method \"* ]] && method=\"\"\n [[ -z \"$method\" && \"$allowed\" != *\" \"* ]] && method=\"$allowed\"\n fi\n case \"$method\" in\n squash) workflow=\"$workflow- PRs: Squash and merge\\n\" ;;\n merge) workflow=\"$workflow- PRs: Create merge commits\\n\" ;;\n rebase) workflow=\"$workflow- PRs: Rebase and merge\\n\" ;;\n esac\n\n # Branch naming\n local branch_pattern\n branch_pattern=$(echo \"$git_json\" | jq -r '.branch_naming.pattern // \"unknown\"')\n local uses_feature\n uses_feature=$(echo \"$git_json\" | jq -r '.branch_naming.uses_feature_branches // false')\n\n if [ \"$uses_feature\" = \"true\" ]; then\n workflow=\"$workflow- Branches: feature/, fix/, hotfix/ prefixes\\n\"\n elif [ \"$branch_pattern\" = \"ticket-based\" ]; then\n workflow=\"$workflow- Branches: Include ticket reference in name\\n\"\n fi\n\n # Release pattern\n local release_pattern\n release_pattern=$(echo \"$git_json\" | jq -r '.releases.pattern // \"unknown\"')\n local uses_v\n uses_v=$(echo \"$git_json\" | jq -r '.releases.uses_v_prefix // false')\n\n if [ \"$release_pattern\" = \"semver\" ]; then\n [ \"$uses_v\" = \"true\" ] && workflow=\"$workflow- Releases: Semantic versioning (vX.Y.Z)\\n\" || workflow=\"$workflow- Releases: Semantic versioning (X.Y.Z)\\n\"\n elif [ \"$release_pattern\" = \"calver\" ]; then\n workflow=\"$workflow- Releases: Calendar versioning (YYYY.MM.DD)\\n\"\n fi\n\n # Default branch\n local default_branch\n default_branch=$(echo \"$git_json\" | jq -r '.default_branch // \"main\"')\n [ -n \"$default_branch\" ] && [ \"$default_branch\" != \"null\" ] && workflow=\"$workflow- Default branch: $default_branch\\n\"\n\n echo -e \"$workflow\"\n}\n\n# Generate root AGENTS.md\nROOT_FILE=\"$PROJECT_DIR/AGENTS.md\"\n\n# A symlink counts as existing, dangling or not: [ -f ] follows it, and a\n# dangling one would read as absent and be written at its target.\nif { [ -e \"$ROOT_FILE\" ] || [ -L \"$ROOT_FILE\" ]; } && [ \"$FORCE\" = false ] && [ \"$UPDATE_ONLY\" = false ]; then\n emit_op keep agents-file \"$ROOT_FILE\" reason \"already exists\"\n log \"Root AGENTS.md already exists, skipping (use --force to regenerate)\"\nelif [ \"$DRY_RUN\" = true ]; then\n emit_op write agents-file \"$ROOT_FILE\"\n echo \"[DRY-RUN] Would create/update: $ROOT_FILE\"\nelse\n log \"Generating root AGENTS.md...\"\n\n # Select template\n if [ \"$STYLE\" = \"verbose\" ]; then\n TEMPLATE=\"$TEMPLATE_DIR/root-verbose.md\"\n else\n TEMPLATE=\"$TEMPLATE_DIR/root-thin.md\"\n fi\n\n # Prepare template variables\n declare -A vars\n vars[TIMESTAMP]=$(get_timestamp)\n vars[VERIFIED_TIMESTAMP]=\"never\"\n vars[COMMAND_SOURCE]=\"$COMMAND_SOURCE\"\n vars[LANGUAGE_CONVENTIONS]=$(get_language_conventions \"$LANGUAGE\" \"$VERSION\")\n\n # Command extraction - only set if non-empty (leaves placeholder for row deletion)\n set_if_present() {\n local key=\"$1\"\n local value=\"$2\"\n if [ -n \"$value\" ] && [ \"$value\" != \"null\" ]; then\n vars[$key]=\"$value\"\n fi\n return 0\n }\n set_if_present INSTALL_CMD \"$(echo \"$COMMANDS\" | jq -r '.install // empty')\"\n set_if_present TYPECHECK_CMD \"$(echo \"$COMMANDS\" | jq -r '.typecheck // empty')\"\n set_if_present LINT_CMD \"$(echo \"$COMMANDS\" | jq -r '.lint // empty')\"\n set_if_present FORMAT_CMD \"$(echo \"$COMMANDS\" | jq -r '.format // empty')\"\n set_if_present TEST_CMD \"$(echo \"$COMMANDS\" | jq -r '.test // empty')\"\n set_if_present TEST_SINGLE_CMD \"$(echo \"$COMMANDS\" | jq -r '.test_single // empty')\"\n set_if_present BUILD_CMD \"$(echo \"$COMMANDS\" | jq -r '.build // empty')\"\n\n # Time estimates - check verification JSON first, then use heuristics\n VERIFICATION_JSON=\"$PROJECT_DIR/.agents/command-verification.json\"\n get_verified_time() {\n local pattern=\"$1\"\n local default=\"$2\"\n if [ -f \"$VERIFICATION_JSON\" ]; then\n local duration_ms\n # Try to find command in verification JSON using regex pattern\n duration_ms=$(jq -r --arg pattern \"$pattern\" '.commands | to_entries[] | select(.key | test($pattern; \"i\")) | .value.duration_ms // empty' \"$VERIFICATION_JSON\" 2>/dev/null | head -1)\n if [ -n \"$duration_ms\" ] && [ \"$duration_ms\" != \"null\" ] && [ \"$duration_ms\" -gt 0 ] 2>/dev/null; then\n # Convert ms to human-readable\n if [ \"$duration_ms\" -lt 1000 ]; then\n echo \"~${duration_ms}ms\"\n elif [ \"$duration_ms\" -lt 60000 ]; then\n echo \"~$((duration_ms / 1000))s\"\n else\n echo \"~$((duration_ms / 60000))m\"\n fi\n return\n fi\n fi\n echo \"$default\"\n }\n\n # Check if we have verification data\n if [ -f \"$VERIFICATION_JSON\" ]; then\n verified_at=$(jq -r '.verified_at // empty' \"$VERIFICATION_JSON\" 2>/dev/null | cut -dT -f1)\n if [ -n \"$verified_at\" ]; then\n vars[VERIFIED_TIMESTAMP]=\"$verified_at\"\n vars[VERIFIED_STATUS]=\" (verified ✓)\"\n else\n vars[VERIFIED_TIMESTAMP]=\"never\"\n vars[VERIFIED_STATUS]=\" (unverified)\"\n fi\n else\n vars[VERIFIED_TIMESTAMP]=\"never\"\n vars[VERIFIED_STATUS]=\" (unverified)\"\n fi\n\n vars[TYPECHECK_TIME]=$(get_verified_time \"typecheck\" \"~15s\")\n vars[LINT_TIME]=$(get_verified_time \"lint|cs-fixer|eslint\" \"~10s\")\n vars[FORMAT_TIME]=$(get_verified_time \"format|prettier|black|cs-fixer fix$\" \"~5s\")\n vars[TEST_TIME]=$(get_verified_time \"test|phpunit|jest|pytest\" \"~30s\")\n vars[BUILD_TIME]=$(get_verified_time \"build\" \"~30s\")\n\n # File map\n vars[FILE_MAP]=\"$FILE_MAP\"\n\n # Golden samples, utilities, and heuristics from detection scripts\n vars[GOLDEN_SAMPLES]=\"$GOLDEN_SAMPLES\"\n vars[UTILITIES_LIST]=\"$UTILITIES_LIST\"\n\n # Add workflow conventions from git analysis to heuristics\n workflow_info=$(build_workflow_info \"$GIT_HISTORY\" \"$GITHUB_SETTINGS\")\n workflow_heuristics=\"\"\n # Convert workflow info to heuristic table rows\n if echo \"$workflow_info\" | grep -q \"Commits:\"; then\n commit_convention=$(echo \"$workflow_info\" | grep \"Commits:\" | sed 's/- Commits: //')\n workflow_heuristics=\"${workflow_heuristics}| Committing | $commit_convention |\n\"\n fi\n if echo \"$workflow_info\" | grep -q \"PRs:\"; then\n pr_strategy=$(echo \"$workflow_info\" | grep \"PRs:\" | sed 's/- PRs: //')\n workflow_heuristics=\"${workflow_heuristics}| Merging PRs | $pr_strategy |\n\"\n fi\n if echo \"$workflow_info\" | grep -q \"Branches:\"; then\n branch_convention=$(echo \"$workflow_info\" | grep \"Branches:\" | sed 's/- Branches: //')\n workflow_heuristics=\"${workflow_heuristics}| Creating branches | Use $branch_convention |\n\"\n fi\n\n # Combine detected heuristics with workflow heuristics\n # Ensure no trailing blank lines\n combined_heuristics=\"\"\n if [ -n \"$HEURISTICS\" ]; then\n # Trim blank lines and trailing whitespace\n combined_heuristics=$(printf '%s' \"$HEURISTICS\" | sed '/^[[:space:]]*$/d')\n fi\n if [ -n \"$workflow_heuristics\" ]; then\n # Add newline separator only if both have content\n if [ -n \"$combined_heuristics\" ]; then\n combined_heuristics=\"${combined_heuristics}\n${workflow_heuristics}\"\n else\n combined_heuristics=\"$workflow_heuristics\"\n fi\n fi\n # Remove any trailing newlines\n combined_heuristics=$(printf '%s' \"$combined_heuristics\" | sed '/^[[:space:]]*$/d')\n vars[HEURISTICS]=\"$combined_heuristics\"\n\n # Repository settings (from GitHub API)\n build_repo_settings() {\n local settings_json=\"$1\"\n local available\n available=$(echo \"$settings_json\" | jq -r '.available // false')\n\n [ \"$available\" != \"true\" ] && return 0\n\n local content=\"\"\n local default_branch merge_strategies required_approvals required_checks require_up_to_date\n\n default_branch=$(echo \"$settings_json\" | jq -r '.default_branch')\n merge_strategies=$(echo \"$settings_json\" | jq -r '.merge_strategies | join(\", \")')\n required_approvals=$(echo \"$settings_json\" | jq -r '.required_approvals')\n required_checks=$(echo \"$settings_json\" | jq -r 'if .required_checks | length > 0 then .required_checks | map(\"`\" + . + \"`\") | join(\", \") else \"\" end')\n require_up_to_date=$(echo \"$settings_json\" | jq -r '.require_up_to_date')\n\n content=\"- **Default branch:** \\`$default_branch\\`\\n\"\n [ -n \"$merge_strategies\" ] && content=\"${content}- **Merge strategy:** $merge_strategies\\n\"\n [ \"$required_approvals\" != \"0\" ] && [ \"$required_approvals\" != \"null\" ] && content=\"${content}- **Required approvals:** $required_approvals\\n\"\n [ -n \"$required_checks\" ] && content=\"${content}- **Required checks:** $required_checks\\n\"\n [ \"$require_up_to_date\" = \"true\" ] && content=\"${content}- **Require up-to-date:** yes — rebase before merge\\n\"\n\n printf '%b' \"$content\"\n }\n\n # Append ruleset information to repo settings\n build_ruleset_settings() {\n local rulesets_json=\"$1\"\n local ruleset_count\n ruleset_count=$(echo \"$rulesets_json\" | jq '.rulesets | length')\n\n [ \"$ruleset_count\" -eq 0 ] && return 0\n\n local content=\"\"\n local merge_queue signed_commits required_checks\n\n merge_queue=$(echo \"$rulesets_json\" | jq -r '.merge_queue')\n signed_commits=$(echo \"$rulesets_json\" | jq -r '.signed_commits')\n required_checks=$(echo \"$rulesets_json\" | jq -r 'if .required_checks | length > 0 then .required_checks | map(\"`\" + . + \"`\") | join(\", \") else \"\" end')\n\n [ \"$merge_queue\" = \"true\" ] && content=\"${content}- **Merge queue:** enabled — PRs merge via queue, not direct merge\\n\"\n [ \"$signed_commits\" = \"true\" ] && content=\"${content}- **Signed commits:** required\\n\"\n [ -n \"$required_checks\" ] && content=\"${content}- **Required checks (rulesets):** $required_checks\\n\"\n\n # List active rulesets\n local ruleset_names\n ruleset_names=$(echo \"$rulesets_json\" | jq -r '[.rulesets[].name] | join(\", \")')\n [ -n \"$ruleset_names\" ] && content=\"${content}- **Active rulesets:** $ruleset_names\\n\"\n\n printf '%b' \"$content\"\n }\n vars[REPO_SETTINGS]=$(build_repo_settings \"$GITHUB_SETTINGS\")\n ruleset_settings=$(build_ruleset_settings \"$GITHUB_RULESETS\")\n if [ -n \"$ruleset_settings\" ]; then\n vars[REPO_SETTINGS]=\"${vars[REPO_SETTINGS]}${vars[REPO_SETTINGS]:+

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\\n'}${ruleset_settings}\"\n fi\n\n # Module boundaries (from architecture rules)\n vars[MODULE_BOUNDARIES]=$(build_module_boundaries \"$ARCH_RULES\")\n\n # CI/Quality Gates section (from CI rules)\n vars[CI_RULES_SECTION]=$(build_ci_rules_section \"$CI_RULES\")\n\n # Key Decisions (from ADRs)\n build_key_decisions() {\n local adr_json=\"$1\"\n local adr_count\n adr_count=$(echo \"$adr_json\" | jq -r '.adr_count')\n\n [ \"$adr_count\" -eq 0 ] || [ \"$adr_count\" = \"null\" ] && return 0\n\n local adr_dir content=\"\"\n adr_dir=$(echo \"$adr_json\" | jq -r '.adr_directory')\n\n for i in $(seq 0 $((adr_count - 1))); do\n local title status summary file\n file=$(echo \"$adr_json\" | jq -r \".adrs[$i].file\")\n title=$(echo \"$adr_json\" | jq -r \".adrs[$i].title\")\n status=$(echo \"$adr_json\" | jq -r \".adrs[$i].status\")\n summary=$(echo \"$adr_json\" | jq -r \".adrs[$i].summary\")\n\n # Show status badge only for non-Accepted (Accepted is the norm)\n local status_badge=\"\"\n if [ \"$status\" != \"Accepted\" ] && [ \"$status\" != \"Unknown\" ]; then\n status_badge=\" [$status]\"\n fi\n\n content=\"${content}- **${title}**${status_badge} — ${summary} → [\\`${file}\\`](${adr_dir}/${file})\\n\"\n done\n\n printf '%b' \"$content\"\n }\n vars[KEY_DECISIONS]=$(build_key_decisions \"$ADR_INFO\")\n\n # Contribution rules — detect issue-before-PR, AI disclosure, PR templates\n build_contribution_rules() {\n local rules=\"\"\n\n # Check CONTRIBUTING.md for issue-before-PR\n if [ -f \"CONTRIBUTING.md\" ]; then\n if grep -qi \"open an issue\\|issue first\\|issue before\\|create an issue\" CONTRIBUTING.md 2>/dev/null; then\n rules=\"$rules\\n- **Issue first**: Open an issue and get approval before submitting a PR\"\n fi\n if grep -qi \"AI\\|artificial intelligence\\|copilot\\|generated\\|LLM\\|disclose\" CONTRIBUTING.md 2>/dev/null; then\n rules=\"$rules\\n- **AI disclosure**: This project requires disclosure of AI-assisted contributions in PR descriptions\"\n fi\n fi\n\n # Check PR template for issue linking\n for tmpl in .github/pull_request_template.md .github/PULL_REQUEST_TEMPLATE/*.md; do\n if [ -f \"$tmpl\" ] && grep -qi \"Fixes #\\|Closes #\\|Related issue\\|Issue number\" \"$tmpl\" 2>/dev/null; then\n rules=\"$rules\\n- **Link issues**: PR template requires linking to an issue (Fixes #NNN)\"\n break\n fi\n done\n\n # Check branch protection for linked issues requirement\n local protection\n protection=$(echo \"$GITHUB_SETTINGS\" | jq -r '.required_linked_issues // false' 2>/dev/null)\n if [ \"$protection\" = \"true\" ]; then\n rules=\"$rules\\n- **Linked issues required**: Branch protection requires PRs to reference an issue\"\n fi\n\n [ -n \"$rules\" ] && printf '%b' \"$rules\" || true\n }\n vars[CONTRIBUTION_RULES]=$(build_contribution_rules)\n\n # Codebase state - detect migrations, deprecations\n codebase_state=\"\"\n [ -d \"migrations\" ] || [ -d \"db/migrate\" ] && codebase_state=\"$codebase_state\\n- Database migrations present in migrations/\"\n [ -d \"prisma/migrations\" ] && codebase_state=\"$codebase_state\\n- Prisma migrations present\"\n grep -rq \"DEPRECATED\\|@deprecated\" --include=\"*.ts\" --include=\"*.go\" --include=\"*.php\" --include=\"*.py\" . 2>/dev/null && \\\n codebase_state=\"$codebase_state\\n- Contains deprecated code (grep for @deprecated)\"\n [ -z \"$codebase_state\" ] && codebase_state=\"- No known migrations or tech debt documented\"\n vars[CODEBASE_STATE]=$(echo -e \"$codebase_state\")\n\n # Terminology - leave empty for now, needs manual curation\n vars[TERMINOLOGY]=\"\"\n\n # Scope index\n vars[SCOPE_INDEX]=$(build_scope_index \"$SCOPES_INFO\")\n\n # Verbose template additional vars\n if [ \"$STYLE\" = \"verbose\" ]; then\n # Use extracted documentation data where available\n readme_desc=$(echo \"$DOCS_INFO\" | jq -r '.readme.description // empty')\n if [ -n \"$readme_desc\" ]; then\n vars[PROJECT_DESCRIPTION]=\"$readme_desc\"\n else\n vars[PROJECT_DESCRIPTION]=\"TODO: Add project description\"\n fi\n\n vars[LANGUAGE]=\"$LANGUAGE\"\n vars[VERSION]=\"$VERSION\"\n vars[BUILD_TOOL]=$(echo \"$PROJECT_INFO\" | jq -r '.build_tool')\n vars[FRAMEWORK]=$(echo \"$PROJECT_INFO\" | jq -r '.framework')\n vars[PROJECT_TYPE]=\"$PROJECT_TYPE\"\n vars[BUILD_CMD]=$(echo \"$COMMANDS\" | jq -r '.build')\n\n # Extract quality standards from contributing guidelines AND quality config\n contributing_rules=$(echo \"$DOCS_INFO\" | jq -r '.contributing.code_style // empty')\n quality_standards=$(build_quality_standards \"$QUALITY_CONFIG\")\n if [ -n \"$contributing_rules\" ] && [ \"$contributing_rules\" != \"null\" ]; then\n # Combine contributing rules with detected quality config\n vars[QUALITY_STANDARDS]=\"$contributing_rules\n$quality_standards\"\n else\n vars[QUALITY_STANDARDS]=\"$quality_standards\"\n fi\n\n # Extract security guidelines\n security_policy=$(echo \"$DOCS_INFO\" | jq -r '.security.policy // empty')\n if [ -n \"$security_policy\" ] && [ \"$security_policy\" != \"null\" ]; then\n vars[SECURITY_SPECIFIC]=\"$security_policy\"\n else\n vars[SECURITY_SPECIFIC]=\"- Report vulnerabilities via security@project or SECURITY.md\n- Never commit secrets or credentials\"\n fi\n\n vars[TEST_COVERAGE]=\"40\"\n\n # Try to get test commands from CI info or fall back to detected commands\n test_cmd=$(echo \"$COMMANDS\" | jq -r '.test')\n ci_system=$(echo \"$CI_INFO\" | jq -r '.ci_system // \"none\"')\n\n # Look for specific test commands in CI\n ci_test_cmd=\"\"\n case \"$ci_system\" in\n \"github-actions\")\n ci_test_cmd=$(echo \"$CI_INFO\" | jq -r '.github_actions.run_commands[]? | select(. | test(\"test|phpunit|jest|pytest|go test\"; \"i\"))' 2>/dev/null | head -1)\n ;;\n \"gitlab-ci\")\n ci_test_cmd=$(echo \"$CI_INFO\" | jq -r '.gitlab_ci.script_commands[]? | select(. | test(\"test|phpunit|jest|pytest|go test\"; \"i\"))' 2>/dev/null | head -1)\n ;;\n esac\n\n vars[TEST_FAST_CMD]=\"${test_cmd:-make test}\"\n if [ -n \"$ci_test_cmd\" ]; then\n vars[TEST_FULL_CMD]=\"$ci_test_cmd\"\n else\n vars[TEST_FULL_CMD]=\"${test_cmd:-make test}\"\n fi\n\n # Check if docs exist\n [ -d \"./docs\" ] && vars[ARCHITECTURE_DOC]=\"./docs/architecture.md\" || vars[ARCHITECTURE_DOC]=\"(not available)\"\n [ -d \"./docs\" ] && vars[API_DOC]=\"./docs/api.md\" || vars[API_DOC]=\"(not available)\"\n\n # Use extracted contributing file path or default\n contrib_file=$(echo \"$DOCS_INFO\" | jq -r '.contributing.file // empty')\n if [ -n \"$contrib_file\" ] && [ \"$contrib_file\" != \"null\" ]; then\n vars[CONTRIBUTING_DOC]=\"./$contrib_file\"\n else\n vars[CONTRIBUTING_DOC]=\"./CONTRIBUTING.md\"\n fi\n fi\n\n # Language-specific conflict resolution, never-do rules, and code examples\n case \"$LANGUAGE\" in\n \"go\")\n vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=\"- For Go-specific patterns, defer to language idioms and standard library conventions\"\n vars[LANGUAGE_SPECIFIC_NEVER]=\"- Commit go.sum without go.mod changes\"\n vars[CODE_EXAMPLES]=\"**Good:** \\`if err != nil { return fmt.Errorf(\\\"op failed: %w\\\", err) }\\`\n**Avoid:** \\`if err != nil { panic(err) }\\` or ignoring errors\"\n vars[GOOD_EXAMPLE]=\"\\`\\`\\`go\n// Wrap errors with context\nif err != nil {\n return fmt.Errorf(\\\"failed to process %s: %w\\\", item, err)\n}\n\n// Use structured logging\nslog.Info(\\\"operation completed\\\", \\\"item\\\", item, \\\"duration\\\", elapsed)\n\\`\\`\\`\"\n vars[BAD_EXAMPLE]=\"\\`\\`\\`go\n// Don't panic on recoverable errors\nif err != nil {\n panic(err) // Use return instead\n}\n\n// Don't use fmt.Println for logging\nfmt.Println(\\\"something happened\\\") // Use slog/log package\n\\`\\`\\`\"\n ;;\n \"php\")\n vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=\"- For PHP-specific patterns, follow PSR standards\"\n vars[LANGUAGE_SPECIFIC_NEVER]=\"- Commit composer.lock without composer.json changes\n- Modify core framework files\"\n vars[CODE_EXAMPLES]=\"**Good:** Constructor injection, typed properties, return types\n**Avoid:** Service locator, untyped parameters, \\`@var\\` without types\"\n vars[GOOD_EXAMPLE]=\"\\`\\`\\`php\n// Use constructor injection with typed properties\npublic function __construct(\n private readonly UserRepository \\$userRepository,\n private readonly LoggerInterface \\$logger,\n) {}\n\n// Always use return types and parameter types\npublic function findById(int \\$id): ?User\n{\n return \\$this->userRepository->find(\\$id);\n}\n\\`\\`\\`\"\n vars[BAD_EXAMPLE]=\"\\`\\`\\`php\n// Don't use service locator or globals\n\\$user = Container::get('user.repository')->find(\\$id);\n\n// Don't omit types\npublic function process(\\$data) // Missing types\n{\n return \\$data; // Missing return type\n}\n\\`\\`\\`\"\n ;;\n \"typescript\")\n vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=\"- For TypeScript/JavaScript patterns, follow project eslint/prettier config\"\n vars[LANGUAGE_SPECIFIC_NEVER]=\"- Commit package-lock.json without package.json changes\n- Use any type without justification\"\n vars[CODE_EXAMPLES]=\"**Good:** Strict types, async/await, destructuring\n**Avoid:** \\`any\\` type, callback hell, mutable state in components\"\n vars[GOOD_EXAMPLE]=\"\\`\\`\\`typescript\n// Use explicit types and async/await\nasync function fetchUser(id: string): Promise\u003cUser | null> {\n const response = await api.get\u003cUser>(\\\\\\`/users/\\\\\\${id}\\\\\\`);\n return response.data;\n}\n\n// Use destructuring and const\nconst { name, email } = user;\n\\`\\`\\`\"\n vars[BAD_EXAMPLE]=\"\\`\\`\\`typescript\n// Don't use 'any' without justification\nfunction process(data: any): any { // Type properly\n return data;\n}\n\n// Don't use var or nested callbacks\nvar result; // Use const/let\nfetchData(function(data) { // Use async/await\n processData(data, function(result) { ... });\n});\n\\`\\`\\`\"\n ;;\n \"python\")\n vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=\"- For Python-specific patterns, follow PEP 8 and project tooling (ruff/black)\"\n vars[LANGUAGE_SPECIFIC_NEVER]=\"- Commit requirements.txt without pyproject.toml changes\n- Use print() for logging in production code\"\n vars[CODE_EXAMPLES]=\"**Good:** Type hints, dataclasses, context managers\n**Avoid:** Bare \\`except:\\`, mutable default args, \\`print()\\` for logging\"\n vars[GOOD_EXAMPLE]=\"\\`\\`\\`python\n# Use type hints and dataclasses\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n name: str\n email: str\n\ndef find_user(user_id: int) -> User | None:\n \\\"\\\"\\\"Find user by ID.\\\"\\\"\\\"\n return db.query(User).filter_by(id=user_id).first()\n\\`\\`\\`\"\n vars[BAD_EXAMPLE]=\"\\`\\`\\`python\n# Don't use bare except or mutable defaults\ndef process(items=[]): # Mutable default arg!\n try:\n return do_something(items)\n except: # Too broad, catches KeyboardInterrupt\n pass\n\n# Don't use print() for logging\nprint(f\\\"Processing {item}\\\") # Use logging module\n\\`\\`\\`\"\n ;;\n *)\n vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=\"\"\n vars[LANGUAGE_SPECIFIC_NEVER]=\"\"\n vars[CODE_EXAMPLES]=\"\"\n vars[GOOD_EXAMPLE]=\"TODO: Add language-specific good patterns\"\n vars[BAD_EXAMPLE]=\"TODO: Add language-specific anti-patterns\"\n ;;\n esac\n\n # Render template (smart mode respects --update flag)\n render_template_smart \"$TEMPLATE\" \"$ROOT_FILE\" vars \"$UPDATE_ONLY\"\n\n # Enforce byte budget for agent instruction limits\n enforce_byte_budget \"$ROOT_FILE\" \"$BYTE_BUDGET\"\n\n if [ \"$UPDATE_ONLY\" = true ]; then\n emit_op write agents-file \"$ROOT_FILE\"\n echo \"✅ Updated: $ROOT_FILE\"\n else\n emit_op write agents-file \"$ROOT_FILE\"\n echo \"✅ Created: $ROOT_FILE\"\n fi\nfi\n\n# Generate CLAUDE.md shim if requested\nif [ \"$CLAUDE_SHIM\" = true ]; then\n CLAUDE_FILE=\"$PROJECT_DIR/CLAUDE.md\"\n # -e alone follows a symlink, so a dangling one would count as absent and\n # the write below would land at its target, outside the project.\n if { [ -e \"$CLAUDE_FILE\" ] || [ -L \"$CLAUDE_FILE\" ]; } && [ \"$FORCE\" = false ]; then\n if [ ! -L \"$CLAUDE_FILE\" ] && head -n 1 \"$CLAUDE_FILE\" 2>/dev/null | grep -qF 'Auto-generated shim for Claude Code compatibility'; then\n emit_op keep shim \"$CLAUDE_FILE\" reason \"already exists\"\n log \"CLAUDE.md already exists, skipping (use --force to regenerate)\"\n else\n # Not our shim: say so, as the symlink mode does for a kept file.\n report_kept_file \"$CLAUDE_FILE\" CLAUDE.md\n fi\n elif [ \"$DRY_RUN\" = true ]; then\n emit_op write shim \"$CLAUDE_FILE\"\n echo \"[DRY-RUN] Would create: $CLAUDE_FILE\"\n else\n # replace_file replaces a symlink rather than writing through it.\n replace_file \"$CLAUDE_FILE\" \u003c\u003c 'CLAUDESHIM'\n\u003c!-- Auto-generated shim for Claude Code compatibility -->\n\u003c!-- Source of truth: AGENTS.md -->\n\u003c!-- Re-generate with: generate-agents.sh --claude-shim -->\n\n@import AGENTS.md\n\n\u003c!-- Add Claude-specific overrides below if needed -->\nCLAUDESHIM\n emit_op write shim \"$CLAUDE_FILE\"\n echo \"✅ Created: $CLAUDE_FILE (shim importing AGENTS.md)\"\n fi\nfi\n\n# Auto-detect Claude Code environment and ensure CLAUDE.md symlinks exist\n\n# Create compatibility symlinks if requested (root level)\n# Subdirectory symlinks are created after scoped files are generated (see below).\nif [ \"$CREATE_SYMLINKS\" = true ] && [ \"$CLAUDE_SHIM\" = false ]; then\n for symlink_name in CLAUDE.md GEMINI.md; do\n SYMLINK_FILE=\"$PROJECT_DIR/$symlink_name\"\n if compat_file_is_ours \"$SYMLINK_FILE\"; then\n if [ \"$DRY_RUN\" = true ]; then\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"[DRY-RUN] Would symlink: $SYMLINK_FILE → AGENTS.md\"\n else\n ln -sf AGENTS.md \"$SYMLINK_FILE\"\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"✅ Symlinked: $SYMLINK_FILE → AGENTS.md\"\n fi\n elif [ \"$FORCE\" = true ]; then\n if [ \"$DRY_RUN\" = true ]; then\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"[DRY-RUN] Would replace: $SYMLINK_FILE → AGENTS.md (--force)\"\n else\n rm -f \"$SYMLINK_FILE\"\n ln -s AGENTS.md \"$SYMLINK_FILE\"\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"✅ Replaced: $SYMLINK_FILE → AGENTS.md (--force)\"\n fi\n else\n report_kept_file \"$SYMLINK_FILE\" \"$symlink_name\"\n fi\n done\nfi\n\n# Generate scoped AGENTS.md files\nSCOPE_COUNT=$(echo \"$SCOPES_INFO\" | jq '.scopes | length')\n\nif [ \"$SCOPE_COUNT\" -eq 0 ]; then\n log \"No scopes detected (no directories with sufficient source files)\"\nelse\n log \"Generating $SCOPE_COUNT scoped AGENTS.md files...\"\n\n while read -r scope; do\n SCOPE_PATH=$(echo \"$scope\" | jq -r '.path')\n SCOPE_TYPE=$(echo \"$scope\" | jq -r '.type')\n SCOPE_FILE=\"$PROJECT_DIR/$SCOPE_PATH/AGENTS.md\"\n\n\n if { [ -e \"$SCOPE_FILE\" ] || [ -L \"$SCOPE_FILE\" ]; } && [ \"$FORCE\" = false ] && [ \"$UPDATE_ONLY\" = false ]; then\n emit_op keep agents-file \"$SCOPE_FILE\" reason \"already exists\"\n log \"Scoped AGENTS.md already exists: $SCOPE_PATH, skipping\"\n continue\n fi\n\n if [ \"$DRY_RUN\" = true ]; then\n emit_op write agents-file \"$SCOPE_FILE\"\n echo \"[DRY-RUN] Would create/update: $SCOPE_FILE\"\n continue\n fi\n\n # Select template based on scope type\n SCOPE_TEMPLATE=\"$TEMPLATE_DIR/scoped/$SCOPE_TYPE.md\"\n\n if [ ! -f \"$SCOPE_TEMPLATE\" ]; then\n log \"No template for scope type: $SCOPE_TYPE, skipping $SCOPE_PATH\"\n continue\n fi\n\n # Try to extract commands from scope directory (for monorepos)\n SCOPE_COMMANDS=\"\"\n if [ -f \"$SCOPE_PATH/package.json\" ] || [ -f \"$SCOPE_PATH/composer.json\" ] || [ -f \"$SCOPE_PATH/pyproject.toml\" ] || [ -f \"$SCOPE_PATH/go.mod\" ]; then\n SCOPE_COMMANDS=$(\"$SCRIPT_DIR/extract-commands.sh\" \"$SCOPE_PATH\" 2>/dev/null || echo '')\n log \"Found scope-local config in $SCOPE_PATH\"\n fi\n # Fall back to root commands if scope has no local config\n [ -z \"$SCOPE_COMMANDS\" ] && SCOPE_COMMANDS=\"$COMMANDS\"\n\n # Prepare scoped template variables\n declare -A scope_vars\n scope_vars[TIMESTAMP]=$(get_timestamp)\n scope_vars[SCOPE_NAME]=$(basename \"$SCOPE_PATH\")\n scope_vars[SCOPE_DESCRIPTION]=$(get_scope_description \"$SCOPE_TYPE\")\n scope_vars[FILE_PATH]=\"\u003cfile>\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Helper to only set var if value is non-empty (leaves placeholder for row deletion)\n set_scope_if_present() {\n local key=\"$1\"\n local value=\"$2\"\n if [ -n \"$value\" ] && [ \"$value\" != \"null\" ]; then\n scope_vars[$key]=\"$value\"\n fi\n return 0\n }\n\n # Generate scope-specific file map\n # Usage: generate_scope_file_map \u003cpath> \u003cext1> [ext2] [ext3]...\n generate_scope_file_map() {\n local scope_path=\"$1\"\n shift # Remove first arg, leaving extensions\n local extensions=(\"$@\")\n local result=\"\"\n\n # Build find pattern for multiple extensions\n local find_args=()\n for ext in \"${extensions[@]}\"; do\n if [[ ${#find_args[@]} -gt 0 ]]; then\n find_args+=(\"-o\")\n fi\n find_args+=(\"-name\" \"*.$ext\")\n done\n\n # Find key files (most recently modified, largest, or entry points)\n local files\n files=$(find \"$scope_path\" -maxdepth 2 -type f \\( \"${find_args[@]}\" \\) 2>/dev/null | head -5)\n\n if [ -n \"$files\" ]; then\n result=\"| File | Purpose |\\n|------|---------|\"\n while IFS= read -r file; do\n local rel_path=\"${file#\"$PROJECT_DIR\"/}\"\n # Try to extract purpose from first docblock or comment\n local purpose\n purpose=$(head -20 \"$file\" 2>/dev/null | grep -E '^\\s*(//|#|\\*|/\\*\\*)' | head -1 | sed 's/^[[:space:]]*[/*#]*[[:space:]]*//' | cut -c1-50)\n [ -z \"$purpose\" ] && purpose=\"(add description)\"\n result=\"$result\\n| \\`$rel_path\\` | $purpose |\"\n done \u003c\u003c\u003c \"$files\"\n fi\n echo -e \"$result\"\n }\n\n # Generate scope-specific golden samples\n # Usage: generate_scope_golden_samples \u003cpath> \u003cext1> [ext2] [ext3]...\n generate_scope_golden_samples() {\n local scope_path=\"$1\"\n shift # Remove first arg, leaving extensions\n local extensions=(\"$@\")\n local result=\"\"\n\n # Build find pattern for multiple extensions\n local find_args=()\n for ext in \"${extensions[@]}\"; do\n if [[ ${#find_args[@]} -gt 0 ]]; then\n find_args+=(\"-o\")\n fi\n find_args+=(\"-name\" \"*.$ext\")\n done\n\n # Look for well-documented files with tests\n local sample\n # File names come from the target project: they reach wc as\n # arguments (-exec ... \\;), never inside a shell command string.\n sample=$(find \"$scope_path\" -maxdepth 2 -type f \\( \"${find_args[@]}\" \\) \\\n -exec wc -l {} \\; 2>/dev/null | \\\n grep -v \"^0\" | sort -rn | head -1 | awk '{print $2}')\n\n if [ -n \"$sample\" ] && [ -f \"$sample\" ]; then\n local rel_path=\"${sample#\"$PROJECT_DIR\"/}\"\n result=\"| Pattern | Reference |\\n|---------|-----------|\"\n result=\"$result\\n| Standard implementation | \\`$rel_path\\` |\"\n fi\n echo -e \"$result\"\n }\n\n # Language-specific variables\n case \"$SCOPE_TYPE\" in\n \"backend-go\")\n scope_vars[GO_VERSION]=\"$VERSION\"\n scope_vars[GO_MINOR_VERSION]=$(echo \"$VERSION\" | cut -d. -f2)\n scope_vars[GO_TOOLS]=\"golangci-lint, gofmt\"\n scope_vars[ENV_VARS]=\"See .env.example\"\n set_scope_if_present BUILD_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.build // empty')\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`go mod download\\`\"\n [ -n \"${scope_vars[GO_VERSION]:-}\" ] && [ \"${scope_vars[GO_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[GO_VERSION_LINE]=\"- Go version: ${scope_vars[GO_VERSION]}\"\n [ -n \"${scope_vars[GO_TOOLS]:-}\" ] && \\\n scope_vars[GO_TOOLS_LINE]=\"- Required tools: ${scope_vars[GO_TOOLS]}\"\n [ -n \"${scope_vars[ENV_VARS]:-}\" ] && \\\n scope_vars[ENV_VARS_LINE]=\"- Environment variables: ${scope_vars[ENV_VARS]}\"\n\n # Build whole-line placeholders for commands section\n scope_vars[VET_LINE]=\"- Vet (static analysis): \\`go vet ./...\\`\"\n scope_vars[FORMAT_LINE]=\"- Format: \\`gofmt -w .\\`\"\n scope_vars[LINT_LINE]=\"- Lint: \\`golangci-lint run ./...\\`\"\n scope_vars[TEST_LINE]=\"- Test: \\`go test -v -race ./...\\`\"\n scope_vars[TEST_SINGLE_LINE]=\"- Test specific: \\`go test -v -race -run TestName ./...\\`\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build: \\`${scope_vars[BUILD_CMD]}\\`\"\n\n # Build checklist lines\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`go test -v -race ./...\\`\"\n scope_vars[LINT_CHECKLIST_LINE]=\"- [ ] Lint clean: \\`golangci-lint run ./...\\`\"\n scope_vars[FORMAT_CHECKLIST_LINE]=\"- [ ] Formatted: \\`gofmt -w .\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"go\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"go\")\n ;;\n\n \"backend-php\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n FRAMEWORK=$(echo \"$PROJECT_INFO\" | jq -r '.framework')\n scope_vars[FRAMEWORK]=\"$FRAMEWORK\"\n scope_vars[PHP_EXTENSIONS]=\"json, mbstring, xml\"\n scope_vars[ENV_VARS]=\"See .env.example\"\n scope_vars[PHPSTAN_LEVEL]=\"10\"\n set_scope_if_present BUILD_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.build // empty')\"\n\n if [ \"$FRAMEWORK\" = \"typo3\" ]; then\n scope_vars[FRAMEWORK_CONVENTIONS]=\"- TYPO3-specific: Use dependency injection, follow TYPO3 CGL\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- TYPO3 documentation: https://docs.typo3.org\"\n else\n scope_vars[FRAMEWORK_CONVENTIONS]=\"\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"\"\n fi\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\`\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[FRAMEWORK]:-}\" ] && [ \"${scope_vars[FRAMEWORK]}\" != \"none\" ] && \\\n scope_vars[FRAMEWORK_LINE]=\"- Framework: ${scope_vars[FRAMEWORK]}\"\n [ -n \"${scope_vars[PHP_EXTENSIONS]:-}\" ] && \\\n scope_vars[PHP_EXTENSIONS_LINE]=\"- Required extensions: ${scope_vars[PHP_EXTENSIONS]}\"\n [ -n \"${scope_vars[ENV_VARS]:-}\" ] && \\\n scope_vars[ENV_VARS_LINE]=\"- Environment variables: ${scope_vars[ENV_VARS]}\"\n\n # Build whole-line placeholders for commands section\n scope_vars[TYPECHECK_LINE]=\"- Typecheck: \\`vendor/bin/phpstan analyze --level=${scope_vars[PHPSTAN_LEVEL]}\\`\"\n scope_vars[FORMAT_LINE]=\"- Format: \\`vendor/bin/php-cs-fixer fix\\`\"\n scope_vars[LINT_LINE]=\"- Lint: \\`php -l\\`\"\n scope_vars[TEST_LINE]=\"- Test: \\`vendor/bin/phpunit\\`\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build: \\`${scope_vars[BUILD_CMD]}\\`\"\n\n # Build checklist lines\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`vendor/bin/phpunit\\`\"\n scope_vars[TYPECHECK_CHECKLIST_LINE]=\"- [ ] PHPStan Level ${scope_vars[PHPSTAN_LEVEL]} clean: \\`vendor/bin/phpstan analyze\\`\"\n scope_vars[FORMAT_CHECKLIST_LINE]=\"- [ ] PSR-12 compliant: \\`vendor/bin/php-cs-fixer fix --dry-run\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"typo3-extension\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n TYPO3_VERSION=$(jq -r '.require.\"typo3/cms-core\" // .[\"require-dev\"].\"typo3/cms-core\" // \"^12.4 || ^13.4\"' composer.json 2>/dev/null || echo \"^12.4 || ^13.4\")\n scope_vars[TYPO3_VERSION]=\"$TYPO3_VERSION\"\n scope_vars[PHPSTAN_LEVEL]=\"10\"\n\n # Extract extension key and vendor from composer.json\n EXT_KEY=$(jq -r '.extra.\"typo3/cms\".\"extension-key\" // empty' composer.json 2>/dev/null || echo \"\")\n if [ -z \"$EXT_KEY\" ]; then\n EXT_KEY=$(basename \"$PROJECT_DIR\" | tr '-' '_')\n fi\n scope_vars[EXT_KEY]=\"$EXT_KEY\"\n\n VENDOR=$(jq -r '.name // empty' composer.json 2>/dev/null | cut -d'/' -f1 || echo \"Vendor\")\n scope_vars[VENDOR]=\"$VENDOR\"\n scope_vars[REQUIRED_EXTENSIONS]=\"See ext_emconf.php\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\` or via Extension Manager\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[TYPO3_VERSION]:-}\" ] && \\\n scope_vars[TYPO3_VERSION_LINE]=\"- TYPO3 version: ${scope_vars[TYPO3_VERSION]}\"\n scope_vars[DEV_SETUP_LINE]=\"- Local dev: \\`ddev start && ddev composer install\\`\"\n [ -n \"${scope_vars[REQUIRED_EXTENSIONS]:-}\" ] && \\\n scope_vars[REQUIRED_EXTENSIONS_LINE]=\"- Required extensions: ${scope_vars[REQUIRED_EXTENSIONS]}\"\n\n # Build commands table\n scope_vars[COMMANDS_TABLE]=\"| Task | Command |\n|------|---------|\n| Lint | \\`composer ci:php:lint\\` |\n| CS Fix | \\`composer ci:php:cs-fixer\\` |\n| PHPStan | \\`composer ci:php:stan\\` |\n| Unit tests | \\`composer ci:tests:unit\\` |\n| Functional | \\`composer ci:tests:functional\\` |\n| All CI | \\`composer ci\\` |\"\n\n scope_vars[DDEV_ALTERNATIVE]=\"Alternative with ddev:\n- \\`ddev composer ci:tests:unit\\`\n- \\`ddev exec vendor/bin/phpunit -c Tests/Build/UnitTests.xml\\`\"\n\n # Build checklist lines\n scope_vars[CI_CHECKLIST_LINE]=\"- [ ] \\`composer ci\\` passes (lint, cs-fixer, phpstan, tests)\"\n scope_vars[PHPSTAN_CHECKLIST_LINE]=\"- [ ] PHPStan level ${scope_vars[PHPSTAN_LEVEL]} clean\"\n scope_vars[TYPO3_VERSION_CHECKLIST_LINE]=\"- [ ] Tested on target TYPO3 versions (${scope_vars[TYPO3_VERSION]})\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"typo3-project\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n TYPO3_VERSION=$(jq -r '.require.\"typo3/cms-core\" // \"^12.4 || ^13.4\"' composer.json 2>/dev/null || echo \"^12.4 || ^13.4\")\n scope_vars[TYPO3_VERSION]=\"$TYPO3_VERSION\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\`\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[TYPO3_VERSION]:-}\" ] && \\\n scope_vars[TYPO3_VERSION_LINE]=\"- TYPO3 version: ${scope_vars[TYPO3_VERSION]}\"\n scope_vars[DEV_SETUP_LINE]=\"- Local dev: \\`ddev start && ddev composer install\\`\"\n\n # Detect composer mode\n if [ -f \"public/typo3\" ] || [ -d \"public/typo3\" ]; then\n scope_vars[COMPOSER_MODE_LINE]=\"- Composer mode: enabled (public/ web root)\"\n fi\n\n # Build commands table\n scope_vars[COMMANDS_TABLE]=\"| Task | Command |\n|------|---------|\n| Clear cache | \\`vendor/bin/typo3 cache:flush\\` |\n| Warmup cache | \\`vendor/bin/typo3 cache:warmup\\` |\n| Update DB | \\`vendor/bin/typo3 database:updateschema\\` |\n| List commands | \\`vendor/bin/typo3 list\\` |\"\n\n # Build checklist lines\n scope_vars[CI_CHECKLIST_LINE]=\"- [ ] Site configuration valid\"\n scope_vars[TYPO3_VERSION_CHECKLIST_LINE]=\"- [ ] Tested on TYPO3 ${scope_vars[TYPO3_VERSION]}\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"oro-bundle\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n ORO_VERSION=$(jq -r '.require.\"oro/platform\" // .require.\"oro/commerce\" // .require.\"oro/crm\" // \"^6.0\"' composer.json 2>/dev/null || echo \"^6.0\")\n scope_vars[ORO_VERSION]=\"$ORO_VERSION\"\n scope_vars[PHPSTAN_LEVEL]=\"8\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\`\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[ORO_VERSION]:-}\" ] && \\\n scope_vars[ORO_VERSION_LINE]=\"- Oro version: ${scope_vars[ORO_VERSION]}\"\n scope_vars[DATABASE_LINE]=\"- Database: PostgreSQL (recommended) or MySQL\"\n scope_vars[SETUP_COMMANDS]=\"- Required: \\`bin/console oro:install\\` for fresh setup\n- Cache clear: \\`bin/console cache:clear\\`\n- Assets: \\`bin/console oro:assets:install\\`\"\n\n # Build commands table\n scope_vars[COMMANDS_TABLE]=\"| Task | Command |\n|------|---------|\n| Lint | \\`bin/console lint:yaml src/\\` |\n| CS Fix | \\`vendor/bin/php-cs-fixer fix\\` |\n| PHPStan | \\`vendor/bin/phpstan analyse\\` |\n| Unit tests | \\`vendor/bin/phpunit --testsuite=unit\\` |\n| Functional | \\`vendor/bin/phpunit --testsuite=functional\\` |\n| Behat | \\`vendor/bin/behat\\` |\n| Full CI | \\`bin/console oro:test:all\\` |\"\n\n # Build checklist lines\n scope_vars[CACHE_CHECKLIST_LINE]=\"- [ ] \\`bin/console cache:clear\\` runs without errors\"\n scope_vars[PHPSTAN_CHECKLIST_LINE]=\"- [ ] PHPStan passes at configured level\"\n scope_vars[UNIT_TEST_CHECKLIST_LINE]=\"- [ ] Unit tests pass: \\`vendor/bin/phpunit --testsuite=unit\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"oro-project\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n ORO_VERSION=$(jq -r '.require.\"oro/platform\" // .require.\"oro/commerce\" // .require.\"oro/crm\" // \"^6.0\"' composer.json 2>/dev/null || echo \"^6.0\")\n scope_vars[ORO_VERSION]=\"$ORO_VERSION\"\n scope_vars[PHPSTAN_LEVEL]=\"8\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\`\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[ORO_VERSION]:-}\" ] && \\\n scope_vars[ORO_VERSION_LINE]=\"- Oro version: ${scope_vars[ORO_VERSION]}\"\n scope_vars[DATABASE_LINE]=\"- Database: PostgreSQL (recommended) or MySQL\"\n scope_vars[MESSAGE_QUEUE_LINE]=\"- Message queue: Required for background jobs\"\n\n # Build commands table\n scope_vars[COMMANDS_TABLE]=\"| Task | Command |\n|------|---------|\n| Install | \\`bin/console oro:install\\` |\n| Update platform | \\`bin/console oro:platform:update\\` |\n| Clear cache | \\`bin/console cache:clear\\` |\n| Install assets | \\`bin/console oro:assets:install\\` |\n| Message queue | \\`bin/console oro:message-queue:consume\\` |\n| Run cron | \\`bin/console oro:cron\\` |\n| Unit tests | \\`vendor/bin/phpunit --testsuite=unit\\` |\"\n\n # Build checklist lines\n scope_vars[CACHE_CHECKLIST_LINE]=\"- [ ] \\`bin/console cache:clear\\` runs without errors\"\n scope_vars[PHPSTAN_CHECKLIST_LINE]=\"- [ ] PHPStan passes at configured level\"\n scope_vars[UNIT_TEST_CHECKLIST_LINE]=\"- [ ] Unit tests pass\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"symfony\")\n scope_vars[PHP_VERSION]=\"$VERSION\"\n SYMFONY_VERSION=$(jq -r '.require.\"symfony/framework-bundle\" // .require.\"symfony/symfony\" // \"^7.0\"' composer.json 2>/dev/null || echo \"^7.0\")\n scope_vars[SYMFONY_VERSION]=\"$SYMFONY_VERSION\"\n scope_vars[PHPSTAN_LEVEL]=\"9\"\n scope_vars[HOUSE_RULES]=\"\"\n\n # Build whole-line placeholders for setup section\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer install\\`\"\n [ -n \"${scope_vars[PHP_VERSION]:-}\" ] && [ \"${scope_vars[PHP_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PHP_VERSION_LINE]=\"- PHP version: ${scope_vars[PHP_VERSION]}\"\n [ -n \"${scope_vars[SYMFONY_VERSION]:-}\" ] && \\\n scope_vars[SYMFONY_VERSION_LINE]=\"- Symfony version: ${scope_vars[SYMFONY_VERSION]}\"\n # Detect database from doctrine config\n if [ -f \"config/packages/doctrine.yaml\" ]; then\n scope_vars[DATABASE_LINE]=\"- Database: See config/packages/doctrine.yaml\"\n fi\n\n # Build commands table\n scope_vars[COMMANDS_TABLE]=\"| Task | Command |\n|------|---------|\n| Lint | \\`bin/console lint:yaml config/\\` |\n| CS Fix | \\`vendor/bin/php-cs-fixer fix\\` |\n| PHPStan | \\`vendor/bin/phpstan analyse\\` |\n| Unit tests | \\`vendor/bin/phpunit\\` |\n| Clear cache | \\`bin/console cache:clear\\` |\n| Debug routes | \\`bin/console debug:router\\` |\n| Debug container | \\`bin/console debug:container\\` |\"\n\n # Build checklist lines\n scope_vars[PHPSTAN_CHECKLIST_LINE]=\"- [ ] PHPStan level ${scope_vars[PHPSTAN_LEVEL]} clean\"\n scope_vars[CS_CHECKLIST_LINE]=\"- [ ] Code style clean: \\`vendor/bin/php-cs-fixer fix --dry-run\\`\"\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`vendor/bin/phpunit\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"php\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"php\")\n ;;\n\n \"backend-python\")\n scope_vars[PYTHON_VERSION]=\"$VERSION\"\n scope_vars[PACKAGE_MANAGER]=$(echo \"$PROJECT_INFO\" | jq -r '.package_manager')\n scope_vars[ENV_VARS]=\"See .env or .env.example\"\n # Populate all command variables from scope-local extraction\n set_scope_if_present INSTALL_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.install // empty')\"\n set_scope_if_present TYPECHECK_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.typecheck // empty')\"\n set_scope_if_present LINT_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.lint // empty')\"\n set_scope_if_present FORMAT_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.format // empty')\"\n set_scope_if_present TEST_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test // empty')\"\n set_scope_if_present TEST_SINGLE_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test_single // empty')\"\n set_scope_if_present BUILD_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.build // empty')\"\n set_scope_if_present DEV_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.dev // empty')\"\n\n # Build whole-line placeholders for setup section\n [ -n \"${scope_vars[INSTALL_CMD]:-}\" ] && \\\n scope_vars[INSTALL_LINE]=\"- Install: \\`${scope_vars[INSTALL_CMD]}\\`\"\n [ -n \"${scope_vars[PYTHON_VERSION]:-}\" ] && [ \"${scope_vars[PYTHON_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PYTHON_VERSION_LINE]=\"- Python version: ${scope_vars[PYTHON_VERSION]}\"\n [ -n \"${scope_vars[PACKAGE_MANAGER]:-}\" ] && [ \"${scope_vars[PACKAGE_MANAGER]}\" != \"unknown\" ] && \\\n scope_vars[PACKAGE_MANAGER_LINE]=\"- Package manager: ${scope_vars[PACKAGE_MANAGER]}\"\n [ -n \"${scope_vars[ENV_VARS]:-}\" ] && \\\n scope_vars[ENV_VARS_LINE]=\"- Environment variables: ${scope_vars[ENV_VARS]}\"\n\n # Build whole-line placeholders for commands section\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_LINE]=\"- Typecheck: \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_LINE]=\"- Format: \\`${scope_vars[FORMAT_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_LINE]=\"- Lint: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_LINE]=\"- Test: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build: \\`${scope_vars[BUILD_CMD]}\\`\"\n\n # Build checklist lines\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_CHECKLIST_LINE]=\"- [ ] Type check clean: \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_CHECKLIST_LINE]=\"- [ ] Lint clean: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_CHECKLIST_LINE]=\"- [ ] Formatted: \\`${scope_vars[FORMAT_CMD]}\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"py\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"py\")\n ;;\n\n \"backend-typescript\")\n # CRITICAL: Get Node version from nearest package.json,\n # NOT from root PROJECT_INFO (which may be PHP/Go/Python)\n _node_config_root=$(find_node_config_root \"$SCOPE_PATH\") || _node_config_root=\".\"\n\n # Extract Node-specific metadata from correct config root\n _node_version=$(get_node_version \"$_node_config_root\") || _node_version=\"\"\n scope_vars[NODE_VERSION]=\"$_node_version\"\n\n # Package manager from scope's config root\n scope_vars[PACKAGE_MANAGER]=$(detect_node_package_manager \"$SCOPE_PATH\")\n scope_vars[ENV_VARS]=\"See .env or .env.example\"\n\n # Extract commands using Node-specific extraction from config root\n _node_commands=$(\"$SCRIPT_DIR/extract-commands.sh\" \"$_node_config_root\" node 2>/dev/null || echo '{}')\n\n set_scope_if_present INSTALL_CMD \"$(echo \"$_node_commands\" | jq -r '.install // empty')\"\n set_scope_if_present TYPECHECK_CMD \"$(echo \"$_node_commands\" | jq -r '.typecheck // empty')\"\n set_scope_if_present LINT_CMD \"$(echo \"$_node_commands\" | jq -r '.lint // empty')\"\n set_scope_if_present FORMAT_CMD \"$(echo \"$_node_commands\" | jq -r '.format // empty')\"\n set_scope_if_present TEST_CMD \"$(echo \"$_node_commands\" | jq -r '.test // empty')\"\n set_scope_if_present TEST_SINGLE_CMD \"$(echo \"$_node_commands\" | jq -r '.test_single // empty')\"\n set_scope_if_present BUILD_CMD \"$(echo \"$_node_commands\" | jq -r '.build // empty')\"\n set_scope_if_present DEV_CMD \"$(echo \"$_node_commands\" | jq -r '.dev // empty')\"\n\n # Build whole-line placeholders for setup section\n [ -n \"${scope_vars[INSTALL_CMD]:-}\" ] && \\\n scope_vars[INSTALL_LINE]=\"- Install: \\`${scope_vars[INSTALL_CMD]}\\`\"\n [ -n \"${scope_vars[NODE_VERSION]:-}\" ] && [ \"${scope_vars[NODE_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[NODE_VERSION_LINE]=\"- Node version: ${scope_vars[NODE_VERSION]}\"\n [ -n \"${scope_vars[PACKAGE_MANAGER]:-}\" ] && [ \"${scope_vars[PACKAGE_MANAGER]}\" != \"unknown\" ] && \\\n scope_vars[PACKAGE_MANAGER_LINE]=\"- Package manager: ${scope_vars[PACKAGE_MANAGER]}\"\n [ -n \"${scope_vars[ENV_VARS]:-}\" ] && \\\n scope_vars[ENV_VARS_LINE]=\"- Environment variables: ${scope_vars[ENV_VARS]}\"\n\n # Build whole-line placeholders for commands section\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_LINE]=\"- Typecheck (project-wide): \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_LINE]=\"- Format: \\`${scope_vars[FORMAT_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_LINE]=\"- Lint: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_LINE]=\"- Test: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build: \\`${scope_vars[BUILD_CMD]}\\`\"\n [ -n \"${scope_vars[DEV_CMD]:-}\" ] && \\\n scope_vars[DEV_LINE]=\"- Dev server: \\`${scope_vars[DEV_CMD]}\\`\"\n\n # Build checklist lines\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_CHECKLIST_LINE]=\"- [ ] Type check clean: \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_CHECKLIST_LINE]=\"- [ ] Lint clean: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_CHECKLIST_LINE]=\"- [ ] Formatted: \\`${scope_vars[FORMAT_CMD]}\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"ts\" \"tsx\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"ts\" \"tsx\")\n ;;\n\n \"frontend-typescript\")\n # CRITICAL: Get Node version and framework from nearest package.json,\n # NOT from root PROJECT_INFO (which may be PHP/Go/Python)\n _node_config_root=$(find_node_config_root \"$SCOPE_PATH\") || _node_config_root=\".\"\n\n # Extract Node-specific metadata from correct config root\n _node_version=$(get_node_version \"$_node_config_root\") || _node_version=\"\"\n scope_vars[NODE_VERSION]=\"$_node_version\"\n\n _js_framework=$(get_js_framework \"$_node_config_root\") || _js_framework=\"\"\n FRAMEWORK=\"$_js_framework\"\n scope_vars[FRAMEWORK]=\"$FRAMEWORK\"\n\n # Package manager from scope's config root\n scope_vars[PACKAGE_MANAGER]=$(detect_node_package_manager \"$SCOPE_PATH\")\n scope_vars[ENV_VARS]=\"See .env.example\"\n\n # Extract commands using Node-specific extraction from config root\n _node_commands=$(\"$SCRIPT_DIR/extract-commands.sh\" \"$_node_config_root\" node 2>/dev/null || echo '{}')\n\n set_scope_if_present INSTALL_CMD \"$(echo \"$_node_commands\" | jq -r '.install // empty')\"\n set_scope_if_present TYPECHECK_CMD \"$(echo \"$_node_commands\" | jq -r '.typecheck // empty')\"\n set_scope_if_present LINT_CMD \"$(echo \"$_node_commands\" | jq -r '.lint // empty')\"\n set_scope_if_present FORMAT_CMD \"$(echo \"$_node_commands\" | jq -r '.format // empty')\"\n set_scope_if_present TEST_CMD \"$(echo \"$_node_commands\" | jq -r '.test // empty')\"\n set_scope_if_present BUILD_CMD \"$(echo \"$_node_commands\" | jq -r '.build // empty')\"\n set_scope_if_present DEV_CMD \"$(echo \"$_node_commands\" | jq -r '.dev // empty')\"\n\n # TypeScript strict mode - check, don't assume\n _ts_strict=$(get_ts_strict_mode \"$_node_config_root\") || _ts_strict=\"\"\n scope_vars[TS_STRICT]=\"$_ts_strict\"\n\n # Conditional TS_STRICT_LINE based on actual tsconfig.json\n if [[ \"${scope_vars[TS_STRICT]}\" == \"true\" ]]; then\n scope_vars[TS_STRICT_LINE]=\"- TypeScript strict mode enabled (verified from tsconfig.json)\"\n elif [[ \"${scope_vars[TS_STRICT]}\" == \"false\" ]]; then\n scope_vars[TS_STRICT_LINE]=\"- TypeScript strict mode: disabled (consider enabling)\"\n else\n scope_vars[TS_STRICT_LINE]=\"- Follow tsconfig.json compiler options\"\n fi\n\n # CSS approach detection\n _css_approach=$(detect_css_approach \"$_node_config_root\") || _css_approach=\"\"\n scope_vars[CSS_APPROACH]=\"$_css_approach\"\n\n # CSS approach line (only show if detected)\n [[ -n \"${scope_vars[CSS_APPROACH]:-}\" ]] && \\\n scope_vars[CSS_APPROACH_LINE]=\"- CSS: ${scope_vars[CSS_APPROACH]}\"\n\n # Build whole-line placeholders for commands section\n [ -n \"${scope_vars[INSTALL_CMD]:-}\" ] && \\\n scope_vars[INSTALL_LINE]=\"- Install: \\`${scope_vars[INSTALL_CMD]}\\`\"\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_LINE]=\"- Typecheck: \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_LINE]=\"- Lint: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_LINE]=\"- Format: \\`${scope_vars[FORMAT_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_LINE]=\"- Test: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build: \\`${scope_vars[BUILD_CMD]}\\`\"\n [ -n \"${scope_vars[DEV_CMD]:-}\" ] && \\\n scope_vars[DEV_LINE]=\"- Dev server: \\`${scope_vars[DEV_CMD]}\\`\"\n\n # Framework-specific component style and conventions\n case \"$FRAMEWORK\" in\n \"react\")\n scope_vars[COMPONENT_STYLE_LINE]=\"- Use functional components with hooks\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"- Avoid class components\"\n scope_vars[FRAMEWORK_DOCS]=\"https://react.dev\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- Check React documentation: https://react.dev\"\n ;;\n \"next.js\")\n scope_vars[COMPONENT_STYLE_LINE]=\"- Use functional components with hooks\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"- Use App Router (app/)\n- Server Components by default\"\n scope_vars[FRAMEWORK_DOCS]=\"https://nextjs.org/docs\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- Check Next.js documentation: https://nextjs.org/docs\"\n ;;\n \"vue\")\n scope_vars[COMPONENT_STYLE_LINE]=\"- Use Composition API with script setup\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"- Avoid Options API for new code\"\n scope_vars[FRAMEWORK_DOCS]=\"https://vuejs.org/guide\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- Check Vue documentation: https://vuejs.org/guide\"\n ;;\n \"svelte\")\n scope_vars[COMPONENT_STYLE_LINE]=\"- Use Svelte component syntax\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"\"\n scope_vars[FRAMEWORK_DOCS]=\"https://svelte.dev/docs\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- Check Svelte documentation: https://svelte.dev/docs\"\n ;;\n \"angular\")\n scope_vars[COMPONENT_STYLE_LINE]=\"- Use standalone components\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"- Follow Angular style guide\"\n scope_vars[FRAMEWORK_DOCS]=\"https://angular.dev\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"- Check Angular documentation: https://angular.dev\"\n ;;\n *)\n scope_vars[COMPONENT_STYLE_LINE]=\"\"\n scope_vars[FRAMEWORK_CONVENTIONS]=\"\"\n scope_vars[FRAMEWORK_DOCS]=\"\"\n scope_vars[FRAMEWORK_DOCS_LINE]=\"\"\n ;;\n esac\n\n # Build whole-line placeholders (disappear cleanly when empty)\n [ -n \"${scope_vars[NODE_VERSION]:-}\" ] && [ \"${scope_vars[NODE_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[NODE_VERSION_LINE]=\"- Node version: ${scope_vars[NODE_VERSION]}\"\n [ -n \"${scope_vars[FRAMEWORK]:-}\" ] && [ \"${scope_vars[FRAMEWORK]}\" != \"none\" ] && \\\n scope_vars[FRAMEWORK_LINE]=\"- Framework: ${scope_vars[FRAMEWORK]}\"\n [ -n \"${scope_vars[PACKAGE_MANAGER]:-}\" ] && [ \"${scope_vars[PACKAGE_MANAGER]}\" != \"unknown\" ] && \\\n scope_vars[PACKAGE_MANAGER_LINE]=\"- Package manager: ${scope_vars[PACKAGE_MANAGER]}\"\n [ -n \"${scope_vars[ENV_VARS]:-}\" ] && \\\n scope_vars[ENV_VARS_LINE]=\"- Environment variables: ${scope_vars[ENV_VARS]}\"\n\n # Build checklist lines (only when command exists)\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_CHECKLIST_LINE]=\"- [ ] Tests pass: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[TYPECHECK_CHECKLIST_LINE]=\"- [ ] TypeScript compiles: \\`${scope_vars[TYPECHECK_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_CHECKLIST_LINE]=\"- [ ] Lint clean: \\`${scope_vars[LINT_CMD]}\\`\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[FORMAT_CHECKLIST_LINE]=\"- [ ] Formatted: \\`${scope_vars[FORMAT_CMD]}\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"ts\" \"tsx\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"ts\" \"tsx\")\n ;;\n\n \"cli\")\n scope_vars[LANGUAGE]=\"$LANGUAGE\"\n CLI_FRAMEWORK=\"standard\"\n [ -f \"go.mod\" ] && grep -q \"github.com/spf13/cobra\" go.mod 2>/dev/null && CLI_FRAMEWORK=\"cobra\"\n [ -f \"go.mod\" ] && grep -q \"github.com/urfave/cli\" go.mod 2>/dev/null && CLI_FRAMEWORK=\"urfave/cli\"\n scope_vars[CLI_FRAMEWORK]=\"$CLI_FRAMEWORK\"\n scope_vars[BUILD_OUTPUT_PATH]=\"./bin/\"\n build_cmd=\"$(echo \"$SCOPE_COMMANDS\" | jq -r '.build // empty')\"\n [ -n \"$build_cmd\" ] && scope_vars[SETUP_INSTRUCTIONS]=\"- Build: \\`$build_cmd\\`\"\n set_scope_if_present BUILD_CMD \"$build_cmd\"\n scope_vars[RUN_CMD]=\"./bin/$(basename \"$PROJECT_DIR\")\"\n set_scope_if_present TEST_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test // empty')\"\n set_scope_if_present LINT_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.lint // empty')\"\n\n # Build whole-line placeholders for setup section\n [ -n \"${scope_vars[CLI_FRAMEWORK]:-}\" ] && [ \"${scope_vars[CLI_FRAMEWORK]}\" != \"standard\" ] && \\\n scope_vars[CLI_FRAMEWORK_LINE]=\"- CLI framework: ${scope_vars[CLI_FRAMEWORK]}\"\n [ -n \"${scope_vars[BUILD_OUTPUT_PATH]:-}\" ] && \\\n scope_vars[BUILD_OUTPUT_LINE]=\"- Build output: ${scope_vars[BUILD_OUTPUT_PATH]}\"\n\n # Build whole-line placeholders for commands section\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"- Build CLI: \\`${scope_vars[BUILD_CMD]}\\`\"\n [ -n \"${scope_vars[RUN_CMD]:-}\" ] && \\\n scope_vars[RUN_LINE]=\"- Run CLI: \\`${scope_vars[RUN_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_LINE]=\"- Test: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[LINT_LINE]=\"- Lint: \\`${scope_vars[LINT_CMD]}\\`\"\n\n # Build convention and help lines\n [ -n \"${scope_vars[CLI_FRAMEWORK]:-}\" ] && [ \"${scope_vars[CLI_FRAMEWORK]}\" != \"standard\" ] && \\\n scope_vars[CLI_FRAMEWORK_CONVENTION_LINE]=\"- Use flag parsing library consistently (${scope_vars[CLI_FRAMEWORK]})\"\n [ -n \"${scope_vars[CLI_FRAMEWORK]:-}\" ] && [ \"${scope_vars[CLI_FRAMEWORK]}\" != \"standard\" ] && \\\n scope_vars[CLI_FRAMEWORK_DOCS_LINE]=\"- Review ${scope_vars[CLI_FRAMEWORK]} documentation\"\n\n # Detect language for CLI scope\n cli_ext=\"go\"\n [ -f \"package.json\" ] && cli_ext=\"ts\"\n [ -f \"setup.py\" ] || [ -f \"pyproject.toml\" ] && cli_ext=\"py\"\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"$cli_ext\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"$cli_ext\")\n ;;\n\n\n \"testing\")\n set_scope_if_present TEST_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test // empty')\"\n set_scope_if_present TEST_SINGLE_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test_single // empty')\"\n set_scope_if_present TEST_COVERAGE_CMD \"$(echo \"$SCOPE_COMMANDS\" | jq -r '.test_coverage // empty')\"\n\n # Build whole-line placeholders for commands section\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[TEST_LINE]=\"- Run all tests: \\`${scope_vars[TEST_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_SINGLE_CMD]:-}\" ] && \\\n scope_vars[TEST_SINGLE_LINE]=\"- Run specific test file: \\`${scope_vars[TEST_SINGLE_CMD]}\\`\"\n [ -n \"${scope_vars[TEST_COVERAGE_CMD]:-}\" ] && \\\n scope_vars[TEST_COVERAGE_LINE]=\"- Run with coverage: \\`${scope_vars[TEST_COVERAGE_CMD]}\\`\"\n\n # Detect test file extension\n test_ext=\"php\"\n [ -f \"go.mod\" ] && test_ext=\"go\"\n [ -f \"package.json\" ] && test_ext=\"ts\"\n [ -f \"pyproject.toml\" ] && test_ext=\"py\"\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"$test_ext\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"$test_ext\")\n ;;\n\n \"documentation\")\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"md\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"md\")\n ;;\n\n \"examples\")\n # Detect example file extension\n ex_ext=\"go\"\n [ -f \"package.json\" ] && ex_ext=\"ts\"\n [ -f \"pyproject.toml\" ] && ex_ext=\"py\"\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"$ex_ext\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"$ex_ext\")\n ;;\n\n \"resources\")\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"*\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=\"\"\n ;;\n\n \"claude-code-skill\")\n # Extract plugin info (if plugin.json exists)\n plugin_name=$(jq -r '.name // \"unknown\"' .claude-plugin/plugin.json 2>/dev/null || echo \"unknown\")\n plugin_version=$(jq -r '.version // \"unknown\"' .claude-plugin/plugin.json 2>/dev/null || echo \"unknown\")\n skill_count=$(find skills -maxdepth 2 -name \"SKILL.md\" -type f 2>/dev/null | wc -l)\n\n # Build whole-line placeholders\n if [ -f \".claude-plugin/plugin.json\" ]; then\n scope_vars[PLUGIN_JSON_LINE]=\"- Plugin: $plugin_name v$plugin_version\"\n fi\n scope_vars[SKILLS_LINE]=\"- Skills: $skill_count skill(s) in \\`skills/\\`\"\n if [ -f \"composer.json\" ]; then\n scope_vars[INSTALL_LINE]=\"- Install: \\`composer require $(jq -r '.name // \"vendor/package\"' composer.json 2>/dev/null)\\`\"\n fi\n\n # Build command lines if shell scripts exist\n sh_count=$(find . -maxdepth 5 -name \"*.sh\" -type f 2>/dev/null | wc -l)\n if [ \"$sh_count\" -gt 0 ]; then\n scope_vars[LINT_LINE]=\"- Lint scripts: \\`shellcheck skills/*/scripts/*.sh\\`\"\n scope_vars[LINT_CHECKLIST_LINE]=\"- [ ] ShellCheck passes: \\`shellcheck skills/*/scripts/*.sh\\`\"\n fi\n scope_vars[VALIDATE_LINE]=\"- Validate plugin: \\`jq . .claude-plugin/plugin.json\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"sh\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"md\")\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"docker\")\n # Detect Docker and Compose versions from CI or system\n docker_version=\"latest\"\n compose_version=\"v2\"\n\n # Try to get from CI config\n if [ -f \".github/workflows/docker-build.yml\" ]; then\n docker_version=$(grep -E 'docker.*version' .github/workflows/*.yml 2>/dev/null | head -1 | sed 's/.*: *//' || echo \"latest\")\n fi\n\n # Build whole-line placeholders for setup section\n [ -n \"$docker_version\" ] && [ \"$docker_version\" != \"latest\" ] && \\\n scope_vars[DOCKER_VERSION_LINE]=\"- Docker version: $docker_version\"\n [ -n \"$compose_version\" ] && \\\n scope_vars[COMPOSE_VERSION_LINE]=\"- Compose version: $compose_version\"\n\n # Detect registry from CI or compose files\n registry=\"\"\n if [ -f \"docker-compose.yml\" ]; then\n registry=$(grep -E 'image:' docker-compose.yml 2>/dev/null | head -1 | sed 's/.*image: *//;s/:.*//;s|/.*||' || echo \"\")\n fi\n if [ -z \"$registry\" ] && [ -d \".github/workflows\" ]; then\n registry=$(grep -E 'registry:|ghcr.io|docker.io|quay.io' .github/workflows/*.yml 2>/dev/null | head -1 | sed 's/.*: *//' || echo \"\")\n fi\n [ -n \"$registry\" ] && scope_vars[REGISTRY_LINE]=\"- Registry: $registry\"\n\n # Generate file map for Docker files\n docker_file_map=\"\"\n while IFS= read -r df; do\n [ -z \"$df\" ] && continue\n filename=$(basename \"$df\")\n [ -z \"$docker_file_map\" ] && docker_file_map=\"| File | Purpose |\\n|------|---------|\"\n docker_file_map=\"$docker_file_map\\n| \\`$filename\\` | (add description) |\"\n done \u003c \u003c(find \"$SCOPE_PATH\" -maxdepth 1 -type f \\( -name \"Dockerfile*\" -o -name \"*.dockerfile\" -o -name \"docker-compose*.yml\" -o -name \"compose*.yml\" -o -name \".dockerignore\" \\) 2>/dev/null | sort)\n scope_vars[SCOPE_FILE_MAP]=$(echo -e \"$docker_file_map\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=\"\"\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"github-actions\")\n # Count workflows\n workflow_count=$(find .github/workflows -type f \\( -name \"*.yml\" -o -name \"*.yaml\" \\) 2>/dev/null | wc -l)\n scope_vars[WORKFLOW_COUNT_LINE]=\"- Workflows: $workflow_count workflow file(s)\"\n\n # Check for reusable workflows\n reusable_count=$(grep -l \"workflow_call:\" .github/workflows/*.yml 2>/dev/null | wc -l || true)\n [ \"$reusable_count\" -gt 0 ] && \\\n scope_vars[REUSABLE_WORKFLOWS_LINE]=\"- Reusable workflows: $reusable_count\"\n\n # Generate file map for workflows\n workflow_file_map=\"\"\n while IFS= read -r wf; do\n [ -z \"$wf\" ] && continue\n filename=$(basename \"$wf\")\n # Try to extract workflow name from file\n wf_name=$(grep -m1 \"^name:\" \"$wf\" 2>/dev/null | sed 's/name: *//' || echo \"(add description)\")\n [ -z \"$workflow_file_map\" ] && workflow_file_map=\"| File | Purpose |\\n|------|---------|\"\n workflow_file_map=\"$workflow_file_map\\n| \\`$filename\\` | $wf_name |\"\n done \u003c \u003c(find .github/workflows -maxdepth 1 -type f \\( -name \"*.yml\" -o -name \"*.yaml\" \\) 2>/dev/null | sort)\n scope_vars[SCOPE_FILE_MAP]=$(echo -e \"$workflow_file_map\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=\"\"\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"gitlab-ci\")\n # Count pipeline stages/jobs\n job_count=$(grep -cE \"^[a-zA-Z_-]+:$\" .gitlab-ci.yml 2>/dev/null) || job_count=0\n scope_vars[JOB_COUNT_LINE]=\"- Jobs defined: ~$job_count\"\n\n # Check for includes\n include_count=$(grep -cE \"^ *- \" .gitlab-ci.yml 2>/dev/null) || include_count=0\n [ \"$include_count\" -gt 0 ] && \\\n scope_vars[INCLUDES_LINE]=\"- Includes external files/templates\"\n\n # File map - just the main file and any .gitlab/ directory contents\n gitlab_file_map=\"| File | Purpose |\\n|------|---------|\"\n gitlab_file_map=\"$gitlab_file_map\\n| \\`.gitlab-ci.yml\\` | Main pipeline configuration |\"\n if [ -d \".gitlab\" ]; then\n while IFS= read -r gf; do\n [ -z \"$gf\" ] && continue\n filename=\"${gf#.gitlab/}\"\n gitlab_file_map=\"$gitlab_file_map\\n| \\`.gitlab/$filename\\` | (add description) |\"\n done \u003c \u003c(find .gitlab -type f -name \"*.yml\" 2>/dev/null | sort)\n fi\n scope_vars[SCOPE_FILE_MAP]=$(echo -e \"$gitlab_file_map\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=\"\"\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"concourse\")\n # Count pipeline files\n pipeline_count=$(find . -maxdepth 2 -name \"*pipeline*.yml\" -o -name \"*pipeline*.yaml\" 2>/dev/null | wc -l)\n scope_vars[PIPELINE_COUNT_LINE]=\"- Pipeline files: $pipeline_count\"\n\n # Check for tasks directory\n [ -d \"ci/tasks\" ] || [ -d \"tasks\" ] && \\\n scope_vars[TASKS_LINE]=\"- Tasks directory present\"\n\n # File map\n concourse_file_map=\"| File | Purpose |\\n|------|---------|\"\n while IFS= read -r pf; do\n [ -z \"$pf\" ] && continue\n filename=$(basename \"$pf\")\n concourse_file_map=\"$concourse_file_map\\n| \\`$filename\\` | (add description) |\"\n done \u003c \u003c(find . -maxdepth 2 \\( -name \"*pipeline*.yml\" -o -name \"*pipeline*.yaml\" \\) 2>/dev/null | sort)\n if [ -d \"ci/tasks\" ]; then\n task_count=$(find ci/tasks -name \"*.yml\" 2>/dev/null | wc -l)\n [ \"$task_count\" -gt 0 ] && \\\n concourse_file_map=\"$concourse_file_map\\n| \\`ci/tasks/\\` | $task_count task definition(s) |\"\n fi\n scope_vars[SCOPE_FILE_MAP]=$(echo -e \"$concourse_file_map\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=\"\"\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"python-modern\")\n scope_vars[PYTHON_VERSION]=\"$VERSION\"\n BUILD_TOOL=$(echo \"$PROJECT_INFO\" | jq -r '.build_tool')\n scope_vars[PACKAGE_MANAGER]=\"$BUILD_TOOL\"\n scope_vars[ENV_VARS]=\"See .env or .env.example\"\n\n # Extract commands from detected commands\n scope_vars[INSTALL_CMD]=$(echo \"$COMMANDS\" | jq -r '.install // empty')\n scope_vars[LINT_CMD]=$(echo \"$COMMANDS\" | jq -r '.lint // empty')\n scope_vars[FORMAT_CMD]=$(echo \"$COMMANDS\" | jq -r '.format // empty')\n scope_vars[TEST_CMD]=$(echo \"$COMMANDS\" | jq -r '.test // empty')\n scope_vars[TYPECHECK_CMD]=$(echo \"$COMMANDS\" | jq -r '.typecheck // empty')\n scope_vars[BUILD_CMD]=$(echo \"$COMMANDS\" | jq -r '.build // empty')\n\n # Set ruff/mypy-aware defaults\n case \"$BUILD_TOOL\" in\n \"poetry\")\n [ -z \"${scope_vars[INSTALL_CMD]}\" ] && scope_vars[INSTALL_CMD]=\"poetry install\"\n [ -z \"${scope_vars[LINT_CMD]}\" ] && scope_vars[LINT_CMD]=\"poetry run ruff check .\"\n [ -z \"${scope_vars[FORMAT_CMD]}\" ] && scope_vars[FORMAT_CMD]=\"poetry run ruff format .\"\n [ -z \"${scope_vars[TEST_CMD]}\" ] && scope_vars[TEST_CMD]=\"poetry run pytest\"\n [ -z \"${scope_vars[TYPECHECK_CMD]}\" ] && scope_vars[TYPECHECK_CMD]=\"poetry run mypy .\"\n scope_vars[VENV_CMD]=\"poetry shell\"\n ;;\n \"uv\")\n [ -z \"${scope_vars[INSTALL_CMD]}\" ] && scope_vars[INSTALL_CMD]=\"uv sync\"\n [ -z \"${scope_vars[LINT_CMD]}\" ] && scope_vars[LINT_CMD]=\"uv run ruff check .\"\n [ -z \"${scope_vars[FORMAT_CMD]}\" ] && scope_vars[FORMAT_CMD]=\"uv run ruff format .\"\n [ -z \"${scope_vars[TEST_CMD]}\" ] && scope_vars[TEST_CMD]=\"uv run pytest\"\n [ -z \"${scope_vars[TYPECHECK_CMD]}\" ] && scope_vars[TYPECHECK_CMD]=\"uv run mypy .\"\n scope_vars[VENV_CMD]=\"Managed by uv (auto-creates .venv)\"\n ;;\n \"hatch\")\n [ -z \"${scope_vars[INSTALL_CMD]}\" ] && scope_vars[INSTALL_CMD]=\"hatch env create\"\n [ -z \"${scope_vars[LINT_CMD]}\" ] && scope_vars[LINT_CMD]=\"hatch run lint:check\"\n [ -z \"${scope_vars[FORMAT_CMD]}\" ] && scope_vars[FORMAT_CMD]=\"hatch run lint:fmt\"\n [ -z \"${scope_vars[TEST_CMD]}\" ] && scope_vars[TEST_CMD]=\"hatch run test\"\n [ -z \"${scope_vars[TYPECHECK_CMD]}\" ] && scope_vars[TYPECHECK_CMD]=\"hatch run types:check\"\n scope_vars[VENV_CMD]=\"Managed by hatch\"\n ;;\n *)\n [ -z \"${scope_vars[LINT_CMD]}\" ] && scope_vars[LINT_CMD]=\"ruff check .\"\n [ -z \"${scope_vars[FORMAT_CMD]}\" ] && scope_vars[FORMAT_CMD]=\"ruff format .\"\n [ -z \"${scope_vars[TEST_CMD]}\" ] && scope_vars[TEST_CMD]=\"pytest\"\n [ -z \"${scope_vars[TYPECHECK_CMD]}\" ] && scope_vars[TYPECHECK_CMD]=\"mypy .\"\n scope_vars[VENV_CMD]=\"python -m venv .venv && source .venv/bin/activate\"\n ;;\n esac\n\n # Build whole-line placeholders for setup\n [ -n \"${scope_vars[PYTHON_VERSION]:-}\" ] && [ \"${scope_vars[PYTHON_VERSION]}\" != \"unknown\" ] && \\\n scope_vars[PYTHON_VERSION_LINE]=\"- Python: ${scope_vars[PYTHON_VERSION]}\"\n [ -n \"${scope_vars[PACKAGE_MANAGER]:-}\" ] && [ \"${scope_vars[PACKAGE_MANAGER]}\" != \"unknown\" ] && \\\n scope_vars[PACKAGE_MANAGER_LINE]=\"- Package manager: ${scope_vars[PACKAGE_MANAGER]}\"\n [ -n \"${scope_vars[VENV_CMD]:-}\" ] && \\\n scope_vars[VENV_LINE]=\"- Virtual env: \\`${scope_vars[VENV_CMD]}\\`\"\n scope_vars[ENV_VARS_LINE]=\"- Environment: see \\`.env\\` or \\`.env.example\\`\"\n\n # Build command table lines\n [ -n \"${scope_vars[LINT_CMD]:-}\" ] && \\\n scope_vars[RUFF_CHECK_LINE]=\"| \\`${scope_vars[LINT_CMD]}\\` | Lint (ruff) | ~5s |\"\n [ -n \"${scope_vars[FORMAT_CMD]:-}\" ] && \\\n scope_vars[RUFF_FORMAT_LINE]=\"| \\`${scope_vars[FORMAT_CMD]} --check\\` | Format check | ~3s |\"\n [ -n \"${scope_vars[TYPECHECK_CMD]:-}\" ] && \\\n scope_vars[MYPY_LINE]=\"| \\`${scope_vars[TYPECHECK_CMD]}\\` | Type check (mypy) | ~15s |\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[PYTEST_LINE]=\"| \\`${scope_vars[TEST_CMD]}\\` | Run tests | ~30s |\"\n [ -n \"${scope_vars[TEST_CMD]:-}\" ] && \\\n scope_vars[PYTEST_COV_LINE]=\"| \\`${scope_vars[TEST_CMD]} --cov=src\\` | Tests + coverage | ~45s |\"\n [ -n \"${scope_vars[BUILD_CMD]:-}\" ] && \\\n scope_vars[BUILD_LINE]=\"| \\`${scope_vars[BUILD_CMD]}\\` | Build | ~10s |\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"py\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"py\")\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n\n \"skill-repo\")\n # Extract plugin info (if plugin.json exists)\n plugin_name=$(jq -r '.name // \"unknown\"' .claude-plugin/plugin.json 2>/dev/null || echo \"unknown\")\n plugin_version=$(jq -r '.version // \"unknown\"' .claude-plugin/plugin.json 2>/dev/null || echo \"unknown\")\n skill_count=$(find skills -maxdepth 2 -name \"SKILL.md\" -type f 2>/dev/null | wc -l)\n\n # Build whole-line placeholders\n if [ -f \".claude-plugin/plugin.json\" ]; then\n scope_vars[PLUGIN_JSON_LINE]=\"- Plugin: $plugin_name v$plugin_version\"\n fi\n scope_vars[SKILLS_LINE]=\"- Skills: $skill_count skill(s) in \\`skills/\\`\"\n\n # Build command lines if shell scripts exist\n sh_count=$(find . -maxdepth 5 -name \"*.sh\" -type f 2>/dev/null | wc -l)\n if [ \"$sh_count\" -gt 0 ]; then\n scope_vars[LINT_LINE]=\"- Lint scripts: \\`shellcheck skills/*/scripts/*.sh\\`\"\n scope_vars[SHELLCHECK_LINE]=\"- ShellCheck: \\`shellcheck skills/*/scripts/*.sh\\`\"\n fi\n scope_vars[VALIDATE_LINE]=\"- Validate plugin: \\`jq . .claude-plugin/plugin.json\\`\"\n\n scope_vars[SCOPE_FILE_MAP]=$(generate_scope_file_map \"$SCOPE_PATH\" \"sh\")\n scope_vars[SCOPE_GOLDEN_SAMPLES]=$(generate_scope_golden_samples \"$SCOPE_PATH\" \"md\")\n scope_vars[HOUSE_RULES]=\"\"\n ;;\n esac\n\n # Render template (smart mode respects --update flag)\n render_template_smart \"$SCOPE_TEMPLATE\" \"$SCOPE_FILE\" scope_vars \"$UPDATE_ONLY\"\n\n # Enforce byte budget for scoped files (use half of root budget)\n enforce_byte_budget \"$SCOPE_FILE\" \"$((BYTE_BUDGET / 2))\"\n\n if [ \"$UPDATE_ONLY\" = true ]; then\n emit_op write agents-file \"$SCOPE_FILE\"\n echo \"✅ Updated: $SCOPE_FILE\"\n else\n emit_op write agents-file \"$SCOPE_FILE\"\n echo \"✅ Created: $SCOPE_FILE\"\n fi\n\n # Create subdirectory symlinks for cross-agent compatibility.\n # Documentation/ is symlink-hostile: the TYPO3 docs renderer lists it\n # via Flysystem, which aborts on ANY symbolic link (#82). Emit a\n # regular @AGENTS.md import file there instead (both Claude Code and\n # Gemini CLI support the @file import syntax).\n if [ \"$CREATE_SYMLINKS\" = true ]; then\n for symlink_name in CLAUDE.md GEMINI.md; do\n SYMLINK_FILE=\"$PROJECT_DIR/$SCOPE_PATH/$symlink_name\"\n if [ \"$(basename \"$SCOPE_PATH\")\" = \"Documentation\" ]; then\n if ! compat_file_is_ours \"$SYMLINK_FILE\" && [ \"$FORCE\" = false ]; then\n report_kept_file \"$SYMLINK_FILE\" \"$SCOPE_PATH/$symlink_name\"\n continue\n fi\n if [ \"$DRY_RUN\" = true ]; then\n emit_op write compat-file \"$SYMLINK_FILE\" note \"@AGENTS.md import file\"\n echo \"[DRY-RUN] Would write import file: $SYMLINK_FILE (@AGENTS.md)\"\n else\n printf '%s\\n\\n%s\\n' \\\n '\u003c!-- Regular file, not a symlink: the TYPO3 docs renderer (Flysystem) rejects symbolic links inside Documentation/. -->' \\\n '@AGENTS.md' | replace_file \"$SYMLINK_FILE\"\n emit_op write compat-file \"$SYMLINK_FILE\" note \"@AGENTS.md import file\"\n echo \" ↳ Import file: $SCOPE_PATH/$symlink_name (@AGENTS.md, symlink-hostile directory)\"\n fi\n continue\n fi\n if compat_file_is_ours \"$SYMLINK_FILE\"; then\n if [ \"$DRY_RUN\" = true ]; then\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"[DRY-RUN] Would symlink: $SYMLINK_FILE → AGENTS.md\"\n else\n ln -sf AGENTS.md \"$SYMLINK_FILE\"\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \" ↳ Symlinked: $SCOPE_PATH/$symlink_name → AGENTS.md\"\n fi\n elif [ \"$FORCE\" = true ]; then\n if [ \"$DRY_RUN\" = true ]; then\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \"[DRY-RUN] Would replace: $SYMLINK_FILE → AGENTS.md (--force)\"\n else\n rm -f \"$SYMLINK_FILE\"\n ln -s AGENTS.md \"$SYMLINK_FILE\"\n emit_op symlink compat-file \"$SYMLINK_FILE\" target AGENTS.md\n echo \" ↳ Replaced: $SCOPE_PATH/$symlink_name → AGENTS.md (--force)\"\n fi\n else\n report_kept_file \"$SYMLINK_FILE\" \"$SCOPE_PATH/$symlink_name\"\n fi\n done\n fi\n\n done \u003c \u003c(echo \"$SCOPES_INFO\" | jq -c '.scopes[]')\nfi\n\nif [ \"$DRY_RUN\" = true ]; then\n echo \"\"\n echo \"[DRY-RUN] No files were modified. Remove --dry-run to apply changes.\"\nfi\n\n# Print extraction summary\nif [ \"$VERBOSE\" = true ]; then\n print_summary \"$PROJECT_INFO\" \"$SCOPES_INFO\" \"$COMMANDS\" \"$DOCS_INFO\" \"$PLATFORM_INFO\" \"$IDE_INFO\" \"$AGENT_INFO\"\nelse\n echo \"\"\n print_compact_summary \"$PROJECT_INFO\" \"$SCOPES_INFO\"\nfi\n\necho \"\"\necho \"✅ AGENTS.md generation complete!\"\n[ \"$SCOPE_COUNT\" -gt 0 ] && echo \" Generated: 1 root + $SCOPE_COUNT scoped files\" || true\n\n# The manifest goes to the real stdout (fd 3), which --json parked before any\n# human line was written. dry_run tells a reader whether these operations are\n# planned or already carried out.\nif [ \"$JSON\" = true ]; then\n printf '%s\\n' \"${MANIFEST[@]:-}\" \\\n | jq -s --arg project \"$PROJECT_DIR\" --argjson dry \"$([ \"$DRY_RUN\" = true ] && echo true || echo false)\" \\\n 'map(select(. != null)) as $ops\n | {script: \"generate-agents\",\n schema: 1,\n dry_run: $dry,\n project: $project,\n summary: ($ops | group_by(.op) | map({key: .[0].op, value: length}) | from_entries),\n operations: $ops}' >&3\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":119153,"content_sha256":"0e52d828f110f0e46702328c444059271987fb58dda5601c5fc0fe53885ce536"},{"filename":"scripts/generate-file-map.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Generate file map (dir → purpose) for AGENTS.md\nset -euo pipefail\n# shellcheck source-path=SCRIPTDIR\n# shellcheck disable=SC1091 # the pre-commit hook runs shellcheck without\n# -x, so it cannot follow this source no matter how the path is written.\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/lib/git.sh\"\n\nPROJECT_DIR=\"${1:-.}\"\ncd \"$PROJECT_DIR\"\n\n# Check if git is available\nif ! project_git rev-parse --git-dir > /dev/null 2>&1; then\n echo \"# Not a git repository - cannot generate file map\" >&2\n echo \"\"\n exit 0\nfi\n\n# Map of known directory names to their purposes\ndeclare -A DIR_PURPOSES=(\n # Source directories\n [\"src\"]=\"application source code\"\n [\"lib\"]=\"library code\"\n [\"app\"]=\"application code (routes, pages)\"\n [\"pkg\"]=\"public packages\"\n [\"internal\"]=\"internal packages (not exported)\"\n\n # Language-specific\n [\"cmd\"]=\"CLI entrypoints\"\n [\"Classes\"]=\"PHP classes (PSR-4)\"\n [\"Configuration\"]=\"framework configuration\"\n\n # Frontend\n [\"components\"]=\"UI components\"\n [\"pages\"]=\"page components/routes\"\n [\"views\"]=\"view templates\"\n [\"layouts\"]=\"layout components\"\n [\"hooks\"]=\"React/Vue hooks\"\n [\"stores\"]=\"state management\"\n [\"styles\"]=\"CSS/styling\"\n [\"assets\"]=\"static assets (images, fonts)\"\n [\"public\"]=\"public static files\"\n\n # Backend\n [\"api\"]=\"API routes/handlers\"\n [\"routes\"]=\"route definitions\"\n [\"controllers\"]=\"request handlers\"\n [\"services\"]=\"business logic\"\n [\"models\"]=\"data models\"\n [\"entities\"]=\"database entities\"\n [\"repositories\"]=\"data access layer\"\n [\"middleware\"]=\"request middleware\"\n\n # Infrastructure\n [\"scripts\"]=\"automation scripts\"\n [\"bin\"]=\"compiled binaries\"\n [\"build\"]=\"build output\"\n [\"dist\"]=\"distribution files\"\n [\"vendor\"]=\"third-party dependencies (do not edit)\"\n [\"node_modules\"]=\"npm dependencies (do not edit)\"\n\n # Testing\n [\"tests\"]=\"test suites\"\n [\"test\"]=\"test suites\"\n [\"Tests\"]=\"test suites\"\n [\"__tests__\"]=\"Jest test suites\"\n [\"testutil\"]=\"test utilities\"\n [\"fixtures\"]=\"test fixtures\"\n [\"mocks\"]=\"test mocks\"\n\n # Documentation\n [\"docs\"]=\"documentation\"\n [\"Documentation\"]=\"documentation (RST/MD)\"\n [\"doc\"]=\"documentation\"\n\n # Configuration\n [\"config\"]=\"configuration files\"\n [\"configs\"]=\"configuration files\"\n [\".github\"]=\"GitHub Actions, templates\"\n [\".gitlab\"]=\"GitLab CI configuration\"\n\n # Data\n [\"migrations\"]=\"database migrations\"\n [\"seeds\"]=\"database seeds\"\n [\"data\"]=\"data files\"\n [\"Resources\"]=\"templates and assets\"\n\n # Examples\n [\"examples\"]=\"usage examples\"\n [\"example\"]=\"usage examples\"\n [\"samples\"]=\"sample code\"\n)\n\n# Get top-level directories with file counts\nget_directories() {\n project_git ls-files | cut -d/ -f1 | sort | uniq -c | sort -rn | while read -r count dir; do\n # Skip files (no directory)\n [[ \"$dir\" == *.* ]] && continue\n # Skip hidden except .github/.gitlab\n [[ \"$dir\" == .* ]] && [[ \"$dir\" != \".github\" ]] && [[ \"$dir\" != \".gitlab\" ]] && continue\n echo \"$count $dir\"\n done\n}\n\n# Infer purpose from directory contents if not in known list\ninfer_purpose() {\n local dir=\"$1\"\n\n # Check for specific file patterns\n if compgen -G \"$dir/*.go\" > /dev/null; then\n echo \"Go packages\"\n elif compgen -G \"$dir/*.py\" > /dev/null; then\n echo \"Python modules\"\n elif compgen -G \"$dir/*.php\" > /dev/null; then\n echo \"PHP classes\"\n elif compgen -G \"$dir/*.ts\" > /dev/null; then\n echo \"TypeScript modules\"\n elif compgen -G \"$dir/*.tsx\" > /dev/null; then\n echo \"React components\"\n elif compgen -G \"$dir/*.vue\" > /dev/null; then\n echo \"Vue components\"\n elif compgen -G \"$dir/*.sh\" > /dev/null; then\n echo \"shell scripts\"\n elif compgen -G \"$dir/*.md\" > /dev/null; then\n echo \"documentation\"\n elif compgen -G \"$dir/*.json\" > /dev/null; then\n echo \"configuration/data\"\n else\n echo \"project files\"\n fi\n}\n\n# Generate the file map\ngenerate_map() {\n local max_dirs=15 # Limit to top 15 directories\n\n while read -r count dir; do\n [ -z \"$dir\" ] && continue\n [ \"$max_dirs\" -le 0 ] && break\n\n local purpose=\"\"\n if [ -n \"${DIR_PURPOSES[$dir]:-}\" ]; then\n purpose=\"${DIR_PURPOSES[$dir]}\"\n else\n purpose=$(infer_purpose \"$dir\")\n fi\n\n # Format: dir/ → purpose\n # Pad directory name for alignment\n printf \"%-16s → %s\\n\" \"${dir}/\" \"$purpose\"\n\n ((max_dirs--))\n done \u003c \u003c(get_directories)\n}\n\n# Output the file map\ngenerate_map\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4708,"content_sha256":"a13f9197e8e7f805421b615c6dc4a212e4915914444a6f2184cbd91c42eb6909"},{"filename":"scripts/lib/config-root.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Find nearest config root for a given stack type\n\n# Find nearest directory containing package.json (for Node scopes)\n# Usage: find_node_config_root \"/path/to/scope\"\n# Returns: directory path or empty string\nfind_node_config_root() {\n local start_dir=\"$1\"\n local search_dir=\"$start_dir\"\n\n while [[ \"$search_dir\" != \".\" && \"$search_dir\" != \"/\" ]]; do\n if [[ -f \"$search_dir/package.json\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n search_dir=$(dirname \"$search_dir\")\n done\n\n # Check project root as fallback\n if [[ -f \"package.json\" ]]; then\n echo \".\"\n return 0\n fi\n\n return 1\n}\n\n# Find nearest directory containing composer.json (for PHP scopes)\nfind_php_config_root() {\n local start_dir=\"$1\"\n local search_dir=\"$start_dir\"\n\n while [[ \"$search_dir\" != \".\" && \"$search_dir\" != \"/\" ]]; do\n if [[ -f \"$search_dir/composer.json\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n search_dir=$(dirname \"$search_dir\")\n done\n\n if [[ -f \"composer.json\" ]]; then\n echo \".\"\n return 0\n fi\n\n return 1\n}\n\n# Find nearest directory containing go.mod (for Go scopes)\nfind_go_config_root() {\n local start_dir=\"$1\"\n local search_dir=\"$start_dir\"\n\n while [[ \"$search_dir\" != \".\" && \"$search_dir\" != \"/\" ]]; do\n if [[ -f \"$search_dir/go.mod\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n search_dir=$(dirname \"$search_dir\")\n done\n\n if [[ -f \"go.mod\" ]]; then\n echo \".\"\n return 0\n fi\n\n return 1\n}\n\n# Find nearest directory containing pyproject.toml or setup.py (for Python scopes)\nfind_python_config_root() {\n local start_dir=\"$1\"\n local search_dir=\"$start_dir\"\n\n while [[ \"$search_dir\" != \".\" && \"$search_dir\" != \"/\" ]]; do\n if [[ -f \"$search_dir/pyproject.toml\" || -f \"$search_dir/setup.py\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n search_dir=$(dirname \"$search_dir\")\n done\n\n if [[ -f \"pyproject.toml\" || -f \"setup.py\" ]]; then\n echo \".\"\n return 0\n fi\n\n return 1\n}\n\n# Find Node workspace root (monorepo root)\n# Detects: pnpm-workspace.yaml, package.json with \"workspaces\", lerna.json, nx.json\n# Usage: find_node_workspace_root \"/path/to/scope\"\n# Returns: workspace root directory or empty string\nfind_node_workspace_root() {\n local start_dir=\"$1\"\n local search_dir=\"$start_dir\"\n\n while [[ \"$search_dir\" != \".\" && \"$search_dir\" != \"/\" ]]; do\n # pnpm workspace\n if [[ -f \"$search_dir/pnpm-workspace.yaml\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n\n # npm/yarn workspaces (package.json with \"workspaces\" field)\n if [[ -f \"$search_dir/package.json\" ]]; then\n if jq -e '.workspaces' \"$search_dir/package.json\" >/dev/null 2>&1; then\n echo \"$search_dir\"\n return 0\n fi\n fi\n\n # Lerna monorepo\n if [[ -f \"$search_dir/lerna.json\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n\n # Nx monorepo\n if [[ -f \"$search_dir/nx.json\" ]]; then\n echo \"$search_dir\"\n return 0\n fi\n\n search_dir=$(dirname \"$search_dir\")\n done\n\n # Check project root as fallback\n if [[ -f \"pnpm-workspace.yaml\" ]]; then\n echo \".\"\n return 0\n fi\n if [[ -f \"package.json\" ]] && jq -e '.workspaces' \"package.json\" >/dev/null 2>&1; then\n echo \".\"\n return 0\n fi\n if [[ -f \"lerna.json\" || -f \"nx.json\" ]]; then\n echo \".\"\n return 0\n fi\n\n return 1\n}\n\n# Extract Node version from nearest config\n# Priority: package.json engines.node > .nvmrc > .node-version > .tool-versions\nget_node_version() {\n local config_root=\"$1\"\n local version=\"\"\n\n # Try package.json engines.node\n if [[ -f \"$config_root/package.json\" ]]; then\n version=$(jq -r '.engines.node // empty' \"$config_root/package.json\" 2>/dev/null)\n [[ -n \"$version\" ]] && echo \"$version\" && return 0\n fi\n\n # Try .nvmrc\n if [[ -f \"$config_root/.nvmrc\" ]]; then\n version=$(tr -d '[:space:]' \u003c \"$config_root/.nvmrc\")\n [[ -n \"$version\" ]] && echo \"$version\" && return 0\n fi\n\n # Try .node-version\n if [[ -f \"$config_root/.node-version\" ]]; then\n version=$(tr -d '[:space:]' \u003c \"$config_root/.node-version\")\n [[ -n \"$version\" ]] && echo \"$version\" && return 0\n fi\n\n # Try .tool-versions (asdf)\n if [[ -f \"$config_root/.tool-versions\" ]]; then\n version=$(grep '^nodejs ' \"$config_root/.tool-versions\" 2>/dev/null | awk '{print $2}')\n [[ -n \"$version\" ]] && echo \"$version\" && return 0\n fi\n\n return 1\n}\n\n# Extract JS framework from package.json dependencies\nget_js_framework() {\n local config_root=\"$1\"\n\n [[ ! -f \"$config_root/package.json\" ]] && return 1\n\n local deps\n deps=$(jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' \\\n \"$config_root/package.json\" 2>/dev/null)\n\n # Check in priority order (more specific first)\n echo \"$deps\" | grep -qw 'next' && echo \"next.js\" && return 0\n echo \"$deps\" | grep -qw 'nuxt' && echo \"nuxt\" && return 0\n echo \"$deps\" | grep -qw 'svelte' && echo \"svelte\" && return 0\n echo \"$deps\" | grep -qw 'vue' && echo \"vue\" && return 0\n echo \"$deps\" | grep -qw 'react' && echo \"react\" && return 0\n echo \"$deps\" | grep -qw 'express' && echo \"express\" && return 0\n\n return 1\n}\n\n# Check if TypeScript strict mode is enabled\nget_ts_strict_mode() {\n local config_root=\"$1\"\n\n [[ ! -f \"$config_root/tsconfig.json\" ]] && return 1\n\n local strict\n strict=$(jq -r '.compilerOptions.strict // false' \"$config_root/tsconfig.json\" 2>/dev/null)\n\n [[ \"$strict\" == \"true\" ]] && echo \"true\" && return 0\n return 1\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5969,"content_sha256":"51e9b61a3f4e301c8ccf4256db7ced476ebb8ea8b2465c0193748a1bd6033caf"},{"filename":"scripts/lib/git.sh","content":"# shellcheck shell=bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n#\n# Git in the analysed project. The scripts read a project they did not write,\n# and that project's .git/config can name commands git runs: core.fsmonitor\n# when git refreshes the index (git ls-files does), core.hooksPath when it\n# reaches a hook, and the signature program (gpg.program, gpg.ssh.program)\n# when log.showSignature makes git log verify signed commits, and the\n# transport command of a promisor remote when git fetches a missing object.\n# project_git turns these off (GIT_NO_LAZY_FETCH, and an empty\n# GIT_ALLOW_PROTOCOL, which the repository's protocol.*.allow settings cannot\n# override) and skips the system config. Inherited variables that point git at another repository\n# or index (set when the scripts run from a git hook) are removed, so git\n# answers for the analysed project.\n\nproject_git() {\n env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_OBJECT_DIRECTORY \\\n -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_COMMON_DIR -u GIT_NAMESPACE \\\n GIT_CONFIG_NOSYSTEM=1 GIT_NO_LAZY_FETCH=1 GIT_ALLOW_PROTOCOL= \\\n git -c core.fsmonitor=false -c core.hooksPath=/dev/null \\\n -c log.showSignature=false \"$@\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1261,"content_sha256":"8730c27d9b5604dee18e89163b0d7e945fe309acf9f214d3f636a168e0aebd27"},{"filename":"scripts/lib/summary.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Summary output formatting helpers\n\n# Colors (only if terminal supports it)\nif [[ -t 1 ]]; then\n GREEN='\\033[0;32m'\n YELLOW='\\033[0;33m'\n # shellcheck disable=SC2034 # part of the palette; kept so the colour and\n # no-colour branches stay symmetric rather than losing one entry.\n BLUE='\\033[0;34m'\n CYAN='\\033[0;36m'\n GRAY='\\033[0;90m'\n BOLD='\\033[1m'\n NC='\\033[0m' # No Color\nelse\n GREEN=''\n YELLOW=''\n # shellcheck disable=SC2034 # see the palette comment above\n BLUE=''\n CYAN=''\n GRAY=''\n BOLD=''\n NC=''\nfi\n\n# Summary data storage\n# shellcheck disable=SC2034 # written by add_summary below and read by nothing\n# in this repository — the provenance it records has no consumer yet. Left in\n# place rather than deleted, but it buys no trust until something reads it.\ndeclare -A SUMMARY_SOURCES\ndeclare -a SUMMARY_RULES\ndeclare -a SUMMARY_WARNINGS\n\n# Initialize summary\ninit_summary() {\n SUMMARY_SOURCES=()\n SUMMARY_RULES=()\n SUMMARY_WARNINGS=()\n}\n\n# Record a detected value with its source\n# Usage: record_detection \"category\" \"value\" \"source_file\" \"source_detail\"\nrecord_detection() {\n local category=\"$1\"\n local value=\"$2\"\n local source_file=\"$3\"\n local source_detail=\"${4:-}\"\n\n if [[ -n \"$source_detail\" ]]; then\n SUMMARY_SOURCES[\"$category\"]=\"$value (from $source_file: $source_detail)\"\n else\n # shellcheck disable=SC2034 # see the declaration comment above\n SUMMARY_SOURCES[\"$category\"]=\"$value (from $source_file)\"\n fi\n}\n\n# Record an extracted rule\n# Usage: record_rule \"rule_type\" \"rule_value\" \"source\"\nrecord_rule() {\n local rule_type=\"$1\"\n local rule_value=\"$2\"\n local source=\"$3\"\n\n SUMMARY_RULES+=(\"$rule_type: $rule_value [$source]\")\n}\n\n# Record a warning\n# Usage: record_warning \"message\"\nrecord_warning() {\n local message=\"$1\"\n SUMMARY_WARNINGS+=(\"$message\")\n}\n\n# Print the extraction summary\nprint_summary() {\n local project_info=\"$1\"\n local scopes_info=\"$2\"\n local commands_info=\"$3\"\n local docs_info=\"${4:-{}}\"\n local platform_info=\"${5:-{}}\"\n local ide_info=\"${6:-{}}\"\n local agent_info=\"${7:-{}}\"\n\n echo \"\"\n echo -e \"${BOLD}═══════════════════════════════════════════════════════════════${NC}\"\n echo -e \"${BOLD} EXTRACTION SUMMARY ${NC}\"\n echo -e \"${BOLD}═══════════════════════════════════════════════════════════════${NC}\"\n echo \"\"\n\n # Project Detection\n echo -e \"${CYAN}▸ Project Detection${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n\n local lang\n\n lang=$(echo \"$project_info\" | jq -r '.language')\n local version\n version=$(echo \"$project_info\" | jq -r '.version')\n local ptype\n ptype=$(echo \"$project_info\" | jq -r '.type')\n local framework\n framework=$(echo \"$project_info\" | jq -r '.framework')\n local build_tool\n build_tool=$(echo \"$project_info\" | jq -r '.build_tool')\n local test_fw\n test_fw=$(echo \"$project_info\" | jq -r '.test_framework')\n local ci\n ci=$(echo \"$project_info\" | jq -r '.ci')\n local docker\n docker=$(echo \"$project_info\" | jq -r '.has_docker')\n\n # Determine source file\n local lang_source=\"\"\n case \"$lang\" in\n go) lang_source=\"go.mod\" ;;\n php) lang_source=\"composer.json\" ;;\n typescript) lang_source=\"package.json\" ;;\n python) lang_source=\"pyproject.toml\" ;;\n esac\n\n printf \" %-16s ${GREEN}%s${NC} ${GRAY}(from %s)${NC}\\n\" \"Language:\" \"$lang\" \"$lang_source\"\n [[ \"$version\" != \"unknown\" ]] && printf \" %-16s ${GREEN}%s${NC}\\n\" \"Version:\" \"$version\"\n [[ \"$ptype\" != \"unknown\" ]] && printf \" %-16s %s\\n\" \"Project type:\" \"$ptype\"\n [[ \"$framework\" != \"none\" ]] && printf \" %-16s %s\\n\" \"Framework:\" \"$framework\"\n printf \" %-16s %s\\n\" \"Build tool:\" \"$build_tool\"\n [[ \"$test_fw\" != \"unknown\" ]] && printf \" %-16s %s\\n\" \"Test framework:\" \"$test_fw\"\n [[ \"$ci\" != \"none\" ]] && printf \" %-16s %s\\n\" \"CI/CD:\" \"$ci\"\n [[ \"$docker\" == \"true\" ]] && printf \" %-16s %s\\n\" \"Docker:\" \"yes\"\n\n # Quality Tools\n local tools\n tools=$(echo \"$project_info\" | jq -r '.quality_tools | join(\", \")')\n if [[ -n \"$tools\" && \"$tools\" != \"\" ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ Quality Tools Detected${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n printf \" %s\\n\" \"$tools\"\n fi\n\n # IDE Configs (from ide_info)\n local detected_ides\n detected_ides=$(echo \"$ide_info\" | jq -r '.detected_ides | join(\", \")' 2>/dev/null || echo \"\")\n if [[ -n \"$detected_ides\" && \"$detected_ides\" != \"\" ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ IDE Configurations${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n printf \" %s\\n\" \"$detected_ides\"\n\n # Show editorconfig details if present\n local indent\n indent=$(echo \"$ide_info\" | jq -r '.editorconfig.indent_style // \"\"' 2>/dev/null || echo \"\")\n if [[ -n \"$indent\" ]]; then\n local indent_size\n indent_size=$(echo \"$ide_info\" | jq -r '.editorconfig.indent_size // \"\"' 2>/dev/null || echo \"\")\n printf \" ${GRAY}→ indent: %s (%s)${NC}\\n\" \"$indent\" \"$indent_size\"\n fi\n fi\n\n # AI Agent Configs (from agent_info)\n local detected_agents\n detected_agents=$(echo \"$agent_info\" | jq -r '.detected_agents | join(\", \")' 2>/dev/null || echo \"\")\n if [[ -n \"$detected_agents\" && \"$detected_agents\" != \"\" ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ AI Agent Configurations${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n printf \" %s\\n\" \"$detected_agents\"\n fi\n\n # Documentation Files (from docs_info)\n local has_contributing\n has_contributing=$(echo \"$docs_info\" | jq -r '.contributing.file // \"\"' 2>/dev/null || echo \"\")\n local has_security\n has_security=$(echo \"$docs_info\" | jq -r '.security.file // \"\"' 2>/dev/null || echo \"\")\n local has_changelog\n has_changelog=$(echo \"$docs_info\" | jq -r '.changelog.file // \"\"' 2>/dev/null || echo \"\")\n local has_coc\n has_coc=$(echo \"$docs_info\" | jq -r '.code_of_conduct.exists // false' 2>/dev/null || echo \"false\")\n\n if [[ -n \"$has_contributing\" || -n \"$has_security\" || -n \"$has_changelog\" || \"$has_coc\" == \"true\" ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ Documentation Files${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n [[ -n \"$has_contributing\" ]] && printf \" ✓ %s\\n\" \"$has_contributing\"\n [[ -n \"$has_security\" ]] && printf \" ✓ %s\\n\" \"$has_security\"\n [[ -n \"$has_changelog\" ]] && printf \" ✓ %s\\n\" \"$has_changelog\"\n [[ \"$has_coc\" == \"true\" ]] && printf \" ✓ CODE_OF_CONDUCT.md\\n\"\n fi\n\n # Platform Files (from platform_info)\n local platform\n platform=$(echo \"$platform_info\" | jq -r '.platform // \"none\"' 2>/dev/null || echo \"none\")\n if [[ \"$platform\" != \"none\" ]]; then\n local pr_template\n pr_template=$(echo \"$platform_info\" | jq -r '.pull_request.template_file // \"\"' 2>/dev/null || echo \"\")\n local codeowners\n codeowners=$(echo \"$platform_info\" | jq -r '.codeowners.file // \"\"' 2>/dev/null || echo \"\")\n local dependabot\n dependabot=$(echo \"$platform_info\" | jq -r '.dependency_updates.dependabot.file // \"\"' 2>/dev/null || echo \"\")\n\n if [[ -n \"$pr_template\" || -n \"$codeowners\" || -n \"$dependabot\" ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ Platform Files (${platform})${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n [[ -n \"$pr_template\" ]] && printf \" ✓ PR template: %s\\n\" \"$pr_template\"\n [[ -n \"$codeowners\" ]] && printf \" ✓ CODEOWNERS: %s\\n\" \"$codeowners\"\n [[ -n \"$dependabot\" ]] && printf \" ✓ Dependabot: %s\\n\" \"$dependabot\"\n fi\n fi\n\n # Commands Extracted\n echo \"\"\n echo -e \"${CYAN}▸ Commands Extracted${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n\n local typecheck\n\n typecheck=$(echo \"$commands_info\" | jq -r '.typecheck')\n local lint\n lint=$(echo \"$commands_info\" | jq -r '.lint')\n local format\n format=$(echo \"$commands_info\" | jq -r '.format')\n local test_cmd\n test_cmd=$(echo \"$commands_info\" | jq -r '.test')\n local build\n build=$(echo \"$commands_info\" | jq -r '.build')\n local dev\n dev=$(echo \"$commands_info\" | jq -r '.dev')\n\n [[ -n \"$typecheck\" && \"$typecheck\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"typecheck:\" \"$typecheck\"\n [[ -n \"$lint\" && \"$lint\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"lint:\" \"$lint\"\n [[ -n \"$format\" && \"$format\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"format:\" \"$format\"\n [[ -n \"$test_cmd\" && \"$test_cmd\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"test:\" \"$test_cmd\"\n [[ -n \"$build\" && \"$build\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"build:\" \"$build\"\n [[ -n \"$dev\" && \"$dev\" != \"\" ]] && printf \" %-12s ${YELLOW}%s${NC}\\n\" \"dev:\" \"$dev\"\n\n # Scopes Detected\n local scope_count\n scope_count=$(echo \"$scopes_info\" | jq '.scopes | length')\n if [[ \"$scope_count\" -gt 0 ]]; then\n echo \"\"\n echo -e \"${CYAN}▸ Scopes Detected ($scope_count)${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n\n echo \"$scopes_info\" | jq -r '.scopes[] | \" \\(.path)/ (\\(.files) files) → \\(.type)\"'\n fi\n\n # Files to Generate\n echo \"\"\n echo -e \"${CYAN}▸ Files to Generate${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n echo \" AGENTS.md (root)\"\n if [[ \"$scope_count\" -gt 0 ]]; then\n echo \"$scopes_info\" | jq -r '.scopes[] | \" \\(.path)/AGENTS.md\"'\n fi\n\n # Warnings\n if [[ ${#SUMMARY_WARNINGS[@]} -gt 0 ]]; then\n echo \"\"\n echo -e \"${YELLOW}▸ Warnings${NC}\"\n echo -e \" ${GRAY}────────────────────────────────────────${NC}\"\n for warning in \"${SUMMARY_WARNINGS[@]}\"; do\n echo -e \" ${YELLOW}⚠${NC} $warning\"\n done\n fi\n\n echo \"\"\n echo -e \"${BOLD}═══════════════════════════════════════════════════════════════${NC}\"\n echo \"\"\n}\n\n# Print a compact summary (for non-verbose mode)\nprint_compact_summary() {\n local project_info=\"$1\"\n local scopes_info=\"$2\"\n\n local lang\n\n lang=$(echo \"$project_info\" | jq -r '.language')\n local ptype\n ptype=$(echo \"$project_info\" | jq -r '.type')\n local scope_count\n scope_count=$(echo \"$scopes_info\" | jq '.scopes | length')\n\n echo \"Detected: $lang ($ptype), $((scope_count + 1)) AGENTS.md file(s) to generate\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":11970,"content_sha256":"eaf6c3cdcecbef3d0e4082b6594fce353a05b398f9dd1c77be35d681236a21a2"},{"filename":"scripts/lib/template.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Template rendering helper functions\n\n# Remove sections that are entirely empty (only header + whitespace/empty tables)\n# A section is: ## Header followed by content until next ## or EOF\n# Empty means: only whitespace, empty table headers (2 rows only), or empty code blocks\nremove_empty_sections() {\n local content=\"$1\"\n\n # Use awk to process sections\n echo \"$content\" | awk '\n BEGIN {\n section_header = \"\"\n section_body = \"\"\n preamble = \"\"\n in_section = 0\n }\n\n # Match section headers (## Something)\n /^## / {\n # Flush previous section if it had real content\n if (in_section) {\n if (has_content(section_body)) {\n print section_header\n printf \"%s\", section_body\n }\n } else {\n # Print any preamble before first section\n printf \"%s\", preamble\n }\n\n section_header = $0\n section_body = \"\"\n in_section = 1\n next\n }\n\n # Accumulate content\n {\n if (in_section) {\n section_body = section_body $0 \"\\n\"\n } else {\n preamble = preamble $0 \"\\n\"\n }\n }\n\n END {\n # Flush last section\n if (in_section && has_content(section_body)) {\n print section_header\n printf \"%s\", section_body\n }\n }\n\n # Check if body has real content (not just empty structural elements)\n function has_content(body, stripped, table_rows, n, i, data_rows) {\n stripped = body\n\n # Remove HTML comments\n gsub(/\u003c!--[^>]*-->/, \"\", stripped)\n\n # Count table rows - if exactly 2 (header + divider), table is empty\n # Split by newlines and count lines starting with |\n n = split(stripped, lines, \"\\n\")\n table_rows = 0\n data_rows = 0\n for (i = 1; i \u003c= n; i++) {\n if (lines[i] ~ /^\\|/) {\n table_rows++\n # Data row = table row that is NOT a header divider (|---|)\n if (lines[i] !~ /^\\|[-:| ]+\\|$/) {\n # Also not just the header row if we are still in first 2\n if (table_rows > 2) {\n data_rows++\n }\n }\n }\n }\n\n # If we have a table with more than 2 rows, it has data\n if (table_rows > 2) {\n return 1\n }\n\n # Remove tables that are just header+divider (2 rows)\n # Pattern: | ... |\\n|---...|\\n\n gsub(/\\|[^|\\n]+(\\|[^|\\n]+)*\\|\\n\\|[-:| ]+\\|\\n?/, \"\", stripped)\n\n # Remove empty code blocks (``` followed by optional whitespace and ```)\n gsub(/```[^\\n]*\\n[ \\t]*\\n?```/, \"\", stripped)\n # Remove standalone code block markers\n gsub(/```[a-z]*\\n?/, \"\", stripped)\n\n # Remove bullet points that are just placeholder markers or empty\n gsub(/^[ \\t]*[-*][ \\t]*\\n/, \"\", stripped)\n\n # Remove lines that say \"No scoped AGENTS.md files yet\" or similar placeholder text\n gsub(/- \\(No scoped [^)]+\\)/, \"\", stripped)\n gsub(/- No known [^\\n]+/, \"\", stripped)\n\n # Check if anything substantive remains\n gsub(/[ \\t\\n]/, \"\", stripped)\n\n return length(stripped) > 0\n }\n '\n}\n\n# Render template with placeholder replacement\n# Supports multi-line values using bash string replacement\nrender_template() {\n local template_file=\"$1\"\n local output_file=\"$2\"\n local -n template_vars=$3\n\n local content\n content=$(cat \"$template_file\")\n\n # Replace all placeholders using bash parameter expansion\n # This handles multi-line values correctly\n for key in \"${!template_vars[@]}\"; do\n local value=\"${template_vars[$key]}\"\n content=\"${content//\"{{$key}}\"/$value}\"\n done\n\n # Handle remaining unfilled placeholders\n # Delete table rows containing unresolved placeholders\n content=$(echo \"$content\" | sed '/^|.*{{[A-Z_]*}}.*|$/d')\n\n # Delete bullet points with only unresolved placeholders\n content=$(echo \"$content\" | sed '/^[[:space:]]*[-*][[:space:]]*{{[A-Z_][A-Z0-9_]*}}[[:space:]]*$/d')\n\n # Delete lines that are ONLY a placeholder (prevents blank lines in tables)\n content=$(echo \"$content\" | sed '/^{{[A-Z_][A-Z0-9_]*}}$/d')\n # Also handle placeholders with leading/trailing whitespace\n content=$(echo \"$content\" | sed '/^[[:space:]]*{{[A-Z_][A-Z0-9_]*}}[[:space:]]*$/d')\n\n # Remove any remaining inline placeholders (better than \"(not configured)\" noise)\n # shellcheck disable=SC2001 # regex quantifier over a character class;\n # bash glob replacement cannot express \"one class char then zero or more\".\n content=$(echo \"$content\" | sed 's/{{[A-Z_][A-Z0-9_]*}}//g')\n\n # Remove empty lines that appear after table rows but before non-table content\n # This fixes broken tables when placeholders were replaced with empty content\n content=$(echo \"$content\" | awk '\n /^\\|.*\\|$/ {\n # Print any pending empty lines first if we are continuing a table\n if (prev_was_table && pending_empty) {\n # Skip the pending empty - it was between table rows with no data\n }\n print\n prev_was_table=1\n pending_empty=0\n next\n }\n /^[[:space:]]*$/ {\n if (prev_was_table) {\n pending_empty=1 # Hold empty line, might be inside table\n } else {\n print # Normal empty line outside table\n }\n next\n }\n {\n # Non-table line - do not print pending empty from table section\n pending_empty=0\n prev_was_table=0\n print\n }\n ')\n\n # Clean up multiple consecutive empty lines\n content=$(echo \"$content\" | cat -s)\n\n # Remove empty sections (only header + whitespace/empty tables)\n content=$(remove_empty_sections \"$content\")\n\n # Final cleanup of multiple consecutive empty lines after section removal\n content=$(echo \"$content\" | cat -s)\n\n # Write output\n printf '%s\\n' \"$content\" | replace_file \"$output_file\"\n}\n\n# Write stdin to \u003cfile> by replacement: a temporary file in the same directory\n# is renamed over the target, so a symlink there is replaced, never followed.\n# An existing regular file keeps its mode; a new file gets the mode the umask\n# gives a newly created file, not the 0600 of a temporary file. A directory at\n# the path is refused (mv would move the file into it).\nreplace_file() {\n local file=\"$1\" dir tmp mode\n if [ -d \"$file\" ] && [ ! -L \"$file\" ]; then\n echo \"Error: $file is a directory; not writing it\" >&2\n return 1\n fi\n dir=\"$(dirname \"$file\")\"\n if [ -f \"$file\" ] && [ ! -L \"$file\" ]; then\n mode=\"$(stat -c %a \"$file\" 2>/dev/null || stat -f %Lp \"$file\")\"\n else\n mode=\"$(printf '%o' $(( 0666 & ~0$(umask) )))\"\n fi\n tmp=\"$(mktemp \"$dir/.agents-write.XXXXXX\")\"\n cat > \"$tmp\"\n chmod \"$mode\" \"$tmp\"\n [ -L \"$file\" ] && rm -f \"$file\"\n mv -f \"$tmp\" \"$file\"\n}\n\n# Validate rendered content has no remaining placeholders\n# Returns 0 if valid, 1 if placeholders found\nvalidate_no_placeholders() {\n local file=\"$1\"\n local strict=\"${2:-false}\"\n\n if grep -qE '\\{\\{[A-Z][A-Z0-9_]*\\}\\}' \"$file\"; then\n local placeholders\n placeholders=$(grep -oE '\\{\\{[A-Z][A-Z0-9_]*\\}\\}' \"$file\" | sort -u | tr '\\n' ' ')\n\n if [ \"$strict\" = \"true\" ]; then\n echo \"[ERROR] Unresolved placeholders in $file: $placeholders\" >&2\n return 1\n else\n echo \"[WARN] Unresolved placeholders in $file: $placeholders\" >&2\n return 0\n fi\n fi\n return 0\n}\n\n# Generate timestamp\nget_timestamp() {\n date +%Y-%m-%d\n}\n\n# Build scope index for root template\nbuild_scope_index() {\n local scopes_json=\"$1\"\n local index=\"\"\n\n local count\n count=$(echo \"$scopes_json\" | jq '.scopes | length')\n if [ \"$count\" -eq 0 ]; then\n echo \"- (No scoped AGENTS.md files yet)\"\n return\n fi\n\n while read -r scope; do\n local path type description\n path=$(echo \"$scope\" | jq -r '.path')\n type=$(echo \"$scope\" | jq -r '.type')\n description=$(get_scope_description \"$type\")\n\n # A scope discovered only because it already carries an AGENTS.md has no\n # type to describe. Let the file speak for itself — its own Overview\n # line says what the directory is for, which beats any label this\n # script could invent. Falls back to a neutral phrase, never to the\n # bare type name, which reads as a bug in the generated output.\n if [ \"$type\" = \"existing\" ]; then\n description=$(sed -n '/^## Overview/,/^## /p' \"$path/AGENTS.md\" 2>/dev/null \\\n | grep -v '^##' | grep -v '^

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| head -1 | cut -c1-100)\n [ -n \"$description\" ] || description=\"Scoped rules for \\`$path/\\`\"\n fi\n\n index=\"$index- \\`./$path/AGENTS.md\\` — $description\\n\"\n done \u003c \u003c(echo \"$scopes_json\" | jq -c '.scopes[]')\n\n echo -e \"$index\"\n}\n\n# Get description for scope type\nget_scope_description() {\n local type=\"$1\"\n\n case \"$type\" in\n \"backend-go\") echo \"Backend services (Go)\" ;;\n \"backend-php\") echo \"Backend services (PHP)\" ;;\n \"backend-typescript\") echo \"Backend services (TypeScript/Node.js)\" ;;\n \"backend-python\") echo \"Backend services (Python)\" ;;\n \"frontend-typescript\") echo \"Frontend application (TypeScript/React/Vue)\" ;;\n \"cli\") echo \"Command-line interface tools and entry points\" ;;\n \"testing\") echo \"Test suites, fixtures, and testing utilities\" ;;\n \"documentation\") echo \"Project documentation, guides, and reference materials\" ;;\n \"examples\") echo \"Example applications, usage patterns, and sample code\" ;;\n \"resources\") echo \"Static resources, assets, templates, and configuration files\" ;;\n \"docker\") echo \"Container/Docker configuration for building and deploying images\" ;;\n \"claude-code-skill\") echo \"Claude Code skill/plugin providing AI agent capabilities\" ;;\n \"typo3-extension\") echo \"TYPO3 extension following TYPO3 CGL and PSR-12\" ;;\n \"typo3-project\") echo \"TYPO3 project installation with site configuration\" ;;\n \"oro-bundle\") echo \"Oro bundle following Oro Architecture and Symfony best practices\" ;;\n \"oro-project\") echo \"Oro application with platform configuration and bundles\" ;;\n \"symfony\") echo \"Symfony application following Symfony best practices\" ;;\n \"github-actions\") echo \"GitHub Actions workflows and CI/CD automation\" ;;\n \"gitlab-ci\") echo \"GitLab CI/CD pipeline configuration\" ;;\n \"concourse\") echo \"Concourse CI pipeline and task definitions\" ;;\n \"typo3-testing\") echo \"TYPO3 test suites, fixtures and the Docker test runner\" ;;\n \"typo3-docs\") echo \"TYPO3 documentation (reStructuredText, rendered by the docs toolchain)\" ;;\n \"ddev\") echo \"DDEV local development environment and its commands\" ;;\n \"python-modern\") echo \"Python package using the modern toolchain (uv, ruff, pyproject)\" ;;\n # Falling through prints the raw type name into user-facing output,\n # which reads as a bug rather than a description. Anything reaching\n # here is a type someone added to detect-scopes.sh without a matching\n # case above — say so plainly instead of leaking the identifier.\n *) echo \"Scoped rules for this directory\" ;;\n esac\n}\n\n# Get language-specific conventions text\nget_language_conventions() {\n local language=\"$1\"\n local version=\"$2\"\n\n case \"$language\" in\n \"go\")\n echo \"- Follow Go $version conventions and idioms\"\n ;;\n \"php\")\n echo \"- Follow PSR-12 coding standards and PHP $version features\"\n ;;\n \"typescript\")\n echo \"- Use TypeScript strict mode with proper type annotations\"\n ;;\n \"python\")\n echo \"- Follow PEP 8 style guide and Python $version features\"\n ;;\n *)\n echo \"\"\n ;;\n esac\n}\n\n# Format command with fallback text\nformat_command() {\n local cmd=\"$1\"\n local fallback=\"$2\"\n\n if [ -n \"$cmd\" ] && [ \"$cmd\" != \"null\" ]; then\n echo \"$cmd\"\n else\n echo \"$fallback\"\n fi\n}\n\n# Check if file has generated section markers\nhas_generated_markers() {\n local file=\"$1\"\n grep -q 'AGENTS-GENERATED:START' \"$file\" 2>/dev/null\n}\n\n# Extract section names from file\nget_section_names() {\n local file=\"$1\"\n grep -oE 'AGENTS-GENERATED:START [a-z0-9-]+' \"$file\" 2>/dev/null | sed 's/AGENTS-GENERATED:START //' | sort -u\n}\n\n# Extract content between markers for a section\nget_section_content() {\n local file=\"$1\"\n local section=\"$2\"\n\n sed -n \"/AGENTS-GENERATED:START $section/,/AGENTS-GENERATED:END $section/p\" \"$file\" 2>/dev/null | \\\n sed '1d;$d' # Remove the marker lines themselves\n}\n\n# Update only generated sections in an existing file\n# Preserves all content outside markers\nupdate_generated_sections() {\n local template_file=\"$1\"\n local existing_file=\"$2\"\n local output_file=\"$3\"\n # shellcheck disable=SC2034 # nameref, read through the indirect name below\n local -n update_vars=$4\n\n # First render the template to get new content\n local temp_rendered\n temp_rendered=$(mktemp)\n render_template \"$template_file\" \"$temp_rendered\" update_vars\n\n # If existing file doesn't have markers, just overwrite\n if ! has_generated_markers \"$existing_file\"; then\n replace_file \"$output_file\" \u003c \"$temp_rendered\"\n rm -f \"$temp_rendered\"\n return 0\n fi\n\n # Get all section names from the rendered template\n local sections\n sections=$(get_section_names \"$temp_rendered\")\n\n # Start with the existing file content\n local result\n result=$(cat \"$existing_file\")\n\n # Replace each generated section\n for section in $sections; do\n local new_content\n new_content=$(get_section_content \"$temp_rendered\" \"$section\")\n\n if [ -n \"$new_content\" ]; then\n # Create a sed-safe version of the new content\n # Use awk for multi-line replacement\n result=$(echo \"$result\" | awk -v section=\"$section\" -v newcontent=\"$new_content\" '\n BEGIN { in_section = 0 }\n /AGENTS-GENERATED:START / && $0 ~ section {\n print\n in_section = 1\n next\n }\n /AGENTS-GENERATED:END / && $0 ~ section {\n print newcontent\n print\n in_section = 0\n next\n }\n !in_section { print }\n ')\n fi\n done\n\n # Update timestamp\n local today\n today=$(date +%Y-%m-%d)\n # shellcheck disable=SC2001 # see above: quantified class, not a glob.\n result=$(echo \"$result\" | sed \"s/Last updated: [0-9-]*/Last updated: $today/\")\n\n # Write result\n printf '%s\\n' \"$result\" | replace_file \"$output_file\"\n\n # Clean up\n rm -f \"$temp_rendered\"\n}\n\n# Render template - respects update mode\n# If update_mode=true and file exists with markers, only updates generated sections\nrender_template_smart() {\n local template_file=\"$1\"\n local output_file=\"$2\"\n # shellcheck disable=SC2034 # nameref, read through the indirect name below\n local -n smart_vars=$3\n local update_mode=\"${4:-false}\"\n\n if [ \"$update_mode\" = \"true\" ] && [ -f \"$output_file\" ] && has_generated_markers \"$output_file\"; then\n # Update mode - preserve human edits\n update_generated_sections \"$template_file\" \"$output_file\" \"$output_file\" smart_vars\n else\n # Normal mode - full render\n render_template \"$template_file\" \"$output_file\" smart_vars\n fi\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":16267,"content_sha256":"5db7a930c6fb28766c0c4196e729d745411193890dcd62d8018f7eec33c33608"},{"filename":"scripts/score-agents.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# score-agents.sh - Grade AGENTS.md files with a reproducible quality score.\n#\n# Aggregates the --json output of the four verifier scripts into a per-file\n# letter grade (A-F) on a 0-100 scale, ranked worst-first so you know where to\n# spend effort. Reproducible: re-running on an unchanged tree yields the same\n# grade (no model call, CI-friendly). One caveat - the Currency axis can shift\n# within a day for a file whose \"Last updated\" date is today, because\n# check-freshness.sh uses git's bare-date --since.\n#\n# Five script-measured axes (max points):\n# Structure 25 | Currency 20 | Content 20 | Commands 15 (root only) | Conciseness 20\n# A file's percentage = earned / (sum of applicable axis maxima) * 100.\n# Scoped files have no Commands axis; their maxima sum to 85 and normalise to 100.\n#\n# Grades: A >=90 B >=75 C >=50 D >=30 F \u003c30\n#\n# A separate, QUALITATIVE LLM overlay (Architecture / Actionability /\n# Non-obvious-patterns) is NOT part of this number - it varies run to run. See\n# references/quality-rubric.md for how an agent layers that review on top.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\nPROJECT_DIR=\"\"\nJSON=false\nREVIEW_FILE=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --json)\n JSON=true\n shift\n ;;\n --review)\n REVIEW_FILE=\"${2:-}\"\n shift 2\n ;;\n --review=*)\n REVIEW_FILE=\"${1#*=}\"\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: score-agents.sh [PROJECT_DIR] [OPTIONS]\n\nGrade AGENTS.md files with a reproducible 0-100 quality score (worst-first).\n\nOptions:\n --json Emit the machine-readable scoring document on stdout\n --review FILE Add a secondary, NON-reproducible \"with review\" grade from a\n JSON of agent-judged LLM-axis ratings (see quality-rubric.md):\n { \"PATH\": {\"architecture\":\"strong|adequate|weak\",\n \"actionability\":\"...\",\"non_obvious\":\"...\"}, ... }\n --help, -h Show this help message\n\nAxes: Structure 25 | Currency 20 | Content 20 | Commands 15 (root) | Conciseness 20\nGrades: A >=90 B >=75 C >=50 D >=30 F \u003c30\n\nThe deterministic score never changes on an unchanged tree. The --review overlay\n(Architecture, Actionability, Non-obvious patterns) varies and is shown separately.\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\nPROJECT_DIR=\"${PROJECT_DIR:-.}\"\n\nif ! command -v jq >/dev/null 2>&1; then\n echo \"Error: jq is required\" >&2\n exit 2\nfi\n\n# Optional LLM-axis ratings for the secondary \"with review\" grade.\nREVIEW_JSON='{}'\nif [[ -n \"$REVIEW_FILE\" ]]; then\n if [[ -f \"$REVIEW_FILE\" ]] && jq -e . \"$REVIEW_FILE\" >/dev/null 2>&1; then\n REVIEW_JSON=$(cat \"$REVIEW_FILE\")\n else\n echo \"Error: --review file missing or not valid JSON: $REVIEW_FILE\" >&2\n exit 2\n fi\nfi\n\n# Run a verifier in --json mode; tolerate its non-zero \"issues found\" exit and\n# its human stderr. Echo '{}' if it produced nothing parseable so jq stays happy.\nrun_json() {\n local script=\"$1\"\n shift\n local out\n out=$(bash \"$SCRIPT_DIR/$script\" \"$PROJECT_DIR\" --json \"$@\" 2>/dev/null || true)\n if [[ -n \"$out\" ]] && printf '%s' \"$out\" | jq -e . >/dev/null 2>&1; then\n printf '%s' \"$out\"\n else\n echo '{}'\n fi\n return 0\n}\n\nSTRUCT_JSON=$(run_json validate-structure.sh)\nFRESH_JSON=$(run_json check-freshness.sh)\nCONTENT_JSON=$(run_json verify-content.sh)\nCOMMANDS_JSON=$(run_json verify-commands.sh)\n\n# Spine = the files validate-structure reports (it already excludes vendored\n# example fixtures). Precompute a path -> line-count map for the Conciseness axis.\nmapfile -t SPINE \u003c \u003c(printf '%s' \"$STRUCT_JSON\" | jq -r '(.files // [])[].path')\n\n# Build the path -> line-count map in a SINGLE jq pass (tab-separated stream)\n# rather than spawning jq once per file.\nLINES_JSON=$(\n for path in \"${SPINE[@]}\"; do\n full=\"$PROJECT_DIR/$path\"\n n=0\n [[ -f \"$full\" ]] && n=$(wc -l \u003c \"$full\" | tr -d ' ')\n printf '%s\\t%s\\n' \"$path\" \"$n\"\n done | jq -Rs 'split(\"\\n\") | map(select(. != \"\")) | map(split(\"\\t\"))\n | map({key: .[0], value: (.[1]|tonumber)}) | from_entries'\n)\n\n# --- Scoring (single jq pass; pure function of its inputs => reproducible) -----\nSCORING=$(jq -nc \\\n --argjson struct \"$STRUCT_JSON\" \\\n --argjson fresh \"$FRESH_JSON\" \\\n --argjson content \"$CONTENT_JSON\" \\\n --argjson commands \"$COMMANDS_JSON\" \\\n --argjson lines \"$LINES_JSON\" \\\n --argjson review \"$REVIEW_JSON\" \\\n '\n def clamp(max): if . \u003c 0 then 0 elif . > max then max else . end;\n def grade(p): if p>=90 then \"A\" elif p>=75 then \"B\" elif p>=50 then \"C\" elif p>=30 then \"D\" else \"F\" end;\n def frac(r): if r==\"strong\" then 1.0 elif r==\"adequate\" then 0.6 else 0.2 end;\n\n ($fresh.files // [] | map({(.path): .}) | add // {}) as $fmap |\n ($commands.summary // {passed:0,skipped:0,failed:0}) as $cmd |\n (($cmd.passed // 0) + ($cmd.failed // 0)) as $cmdTotal |\n\n [ ($struct.files // [])[] |\n .path as $p | .role as $role |\n (.errors // 0) as $se | (.warnings // 0) as $sw |\n\n # Structure (25)\n ((25 - $se*5 - $sw*2) | clamp(25)) as $structure |\n\n # Currency (20)\n ($fmap[$p]) as $fr |\n ( if $fr == null then 8\n elif $fr.status == \"fresh\" then 20\n elif $fr.status == \"unknown\" then 8\n else ( ($fr.commits_since // 0) as $c |\n if $c \u003c= 7 then 15 elif $c \u003c= 14 then 11 elif $c \u003c= 30 then 7 else 4 end )\n end ) as $currency |\n\n # Content (20) - issues attributed to this file\n ( [ ($content.issues // [])[] | select(.file == $p) ] ) as $ci |\n ( [ $ci[] | select(.severity==\"ERROR\") ] | length ) as $ce |\n ( [ $ci[] | select(.severity==\"WARN\") ] | length ) as $cw |\n ((20 - $ce*5 - $cw*2) | clamp(20)) as $content_s |\n\n # Conciseness (20) - by role, from line count\n ($lines[$p] // 0) as $ln |\n ( if $role == \"root\"\n then ( if $ln\u003c=50 then 20 elif $ln\u003c=80 then 16 elif $ln\u003c=150 then 10 else 5 end )\n else ( if $ln\u003c=150 then 20 elif $ln\u003c=250 then 15 else 8 end )\n end ) as $concise |\n\n # Commands (15) - root only, and only if there are verifiable commands\n ( if $role==\"root\" and $cmdTotal>0\n then { applies:true, score: (($cmd.passed*15/$cmdTotal)|floor) }\n else { applies:false, score:0 }\n end ) as $commands_axis |\n\n ( 25+20+20+20 + (if $commands_axis.applies then 15 else 0 end) ) as $maxApplicable |\n ( $structure+$currency+$content_s+$concise + $commands_axis.score ) as $earned |\n (($earned*100/$maxApplicable)|round) as $pct |\n\n # Optional qualitative overlay (non-reproducible): blend agent-judged LLM\n # axes (Architecture 10 / Actionability 8 / Non-obvious 7) into a secondary\n # grade. Only present when --review supplied a rating for this path.\n ($review[$p]) as $rv |\n ( if $rv == null then null\n else (frac($rv.architecture // \"weak\")*10\n + frac($rv.actionability // \"weak\")*8\n + frac($rv.non_obvious // \"weak\")*7) end ) as $llm |\n ( if $llm == null then null\n else ((($earned + $llm) * 100 / ($maxApplicable + 25)) | round) end ) as $blended |\n\n { path:$p, role:$role, percent:$pct, grade: grade($pct),\n review: (if $rv == null then null\n else {ratings:$rv, blended_percent:$blended, blended_grade:grade($blended)} end),\n axes: {\n structure: {score:$structure, max:25},\n currency: {score:$currency, max:20},\n content: {score:$content_s, max:20},\n conciseness: {score:$concise, max:20},\n commands: (if $commands_axis.applies then {score:$commands_axis.score, max:15} else {score:null, max:null, note:\"n/a (scoped / no verifiable commands)\"} end)\n }\n }\n ] | sort_by(.percent) as $files |\n ( [ $files[] | select(.review != null) ] ) as $reviewed |\n { schema:1,\n summary: {\n files: ($files|length),\n average: (if ($files|length)>0 then (([$files[].percent]|add)/($files|length))|round else 0 end),\n grade_counts: ($files | group_by(.grade) | map({(.[0].grade): length}) | add // {}),\n reviewed: ($reviewed|length),\n blended_average: (if ($reviewed|length)>0 then (([$reviewed[].review.blended_percent]|add)/($reviewed|length))|round else null end)\n },\n files: $files\n }\n ')\n\nif [[ \"$JSON\" = true ]]; then\n printf '%s\\n' \"$SCORING\" | jq .\n exit 0\nfi\n\n# --- Human report (rendered from the deterministic scoring document) ----------\nprintf '%s' \"$SCORING\" | jq -r '\n \"AGENTS.md Quality Report - \" + (.summary.files|tostring) + \" file(s), average \"\n + (.summary.average|tostring) + \"/100 (worst-first)\"\n + (if .summary.blended_average != null then \"; with-review avg ~\" + (.summary.blended_average|tostring) else \"\" end),\n \"\",\n ( .files[] |\n \" \" + (.grade) + \" \" + ((.percent|tostring)+\"/100\" | (. + (\" \" * (8 - (.|length)))))\n + \" \" + .path,\n \" structure \" + (.axes.structure.score|tostring) + \"/25\"\n + \" currency \" + (.axes.currency.score|tostring) + \"/20\"\n + \" content \" + (.axes.content.score|tostring) + \"/20\"\n + \" conciseness \" + (.axes.conciseness.score|tostring) + \"/20\"\n + (if .axes.commands.score != null then \" commands \" + (.axes.commands.score|tostring) + \"/15\" else \"\" end),\n ( if .review != null\n then \" with review: \" + .review.blended_grade + \" ~\" + (.review.blended_percent|tostring)\n + \"/100 (non-reproducible)\"\n else empty end )\n )\n'\n\necho \"\"\necho \"Headline grades are deterministic (reproducible). Any 'with review' line is a\"\necho \"qualitative LLM overlay (non-reproducible) - see references/quality-rubric.md.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":10083,"content_sha256":"e260811ce0505a51a9ad298a165fff78a4a32e0a416f1411ced11fe541725911"},{"filename":"scripts/tests/test-agents-file-write-boundary.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# generate-agents.sh writes AGENTS.md files inside the project only. A symlink\n# at a root or scoped AGENTS.md counts as an existing file (kept without\n# --force, like a regular one), and --force or --update replaces the link with\n# the generated file instead of writing through it.\n# test-symlink-write-boundary.sh covers the same rule for CLAUDE.md/GEMINI.md.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A Go module whose internal/ directory becomes a scope.\nmake_fixture() {\n local fx=\"$1\" i\n mkdir -p \"$fx/internal/foo\"\n printf 'module example.com/fixture\\n\\ngo 1.22\\n' > \"$fx/go.mod\"\n for i in $(seq 1 13); do\n printf 'package foo\\n\\nfunc F%d() {}\\n' \"$i\" > \"$fx/internal/foo/f$i.go\"\n done\n git -C \"$fx\" init -q\n git -C \"$fx\" -c [email protected] -c user.name=t add -A\n git -C \"$fx\" -c [email protected] -c user.name=t commit -qm init\n}\ngenerate() { (cd \"$WORK\" && bash \"$GENERATE\" \"$@\" --no-symlinks >/dev/null 2>&1); }\nis_regular() { [ -f \"$1\" ] && [ ! -L \"$1\" ]; }\n\nmkdir -p \"$WORK/outside\"\n\n# --- Test 1: a dangling root symlink is kept, and --force replaces it\nFX=\"$WORK/root-dangling\"\nmake_fixture \"$FX\"\nln -s ../outside/AGENTS.md \"$FX/AGENTS.md\"\ngenerate \"$FX\" || fail \"generate-agents.sh errored\"\n[ -e \"$WORK/outside/AGENTS.md\" ] && fail \"the root AGENTS.md was written through a dangling symlink\"\n[ \"$(readlink \"$FX/AGENTS.md\")\" = ../outside/AGENTS.md ] || fail \"the symlink was changed without --force\"\ngenerate \"$FX\" --force || fail \"generate-agents.sh --force errored\"\n[ -e \"$WORK/outside/AGENTS.md\" ] && fail \"--force wrote the root AGENTS.md through the symlink\"\nis_regular \"$FX/AGENTS.md\" || fail \"--force did not replace the symlink with the generated file\"\npass \"a dangling root symlink is kept, and --force replaces it\"\n\n# --- Test 2: --force does not write through a root symlink to an existing file\nFX=\"$WORK/root-live\"\nmake_fixture \"$FX\"\nprintf 'shared\\n' > \"$WORK/outside/shared.md\"\nln -s ../outside/shared.md \"$FX/AGENTS.md\"\ngenerate \"$FX\" --force || fail \"generate-agents.sh errored\"\n[ \"$(cat \"$WORK/outside/shared.md\")\" = shared ] || fail \"--force overwrote the symlink's target\"\nis_regular \"$FX/AGENTS.md\" || fail \"--force did not replace the symlink\"\npass \"--force replaces a root symlink instead of overwriting its target\"\n\n# --- Test 3: the same for a scoped AGENTS.md\nFX=\"$WORK/scope-live\"\nmake_fixture \"$FX\"\nprintf 'scoped\\n' > \"$WORK/outside/scoped.md\"\nln -s ../../outside/scoped.md \"$FX/internal/AGENTS.md\"\ngenerate \"$FX\" || fail \"generate-agents.sh errored\"\n[ \"$(cat \"$WORK/outside/scoped.md\")\" = scoped ] || fail \"the scoped symlink was written through without --force\"\ngenerate \"$FX\" --force || fail \"generate-agents.sh --force errored\"\n[ \"$(cat \"$WORK/outside/scoped.md\")\" = scoped ] || fail \"--force overwrote the scoped symlink's target\"\nis_regular \"$FX/internal/AGENTS.md\" || fail \"--force did not replace the scoped symlink\"\npass \"a scoped symlink is kept, and --force replaces it instead of writing through\"\n\n# --- Test 4: --update does not write through a symlink either\nFX=\"$WORK/update\"\nmake_fixture \"$FX\"\ngenerate \"$FX\" || fail \"generate-agents.sh errored\"\ncp \"$FX/AGENTS.md\" \"$WORK/outside/generated.md\"\ncp \"$WORK/outside/generated.md\" \"$WORK/outside/generated.orig\"\nrm \"$FX/AGENTS.md\"\nln -s ../outside/generated.md \"$FX/AGENTS.md\"\ngenerate \"$FX\" --update || fail \"generate-agents.sh --update errored\"\ncmp -s \"$WORK/outside/generated.md\" \"$WORK/outside/generated.orig\" || fail \"--update wrote through the symlink\"\nis_regular \"$FX/AGENTS.md\" || fail \"--update did not replace the symlink\"\npass \"--update replaces a symlink instead of writing through it\"\n\n# --- Test 5: a generated file gets the usual permissions, not a temp file's\nFX=\"$WORK/mode\"\nmake_fixture \"$FX\"\n(umask 022 && generate \"$FX\") || fail \"generate-agents.sh errored\"\n[ \"$(stat -c %a \"$FX/AGENTS.md\" 2>/dev/null || stat -f %Lp \"$FX/AGENTS.md\")\" = 644 ] \\\n || fail \"AGENTS.md is not mode 644 under umask 022\"\npass \"a generated AGENTS.md follows the umask\"\n\n# --- Test 6: --force keeps the mode of an existing AGENTS.md\nFX=\"$WORK/keep-mode\"\nmake_fixture \"$FX\"\nprintf 'private\\n' > \"$FX/AGENTS.md\"\nchmod 600 \"$FX/AGENTS.md\"\n(umask 022 && generate \"$FX\" --force) || fail \"generate-agents.sh --force errored\"\n[ \"$(stat -c %a \"$FX/AGENTS.md\" 2>/dev/null || stat -f %Lp \"$FX/AGENTS.md\")\" = 600 ] \\\n || fail \"--force widened the mode of an existing AGENTS.md\"\npass \"--force keeps the mode of an existing AGENTS.md\"\n\n# --- Test 7: a directory named AGENTS.md is refused, not written into\nFX=\"$WORK/dir-target\"\nmake_fixture \"$FX\"\nmkdir \"$FX/AGENTS.md\"\ngenerate \"$FX\" --force && fail \"a directory named AGENTS.md was reported as written\"\n[ -z \"$(ls -A \"$FX/AGENTS.md\")\" ] || fail \"a file was moved into the AGENTS.md directory\"\npass \"a directory named AGENTS.md is refused\"\n\n# --- Test 8: a scope directory that is a symlink out of the project is skipped\nFX=\"$WORK/scope-dir-link\"\nmake_fixture \"$FX\"\nmkdir -p \"$WORK/outside-skill\" \"$FX/skills\"\nprintf -- '---\\nname: x\\ndescription: \"Use when testing.\"\\n---\\n' > \"$WORK/outside-skill/SKILL.md\"\nln -s ../../outside-skill \"$FX/skills/evil\"\ngenerate \"$FX\" || fail \"generate-agents.sh errored\"\n[ -e \"$WORK/outside-skill/AGENTS.md\" ] && fail \"a scoped AGENTS.md was written through a symlinked directory\"\n[ \"$(ls -A \"$WORK/outside-skill\")\" = SKILL.md ] || fail \"files were created in the symlinked scope's target\"\ngrep -q 'skills/evil' \"$FX/AGENTS.md\" && fail \"the root index lists the skipped scope\"\npass \"a scope directory resolving outside the project is skipped\"\n\necho \"\"\necho \"All AGENTS.md write-boundary tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5948,"content_sha256":"4ac1778cd7f3a0ffec2a8d6ad52aff409b4b0054c29c867699b7c273e3717ee1"},{"filename":"scripts/tests/test-agents-scan-dependency-trees.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for dependency-tree exclusion in verify-content.sh and\n# check-freshness.sh.\n#\n# Both scripts inspected every AGENTS.md below the project root, including\n# third-party ones shipped in vendor/, node_modules/ and a TYPO3 extension's\n# .Build/. verify-content.sh then reported those packages' own file references\n# as \"File documented in ... does not exist\" errors against the project (12\n# errors on a TYPO3 extension once `composer install` had run), and\n# check-freshness.sh rated their freshness. validate-structure.sh already\n# excludes the same trees (#84).\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVERIFY=\"$SCRIPTS_DIR/verify-content.sh\"\nFRESHNESS=\"$SCRIPTS_DIR/check-freshness.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A minimal project whose own AGENTS.md is correct, plus third-party AGENTS.md\n# files in the three dependency trees that name files absent from the project.\nFX=\"$WORK/proj\"\nmkdir -p \"$FX/vendor/acme/lib\" \"$FX/node_modules/pkg\" \"$FX/.Build/vendor/acme/ext\" \\\n \"$FX/.Build/public/typo3conf/ext/acme\"\nprintf '# AGENTS.md\\n\\nNo commands documented.\\n' > \"$FX/AGENTS.md\"\n# .Build/public/... is outside any vendor/ directory, so only the .Build\n# exclusion keeps it out; .Build/vendor/... is also covered by */vendor/*.\nfor d in vendor/acme/lib node_modules/pkg .Build/vendor/acme/ext .Build/public/typo3conf/ext/acme; do\n # shellcheck disable=SC2016 # Literal backticks are markdown code spans\n printf '# Third-party\\n\\nSee `AcmeWidgetService.php` and `build-acme.sh`.\\n' > \"$FX/$d/AGENTS.md\"\ndone\n\nout=$(cd \"$FX\" && bash \"$VERIFY\" . 2>&1); rc=$?\n\nfor d in vendor node_modules .Build; do\n if grep -q \"/$d/\" \u003c\u003c\u003c\"$out\"; then\n echo \"$out\" | grep \"/$d/\" | head -3\n fail \"verify-content.sh scanned the $d/ dependency tree\"\n fi\ndone\npass \"dependency trees (vendor, node_modules, .Build) are not scanned\"\n\nif [ \"$rc\" -ne 0 ]; then\n echo \"$out\"\n fail \"a project that is correct on its own files exited non-zero\"\nfi\npass \"project with correct own files passes despite third-party AGENTS.md files\"\n\n# check-freshness.sh needs a git repository; it lists every file it checks.\ngit -C \"$FX\" init -q\nout=$(bash \"$FRESHNESS\" \"$FX\" 2>&1)\ngrep -q \"Checking: AGENTS.md\" \u003c\u003c\u003c\"$out\" \\\n || { echo \"$out\"; fail \"check-freshness.sh did not check the project's own AGENTS.md\"; }\nfor d in vendor node_modules .Build; do\n if grep -q \"$d/\" \u003c\u003c\u003c\"$out\"; then\n echo \"$out\" | grep \"$d/\" | head -3\n fail \"check-freshness.sh checked the $d/ dependency tree\"\n fi\ndone\npass \"check-freshness.sh skips dependency trees and still checks the project's own file\"\n\n# The project's own scoped files must still be verified, and a real defect\n# there must still be reported.\nmkdir -p \"$FX/src\"\n# shellcheck disable=SC2016 # Literal backticks are markdown code spans\nprintf '# AGENTS.md -- src\\n\\nEntry point: `MissingEntryPoint.php`.\\n' > \"$FX/src/AGENTS.md\"\nout=$(cd \"$FX\" && bash \"$VERIFY\" . 2>&1); rc=$?\ngrep -q \"src/AGENTS.md does not exist: MissingEntryPoint.php\" \u003c\u003c\u003c\"$out\" \\\n || { echo \"$out\"; fail \"a missing file documented in the project's own src/AGENTS.md was not reported\"; }\n[ \"$rc\" -ne 0 ] || fail \"a real defect in the project's own scoped file did not fail the run\"\npass \"the project's own scoped files are still verified\"\n\n# A file that exists only inside a dependency tree is not a project file, so\n# naming it in the project's own AGENTS.md must still be reported. The lookup\n# for documented files uses the same three exclusions as the scan above.\ntouch \"$FX/vendor/acme/lib/OnlyInVendor.php\" \\\n \"$FX/node_modules/pkg/OnlyInNodeModules.js\" \\\n \"$FX/.Build/public/typo3conf/ext/acme/OnlyInBuild.php\"\n# shellcheck disable=SC2016 # Literal backticks are markdown code spans\nprintf '# AGENTS.md -- src\\n\\nSee `OnlyInVendor.php`, `OnlyInNodeModules.js` and `OnlyInBuild.php`.\\n' > \"$FX/src/AGENTS.md\"\nout=$(cd \"$FX\" && bash \"$VERIFY\" . 2>&1)\nfor f in OnlyInVendor.php OnlyInNodeModules.js OnlyInBuild.php; do\n grep -q \"src/AGENTS.md does not exist: $f\" \u003c\u003c\u003c\"$out\" \\\n || { echo \"$out\"; fail \"$f exists only in a dependency tree but was accepted as a project file\"; }\ndone\npass \"files that exist only in a dependency tree do not count as documented project files\"\n\necho \"All verify-content.sh dependency-tree tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4564,"content_sha256":"02dfebab8b48e19a06940e55d5ecc929c843b08150f987a90378ad85e4ed3dd7"},{"filename":"scripts/tests/test-claude-import-file.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for CLAUDE.md import-file handling (issue #82).\n#\n# The TYPO3 docs renderer lists Documentation/ via Flysystem, which aborts on\n# any symbolic link, so a CLAUDE.md -> AGENTS.md symlink there breaks docs CI.\n# generate-agents.sh must emit a regular \"@AGENTS.md\" import file for\n# Documentation/ scopes, and validate-structure.sh must accept that file as\n# fully valid, while still warning on other regular CLAUDE.md files.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\nVALIDATE=\"$SCRIPTS_DIR/validate-structure.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# PHP fixture with a Documentation/ scope (>=3 RST files) and enough source\n# files that generate-agents.sh emits a root plus scoped files.\nFX=\"$WORK/php-docs\"\nmkdir -p \"$FX/src\" \"$FX/Documentation\"\ncat > \"$FX/composer.json\" \u003c\u003c'JSON'\n{ \"name\": \"acme/fixture\", \"require\": { \"php\": \"^8.2\" }, \"scripts\": { \"test\": \"phpunit\" } }\nJSON\n# detect-scopes.sh needs MIN_FILES=5 source files for an src scope.\nfor i in 1 2 3 4 5; do printf '\u003c?php\\nclass C%s {}\\n' \"$i\" > \"$FX/src/C$i.php\"; done\nfor i in 1 2 3; do printf 'Chapter %s\\n=========\\n' \"$i\" > \"$FX/Documentation/Ch$i.rst\"; done\ngit -C \"$FX\" init -q\ngit -C \"$FX\" -c [email protected] -c user.name=t add -A\ngit -C \"$FX\" -c [email protected] -c user.name=t commit -qm init\n\n# --- Test 1: generator emits an import FILE, not a symlink, in Documentation/\nbash \"$GENERATE\" \"$FX\" --style=thin >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ -f \"$FX/Documentation/AGENTS.md\" ] \\\n || fail \"fixture did not produce a scoped Documentation/AGENTS.md (scope not detected; fix the fixture)\"\n[ -e \"$FX/Documentation/CLAUDE.md\" ] || fail \"no Documentation/CLAUDE.md generated\"\nif [ -L \"$FX/Documentation/CLAUDE.md\" ]; then\n fail \"Documentation/CLAUDE.md is a symlink -- breaks TYPO3 docs rendering (#82)\"\nfi\ngrep -qE '^@AGENTS\\.md[[:space:]]*

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\"$FX/Documentation/CLAUDE.md\" \\\n || fail \"Documentation/CLAUDE.md lacks the @AGENTS.md import line\"\npass \"generator emits @AGENTS.md import file in Documentation/\"\n\n# Non-hostile scope dirs must still get symlinks.\n[ -e \"$FX/src/CLAUDE.md\" ] || fail \"no src/CLAUDE.md generated (src scope not detected; fixture needs >=5 source files)\"\n[ -L \"$FX/src/CLAUDE.md\" ] || fail \"src/CLAUDE.md should still be a symlink\"\npass \"non-hostile scope dirs still get symlinks\"\n\n# --- Test 2: validator accepts the import file without a warning -------------\nout=$(bash \"$VALIDATE\" \"$FX\" 2>&1)\nif echo \"$out\" | grep -q \"CLAUDE.md is a regular file\"; then\n fail \"validate-structure.sh still warns about the @AGENTS.md import file\"\nfi\npass \"validator accepts the @AGENTS.md import file\"\n\n# --- Test 3: a regular CLAUDE.md without the import still warns --------------\nprintf 'unrelated content\\n' > \"$FX/Documentation/CLAUDE.md\"\nout=$(bash \"$VALIDATE\" \"$FX\" 2>&1)\nif ! echo \"$out\" | grep -q \"CLAUDE.md is a regular file\"; then\n fail \"validate-structure.sh no longer warns about a non-import regular CLAUDE.md\"\nfi\npass \"non-import regular CLAUDE.md still warns\"\n\necho \"All CLAUDE.md import-file regression tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3365,"content_sha256":"741e069a5ce9e1b198974b2be6a7c985e5e353c7f82dd579bc1608d81d6d8dd9"},{"filename":"scripts/tests/test-command-allowlist.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for the smoke-test allowlist in verify-commands.sh (issue #104).\n#\n# is_safe_command() matched the first word of a command and the runner then\n# passed the whole string to `bash -c`, so \"git status; touch PWNED\" was\n# approved on its prefix and both halves ran. Commands carrying shell syntax\n# are now rejected outright.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVERIFY=\"$SCRIPTS_DIR/verify-commands.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nwrite_agents() {\n local dir=\"$1\" cmd=\"$2\"\n mkdir -p \"$dir\"\n cat > \"$dir/AGENTS.md\" \u003c\u003cAGENTS\n# Fixture\n\n## Commands\n\n| Command | Purpose |\n|---------|---------|\n| \\`$cmd\\` | fixture |\nAGENTS\n}\n\n# --- Test 1: a chained command does not run, and is not reported as working\nFX=\"$WORK/chained\"\nwrite_agents \"$FX\" \"git status; touch $FX/PWNED\"\nOUT=\"$(cd \"$FX\" && SMOKE_TEST=true bash \"$VERIFY\" . 2>&1)\" || true\n[ -e \"$FX/PWNED\" ] && fail \"the chained command executed — allowlist bypassed\"\ngrep -q \"Not smoke-tested\" \u003c\u003c\u003c\"$OUT\" || fail \"chained command was not reported as skipped (output was: $OUT)\"\npass \"chained command neither runs nor counts as verified\"\n\n# --- Test 2 (companion): a plain allowlisted command still runs\n# Without this the test above would also pass if everything were rejected.\nFX=\"$WORK/plain\"\nwrite_agents \"$FX\" \"git --version\"\nOUT=\"$(cd \"$FX\" && SMOKE_TEST=true bash \"$VERIFY\" . 2>&1)\" || true\ngrep -q \"command works\" \u003c\u003c\u003c\"$OUT\" || fail \"plain allowlisted command was not executed (output was: $OUT)\"\npass \"plain allowlisted command is still smoke-tested\"\n\n# --- Test 3: the predicate itself, per shell construct\neval \"$(awk '/^is_safe_command\\(\\) \\{/,/^\\}/' \"$VERIFY\")\"\neval \"$(awk '/^has_safe_arguments\\(\\) \\{/,/^\\}/' \"$VERIFY\")\"\nexpect() {\n local cmd=\"$1\" want=\"$2\" got\n if is_safe_command \"$cmd\"; then got=allow; else got=reject; fi\n [ \"$got\" = \"$want\" ] || fail \"is_safe_command: want $want, got $got for \u003c$cmd>\"\n}\nexpect 'git --version' allow\nexpect 'npm test' allow\nexpect 'make -n test' allow\nexpect 'vendor/bin/phpunit --filter Foo' allow\nexpect './gradlew build' allow\nexpect 'git status; touch /tmp/x' reject # separator\nexpect 'npm test && curl http://x' reject # conjunction\nexpect 'npm test | sh' reject # pipe\n# The next two are deliberately single-quoted: the point is that the predicate\n# sees the substitution syntax literally, so it must not expand here.\n# shellcheck disable=SC2016\nexpect 'echo $(id)' reject # substitution\n# shellcheck disable=SC2016\nexpect 'git log `id`' reject # legacy substitution\nexpect 'npm test > /tmp/out' reject # redirection\nexpect 'bash -n scripts/*.sh' reject # glob\nexpect 'rm -rf /' reject # not on the allowlist\nexpect 'curl http://example.com' reject # network fetch, deliberately dropped\npass \"is_safe_command rejects every shell construct that can chain a command\"\n\n# --- Test 4: the first word alone decides nothing.\n# git and docker/podman are smoke-tested only in informational forms. Inspection\n# tools whose own options can run a program or write a file are not smoke-tested\n# at all; a blocklist of their options would always miss one.\nexpect 'git status' reject # reads repository config that can name commands\nexpect 'git -c core.pager=x log' reject # config on the command line\nexpect 'git version' allow\nexpect 'docker ps' allow\nexpect 'docker run alpine touch x' reject\nexpect 'podman -H tcp://x ps' reject\nexpect 'find . -name x' reject\nexpect 'rg TODO src' reject\nexpect 'rg --hostname-bin=./x foo' reject\nexpect 'ag TODO' reject\nexpect 'yq .a f.yaml' reject\nexpect 'file -C -m m' reject\nexpect 'sed -n p README.md' reject\nexpect 'awk -f x.awk README.md' reject\nexpect 'sort -o out README.md' reject\nexpect 'grep -n TODO README.md' allow\nexpect 'cat README.md' allow\nexpect 'jq . package.json' allow\nexpect 'ls -la' allow\npass \"is_safe_command judges git and docker by their arguments and leaves out inspection tools that run programs\"\n\n# --- Test 5: the git case end to end. A repository's own config can name a\n# command that `git status` runs; that command must not run.\nFX=\"$WORK/git-config\"\nwrite_agents \"$FX\" \"git status\"\ngit -C \"$FX\" init -q\ngit -C \"$FX\" config core.fsmonitor \"touch $FX/PWNED; false\"\nOUT=\"$(cd \"$FX\" && SMOKE_TEST=true bash \"$VERIFY\" . 2>&1)\" || true\n[ -e \"$FX/PWNED\" ] && fail \"a command from the repository's git config ran during the smoke test\"\npass \"git status is not smoke-run in the analysed repository\"\n\necho \"\"\necho \"All command allowlist tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4844,"content_sha256":"e7d2840376d2b577898430f3cc9d71bb007116eb6f7c762617f13a72dc4be7bb"},{"filename":"scripts/tests/test-dependency-tree-exclusion.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for dependency-tree exclusion in validate-structure.sh (issue #84).\n#\n# The CLAUDE.md-symlink scan excludes vendor/ and node_modules/, but the scoped\n# AGENTS.md scan did not — so third-party AGENTS.md files shipped inside an\n# installed composer vendor/ tree were validated as if they were the project's\n# own, reporting their missing sections as project errors. TYPO3 extensions\n# install dependencies under .Build/vendor/, so that path must be excluded too.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVALIDATE=\"$SCRIPTS_DIR/validate-structure.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A minimal project whose OWN files are valid, plus third-party AGENTS.md files\n# in the three dependency trees. Only the dependency files are non-conforming.\nFX=\"$WORK/proj\"\nmkdir -p \"$FX/vendor/acme/lib\" \"$FX/node_modules/pkg\" \"$FX/.Build/vendor/acme/ext\"\n\ncat > \"$FX/AGENTS.md\" \u003c\u003c'MD'\n\u003c!-- Managed by agent: keep sections and order; edit content, not structure -->\n\n# AGENTS.md\n\n**Precedence:** The **closest AGENTS.md** to changed files wins. Root holds global defaults only.\n\n## Index of scoped AGENTS.md\n\n- nothing scoped yet\nMD\nln -s AGENTS.md \"$FX/CLAUDE.md\"\n\nfor d in vendor/acme/lib node_modules/pkg .Build/vendor/acme/ext; do\n printf '# Third-party AGENTS.md\\n\\nNo managed header, no required sections.\\n' > \"$FX/$d/AGENTS.md\"\ndone\n\nout=$(bash \"$VALIDATE\" \"$FX\" 2>&1); rc=$?\n\nfor d in vendor node_modules .Build; do\n if grep -q \"/$d/\" \u003c\u003c\u003c\"$out\"; then\n echo \"$out\" | grep \"/$d/\" | head -3\n fail \"validate-structure.sh scanned the $d/ dependency tree (#84)\"\n fi\ndone\npass \"dependency trees (vendor, node_modules, .Build) are not scanned\"\n\nif [ \"$rc\" -ne 0 ]; then\n echo \"$out\"\n fail \"a project that is valid on its own files exited non-zero\"\nfi\npass \"project with valid own files passes despite third-party AGENTS.md files\"\n\n# The project's own scoped files must still be validated.\nmkdir -p \"$FX/src\"\nprintf '# AGENTS.md -- src\\n\\nNo managed header, no required sections.\\n' > \"$FX/src/AGENTS.md\"\nout=$(bash \"$VALIDATE\" \"$FX\" 2>&1)\ngrep -q \"src/AGENTS.md\" \u003c\u003c\u003c\"$out\" || fail \"the project's own scoped AGENTS.md was skipped\"\npass \"the project's own scoped files are still validated\"\n\necho \"All dependency-tree exclusion tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2535,"content_sha256":"279a12c2f8cbde6f25d6cc784df5741cd024a7a40551a8207e856495d25bb8d9"},{"filename":"scripts/tests/test-extract-commands-real-scripts.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test: extract-commands.sh must emit the composer script that the\n# project actually defines, never the first name it happened to look for.\n#\n# The bug: the PHP branch accepted `.scripts.format // .scripts[\"cs:fix\"]` as\n# evidence and then printed `composer run format` unconditionally. A project\n# defining only `cs:fix` therefore had `composer run format` written into its\n# AGENTS.md — a command that does not exist, presented to agents as fact.\n# The same shape applied to `lint` (via `cs:check`) and `phpstan` (via `stan`).\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nEXTRACT=\"$(dirname \"$SCRIPT_DIR\")/extract-commands.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A PHP project whose scripts use the alternate names only.\nmkdir -p \"$WORK/alt\"\ncat > \"$WORK/alt/composer.json\" \u003c\u003c'JSON'\n{\n \"name\": \"acme/alt-names\",\n \"require\": {\"php\": \"^8.2\"},\n \"scripts\": {\n \"cs:fix\": \"php-cs-fixer fix\",\n \"cs:check\": \"php-cs-fixer fix --dry-run\",\n \"stan\": \"phpstan analyse\"\n }\n}\nJSON\n\nOUT=$(bash \"$EXTRACT\" \"$WORK/alt\" 2>/dev/null)\n\nfor pair in \"format:cs:fix\" \"lint:cs:check\" \"typecheck:stan\"; do\n field=\"${pair%%:*}\"\n want=\"composer run ${pair#*:}\"\n got=$(printf '%s' \"$OUT\" | jq -r --arg f \"$field\" '.[$f] // \"\"')\n [ \"$got\" = \"$want\" ] || fail \"$field: expected '$want', got '$got'\"\ndone\npass \"alternate script names are emitted as themselves\"\n\n# Every emitted `composer run X` must name a script the project defines.\n# This is the invariant the bug violated, stated independently of which\n# names the extractor happens to prefer today.\nwhile read -r key; do\n [ -n \"$key\" ] || continue\n jq -e --arg k \"$key\" '.scripts[$k]' \"$WORK/alt/composer.json\" >/dev/null 2>&1 \\\n || fail \"extractor emitted 'composer run $key', which composer.json does not define\"\ndone \u003c \u003c(printf '%s' \"$OUT\" | jq -r '.[] | select(type == \"string\") | select(startswith(\"composer run \")) | sub(\"composer run \";\"\")')\npass \"every emitted composer command exists in composer.json\"\n\n# The preferred names still win when they are present.\nmkdir -p \"$WORK/std\"\ncat > \"$WORK/std/composer.json\" \u003c\u003c'JSON'\n{\n \"name\": \"acme/standard-names\",\n \"require\": {\"php\": \"^8.2\"},\n \"scripts\": {\"format\": \"php-cs-fixer fix\", \"cs:fix\": \"should not be chosen\"}\n}\nJSON\ngot=$(bash \"$EXTRACT\" \"$WORK/std\" 2>/dev/null | jq -r '.format')\n[ \"$got\" = \"composer run format\" ] || fail \"preferred name not chosen: got '$got'\"\npass \"the preferred script name still wins when defined\"\n\n# The same defect shape lived in the package.json branch: `typecheck` was\n# accepted via `type-check`, and `dev` via `start`, while the emitted command\n# always used the first name.\nmkdir -p \"$WORK/npm\"\ncat > \"$WORK/npm/package.json\" \u003c\u003c'JSON'\n{\n \"name\": \"acme-alt\",\n \"scripts\": {\n \"type-check\": \"tsc --noEmit\",\n \"start\": \"vite\",\n \"lint\": \"eslint .\"\n }\n}\nJSON\n\nNPM_OUT=$(bash \"$EXTRACT\" \"$WORK/npm\" 2>/dev/null)\n\nfor pair in \"typecheck:type-check\" \"dev:start\"; do\n field=\"${pair%%:*}\"\n want=\"npm run ${pair#*:}\"\n got=$(printf '%s' \"$NPM_OUT\" | jq -r --arg f \"$field\" '.[$f] // \"\"')\n [ \"$got\" = \"$want\" ] || fail \"$field: expected '$want', got '$got'\"\ndone\npass \"alternate package.json script names are emitted as themselves\"\n\n# Same invariant as for composer, stated for the npm runner: an emitted\n# `\u003cpm> run X` must name a script package.json defines. Commands that are not\n# script invocations (npx/tsc/eslint fallbacks) are out of scope here.\nwhile read -r key; do\n [ -n \"$key\" ] || continue\n jq -e --arg k \"$key\" '.scripts[$k]' \"$WORK/npm/package.json\" >/dev/null 2>&1 \\\n || fail \"extractor emitted 'npm run $key', which package.json does not define\"\ndone \u003c \u003c(printf '%s' \"$NPM_OUT\" | jq -r '.[] | select(type == \"string\") | select(startswith(\"npm run \")) | sub(\"npm run \";\"\")')\npass \"every emitted npm script command exists in package.json\"\n\necho \"All extract-commands script-name regression tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4129,"content_sha256":"8fb0f63a28ffd0a9f7dc92e15031bbf3f83d39bc476dd02c9a6003bcc9a9bb7a"},{"filename":"scripts/tests/test-git-history-counts.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# analyze-git-history.sh counts matches with grep -c. grep prints 0 and exits 1\n# when nothing matches, so a count written as `$(... | grep -c ... || echo 0)`\n# held \"0\" followed by a second \"0\", and the arithmetic that read it failed. A\n# repository whose tags match neither semver nor a leading v, and whose\n# branches or commit subjects match none of the patterns, must still produce\n# the JSON with those counts at 0.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nexport GIT_CONFIG_GLOBAL=/dev/null\ngit_t() { git -C \"$1\" -c [email protected] -c user.name=t -c commit.gpgsign=false -c tag.gpgsign=false \"${@:2}\"; }\n\n# One commit, one annotated tag that is neither semver, calver nor v-prefixed.\nfx=\"$WORK/no-match\"\ngit init -q \"$fx\"\necho a > \"$fx/a\"\ngit_t \"$fx\" add a\ngit_t \"$fx\" commit -qm \"initial commit\"\ngit_t \"$fx\" tag -a fixture-1.0.0 -m fixture\n\nout=\"$(bash \"$SCRIPTS_DIR/analyze-git-history.sh\" \"$fx\" 2>\"$WORK/err\")\"\nrc=$?\n[ \"$rc\" -eq 0 ] || fail \"exit $rc on a repository without a pattern match: $(cat \"$WORK/err\")\"\n[ ! -s \"$WORK/err\" ] || fail \"stderr not empty: $(cat \"$WORK/err\")\"\njq -e . \u003c\u003c\u003c\"$out\" >/dev/null || fail \"output is not JSON\"\npass \"no pattern matches: exit 0, clean stderr, JSON output\"\n\npattern=\"$(jq -r '.release_tagging.pattern // .releases.pattern // empty' \u003c\u003c\u003c\"$out\")\"\n[ \"$pattern\" = custom ] || fail \"tag pattern is '$pattern', expected custom: $out\"\npass \"unmatched tag is reported as a custom pattern\"\n\n# A branch on the remote side that matches nothing in the branch patterns.\nremote=\"$WORK/remote\"\ngit init -q --bare \"$remote\"\ngit_t \"$fx\" remote add origin \"$remote\"\ngit_t \"$fx\" push -q origin HEAD:refs/heads/feature/x\ngit_t \"$fx\" fetch -q origin\nout=\"$(bash \"$SCRIPTS_DIR/analyze-git-history.sh\" \"$fx\" 2>\"$WORK/err\")\"\nrc=$?\n[ \"$rc\" -eq 0 ] || fail \"exit $rc with a remote branch: $(cat \"$WORK/err\")\"\n[ ! -s \"$WORK/err\" ] || fail \"stderr not empty with a remote branch: $(cat \"$WORK/err\")\"\njq -e . \u003c\u003c\u003c\"$out\" >/dev/null || fail \"output with a remote branch is not JSON\"\npass \"remote branch without a pattern match: exit 0, clean stderr, JSON output\"\n\n# generate-agents.sh counts the table rows of its generated sections the same\n# way before it prunes an AGENTS.md over the byte budget. A section without a\n# table row (grep -c prints 0 and exits 1) must read as 0 rows, not as \"0\\n0\".\n# The function is taken from the script itself; replace_file is the only helper\n# it needs besides log.\nfn_file=\"$WORK/enforce_byte_budget.sh\"\nawk '/^enforce_byte_budget\\(\\) \\{/ { f = 1 } f { print } f && /^}/ { exit }' \\\n \"$SCRIPTS_DIR/generate-agents.sh\" > \"$fn_file\"\n[ -s \"$fn_file\" ] || fail \"enforce_byte_budget not found in generate-agents.sh\"\nbudget_file=\"$WORK/AGENTS.md\"\n{\n for section in golden-samples heuristics utilities; do\n echo \"\u003c!-- AGENTS-GENERATED:START $section -->\"\n echo \"no table rows in this section\"\n echo \"\u003c!-- AGENTS-GENERATED:END $section -->\"\n done\n printf 'filler line to exceed the budget\\n%.0s' 1 2 3 4 5 6 7 8 9 10\n} > \"$budget_file\"\nerr=\"$(bash -c '\n set -euo pipefail\n log() { :; }\n replace_file() { cat > \"$1\"; }\n source \"$1\"\n enforce_byte_budget \"$2\" 10\n' _ \"$fn_file\" \"$budget_file\" 2>&1 >/dev/null)\" || fail \"enforce_byte_budget failed: $err\"\n[ -z \"$err\" ] || fail \"enforce_byte_budget wrote to stderr: $err\"\npass \"sections without table rows count as 0 rows in enforce_byte_budget\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3684,"content_sha256":"d07f122790d348ac6e4ca58c89febb66614f3b2e4851dbc051c0f3049edd9b15"},{"filename":"scripts/tests/test-golden-sample-file-names.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test: the golden-sample lookup in generate-agents.sh must treat\n# the target project's file names as data.\n#\n# It counted lines with `xargs -I{} sh -c 'wc -l \"{}\" ...'`, which pastes each\n# file name into a shell command string, so a file named `a$(touch X).go`\n# ran `touch X`. The file names now reach `wc` as arguments.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A Go module whose internal/ directory holds enough files to become a scope,\n# so the golden-sample lookup runs for it.\nFX=\"$WORK/proj\"\nmkdir -p \"$FX/internal/foo\" \"$FX/internal/bar\"\nprintf 'module example.com/fixture\\n\\ngo 1.22\\n' > \"$FX/go.mod\"\nfor i in $(seq 1 13); do\n printf 'package foo\\n\\nfunc F%d() {}\\n' \"$i\" > \"$FX/internal/foo/f$i.go\"\n printf 'package bar\\n\\nfunc B%d() {}\\n' \"$i\" > \"$FX/internal/bar/b$i.go\"\ndone\n# The largest file, so the lookup has one clear answer.\n{ printf 'package foo\\n\\n'; for i in $(seq 1 40); do printf 'func Big%d() {}\\n' \"$i\"; done; } \\\n > \"$FX/internal/foo/big.go\"\n# shellcheck disable=SC2016 # the literal $( in the file name is the test input\ntouch \"$FX/internal/foo/\"'a$(touch PWNED).go'\ngit -C \"$FX\" init -q\ngit -C \"$FX\" -c [email protected] -c user.name=t add -A\ngit -C \"$FX\" -c [email protected] -c user.name=t commit -qm init\n\n(cd \"$WORK\" && bash \"$GENERATE\" \"$FX\" --no-symlinks >/dev/null 2>&1) \\\n || fail \"generate-agents.sh errored\"\n\n[ -f \"$FX/internal/AGENTS.md\" ] \\\n || fail \"internal/ did not become a scope, so the lookup never ran\"\n\nif [ -e \"$FX/PWNED\" ] || [ -e \"$WORK/PWNED\" ] || [ -e \"$FX/internal/PWNED\" ]; then\n fail \"a file name from the target project was executed as shell code\"\nfi\npass \"file names from the target project are not executed\"\n\ngrep -qF 'internal/foo/big.go' \"$FX/internal/AGENTS.md\" \\\n || fail \"the golden sample is not the largest file (internal/foo/big.go)\"\npass \"the largest file is still chosen as the golden sample\"\n\necho \"All golden-sample file-name tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2286,"content_sha256":"6d37646cefeb1e2e31d1f122c1353978ab01942f495f2fde228ee9547efc04eb"},{"filename":"scripts/tests/test-json-manifest.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Test for the generate-agents.sh write manifest (issue #107).\n#\n# generate-agents.sh is the only script in the skill that writes, and was the\n# only one without --json. The manifest lists one entry per path the run\n# touches, so a caller can review exactly what would change (--dry-run) or what\n# did change (without it) instead of parsing nine different prose lines.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nmake_fixture() {\n local fx=\"$1\"\n mkdir -p \"$fx/src\"\n cat > \"$fx/package.json\" \u003c\u003c'JSON'\n{ \"name\": \"fixture\", \"version\": \"1.0.0\", \"scripts\": { \"test\": \"echo t\" } }\nJSON\n for i in 1 2 3 4 5 6; do printf 'export const c%s = %s\\n' \"$i\" \"$i\" > \"$fx/src/c$i.js\"; done\n git -C \"$fx\" init -q\n git -C \"$fx\" -c [email protected] -c user.name=t add -A\n git -C \"$fx\" -c [email protected] -c user.name=t commit -qm init\n}\n\n# --- Test 1: --dry-run --json is valid JSON, carries the schema, writes nothing\nFX=\"$WORK/plan\"\nmake_fixture \"$FX\"\nprintf '# my own rules\\n' > \"$FX/GEMINI.md\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --dry-run --json 2>/dev/null)\" || fail \"generate-agents.sh errored\"\njq -e . >/dev/null 2>&1 \u003c\u003c\u003c\"$OUT\" || fail \"output is not valid JSON: $OUT\"\n[ \"$(jq -r '.script' \u003c\u003c\u003c\"$OUT\")\" = \"generate-agents\" ] || fail \"wrong script field\"\n[ \"$(jq -r '.schema' \u003c\u003c\u003c\"$OUT\")\" = \"1\" ] || fail \"wrong schema field\"\n[ \"$(jq -r '.dry_run' \u003c\u003c\u003c\"$OUT\")\" = \"true\" ] || fail \"dry_run not true under --dry-run\"\n[ -e \"$FX/AGENTS.md\" ] && fail \"--dry-run --json wrote AGENTS.md\"\n[ -e \"$FX/CLAUDE.md\" ] && fail \"--dry-run --json created a symlink\"\npass \"--dry-run --json emits a valid manifest and writes nothing\"\n\n# --- Test 2: human output is suppressed, so stdout is parseable\ngrep -q '✅' \u003c\u003c\u003c\"$OUT\" && fail \"human output leaked into the JSON stream\"\npass \"--json suppresses the prose output\"\n\n# --- Test 3: every operation kind appears, with the fields it needs\n[ \"$(jq -r '[.operations[] | select(.op==\"write\" and .path==\"AGENTS.md\")] | length' \u003c\u003c\u003c\"$OUT\")\" = \"1\" ] \\\n || fail \"root AGENTS.md missing from the manifest\"\n[ \"$(jq -r '.operations[] | select(.path==\"CLAUDE.md\") | .target' \u003c\u003c\u003c\"$OUT\")\" = \"AGENTS.md\" ] \\\n || fail \"symlink entry has no target\"\n[ \"$(jq -r '.operations[] | select(.path==\"GEMINI.md\") | .op' \u003c\u003c\u003c\"$OUT\")\" = \"keep\" ] \\\n || fail \"the kept regular file is not recorded as keep\"\njq -e '.operations[] | select(.path==\"GEMINI.md\") | .reason' >/dev/null \u003c\u003c\u003c\"$OUT\" \\\n || fail \"keep entry carries no reason\"\npass \"write, symlink and keep entries carry their fields\"\n\n# --- Test 4: paths are relative to the project root, never absolute\njq -e '[.operations[].path | select(startswith(\"/\"))] | length == 0' >/dev/null \u003c\u003c\u003c\"$OUT\" \\\n || fail \"manifest contains absolute paths\"\npass \"paths are relative to the project root\"\n\n# --- Test 5: the summary counts what the operations list holds\nfor op in write symlink keep; do\n want=\"$(jq -r --arg op \"$op\" '[.operations[] | select(.op==$op)] | length' \u003c\u003c\u003c\"$OUT\")\"\n got=\"$(jq -r --arg op \"$op\" '.summary[$op] // 0' \u003c\u003c\u003c\"$OUT\")\"\n [ \"$want\" = \"$got\" ] || fail \"summary.$op says $got, operations hold $want\"\ndone\npass \"summary matches the operations list\"\n\n# --- Test 6: without --dry-run the same manifest is a receipt of real writes\nFX=\"$WORK/receipt\"\nmake_fixture \"$FX\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --json 2>/dev/null)\" || fail \"generate-agents.sh errored\"\n[ \"$(jq -r '.dry_run' \u003c\u003c\u003c\"$OUT\")\" = \"false\" ] || fail \"dry_run not false on a real run\"\n[ -f \"$FX/AGENTS.md\" ] || fail \"real run wrote no AGENTS.md\"\nwhile read -r p; do\n [ -e \"$FX/$p\" ] || fail \"manifest claims $p but it does not exist\"\ndone \u003c \u003c(jq -r '.operations[] | select(.op!=\"keep\") | .path' \u003c\u003c\u003c\"$OUT\")\npass \"every non-keep path in the receipt exists on disk\"\n\n# --- Test 7: the prose output still works without --json (companion)\nFX=\"$WORK/prose\"\nmake_fixture \"$FX\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --dry-run 2>/dev/null)\" || fail \"generate-agents.sh errored\"\ngrep -q 'DRY-RUN' \u003c\u003c\u003c\"$OUT\" || fail \"prose output lost its DRY-RUN lines\"\njq -e . >/dev/null 2>&1 \u003c\u003c\u003c\"$OUT\" && fail \"prose run emitted JSON\"\npass \"the default output is unchanged\"\n\n# --- Test 8: a file left alone is recorded, not merely absent from the list\n# Without this, a consumer cannot tell \"AGENTS.md was skipped because it exists\"\n# from \"AGENTS.md was never considered\" — both look like a missing entry.\nFX=\"$WORK/second-run\"\nmake_fixture \"$FX\"\nbash \"$GENERATE\" \"$FX\" >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --dry-run --json 2>/dev/null)\" || fail \"generate-agents.sh errored\"\n[ \"$(jq -r '.operations[] | select(.path==\"AGENTS.md\") | .op' \u003c\u003c\u003c\"$OUT\")\" = \"keep\" ] \\\n || fail \"an existing AGENTS.md is not recorded as keep on a second run\"\n[ \"$(jq -r '.operations[] | select(.path==\"AGENTS.md\") | .reason' \u003c\u003c\u003c\"$OUT\")\" = \"already exists\" ] \\\n || fail \"the keep entry for AGENTS.md carries no reason\"\npass \"an existing AGENTS.md is recorded as keep, with its reason\"\n\n# --- Test 9: --update writes, and the manifest says so\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --update --json 2>/dev/null)\" || fail \"generate-agents.sh errored\"\n[ \"$(jq -r '.operations[] | select(.path==\"AGENTS.md\") | .op' \u003c\u003c\u003c\"$OUT\")\" = \"write\" ] \\\n || fail \"--update rewrote AGENTS.md without recording it as a write\"\npass \"--update records the rewrite it performs\"\n\necho \"\"\necho \"All JSON manifest tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5676,"content_sha256":"bdfa093710b8a01bd8b8334daf49595239d3f44a8240a56e3d732aa061b02b5c"},{"filename":"scripts/tests/test-last-verified-opt-in.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# verify-commands.sh checks AGENTS.md; it changes the file only when asked.\n# A plain run leaves AGENTS.md byte for byte as it was, so running the check\n# (in CI, as a local gate) never leaves a modified file behind. With\n# --update-verified, a successful run records the date in the \"Last verified\"\n# marker.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVERIFY=\"$SCRIPTS_DIR/verify-commands.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nFX=\"$WORK/project\"\nmkdir -p \"$FX\"\n# shellcheck disable=SC2016 # backticks are markdown, not substitution\nprintf '\u003c!-- Last updated: 2026-01-01 | Last verified: never -->\\n# Fixture\\n\\n## Commands\\n\\n| Command | Purpose |\\n|---|---|\\n| `ls -la` | list |\\n' > \"$FX/AGENTS.md\"\ncp \"$FX/AGENTS.md\" \"$WORK/before.md\"\n\n(cd \"$FX\" && bash \"$VERIFY\" . >/dev/null 2>&1) || fail \"verify-commands.sh errored\"\ncmp -s \"$FX/AGENTS.md\" \"$WORK/before.md\" || fail \"a plain run modified AGENTS.md\"\npass \"a plain run leaves AGENTS.md unchanged\"\n\n(cd \"$FX\" && bash \"$VERIFY\" . --update-verified >/dev/null 2>&1) || fail \"verify-commands.sh --update-verified errored\"\ngrep -q \"Last verified: $(date +%Y-%m-%d) -->\" \"$FX/AGENTS.md\" \\\n || fail \"--update-verified did not record today's date (file: $(head -1 \"$FX/AGENTS.md\"))\"\npass \"--update-verified records the date in the Last verified marker\"\n\necho \"\"\necho \"All Last verified tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1611,"content_sha256":"0ce1266310892599216f4bebb3c159bd9342e6a305b8b9286e514be054d53248"},{"filename":"scripts/tests/test-make-target-lookup.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# verify-commands.sh looks up documented `make \u003ctarget>` commands by reading the\n# makefile and the files it includes. It does not run make for that: even\n# `make -n` expands $(shell ...) while it parses, so checking whether a target\n# exists would run the project's commands.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVERIFY=\"$SCRIPTS_DIR/verify-commands.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nFX=\"$WORK/project\"\nmkdir -p \"$FX/Make\"\n# Recipe lines start with a tab. The $(shell ...) is literal makefile text.\n# shellcheck disable=SC2016\nprintf 'STAMP := $(shell touch %s/parsed)\\ninclude Make/*.mk\\n-include missing.mk\\n\\nbuild: deps\\n\\techo build\\n\\ndeps:\\n\\techo deps\\n\\nVERSION := 1\\n' \\\n \"$WORK\" > \"$FX/Makefile\"\nprintf 'lint test-unit:: \\n\\techo lint\\n\\nplan-%%:\\n\\techo $*\\n\\n.PHONY: lint docs-only\\n' > \"$FX/Make/quality.mk\"\ncat > \"$FX/AGENTS.md\" \u003c\u003c'AGENTS'\n# Fixture\n\n## Commands\n\n| Task | Command |\n|------|---------|\n| Build | `make build` |\n| Lint | `make lint` |\n| Unit | `make test-unit` |\n| Plan | `make plan-prod` |\n| Phony | `make docs-only` |\n| Missing | `make release` |\n| Variable | `make VERSION` |\nAGENTS\n\nOUT=\"$(cd \"$FX\" && DRY_RUN=true bash \"$VERIFY\" . 2>&1)\" || true\n\n[ -e \"$WORK/parsed\" ] && fail \"the makefile was evaluated while looking up targets\"\npass \"looking up targets does not evaluate the makefile\"\n\nfor t in build lint test-unit plan-prod docs-only; do\n grep -q \"make target exists: $t\" \u003c\u003c\u003c\"$OUT\" || fail \"target '$t' was not found (output was: $OUT)\"\ndone\npass \"targets, .PHONY names and pattern rules in the makefile and its includes are found\"\n\ngrep -q \"make target not found: release\" \u003c\u003c\u003c\"$OUT\" || fail \"a missing target was not reported (output was: $OUT)\"\ngrep -q \"make target not found: VERSION\" \u003c\u003c\u003c\"$OUT\" || fail \"a variable assignment was taken for a target (output was: $OUT)\"\npass \"a missing target and a variable are reported as not found\"\n\n# macOS realpath has no -e; the lookup must not depend on it.\nmkdir -p \"$WORK/bin\"\nprintf '#!/bin/sh\\nexit 1\\n' > \"$WORK/bin/realpath\"\nchmod +x \"$WORK/bin/realpath\"\nOUT=\"$(cd \"$FX\" && PATH=\"$WORK/bin:$PATH\" DRY_RUN=true bash \"$VERIFY\" . 2>&1)\" || true\ngrep -q \"make target exists: lint\" \u003c\u003c\u003c\"$OUT\" || fail \"the lookup needs realpath (output was: $OUT)\"\npass \"the lookup works without a usable realpath\"\n\necho \"\"\necho \"All make target lookup tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2609,"content_sha256":"c29c4cd5c219304dcf6b3f5583e1801584e962a9000eb7616f4d67ea6a2750e5"},{"filename":"scripts/tests/test-merge-heuristic.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# The \"Merging PRs\" heuristic in a generated AGENTS.md is guessed from git\n# history. When the repository's settings are known, the advice never names a\n# merge method they do not allow, and when they allow exactly one, that one is\n# the advice. Without settings (no gh login, not GitHub), the history guess\n# stands as before.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\neval \"$(awk '/^build_workflow_info\\(\\) \\{/,/^\\}/' \"$GENERATE\")\"\n\npr_line() { # pr_line \u003chistory strategy> \u003csettings json>\n build_workflow_info \"{\\\"merge_strategy\\\":{\\\"strategy\\\":\\\"$1\\\"}}\" \"$2\" | grep '^- PRs:' || true\n}\nexpect() { # expect \u003chistory> \u003csettings> \u003cwanted line or empty>\n local got\n got=\"$(pr_line \"$1\" \"$2\")\"\n [ \"$got\" = \"$3\" ] || fail \"history=$1 settings=$2: want '$3', got '$got'\"\n}\n\nexpect squash-and-merge '{\"merge_strategies\":[\"merge\"]}' '- PRs: Create merge commits'\nexpect squash-and-merge '{\"merge_strategies\":[\"rebase\"]}' '- PRs: Rebase and merge'\nexpect merge-commits '{\"merge_strategies\":[\"squash\",\"rebase\"]}' ''\nexpect mixed '{\"merge_strategies\":[\"merge\"]}' '- PRs: Create merge commits'\npass \"a merge method the settings do not allow is never advised\"\n\nexpect squash-and-merge '{\"merge_strategies\":[\"squash\",\"merge\"]}' '- PRs: Squash and merge'\nexpect squash-and-merge '{}' '- PRs: Squash and merge'\nexpect merge-commits '{}' '- PRs: Create merge commits'\nexpect mixed '{}' ''\npass \"without settings, or when they allow it, the history guess stands\"\n\necho \"\"\necho \"All merge heuristic tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1799,"content_sha256":"b8eee18159264bd0d978bfd104170f49a2bdf1ca9472dbac001f9771bf4f9a4f"},{"filename":"scripts/tests/test-scope-index-heading.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for the scope-index heading contract between\n# generate-agents.sh and validate-structure.sh (issue #55).\n#\n# generate-agents.sh emits the root scope index under the heading\n# \"## Scoped AGENTS.md (MUST read when working in these directories)\"\n# while older output used the legacy heading\n# \"## Index of scoped AGENTS.md\".\n# validate-structure.sh must accept BOTH so a freshly generated root does not\n# fail the skill's own structure validation, while a bloated root without any\n# scope index still fails.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\nVALIDATE=\"$SCRIPTS_DIR/validate-structure.sh\"\n\nNEW_HEADING='## Scoped AGENTS.md (MUST read when working in these directories)'\nLEGACY_HEADING='## Index of scoped AGENTS.md'\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# Build a minimal repo with a scoped directory so the generated root exceeds\n# 50 lines and includes a populated scope index.\nbuild_fixture() {\n local dir=\"$1\"\n rm -rf \"$dir\"\n mkdir -p \"$dir/src\" \"$dir/.github/workflows\"\n cat > \"$dir/package.json\" \u003c\u003c'JSON'\n{ \"name\": \"fixture\", \"version\": \"1.0.0\", \"scripts\": { \"test\": \"vitest\", \"build\": \"tsc\" } }\nJSON\n echo \"console.log('hi')\" > \"$dir/src/index.ts\"\n echo \"name: ci\" > \"$dir/.github/workflows/ci.yml\"\n git -C \"$dir\" init -q\n git -C \"$dir\" -c [email protected] -c user.name=t add -A\n git -C \"$dir\" -c [email protected] -c user.name=t commit -qm init\n}\n\n# --- Test 1: generated root (new heading) passes validation -----------------\nFX1=\"$WORK/new-heading\"\nbuild_fixture \"$FX1\"\nbash \"$GENERATE\" \"$FX1\" --style=thin >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n\ngrep -qF \"$NEW_HEADING\" \"$FX1/AGENTS.md\" \\\n || fail \"generator no longer emits the expected heading: $NEW_HEADING\"\nlines=$(wc -l \u003c \"$FX1/AGENTS.md\")\n[ \"$lines\" -gt 50 ] || fail \"generated root is only $lines lines; expected >50 to exercise the scope-index path\"\n\nif bash \"$VALIDATE\" \"$FX1\" >/dev/null 2>&1; then\n pass \"generated root ($lines lines, new heading) validates\"\nelse\n fail \"validate-structure.sh rejected a freshly generated root (#55 regression)\"\nfi\n\n# --- Test 2: legacy heading still accepted (backward compatibility) ----------\nFX2=\"$WORK/legacy-heading\"\ncp -r \"$FX1\" \"$FX2\"\n# Rewrite only the heading line, keeping the rest of the generated root intact.\n# Use a temp file + mv rather than `sed -i` so the test stays portable across\n# GNU sed (Linux) and BSD sed (macOS); this repo's CI also runs on macos-latest.\nsed \"s|^${NEW_HEADING}\\$|${LEGACY_HEADING}|\" \"$FX2/AGENTS.md\" > \"$FX2/AGENTS.md.tmp\"\nmv \"$FX2/AGENTS.md.tmp\" \"$FX2/AGENTS.md\"\ngrep -qF \"$LEGACY_HEADING\" \"$FX2/AGENTS.md\" || fail \"could not rewrite heading to legacy form\"\n\nif bash \"$VALIDATE\" \"$FX2\" >/dev/null 2>&1; then\n pass \"root with legacy heading still validates (backward compatible)\"\nelse\n fail \"validate-structure.sh rejected the legacy scope-index heading\"\nfi\n\n# --- Test 3: title-case heading (older generated roots) accepted (#81) -------\nTITLE_HEADING='## Index of Scoped AGENTS.md'\nFX4=\"$WORK/title-case-heading\"\ncp -r \"$FX1\" \"$FX4\"\n# Temp file + mv keeps this portable across GNU and BSD sed (see Test 2).\nsed \"s|^${NEW_HEADING}\\$|${TITLE_HEADING}|\" \"$FX4/AGENTS.md\" > \"$FX4/AGENTS.md.tmp\"\nmv \"$FX4/AGENTS.md.tmp\" \"$FX4/AGENTS.md\"\ngrep -qF \"$TITLE_HEADING\" \"$FX4/AGENTS.md\" || fail \"could not rewrite heading to title-case form\"\n\nif bash \"$VALIDATE\" \"$FX4\" >/dev/null 2>&1; then\n pass \"root with title-case heading validates (#81)\"\nelse\n fail \"validate-structure.sh rejected the title-case scope-index heading (#81 regression)\"\nfi\n\n# --- Test 4: bloated root without any scope index still fails ----------------\nFX3=\"$WORK/no-heading\"\ncp -r \"$FX1\" \"$FX3\"\n# Drop the scope-index heading so the >50-line root has no index at all.\n# Temp file + mv keeps this portable across GNU and BSD sed (see Test 2).\nsed \"/^${NEW_HEADING}\\$/d\" \"$FX3/AGENTS.md\" > \"$FX3/AGENTS.md.tmp\"\nmv \"$FX3/AGENTS.md.tmp\" \"$FX3/AGENTS.md\"\n\nif bash \"$VALIDATE\" \"$FX3\" >/dev/null 2>&1; then\n fail \"validate-structure.sh accepted a bloated root with no scope index\"\nelse\n pass \"bloated root without a scope index is still rejected\"\nfi\n\necho \"All scope-index heading regression tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4503,"content_sha256":"d7c3c9a57b74a265f9f149fb92ce888b6f46240364a36511dc41382714ac9672"},{"filename":"scripts/tests/test-scope-index-keeps-existing.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test: a directory that already carries an AGENTS.md must appear in\n# the root scope index, even when detect-scopes.sh has no rule for that\n# directory name.\n#\n# The bug: the index was built purely from detected scopes. detect-scopes.sh\n# knows a fixed set of directory names, so a hand-authored scoped file\n# elsewhere (TYPO3 `Configuration/`, for one) was silently dropped from the\n# root index on `--update`. An agent reading the root then never learns the\n# file exists, which defeats the precedence rule the index exists to serve —\n# and nothing reports the omission.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nDETECT=\"$SCRIPTS_DIR/detect-scopes.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# A PHP project with a scoped AGENTS.md in a directory the detector has no\n# rule for, plus one it does recognise.\nmkdir -p \"$WORK/proj/Configuration\" \"$WORK/proj/Classes\"\ncat > \"$WORK/proj/composer.json\" \u003c\u003c'JSON'\n{\"name\": \"acme/thing\", \"require\": {\"php\": \"^8.2\"}}\nJSON\nfor i in 1 2 3 4 5 6; do\n echo \"\u003c?php\" > \"$WORK/proj/Classes/File$i.php\"\n echo \"services:\" > \"$WORK/proj/Configuration/Services$i.yaml\"\ndone\ncat > \"$WORK/proj/Configuration/AGENTS.md\" \u003c\u003c'MD'\n# Configuration\n\n## Overview\nDI services, TCA and backend routes for this extension.\nMD\necho \"# root\" > \"$WORK/proj/AGENTS.md\"\n\nSCOPES=$(cd \"$WORK/proj\" && bash \"$DETECT\" . 2>/dev/null)\n\nprintf '%s' \"$SCOPES\" | jq -e '.scopes[] | select(.path == \"Configuration\")' >/dev/null 2>&1 \\\n || fail \"a directory with its own AGENTS.md was not reported as a scope\"\npass \"an existing scoped AGENTS.md is reported even without a detection rule\"\n\n# The root file itself must never be listed as a scope of itself.\nif printf '%s' \"$SCOPES\" | jq -e '.scopes[] | select(.path == \".\" or .path == \"AGENTS.md\")' >/dev/null 2>&1; then\n fail \"the root AGENTS.md was reported as its own scope\"\nfi\npass \"the root AGENTS.md is not listed as a scope\"\n\n# A recognised directory keeps its specific type rather than being relabelled.\ngot=$(printf '%s' \"$SCOPES\" | jq -r '.scopes[] | select(.path == \"Classes\") | .type')\n[ \"$got\" != \"existing\" ] && [ -n \"$got\" ] \\\n || fail \"a detected scope lost its type (got '$got')\"\npass \"a detected scope keeps its own type\"\n\necho \"All scope-index existing-file regression tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2535,"content_sha256":"7e4210c413dec659db1ac258b4c09b727fe0cc25c308f423c4058ea185603c45"},{"filename":"scripts/tests/test-symlink-write-boundary.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Regression test for the CLAUDE.md/GEMINI.md write boundary (issues #102, #103).\n#\n# Two defects, both in generate-agents.sh:\n# #102 --no-symlinks was documented in --help but dropped by the parser, so\n# the symlinks were created regardless of the flag.\n# #103 an existing CLAUDE.md that is itself a symlink was treated like a\n# missing file and repointed to AGENTS.md — no --force, no message —\n# because the guard tested \"[ -L ] || [ ! -e ]\".\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nGENERATE=\"$SCRIPTS_DIR/generate-agents.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# Minimal JS fixture: enough for a root AGENTS.md, no scopes needed.\nmake_fixture() {\n local fx=\"$1\"\n mkdir -p \"$fx/src\"\n cat > \"$fx/package.json\" \u003c\u003c'JSON'\n{ \"name\": \"fixture\", \"version\": \"1.0.0\", \"scripts\": { \"test\": \"echo t\" } }\nJSON\n for i in 1 2 3 4 5; do printf 'export const c%s = %s\\n' \"$i\" \"$i\" > \"$fx/src/c$i.js\"; done\n git -C \"$fx\" init -q\n git -C \"$fx\" -c [email protected] -c user.name=t add -A\n git -C \"$fx\" -c [email protected] -c user.name=t commit -qm init\n}\n\n# --- Test 1 (#102): --no-symlinks suppresses both compatibility files\nFX=\"$WORK/no-symlinks\"\nmake_fixture \"$FX\"\nbash \"$GENERATE\" \"$FX\" --no-symlinks >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ -f \"$FX/AGENTS.md\" ] || fail \"--no-symlinks suppressed AGENTS.md itself\"\n[ -e \"$FX/CLAUDE.md\" ] && fail \"--no-symlinks still created CLAUDE.md\"\n[ -e \"$FX/GEMINI.md\" ] && fail \"--no-symlinks still created GEMINI.md\"\npass \"--no-symlinks creates AGENTS.md and no compatibility files\"\n\n# --- Test 2: the default still creates them (companion to test 1)\nFX=\"$WORK/default\"\nmake_fixture \"$FX\"\nbash \"$GENERATE\" \"$FX\" >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ -L \"$FX/CLAUDE.md\" ] || fail \"default run did not symlink CLAUDE.md\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"AGENTS.md\" ] || fail \"CLAUDE.md points elsewhere\"\npass \"default run symlinks CLAUDE.md → AGENTS.md\"\n\n# --- Test 3 (#103): a foreign symlink is kept, and the user is told\nFX=\"$WORK/foreign-link\"\nmake_fixture \"$FX\"\nmkdir -p \"$WORK/shared\"\nprintf '# curated rules\\n' > \"$WORK/shared/CLAUDE.md\"\nln -s ../shared/CLAUDE.md \"$FX/CLAUDE.md\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" 2>&1)\" || fail \"generate-agents.sh errored\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"../shared/CLAUDE.md\" ] \\\n || fail \"foreign CLAUDE.md symlink was repointed without --force\"\ngrep -q \"Kept: \" \u003c\u003c\u003c\"$OUT\" || fail \"no notice printed for the kept file (output was: $OUT)\"\npass \"foreign CLAUDE.md symlink kept, notice printed\"\n\n# --- Test 4: a regular foreign file is kept too, and reported\nFX=\"$WORK/foreign-file\"\nmake_fixture \"$FX\"\nprintf '# my own rules\\n' > \"$FX/GEMINI.md\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" 2>&1)\" || fail \"generate-agents.sh errored\"\ngrep -qx '# my own rules' \"$FX/GEMINI.md\" || fail \"regular GEMINI.md was overwritten\"\ngrep -q \"Kept: GEMINI.md\" \u003c\u003c\u003c\"$OUT\" || fail \"no notice printed for kept GEMINI.md (output was: $OUT)\"\npass \"regular GEMINI.md kept, notice printed\"\n\n# --- Test 5: --force replaces a foreign symlink, --dry-run --force does not\nFX=\"$WORK/force\"\nmake_fixture \"$FX\"\nln -s ../shared/CLAUDE.md \"$FX/CLAUDE.md\"\nbash \"$GENERATE\" \"$FX\" --force --dry-run >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"../shared/CLAUDE.md\" ] \\\n || fail \"--dry-run --force modified the tree\"\nbash \"$GENERATE\" \"$FX\" --force >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"AGENTS.md\" ] || fail \"--force did not replace the foreign symlink\"\npass \"--force replaces, --dry-run --force does not\"\n\n# --- Test 6: a second run over our own symlink is a no-op, not a \"Kept\" notice\nFX=\"$WORK/idempotent\"\nmake_fixture \"$FX\"\nbash \"$GENERATE\" \"$FX\" >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" 2>&1)\" || fail \"generate-agents.sh errored on second run\"\ngrep -q \"Kept: CLAUDE.md\" \u003c\u003c\u003c\"$OUT\" && fail \"our own symlink was reported as a foreign file\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"AGENTS.md\" ] || fail \"second run broke CLAUDE.md\"\npass \"re-running keeps our own symlink without a notice\"\n\n# --- Test 7: --claude-shim treats a dangling foreign symlink as existing.\n# [ -f ] follows the link, so it read as absent and the shim was written\n# at the link's target, outside the project.\nFX=\"$WORK/shim-dangling\"\nmake_fixture \"$FX\"\nmkdir -p \"$WORK/shim-target\"\nln -s ../shim-target/CLAUDE.md \"$FX/CLAUDE.md\"\nbash \"$GENERATE\" \"$FX\" --claude-shim --no-symlinks >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ -e \"$WORK/shim-target/CLAUDE.md\" ] && fail \"--claude-shim wrote through a dangling symlink\"\n[ \"$(readlink \"$FX/CLAUDE.md\")\" = \"../shim-target/CLAUDE.md\" ] || fail \"--claude-shim changed the symlink without --force\"\nbash \"$GENERATE\" \"$FX\" --claude-shim --no-symlinks --force >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\n[ -e \"$WORK/shim-target/CLAUDE.md\" ] && fail \"--claude-shim --force wrote through the symlink\"\n{ [ -f \"$FX/CLAUDE.md\" ] && [ ! -L \"$FX/CLAUDE.md\" ]; } || fail \"--claude-shim --force did not replace the symlink with the shim\"\npass \"--claude-shim keeps a dangling symlink, and --force replaces it instead of writing through it\"\n\n# --- Test 8: --claude-shim reports a kept foreign CLAUDE.md, not its own shim\nFX=\"$WORK/shim-foreign-file\"\nmake_fixture \"$FX\"\nprintf '# my own rules\\n' > \"$FX/CLAUDE.md\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --claude-shim --no-symlinks 2>&1)\" || fail \"generate-agents.sh errored\"\ngrep -q '^# my own rules

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\"$FX/CLAUDE.md\" || fail \"--claude-shim replaced a foreign CLAUDE.md without --force\"\ngrep -q \"Kept: CLAUDE.md\" \u003c\u003c\u003c\"$OUT\" || fail \"--claude-shim kept a foreign CLAUDE.md without saying so (output was: $OUT)\"\nFX=\"$WORK/shim-own\"\nmake_fixture \"$FX\"\nbash \"$GENERATE\" \"$FX\" --claude-shim --no-symlinks >/dev/null 2>&1 || fail \"generate-agents.sh errored\"\nOUT=\"$(bash \"$GENERATE\" \"$FX\" --claude-shim --no-symlinks 2>&1)\" || fail \"generate-agents.sh errored on second run\"\ngrep -q \"Kept: CLAUDE.md\" \u003c\u003c\u003c\"$OUT\" && fail \"re-running --claude-shim reported its own shim as kept\"\npass \"--claude-shim reports a kept foreign CLAUDE.md and stays quiet about its own shim\"\n\necho \"\"\necho \"All symlink write-boundary tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6478,"content_sha256":"a674cd20423b5eceeda4eb4c54efb47370ca64411ac9d9120597c25eb491b9b7"},{"filename":"scripts/tests/test-untrusted-git-config.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# The scripts read the analysed project's history and file list with git. The\n# project's .git/config is input like any other file, so a command it names in\n# core.fsmonitor, a hook or the signature program does not run. lib/git.sh\n# holds the one wrapper all of them use.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\nexport GIT_CONFIG_GLOBAL=/dev/null\nfor script in generate-file-map.sh analyze-git-history.sh check-freshness.sh \\\n detect-golden-samples.sh detect-utilities.sh \\\n extract-github-settings.sh extract-github-rulesets.sh; do\n fx=\"$WORK/$script\"\n mkdir -p \"$fx/src\"\n printf 'package main\\n' > \"$fx/src/main.go\"\n printf '# AGENTS.md\\n' > \"$fx/AGENTS.md\"\n git -C \"$fx\" init -q\n git -C \"$fx\" -c [email protected] -c user.name=t add -A\n git -C \"$fx\" -c [email protected] -c user.name=t commit -qm init\n git -C \"$fx\" config core.fsmonitor \"touch $WORK/$script.ran; false\"\n # A modified file makes git refresh the index, which is when it asks the\n # fsmonitor command.\n echo changed >> \"$fx/src/main.go\"\n (cd \"$fx\" && timeout 60 bash \"$SCRIPTS_DIR/$script\" \"$fx\" >/dev/null 2>&1)\n [ $? -eq 124 ] && fail \"$script timed out\"\n [ -e \"$WORK/$script.ran\" ] && fail \"$script ran a command from the project's git config\"\ndone\npass \"no script runs the core.fsmonitor command of the analysed project\"\n\n# log.showSignature makes git log verify every signed commit with the\n# signature program the project's config names.\nif command -v ssh-keygen >/dev/null 2>&1; then\n fx=\"$WORK/signed\"\n git init -q \"$fx\"\n echo a > \"$fx/a\"\n git -C \"$fx\" add a\n ssh-keygen -q -t ed25519 -N '' -f \"$WORK/key\"\n git -C \"$fx\" -c [email protected] -c user.name=t -c gpg.format=ssh \\\n -c user.signingkey=\"$WORK/key\" commit -q -S -m init\n echo \"[email protected] $(cat \"$WORK/key.pub\")\" > \"$fx/.allowed\"\n printf '#!/bin/sh\\ntouch %s/signature.ran\\nexit 1\\n' \"$WORK\" > \"$WORK/fake-ssh-keygen\"\n chmod +x \"$WORK/fake-ssh-keygen\"\n git -C \"$fx\" config gpg.format ssh\n git -C \"$fx\" config gpg.ssh.program \"$WORK/fake-ssh-keygen\"\n git -C \"$fx\" config gpg.ssh.allowedSignersFile .allowed\n git -C \"$fx\" config log.showSignature true\n for script in analyze-git-history.sh check-freshness.sh detect-golden-samples.sh; do\n (cd \"$fx\" && timeout 60 bash \"$SCRIPTS_DIR/$script\" \"$fx\" >/dev/null 2>&1)\n [ $? -eq 124 ] && fail \"$script timed out\"\n [ -e \"$WORK/signature.ran\" ] && fail \"$script ran the signature program of the project's git config\"\n done\n pass \"no script runs the signature program of the analysed project\"\nelse\n echo \"⚠️ SKIP: ssh-keygen not found, signature program case not run\"\nfi\n\n# A partial clone fetches a missing object from its promisor remote on demand,\n# running the transport command the project's config names.\nfx=\"$WORK/partial\"\nmkdir -p \"$fx/src\"\nprintf 'package main\\n' > \"$fx/src/main.go\"\n# An old \"Last updated\" date makes check-freshness.sh list the commits since.\nprintf '\u003c!-- Last updated: 2000-01-01 -->\\n# AGENTS.md\\n' > \"$fx/AGENTS.md\"\ngit -C \"$fx\" init -q\ngit -C \"$fx\" add -A\ngit -C \"$fx\" -c [email protected] -c user.name=t commit -qm init\necho 'package util' > \"$fx/src/util.go\"\ngit -C \"$fx\" add -A\ngit -C \"$fx\" -c [email protected] -c user.name=t commit -qm second\ntree=\"$(git -C \"$fx\" rev-parse 'HEAD~1^{tree}')\"\nrm -f \"$fx/.git/objects/${tree:0:2}/${tree:2}\"\ngit -C \"$fx\" config core.repositoryformatversion 1\ngit -C \"$fx\" config extensions.partialClone origin\ngit -C \"$fx\" config remote.origin.url ssh://example.invalid/x.git\ngit -C \"$fx\" config remote.origin.promisor true\ngit -C \"$fx\" config core.sshCommand \"touch $WORK/transport.ran; false\"\n# The project may allow ssh explicitly; that must not reopen the transport.\ngit -C \"$fx\" config protocol.ssh.allow always\nfor script in analyze-git-history.sh check-freshness.sh; do\n (cd \"$fx\" && timeout 60 bash \"$SCRIPTS_DIR/$script\" \"$fx\" >/dev/null 2>&1)\n [ $? -eq 124 ] && fail \"$script timed out\"\n [ -e \"$WORK/transport.ran\" ] && fail \"$script ran the promisor remote's transport command\"\ndone\npass \"no script fetches a missing object from the analysed project's remote\"\n\n# Run from a git hook, GIT_DIR and GIT_INDEX_FILE point at the calling\n# repository; the scripts still answer for the analysed project.\nfx=\"$WORK/located\"\nmkdir -p \"$fx/src\"\nprintf 'package main\\n' > \"$fx/src/main.go\"\ngit -C \"$fx\" init -q\ngit -C \"$fx\" add -A\ngit init -q \"$WORK/caller\"\nout=\"$(cd \"$fx\" && GIT_DIR=\"$WORK/caller/.git\" GIT_INDEX_FILE=\"$WORK/caller/.git/index\" \\\n timeout 60 bash \"$SCRIPTS_DIR/generate-file-map.sh\" \"$fx\" 2>/dev/null)\"\ngrep -q 'src/' \u003c\u003c\u003c\"$out\" || fail \"generate-file-map.sh listed another repository's files (output: $out)\"\npass \"inherited GIT_DIR and GIT_INDEX_FILE do not redirect the scripts\"\n\necho \"\"\necho \"All untrusted git config tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5146,"content_sha256":"653f81b939e2947026b93e3693fb61de4c1827d8851f387d527b10b99f606b97"},{"filename":"scripts/tests/test-verification-sidecar.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# verify-commands.sh records its results in .agents/command-verification.json\n# inside the analysed project. The file is valid JSON whatever the documented\n# commands contain, and it is written inside the project only: a symlinked\n# sidecar file is replaced, a symlinked .agents directory is not written to.\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSCRIPTS_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nVERIFY=\"$SCRIPTS_DIR/verify-commands.sh\"\n\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nfail() { echo \"❌ FAIL: $1\"; exit 1; }\npass() { echo \"✅ PASS: $1\"; }\n\n# Two documented commands that exist on any host: one plain, one carrying a\n# tab and double quotes.\nwrite_agents() {\n # shellcheck disable=SC2016 # backticks are markdown, not substitution\n printf '# Fixture\\n\\n## Commands\\n\\n| Command | Purpose |\\n|---|---|\\n| `ls -la` | list |\\n| `ls\\t\"a b\"` | quoted |\\n' > \"$1/AGENTS.md\"\n}\n\n# --- Test 1: the sidecar is valid JSON and names both commands\nFX=\"$WORK/plain\"\nmkdir -p \"$FX\"\nwrite_agents \"$FX\"\n(cd \"$FX\" && bash \"$VERIFY\" . >/dev/null 2>&1) || fail \"verify-commands.sh errored\"\njq -e . \"$FX/.agents/command-verification.json\" >/dev/null 2>&1 \\\n || fail \"the sidecar is not valid JSON: $(cat \"$FX/.agents/command-verification.json\")\"\njq -e '.commands | has(\"ls -la\") and has(\"ls\\t\\\"a b\\\"\")' \"$FX/.agents/command-verification.json\" >/dev/null \\\n || fail \"the sidecar does not hold both commands\"\npass \"the sidecar is valid JSON for a command with a tab and quotes\"\n\n# --- Test 2: a symlinked sidecar file is replaced, its target left alone\nFX=\"$WORK/file-link\"\nmkdir -p \"$FX/.agents\"\nwrite_agents \"$FX\"\nprintf 'keep\\n' > \"$WORK/outside.txt\"\nln -s ../../outside.txt \"$FX/.agents/command-verification.json\"\n(cd \"$FX\" && bash \"$VERIFY\" . >/dev/null 2>&1) || fail \"verify-commands.sh errored\"\n[ \"$(cat \"$WORK/outside.txt\")\" = keep ] || fail \"the sidecar was written through a symlink\"\n{ [ -f \"$FX/.agents/command-verification.json\" ] && [ ! -L \"$FX/.agents/command-verification.json\" ]; } \\\n || fail \"the symlinked sidecar was not replaced by a regular file\"\npass \"a symlinked sidecar is replaced, not written through\"\n\n# --- Test 3: a symlinked .agents directory is not written to\nFX=\"$WORK/dir-link\"\nmkdir -p \"$FX\" \"$WORK/outside-dir\"\nwrite_agents \"$FX\"\nln -s ../outside-dir \"$FX/.agents\"\n(cd \"$FX\" && bash \"$VERIFY\" . >/dev/null 2>&1) || fail \"verify-commands.sh errored\"\n[ -z \"$(ls -A \"$WORK/outside-dir\")\" ] || fail \"a file was written into the symlinked .agents directory\"\npass \"nothing is written through a symlinked .agents directory\"\n\necho \"\"\necho \"All verification sidecar tests passed.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2744,"content_sha256":"10c08cb5564735f5d0ec6b8bbbd17cd5de0b5d6fcb53853300c5d1138e929cda"},{"filename":"scripts/validate-structure.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Validate AGENTS.md structure compliance and optionally check freshness\n# Note: -e intentionally omitted - we accumulate errors and report at end\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Scope-index heading regex (ERE). Matches both the current template heading\n# \"## Scoped AGENTS.md (MUST read ...)\" and the legacy \"## Index of scoped\n# AGENTS.md\" so validation stays in sync with generate-agents.sh output and\n# remains backward-compatible with files produced by older versions.\n# Case-tolerant via bracket classes (not grep -i / sed I) so the same regex\n# works in grep -E and sed -E on both GNU and BSD (#81): older generated roots\n# use the title-case \"## Index of Scoped AGENTS.md\".\nSCOPE_INDEX_HEADING_RE='^## ([Ii]ndex of [Ss]coped|[Ss]coped) AGENTS\\.md'\n\n# Default options\nPROJECT_DIR=\"\"\nCHECK_FRESHNESS=false\nVERBOSE=false\nJSON=false\n\n# Parse flags\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --check-freshness|-f)\n CHECK_FRESHNESS=true\n shift\n ;;\n --verbose|-v)\n VERBOSE=true\n shift\n ;;\n --json)\n JSON=true\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: validate-structure.sh [PROJECT_DIR] [OPTIONS]\n\nValidate AGENTS.md structure compliance and optionally check freshness.\n\nOptions:\n --check-freshness, -f Also check if files are up to date with git commits\n --verbose, -v Show detailed output\n --json Emit machine-readable JSON on stdout (structure only)\n --help, -h Show this help message\n\nExamples:\n validate-structure.sh . # Structure check only\n validate-structure.sh . --check-freshness # Structure + freshness check\n validate-structure.sh . -f -v # Full check with details\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\nPROJECT_DIR=\"${PROJECT_DIR:-.}\"\ncd \"$PROJECT_DIR\" || exit 1\n\nERRORS=0\nWARNINGS=0\n\n# --json state: each helper records the outcome of the just-run check so that\n# record_check can attach it to a per-file record. Default-path output is unchanged.\nLAST_STATUS=\"\"\nLAST_DETAIL=\"\"\ndeclare -A FILE_CHECKS_JSON=()\ndeclare -A FILE_ROLE=()\n\nerror() {\n echo \"❌ ERROR: $*\"\n ((ERRORS+=1))\n LAST_STATUS=\"fail\"; LAST_DETAIL=\"$*\"\n}\n\nwarning() {\n echo \"⚠️ WARNING: $*\"\n ((WARNINGS+=1))\n LAST_STATUS=\"warn\"; LAST_DETAIL=\"$*\"\n}\n\nsuccess() {\n echo \"✅ $*\"\n LAST_STATUS=\"pass\"; LAST_DETAIL=\"$*\"\n}\n\ninfo() {\n echo \"ℹ️ $*\"\n LAST_STATUS=\"pass\"; LAST_DETAIL=\"$*\"\n}\n\n# Record the just-run check (LAST_STATUS/LAST_DETAIL) under a relative file path.\n# No-op unless --json; never writes to stdout.\nrecord_check() {\n [[ \"$JSON\" = true ]] || return 0\n local path=\"$1\" name=\"$2\" obj\n obj=$(jq -nc --arg name \"$name\" --arg status \"$LAST_STATUS\" --arg detail \"$LAST_DETAIL\" \\\n '{name:$name,status:$status,detail:$detail}')\n FILE_CHECKS_JSON[\"$path\"]=\"${FILE_CHECKS_JSON[\"$path\"]:-}${obj}\"

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\\n'\n}\n\n# Check if file has managed header\ncheck_managed_header() {\n local file=\"$1\"\n\n if grep -q \"^\u003c!-- Managed by agent:\" \"$file\"; then\n success \"Managed header present: $file\"\n return 0\n else\n warning \"Missing managed header: $file\"\n return 1\n fi\n}\n\n# Check if root is thin (≤50 lines or has scope index)\ncheck_root_is_thin() {\n local file=\"$1\"\n local line_count\n line_count=$(wc -l \u003c \"$file\")\n\n if [ \"$line_count\" -le 50 ]; then\n success \"Root is thin: $line_count lines\"\n return 0\n elif grep -qE \"$SCOPE_INDEX_HEADING_RE\" \"$file\"; then\n success \"Root has scope index (verbose style acceptable)\"\n return 0\n else\n error \"Root is bloated: $line_count lines and no scope index (expected an '## Index of scoped AGENTS.md' or '## Scoped AGENTS.md ...' heading)\"\n return 1\n fi\n}\n\n# Check if root has precedence statement\ncheck_precedence_statement() {\n local file=\"$1\"\n\n if grep -qi \"precedence\" \"$file\" && grep -qi \"closest.*AGENTS.md.*wins\" \"$file\"; then\n success \"Precedence statement present\"\n return 0\n else\n error \"Missing precedence statement in root\"\n return 1\n fi\n}\n\n# Check if scoped file has all required sections (with alternatives)\ncheck_scoped_sections() {\n local file=\"$1\"\n\n # Each entry: \"Display Name|pattern1|pattern2|...\"\n local section_patterns=(\n \"Overview|## Overview\"\n \"Setup|## Setup|## Environment|## Prerequisites|## Getting Started|## Workflow files\"\n \"Build/Tests|## Build|## Tests|## Running|## Commands|## Common patterns\"\n \"Code style|## Code style|## Style|## Conventions|## Workflow conventions\"\n \"Security|## Security\"\n \"Checklist|## PR|## Commit|## Checklist\"\n \"Examples|## Good vs|## Examples|## Bad examples|## Patterns to Follow\"\n \"When stuck|## When stuck|## Help|## Resources|## Troubleshooting\"\n )\n\n local missing=()\n\n for entry in \"${section_patterns[@]}\"; do\n local name=\"${entry%%|*}\"\n local patterns=\"${entry#*|}\"\n local found=false\n\n # Try each pattern\n IFS='|' read -ra pattern_array \u003c\u003c\u003c \"$patterns\"\n for pattern in \"${pattern_array[@]}\"; do\n if grep -qi \"^$pattern\" \"$file\"; then\n found=true\n break\n fi\n done\n\n if [ \"$found\" = false ]; then\n missing+=(\"$name\")\n fi\n done\n\n if [ ${#missing[@]} -eq 0 ]; then\n success \"All required sections present: $file\"\n return 0\n else\n error \"Missing sections in $file: ${missing[*]}\"\n return 1\n fi\n}\n\n# Check if scope index links work\ncheck_scope_links() {\n local root_file=\"$1\"\n\n if ! grep -qE \"$SCOPE_INDEX_HEADING_RE\" \"$root_file\"; then\n # No scope index (thin root without scopes) -- nothing to check. Set an\n # explicit status so a subsequent record_check does not reuse the\n # previous check's LAST_STATUS/LAST_DETAIL in --json mode.\n LAST_STATUS=\"pass\"; LAST_DETAIL=\"No scope index (no scoped files to link)\"\n return 0\n fi\n\n # Extract links from scope index\n local links\n links=$(sed -nE \"/$SCOPE_INDEX_HEADING_RE/,/^##/p\" \"$root_file\" | grep -o '\\./[^)]*AGENTS.md' || true)\n\n if [ -z \"$links\" ]; then\n # Empty scope index with AGENTS-GENERATED markers is valid (placeholder)\n if grep -q \"\u003c!-- AGENTS-GENERATED:START scope-index -->\" \"$root_file\" && \\\n grep -q \"\u003c!-- AGENTS-GENERATED:END scope-index -->\" \"$root_file\"; then\n info \"Scope index is empty (placeholder)\"\n return 0\n fi\n warning \"Scope index present but no links found\"\n return 1\n fi\n\n local broken=()\n while read -r link; do\n # Remove leading ./\n local clean_link=\"${link#./}\"\n local full_path=\"$PROJECT_DIR/$clean_link\"\n\n if [ ! -f \"$full_path\" ]; then\n broken+=(\"$link\")\n fi\n done \u003c\u003c\u003c \"$links\"\n\n if [ ${#broken[@]} -eq 0 ]; then\n success \"All scope index links work\"\n return 0\n else\n error \"Broken scope index links: ${broken[*]}\"\n return 1\n fi\n}\n\n# Check CLAUDE.md symlink exists alongside an AGENTS.md file\ncheck_claude_symlink() {\n local agents_file=\"$1\"\n local dir\n dir=$(dirname \"$agents_file\")\n local claude_file=\"$dir/CLAUDE.md\"\n local rel_dir=\"${dir#\"$PROJECT_DIR\"}\"\n [ -z \"$rel_dir\" ] && rel_dir=\"(root)\"\n\n if [ -L \"$claude_file\" ]; then\n local target\n target=$(readlink \"$claude_file\")\n if [ \"$target\" = \"AGENTS.md\" ]; then\n success \"CLAUDE.md symlink correct: $rel_dir\"\n return 0\n else\n error \"CLAUDE.md symlink points to '$target', expected 'AGENTS.md': $rel_dir\"\n return 1\n fi\n elif [ -f \"$claude_file\" ]; then\n # A regular file with an @AGENTS.md import line is fully valid: some\n # directories cannot hold symlinks (the TYPO3 docs renderer lists\n # Documentation/ via Flysystem, which aborts on symlinks -- #82).\n if grep -qE '^@AGENTS\\.md[[:space:]]*

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\"$claude_file\"; then\n success \"CLAUDE.md import file (@AGENTS.md): $rel_dir\"\n return 0\n fi\n warning \"CLAUDE.md is a regular file, not a symlink to AGENTS.md (add an '@AGENTS.md' import line or replace with a symlink): $rel_dir\"\n return 1\n else\n error \"Missing CLAUDE.md symlink to AGENTS.md: $rel_dir (Claude Code won't read AGENTS.md without it)\"\n return 1\n fi\n}\n\n# In JSON mode, route all human-readable output to /dev/null and reserve the\n# original stdout (fd 3) for the single JSON document emitted at the end.\nif [[ \"$JSON\" = true ]]; then\n exec 3>&1 1>/dev/null\nfi\n\n# Main validation\necho \"Validating AGENTS.md structure in: $PROJECT_DIR\"\necho \"\"\n\n# Check root AGENTS.md\nROOT_FILE=\"$PROJECT_DIR/AGENTS.md\"\n\nif [ ! -f \"$ROOT_FILE\" ]; then\n error \"Root AGENTS.md not found\"\nelse\n echo \"=== Root AGENTS.md ===\"\n FILE_ROLE[\"AGENTS.md\"]=\"root\"\n check_managed_header \"$ROOT_FILE\"; record_check \"AGENTS.md\" \"managed_header\"\n check_root_is_thin \"$ROOT_FILE\"; record_check \"AGENTS.md\" \"root_is_thin\"\n check_precedence_statement \"$ROOT_FILE\"; record_check \"AGENTS.md\" \"precedence\"\n check_scope_links \"$ROOT_FILE\"; record_check \"AGENTS.md\" \"scope_links\"\n echo \"\"\nfi\n\n# Check CLAUDE.md symlinks\necho \"=== CLAUDE.md Symlinks ===\"\nALL_AGENTS_FILES=$(find \"$PROJECT_DIR\" -name \"AGENTS.md\" \\\n -not -path \"*/references/examples/*\" \\\n -not -path \"*/examples/*\" \\\n -not -path \"*/.git/*\" \\\n -not -path \"*/vendor/*\" \\\n -not -path \"*/node_modules/*\" \\\n -not -path \"*/.Build/*\" \\\n 2>/dev/null || true)\n\nif [ -n \"$ALL_AGENTS_FILES\" ]; then\n while read -r file; do\n check_claude_symlink \"$file\"\n sym_rel=\"${file#\"$PROJECT_DIR\"/}\"; sym_rel=\"${sym_rel#./}\"\n record_check \"$sym_rel\" \"claude_symlink\"\n done \u003c\u003c\u003c \"$ALL_AGENTS_FILES\"\nfi\necho \"\"\n\n# Check scoped AGENTS.md files (exclude reference examples and dependency trees:\n# third-party AGENTS.md files are not the project's to fix -- #84)\nSCOPED_FILES=$(find \"$PROJECT_DIR\" -name \"AGENTS.md\" \\\n -not -path \"$ROOT_FILE\" \\\n -not -path \"*/references/examples/*\" \\\n -not -path \"*/examples/*\" \\\n -not -path \"*/.git/*\" \\\n -not -path \"*/vendor/*\" \\\n -not -path \"*/node_modules/*\" \\\n -not -path \"*/.Build/*\" \\\n 2>/dev/null || true)\n\nif [ -n \"$SCOPED_FILES\" ]; then\n echo \"=== Scoped AGENTS.md Files ===\"\n while read -r file; do\n rel_path=\"${file#\"$PROJECT_DIR\"/}\"\n echo \"Checking: $rel_path\"\n json_key=\"${rel_path#./}\"\n FILE_ROLE[\"$json_key\"]=\"scoped\"\n check_managed_header \"$file\"; record_check \"$json_key\" \"managed_header\"\n check_scoped_sections \"$file\"; record_check \"$json_key\" \"required_sections\"\n echo \"\"\n done \u003c\u003c\u003c \"$SCOPED_FILES\"\nfi\n\n# Emit JSON document and exit before the human summary. JSON mode is structure-only\n# and deliberately does NOT run the --check-freshness shell-out below (score-agents\n# calls check-freshness.sh --json itself), keeping the two signals decoupled.\nif [[ \"$JSON\" = true ]]; then\n json_files=()\n if [[ \"${#FILE_CHECKS_JSON[@]}\" -gt 0 ]]; then\n while IFS= read -r path; do\n [[ -z \"$path\" ]] && continue\n checks_json=$(printf '%s' \"${FILE_CHECKS_JSON[$path]}\" | jq -s '.')\n json_files+=(\"$(jq -nc \\\n --arg path \"$path\" \\\n --arg role \"${FILE_ROLE[$path]:-scoped}\" \\\n --argjson checks \"$checks_json\" \\\n '{path:$path,role:$role,\n errors:($checks|map(select(.status==\"fail\"))|length),\n warnings:($checks|map(select(.status==\"warn\"))|length),\n checks:$checks}')\")\n done \u003c \u003c(printf '%s\\n' \"${!FILE_CHECKS_JSON[@]}\" | sort)\n fi\n if [[ \"${#json_files[@]}\" -eq 0 ]]; then\n files_json='[]'\n else\n files_json=$(printf '%s\\n' \"${json_files[@]}\" | jq -s '.')\n fi\n jq -nc \\\n --argjson files \"$files_json\" \\\n --argjson e \"$ERRORS\" \\\n --argjson w \"$WARNINGS\" \\\n '{script:\"validate-structure\",schema:1,summary:{errors:$e,warnings:$w},files:$files}' >&3\n if [[ \"$ERRORS\" -eq 0 ]]; then exit 0; else exit 1; fi\nfi\n\n# Summary\necho \"=== Structure Validation Summary ===\"\nif [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then\n echo \"✅ All structure checks passed!\"\nelif [ $ERRORS -eq 0 ]; then\n echo \"⚠️ Structure validation passed with $WARNINGS warning(s)\"\nelse\n echo \"❌ Structure validation failed with $ERRORS error(s) and $WARNINGS warning(s)\"\nfi\n\n# Run freshness check if requested\nif [ \"$CHECK_FRESHNESS\" = true ]; then\n echo \"\"\n echo \"=== Freshness Check ===\"\n FRESHNESS_ARGS=\"\"\n [ \"$VERBOSE\" = true ] && FRESHNESS_ARGS=\"--verbose\"\n\n if \"$SCRIPT_DIR/check-freshness.sh\" \"$PROJECT_DIR\" $FRESHNESS_ARGS; then\n echo \"✅ All files are up to date!\"\n else\n # Freshness issues are warnings, not errors\n ((WARNINGS+=1))\n fi\nfi\n\necho \"\"\necho \"=== Final Summary ===\"\nif [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then\n echo \"✅ All checks passed!\"\n exit 0\nelif [ $ERRORS -eq 0 ]; then\n echo \"⚠️ Passed with $WARNINGS warning(s)\"\n exit 0\nelse\n echo \"❌ Failed with $ERRORS error(s) and $WARNINGS warning(s)\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":13620,"content_sha256":"ccfe7117b94f88eea867a2fd11e2bb3fa5ab8f8cc7c91bfc0f65d51f086bb3e2"},{"filename":"scripts/verify-commands.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# Verify that commands documented in AGENTS.md actually work\n# This prevents \"command rot\" - documented commands that no longer exist\n# Requires: Bash 4.0+ (for associative arrays)\nset -euo pipefail\n\n# Check Bash version - we need 4.0+ for associative arrays (declare -A)\nif ((BASH_VERSINFO[0] \u003c 4)); then\n echo \"Error: Bash 4.0+ required (found ${BASH_VERSION}).\" >&2\n echo \"On macOS: brew install bash\" >&2\n exit 1\nfi\n\nPROJECT_DIR=\".\"\nJSON=false\nUPDATE_VERIFIED=false\n\n# Parse flags. Preserves the original positional semantics (PROJECT_DIR=\"${1:-.}\"):\n# the first non-flag argument becomes PROJECT_DIR, defaulting to \".\" when absent.\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --json)\n JSON=true\n shift\n ;;\n --update-verified)\n UPDATE_VERIFIED=true\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: verify-commands.sh [PROJECT_DIR] [OPTIONS]\n\nVerify that commands documented in AGENTS.md actually exist (and optionally run).\n\nOptions:\n --json Emit machine-readable JSON on stdout (human output suppressed)\n --update-verified On success, record today's date in the \"Last verified\"\n marker of AGENTS.md (without it, AGENTS.md is not changed)\n --help, -h Show this help message\n\nEnvironment variables:\n VERBOSE=true Show detailed [INFO] output on stderr\n DRY_RUN=true Skip writing the JSON sidecar and the --update-verified date\n SMOKE_TEST=true Actually run safe commands (not just check existence)\n TIMEOUT=SECONDS Per-command timeout for smoke tests (default: 60)\n OUTPUT_JSON=PATH Sidecar results file (default: PROJECT_DIR/.agents/command-verification.json)\n\nExamples:\n verify-commands.sh . # Verify commands in ./AGENTS.md\n verify-commands.sh . --json # Machine-readable JSON output\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\nAGENTS_FILE=\"$PROJECT_DIR/AGENTS.md\"\nVERBOSE=\"${VERBOSE:-false}\"\nDRY_RUN=\"${DRY_RUN:-false}\"\nSMOKE_TEST=\"${SMOKE_TEST:-false}\"\nTIMEOUT=\"${TIMEOUT:-60}\"\n# The default sidecar lives inside the analysed project, so it is only written\n# when its directory is a real directory there (see write_json_results).\nOUTPUT_JSON_IS_DEFAULT=false\n[ -z \"${OUTPUT_JSON:-}\" ] && OUTPUT_JSON_IS_DEFAULT=true\nOUTPUT_JSON=\"${OUTPUT_JSON:-$PROJECT_DIR/.agents/command-verification.json}\"\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\nlog() {\n if [ \"$VERBOSE\" = true ]; then\n echo -e \"[INFO] $*\" >&2\n fi\n}\n\nerror() {\n echo -e \"${RED}[ERROR]${NC} $*\" >&2\n}\n\nsuccess() {\n echo -e \"${GREEN}[OK]${NC} $*\"\n}\n\nwarn() {\n echo -e \"${YELLOW}[WARN]${NC} $*\"\n}\n\n# Check if AGENTS.md exists\nif [ ! -f \"$AGENTS_FILE\" ]; then\n error \"AGENTS.md not found at $AGENTS_FILE\"\n exit 1\nfi\n\ncd \"$PROJECT_DIR\"\n\n# In JSON mode, route all human-readable output to /dev/null and reserve the\n# original stdout (fd 3) for the single JSON document emitted at the end. This\n# keeps --json strictly additive: the default (no-flag) path is untouched.\nJSON_CMDS=()\nif [[ \"$JSON\" = true ]]; then\n exec 3>&1 1>/dev/null\nfi\n\necho \"Verifying commands in $AGENTS_FILE...\"\necho \"\"\n\nFAILED=0\nPASSED=0\nSKIPPED=0\n\n# JSON results storage\ndeclare -A COMMAND_RESULTS\n\n# Initialize JSON output directory (always needed for results). A symlinked\n# .agents directory in the analysed project would put the sidecar outside it.\nSIDECAR_DIR=\"$(dirname \"$OUTPUT_JSON\")\"\nSIDECAR_WRITABLE=true\nif [ \"$OUTPUT_JSON_IS_DEFAULT\" = true ] && [ -L \"$SIDECAR_DIR\" ]; then\n SIDECAR_WRITABLE=false\nelse\n mkdir -p \"$SIDECAR_DIR\"\nfi\n\n# Check if a command is safe to execute using a whitelist approach.\n# Only commands whose base binary is in the ALLOWED_COMMANDS list are permitted.\n# Returns 0 if safe, 1 if not whitelisted.\n#\n# The command is executed later as a whole string by `bash -c`, so checking the\n# first word alone decides nothing: \"git status; curl http://x | sh\" passes a\n# base-command check and then runs both halves (#104). Anything carrying shell\n# syntax that could chain, redirect or substitute a second command is therefore\n# rejected outright rather than approved on its prefix.\nis_safe_command() {\n local cmd=\"$1\"\n\n # Shell metacharacters: command separators (; & |), substitution ($ `),\n # redirection (\u003c >), grouping ({ } ( )), globs that could expand into\n # further arguments, and newlines. A documented build command needs none\n # of them; anything that does is not verifiable by smoke-running it.\n # A backslash is rejected too: bash -c would remove it, so \"-ex\\ec\" would\n # reach the program as an option the checks below look for by name.\n if [[ \"$cmd\" == *[\\;\\&\\|\\`\\$\\\u003c\\>\\{\\}\\(\\)\\*\\?\\!\\\\

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

\\n']* ]]; then\n return 1\n fi\n\n # Whitelist of known safe base commands.\n # These are common build/dev tools that are safe to invoke for verification.\n # Inspection tools whose own options can run a program or write a file are\n # not on it (sed, awk, sort, uniq, less, find, rg, ag, yq, file): each has\n # several such options, and none of them is a build command whose\n # documentation needs verifying.\n local -a ALLOWED_COMMANDS=(\n # Version control (only `git --version`, see has_safe_arguments)\n git\n # File inspection\n ls cat head tail wc stat grep egrep fgrep diff\n # Build tools / package managers\n make go npm yarn pnpm bun composer cargo deno gradle gradlew python python3 pip pip3\n poetry uv pytest php phpunit node ruby bundle gem mvn ant\n # Project-specific binaries (resolved via PATH or relative path)\n vendor/bin\n # Container tools (read-only / informational subcommands only)\n docker podman\n # Linters and formatters\n eslint prettier phpcs phpcbf phpstan psalm rector black flake8 mypy ruff shellcheck\n # Documentation / misc\n # curl/wget are deliberately absent: fetching a URL is not a way to\n # verify that a documented build command works, and they are the two\n # entries that turn an allowlisted base command into an outbound\n # request (#104). Such commands are reported as not smoke-tested.\n jq\n # Testing\n jest vitest mocha\n )\n\n # Extract the base command (first word), stripping any leading ./\n local base_cmd\n base_cmd=$(echo \"$cmd\" | awk '{print $1}' | sed 's|^\\./||')\n\n # Allow vendor/bin/* paths\n if [[ \"$base_cmd\" == vendor/bin/* ]]; then\n return 0\n fi\n\n # Check against whitelist, then the arguments\n for allowed in \"${ALLOWED_COMMANDS[@]}\"; do\n if [[ \"$base_cmd\" == \"$allowed\" ]]; then\n has_safe_arguments \"$base_cmd\" \"$cmd\"\n return\n fi\n done\n\n # Not in whitelist - reject\n return 1\n}\n\n# The first word decides nothing for a tool whose own arguments can run a\n# command or write a file. These are allowed only in the forms that do\n# neither. Quotes are removed before comparing, as bash -c would remove them.\nhas_safe_arguments() {\n local base=\"$1\" cmd=\"$2\"\n local -a words=()\n read -ra words \u003c\u003c\u003c\"${cmd//[\\'\\\"]/}\"\n case \"$base\" in\n git)\n # Any other git command reads the analysed repository's config,\n # which can name commands git runs (core.fsmonitor, filters,\n # aliases, diff drivers).\n [[ ${#words[@]} -eq 2 && ( \"${words[1]}\" == \"--version\" || \"${words[1]}\" == \"version\" ) ]]\n return\n ;;\n docker|podman)\n # Informational subcommands only, named first (no global options).\n [[ ${#words[@]} -ge 2 ]] || return 1\n case \"${words[1]}\" in\n version|--version|info|ps|images) return 0 ;;\n *) return 1 ;;\n esac\n ;;\n esac\n return 0\n}\n\n# Portable milliseconds timestamp (works on both GNU and BSD date)\nget_time_ms() {\n # Try GNU date with nanoseconds first, fall back to seconds * 1000\n if date +%s%3N 2>/dev/null | grep -qE '^[0-9]+

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

; then\n date +%s%3N\n else\n echo $(( $(date +%s) * 1000 ))\n fi\n}\n\n# Run a command with timeout and measure duration\nsmoke_test_command() {\n local cmd=\"$1\"\n local start_time end_time duration_ms exit_code\n\n # Safety check - skip dangerous commands\n if ! is_safe_command \"$cmd\"; then\n warn \"Not smoke-tested (not on the allowlist, or contains shell syntax): $cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false, \"skipped\": true, \"reason\": \"safety\"}'\n return 1\n fi\n\n start_time=$(get_time_ms)\n\n # Run with timeout, capture exit code\n if timeout \"${TIMEOUT}s\" bash -c \"$cmd\" > /dev/null 2>&1; then\n exit_code=0\n else\n exit_code=$?\n fi\n\n end_time=$(get_time_ms)\n duration_ms=$((end_time - start_time))\n\n # Store result\n if [ $exit_code -eq 0 ]; then\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": true, \"duration_ms\": '\"$duration_ms\"'}'\n return 0\n elif [ $exit_code -eq 124 ]; then\n # Timeout\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false, \"timeout\": true, \"duration_ms\": '\"$((TIMEOUT * 1000))\"'}'\n return 1\n else\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false, \"exit_code\": '\"$exit_code\"', \"duration_ms\": '\"$duration_ms\"'}'\n return 1\n fi\n}\n\n# Write results to JSON file\n# The document is built by jq, so any command text is encoded correctly. It is\n# written to a temporary file beside the target and renamed into place: a\n# symlink at the target is replaced, never written through.\nwrite_json_results() {\n if [ \"$SIDECAR_WRITABLE\" != true ]; then\n warn \"Not writing $OUTPUT_JSON: $SIDECAR_DIR is a symlink\"\n return 0\n fi\n\n local timestamp commands='{}' cmd tmp\n # Portable ISO 8601 timestamp (works on both GNU and BSD date)\n timestamp=$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\n\n for cmd in \"${!COMMAND_RESULTS[@]}\"; do\n commands=$(jq -c --arg k \"$cmd\" --argjson v \"${COMMAND_RESULTS[$cmd]}\" '. + {($k): $v}' \u003c\u003c\u003c\"$commands\")\n done\n\n tmp=$(mktemp \"$SIDECAR_DIR/.command-verification.XXXXXX\")\n jq -n --arg t \"$timestamp\" --arg smoke \"$SMOKE_TEST\" --argjson c \"$commands\" \\\n '{verified_at: $t, smoke_tested: ($smoke == \"true\"), commands: $c}' > \"$tmp\"\n [ -L \"$OUTPUT_JSON\" ] && rm -f \"$OUTPUT_JSON\"\n mv -f \"$tmp\" \"$OUTPUT_JSON\"\n\n log \"Results written to $OUTPUT_JSON\"\n}\n\n# Extract commands from markdown code blocks and table cells\n# Look for patterns like: `command arg` or | command | or | `command` |\nextract_commands() {\n # Extract from tables with backticks (| `command` | format)\n # shellcheck disable=SC2016 # grep/sed pattern: backticks are literal.\n grep -oE '\\| `[^`]+`' \"$AGENTS_FILE\" 2>/dev/null | sed 's/| `//;s/`$//' | grep -v '^\\s*

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

|| true\n\n # Extract from tables without backticks in Commands section\n # Look for lines like \"| Lint | vendor/bin/php-cs-fixer fix --dry-run |\"\n # Skip header row and separator row, get 3rd column (command), filter empty and time estimates\n sed -n '/^## Commands/,/^##/p' \"$AGENTS_FILE\" 2>/dev/null | \\\n grep -E '^\\|' | \\\n tail -n +3 | \\\n cut -d'|' -f3 | \\\n sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \\\n grep -v '^

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

| \\\n grep -v '^~' || true\n\n # Extract from inline code that looks like commands\n # Includes: npm, yarn, pnpm, bun, make, go, composer, cargo, python, pip, poetry, uv, deno, gradle, php, vendor/bin\n # shellcheck disable=SC2016 # grep/sed pattern: backticks are literal.\n grep -oE '`(npm |yarn |pnpm |bun |make |go |composer |cargo |pytest |python |pip |poetry |uv |deno |gradle |php |vendor/bin/)[^`]+`' \"$AGENTS_FILE\" 2>/dev/null | sed 's/`//g' || true\n}\n\n# Print the makefile make would read in the current directory and every file\n# it includes, without running make: even `make -n` expands $(shell ...) while\n# it parses, which would run the project's commands just to look up a target.\n# An include naming a variable needs make to expand it and is skipped; globs\n# are expanded; a file outside the project or a symlinked one is ignored.\nmakefile_files() {\n local first=\"\" f word match real root\n for f in GNUmakefile makefile Makefile; do\n if [ -f \"$f\" ]; then first=\"$f\"; break; fi\n done\n [ -n \"$first\" ] || return 0\n root=\"$(pwd -P)\"\n local -a queue=(\"$first\")\n local -A seen=()\n while [ \"${#queue[@]}\" -gt 0 ]; do\n f=\"${queue[0]}\"\n queue=(\"${queue[@]:1}\")\n # A symlinked makefile is skipped; the directory is resolved with\n # cd/pwd -P because macOS realpath has no -e.\n [ -L \"$f\" ] && continue\n [ -f \"$f\" ] || continue\n real=\"$(cd \"$(dirname \"$f\")\" 2>/dev/null && pwd -P)/$(basename \"$f\")\" || continue\n [[ \"$real\" == \"$root\"/* ]] || continue\n [ -n \"${seen[$real]:-}\" ] && continue\n seen[$real]=1\n printf '%s\\n' \"$f\"\n while IFS= read -r word; do\n [[ \"$word\" == *'

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…

* ]] && continue\n while IFS= read -r match; do\n [ -n \"$match\" ] && queue+=(\"$match\")\n done \u003c \u003c(compgen -G \"$word\" || true)\n done \u003c \u003c(awk '/^[ \\t]*(-|s)?include[ \\t]/ {\n sub(/^[ \\t]*(-|s)?include[ \\t]+/, \"\"); sub(/[ \\t]*#.*/, \"\")\n n = split($0, w, /[ \\t]+/)\n for (i = 1; i \u003c= n; i++) if (w[i] != \"\") print w[i]\n }' \"$f\")\n done\n}\n\n# Print the targets of the makefile and its includes, read as text: explicit\n# targets, names listed as .PHONY prerequisites (make accepts those), and\n# pattern rules such as plan-% (matched by makefile_has_target). Variable\n# assignments (VAR = x, VAR := x, VAR ::= x, export VAR := x), recipe lines,\n# define blocks and other special targets are not targets.\nmakefile_targets() {\n local f\n while IFS= read -r f; do\n awk '\n /^define[ \\t]/ || /^define$/ { in_define = 1; next }\n in_define { if ($0 ~ /^endef/) in_define = 0; next }\n /^\\t/ || /^[ \\t]*#/ { next }\n {\n line = $0\n sub(/[ \\t]*#.*/, \"\", line)\n p = index(line, \":\")\n if (p == 0) next\n head = substr(line, 1, p - 1)\n rest = substr(line, p + 1)\n if (rest ~ /^:?=/ || head ~ /[=$]/) next\n if (head ~ /^[ \\t]*\\.PHONY[ \\t]*$/) {\n n = split(rest, t, /[ \\t]+/)\n for (i = 1; i \u003c= n; i++) if (t[i] != \"\" && t[i] !~ /\\$/) print t[i]\n next\n }\n n = split(head, t, /[ \\t]+/)\n for (i = 1; i \u003c= n; i++)\n if (t[i] != \"\" && t[i] !~ /^\\./) print t[i]\n }' \"$f\"\n done \u003c \u003c(makefile_files)\n}\n\n# Is \u003ctarget> a target of the makefile in the current directory, by name or\n# through a pattern rule?\nmakefile_has_target() {\n local target=\"$1\" targets t\n targets=\"$(makefile_targets)\"\n grep -qxF -- \"$target\" \u003c\u003c\u003c\"$targets\" && return 0\n while IFS= read -r t; do\n [[ \"$t\" == *%* ]] || continue\n # shellcheck disable=SC2053 # the pattern is meant to match as a glob\n [[ \"$target\" == ${t//%/*} ]] && return 0\n done \u003c\u003c\u003c\"$targets\"\n return 1\n}\n\n# Verify a single command exists (not that it succeeds, just that it's callable)\nverify_command() {\n local cmd=\"$1\"\n local base_cmd\n\n # Extract the base command (first word)\n base_cmd=$(echo \"$cmd\" | awk '{print $1}')\n\n # Skip placeholders\n if [[ \"$cmd\" == *\"\u003c\"* ]] || [[ \"$cmd\" == *\"{{{\"* ]]; then\n log \"Skipping placeholder: $cmd\"\n ((SKIPPED+=1))\n return 0\n fi\n\n # Skip if it's just a flag or option\n if [[ \"$base_cmd\" == -* ]]; then\n return 0\n fi\n\n log \"Checking: $cmd\"\n\n # Check different command types\n case \"$base_cmd\" in\n npm|yarn|pnpm|bun)\n # Check if package.json script exists\n local script=\"${cmd#* }\"\n script=\"${script#run }\"\n script=\"${script%% *}\"\n if [ -f \"package.json\" ]; then\n local script_exists=false\n if jq -e \".scripts[\\\"$script\\\"]\" package.json > /dev/null 2>&1; then\n script_exists=true\n elif [[ \"$script\" =~ ^(install|test|build|start|run)$ ]]; then\n script_exists=true\n fi\n\n if [ \"$script_exists\" = true ]; then\n if [ \"$SMOKE_TEST\" = true ]; then\n # For test commands, only verify they start (dry-run if available)\n local test_cmd=\"$cmd\"\n if [[ \"$script\" == \"test\" ]]; then\n # Try to use --help or --dry-run to avoid full test run\n test_cmd=\"$base_cmd run $script -- --help 2>/dev/null || $base_cmd run $script --dry-run 2>/dev/null || true\"\n fi\n if smoke_test_command \"$test_cmd\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"$base_cmd script works: $script (~${duration}ms)\"\n ((PASSED+=1))\n else\n warn \"$base_cmd script exists but smoke test failed: $script\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false}'\n ((SKIPPED+=1))\n fi\n else\n success \"$base_cmd script exists: $script\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n warn \"$base_cmd script not found: $script (in $cmd)\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n else\n warn \"No package.json found for: $cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n make)\n # Check if Makefile target exists\n local target=\"${cmd#make }\"\n target=\"${target%% *}\"\n if [ -f \"Makefile\" ] || [ -f \"makefile\" ] || [ -f \"GNUmakefile\" ]; then\n if makefile_has_target \"$target\"; then\n if [ \"$SMOKE_TEST\" = true ]; then\n # Use make -n (dry run) for smoke test to avoid side effects\n if smoke_test_command \"make -n $target\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"make target works: $target (~${duration}ms)\"\n ((PASSED+=1))\n else\n warn \"make target exists but dry-run failed: $target\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false}'\n ((SKIPPED+=1))\n fi\n else\n success \"make target exists: $target\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n error \"make target not found: $target\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((FAILED+=1))\n fi\n else\n warn \"No Makefile found for: $cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n composer)\n # Check if composer script exists\n local script=\"${cmd#composer }\"\n script=\"${script%% *}\"\n if [ -f \"composer.json\" ]; then\n local script_exists=false\n if [[ \"$script\" =~ ^(install|update|require|remove|dump-autoload)$ ]]; then\n script_exists=true\n elif jq -e \".scripts[\\\"$script\\\"]\" composer.json > /dev/null 2>&1; then\n script_exists=true\n fi\n\n if [ \"$script_exists\" = true ]; then\n if [ \"$SMOKE_TEST\" = true ]; then\n # For composer scripts, try --dry-run or --help\n local test_cmd=\"$cmd\"\n if [[ \"$script\" =~ ^(install|update)$ ]]; then\n test_cmd=\"composer $script --dry-run 2>/dev/null || true\"\n fi\n if smoke_test_command \"$test_cmd\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"composer script works: $script (~${duration}ms)\"\n ((PASSED+=1))\n else\n warn \"composer script exists but smoke test failed: $script\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true, \"runs\": false}'\n ((SKIPPED+=1))\n fi\n else\n success \"composer script exists: $script\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n warn \"composer script not found: $script\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n else\n warn \"No composer.json found for: $cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n python|python3|pip|pip3)\n # Python commands\n if command -v \"$base_cmd\" > /dev/null 2>&1; then\n success \"Python command available: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"Python command not found: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n poetry)\n # Poetry package manager\n if command -v poetry > /dev/null 2>&1; then\n local subcmd=\"${cmd#poetry }\"\n subcmd=\"${subcmd%% *}\"\n if [[ \"$subcmd\" =~ ^(install|add|remove|update|build|publish|run|shell)$ ]]; then\n success \"poetry command: $subcmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n # Check if it's a custom script\n if [ -f \"pyproject.toml\" ] && grep -q \"\\[tool.poetry.scripts\\]\" pyproject.toml 2>/dev/null; then\n success \"poetry command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"poetry script not found: $subcmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n fi\n else\n warn \"poetry not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n uv)\n # uv package manager\n if command -v uv > /dev/null 2>&1; then\n success \"uv command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"uv not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n pytest)\n # pytest test runner\n if command -v pytest > /dev/null 2>&1; then\n success \"pytest available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"pytest not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n cargo)\n # Rust cargo\n if command -v cargo > /dev/null 2>&1; then\n success \"cargo command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"cargo not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n deno)\n # Deno runtime\n if command -v deno > /dev/null 2>&1; then\n success \"deno command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"deno not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n gradle|gradlew|./gradlew)\n # Gradle build tool\n if [ -f \"gradlew\" ] || [ -f \"build.gradle\" ] || [ -f \"build.gradle.kts\" ]; then\n success \"gradle project detected\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n elif command -v gradle > /dev/null 2>&1; then\n success \"gradle command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n else\n warn \"gradle not found\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n\n go)\n # Go commands are generally available if go is installed\n if command -v go > /dev/null 2>&1; then\n if [ \"$SMOKE_TEST\" = true ]; then\n if smoke_test_command \"$cmd\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"go command ran successfully (~${duration}ms)\"\n ((PASSED+=1))\n else\n error \"go command failed: $cmd\"\n ((FAILED+=1))\n fi\n else\n success \"go command available\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n error \"go not installed\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((FAILED+=1))\n fi\n ;;\n\n vendor/bin/*)\n # PHP vendor binary\n if [ -f \"$base_cmd\" ]; then\n if [ \"$SMOKE_TEST\" = true ]; then\n # For test commands, run with --help to avoid actually running tests\n local test_cmd=\"$cmd\"\n if [[ \"$cmd\" == *\"phpunit\"* ]] && [[ \"$cmd\" != *\"--help\"* ]]; then\n test_cmd=\"$base_cmd --help\"\n fi\n if smoke_test_command \"$test_cmd\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"vendor binary works (~${duration}ms)\"\n ((PASSED+=1))\n else\n warn \"vendor binary exists but failed: $cmd\"\n ((SKIPPED+=1))\n fi\n else\n success \"vendor binary exists: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n error \"vendor binary not found: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((FAILED+=1))\n fi\n ;;\n\n *)\n # Check if command exists in PATH\n if command -v \"$base_cmd\" > /dev/null 2>&1; then\n if [ \"$SMOKE_TEST\" = true ]; then\n if smoke_test_command \"$cmd\"; then\n local duration=\"${COMMAND_RESULTS[$cmd]}\"\n duration=$(echo \"$duration\" | grep -oE '\"duration_ms\": [0-9]+' | cut -d: -f2 | tr -d ' ')\n success \"command works (~${duration}ms)\"\n ((PASSED+=1))\n else\n warn \"command exists but failed: $cmd\"\n ((SKIPPED+=1))\n fi\n else\n success \"command exists: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": true}'\n ((PASSED+=1))\n fi\n else\n warn \"command not in PATH: $base_cmd\"\n COMMAND_RESULTS[\"$cmd\"]='{\"exists\": false}'\n ((SKIPPED+=1))\n fi\n ;;\n esac\n}\n\n# Get unique commands\nmapfile -t commands \u003c \u003c(extract_commands | sort -u)\n\nif [ ${#commands[@]} -eq 0 ]; then\n warn \"No commands found in AGENTS.md\"\n if [[ \"$JSON\" = true ]]; then\n jq -nc --argjson p \"$PASSED\" --argjson s \"$SKIPPED\" --argjson f \"$FAILED\" \\\n '{script:\"verify-commands\",schema:1,summary:{passed:$p,skipped:$s,failed:$f},commands:[]}' >&3\n fi\n exit 0\nfi\n\necho \"Found ${#commands[@]} unique commands to verify\"\necho \"\"\n\nfor cmd in \"${commands[@]}\"; do\n [ -n \"$cmd\" ] && verify_command \"$cmd\"\ndone\n\n# Emit JSON document (machine-readable) and exit before the human summary.\n# Summary counts come from the authoritative PASSED/SKIPPED/FAILED counters; the\n# commands[] array is built best-effort from COMMAND_RESULTS (iterating its keys,\n# parsing each stored value as JSON, else null).\nif [[ \"$JSON\" = true ]]; then\n if [[ \"${#COMMAND_RESULTS[@]}\" -gt 0 ]]; then\n # Iterate keys in SORTED order: associative-array key order is otherwise\n # unspecified in Bash, which would make commands[] order vary run to run.\n # read -r (not word-split) preserves command strings that contain spaces.\n while IFS= read -r cmd; do\n stored=\"${COMMAND_RESULTS[$cmd]}\"\n if entry=$(jq -nc --arg cmd \"$cmd\" --argjson detail \"$stored\" \\\n '{cmd:$cmd,detail:$detail}' 2>/dev/null); then\n JSON_CMDS+=(\"$entry\")\n else\n JSON_CMDS+=(\"$(jq -nc --arg cmd \"$cmd\" '{cmd:$cmd,detail:null}')\")\n fi\n done \u003c \u003c(printf '%s\\n' \"${!COMMAND_RESULTS[@]}\" | LC_ALL=C sort)\n fi\n\n if [[ \"${#JSON_CMDS[@]}\" -eq 0 ]]; then\n arr_json='[]'\n else\n arr_json=$(printf '%s\\n' \"${JSON_CMDS[@]}\" | jq -s '.')\n fi\n\n jq -nc \\\n --argjson items \"$arr_json\" \\\n --argjson p \"$PASSED\" \\\n --argjson s \"$SKIPPED\" \\\n --argjson f \"$FAILED\" \\\n '{script:\"verify-commands\",schema:1,summary:{passed:$p,skipped:$s,failed:$f},commands:$items}' >&3\n\n if [[ \"$FAILED\" -gt 0 ]]; then exit 1; fi\n exit 0\nfi\n\necho \"\"\necho \"======================================\"\necho \"Verification Summary\"\necho \"======================================\"\necho -e \"${GREEN}Passed:${NC} $PASSED\"\necho -e \"${YELLOW}Skipped:${NC} $SKIPPED\"\necho -e \"${RED}Failed:${NC} $FAILED\"\necho \"\"\n\nif [ \"$FAILED\" -gt 0 ]; then\n echo -e \"${RED}Some commands in AGENTS.md are invalid!${NC}\"\n echo \"Update AGENTS.md to fix broken command references.\"\n\n # Still write JSON results\n if [ \"$DRY_RUN\" = false ] && [ ${#COMMAND_RESULTS[@]} -gt 0 ]; then\n write_json_results\n fi\n\n exit 1\nelse\n echo -e \"${GREEN}All verifiable commands are valid.${NC}\"\n\n # Write JSON results\n if [ \"$DRY_RUN\" = false ] && [ ${#COMMAND_RESULTS[@]} -gt 0 ]; then\n write_json_results\n [ \"$SIDECAR_WRITABLE\" = true ] && echo \"Verification results saved to $OUTPUT_JSON\"\n fi\n\n # A verification run changes the file it checks only when asked to.\n if [ \"$DRY_RUN\" = false ] && [ \"$UPDATE_VERIFIED\" = true ] && [ -w \"$AGENTS_FILE\" ]; then\n TODAY=$(date +%Y-%m-%d)\n if grep -q \"Last verified:\" \"$AGENTS_FILE\"; then\n # Portable sed -i: use backup extension then remove backup\n sed -i.bak \"s/Last verified: .*/Last verified: $TODAY -->/\" \"$AGENTS_FILE\" && rm -f \"$AGENTS_FILE.bak\"\n echo \"Updated 'Last verified' timestamp to $TODAY\"\n fi\n fi\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":33268,"content_sha256":"5fb4bf44e6a73a9471f2a02709dd5211873bed5597a0b70c5607ade2e51ca938"},{"filename":"scripts/verify-content.sh","content":"#!/usr/bin/env bash\n# SPDX-License-Identifier: MIT\n# SPDX-FileCopyrightText: Netresearch DTT GmbH\n# verify-content.sh - Verify AGENTS.md content against actual codebase state\n# This script checks if documented information matches reality.\n#\n# CRITICAL: Never trust existing AGENTS.md content. Always verify.\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\nPROJECT_DIR=\"${1:-.}\"\nVERBOSE=false\nFIX_MODE=false\nJSON=false\nEXIT_CODE=0\n# Relative path of the AGENTS.md currently being verified; captured into each\n# issue's \"file\" field in JSON mode. Empty => emitted as JSON null.\nCURRENT_FILE=\"\"\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\n# Parse flags\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --verbose|-v)\n VERBOSE=true\n shift\n ;;\n --fix)\n FIX_MODE=true\n shift\n ;;\n --json)\n JSON=true\n shift\n ;;\n --help|-h)\n cat \u003c\u003cEOF\nUsage: verify-content.sh [PROJECT_DIR] [OPTIONS]\n\nVerify AGENTS.md content against actual codebase state.\n\nThis script extracts information from the actual codebase and compares\nit against what's documented in AGENTS.md files. Reports discrepancies\nthat need manual review or correction.\n\nOptions:\n --verbose, -v Show detailed comparison output\n --fix Suggest fixes for common issues\n --json Emit machine-readable JSON on stdout (human output suppressed)\n --help, -h Show this help message\n\nVerification checks:\n 1. Documented files exist (scripts, modules, tests)\n 2. Documented commands work (make targets)\n 3. Counts are accurate (module count, script count)\n 4. Descriptions match actual docstrings\n\nExit codes:\n 0 - All checks passed\n 1 - Discrepancies found\n 2 - Error running verification\n\nExamples:\n verify-content.sh . # Verify current project\n verify-content.sh /path/to/project -v # Verbose output\n verify-content.sh . --fix # Show fix suggestions\nEOF\n exit 0\n ;;\n *)\n PROJECT_DIR=\"$1\"\n shift\n ;;\n esac\ndone\n\ncd \"$PROJECT_DIR\"\n\n# In JSON mode, route all human-readable output to /dev/null and reserve the\n# original stdout (fd 3) for the single JSON document emitted at the end. This\n# keeps --json strictly additive: the default (no-flag) path is byte-for-byte\n# unchanged.\nJSON_ISSUES=()\nif [[ \"$JSON\" = true ]]; then\n exec 3>&1 1>/dev/null\nfi\n\necho \"Verifying AGENTS.md content in: $(pwd)\"\necho \"\"\n\n# Track issues\ndeclare -a ISSUES=()\n\nadd_issue() {\n local severity=\"$1\"\n local message=\"$2\"\n ISSUES+=(\"[$severity] $message\")\n EXIT_CODE=1\n # In JSON mode, also record a structured issue. \"message\" is stored without\n # the [SEVERITY] prefix; \"file\" is the AGENTS.md currently under inspection\n # (CURRENT_FILE), coerced to JSON null when empty.\n if [[ \"$JSON\" = true ]]; then\n JSON_ISSUES+=(\"$(jq -nc --arg sev \"$severity\" --arg msg \"$message\" --arg file \"$CURRENT_FILE\" \\\n '{severity:$sev,message:$msg,file:($file|if .==\"\" then null else . end)}')\")\n fi\n}\n\nlog_check() {\n if [ \"$VERBOSE\" = true ]; then\n echo \" Checking: $1\"\n fi\n}\n\n# ============================================================================\n# VERIFICATION FUNCTIONS\n# ============================================================================\n\n# shellcheck disable=SC2329 # Called indirectly by verification driver\nverify_file_exists() {\n local file=\"$1\"\n local source=\"$2\"\n if [ ! -f \"$file\" ] && [ ! -d \"$file\" ]; then\n add_issue \"ERROR\" \"File documented in $source does not exist: $file\"\n return 1\n fi\n return 0\n}\n\nverify_makefile_target() {\n local target=\"$1\"\n if [ -f \"Makefile\" ]; then\n # Check in main Makefile and includes\n if ! grep -rq \"^${target}:\" Makefile Makefile.d/ 2>/dev/null; then\n add_issue \"WARN\" \"Makefile target may not exist: $target\"\n return 1\n fi\n fi\n return 0\n}\n\nextract_documented_files() {\n local agents_file=\"$1\"\n # Extract file references inside markdown code spans, e.g. `filename.sh`.\n # NOTE: the delimiters must be LITERAL backticks. A previous version used\n # \\` which GNU grep interprets as the \"start of buffer\" anchor, so this\n # check silently matched nothing.\n # shellcheck disable=SC2016 # Literal backticks are intended (markdown code spans), no expansion wanted\n grep -oE '`[a-zA-Z0-9_-]+\\.(sh|py|go|php|ts|js|json)`' \"$agents_file\" 2>/dev/null | tr -d '`' | sort -u || true\n}\n\nextract_documented_scripts() {\n local agents_file=\"$1\"\n # Extract script references like install_python.sh, guide.sh\n grep -oE '\\b[a-zA-Z0-9_-]+\\.sh\\b' \"$agents_file\" 2>/dev/null | sort -u || true\n}\n\n# ============================================================================\n# ROOT AGENTS.MD VERIFICATION\n# ============================================================================\n\necho \"=== Verifying Root AGENTS.md ===\"\n\n# Attribute root-section issues to the root AGENTS.md.\nCURRENT_FILE=\"AGENTS.md\"\n\nif [ -f \"AGENTS.md\" ]; then\n echo -e \"${GREEN}✓${NC} Root AGENTS.md exists\"\n\n # Check documented make targets\n log_check \"Makefile targets\"\n # `|| true`: with `set -o pipefail`, a no-match grep makes this pipeline exit\n # non-zero, which under `set -e` aborts the entire script here in the root\n # section -- so the scoped-file loop below never runs. Tolerating the\n # non-zero (as MODULE_COUNTS/SCRIPT_COUNTS already do) lets verification\n # continue. Pre-existing bug: AGENTS.md without 'make ' commands aborted early.\n DOCUMENTED_TARGETS=$(grep -oE 'make [a-z_-]+' AGENTS.md | sed 's/make //' | sort -u || true)\n for target in $DOCUMENTED_TARGETS; do\n verify_makefile_target \"$target\" || true\n done\n\n # Check for module count claims\n log_check \"Module counts\"\n MODULE_COUNTS=$(grep -oE '[0-9]+ (Python )?modules?' AGENTS.md | grep -oE '[0-9]+' || true)\n for count in $MODULE_COUNTS; do\n # Try to find actual module directories\n for dir in src cli_audit lib Classes app; do\n if [ -d \"$dir\" ]; then\n ACTUAL_COUNT=$(find \"$dir\" -maxdepth 1 -name \"*.py\" -o -name \"*.php\" -o -name \"*.go\" 2>/dev/null | wc -l)\n if [ \"$ACTUAL_COUNT\" -gt 0 ] && [ \"$count\" != \"$ACTUAL_COUNT\" ]; then\n add_issue \"WARN\" \"Module count mismatch in $dir: documented=$count, actual=$ACTUAL_COUNT\"\n fi\n fi\n done\n done\n\n # Check for script count claims\n log_check \"Script counts\"\n SCRIPT_COUNTS=$(grep -oE '[0-9]+ (Bash )?scripts?' AGENTS.md | grep -oE '[0-9]+' || true)\n if [ -d \"scripts\" ] && [ -n \"$SCRIPT_COUNTS\" ]; then\n ACTUAL_SCRIPTS=$(find scripts -maxdepth 1 -name \"*.sh\" 2>/dev/null | wc -l)\n for count in $SCRIPT_COUNTS; do\n if [ \"$count\" != \"$ACTUAL_SCRIPTS\" ]; then\n add_issue \"WARN\" \"Script count mismatch: documented=$count, actual=$ACTUAL_SCRIPTS\"\n fi\n done\n fi\nelse\n add_issue \"ERROR\" \"Root AGENTS.md does not exist\"\nfi\n\necho \"\"\n\n# ============================================================================\n# SCOPED AGENTS.MD VERIFICATION\n# ============================================================================\n\necho \"=== Verifying Scoped AGENTS.md Files ===\"\n\n# Find all scoped AGENTS.md files. Dependency trees are excluded, as in\n# validate-structure.sh (#84): an AGENTS.md shipped by a package in vendor/,\n# node_modules/ or a TYPO3 extension's .Build/vendor/ documents that package,\n# not this project, and its file references would be reported as errors here.\nSCOPED_FILES=$(find . -mindepth 2 -name \"AGENTS.md\" \\\n -not -path \"./.git/*\" \\\n -not -path \"*/vendor/*\" \\\n -not -path \"*/node_modules/*\" \\\n -not -path \"*/.Build/*\" \\\n 2>/dev/null || true)\n\nfor scoped_file in $SCOPED_FILES; do\n scope_dir=$(dirname \"$scoped_file\")\n scope_name=$(basename \"$scope_dir\")\n # Attribute this iteration's issues to the scoped file (relative, no ./).\n CURRENT_FILE=\"${scoped_file#./}\"\n echo \"\"\n echo \"Checking: $scoped_file\"\n\n # Extract and verify documented files\n log_check \"Documented files exist\"\n DOCUMENTED=$(extract_documented_files \"$scoped_file\")\n\n for doc_file in $DOCUMENTED; do\n # A documented file may live anywhere in the tree (src/, app/, ...), not\n # just the scope dir, so search the whole project for its basename and\n # only flag when it exists NOWHERE. This avoids false positives from the\n # previous narrow 3-location check.\n if ! find . -name \"$doc_file\" \\\n -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' \\\n -not -path '*/.Build/*' \\\n -print -quit 2>/dev/null | grep -q .; then\n # Special handling for common false positives\n case \"$doc_file\" in\n *.json|*.md|*.yml|*.yaml)\n # Config files might be in various locations, skip\n ;;\n *)\n add_issue \"ERROR\" \"File documented in $scoped_file does not exist: $doc_file\"\n ;;\n esac\n fi\n done\n\n # For scripts/ scope, verify all documented scripts exist\n if [ \"$scope_name\" = \"scripts\" ]; then\n log_check \"Script files exist\"\n SCRIPTS=$(extract_documented_scripts \"$scoped_file\")\n for script in $SCRIPTS; do\n # Check multiple locations: scripts/, scripts/lib/, ./\n if [ ! -f \"scripts/$script\" ] && [ ! -f \"scripts/lib/$script\" ] && [ ! -f \"./$script\" ]; then\n # Skip if it's in a commit message example (common pattern: feat(scripts): add xxx.sh)\n if grep -q \"feat(scripts):.*$script\\|fix(scripts):.*$script\\|chore(scripts):.*$script\" \"$scoped_file\" 2>/dev/null; then\n [ \"$VERBOSE\" = true ] && echo \" Skipping $script (commit message example)\"\n continue\n fi\n add_issue \"ERROR\" \"Script documented but does not exist: $script\"\n fi\n done\n\n # Check for undocumented scripts\n log_check \"All scripts documented\"\n if [ -d \"scripts\" ]; then\n for actual_script in scripts/*.sh; do\n script_name=$(basename \"$actual_script\")\n if ! grep -q \"$script_name\" \"$scoped_file\" 2>/dev/null; then\n add_issue \"WARN\" \"Script exists but not documented: $script_name\"\n fi\n done\n fi\n fi\n\n # For tests/ scope, verify test file listing\n if [ \"$scope_name\" = \"tests\" ]; then\n log_check \"Test files documented\"\n if [ -d \"tests\" ]; then\n for test_file in tests/test_*.py; do\n if [ -f \"$test_file\" ]; then\n test_name=$(basename \"$test_file\")\n if ! grep -q \"$test_name\" \"$scoped_file\" 2>/dev/null; then\n add_issue \"WARN\" \"Test file exists but not documented: $test_name\"\n fi\n fi\n done\n fi\n fi\n\n echo -e \"${GREEN}✓${NC} Checked: $scoped_file\"\ndone\n\n# ============================================================================\n# JSON OUTPUT\n# ============================================================================\n\n# Emit the machine-readable JSON document (to the reserved fd 3) and exit using\n# the SAME EXIT_CODE the human path uses. summary.errors/warns are derived from\n# the assembled issues array; total mirrors ${#ISSUES[@]}.\nif [[ \"$JSON\" = true ]]; then\n if [[ \"${#JSON_ISSUES[@]}\" -eq 0 ]]; then\n issues_json='[]'\n else\n issues_json=$(printf '%s\\n' \"${JSON_ISSUES[@]}\" | jq -s '.')\n fi\n jq -nc \\\n --argjson items \"$issues_json\" \\\n --argjson total \"${#ISSUES[@]}\" \\\n '{script:\"verify-content\",schema:1,\n summary:{\n errors:([$items[]|select(.severity==\"ERROR\")]|length),\n warns:([$items[]|select(.severity==\"WARN\")]|length),\n total:$total\n },\n issues:$items}' >&3\n exit $EXIT_CODE\nfi\n\n# ============================================================================\n# SUMMARY\n# ============================================================================\n\necho \"\"\necho \"=== Verification Summary ===\"\n\nif [ ${#ISSUES[@]} -eq 0 ]; then\n echo -e \"${GREEN}✓ All verification checks passed!${NC}\"\nelse\n echo -e \"${RED}Found ${#ISSUES[@]} issue(s):${NC}\"\n echo \"\"\n for issue in \"${ISSUES[@]}\"; do\n case \"$issue\" in\n *ERROR*)\n echo -e \" ${RED}$issue${NC}\"\n ;;\n *WARN*)\n echo -e \" ${YELLOW}$issue${NC}\"\n ;;\n *)\n echo \" $issue\"\n ;;\n esac\n done\n\n if [ \"$FIX_MODE\" = true ]; then\n echo \"\"\n echo \"=== Fix Suggestions ===\"\n echo \"1. Run extraction scripts to get actual state:\"\n echo \" $SCRIPT_DIR/detect-project.sh .\"\n echo \" $SCRIPT_DIR/extract-commands.sh .\"\n echo \"\"\n echo \"2. Compare extracted info with AGENTS.md content\"\n echo \"\"\n echo \"3. Update AGENTS.md files with verified information\"\n echo \"\"\n echo \"4. Re-run this verification: $0 . --verbose\"\n fi\nfi\n\necho \"\"\nexit $EXIT_CODE\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":13518,"content_sha256":"029aa53c25d5a7947388529ae3ef7e0aaf73ce391f89404783426279a8e5b9d0"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"AGENTS.md Generator Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Generate and maintain AGENTS.md files following the ","type":"text"},{"text":"agents.md convention","type":"text","marks":[{"type":"link","attrs":{"href":"https://agents.md/","title":null}}]},{"text":". AGENTS.md is FOR AGENTS, not humans.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating or updating AGENTS.md for new/existing projects","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scaffolding a new repository","type":"text","marks":[{"type":"strong"}]},{"text":" — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Standardizing agent documentation across repositories","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checking AGENTS.md freshness after code changes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Onboarding AI agents to an unfamiliar codebase","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Call every script by its full path: ","type":"text"},{"text":"bash ${CLAUDE_SKILL_DIR}/scripts/\u003cname> PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":". Calling one relative to the working directory is not covered by the frontmatter rule and raises a permission prompt per call.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generate-agents.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate AGENTS.md files","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validate-structure.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validate structure compliance","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check-freshness.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check if files are outdated","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"verify-content.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify documented files/commands match codebase","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"verify-commands.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify documented commands execute","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"score-agents.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Grade AGENTS.md quality, worst-first","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"detect-project.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Detect language, version, build tools","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"detect-scopes.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Identify directories needing scoped files","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"extract-commands.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract commands from build configs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"extract-ci-rules.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract CI quality gates and version matrix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"extract-architecture-rules.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract module boundaries","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"extract-adrs.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract architectural decision records","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"extract-github-rulesets.sh PATH","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract GitHub rulesets and merge rules","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/scripts-guide.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for full options.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detect","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"detect-project.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"detect-scopes.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — stacks and subsystems","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Extract","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"extract-commands.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"extract-ci-rules.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — gather facts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Generate","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"generate-agents.sh --style=thin","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default) or ","type":"text"},{"text":"--verbose","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"verify-content.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"verify-commands.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- MANDATORY before done","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Line cap","type":"text","marks":[{"type":"strong"}]},{"text":": before committing any root AGENTS.md edit, ","type":"text"},{"text":"wc -l AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" must be under 150 (harness ","type":"text"},{"text":"AH-02","type":"text","marks":[{"type":"code_inline"}]},{"text":"); run the repo's ","type":"text"},{"text":"verify-harness.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" where it exists -- CI's harness check fails otherwise","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"--update","type":"text","marks":[{"type":"code_inline"}]},{"text":" preserves curated content outside ","type":"text"},{"text":"\u003c!-- GENERATED -->","type":"text","marks":[{"type":"code_inline"}]},{"text":" markers.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Principles","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Structured over Prose","type":"text","marks":[{"type":"strong"}]},{"text":" -- tables parse faster than paragraphs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never Fabricate","type":"text","marks":[{"type":"strong"}]},{"text":" -- only document what exists; verify every command and path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pointer Principle","type":"text","marks":[{"type":"strong"}]},{"text":" -- point to files, don't duplicate content","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Auto Symlinks","type":"text","marks":[{"type":"strong"}]},{"text":" -- CLAUDE.md/GEMINI.md by default (","type":"text"},{"text":"ai-tool-compatibility.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ai-tool-compatibility.md","title":null}},{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"File","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Contents","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"verification-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/verification-guide.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verification steps, anti-bloat, preservation check","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fleet-sync-sweep.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/fleet-sync-sweep.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fleet-wide AGENTS.md sweeps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/scripts-guide.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script options, validation checklist","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"quality-rubric.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/quality-rubric.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Grading rubric","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai-tool-compatibility.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ai-tool-compatibility.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"16-agent compatibility matrix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"output-structure.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/output-structure.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Root/scoped sections","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"git-hooks-setup.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/git-hooks-setup.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hook framework setup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"examples/","type":"text","marks":[{"type":"link","attrs":{"href":"references/examples/","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complete examples","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai-contribution-guidelines.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ai-contribution-guidelines.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"3 Cs\" AI-contribution framework","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"directory-coverage.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/directory-coverage.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scoped-file coverage rationale","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"feedback-memory-schema.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/feedback-memory-schema.md","title":null}},{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Approved-learning file format","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Templates","type":"text"}]},{"type":"paragraph","content":[{"text":"Root: ","type":"text"},{"text":"assets/root-thin.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default) or ","type":"text"},{"text":"root-verbose.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Scoped: ","type":"text"},{"text":"assets/scoped/","type":"text","marks":[{"type":"code_inline"}]},{"text":", one per stack (Go/PHP/Python/TYPO3/Symfony/Oro/CLI/TS/skill-repo).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Supported Projects","type":"text"}]},{"type":"paragraph","content":[{"text":"Go, PHP (Composer/Laravel/Symfony/TYPO3/Oro), TypeScript (React/Next/Vue/Node), Python (pip/poetry/ruff/mypy), skill repos, hybrid.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"See Also","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"agent-harness-skill","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/netresearch/agent-harness-skill","title":null}},{"type":"code_inline"}]},{"text":" — agent-readiness harness (CI enforcement).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"skill-repo-skill","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/netresearch/skill-repo-skill","title":null}},{"type":"code_inline"}]},{"text":" — skill-repo structure (plugin.json, licensing, releases).","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"agent-rules","author":"@skillopedia","source":{"stars":89,"repo_name":"agent-rules-skill","origin_url":"https://github.com/netresearch/agent-rules-skill/blob/HEAD/skills/agent-rules/SKILL.md","repo_owner":"netresearch","body_sha256":"00303bcca16983039caaeaca9db2b1b07645dda62528b09f3fd9fbb2cb7ca5b5","cluster_key":"b2d995fc9aeb17c16a40bc262e07fc5526c2669bee45fcafc7ef5b829a3cca66","clean_bundle":{"format":"clean-skill-bundle-v1","source":"netresearch/agent-rules-skill/skills/agent-rules/SKILL.md","text_bytes":733612,"source_commit":"aa0ff50699a60d4a02de70be3af83182b766fe33","skills_sh_rank":14627,"attachment_count":100,"text_attachments":100,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/agent-rules/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"license":"(MIT AND CC-BY-SA-4.0). See LICENSE-MIT and LICENSE-CC-BY-SA-4.0","version":"v1","category":"general","metadata":{"author":"Netresearch DTT GmbH","version":"3.16.5","repository":"https://github.com/netresearch/agent-rules-skill"},"import_tag":"skills-sh-top20k-2026-11","description":"Use when creating or updating AGENTS.md files, .github/copilot-instructions.md, or other AI agent rule files, onboarding AI agents to a project, standardizing agent documentation, or when anyone mentions AGENTS.md, agent rules, project onboarding, or codebase documentation for AI agents.","allowed-tools":"Bash(${CLAUDE_SKILL_DIR}/scripts/*) Bash(bash ${CLAUDE_SKILL_DIR}/scripts/*) Bash(git:*) Bash(jq:*) Bash(grep:*) Bash(find:*) Read Glob Grep","compatibility":"Requires bash 4.3+, jq 1.7+, git 2.0+."}},"renderedAt":1793661317857}

AGENTS.md Generator Skill Generate and maintain AGENTS.md files following the agents.md convention. AGENTS.md is FOR AGENTS, not humans. When to Use - Creating or updating AGENTS.md for new/existing projects - Scaffolding a new repository — ship AGENTS.md with the initial commits; retrofitting later needs full re-verification - Standardizing agent documentation across repositories - Checking AGENTS.md freshness after code changes - Onboarding AI agents to an unfamiliar codebase Scripts Call every script by its full path: . Calling one relative to the working directory is not covered by the fr…