diff --git a/.github/prompts/create-agentic-workflow.prompt.md b/.github/prompts/create-agentic-workflow.prompt.md index 3001898091..769d4a1ed6 100644 --- a/.github/prompts/create-agentic-workflow.prompt.md +++ b/.github/prompts/create-agentic-workflow.prompt.md @@ -45,6 +45,11 @@ Analyze the user's response and map it to agentic workflows. Ask clarifying ques - ⚠️ If you think the task requires **network access beyond localhost**, explicitly ask about configuring the top-level `network:` allowlist (ecosystems like `node`, `python`, `playwright`, or specific domains). - 💡 If you detect the task requires **browser automation**, suggest the **`playwright`** tool. +**Scheduling Best Practices:** + - 📅 When creating a **daily scheduled workflow**, analyze existing workflows to find an unused hour. Check `.github/workflows/*.md` for existing `schedule:` triggers and suggest an available hour. + - 🚫 **Avoid weekend scheduling**: For daily workflows, use `cron: "0 * * 1-5"` to run only on weekdays (Monday-Friday) instead of `* * *` which includes weekends. + - Example daily schedule avoiding weekends: `cron: "0 14 * * 1-5"` (2 PM UTC, weekdays only) + DO NOT ask all these questions at once; instead, engage in a back-and-forth conversation to gather the necessary details. 4. **Tools & MCP Servers** diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 494d36a953..9db2df0e4b 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -1071,24 +1071,133 @@ jobs: main().catch(error => { core.setFailed(error instanceof Error ? error.message : String(error)); }); - - name: Validate COPILOT_CLI_TOKEN secret + - name: Validate ANTHROPIC_API_KEY secret run: | - if [ -z "$COPILOT_CLI_TOKEN" ]; then - echo "Error: COPILOT_CLI_TOKEN secret is not set" - echo "The GitHub Copilot CLI engine requires the COPILOT_CLI_TOKEN secret to be configured." + if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "Error: ANTHROPIC_API_KEY secret is not set" + echo "The Claude Code engine requires the ANTHROPIC_API_KEY secret to be configured." echo "Please configure this secret in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" + echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#anthropic-claude-code" exit 1 fi - echo "COPILOT_CLI_TOKEN secret is configured" + echo "ANTHROPIC_API_KEY secret is configured" env: - COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.347 + - name: Install Claude Code CLI + run: npm install -g @anthropic-ai/claude-code@2.0.24 + - name: Generate Claude Settings + run: | + mkdir -p /tmp/gh-aw/.claude + cat > /tmp/gh-aw/.claude/settings.json << 'EOF' + { + "hooks": { + "PreToolUse": [ + { + "matcher": "WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/network_permissions.py" + } + ] + } + ] + } + } + EOF + - name: Generate Network Permissions Hook + run: | + mkdir -p .claude/hooks + cat > .claude/hooks/network_permissions.py << 'EOF' + #!/usr/bin/env python3 + """ + Network permissions validator for Claude Code engine. + Generated by gh-aw from engine network permissions configuration. + """ + + import json + import sys + import urllib.parse + import re + + # Domain allow-list (populated during generation) + # JSON array safely embedded as Python list literal + ALLOWED_DOMAINS = ["crl3.digicert.com","crl4.digicert.com","ocsp.digicert.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","crl.geotrust.com","ocsp.geotrust.com","crl.thawte.com","ocsp.thawte.com","crl.verisign.com","ocsp.verisign.com","crl.globalsign.com","ocsp.globalsign.com","crls.ssl.com","ocsp.ssl.com","crl.identrust.com","ocsp.identrust.com","crl.sectigo.com","ocsp.sectigo.com","crl.usertrust.com","ocsp.usertrust.com","s.symcb.com","s.symcd.com","json-schema.org","json.schemastore.org","archive.ubuntu.com","security.ubuntu.com","ppa.launchpad.net","keyserver.ubuntu.com","azure.archive.ubuntu.com","api.snapcraft.io","packagecloud.io","packages.cloud.google.com","packages.microsoft.com"] + + def extract_domain(url_or_query): + """Extract domain from URL or search query.""" + if not url_or_query: + return None + + if url_or_query.startswith(('http://', 'https://')): + return urllib.parse.urlparse(url_or_query).netloc.lower() + + # Check for domain patterns in search queries + match = re.search(r'site:([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', url_or_query) + if match: + return match.group(1).lower() + + return None + + def is_domain_allowed(domain): + """Check if domain is allowed.""" + if not domain: + # If no domain detected, allow only if not under deny-all policy + return bool(ALLOWED_DOMAINS) # False if empty list (deny-all), True if has domains + + # Empty allowed domains means deny all + if not ALLOWED_DOMAINS: + return False + + for pattern in ALLOWED_DOMAINS: + regex = pattern.replace('.', r'\.').replace('*', '.*') + if re.match(f'^{regex}$', domain): + return True + return False + + # Main logic + try: + data = json.load(sys.stdin) + tool_name = data.get('tool_name', '') + tool_input = data.get('tool_input', {}) + + if tool_name not in ['WebFetch', 'WebSearch']: + sys.exit(0) # Allow other tools + + target = tool_input.get('url') or tool_input.get('query', '') + domain = extract_domain(target) + + # For WebSearch, apply domain restrictions consistently + # If no domain detected in search query, check if restrictions are in place + if tool_name == 'WebSearch' and not domain: + # Since this hook is only generated when network permissions are configured, + # empty ALLOWED_DOMAINS means deny-all policy + if not ALLOWED_DOMAINS: # Empty list means deny all + print(f"Network access blocked: deny-all policy in effect", file=sys.stderr) + print(f"No domains are allowed for WebSearch", file=sys.stderr) + sys.exit(2) # Block under deny-all policy + else: + print(f"Network access blocked for web-search: no specific domain detected", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block general searches when domain allowlist is configured + + if not is_domain_allowed(domain): + print(f"Network access blocked for domain: {domain}", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block with feedback to Claude + + sys.exit(0) # Allow + + except Exception as e: + print(f"Network validation error: {e}", file=sys.stderr) + sys.exit(2) # Block on errors + + EOF + chmod +x .claude/hooks/network_permissions.py - name: Downloading container images run: | set -e @@ -2116,18 +2225,12 @@ jobs: - name: Setup MCPs run: | mkdir -p /tmp/gh-aw/mcp-config - mkdir -p /home/runner/.copilot - cat > /home/runner/.copilot/mcp-config.json << EOF + cat > /tmp/gh-aw/mcp-config/mcp-servers.json << EOF { "mcpServers": { "arxiv": { - "type": "local", + "type": "stdio", "command": "docker", - "tools": [ - "search_arxiv", - "get_paper_details", - "get_paper_pdf" - ], "args": [ "run", "--rm", @@ -2136,12 +2239,8 @@ jobs: ] }, "context7": { - "type": "local", + "type": "stdio", "command": "docker", - "tools": [ - "get-library-docs", - "resolve-library-id" - ], "args": [ "run", "--rm", @@ -2156,15 +2255,9 @@ jobs: }, "deepwiki": { "type": "http", - "url": "https://mcp.deepwiki.com/sse", - "tools": [ - "read_wiki_structure", - "read_wiki_contents", - "ask_question" - ] + "url": "https://mcp.deepwiki.com/sse" }, "github": { - "type": "local", "command": "docker", "args": [ "run", @@ -2178,61 +2271,39 @@ jobs: "GITHUB_TOOLSETS=default", "ghcr.io/github/github-mcp-server:v0.19.0" ], - "tools": ["*"], "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_PERSONAL_ACCESS_TOKEN}" + "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" } }, "markitdown": { - "type": "local", - "command": "markitdown-mcp", - "tools": [ - "*" - ] + "type": "stdio", + "command": "markitdown-mcp" }, "microsoftdocs": { "type": "http", - "url": "https://learn.microsoft.com/api/mcp", - "tools": [ - "*" - ] + "url": "https://learn.microsoft.com/api/mcp" }, "safe_outputs": { - "type": "local", "command": "node", "args": ["/tmp/gh-aw/safe-outputs/mcp-server.cjs"], - "tools": ["*"], "env": { - "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", - "GH_AW_SAFE_OUTPUTS_CONFIG": "\${GH_AW_SAFE_OUTPUTS_CONFIG}", - "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", - "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", - "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}" + "GH_AW_SAFE_OUTPUTS": "${{ env.GH_AW_SAFE_OUTPUTS }}", + "GH_AW_SAFE_OUTPUTS_CONFIG": ${{ toJSON(env.GH_AW_SAFE_OUTPUTS_CONFIG) }}, + "GH_AW_ASSETS_BRANCH": "${{ env.GH_AW_ASSETS_BRANCH }}", + "GH_AW_ASSETS_MAX_SIZE_KB": "${{ env.GH_AW_ASSETS_MAX_SIZE_KB }}", + "GH_AW_ASSETS_ALLOWED_EXTS": "${{ env.GH_AW_ASSETS_ALLOWED_EXTS }}" } }, "tavily": { "type": "http", "url": "https://mcp.tavily.com/mcp/", "headers": { - "Authorization": "Bearer \${TAVILY_API_KEY}" - }, - "tools": [ - "*" - ], - "env": { - "TAVILY_API_KEY": "\${TAVILY_API_KEY}" + "Authorization": "Bearer ${{ secrets.TAVILY_API_KEY }}" } } } } EOF - echo "-------START MCP CONFIG-----------" - cat /home/runner/.copilot/mcp-config.json - echo "-------END MCP CONFIG-----------" - echo "-------/home/runner/.copilot-----------" - find /home/runner/.copilot - echo "HOME: $HOME" - echo "GITHUB_COPILOT_CLI_MODE: $GITHUB_COPILOT_CLI_MODE" - name: Create prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -2708,7 +2779,7 @@ jobs: if-no-files-found: warn - name: Capture agent version run: | - VERSION_OUTPUT=$(copilot --version 2>&1 || echo "unknown") + VERSION_OUTPUT=$(claude --version 2>&1 || echo "unknown") # Extract semantic version pattern (e.g., 1.2.3, v1.2.3-beta) CLEAN_VERSION=$(echo "$VERSION_OUTPUT" | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?' | head -n1 || echo "unknown") echo "AGENT_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV @@ -2720,8 +2791,8 @@ jobs: const fs = require('fs'); const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", + engine_id: "claude", + engine_name: "Claude Code", model: "", version: "", agent_version: process.env.AGENT_VERSION || "", @@ -2753,64 +2824,129 @@ jobs: name: aw_info.json path: /tmp/gh-aw/aw_info.json if-no-files-found: warn - - name: Execute GitHub Copilot CLI + - name: Execute Claude Code CLI id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool arxiv - # --allow-tool arxiv(get_paper_details) - # --allow-tool arxiv(get_paper_pdf) - # --allow-tool arxiv(search_arxiv) - # --allow-tool context7 - # --allow-tool context7(get-library-docs) - # --allow-tool context7(resolve-library-id) - # --allow-tool deepwiki - # --allow-tool deepwiki(ask_question) - # --allow-tool deepwiki(read_wiki_contents) - # --allow-tool deepwiki(read_wiki_structure) - # --allow-tool github - # --allow-tool markitdown - # --allow-tool markitdown(*) - # --allow-tool microsoftdocs - # --allow-tool microsoftdocs(*) - # --allow-tool safe_outputs - # --allow-tool shell(/tmp/gh-aw/jqschema.sh) - # --allow-tool shell(cat) - # --allow-tool shell(date) - # --allow-tool shell(echo) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq *) - # --allow-tool shell(ls) - # --allow-tool shell(pwd) - # --allow-tool shell(sort) - # --allow-tool shell(tail) - # --allow-tool shell(uniq) - # --allow-tool shell(wc) - # --allow-tool shell(yq) - # --allow-tool tavily - # --allow-tool tavily(*) - # --allow-tool write + # Allowed tools (sorted): + # - Bash(/tmp/gh-aw/jqschema.sh) + # - Bash(cat) + # - Bash(date) + # - Bash(echo) + # - Bash(grep) + # - Bash(head) + # - Bash(jq *) + # - Bash(ls) + # - Bash(pwd) + # - Bash(sort) + # - Bash(tail) + # - Bash(uniq) + # - Bash(wc) + # - Bash(yq) + # - BashOutput + # - Edit + # - Edit(/tmp/gh-aw/cache-memory/*) + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - MultiEdit + # - MultiEdit(/tmp/gh-aw/cache-memory/*) + # - NotebookEdit + # - NotebookRead + # - Read + # - Read(/tmp/gh-aw/cache-memory/*) + # - Task + # - TodoWrite + # - Write + # - Write(/tmp/gh-aw/cache-memory/*) + # - mcp__arxiv__get_paper_details + # - mcp__arxiv__get_paper_pdf + # - mcp__arxiv__search_arxiv + # - mcp__context7__get-library-docs + # - mcp__context7__resolve-library-id + # - mcp__deepwiki__ask_question + # - mcp__deepwiki__read_wiki_contents + # - mcp__deepwiki__read_wiki_structure + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_issue + # - mcp__github__get_issue_comments + # - mcp__github__get_job_logs + # - mcp__github__get_label + # - mcp__github__get_latest_release + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_review_comments + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_release_by_tag + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issue_types + # - mcp__github__list_issues + # - mcp__github__list_label + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_releases + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_starred_repositories + # - mcp__github__list_sub_issues + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__pull_request_read + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + # - mcp__markitdown + # - mcp__microsoftdocs + # - mcp__tavily timeout-minutes: 10 run: | set -o pipefail - COPILOT_CLI_INSTRUCTION=$(cat /tmp/gh-aw/aw-prompts/prompt.txt) - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/cache-memory/ - mkdir -p /tmp/gh-aw/.copilot/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --allow-tool arxiv --allow-tool 'arxiv(get_paper_details)' --allow-tool 'arxiv(get_paper_pdf)' --allow-tool 'arxiv(search_arxiv)' --allow-tool context7 --allow-tool 'context7(get-library-docs)' --allow-tool 'context7(resolve-library-id)' --allow-tool deepwiki --allow-tool 'deepwiki(ask_question)' --allow-tool 'deepwiki(read_wiki_contents)' --allow-tool 'deepwiki(read_wiki_structure)' --allow-tool github --allow-tool markitdown --allow-tool 'markitdown(*)' --allow-tool microsoftdocs --allow-tool 'microsoftdocs(*)' --allow-tool safe_outputs --allow-tool 'shell(/tmp/gh-aw/jqschema.sh)' --allow-tool 'shell(cat)' --allow-tool 'shell(date)' --allow-tool 'shell(echo)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq *)' --allow-tool 'shell(ls)' --allow-tool 'shell(pwd)' --allow-tool 'shell(sort)' --allow-tool 'shell(tail)' --allow-tool 'shell(uniq)' --allow-tool 'shell(wc)' --allow-tool 'shell(yq)' --allow-tool tavily --allow-tool 'tavily(*)' --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log + # Execute Claude Code CLI with prompt from file + claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat),Bash(date),Bash(echo),Bash(grep),Bash(head),Bash(jq *),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__arxiv__get_paper_details,mcp__arxiv__get_paper_pdf,mcp__arxiv__search_arxiv,mcp__context7__get-library-docs,mcp__context7__resolve-library-id,mcp__deepwiki__ask_question,mcp__deepwiki__read_wiki_contents,mcp__deepwiki__read_wiki_structure,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__markitdown,mcp__microsoftdocs,mcp__tavily" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + DISABLE_TELEMETRY: "1" + DISABLE_ERROR_REPORTING: "1" + DISABLE_BUG_COMMAND: "1" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json + MCP_TIMEOUT: "120000" + MCP_TOOL_TIMEOUT: "60000" + BASH_DEFAULT_TIMEOUT_MS: "60000" + BASH_MAX_TIMEOUT_MS: "60000" GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"missing_tool\":{}}" - GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - XDG_CONFIG_HOME: /home/runner + - name: Clean up network proxy hook files + if: always() + run: | + rm -rf .claude/hooks/network_permissions.py || true + rm -rf .claude/hooks || true + rm -rf .claude || true - name: Upload Safe Outputs if: always() uses: actions/upload-artifact@v4 @@ -3587,130 +3723,6 @@ jobs: name: agent_output.json path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - - name: Redact secrets in logs - if: always() - uses: actions/github-script@v8 - with: - script: | - const fs = require("fs"); - const path = require("path"); - function findFiles(dir, extensions) { - const results = []; - try { - if (!fs.existsSync(dir)) { - return results; - } - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findFiles(fullPath, extensions)); - } else if (entry.isFile()) { - const ext = path.extname(entry.name).toLowerCase(); - if (extensions.includes(ext)) { - results.push(fullPath); - } - } - } - } catch (error) { - core.warning(`Failed to scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); - } - return results; - } - function redactSecrets(content, secretValues) { - let redactionCount = 0; - let redacted = content; - const sortedSecrets = secretValues.slice().sort((a, b) => b.length - a.length); - for (const secretValue of sortedSecrets) { - if (!secretValue || secretValue.length < 8) { - continue; - } - const prefix = secretValue.substring(0, 3); - const asterisks = "*".repeat(Math.max(0, secretValue.length - 3)); - const replacement = prefix + asterisks; - const parts = redacted.split(secretValue); - const occurrences = parts.length - 1; - if (occurrences > 0) { - redacted = parts.join(replacement); - redactionCount += occurrences; - core.info(`Redacted ${occurrences} occurrence(s) of a secret`); - } - } - return { content: redacted, redactionCount }; - } - function processFile(filePath, secretValues) { - try { - const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); - } - return redactionCount; - } catch (error) { - core.warning(`Failed to process file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - return 0; - } - } - async function main() { - const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } - core.info("Starting secret redaction in /tmp/gh-aw directory"); - try { - const secretNameList = secretNames.split(",").filter(name => name.trim()); - const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - if (!secretValue || secretValue.trim() === "") { - continue; - } - secretValues.push(secretValue.trim()); - } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; - } - core.info(`Found ${secretValues.length} secret(s) to redact`); - const targetExtensions = [".txt", ".json", ".log"]; - const files = findFiles("/tmp/gh-aw", targetExtensions); - core.info(`Found ${files.length} file(s) to scan for secrets`); - let totalRedactions = 0; - let filesWithRedactions = 0; - for (const file of files) { - const redactionCount = processFile(file, secretValues); - if (redactionCount > 0) { - filesWithRedactions++; - totalRedactions += redactionCount; - } - } - if (totalRedactions > 0) { - core.info(`Secret redaction complete: ${totalRedactions} redaction(s) in ${filesWithRedactions} file(s)`); - } else { - core.info("Secret redaction complete: no secrets found"); - } - } catch (error) { - core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - await main(); - env: - GH_AW_SECRET_NAMES: 'CONTEXT7_API_KEY,COPILOT_CLI_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,TAVILY_API_KEY' - SECRET_CONTEXT7_API_KEY: ${{ secrets.CONTEXT7_API_KEY }} - SECRET_COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SECRET_TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - - name: Upload engine output files - uses: actions/upload-artifact@v4 - with: - name: agent_outputs - path: | - /tmp/gh-aw/.copilot/logs/ - if-no-files-found: ignore - name: Extract squid access logs if: always() run: | @@ -3745,73 +3757,35 @@ jobs: if: always() uses: actions/github-script@v8 env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/.copilot/logs/ + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log with: script: | function main() { const fs = require("fs"); - const path = require("path"); try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { + const logFile = process.env.GH_AW_AGENT_OUTPUT; + if (!logFile) { core.info("No agent log file specified"); return; } - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); + if (!fs.existsSync(logFile)) { + core.info(`Log file not found: ${logFile}`); return; } - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - content += fileContent; - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - } - } else { - content = fs.readFileSync(logPath, "utf8"); - } - const parsedLog = parseCopilotLog(content); - if (parsedLog) { - core.info(parsedLog); - core.summary.addRaw(parsedLog).write(); - core.info("Copilot log parsed successfully"); - } else { - core.error("Failed to parse Copilot log"); + const logContent = fs.readFileSync(logFile, "utf8"); + const result = parseClaudeLog(logContent); + core.info(result.markdown); + core.summary.addRaw(result.markdown).write(); + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); } } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); - } - } - function extractPremiumRequestCount(logContent) { - const patterns = [ - /premium\s+requests?\s+consumed:?\s*(\d+)/i, - /(\d+)\s+premium\s+requests?\s+consumed/i, - /consumed\s+(\d+)\s+premium\s+requests?/i, - ]; - for (const pattern of patterns) { - const match = logContent.match(pattern); - if (match && match[1]) { - const count = parseInt(match[1], 10); - if (!isNaN(count) && count > 0) { - return count; - } - } + const errorMessage = error instanceof Error ? error.message : String(error); + core.setFailed(errorMessage); } - return 1; } - function parseCopilotLog(logContent) { + function parseClaudeLog(logContent) { try { let logEntries; try { @@ -3820,42 +3794,40 @@ jobs: throw new Error("Not a JSON array"); } } catch (jsonArrayError) { - const debugLogEntries = parseDebugLogFormat(logContent); - if (debugLogEntries && debugLogEntries.length > 0) { - logEntries = debugLogEntries; - } else { - logEntries = []; - const lines = logContent.split("\n"); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === "") { - continue; - } - if (trimmedLine.startsWith("[{")) { - try { - const arrayEntries = JSON.parse(trimmedLine); - if (Array.isArray(arrayEntries)) { - logEntries.push(...arrayEntries); - continue; - } - } catch (arrayParseError) { + logEntries = []; + const lines = logContent.split("\n"); + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine === "") { + continue; + } + if (trimmedLine.startsWith("[{")) { + try { + const arrayEntries = JSON.parse(trimmedLine); + if (Array.isArray(arrayEntries)) { + logEntries.push(...arrayEntries); continue; } - } - if (!trimmedLine.startsWith("{")) { - continue; - } - try { - const jsonEntry = JSON.parse(trimmedLine); - logEntries.push(jsonEntry); - } catch (jsonLineError) { + } catch (arrayParseError) { continue; } } + if (!trimmedLine.startsWith("{")) { + continue; + } + try { + const jsonEntry = JSON.parse(trimmedLine); + logEntries.push(jsonEntry); + } catch (jsonLineError) { + continue; + } } } if (!Array.isArray(logEntries) || logEntries.length === 0) { - return "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n"; + return { + markdown: "## Agent Log Summary\n\nLog format not recognized as Claude JSON array or JSONL.\n", + mcpFailures: [], + }; } const toolUsePairs = new Map(); for (const entry of logEntries) { @@ -3868,10 +3840,13 @@ jobs: } } let markdown = ""; + const mcpFailures = []; const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); if (initEntry) { markdown += "## 🚀 Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "\n## 🤖 Reasoning\n\n"; @@ -3885,7 +3860,7 @@ jobs: } } else if (content.type === "tool_use") { const toolResult = toolUsePairs.get(content.id); - const toolMarkdown = formatToolUseWithDetails(content, toolResult); + const toolMarkdown = formatToolUse(content, toolResult); if (toolMarkdown) { markdown += toolMarkdown; } @@ -3902,7 +3877,7 @@ jobs: const toolName = content.name; const input = content.input || {}; if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; + continue; } const toolResult = toolUsePairs.get(content.id); let statusIcon = "❓"; @@ -3944,12 +3919,6 @@ jobs: if (lastEntry.total_cost_usd) { markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; } - const isPremiumModel = - initEntry && initEntry.model_info && initEntry.model_info.billing && initEntry.model_info.billing.is_premium === true; - if (isPremiumModel) { - const premiumRequestCount = extractPremiumRequestCount(logContent); - markdown += `**Premium Requests Consumed:** ${premiumRequestCount}\n\n`; - } if (lastEntry.usage) { const usage = lastEntry.usage; if (usage.input_tokens || usage.output_tokens) { @@ -3961,383 +3930,25 @@ jobs: markdown += "\n"; } } + if (lastEntry.permission_denials && lastEntry.permission_denials.length > 0) { + markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; + } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Copilot log (tried both JSON array and JSONL formats): ${errorMessage}\n`; - } - } - function parseDebugLogFormat(logContent) { - const entries = []; - const lines = logContent.split("\n"); - let model = "unknown"; - let sessionId = null; - let modelInfo = null; - let tools = []; - const modelMatch = logContent.match(/Starting Copilot CLI: ([\d.]+)/); - if (modelMatch) { - sessionId = `copilot-${modelMatch[1]}-${Date.now()}`; - } - const gotModelInfoIndex = logContent.indexOf("[DEBUG] Got model info: {"); - if (gotModelInfoIndex !== -1) { - const jsonStart = logContent.indexOf("{", gotModelInfoIndex); - if (jsonStart !== -1) { - let braceCount = 0; - let inString = false; - let escapeNext = false; - let jsonEnd = -1; - for (let i = jsonStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - jsonEnd = i + 1; - break; - } - } - } - if (jsonEnd !== -1) { - const modelInfoJson = logContent.substring(jsonStart, jsonEnd); - try { - modelInfo = JSON.parse(modelInfoJson); - } catch (e) { - } - } - } - } - const toolsIndex = logContent.indexOf("[DEBUG] Tools:"); - if (toolsIndex !== -1) { - const afterToolsLine = logContent.indexOf("\n", toolsIndex); - let toolsStart = logContent.indexOf("[DEBUG] [", afterToolsLine); - if (toolsStart !== -1) { - toolsStart = logContent.indexOf("[", toolsStart + 7); - } - if (toolsStart !== -1) { - let bracketCount = 0; - let inString = false; - let escapeNext = false; - let toolsEnd = -1; - for (let i = toolsStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "[") { - bracketCount++; - } else if (char === "]") { - bracketCount--; - if (bracketCount === 0) { - toolsEnd = i + 1; - break; - } - } - } - if (toolsEnd !== -1) { - let toolsJson = logContent.substring(toolsStart, toolsEnd); - toolsJson = toolsJson.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /gm, ""); - try { - const toolsArray = JSON.parse(toolsJson); - if (Array.isArray(toolsArray)) { - tools = toolsArray - .map(tool => { - if (tool.type === "function" && tool.function && tool.function.name) { - let name = tool.function.name; - if (name.startsWith("github-")) { - name = "mcp__github__" + name.substring(7); - } else if (name.startsWith("safe_outputs-")) { - name = name; - } - return name; - } - return null; - }) - .filter(name => name !== null); - } - } catch (e) { - } - } - } - } - let inDataBlock = false; - let currentJsonLines = []; - let turnCount = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("[DEBUG] data:")) { - inDataBlock = true; - currentJsonLines = []; - continue; - } - if (inDataBlock) { - const hasTimestamp = line.match(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /); - if (hasTimestamp) { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - const isJsonContent = /^[{\[}\]"]/.test(cleanLine) || cleanLine.trim().startsWith('"'); - if (!isJsonContent) { - if (currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } - } else { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - currentJsonLines.push(cleanLine); - } - } - } - if (inDataBlock && currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - if (entries.length > 0) { - const initEntry = { - type: "system", - subtype: "init", - session_id: sessionId, - model: model, - tools: tools, + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log (tried both JSON array and JSONL formats): ${errorMessage}\n`, + mcpFailures: [], }; - if (modelInfo) { - initEntry.model_info = modelInfo; - } - entries.unshift(initEntry); - if (entries._lastResult) { - entries.push(entries._lastResult); - delete entries._lastResult; - } } - return entries; } function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; } - if (initEntry.model_info) { - const modelInfo = initEntry.model_info; - if (modelInfo.name) { - markdown += `**Model Name:** ${modelInfo.name}`; - if (modelInfo.vendor) { - markdown += ` (${modelInfo.vendor})`; - } - markdown += "\n\n"; - } - if (modelInfo.billing) { - const billing = modelInfo.billing; - if (billing.is_premium === true) { - markdown += `**Premium Model:** Yes`; - if (billing.multiplier && billing.multiplier !== 1) { - markdown += ` (${billing.multiplier}x cost multiplier)`; - } - markdown += "\n"; - if (billing.restricted_to && Array.isArray(billing.restricted_to) && billing.restricted_to.length > 0) { - markdown += `**Required Plans:** ${billing.restricted_to.join(", ")}\n`; - } - markdown += "\n"; - } else if (billing.is_premium === false) { - markdown += `**Premium Model:** No\n\n`; - } - } - } if (initEntry.session_id) { markdown += `**Session ID:** ${initEntry.session_id}\n\n`; } @@ -4350,6 +3961,9 @@ jobs: for (const server of initEntry.mcp_servers) { const statusIcon = server.status === "connected" ? "✅" : server.status === "failed" ? "❌" : "❓"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -4387,7 +4001,17 @@ jobs: } markdown += "\n"; } - return markdown; + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return { markdown, mcpFailures }; } function estimateTokens(text) { if (!text) return 0; @@ -4406,11 +4030,11 @@ jobs: } return `${minutes}m ${remainingSeconds}s`; } - function formatToolUseWithDetails(toolUse, toolResult) { + function formatToolUse(toolUse, toolResult) { const toolName = toolUse.name; const input = toolUse.input || {}; if (toolName === "TodoWrite") { - return ""; + return ""; } function getStatusIcon() { if (toolResult) { @@ -4451,7 +4075,7 @@ jobs: break; case "Read": const filePath = input.file_path || input.path || ""; - const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); + const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); summary = `${statusIcon} Read ${relativePath}${metadata}`; break; case "Write": @@ -4492,19 +4116,9 @@ jobs: } } if (details && details.trim()) { - let detailsContent = ""; - const inputKeys = Object.keys(input); - if (inputKeys.length > 0) { - detailsContent += "**Parameters:**\n\n"; - detailsContent += "``````json\n"; - detailsContent += JSON.stringify(input, null, 2); - detailsContent += "\n``````\n\n"; - } - detailsContent += "**Response:**\n\n"; - detailsContent += "``````\n"; - detailsContent += details; - detailsContent += "\n``````"; - return `
\n${summary}\n\n${detailsContent}\n
\n\n`; + const maxDetailsLength = 500; + const truncatedDetails = details.length > maxDetailsLength ? details.substring(0, maxDetailsLength) + "..." : details; + return `
\n${summary}\n\n\`\`\`\`\`\n${truncatedDetails}\n\`\`\`\`\`\n
\n\n`; } else { return `${summary}\n\n`; } @@ -4513,8 +4127,8 @@ jobs: if (toolName.startsWith("mcp__")) { const parts = toolName.split("__"); if (parts.length >= 3) { - const provider = parts[1]; - const method = parts.slice(2).join("_"); + const provider = parts[1]; + const method = parts.slice(2).join("_"); return `${provider}::${method}`; } } @@ -4535,7 +4149,12 @@ jobs: } function formatBashCommand(command) { if (!command) return ""; - let formatted = command.replace(/\n/g, " ").replace(/\r/g, " ").replace(/\t/g, " ").replace(/\s+/g, " ").trim(); + let formatted = command + .replace(/\n/g, " ") + .replace(/\r/g, " ") + .replace(/\t/g, " ") + .replace(/\s+/g, " ") + .trim(); formatted = formatted.replace(/`/g, "\\`"); const maxLength = 80; if (formatted.length > maxLength) { @@ -4550,14 +4169,11 @@ jobs: } if (typeof module !== "undefined" && module.exports) { module.exports = { - parseCopilotLog, - extractPremiumRequestCount, + parseClaudeLog, + formatToolUse, formatInitializationSummary, - formatToolUseWithDetails, formatBashCommand, truncateString, - formatMcpName, - formatMcpParameters, estimateTokens, formatDuration, }; @@ -4574,8 +4190,8 @@ jobs: if: always() uses: actions/github-script@v8 env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/.copilot/logs/ - GH_AW_ERROR_PATTERNS: "[{\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"},{\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"pattern\":\"(?:command not found|not found):\\\\s*(.+)|(.+):\\\\s*(?:command not found|not found)\",\"level_group\":0,\"message_group\":0,\"description\":\"Shell command not found error\"},{\"pattern\":\"Cannot find module\\\\s+['\\\"](.+)['\\\"]\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"pattern\":\"Permission denied and could not request permission from user\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied warning (user interaction required)\"}]" + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_ERROR_PATTERNS: "[{\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"}]" with: script: | function main() { @@ -4945,49 +4561,59 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_CLI_TOKEN secret + - name: Validate ANTHROPIC_API_KEY secret run: | - if [ -z "$COPILOT_CLI_TOKEN" ]; then - echo "Error: COPILOT_CLI_TOKEN secret is not set" - echo "The GitHub Copilot CLI engine requires the COPILOT_CLI_TOKEN secret to be configured." + if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "Error: ANTHROPIC_API_KEY secret is not set" + echo "The Claude Code engine requires the ANTHROPIC_API_KEY secret to be configured." echo "Please configure this secret in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" + echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#anthropic-claude-code" exit 1 fi - echo "COPILOT_CLI_TOKEN secret is configured" + echo "ANTHROPIC_API_KEY secret is configured" env: - COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.347 - - name: Execute GitHub Copilot CLI + - name: Install Claude Code CLI + run: npm install -g @anthropic-ai/claude-code@2.0.24 + - name: Execute Claude Code CLI id: agentic_execution - # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) + # Allowed tools (sorted): + # - Bash(cat) + # - Bash(grep) + # - Bash(head) + # - Bash(jq) + # - Bash(ls) + # - Bash(tail) + # - Bash(wc) + # - BashOutput + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - NotebookRead + # - Read + # - Task + # - TodoWrite timeout-minutes: 20 run: | set -o pipefail - COPILOT_CLI_INSTRUCTION=$(cat /tmp/gh-aw/aw-prompts/prompt.txt) - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/.copilot/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + # Execute Claude Code CLI with prompt from file + claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + DISABLE_TELEMETRY: "1" + DISABLE_ERROR_REPORTING: "1" + DISABLE_BUG_COMMAND: "1" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} - XDG_CONFIG_HOME: /home/runner + MCP_TIMEOUT: "120000" + MCP_TOOL_TIMEOUT: "60000" + BASH_DEFAULT_TIMEOUT_MS: "60000" + BASH_MAX_TIMEOUT_MS: "60000" - name: Parse threat detection results uses: actions/github-script@v8 with: diff --git a/.github/workflows/scout.md b/.github/workflows/scout.md index 09e265db5b..d4ecc482b3 100644 --- a/.github/workflows/scout.md +++ b/.github/workflows/scout.md @@ -12,7 +12,7 @@ permissions: contents: read actions: read roles: [admin, maintainer, write] -engine: copilot +engine: claude imports: - shared/mcp/arxiv.md - shared/mcp/tavily.md