From a384b9f70c4a133a1d83773100c9063a20916726 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 3 Mar 2026 12:10:49 -0500 Subject: [PATCH] feat: add code-review skill suite (diff, file, standards, github) Three new embedded skills for AI-powered code review: - code-review: Script-backed skill with code_review_diff and code_review_file tools. Supports GitHub PR URLs and local git diffs with Anthropic/OpenAI routing, large diff handling, untracked file inclusion, and custom org standards via .forge-review/ directory. - code-review-standards: Standards discovery and initialization skill with templates for config.yaml, ignore patterns, and language-specific rules (Go, Python, TypeScript, security, testing). - code-review-github: PR workflow orchestration skill for listing PRs, posting inline comments, applying labels, and guarded auto-merge. Also fixes a bug in runner.go where OneOf env vars were not passed through to the SkillCommandExecutor, causing skills using one_of requirements (like ANTHROPIC_API_KEY/OPENAI_API_KEY) to fail at runtime. --- forge-cli/runtime/runner.go | 1 + .../embedded/code-review-github/SKILL.md | 219 ++++++++++ .../embedded/code-review-standards/SKILL.md | 208 ++++++++++ .../.forge-review/.forge-review-ignore | 42 ++ .../templates/.forge-review/config.yaml | 44 ++ .../.forge-review/standards/general.md | 60 +++ .../templates/.forge-review/standards/go.md | 83 ++++ .../.forge-review/standards/python.md | 78 ++++ .../.forge-review/standards/security.md | 74 ++++ .../.forge-review/standards/testing.md | 61 +++ .../.forge-review/standards/typescript.md | 84 ++++ .../local/embedded/code-review/SKILL.md | 224 +++++++++++ .../code-review/scripts/code-review-diff.sh | 376 ++++++++++++++++++ .../code-review/scripts/code-review-file.sh | 284 +++++++++++++ forge-skills/local/registry_embedded_test.go | 17 +- 15 files changed, 1848 insertions(+), 7 deletions(-) create mode 100644 forge-skills/local/embedded/code-review-github/SKILL.md create mode 100644 forge-skills/local/embedded/code-review-standards/SKILL.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/.forge-review-ignore create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/config.yaml create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/general.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/go.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/python.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/security.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/testing.md create mode 100644 forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/typescript.md create mode 100644 forge-skills/local/embedded/code-review/SKILL.md create mode 100755 forge-skills/local/embedded/code-review/scripts/code-review-diff.sh create mode 100755 forge-skills/local/embedded/code-review/scripts/code-review-file.sh diff --git a/forge-cli/runtime/runner.go b/forge-cli/runtime/runner.go index 3058f16..a4410b2 100644 --- a/forge-cli/runtime/runner.go +++ b/forge-cli/runtime/runner.go @@ -1639,6 +1639,7 @@ func (r *Runner) registerSkillTools(reg *tools.Registry, proxyURL string) { var envVars []string if entry.ForgeReqs != nil && entry.ForgeReqs.Env != nil { envVars = append(envVars, entry.ForgeReqs.Env.Required...) + envVars = append(envVars, entry.ForgeReqs.Env.OneOf...) envVars = append(envVars, entry.ForgeReqs.Env.Optional...) } diff --git a/forge-skills/local/embedded/code-review-github/SKILL.md b/forge-skills/local/embedded/code-review-github/SKILL.md new file mode 100644 index 0000000..65129ca --- /dev/null +++ b/forge-skills/local/embedded/code-review-github/SKILL.md @@ -0,0 +1,219 @@ +--- +name: code-review-github +category: dev +tags: + - code-review + - github + - pull-request + - ci + - automation +description: GitHub PR workflow orchestration for code review — list PRs, post comments, apply labels, and guarded auto-merge +metadata: + forge: + requires: + bins: + - gh + - jq + env: + required: + - GH_TOKEN + one_of: [] + optional: + - REVIEW_AUTO_MERGE + - REVIEW_LABEL_PREFIX + egress_domains: + - api.github.com + - github.com + denied_tools: + - http_request + - web_search +--- + +# Code Review GitHub Integration + +Orchestrates GitHub PR workflows for AI code review. Provides tools to list pull requests, post inline review comments, apply labels based on review findings, and (with explicit opt-in) auto-merge clean PRs. + +All GitHub API interactions go through the `gh` CLI. Direct HTTP requests and web searches are denied to ensure all operations are auditable and respect GitHub's rate limiting. + +## Authentication + +```bash +export GH_TOKEN="ghp_..." +``` + +The token needs **read-write** access since this skill posts comments, applies labels, and can merge PRs: + +| Scope (classic PAT) | Fine-grained permission | Why | +|----------------------|------------------------|-----| +| `repo` | Pull requests: Read & Write | List PRs, post review comments, merge | +| `repo` | Contents: Read | Read PR diff and file contents | +| `repo` | Issues: Write | Apply and remove labels | +| `read:org` (optional) | Organization: Read | Org-level label policies | + +**Minimum fine-grained token permissions:** `pull_requests: write`, `contents: read`, `issues: write` on the target repository. + +For read-only review without GitHub interaction, use the `code-review` skill instead — it only needs read access. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| GH_TOKEN | yes | GitHub personal access token or fine-grained token | +| REVIEW_AUTO_MERGE | no | Set to `true` to enable guarded auto-merge. Default: disabled | +| REVIEW_LABEL_PREFIX | no | Prefix for review labels (default: `review/`). Labels: `review/approved`, `review/needs-changes`, `review/security-concern` | + +## Tool: review_github_list_prs + +List open pull requests for a repository, optionally filtered by author or label. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| repo | string | yes | Repository in `owner/repo` format | +| author | string | no | Filter by PR author username | +| label | string | no | Filter by label | +| limit | integer | no | Maximum PRs to return (default: 10, max: 100) | + +**Output:** JSON array of pull requests with `number`, `title`, `author`, `url`, `labels`, `created_at`, `updated_at`, `mergeable_state`. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "list PRs", "show open pull requests", "what PRs need review" +- Requests to find PRs by a specific author or with specific labels + +## Tool: review_github_post_comments + +Post inline review comments on a GitHub pull request based on code review findings. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| repo | string | yes | Repository in `owner/repo` format | +| pr_number | integer | yes | Pull request number | +| findings | array | yes | Array of findings from `code_review_diff` output | +| submit_review | boolean | no | Submit as a formal review (not just individual comments). Default: true | +| review_event | string | no | Review event type: `COMMENT`, `APPROVE`, `REQUEST_CHANGES`. Default: `COMMENT` | + +Each finding in the array should have: `file`, `line`, `severity`, `title`, `description`, `suggestion`. + +**Output:** JSON object with `comments_posted`, `review_url`, and any `errors` for comments that failed. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "post review comments", "comment on the PR", "submit review" +- After running `code_review_diff`, user asks to post findings to GitHub + +### Comment Format + +Comments are posted as inline review comments with this format: + +``` +**[severity] category: title** + +description + +💡 **Suggestion:** suggestion +``` + +## Tool: review_github_apply_labels + +Apply review-status labels to a pull request based on review findings. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| repo | string | yes | Repository in `owner/repo` format | +| pr_number | integer | yes | Pull request number | +| risk_level | string | yes | Overall risk level from review: `low`, `medium`, `high`, `critical` | +| has_security_findings | boolean | no | Whether security issues were found. Default: false | + +**Output:** JSON object with `labels_applied` and `labels_removed`. + +### Label Mapping + +| Risk Level | Label Applied | +|------------|---------------| +| low | `review/approved` | +| medium | `review/needs-changes` | +| high | `review/needs-changes` | +| critical | `review/security-concern` | + +If `has_security_findings` is true, `review/security-concern` is always applied regardless of risk level. + +Previous review labels (with the configured prefix) are removed before applying new ones. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "label the PR", "apply review labels", "mark PR as approved" +- After completing a review, user asks to update PR status + +## Tool: review_github_auto_merge + +Merge a pull request after verifying safety conditions. Requires explicit opt-in via `REVIEW_AUTO_MERGE=true`. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| repo | string | yes | Repository in `owner/repo` format | +| pr_number | integer | yes | Pull request number | +| merge_method | string | no | Merge method: `merge`, `squash`, `rebase`. Default: `squash` | +| require_clean_review | boolean | no | Require no error-severity findings before merging. Default: true | +| review_result | object | no | Review result from `code_review_diff` to verify cleanliness | + +**Output:** JSON object with `merged`, `sha`, `message`, or `blocked_reason` if merge was prevented. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "merge the PR", "auto-merge if clean" +- Requests to "merge after review passes" + +### Safety Guards + +This tool enforces multiple safety layers: + +1. **Env var gate:** `REVIEW_AUTO_MERGE` must be explicitly set to `true`. If unset or any other value, merge is blocked with a clear message. +2. **Clean review required:** By default, the PR must have no `error`-severity findings. Override with `require_clean_review: false`. +3. **CI checks:** All required status checks must pass (enforced by GitHub's branch protection). +4. **No dismissed reviews:** The tool never dismisses existing review requests or approvals. +5. **Merge conflicts:** GitHub API rejects merges with conflicts — the tool surfaces this clearly. + +## Workflow: Full PR Review Cycle + +A typical end-to-end review workflow using all three code-review skills: + +1. **List PRs** → `review_github_list_prs` to find PRs needing review +2. **Run review** → `code_review_diff` with the PR URL to generate findings +3. **Post comments** → `review_github_post_comments` to post findings as inline comments +4. **Apply labels** → `review_github_apply_labels` to set status labels +5. **Auto-merge** (optional) → `review_github_auto_merge` if review is clean and opt-in is enabled + +## Safety Constraints + +This skill MUST: + +- Never merge without `REVIEW_AUTO_MERGE=true` explicitly set +- Never dismiss existing reviews or review requests +- Never force-push or modify PR branch contents +- Never delete branches (leave that to GitHub's auto-delete setting) +- Never bypass branch protection rules +- Never approve its own PRs (if the token belongs to the PR author) +- Post comments as the authenticated user — never impersonate +- All GitHub operations go through `gh` CLI — `http_request` and `web_search` tools are denied +- Rate-limit awareness: back off on 403/429 responses from GitHub API + +## Autonomous Compatibility + +This skill is designed to work in automated pipelines: + +- All inputs and outputs are structured JSON +- Error states return JSON with `error` field and descriptive messages +- Idempotent: re-running label application or comment posting produces consistent results +- Labels are prefix-scoped to avoid conflicting with other label systems diff --git a/forge-skills/local/embedded/code-review-standards/SKILL.md b/forge-skills/local/embedded/code-review-standards/SKILL.md new file mode 100644 index 0000000..4ec419f --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/SKILL.md @@ -0,0 +1,208 @@ +--- +name: code-review-standards +category: dev +tags: + - code-review + - standards + - configuration + - linting + - quality +description: Discover and apply organization coding standards from .forge-review/ configuration +metadata: + forge: + requires: + bins: [] + env: + required: [] + one_of: [] + optional: + - FORGE_REVIEW_STANDARDS_DIR + egress_domains: [] +--- + +# Code Review Standards + +Teaches the agent to discover and apply organization-specific coding standards stored in a `.forge-review/` directory in the target repository. This skill provides configuration templates and standard rule files that teams can customize. + +## Overview + +The `.forge-review/` directory is a convention for storing code review configuration alongside the codebase. When present, the `code-review` skill automatically loads these standards and applies them during reviews. + +This skill: +- Documents the `.forge-review/` directory structure and file formats +- Provides templates for bootstrapping review standards in new repositories +- Teaches the agent how to discover and interpret standards files + +## Directory Structure + +``` +.forge-review/ +├── config.yaml # Review configuration +├── .forge-review-ignore # Files/patterns to skip during review +└── standards/ # Coding standards (one file per topic) + ├── general.md # General coding rules + ├── security.md # Security rules + ├── go.md # Go-specific rules + ├── python.md # Python-specific rules + ├── typescript.md # TypeScript rules + └── testing.md # Testing rules +``` + +## Tool: review_standards_init + +Initialize a `.forge-review/` directory in the current repository with default templates. + +This is a binary-backed tool that uses filesystem operations (mkdir, write) to scaffold the standards directory. The agent reads templates bundled with this skill and writes them to the target repository. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| target_dir | string | no | Target directory (default: current working directory) | +| languages | array | no | Language-specific standards to include: `go`, `python`, `typescript`. Default: all | +| overwrite | boolean | no | Overwrite existing files. Default: false | + +**Output:** List of created files and a summary of the initialized configuration. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "set up code review", "initialize review standards", "configure review" +- Questions about `.forge-review/` directory structure +- Requests to create or customize coding standards + +## Tool: review_standards_check + +Check whether the current repository has a `.forge-review/` configuration and report its status. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| target_dir | string | no | Directory to check (default: current working directory) | + +**Output:** JSON object with configuration status, found files, and any validation issues. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Questions about whether review standards are configured +- Requests to validate review configuration +- Troubleshooting review behavior + +## Configuration Reference + +### config.yaml + +```yaml +# .forge-review/config.yaml +version: "1" + +# Default review settings +review: + # Focus areas to always include (bugs, security, style, performance, maintainability) + default_focus: + - bugs + - security + + # Severity threshold: only report findings at or above this level + # Options: nitpick, info, warning, error + min_severity: info + + # Maximum findings to report per review + max_findings: 50 + +# Language-specific settings +languages: + go: + standards_file: standards/go.md + enabled: true + python: + standards_file: standards/python.md + enabled: true + typescript: + standards_file: standards/typescript.md + enabled: true + +# File patterns to always skip (supplements .forge-review-ignore) +skip_patterns: + - "vendor/**" + - "node_modules/**" + - "*.generated.go" + - "*.min.js" + - "**/*.pb.go" +``` + +### .forge-review-ignore + +Gitignore-style patterns for files that should be skipped during review: + +``` +# Dependencies +vendor/ +node_modules/ + +# Generated code +*.generated.go +*.pb.go +*_gen.go + +# Build artifacts +dist/ +build/ +*.min.js +*.min.css + +# Test fixtures +testdata/ +**/fixtures/** + +# Documentation +*.md +LICENSE +``` + +### Standards Files + +Standards files are markdown documents with rules organized by category. Each rule should be clear and actionable. The `code-review` skill injects these into the LLM prompt during review. + +Format: + +```markdown +# Category Name + +## Rule Title + +Description of the rule and why it matters. + +**Good:** +\`\`\`go +// example of correct code +\`\`\` + +**Bad:** +\`\`\`go +// example of incorrect code +\`\`\` +``` + +## Templates + +This skill bundles template files in the `templates/` directory. Use `review_standards_init` to copy them into a target repository, or browse them directly for reference. + +Available templates: +- `templates/.forge-review/config.yaml` — Default configuration +- `templates/.forge-review/.forge-review-ignore` — Default ignore patterns +- `templates/.forge-review/standards/general.md` — General coding standards +- `templates/.forge-review/standards/security.md` — Security review rules +- `templates/.forge-review/standards/go.md` — Go-specific standards +- `templates/.forge-review/standards/python.md` — Python-specific standards +- `templates/.forge-review/standards/typescript.md` — TypeScript standards +- `templates/.forge-review/standards/testing.md` — Testing standards + +## Safety Constraints + +- This skill only reads and writes configuration files in `.forge-review/` +- It never modifies source code +- Standards files are only used as additional context for LLM-based review; they do not block merges or fail CI on their own +- Overwrite protection is on by default (`overwrite: false`) diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/.forge-review-ignore b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/.forge-review-ignore new file mode 100644 index 0000000..5e3a4d8 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/.forge-review-ignore @@ -0,0 +1,42 @@ +# .forge-review-ignore +# Gitignore-style patterns for files to skip during code review. +# One pattern per line. Blank lines and lines starting with # are ignored. + +# Dependencies +vendor/ +node_modules/ +go.sum + +# Generated code +*.generated.go +*.pb.go +*_gen.go +*_generated.ts + +# Build artifacts +dist/ +build/ +out/ +*.min.js +*.min.css +*.bundle.js + +# Test fixtures and snapshots +testdata/ +**/fixtures/** +**/__snapshots__/** + +# Documentation (review separately if needed) +*.md +LICENSE +CHANGELOG + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/config.yaml b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/config.yaml new file mode 100644 index 0000000..5b9b087 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/config.yaml @@ -0,0 +1,44 @@ +# .forge-review/config.yaml +# Organization code review configuration for Forge AI code review skill. +# See: https://github.com/initializ/forge (code-review-standards skill) + +version: "1" + +# Default review settings +review: + # Focus areas to always include in reviews. + # Options: bugs, security, style, performance, maintainability + default_focus: + - bugs + - security + + # Minimum severity threshold: only report findings at or above this level. + # Options: nitpick, info, warning, error + min_severity: info + + # Maximum findings to report per review (prevents overwhelming output). + max_findings: 50 + +# Language-specific settings. +# Enable/disable standards files per language. +languages: + go: + standards_file: standards/go.md + enabled: true + python: + standards_file: standards/python.md + enabled: true + typescript: + standards_file: standards/typescript.md + enabled: true + +# File patterns to always skip during review. +# Supplements .forge-review-ignore for programmatic exclusions. +skip_patterns: + - "vendor/**" + - "node_modules/**" + - "*.generated.go" + - "*.min.js" + - "**/*.pb.go" + - "dist/**" + - "build/**" diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/general.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/general.md new file mode 100644 index 0000000..96ca420 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/general.md @@ -0,0 +1,60 @@ +# General Coding Standards + +## Error Handling + +All errors must be handled explicitly. Never silently ignore errors. + +**Good:** +```go +result, err := doSomething() +if err != nil { + return fmt.Errorf("doSomething failed: %w", err) +} +``` + +**Bad:** +```go +result, _ := doSomething() +``` + +## Naming Conventions + +- Use descriptive, intention-revealing names +- Avoid single-letter variables except for loop indices and short-lived lambdas +- Boolean variables should read as assertions: `isReady`, `hasPermission`, `canRetry` + +## Function Length + +Functions should do one thing. If a function exceeds ~40 lines, consider splitting it. Long functions are harder to test, debug, and review. + +## Magic Numbers + +Replace magic numbers and strings with named constants. The name should explain the value's purpose. + +**Good:** +```python +MAX_RETRY_ATTEMPTS = 3 +TIMEOUT_SECONDS = 30 +``` + +**Bad:** +```python +if attempts > 3: + time.sleep(30) +``` + +## Comments + +- Write comments that explain **why**, not **what** +- Keep comments up to date — stale comments are worse than no comments +- Use TODO/FIXME with a tracking reference: `// TODO(#123): migrate to new API` + +## Dead Code + +Remove dead code. Do not comment out code "for later." Version control preserves history. + +## Dependencies + +- Pin dependency versions in production code +- Justify new dependencies — prefer standard library when adequate +- Audit transitive dependencies for known vulnerabilities diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/go.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/go.md new file mode 100644 index 0000000..d205958 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/go.md @@ -0,0 +1,83 @@ +# Go Standards + +## Error Handling + +- Always check returned errors. Use `errcheck` or `golangci-lint` to enforce. +- Wrap errors with context: `fmt.Errorf("operation failed: %w", err)` +- Use `errors.Is()` and `errors.As()` for error inspection, not string matching. +- Define sentinel errors as package-level variables: `var ErrNotFound = errors.New("not found")` + +## Goroutines & Concurrency + +- Every goroutine must have a clear shutdown path. Use `context.Context` for cancellation. +- Protect shared state with `sync.Mutex` or use channels. Document the choice. +- Never use `go func()` without considering error propagation and lifecycle. +- Use `sync.WaitGroup` or `errgroup.Group` to wait for goroutine completion. + +**Good:** +```go +g, ctx := errgroup.WithContext(ctx) +g.Go(func() error { + return processItem(ctx, item) +}) +if err := g.Wait(); err != nil { + return err +} +``` + +## Resource Management + +- Use `defer` for cleanup immediately after acquiring a resource. +- Close `io.Closer` values: files, HTTP response bodies, database connections. +- Use `context.WithTimeout` for operations that may hang. + +**Good:** +```go +resp, err := http.Get(url) +if err != nil { + return err +} +defer resp.Body.Close() +``` + +## Interfaces + +- Define interfaces where they are used, not where they are implemented. +- Keep interfaces small — prefer 1-2 methods. +- Accept interfaces, return concrete types. + +## Struct Initialization + +- Use named fields in struct literals. Positional initialization breaks with field additions. + +**Good:** +```go +srv := &Server{ + Addr: ":8080", + Handler: mux, +} +``` + +**Bad:** +```go +srv := &Server{":8080", mux} +``` + +## Testing + +- Use table-driven tests for functions with multiple input/output cases. +- Use `t.Helper()` in test helper functions for better error reporting. +- Use `testify/assert` or `testify/require` for readable assertions. +- Name test cases descriptively: `"empty input returns error"`. + +## Package Design + +- Avoid package-level mutable state (global variables). +- Avoid `init()` functions — they complicate testing and initialization order. +- Package names should be short, lowercase, singular: `user`, not `users` or `userService`. + +## Formatting & Linting + +- Run `gofmt` / `goimports` on all code. +- Use `golangci-lint` with the project's configuration. +- No lint suppressions without a comment explaining why. diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/python.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/python.md new file mode 100644 index 0000000..f946385 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/python.md @@ -0,0 +1,78 @@ +# Python Standards + +## Type Hints + +- Use type hints for all public function signatures. +- Use `from __future__ import annotations` for forward references. +- Run `mypy` or `pyright` in CI. + +**Good:** +```python +def fetch_user(user_id: int, *, include_deleted: bool = False) -> User | None: + ... +``` + +## Error Handling + +- Catch specific exceptions, never bare `except:`. +- Use custom exception classes for domain errors. +- Use context managers (`with`) for resource cleanup. + +**Good:** +```python +try: + result = api.fetch(endpoint) +except requests.HTTPError as e: + logger.error("API request failed: %s", e) + raise +``` + +**Bad:** +```python +try: + result = api.fetch(endpoint) +except: + pass +``` + +## Imports + +- Group imports: stdlib, third-party, local — separated by blank lines. +- Use absolute imports. Avoid wildcard imports (`from module import *`). +- Sort imports with `isort` or `ruff`. + +## String Formatting + +- Use f-strings for readability: `f"Hello, {name}"`. +- Use `%s` formatting in logging calls (lazy evaluation): `logger.info("User %s logged in", user_id)`. + +## Data Classes & Models + +- Use `dataclasses` or `pydantic` for structured data — avoid raw dicts for domain objects. +- Use `Enum` for fixed sets of values. + +## Security + +- Never use `eval()`, `exec()`, or `__import__()` with user input. +- Use `subprocess.run()` with argument lists, never `shell=True` with untrusted input. +- Use `secrets` module for token generation, not `random`. +- Sanitize file paths: validate against path traversal (`../`). + +## Testing + +- Use `pytest` with descriptive test names: `test_fetch_user_returns_none_for_missing_id`. +- Use `pytest.fixture` for shared setup. +- Use `pytest.raises` for expected exceptions. +- Mock external dependencies at the boundary, not deep internals. + +## Performance + +- Use generators for large sequences: `(x for x in items)` over `[x for x in items]`. +- Profile before optimizing — use `cProfile` or `py-spy`. +- Use `functools.lru_cache` for expensive pure functions. + +## Formatting + +- Follow PEP 8. Use `ruff` or `black` for auto-formatting. +- Maximum line length: 88 (black default) or 120 (project configured). +- Use trailing commas in multi-line collections. diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/security.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/security.md new file mode 100644 index 0000000..87e7b7a --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/security.md @@ -0,0 +1,74 @@ +# Security Standards + +## Input Validation + +All external input must be validated before use. This includes HTTP request parameters, CLI arguments, file content, environment variables, and database results used in further queries. + +- Validate type, length, range, and format +- Use allowlists over denylists where possible +- Reject invalid input early with clear error messages + +## Injection Prevention + +### SQL Injection +Always use parameterized queries or prepared statements. Never concatenate user input into SQL strings. + +**Good:** +```go +db.Query("SELECT * FROM users WHERE id = $1", userID) +``` + +**Bad:** +```go +db.Query("SELECT * FROM users WHERE id = " + userID) +``` + +### Command Injection +Never pass unsanitized input to shell commands. Use argument arrays instead of string interpolation. + +**Good:** +```python +subprocess.run(["git", "log", "--oneline", branch_name], check=True) +``` + +**Bad:** +```python +os.system(f"git log --oneline {branch_name}") +``` + +### XSS (Cross-Site Scripting) +All user-supplied content rendered in HTML must be escaped. Use framework-provided auto-escaping. Avoid `innerHTML`, `dangerouslySetInnerHTML`, or template `|safe` filters with untrusted data. + +## Authentication & Authorization + +- Never store plaintext passwords — use bcrypt, scrypt, or argon2 +- Validate authorization on every request, not just at the UI level +- Use constant-time comparison for secrets and tokens +- Implement rate limiting on authentication endpoints + +## Secrets Management + +- Never commit secrets, API keys, or credentials to version control +- Use environment variables or a secrets manager (Vault, AWS Secrets Manager) +- Rotate secrets regularly and after any suspected exposure +- Add secret patterns to `.gitignore` and pre-commit hooks + +## Cryptography + +- Use well-known libraries — never implement custom crypto +- Use TLS 1.2+ for all network communication +- Use authenticated encryption (AES-GCM, ChaCha20-Poly1305) +- Generate random values with cryptographically secure PRNGs + +## Logging & Error Messages + +- Never log sensitive data: passwords, tokens, PII, credit card numbers +- Sanitize error messages shown to users — do not expose stack traces or internal paths +- Log security-relevant events: login attempts, permission changes, data access + +## Dependency Security + +- Monitor dependencies for known CVEs (Dependabot, Snyk, Trivy) +- Pin versions in production — avoid floating ranges +- Review changelogs before upgrading major versions +- Minimize dependency surface area diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/testing.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/testing.md new file mode 100644 index 0000000..4dfee5a --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/testing.md @@ -0,0 +1,61 @@ +# Testing Standards + +## Test Structure + +- Follow the Arrange-Act-Assert (AAA) pattern in every test. +- One logical assertion per test. Multiple assertions are acceptable if they verify the same behavior. +- Name tests descriptively: `test_expired_token_returns_401`, not `test_auth_3`. + +## Test Independence + +- Tests must not depend on execution order. +- Each test sets up its own state and tears it down. +- Never share mutable state between tests. +- Use unique identifiers (UUIDs, timestamps) to avoid collisions in parallel runs. + +## Test Coverage + +- New code should include tests. PRs that add features without tests require justification. +- Cover the happy path, edge cases, and error paths. +- Critical paths (auth, payment, data mutation) require explicit test coverage. +- Coverage percentage is a guideline, not a target — 80% coverage with good tests beats 100% with trivial ones. + +## Mocking + +- Mock at boundaries: HTTP clients, databases, filesystems, time, randomness. +- Never mock the system under test. +- Prefer fakes (in-memory implementations) over mocks for complex dependencies. +- Verify mock expectations — unused mocks indicate dead test code. + +## Test Data + +- Use factory functions or builders for test data — avoid copy-pasting large literals. +- Keep test data minimal: only set fields relevant to the test. +- Use realistic but not real data. Never use production data in tests. + +## Integration Tests + +- Integration tests verify component interactions — they should use real dependencies where practical. +- Tag or separate integration tests so they can run independently: `go test -tags=integration`, `pytest -m integration`. +- Use containers (testcontainers, docker-compose) for database and service dependencies. +- Clean up external state after tests. + +## Flaky Tests + +- Flaky tests must be fixed immediately or quarantined. +- Common causes: timing dependencies, shared state, network calls, uncontrolled randomness. +- Use deterministic seeds for random values in tests. +- Use fake timers instead of `sleep` in tests. + +## Performance Tests + +- Benchmark critical paths. Use `testing.B` (Go), `pytest-benchmark`, or `vitest bench`. +- Set performance budgets for critical operations. +- Run benchmarks in CI to detect regressions. + +## Security Tests + +- Test authentication: valid token, expired token, missing token, wrong role. +- Test authorization: verify users cannot access other users' resources. +- Test input validation: SQL injection, XSS payloads, path traversal, oversized input. +- Test rate limiting and account lockout. diff --git a/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/typescript.md b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/typescript.md new file mode 100644 index 0000000..907d565 --- /dev/null +++ b/forge-skills/local/embedded/code-review-standards/templates/.forge-review/standards/typescript.md @@ -0,0 +1,84 @@ +# TypeScript Standards + +## Type Safety + +- Enable `strict: true` in `tsconfig.json`. +- Avoid `any` — use `unknown` when the type is truly unknown, then narrow with type guards. +- Use discriminated unions over type assertions. +- Prefer `interface` for object shapes, `type` for unions and intersections. + +**Good:** +```typescript +type Result = + | { status: "success"; data: User } + | { status: "error"; message: string }; + +function handleResult(result: Result) { + if (result.status === "success") { + console.log(result.data.name); + } +} +``` + +**Bad:** +```typescript +function handleResult(result: any) { + console.log(result.data.name); // unsafe +} +``` + +## Null Handling + +- Use optional chaining (`?.`) and nullish coalescing (`??`) over manual checks. +- Enable `strictNullChecks`. Handle `null` and `undefined` explicitly. +- Avoid non-null assertions (`!`) unless the invariant is documented. + +## Error Handling + +- Use typed error classes or result types over throwing raw strings. +- Always handle promise rejections — unhandled rejections crash Node.js. +- Use `try/catch` at async boundaries, not around every await. + +**Good:** +```typescript +try { + const user = await fetchUser(id); + return { ok: true, data: user }; +} catch (err) { + logger.error("Failed to fetch user", { userId: id, error: err }); + return { ok: false, error: "User fetch failed" }; +} +``` + +## Async Patterns + +- Use `async/await` over raw promises and callbacks. +- Use `Promise.all()` for independent concurrent operations. +- Set timeouts on external calls — use `AbortController` for fetch. + +## Immutability + +- Use `const` by default. Only use `let` when reassignment is necessary. +- Use `readonly` for properties that should not change after construction. +- Prefer spread/map/filter over mutating arrays and objects. + +## React (if applicable) + +- Use functional components with hooks. +- Memoize expensive computations with `useMemo`/`useCallback` where profiling shows benefit. +- Lift state up or use context — avoid prop drilling beyond 2-3 levels. +- Extract custom hooks for reusable logic. + +## Testing + +- Use `describe`/`it` blocks with descriptive names. +- Test behavior, not implementation details. +- Mock external boundaries (API clients, databases), not internal functions. +- Use `testing-library` patterns: query by role/label, not CSS selectors. + +## Formatting & Linting + +- Use ESLint with the project's shared config. +- Use Prettier for formatting (or Biome). +- No `eslint-disable` without a comment explaining the exception. +- Enable `no-unused-vars`, `no-explicit-any`, `no-floating-promises`. diff --git a/forge-skills/local/embedded/code-review/SKILL.md b/forge-skills/local/embedded/code-review/SKILL.md new file mode 100644 index 0000000..cecaf23 --- /dev/null +++ b/forge-skills/local/embedded/code-review/SKILL.md @@ -0,0 +1,224 @@ +--- +name: code-review +category: dev +tags: + - code-review + - diff + - pull-request + - quality + - security +description: AI-powered code review for diffs and individual files using LLM analysis +metadata: + forge: + requires: + bins: + - curl + - jq + - git + env: + required: [] + one_of: + - ANTHROPIC_API_KEY + - OPENAI_API_KEY + optional: + - REVIEW_MODEL + - REVIEW_MAX_DIFF_BYTES + - GH_TOKEN + - FORGE_REVIEW_STANDARDS_DIR + egress_domains: + - api.anthropic.com + - api.openai.com + timeout_hint: 120 +--- + +# Code Review Skill + +AI-powered code review that analyzes diffs and individual files for bugs, security issues, style violations, and improvement opportunities. Supports both local git diffs and GitHub pull requests. + +## Authentication + +Set at least one LLM API key: + +```bash +# Option A: Anthropic (preferred) +export ANTHROPIC_API_KEY="sk-ant-..." + +# Option B: OpenAI +export OPENAI_API_KEY="sk-..." + +# Optional: override the model (defaults: claude-sonnet-4-20250514 / gpt-4o) +export REVIEW_MODEL="claude-sonnet-4-20250514" +``` + +For GitHub PR review, also set: + +```bash +export GH_TOKEN="ghp_..." +``` + +The token needs **read-only** access: + +| Scope (classic PAT) | Fine-grained permission | Why | +|----------------------|------------------------|-----| +| `repo` (or `public_repo` for public repos) | Contents: Read | Fetch PR diff and file contents | + +This skill only reads diffs — it never posts comments, applies labels, or merges. For write operations, see the `code-review-github` skill. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| ANTHROPIC_API_KEY | one-of | Anthropic API key (preferred) | +| OPENAI_API_KEY | one-of | OpenAI API key (fallback) | +| REVIEW_MODEL | no | Override LLM model name | +| REVIEW_MAX_DIFF_BYTES | no | Max diff size before truncation (default: 100000) | +| GH_TOKEN | no | GitHub token for PR diffs — read-only access is sufficient | +| FORGE_REVIEW_STANDARDS_DIR | no | Path to `.forge-review/standards/` directory for custom rules | + +## Tool: code_review_diff + +Review a code diff for bugs, security issues, and improvements. Accepts either a GitHub PR URL or a local git base ref. + +**Default behavior:** When the user says "review my local changes" or "review changes in " WITHOUT specifying a base branch, default to `base_ref: "HEAD"`. This reviews uncommitted and untracked files — the user's work-in-progress. Do NOT ask the user for a base ref in this case. Only use `main`/`develop` as `base_ref` when the user explicitly says "against main" or "against develop". + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| pr_url | string | no | GitHub PR URL (e.g., `https://github.com/owner/repo/pull/123`) | +| base_ref | string | no | Git ref to compare from. Default: `HEAD` (uncommitted changes). Use `main` only if user says "against main" | +| repo_path | string | no | Absolute path to the local git repository. Required when using `base_ref` | +| focus | string | no | Review focus: `bugs`, `security`, `style`, `all`. Default: `all` | +| extra_context | string | no | Additional context or instructions for the reviewer | + +One of `pr_url` or `base_ref` is required. When using `base_ref`, `repo_path` must point to the user's local repository (scripts run in the agent's directory, not the user's project). + +**Output:** JSON object with structured review findings. + +### Examples + +User says → tool input mapping: + +| User request | Tool input | +|-------------|------------| +| "Review code changes against main in ~/myproject" | `{"base_ref": "main", "repo_path": "~/myproject"}` | +| "Review my changes since develop in /opt/app" | `{"base_ref": "develop", "repo_path": "/opt/app"}` | +| "Review the last 3 commits in ~/myproject for security issues" | `{"base_ref": "HEAD~3", "repo_path": "~/myproject", "focus": "security"}` | +| "Review changes on the feature/auth branch in ~/myproject" | `{"base_ref": "main", "repo_path": "~/myproject"}` | +| "Review my local changes in ~/myproject" | `{"base_ref": "HEAD", "repo_path": "~/myproject"}` | +| "Review my uncommitted changes in ~/myproject" | `{"base_ref": "HEAD", "repo_path": "~/myproject"}` | +| "Review this PR https://github.com/org/repo/pull/42" | `{"pr_url": "https://github.com/org/repo/pull/42"}` | +| "Security audit on PR 99 in org/repo" | `{"pr_url": "https://github.com/org/repo/pull/99", "focus": "security"}` | + +**Important:** +- Pass `repo_path` exactly as the user provides it. The script handles `~` expansion internally — do NOT try to resolve `~` yourself (you do not know the user's home directory). For example, if the user says `~/myproject`, pass `"repo_path": "~/myproject"`. +- For local reviews, `repo_path` is required because the script runs in the agent's working directory, not the user's project directory. +- `base_ref` is the starting point to compare FROM. It is NOT the branch being reviewed — it is the base that changes are measured against. The diff includes both committed and uncommitted (working tree) changes relative to `base_ref`. +- `base_ref` can be a branch name (`main`, `develop`), a commit SHA, or a relative ref (`HEAD~3`). The script uses `git merge-base` to find the fork point, so only changes on the current branch are reviewed. +- When a branch has many commits (e.g., merged PRs), use `HEAD~N` to narrow the review scope to the most recent N commits. For example, `HEAD~5` reviews only the last 5 commits. +- When the user says "review changes on branch X" or "review branch X", they mean: review the changes that branch X introduces. Use `main` (or the repo's default branch) as `base_ref` — NOT the branch name itself. +- When the user says "review my uncommitted changes" or "review my local changes", use `HEAD` as `base_ref` to show only unstaged/staged changes. + +### Pre-Flight Scope Check (IMPORTANT) + +Before calling `code_review_diff` with a `base_ref`, the agent MUST run `cli_execute` to check the diff scope: + +```bash +cd && git log --oneline ..HEAD | head -20 +``` + +This shows how many commits will be reviewed. If there are more than ~5 commits: +1. Tell the user: "There are N commits between `` and HEAD. This includes: [list first few commit subjects]." +2. Ask: "Would you like me to review all N commits, or narrow the scope? For example, I can review just the last 3 commits with `HEAD~3`." +3. Only call `code_review_diff` after the user confirms the scope. + +This prevents reviewing unrelated changes from older commits or merged PRs on long-lived branches. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests mentioning "review", "diff", "PR", "pull request", "changes" +- A directory path or repository path in the user's message +- GitHub PR URLs pasted in conversation +- Requests to check code quality, find bugs, or audit changes + +### Response Format + +```json +{ + "summary": "Brief overall assessment", + "risk_level": "low|medium|high|critical", + "findings": [ + { + "file": "path/to/file.go", + "line": 42, + "severity": "error|warning|info|nitpick", + "category": "bug|security|style|performance|maintainability", + "title": "Short finding title", + "description": "Detailed explanation of the issue", + "suggestion": "Suggested fix or improvement" + } + ], + "stats": { + "files_reviewed": 5, + "total_findings": 3, + "by_severity": {"error": 1, "warning": 1, "nitpick": 1} + } +} +``` + +### Tips + +- Use `focus: security` for security-focused audits +- Use `base_ref: main` to review all uncommitted changes against main +- Large diffs are automatically truncated at `REVIEW_MAX_DIFF_BYTES` (default 100KB) +- Set `FORGE_REVIEW_STANDARDS_DIR` to apply org-specific coding standards + +## Tool: code_review_file + +Deep review of a single file with full context (not just diff). Useful for thorough analysis of critical files. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| file_path | string | yes | Path to the file to review (relative to repo root, or absolute path) | +| repo_path | string | no | Absolute path to the local repository. Required for local file review (scripts run in the agent's directory, not the user's project) | +| pr_url | string | no | If set, fetches the file from the PR head branch | +| focus | string | no | Review focus: `bugs`, `security`, `style`, `all`. Default: `all` | +| extra_context | string | no | Additional context or instructions for the reviewer | + +**Output:** Same JSON structure as `code_review_diff`. + +### Examples + +User says → tool input mapping: + +| User request | Tool input | +|-------------|------------| +| "Review the file src/server.go in ~/myproject" | `{"file_path": "src/server.go", "repo_path": "~/myproject"}` | +| "Security audit on cmd/main.go in /opt/app" | `{"file_path": "cmd/main.go", "repo_path": "/opt/app", "focus": "security"}` | +| "Review auth.py from PR #42 in org/repo" | `{"file_path": "auth.py", "pr_url": "https://github.com/org/repo/pull/42"}` | + +**Important:** For local file review, `repo_path` is required. `file_path` is relative to the repo root. Resolve `~` to the absolute home directory path. + +### Detection Heuristics + +The agent selects this tool when it detects: +- Requests to "review this file", "audit file", "check file for issues" +- A specific file path mentioned with a repository/directory path +- Single-file deep review requests (as opposed to diff-level review) + +### Tips + +- Use this for critical files that need thorough review beyond just changes +- Combine with `code_review_diff` for comprehensive PR review: diff-level first, then deep-dive on flagged files +- Works with any text file: source code, configs, scripts, IaC templates + +## Safety Constraints + +- Never executes code from the diff or reviewed files +- API keys are passed via environment variables, never logged or included in output +- Diff content is sent to the configured LLM API for analysis (respects `egress_domains`) +- Large diffs are truncated, not streamed, to prevent excessive API costs +- No filesystem modifications: read-only analysis only diff --git a/forge-skills/local/embedded/code-review/scripts/code-review-diff.sh b/forge-skills/local/embedded/code-review/scripts/code-review-diff.sh new file mode 100755 index 0000000..f9d4383 --- /dev/null +++ b/forge-skills/local/embedded/code-review/scripts/code-review-diff.sh @@ -0,0 +1,376 @@ +#!/usr/bin/env bash +# code-review-diff.sh — AI-powered code review for diffs (GitHub PR or local git). +# Usage: ./code-review-diff.sh '{"pr_url": "...", "base_ref": "main", "focus": "all"}' +# +# Requires: curl, jq, git +# Env: ANTHROPIC_API_KEY or OPENAI_API_KEY (at least one) +# Optional: REVIEW_MODEL, REVIEW_MAX_DIFF_BYTES, GH_TOKEN, FORGE_REVIEW_STANDARDS_DIR +set -euo pipefail + +# --- Read and validate input first (agent can fix these) --- +INPUT="${1:-}" +if [ -z "$INPUT" ]; then + echo '{"error": "usage: code-review-diff.sh {\"pr_url\": \"...\"} or {\"base_ref\": \"main\", \"repo_path\": \"/path/to/repo\"}"}' >&2 + exit 1 +fi + +if ! echo "$INPUT" | jq empty 2>/dev/null; then + echo '{"error": "invalid JSON input"}' >&2 + exit 1 +fi + +# --- Parse fields --- +PR_URL=$(echo "$INPUT" | jq -r '.pr_url // empty') +BASE_REF=$(echo "$INPUT" | jq -r '.base_ref // empty') +REPO_PATH=$(echo "$INPUT" | jq -r '.repo_path // empty') +FOCUS=$(echo "$INPUT" | jq -r '.focus // "all"') +EXTRA_CONTEXT=$(echo "$INPUT" | jq -r '.extra_context // empty') + +if [ -z "$PR_URL" ] && [ -z "$BASE_REF" ]; then + echo '{"error": "one of pr_url or base_ref is required. Use pr_url for GitHub PRs or base_ref + repo_path for local diffs"}' >&2 + exit 1 +fi + +# --- Validate environment (requires deployment config) --- +if [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then + echo '{"error": "Either ANTHROPIC_API_KEY or OPENAI_API_KEY must be set"}' >&2 + exit 1 +fi + +# --- Change to repo directory for local operations --- +if [ -n "$REPO_PATH" ]; then + # Expand ~ to actual home directory (shell doesn't expand ~ inside variables) + REPO_PATH="${REPO_PATH/#\~/$HOME}" + + if [ ! -d "$REPO_PATH" ]; then + echo "{\"error\": \"repo_path directory does not exist: $REPO_PATH\"}" >&2 + exit 1 + fi + cd "$REPO_PATH" +elif [ -n "$BASE_REF" ]; then + echo '{"error": "repo_path is required for local diff review (scripts run in the agent directory, not the user project)"}' >&2 + exit 1 +fi + +# --- Configuration --- +MAX_DIFF_BYTES="${REVIEW_MAX_DIFF_BYTES:-100000}" + +# --- Obtain diff --- +DIFF_CONTENT="" + +if [ -n "$PR_URL" ]; then + # Extract owner/repo and PR number from various GitHub URL formats + # Supports: https://github.com/owner/repo/pull/123 + # https://github.com/owner/repo/pull/123/files + # https://github.com/owner/repo/pull/123/commits + PR_PATH=$(echo "$PR_URL" | sed -E 's|^https?://github\.com/||' | sed -E 's|/(files|commits|checks)$||') + OWNER_REPO=$(echo "$PR_PATH" | sed -E 's|/pull/[0-9]+.*$||') + PR_NUMBER=$(echo "$PR_PATH" | sed -E 's|.*/pull/([0-9]+).*$|\1|') + + if [ -z "$OWNER_REPO" ] || [ -z "$PR_NUMBER" ]; then + echo '{"error": "could not parse GitHub PR URL. Expected format: https://github.com/owner/repo/pull/123"}' >&2 + exit 1 + fi + + # Use gh CLI if available and GH_TOKEN is set, otherwise use API directly + if command -v gh >/dev/null 2>&1 && [ -n "${GH_TOKEN:-}" ]; then + DIFF_CONTENT=$(gh pr diff "$PR_NUMBER" --repo "$OWNER_REPO" 2>/dev/null) || true + fi + + # Fallback to curl if gh didn't work + if [ -z "$DIFF_CONTENT" ]; then + DIFF_HEADERS=(-H "Accept: application/vnd.github.v3.diff") + if [ -n "${GH_TOKEN:-}" ]; then + DIFF_HEADERS+=(-H "Authorization: Bearer ${GH_TOKEN}") + fi + DIFF_CONTENT=$(curl -s --max-time 30 \ + "${DIFF_HEADERS[@]}" \ + "https://api.github.com/repos/${OWNER_REPO}/pulls/${PR_NUMBER}" 2>/dev/null) || true + fi + + if [ -z "$DIFF_CONTENT" ]; then + echo '{"error": "failed to fetch PR diff. Check GH_TOKEN and PR URL."}' >&2 + exit 1 + fi +else + # Local git diff (repo_path validated and cd'd above) + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo '{"error": "not inside a git repository. Check repo_path or provide pr_url for remote review."}' >&2 + exit 1 + fi + + # Write diff to temp file to avoid storing multi-MB diffs in bash variables. + # Large diffs (e.g. 3MB+) cause SIGPIPE and memory issues in shell pipelines. + DIFF_TMPFILE=$(mktemp) + + # Use merge-base to find the fork point when diffing against a branch name, + # so we only see changes on the current branch — not changes merged to the + # base branch after this branch was created. + # NOTE: Omit "HEAD" as the second argument so the diff includes uncommitted + # working tree changes, not just committed ones. + MERGE_BASE=$(git merge-base "$BASE_REF" HEAD 2>/dev/null) || true + + DIFF_REF="${MERGE_BASE:-$BASE_REF}" + + # Capture tracked changes (committed + uncommitted modifications) + git diff "$DIFF_REF" > "$DIFF_TMPFILE" 2>/dev/null || true + + # Also include UNTRACKED files — git diff ignores them entirely, but they are + # often the user's primary new work (new files not yet git-added). + UNTRACKED_FILES=$(git ls-files --others --exclude-standard 2>/dev/null) || true + if [ -n "$UNTRACKED_FILES" ]; then + while IFS= read -r ufile; do + if [ -f "$ufile" ]; then + # Generate a diff-like entry for the untracked file + git diff --no-index /dev/null "$ufile" >> "$DIFF_TMPFILE" 2>/dev/null || true + fi + done <<< "$UNTRACKED_FILES" + fi + + if [ ! -s "$DIFF_TMPFILE" ]; then + echo '{"error": "no diff found for base_ref: '"$BASE_REF"'. No tracked changes or untracked files detected."}' >&2 + exit 1 + fi +fi + +# --- Handle large diffs --- +# When the diff exceeds MAX_DIFF_BYTES, do NOT silently truncate (the first N bytes +# are just alphabetically-early files, missing the user's actual changes). Instead, +# return the full file list (--stat) so the agent can ask the user to narrow scope. +DIFF_SIZE=0 +if [ -n "${DIFF_TMPFILE:-}" ]; then + DIFF_SIZE=$(wc -c < "$DIFF_TMPFILE" | tr -d ' ') +else + DIFF_SIZE=${#DIFF_CONTENT} +fi + +if [ "$DIFF_SIZE" -gt "$MAX_DIFF_BYTES" ]; then + # Generate diff stat for the agent to present to the user + if [ -n "${DIFF_TMPFILE:-}" ]; then + # Local diff: stat from git (tracked) + untracked file list + DIFF_STAT=$(git diff "${DIFF_REF:-$BASE_REF}" --stat 2>/dev/null || true) + UNTRACKED_LIST=$(git ls-files --others --exclude-standard 2>/dev/null || true) + if [ -n "$UNTRACKED_LIST" ]; then + DIFF_STAT="${DIFF_STAT} +Untracked (new) files: +${UNTRACKED_LIST}" + fi + TRACKED_COUNT=$(git diff "${DIFF_REF:-$BASE_REF}" --name-only 2>/dev/null | wc -l | tr -d ' ') + UNTRACKED_COUNT=$(echo "$UNTRACKED_LIST" | grep -c . 2>/dev/null || echo 0) + FILE_COUNT=$((TRACKED_COUNT + UNTRACKED_COUNT)) + else + # PR diff: extract file list from the diff content + DIFF_STAT=$(echo "$DIFF_CONTENT" | grep '^diff --git' | sed 's|diff --git a/||; s| b/.*||' || true) + FILE_COUNT=$(echo "$DIFF_STAT" | wc -l | tr -d ' ') + fi + + jq -n \ + --arg diff_size "$DIFF_SIZE" \ + --arg max_size "$MAX_DIFF_BYTES" \ + --arg file_count "$FILE_COUNT" \ + --arg diff_stat "$DIFF_STAT" \ + --arg base_ref "$BASE_REF" \ + '{ + "error": "diff_too_large", + "message": "The diff is too large to review in one pass. Ask the user to narrow the scope.", + "diff_bytes": ($diff_size | tonumber), + "max_bytes": ($max_size | tonumber), + "files_changed": ($file_count | tonumber), + "base_ref": $base_ref, + "diff_stat": $diff_stat, + "suggestions": [ + "Review only uncommitted changes: use base_ref=HEAD", + "Review the last N commits: use base_ref=HEAD~N", + "Review specific files: ask the user which files to focus on", + "Increase the limit: set REVIEW_MAX_DIFF_BYTES env var" + ] + }' >&2 + exit 1 +fi + +# Read diff content into variable (only when within size limit) +TRUNCATED="false" +if [ -n "${DIFF_TMPFILE:-}" ]; then + DIFF_CONTENT=$(cat "$DIFF_TMPFILE") +fi + +# --- Load custom standards if available --- +STANDARDS_CONTEXT="" +STANDARDS_DIR="${FORGE_REVIEW_STANDARDS_DIR:-}" +if [ -z "$STANDARDS_DIR" ] && [ -d ".forge-review/standards" ]; then + STANDARDS_DIR=".forge-review/standards" +fi + +if [ -n "$STANDARDS_DIR" ] && [ -d "$STANDARDS_DIR" ]; then + for std_file in "$STANDARDS_DIR"/*.md; do + if [ -f "$std_file" ]; then + STANDARDS_CONTEXT="${STANDARDS_CONTEXT} +--- $(basename "$std_file") --- +$(cat "$std_file") +" + fi + done +fi + +# --- Build review prompt --- +SYSTEM_PROMPT="You are an expert code reviewer. Analyze the provided diff and produce a structured JSON review. + +Focus area: ${FOCUS} + +Your review must be returned as a single JSON object with this exact schema: +{ + \"summary\": \"Brief overall assessment of the changes\", + \"risk_level\": \"low|medium|high|critical\", + \"findings\": [ + { + \"file\": \"path/to/file\", + \"line\": , + \"severity\": \"error|warning|info|nitpick\", + \"category\": \"bug|security|style|performance|maintainability\", + \"title\": \"Short title\", + \"description\": \"Detailed explanation\", + \"suggestion\": \"Suggested fix or improvement\" + } + ], + \"stats\": { + \"files_reviewed\": , + \"total_findings\": , + \"by_severity\": {\"error\": 0, \"warning\": 0, \"info\": 0, \"nitpick\": 0} + } +} + +Rules: +- Return ONLY valid JSON, no markdown fences, no extra text +- Be specific: include file paths and line numbers where possible +- Prioritize actionable findings over style nitpicks +- For security focus, emphasize OWASP top 10, injection, auth issues +- For bug focus, emphasize logic errors, null/nil dereferences, race conditions +- If the diff is truncated, note this in the summary" + +if [ -n "$STANDARDS_CONTEXT" ]; then + SYSTEM_PROMPT="${SYSTEM_PROMPT} + +Apply these organization coding standards in your review: +${STANDARDS_CONTEXT}" +fi + +USER_PROMPT="Review this diff" +if [ -n "$EXTRA_CONTEXT" ]; then + USER_PROMPT="${USER_PROMPT}. Additional context: ${EXTRA_CONTEXT}" +fi +USER_PROMPT="${USER_PROMPT}: + +${DIFF_CONTENT}" +if [ "$TRUNCATED" = "true" ]; then + USER_PROMPT="${USER_PROMPT} + +[NOTE: Diff was truncated at ${MAX_DIFF_BYTES} bytes. Some files may be missing from the review.]" +fi + +# --- Route to LLM API --- +if [ -n "${ANTHROPIC_API_KEY:-}" ]; then + # Anthropic Claude API + MODEL="${REVIEW_MODEL:-claude-sonnet-4-20250514}" + + # Use jq --rawfile for safe JSON encoding of prompts + TEMP_SYSTEM=$(mktemp) + TEMP_USER=$(mktemp) + trap 'rm -f "$TEMP_SYSTEM" "$TEMP_USER" "${DIFF_TMPFILE:-}"' EXIT + printf '%s' "$SYSTEM_PROMPT" > "$TEMP_SYSTEM" + printf '%s' "$USER_PROMPT" > "$TEMP_USER" + + API_PAYLOAD=$(jq -n \ + --arg model "$MODEL" \ + --rawfile system "$TEMP_SYSTEM" \ + --rawfile user "$TEMP_USER" \ + '{ + model: $model, + max_tokens: 4096, + system: $system, + messages: [ + {role: "user", content: $user} + ] + }') + + RESPONSE=$(curl -s --max-time 90 \ + -w "\n%{http_code}" \ + -X POST "https://api.anthropic.com/v1/messages" \ + -H "Content-Type: application/json" \ + -H "x-api-key: ${ANTHROPIC_API_KEY}" \ + -H "anthropic-version: 2023-06-01" \ + -d "$API_PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"Anthropic API returned status $HTTP_CODE\", \"details\": $(echo "$BODY" | jq -c '.' 2>/dev/null || echo '""')}" >&2 + exit 1 + fi + + # Extract text content from Anthropic response + REVIEW_TEXT=$(echo "$BODY" | jq -r '.content[0].text // empty') + +elif [ -n "${OPENAI_API_KEY:-}" ]; then + # OpenAI API + MODEL="${REVIEW_MODEL:-gpt-4o}" + + TEMP_SYSTEM=$(mktemp) + TEMP_USER=$(mktemp) + trap 'rm -f "$TEMP_SYSTEM" "$TEMP_USER" "${DIFF_TMPFILE:-}"' EXIT + printf '%s' "$SYSTEM_PROMPT" > "$TEMP_SYSTEM" + printf '%s' "$USER_PROMPT" > "$TEMP_USER" + + API_PAYLOAD=$(jq -n \ + --arg model "$MODEL" \ + --rawfile system "$TEMP_SYSTEM" \ + --rawfile user "$TEMP_USER" \ + '{ + model: $model, + max_tokens: 4096, + messages: [ + {role: "system", content: $system}, + {role: "user", content: $user} + ] + }') + + RESPONSE=$(curl -s --max-time 90 \ + -w "\n%{http_code}" \ + -X POST "https://api.openai.com/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${OPENAI_API_KEY}" \ + -d "$API_PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"OpenAI API returned status $HTTP_CODE\", \"details\": $(echo "$BODY" | jq -c '.' 2>/dev/null || echo '""')}" >&2 + exit 1 + fi + + # Extract text content from OpenAI response + REVIEW_TEXT=$(echo "$BODY" | jq -r '.choices[0].message.content // empty') +fi + +if [ -z "${REVIEW_TEXT:-}" ]; then + echo '{"error": "LLM returned empty response"}' >&2 + exit 1 +fi + +# --- Parse and validate JSON output --- +# Strip markdown fences if the LLM wrapped the response +REVIEW_TEXT=$(echo "$REVIEW_TEXT" | sed -E 's/^```(json)?//; s/```$//' | sed '/^$/d') + +if echo "$REVIEW_TEXT" | jq empty 2>/dev/null; then + echo "$REVIEW_TEXT" | jq . +else + # If LLM didn't return valid JSON, wrap it + jq -n --arg text "$REVIEW_TEXT" '{ + "summary": $text, + "risk_level": "unknown", + "findings": [], + "stats": {"files_reviewed": 0, "total_findings": 0, "by_severity": {}}, + "parse_warning": "LLM response was not valid JSON; raw text returned as summary" + }' +fi diff --git a/forge-skills/local/embedded/code-review/scripts/code-review-file.sh b/forge-skills/local/embedded/code-review/scripts/code-review-file.sh new file mode 100755 index 0000000..c962152 --- /dev/null +++ b/forge-skills/local/embedded/code-review/scripts/code-review-file.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# code-review-file.sh — AI-powered deep review of a single file. +# Usage: ./code-review-file.sh '{"file_path": "src/main.go", "focus": "security"}' +# +# Requires: curl, jq +# Env: ANTHROPIC_API_KEY or OPENAI_API_KEY (at least one) +# Optional: REVIEW_MODEL, GH_TOKEN, FORGE_REVIEW_STANDARDS_DIR +set -euo pipefail + +# --- Read and validate input first (agent can fix these) --- +INPUT="${1:-}" +if [ -z "$INPUT" ]; then + echo '{"error": "usage: code-review-file.sh {\"file_path\": \"path/to/file\", \"repo_path\": \"/path/to/repo\"}"}' >&2 + exit 1 +fi + +if ! echo "$INPUT" | jq empty 2>/dev/null; then + echo '{"error": "invalid JSON input"}' >&2 + exit 1 +fi + +# --- Parse fields --- +FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // empty') +REPO_PATH=$(echo "$INPUT" | jq -r '.repo_path // empty') +PR_URL=$(echo "$INPUT" | jq -r '.pr_url // empty') +FOCUS=$(echo "$INPUT" | jq -r '.focus // "all"') +EXTRA_CONTEXT=$(echo "$INPUT" | jq -r '.extra_context // empty') + +if [ -z "$FILE_PATH" ]; then + echo '{"error": "file_path field is required"}' >&2 + exit 1 +fi + +# --- Validate environment (requires deployment config) --- +if [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then + echo '{"error": "Either ANTHROPIC_API_KEY or OPENAI_API_KEY must be set"}' >&2 + exit 1 +fi + +# --- Change to repo directory for local operations --- +if [ -n "$REPO_PATH" ]; then + # Expand ~ to actual home directory (shell doesn't expand ~ inside variables) + REPO_PATH="${REPO_PATH/#\~/$HOME}" + + if [ ! -d "$REPO_PATH" ]; then + echo "{\"error\": \"repo_path directory does not exist: $REPO_PATH\"}" >&2 + exit 1 + fi + cd "$REPO_PATH" +elif [ -z "$PR_URL" ]; then + echo '{"error": "repo_path is required for local file review (scripts run in the agent directory, not the user project)"}' >&2 + exit 1 +fi + +# --- Obtain file content --- +FILE_CONTENT="" + +if [ -n "$PR_URL" ]; then + # Extract owner/repo and PR number from GitHub URL + PR_PATH=$(echo "$PR_URL" | sed -E 's|^https?://github\.com/||' | sed -E 's|/(files|commits|checks)$||') + OWNER_REPO=$(echo "$PR_PATH" | sed -E 's|/pull/[0-9]+.*$||') + PR_NUMBER=$(echo "$PR_PATH" | sed -E 's|.*/pull/([0-9]+).*$|\1|') + + if [ -z "$OWNER_REPO" ] || [ -z "$PR_NUMBER" ]; then + echo '{"error": "could not parse GitHub PR URL"}' >&2 + exit 1 + fi + + # Get the head branch SHA from the PR + if command -v gh >/dev/null 2>&1 && [ -n "${GH_TOKEN:-}" ]; then + HEAD_SHA=$(gh pr view "$PR_NUMBER" --repo "$OWNER_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null) || true + fi + + if [ -z "${HEAD_SHA:-}" ] && [ -n "${GH_TOKEN:-}" ]; then + HEAD_SHA=$(curl -s --max-time 15 \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${OWNER_REPO}/pulls/${PR_NUMBER}" \ + | jq -r '.head.sha // empty' 2>/dev/null) || true + fi + + if [ -z "${HEAD_SHA:-}" ]; then + echo '{"error": "could not determine PR head SHA. Check GH_TOKEN."}' >&2 + exit 1 + fi + + # Fetch file content at PR head + API_HEADERS=(-H "Accept: application/vnd.github.v3.raw") + if [ -n "${GH_TOKEN:-}" ]; then + API_HEADERS+=(-H "Authorization: Bearer ${GH_TOKEN}") + fi + + FILE_CONTENT=$(curl -s --max-time 30 \ + "${API_HEADERS[@]}" \ + "https://api.github.com/repos/${OWNER_REPO}/contents/${FILE_PATH}?ref=${HEAD_SHA}" 2>/dev/null) || true + + if [ -z "$FILE_CONTENT" ]; then + echo "{\"error\": \"could not fetch file '${FILE_PATH}' from PR head\"}" >&2 + exit 1 + fi +else + # Local file + if [ ! -f "$FILE_PATH" ]; then + echo "{\"error\": \"file not found: ${FILE_PATH}\"}" >&2 + exit 1 + fi + + FILE_CONTENT=$(cat "$FILE_PATH") +fi + +if [ -z "$FILE_CONTENT" ]; then + echo '{"error": "file is empty or could not be read"}' >&2 + exit 1 +fi + +# --- Load custom standards if available --- +STANDARDS_CONTEXT="" +STANDARDS_DIR="${FORGE_REVIEW_STANDARDS_DIR:-}" +if [ -z "$STANDARDS_DIR" ] && [ -d ".forge-review/standards" ]; then + STANDARDS_DIR=".forge-review/standards" +fi + +if [ -n "$STANDARDS_DIR" ] && [ -d "$STANDARDS_DIR" ]; then + for std_file in "$STANDARDS_DIR"/*.md; do + if [ -f "$std_file" ]; then + STANDARDS_CONTEXT="${STANDARDS_CONTEXT} +--- $(basename "$std_file") --- +$(cat "$std_file") +" + fi + done +fi + +# --- Build review prompt --- +SYSTEM_PROMPT="You are an expert code reviewer performing a deep review of a single file. Analyze the entire file for bugs, security issues, and improvements. + +Focus area: ${FOCUS} +File: ${FILE_PATH} + +Your review must be returned as a single JSON object with this exact schema: +{ + \"summary\": \"Brief overall assessment of the file\", + \"risk_level\": \"low|medium|high|critical\", + \"findings\": [ + { + \"file\": \"${FILE_PATH}\", + \"line\": , + \"severity\": \"error|warning|info|nitpick\", + \"category\": \"bug|security|style|performance|maintainability\", + \"title\": \"Short title\", + \"description\": \"Detailed explanation\", + \"suggestion\": \"Suggested fix or improvement\" + } + ], + \"stats\": { + \"files_reviewed\": 1, + \"total_findings\": , + \"by_severity\": {\"error\": 0, \"warning\": 0, \"info\": 0, \"nitpick\": 0} + } +} + +Rules: +- Return ONLY valid JSON, no markdown fences, no extra text +- Review the ENTIRE file, not just a diff — consider overall structure, error handling, edge cases +- Be specific: include line numbers for each finding +- Prioritize actionable findings over style nitpicks +- For security focus, emphasize OWASP top 10, injection, auth, secrets exposure +- For bug focus, emphasize logic errors, null/nil dereferences, race conditions, resource leaks" + +if [ -n "$STANDARDS_CONTEXT" ]; then + SYSTEM_PROMPT="${SYSTEM_PROMPT} + +Apply these organization coding standards in your review: +${STANDARDS_CONTEXT}" +fi + +USER_PROMPT="Review this file (${FILE_PATH})" +if [ -n "$EXTRA_CONTEXT" ]; then + USER_PROMPT="${USER_PROMPT}. Additional context: ${EXTRA_CONTEXT}" +fi +USER_PROMPT="${USER_PROMPT}: + +${FILE_CONTENT}" + +# --- Route to LLM API --- +if [ -n "${ANTHROPIC_API_KEY:-}" ]; then + MODEL="${REVIEW_MODEL:-claude-sonnet-4-20250514}" + + TEMP_SYSTEM=$(mktemp) + TEMP_USER=$(mktemp) + trap 'rm -f "$TEMP_SYSTEM" "$TEMP_USER"' EXIT + printf '%s' "$SYSTEM_PROMPT" > "$TEMP_SYSTEM" + printf '%s' "$USER_PROMPT" > "$TEMP_USER" + + API_PAYLOAD=$(jq -n \ + --arg model "$MODEL" \ + --rawfile system "$TEMP_SYSTEM" \ + --rawfile user "$TEMP_USER" \ + '{ + model: $model, + max_tokens: 4096, + system: $system, + messages: [ + {role: "user", content: $user} + ] + }') + + RESPONSE=$(curl -s --max-time 90 \ + -w "\n%{http_code}" \ + -X POST "https://api.anthropic.com/v1/messages" \ + -H "Content-Type: application/json" \ + -H "x-api-key: ${ANTHROPIC_API_KEY}" \ + -H "anthropic-version: 2023-06-01" \ + -d "$API_PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"Anthropic API returned status $HTTP_CODE\", \"details\": $(echo "$BODY" | jq -c '.' 2>/dev/null || echo '""')}" >&2 + exit 1 + fi + + REVIEW_TEXT=$(echo "$BODY" | jq -r '.content[0].text // empty') + +elif [ -n "${OPENAI_API_KEY:-}" ]; then + MODEL="${REVIEW_MODEL:-gpt-4o}" + + TEMP_SYSTEM=$(mktemp) + TEMP_USER=$(mktemp) + trap 'rm -f "$TEMP_SYSTEM" "$TEMP_USER"' EXIT + printf '%s' "$SYSTEM_PROMPT" > "$TEMP_SYSTEM" + printf '%s' "$USER_PROMPT" > "$TEMP_USER" + + API_PAYLOAD=$(jq -n \ + --arg model "$MODEL" \ + --rawfile system "$TEMP_SYSTEM" \ + --rawfile user "$TEMP_USER" \ + '{ + model: $model, + max_tokens: 4096, + messages: [ + {role: "system", content: $system}, + {role: "user", content: $user} + ] + }') + + RESPONSE=$(curl -s --max-time 90 \ + -w "\n%{http_code}" \ + -X POST "https://api.openai.com/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${OPENAI_API_KEY}" \ + -d "$API_PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"OpenAI API returned status $HTTP_CODE\", \"details\": $(echo "$BODY" | jq -c '.' 2>/dev/null || echo '""')}" >&2 + exit 1 + fi + + REVIEW_TEXT=$(echo "$BODY" | jq -r '.choices[0].message.content // empty') +fi + +if [ -z "${REVIEW_TEXT:-}" ]; then + echo '{"error": "LLM returned empty response"}' >&2 + exit 1 +fi + +# --- Parse and validate JSON output --- +REVIEW_TEXT=$(echo "$REVIEW_TEXT" | sed -E 's/^```(json)?//; s/```$//' | sed '/^$/d') + +if echo "$REVIEW_TEXT" | jq empty 2>/dev/null; then + echo "$REVIEW_TEXT" | jq . +else + jq -n --arg text "$REVIEW_TEXT" '{ + "summary": $text, + "risk_level": "unknown", + "findings": [], + "stats": {"files_reviewed": 1, "total_findings": 0, "by_severity": {}}, + "parse_warning": "LLM response was not valid JSON; raw text returned as summary" + }' +fi diff --git a/forge-skills/local/registry_embedded_test.go b/forge-skills/local/registry_embedded_test.go index cd13362..e49497a 100644 --- a/forge-skills/local/registry_embedded_test.go +++ b/forge-skills/local/registry_embedded_test.go @@ -16,12 +16,12 @@ func TestEmbeddedRegistry_DiscoverAll(t *testing.T) { t.Fatalf("List error: %v", err) } - if len(skills) != 5 { + if len(skills) != 8 { names := make([]string, len(skills)) for i, s := range skills { names[i] = s.Name } - t.Fatalf("expected 5 skills, got %d: %v", len(skills), names) + t.Fatalf("expected 8 skills, got %d: %v", len(skills), names) } // Verify all expected skills are present @@ -31,11 +31,14 @@ func TestEmbeddedRegistry_DiscoverAll(t *testing.T) { hasBins bool hasEgress bool }{ - "github": {displayName: "Github", hasEnv: true, hasBins: true, hasEgress: true}, - "weather": {displayName: "Weather", hasEnv: false, hasBins: true, hasEgress: true}, - "tavily-search": {displayName: "Tavily Search", hasEnv: true, hasBins: true, hasEgress: true}, - "tavily-research": {displayName: "Tavily Research", hasEnv: true, hasBins: true, hasEgress: true}, - "k8s-incident-triage": {displayName: "K8s Incident Triage", hasEnv: false, hasBins: true, hasEgress: false}, + "github": {displayName: "Github", hasEnv: true, hasBins: true, hasEgress: true}, + "weather": {displayName: "Weather", hasEnv: false, hasBins: true, hasEgress: true}, + "tavily-search": {displayName: "Tavily Search", hasEnv: true, hasBins: true, hasEgress: true}, + "tavily-research": {displayName: "Tavily Research", hasEnv: true, hasBins: true, hasEgress: true}, + "k8s-incident-triage": {displayName: "K8s Incident Triage", hasEnv: false, hasBins: true, hasEgress: false}, + "code-review": {displayName: "Code Review", hasEnv: false, hasBins: true, hasEgress: true}, + "code-review-standards": {displayName: "Code Review Standards", hasEnv: false, hasBins: false, hasEgress: false}, + "code-review-github": {displayName: "Code Review Github", hasEnv: true, hasBins: true, hasEgress: true}, } for _, s := range skills {