diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index e96a36cc15..420aab3450 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -87,7 +87,7 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json outputs: @@ -143,19 +143,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: | - # Download official Copilot CLI installer script - curl -fsSL https://raw.githubusercontent.com/github/copilot-cli/main/install.sh -o /tmp/copilot-install.sh - - # Execute the installer with the specified version - # Pass VERSION directly to sudo to ensure it's available to the installer script - sudo VERSION=0.0.388 bash /tmp/copilot-install.sh - - # Cleanup - rm -f /tmp/copilot-install.sh - - # Verify installation - copilot --version + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.389 - name: Install awf binary run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.10.0 - name: Determine automatic lockdown mode for GitHub MCP server @@ -169,7 +157,7 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/github-mcp-server:v0.29.0 ghcr.io/githubnext/gh-aw-mcpg:v0.0.74 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/github-mcp-server:v0.29.0 ghcr.io/githubnext/gh-aw-mcpg:v0.0.76 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs @@ -350,10 +338,49 @@ jobs: } } EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + API_KEY="" + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + PORT=3001 + + # Register API key as secret to mask it from logs + echo "::add-mask::${API_KEY}" + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + - name: Start MCP gateway id: start-mcp-gateway env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | @@ -370,7 +397,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.74' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' mkdir -p /home/runner/.copilot cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh @@ -387,42 +414,10 @@ jobs: } }, "safeoutputs": { - "type": "stdio", - "container": "node:lts-alpine", - "entrypoint": "node", - "entrypointArgs": ["/opt/gh-aw/safeoutputs/mcp-server.cjs"], - "mounts": ["/opt/gh-aw:/opt/gh-aw:ro", "/tmp/gh-aw:/tmp/gh-aw:rw", "${{ github.workspace }}:${{ github.workspace }}:rw"], - "env": { - "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", - "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", - "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", - "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", - "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}", - "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", - "GITHUB_SERVER_URL": "\${GITHUB_SERVER_URL}", - "GITHUB_SHA": "\${GITHUB_SHA}", - "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", - "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", - "GITHUB_RUN_ID": "\${GITHUB_RUN_ID}", - "GITHUB_RUN_NUMBER": "\${GITHUB_RUN_NUMBER}", - "GITHUB_RUN_ATTEMPT": "\${GITHUB_RUN_ATTEMPT}", - "GITHUB_JOB": "\${GITHUB_JOB}", - "GITHUB_ACTION": "\${GITHUB_ACTION}", - "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", - "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", - "GITHUB_ACTOR": "\${GITHUB_ACTOR}", - "GITHUB_ACTOR_ID": "\${GITHUB_ACTOR_ID}", - "GITHUB_TRIGGERING_ACTOR": "\${GITHUB_TRIGGERING_ACTOR}", - "GITHUB_WORKFLOW": "\${GITHUB_WORKFLOW}", - "GITHUB_WORKFLOW_REF": "\${GITHUB_WORKFLOW_REF}", - "GITHUB_WORKFLOW_SHA": "\${GITHUB_WORKFLOW_SHA}", - "GITHUB_REF": "\${GITHUB_REF}", - "GITHUB_REF_NAME": "\${GITHUB_REF_NAME}", - "GITHUB_REF_TYPE": "\${GITHUB_REF_TYPE}", - "GITHUB_HEAD_REF": "\${GITHUB_HEAD_REF}", - "GITHUB_BASE_REF": "\${GITHUB_BASE_REF}" + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" } } }, @@ -445,7 +440,7 @@ jobs: engine_name: "GitHub Copilot CLI", model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", version: "", - agent_version: "0.0.388", + agent_version: "0.0.389", workflow_name: "Code Simplifier", experimental: false, supports_tools_allowlist: true, @@ -463,7 +458,7 @@ jobs: allowed_domains: [], firewall_enabled: true, awf_version: "v0.10.0", - awmg_version: "v0.0.74", + awmg_version: "v0.0.76", steps: { firewall: "squid" }, @@ -1379,19 +1374,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: | - # Download official Copilot CLI installer script - curl -fsSL https://raw.githubusercontent.com/github/copilot-cli/main/install.sh -o /tmp/copilot-install.sh - - # Execute the installer with the specified version - # Pass VERSION directly to sudo to ensure it's available to the installer script - sudo VERSION=0.0.388 bash /tmp/copilot-install.sh - - # Cleanup - rm -f /tmp/copilot-install.sh - - # Verify installation - copilot --version + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.389 - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): diff --git a/.github/workflows/security-alert-burndown.campaign.lock.yml b/.github/workflows/security-alert-burndown.campaign.lock.yml index fa3be7644e..19c30d45c4 100644 --- a/.github/workflows/security-alert-burndown.campaign.lock.yml +++ b/.github/workflows/security-alert-burndown.campaign.lock.yml @@ -114,8 +114,8 @@ jobs: GH_AW_CAMPAIGN_ID: security-alert-burndown GH_AW_CURSOR_PATH: /tmp/gh-aw/repo-memory/campaigns/security-alert-burndown/cursor.json GH_AW_DISCOVERY_REPOS: githubnext/gh-aw - GH_AW_MAX_DISCOVERY_ITEMS: "50" - GH_AW_MAX_DISCOVERY_PAGES: "3" + GH_AW_MAX_DISCOVERY_ITEMS: "100" + GH_AW_MAX_DISCOVERY_PAGES: "5" GH_AW_PROJECT_URL: https://github.com/orgs/githubnext/projects/134 GH_AW_TRACKER_LABEL: campaign:security-alert-burndown GH_AW_WORKFLOWS: code-scanning-fixer,security-fix-pr @@ -908,8 +908,8 @@ jobs: - Cursor glob: `memory/campaigns/security-alert-burndown/cursor.json` - Project URL: https://github.com/orgs/githubnext/projects/134 - Governance: max new items per run: 3 - - Governance: max discovery items per run: 50 - - Governance: max discovery pages per run: 3 + - Governance: max discovery items per run: 100 + - Governance: max discovery pages per run: 5 - Governance: opt-out labels: no-campaign, no-bot, skip-security-fix - Governance: max project updates per run: 10 - Governance: max comments per run: 3 @@ -1255,10 +1255,10 @@ jobs: - **Read budget**: max discovery items per run: 50 + **Read budget**: max discovery items per run: 100 - **Read budget**: max discovery pages per run: 3 + **Read budget**: max discovery pages per run: 5 **Write budget**: max project updates per run: 10 diff --git a/.github/workflows/security-alert-burndown.campaign.md b/.github/workflows/security-alert-burndown.campaign.md index 5647d1c859..19e586130a 100644 --- a/.github/workflows/security-alert-burndown.campaign.md +++ b/.github/workflows/security-alert-burndown.campaign.md @@ -33,8 +33,8 @@ kpis: priority: supporting governance: max-new-items-per-run: 3 - max-discovery-items-per-run: 50 - max-discovery-pages-per-run: 3 + max-discovery-items-per-run: 100 + max-discovery-pages-per-run: 5 max-project-updates-per-run: 10 max-comments-per-run: 3 opt-out-labels: diff --git a/actions/setup/js/campaign_discovery.cjs b/actions/setup/js/campaign_discovery.cjs index ddf68eab52..1fa61e5dbe 100644 --- a/actions/setup/js/campaign_discovery.cjs +++ b/actions/setup/js/campaign_discovery.cjs @@ -430,6 +430,12 @@ async function discover(config) { const needsAddCount = allItems.filter(i => i.state === "open").length; const needsUpdateCount = allItems.filter(i => i.state === "closed" || i.merged_at).length; + // Determine if budget was exhausted + const itemsBudgetExhausted = totalItemsScanned >= maxDiscoveryItems; + const pagesBudgetExhausted = totalPagesScanned >= maxDiscoveryPages; + const budgetExhausted = itemsBudgetExhausted || pagesBudgetExhausted; + const exhaustedReason = itemsBudgetExhausted ? "max_items_reached" : pagesBudgetExhausted ? "max_pages_reached" : null; + // Build manifest const manifest = { schema_version: MANIFEST_VERSION, @@ -442,6 +448,8 @@ async function discover(config) { pages_scanned: totalPagesScanned, max_items_budget: maxDiscoveryItems, max_pages_budget: maxDiscoveryPages, + budget_exhausted: budgetExhausted, + exhausted_reason: exhaustedReason, cursor: cursor, }, summary: { @@ -460,6 +468,16 @@ async function discover(config) { } core.info(`Discovery complete: ${allItems.length} items found`); + core.info(`Budget utilization: ${totalItemsScanned}/${maxDiscoveryItems} items, ${totalPagesScanned}/${maxDiscoveryPages} pages`); + + if (budgetExhausted) { + if (allItems.length === 0) { + core.warning(`Discovery budget exhausted with 0 items found. Consider increasing budget limits in governance configuration.`); + } else { + core.info(`Discovery stopped at budget limit. Use cursor for continuation in next run.`); + } + } + core.info(`Summary: ${needsAddCount} to add, ${needsUpdateCount} to update`); return manifest; diff --git a/docs/campaign-discovery-budgets.md b/docs/campaign-discovery-budgets.md new file mode 100644 index 0000000000..5e74b2d07a --- /dev/null +++ b/docs/campaign-discovery-budgets.md @@ -0,0 +1,244 @@ +# Campaign Discovery Budget Limits Explained + +## Overview + +Campaign discovery uses **budget limits** to prevent unbounded API usage when searching for worker-created items (issues, PRs, discussions). When these budgets are exhausted before finding any items, the campaign run becomes a **no-op** with zero discovered items. + +## The Workflow Run + +**Example**: [Security Alert Burndown Run #21255054636](https://github.com/githubnext/gh-aw/actions/runs/21255054636) + +This workflow run was a no-op because: +1. **Discovery found 0 items** matching the campaign criteria +2. **Discovery budget limits were reached** before completing the search +3. The campaign orchestrator determined there was **no work to do** + +## Discovery Budget Parameters + +Campaigns enforce two types of budget limits to pace work and protect GitHub's API: + +### 1. Max Discovery Items Per Run + +**Default**: 100 items +**Security Alert Burndown**: 50 items + +This limits the **total number of items** (issues + PRs + discussions) the discovery system will scan in a single run. Once this limit is reached, discovery stops even if more items exist. + +**Config**: `governance.max-discovery-items-per-run` + +```yaml +governance: + max-discovery-items-per-run: 50 # Conservative limit +``` + +### 2. Max Discovery Pages Per Run + +**Default**: 10 pages +**Security Alert Burndown**: 3 pages + +This limits the **number of API pagination pages** fetched during discovery. GitHub's search API returns 100 items per page, so 3 pages = maximum 300 items examined (though only 50 would be retained due to the items budget). + +**Config**: `governance.max-discovery-pages-per-run` + +```yaml +governance: + max-discovery-pages-per-run: 3 # Very conservative +``` + +## Why Security Alert Burndown Had Conservative Budgets + +The Security Alert Burndown campaign uses **extremely conservative** discovery budgets: + +```yaml +governance: + max-new-items-per-run: 3 + max-discovery-items-per-run: 50 # Only 50 items scanned + max-discovery-pages-per-run: 3 # Only 3 API pages + max-project-updates-per-run: 10 + max-comments-per-run: 3 +``` + +These settings were chosen because: +1. **High-risk campaign** (automated security fixes) +2. **Paced rollout** strategy (handle few items at a time) +3. **API rate limit protection** (minimize GitHub API usage) +4. **Incremental progress** (process work gradually over many runs) + +## How Discovery Works + +### Discovery Strategy (Multi-layered) + +The campaign discovery system (`actions/setup/js/campaign_discovery.cjs`) uses a **three-tier search strategy**: + +1. **Primary**: Search by campaign-specific label `z_campaign_` +2. **Secondary**: Search by generic `agentic-campaign` label (filtered by campaign ID) +3. **Fallback**: Search by tracker-id in issue/PR bodies + +### Budget Enforcement + +```javascript +// From campaign_discovery.cjs line 383-385 +if (totalItemsScanned >= maxDiscoveryItems || totalPagesScanned >= maxDiscoveryPages) { + core.warning(`Reached discovery budget limits. Stopping discovery.`); + break; +} +``` + +When **either budget** is exhausted: +- Discovery stops immediately +- A warning is logged +- The manifest includes partial results +- The cursor is saved for continuation in the next run + +## Understanding the No-Op Result + +### What Happened in Run #21255054636 + +1. **Discovery Phase** (Step: "Run campaign discovery precomputation") + - Searched for items with label `z_campaign_security-alert-burndown` + - Scanned up to 50 items across up to 3 API pages + - **Found 0 items** matching the criteria + +2. **Agent Phase** (Job: "agent") + - Received discovery manifest with 0 items + - Had no work to process + - Completed successfully without taking action + +3. **Conclusion Phase** (Job: "conclusion") + - Detected no items to process + - Logged: "No noop items found in agent output" + - Workflow completed as **successful no-op** + +### Why Were No Items Found? + +Possible reasons: +1. **No worker workflows have run yet** - Campaign may be newly created +2. **Worker outputs lack required labels** - Items not tagged with `z_campaign_security-alert-burndown` +3. **Items outside discovery scope** - Workers created items in repos not in `discovery-repos` +4. **Search query too restrictive** - Label search didn't match existing items +5. **Budget exhausted before reaching relevant items** - Items exist but are on page 4+ (beyond 3-page budget) + +## Diagnosing Discovery Issues + +### Check Discovery Configuration + +```yaml +# From .github/workflows/security-alert-burndown.campaign.md +discovery-repos: + - githubnext/gh-aw # Only searching this repo + +workflows: + - code-scanning-fixer + - security-fix-pr +``` + +### Verify Worker Labeling + +Worker workflows MUST create issues/PRs with the campaign-specific label: +``` +z_campaign_security-alert-burndown +``` + +Or use tracker-id in the body: +``` +gh-aw-tracker-id: code-scanning-fixer +``` + +### Check Cursor State + +The cursor tracks pagination state across runs: +```bash +# Location: repo-memory branch +memory/campaigns/security-alert-burndown/cursor.json +``` + +## Adjusting Discovery Budgets + +### When to Increase Budgets + +Increase budgets if: +- Discovery finds 0 items but you know items exist +- Discovery log shows "Reached discovery budget limits" +- Cursor shows pagination stopped early +- Campaign needs faster ramp-up + +### Recommended Adjustments + +**Conservative** (current): +```yaml +max-discovery-items-per-run: 50 +max-discovery-pages-per-run: 3 +``` + +**Moderate**: +```yaml +max-discovery-items-per-run: 100 # Default +max-discovery-pages-per-run: 5 +``` + +**Aggressive**: +```yaml +max-discovery-items-per-run: 200 +max-discovery-pages-per-run: 10 # Default +``` + +### When to Keep Conservative Budgets + +Keep conservative budgets for: +- **High-risk campaigns** (security, compliance) +- **Cross-organization campaigns** (many repos) +- **Experimental campaigns** (testing workflows) +- **API rate limit concerns** + +## Future Improvements + +### Better Logging + +The discovery system should log more details when budgets are reached: + +```javascript +core.warning(`Reached discovery budget limits. Stopping discovery.`); +core.info(`Total items scanned: ${totalItemsScanned}/${maxDiscoveryItems}`); +core.info(`Total pages scanned: ${totalPagesScanned}/${maxDiscoveryPages}`); +core.info(`Items found: ${allItems.length}`); +``` + +### Budget Exhaustion Reasons + +The conclusion job should distinguish between: +1. **True no-op**: No items exist (successful) +2. **Budget-limited**: Discovery stopped early (may need adjustment) +3. **Configuration error**: Wrong labels or scope + +### Discovery Manifest Enhancement + +Include budget utilization in manifest: + +```json +{ + "discovery": { + "total_items": 0, + "items_scanned": 50, + "pages_scanned": 3, + "max_items_budget": 50, + "max_pages_budget": 3, + "budget_exhausted": true, // NEW + "exhausted_reason": "max_pages_reached" // NEW + } +} +``` + +## Summary + +The Security Alert Burndown campaign run was a **no-op by design**: +- Discovery exhausted its **conservative 3-page budget** (50 items max) +- Found **0 matching items** within that budget +- Completed successfully without work to do + +This is **expected behavior** for campaigns with strict governance policies. The conservative budgets ensure: +- Paced rollout +- API rate limit protection +- Incremental progress tracking +- Risk mitigation through small batches + +To change this behavior, increase `max-discovery-pages-per-run` and/or `max-discovery-items-per-run` in the campaign governance section.