diff --git a/.github/aw/close-agentic-campaign.md b/.github/aw/close-agentic-campaign.md index e22f090cee..b3a40b6e01 100644 --- a/.github/aw/close-agentic-campaign.md +++ b/.github/aw/close-agentic-campaign.md @@ -4,13 +4,13 @@ Execute all four steps in strict order: 1. Read State (no writes) 2. Make Decisions (no writes) -3. Write State (update-project only) +3. Dispatch Workers (dispatch-workflow only) 4. Report The following rules are mandatory and override inferred behavior: - The GitHub Project board is the single source of truth. -- All project writes MUST comply with `project_update_instructions.md`. +- All project writes MUST comply with the Project Update Instructions (in workers). - State reads and state writes MUST NOT be interleaved. - Do NOT infer missing data or invent values. - Do NOT reorganize hierarchy. diff --git a/.github/aw/orchestrate-agentic-campaign.md b/.github/aw/orchestrate-agentic-campaign.md index 343477a35b..9318bd46ec 100644 --- a/.github/aw/orchestrate-agentic-campaign.md +++ b/.github/aw/orchestrate-agentic-campaign.md @@ -1,10 +1,10 @@ # Orchestrator Instructions -This orchestrator coordinates a single campaign by discovering worker outputs, making deterministic decisions, -and synchronizing campaign state into a GitHub Project board. +This orchestrator coordinates a single campaign by discovering worker outputs and making deterministic decisions. -**Scope:** orchestration only (discovery, planning, pacing, reporting). -**Write authority:** all project write semantics are governed by **Project Update Instructions** and MUST be followed. +**Scope:** orchestration only (discovery, planning, pacing, reporting). +**Actuation model:** **dispatch-only** — the orchestrator may only act by dispatching allowlisted worker workflows. +**Write authority:** all GitHub writes (Projects, issues/PRs, comments, status updates) must happen in worker workflows. --- @@ -17,33 +17,18 @@ and synchronizing campaign state into a GitHub Project board. - On throttling (HTTP 429 / rate-limit 403), do not retry aggressively; back off and end the run after reporting what remains. {{ if .CursorGlob }} -**Cursor file (repo-memory)**: `{{ .CursorGlob }}` -**File system path**: `/tmp/gh-aw/repo-memory/campaigns/{{.CampaignID}}/cursor.json` -- If it exists: read first and continue from its boundary. -- If it does not exist: create it by end of run. +**Cursor file (repo-memory)**: `{{ .CursorGlob }}` +**File system path**: `/tmp/gh-aw/repo-memory/campaigns/{{.CampaignID}}/cursor.json` +- If it exists: read first and continue from its boundary. +- If it does not exist: create it by end of run. - Always write the updated cursor back to the same path. {{ end }} {{ if .MetricsGlob }} -**Metrics snapshots (repo-memory)**: `{{ .MetricsGlob }}` -**File system path**: `/tmp/gh-aw/repo-memory/campaigns/{{.CampaignID}}/metrics/*.json` +**Metrics snapshots (repo-memory)**: `{{ .MetricsGlob }}` +**File system path**: `/tmp/gh-aw/repo-memory/campaigns/{{.CampaignID}}/metrics/*.json` - Persist one append-only JSON metrics snapshot per run (new file per run; do not rewrite history). - Use UTC date (`YYYY-MM-DD`) in the filename (example: `metrics/2025-12-22.json`). -- Each snapshot MUST include ALL required fields (even if zero): - - `campaign_id` (string): The campaign identifier - - `date` (string): UTC date in YYYY-MM-DD format - - `tasks_total` (number): Total number of tasks (>= 0, even if 0) - - `tasks_completed` (number): Completed task count (>= 0, even if 0) -- Optional fields (include only if available): `tasks_in_progress`, `tasks_blocked`, `velocity_per_day`, `estimated_completion` -- Example minimum valid snapshot: - ```json - { - "campaign_id": "{{.CampaignID}}", - "date": "2025-12-22", - "tasks_total": 0, - "tasks_completed": 0 - } - ``` {{ end }} {{ if gt .MaxDiscoveryItemsPerRun 0 }} @@ -52,12 +37,6 @@ and synchronizing campaign state into a GitHub Project board. {{ if gt .MaxDiscoveryPagesPerRun 0 }} **Read budget**: max discovery pages per run: {{ .MaxDiscoveryPagesPerRun }} {{ end }} -{{ if gt .MaxProjectUpdatesPerRun 0 }} -**Write budget**: max project updates per run: {{ .MaxProjectUpdatesPerRun }} -{{ end }} -{{ if gt .MaxProjectCommentsPerRun 0 }} -**Write budget**: max project comments per run: {{ .MaxProjectCommentsPerRun }} -{{ end }} --- @@ -68,128 +47,26 @@ and synchronizing campaign state into a GitHub Project board. 3. Correlation is explicit (tracker-id AND labels) 4. Reads and writes are separate steps (never interleave) 5. Idempotent operation is mandatory (safe to re-run) -6. Only predefined project fields may be updated -7. **Project Update Instructions take precedence for all project writes** -8. **Campaign items MUST be labeled** for discovery and isolation - ---- - -## Campaign Label Requirements - -**All campaign-related issues, PRs, and discussions MUST have two labels:** - -1. **`agentic-campaign`** - Generic label marking content as part of ANY campaign - - Prevents other workflows from processing campaign items - - Enables campaign-wide queries and filters - -2. **`z_campaign_{{.CampaignID}}`** - Campaign-specific label - - Enables precise discovery of items belonging to THIS campaign - - Format: `z_campaign_` (lowercase, hyphen-separated) - - Example: `z_campaign_security-q1-2025` - -**Worker Responsibilities:** -- Workers creating issues/PRs as campaign output MUST add both labels -- Workers SHOULD use `create-issue` or `create-pr` safe outputs with labels configuration -- If workers cannot add labels automatically, campaign orchestrator will attempt to add them during discovery - -**Non-Campaign Workflow Responsibilities:** -- Workflows triggered by issues/PRs SHOULD skip items with `agentic-campaign` label -- Use `skip-if-match` configuration to filter out campaign items: - ```yaml - on: - issues: - types: [opened, labeled] - skip-if-match: - query: "label:agentic-campaign" - max: 0 # Skip if ANY campaign items match - ``` +6. Orchestrators do not write GitHub state directly --- ## Execution Steps (Required Order) -### Step 0 — Epic Issue Initialization [FIRST RUN ONLY] - -**Campaign Epic Issue Requirements:** -- Each project board MUST have exactly ONE Epic issue representing the campaign -- The Epic serves as the parent for all campaign work issues -- The Epic is narrative-only and tracks overall campaign progress - -**On every run, before other steps:** - -1) **Check for existing Epic issue** by searching the repository for: - - An open issue with label `epic` or `type:epic` - - Body text containing: `campaign_id: {{.CampaignID}}` - -2) **If no Epic issue exists**, create it using `create-issue`: - ```yaml - create-issue: - title: "{{if .CampaignName}}{{.CampaignName}}{{else}}Campaign: {{.CampaignID}}{{end}}" - body: | - ## Campaign Overview - - This Epic issue tracks the overall progress of the campaign. All work items are sub-issues of this Epic. - - **Campaign Details:** - - Campaign ID: `{{.CampaignID}}` - - Project Board: {{.ProjectURL}} - {{ if .Workflows }}- Worker Workflows: {{range $i, $w := .Workflows}}{{if $i}}, {{end}}`{{$w}}`{{end}}{{ end }} - - --- - `campaign_id: {{.CampaignID}}` - labels: - - agentic-campaign - - z_campaign_{{.CampaignID}} - - epic - - type:epic - ``` - -3) **After creating the Epic** (or if Epic exists but not on board), add it to the project board: - ```yaml - update-project: - project: "{{.ProjectURL}}" - campaign_id: "{{.CampaignID}}" - content_type: "issue" - content_number: - fields: - status: "In Progress" - campaign_id: "{{.CampaignID}}" - worker_workflow: "unknown" - repository: "" - priority: "High" - size: "Large" - start_date: "" - end_date: "" - ``` - -4) **Record the Epic issue number** in repo-memory for reference (e.g., in cursor file or metadata). - -**Note:** This step typically runs only on the first orchestrator execution. On subsequent runs, verify the Epic exists and is on the board, but do not recreate it. - ---- - ### Step 1 — Read State (Discovery) [NO WRITES] **IMPORTANT**: Discovery has been precomputed. Read the discovery manifest instead of performing GitHub-wide searches. 1) Read the precomputed discovery manifest: `./.gh-aw/campaign.discovery.json` - - This manifest contains all discovered worker outputs with normalized metadata - - Schema version: v1 - - Fields: campaign_id, generated_at, discovery (total_items, cursor info), summary (counts), items (array of normalized items) - -2) Read current GitHub Project board state (items + required fields). -3) Parse discovered items from the manifest: +2) Parse discovered items from the manifest: - Each item has: url, content_type (issue/pull_request/discussion), number, repo, created_at, updated_at, state - Closed items have: closed_at (for issues) or merged_at (for PRs) - Items are pre-sorted by updated_at for deterministic processing -4) Check the manifest summary for work counts: - - `needs_add_count`: Number of items that need to be added to the project - - `needs_update_count`: Number of items that need status updates - - If both are 0, you may skip to reporting step +3) Check the manifest summary for work counts. -5) Discovery cursor is maintained automatically in repo-memory; do not modify it manually. +4) Discovery cursor is maintained automatically in repo-memory; do not modify it manually. ### Step 2 — Make Decisions (Planning) [NO WRITES] @@ -198,66 +75,27 @@ and synchronizing campaign state into a GitHub Project board. - Closed (issue/discussion) → `Done` - Merged (PR) → `Done` -**Why use explicit GitHub state?** - GitHub is the source of truth for work status. Inferring status from other signals (labels, comments) would be unreliable and could cause incorrect tracking. - -6) Calculate required date fields for each item (per Project Update Instructions): +6) Calculate required date fields (for workers that sync Projects): - `start_date`: format `created_at` as `YYYY-MM-DD` - `end_date`: - if closed/merged → format `closed_at`/`merged_at` as `YYYY-MM-DD` - - if open → **today's date** formatted `YYYY-MM-DD` (required for roadmap view) - -**Why use today for open items?** - GitHub Projects requires end_date for roadmap views. Using today's date shows the item is actively tracked and updates automatically each run until completion. - -7) Do NOT implement idempotency by comparing against the board. You may compare for reporting only. - -**Why no comparison for idempotency?** - The safe-output system handles deduplication. Comparing would add complexity and potential race conditions. Trust the infrastructure. - -8) Apply write budget: -- If `MaxProjectUpdatesPerRun > 0`, select at most that many items this run using deterministic order - (e.g., oldest `updated_at` first; tie-break by ID/number). -- Defer remaining items to next run via cursor. - -**Why use deterministic order?** - Ensures predictable behavior and prevents starvation. Oldest items are processed first, ensuring fair treatment of all work items. The cursor saves progress for next run. - -### Step 3 — Write State (Execution) [WRITES ONLY] - -9) For each selected item, send an `update-project` request. -- Do NOT interleave reads. -- Do NOT pre-check whether the item is on the board. -- **All write semantics MUST follow Project Update Instructions**, including: - - first add → full required fields (status, campaign_id, worker_workflow, repo, priority, size, start_date, end_date) - - existing item → status-only update unless explicit backfill is required - -10) Record per-item outcome: success/failure + error details. + - if open → **today's date** formatted `YYYY-MM-DD` -### Step 4 — Report & Status Update +7) Reads and writes are separate steps (never interleave). -11) **REQUIRED: Create a project status update summarizing this run** +### Step 3 — Dispatch Workers (Execution) [DISPATCH ONLY] -Every campaign run MUST create a status update using `create-project-status-update` safe output. This is the primary communication mechanism for conveying campaign progress to stakeholders. +8) For each selected unit of work, dispatch a worker workflow using `dispatch-workflow`. -**Required Sections:** +Constraints: +- Only dispatch allowlisted workflows. +- Keep within the dispatch-workflow max for this run. -- **Most Important Findings**: Highlight the 2-3 most critical discoveries, insights, or blockers from this run -- **What Was Learned**: Document key learnings, patterns observed, or insights gained during this run -- **Campaign Progress**: Report on campaign metrics and trends with baseline → current → target format, including direction and velocity -- **Campaign Summary**: Tasks completed, in progress, blocked, and overall completion percentage -- **Next Steps**: Clear action items and priorities for the next run +### Step 4 — Report (No Writes) -**Configuration:** -- Set appropriate status: ON_TRACK, AT_RISK, OFF_TRACK, or COMPLETE -- Use today's date for start_date and target_date (or appropriate future date for target) -- Body must be comprehensive yet concise (target: 200-400 words) +9) Summarize what you dispatched, what remains, and what should run next. -Example status update: -```yaml -create-project-status-update: - project: "{{.ProjectURL}}" - status: "ON_TRACK" - start_date: "2026-01-06" - target_date: "2026-01-31" - body: | - ## Campaign Run Summary +If a status update is required on the GitHub Project, dispatch a dedicated reporting/sync worker to perform that write. **Discovered:** 25 items (15 issues, 10 PRs) **Processed:** 10 items added to project, 5 updated diff --git a/.github/aw/update-agentic-campaign-project.md b/.github/aw/update-agentic-campaign-project.md index 8ed4c79a66..c64c49f666 100644 --- a/.github/aw/update-agentic-campaign-project.md +++ b/.github/aw/update-agentic-campaign-project.md @@ -10,7 +10,8 @@ If any other instructions conflict with this file, THIS FILE TAKES PRECEDENCE fo ## 0) Hard Requirements (Do Not Deviate) -- Writes MUST use only the `update-project` safe-output. +- Orchestrators are dispatch-only and MUST NOT perform project writes directly. +- Worker workflows performing project writes MUST use only the `update-project` safe-output. - All writes MUST target exactly: - **Project URL**: `{{.ProjectURL}}` - Every item MUST include: diff --git a/.github/workflows/security-alert-burndown.campaign.lock.yml b/.github/workflows/security-alert-burndown.campaign.lock.yml index 0abccd72ba..c89d95fee6 100644 --- a/.github/workflows/security-alert-burndown.campaign.lock.yml +++ b/.github/workflows/security-alert-burndown.campaign.lock.yml @@ -69,11 +69,9 @@ jobs: needs: activation runs-on: ubuntu-latest permissions: - actions: read contents: read issues: read pull-requests: read - security-events: read concurrency: group: "gh-aw-claude-${{ github.workflow }}" env: @@ -196,71 +194,10 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":3},"create_issue":{"max":1},"create_project_status_update":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1},"update_project":{"max":10}} + {"dispatch_workflow":{"max":3,"workflows":["code-scanning-fixer","security-fix-pr","dependabot-bundler","secret-scanning-triage"]},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ - { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", - "type": "string" - }, - "labels": { - "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "parent": { - "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", - "type": [ - "number", - "string" - ] - }, - "temporary_id": { - "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", - "type": "string" - }, - "title": { - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_issue" - }, - { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 3 comment(s) can be added.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation.", - "type": "string" - }, - "item_number": { - "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", - "type": "number" - } - }, - "required": [ - "body" - ], - "type": "object" - }, - "name": "add_comment" - }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { @@ -303,133 +240,6 @@ jobs: }, "name": "noop" }, - { - "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "campaign_id": { - "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", - "type": "string" - }, - "content_number": { - "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", - "type": "number" - }, - "content_type": { - "description": "Type of item to add to the project. Use 'issue' or 'pull_request' to add existing repo content, or 'draft_issue' to create a draft item inside the project. Required when operation is not specified.", - "enum": [ - "issue", - "pull_request", - "draft_issue" - ], - "type": "string" - }, - "create_if_missing": { - "description": "Whether to create the project if it doesn't exist. Defaults to false. Requires projects:write permission when true.", - "type": "boolean" - }, - "draft_body": { - "description": "Optional body for a Projects v2 draft issue (markdown). Only used when content_type is 'draft_issue'.", - "type": "string" - }, - "draft_title": { - "description": "Title for a Projects v2 draft issue. Required when content_type is 'draft_issue'.", - "type": "string" - }, - "field_definitions": { - "description": "Field definitions to create when operation is create_fields. Required when operation='create_fields'.", - "items": { - "additionalProperties": false, - "properties": { - "data_type": { - "description": "Field type. Use SINGLE_SELECT with options for enumerated values.", - "enum": [ - "TEXT", - "NUMBER", - "DATE", - "SINGLE_SELECT", - "ITERATION" - ], - "type": "string" - }, - "name": { - "description": "Field name to create (e.g., 'size', 'priority').", - "type": "string" - }, - "options": { - "description": "Options for SINGLE_SELECT fields.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "name", - "data_type" - ], - "type": "object" - }, - "type": "array" - }, - "fields": { - "description": "Custom field values to set on the project item (e.g., {'Status': 'In Progress', 'Priority': 'High'}). Field names must match custom fields defined in the project.", - "type": "object" - }, - "operation": { - "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", - "enum": [ - "create_fields", - "create_view" - ], - "type": "string" - }, - "project": { - "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'). Project names or numbers alone are NOT accepted.", - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+$", - "type": "string" - }, - "view": { - "additionalProperties": false, - "description": "View definition to create when operation is create_view. Required when operation='create_view'.", - "properties": { - "filter": { - "type": "string" - }, - "layout": { - "enum": [ - "table", - "board", - "roadmap" - ], - "type": "string" - }, - "name": { - "type": "string" - }, - "visible_fields": { - "description": "Field IDs to show in the view (table/board only).", - "items": { - "type": "number" - }, - "type": "array" - } - }, - "required": [ - "name", - "layout" - ], - "type": "object" - } - }, - "required": [ - "project" - ], - "type": "object" - }, - "name": "update_project" - }, { "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", "inputSchema": { @@ -458,139 +268,55 @@ jobs: "name": "missing_data" }, { - "description": "Create a status update on a GitHub Projects v2 board to communicate project progress. Use this when you need to provide stakeholder updates with status indicators, timeline information, and progress summaries. Status updates create a historical record of project progress tracked over time. Requires project URL, status indicator, dates, and markdown body describing progress/trends/findings.", + "_workflow_name": "code-scanning-fixer", + "description": "Dispatch the 'code-scanning-fixer' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in the same repository.", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "code_scanning_fixer" + }, + { + "_workflow_name": "security-fix-pr", + "description": "Dispatch the 'security-fix-pr' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in the same repository.", "inputSchema": { "additionalProperties": false, "properties": { - "body": { - "description": "Status update body in markdown format describing progress, findings, trends, and next steps. Should provide stakeholders with clear understanding of project state.", - "type": "string" - }, - "project": { - "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'). Project names or numbers alone are NOT accepted.", - "pattern": "^https://github\\\\.com/(orgs|users)/[^/]+/projects/\\\\d+$", - "type": "string" - }, - "start_date": { - "description": "Optional project start date in YYYY-MM-DD format (e.g., '2026-01-06').", - "pattern": "^\\\\d{4}-\\\\d{2}-\\\\d{2}$", - "type": "string" - }, - "status": { - "description": "Status indicator for the project. Defaults to ON_TRACK. Values: ON_TRACK (progressing well), AT_RISK (has issues/blockers), OFF_TRACK (significantly behind), COMPLETE (finished), INACTIVE (paused/cancelled).", - "enum": [ - "ON_TRACK", - "AT_RISK", - "OFF_TRACK", - "COMPLETE", - "INACTIVE" - ], - "type": "string" - }, - "target_date": { - "description": "Optional project target/end date in YYYY-MM-DD format (e.g., '2026-12-31').", - "pattern": "^\\\\d{4}-\\\\d{2}-\\\\d{2}$", + "security_url": { + "default": "", + "description": "Security alert URL (e.g., https://github.com/owner/repo/security/code-scanning/123)", "type": "string" } }, - "required": [ - "project", - "body" - ], "type": "object" }, - "name": "create_project_status_update" + "name": "security_fix_pr" + }, + { + "_workflow_name": "dependabot-bundler", + "description": "Dispatch the 'dependabot-bundler' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in the same repository.", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "dependabot_bundler" + }, + { + "_workflow_name": "secret-scanning-triage", + "description": "Dispatch the 'secret-scanning-triage' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in the same repository.", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "secret_scanning_triage" } ] EOF cat > /opt/gh-aw/safeoutputs/validation.json << 'EOF' { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "item_number": { - "issueOrPRNumber": true - } - } - }, - "create_issue": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "create_project_status_update": { - "defaultMax": 10, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65536 - }, - "project": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", - "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" - }, - "start_date": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "patternError": "must be in YYYY-MM-DD format" - }, - "status": { - "type": "string", - "enum": [ - "INACTIVE", - "ON_TRACK", - "AT_RISK", - "OFF_TRACK", - "COMPLETE" - ] - }, - "target_date": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "patternError": "must be in YYYY-MM-DD format" - } - } - }, "missing_tool": { "defaultMax": 20, "fields": { @@ -623,43 +349,6 @@ jobs: "maxLength": 65000 } } - }, - "update_project": { - "defaultMax": 10, - "fields": { - "campaign_id": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "content_number": { - "optionalPositiveInteger": true - }, - "content_type": { - "type": "string", - "enum": [ - "issue", - "pull_request" - ] - }, - "fields": { - "type": "object" - }, - "issue": { - "optionalPositiveInteger": true - }, - "project": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", - "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" - }, - "pull_request": { - "optionalPositiveInteger": true - } - } } } EOF @@ -733,7 +422,7 @@ jobs: "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,actions,code_security" + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" } }, "safeoutputs": { @@ -851,7 +540,7 @@ jobs: To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - **Available tools**: add_comment, create_issue, create_project_status_update, missing_tool, noop, update_project + **Available tools**: dispatch_workflow, missing_tool, noop **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. @@ -1203,11 +892,11 @@ jobs: --- # Orchestrator Instructions - This orchestrator coordinates a single campaign by discovering worker outputs, making deterministic decisions, - and synchronizing campaign state into a GitHub Project board. + This orchestrator coordinates a single campaign by discovering worker outputs and making deterministic decisions. - **Scope:** orchestration only (discovery, planning, pacing, reporting). - **Write authority:** all project write semantics are governed by **Project Update Instructions** and MUST be followed. + **Scope:** orchestration only (discovery, planning, pacing, reporting). + **Actuation model:** **dispatch-only** — the orchestrator may only act by dispatching allowlisted worker workflows. + **Write authority:** all GitHub writes (Projects, issues/PRs, comments, status updates) must happen in worker workflows. --- @@ -1230,12 +919,6 @@ jobs: **Read budget**: max discovery pages per run: 5 - **Write budget**: max project updates per run: 10 - - - **Write budget**: max project comments per run: 3 - - --- ## Core Principles @@ -1245,128 +928,26 @@ jobs: 3. Correlation is explicit (tracker-id AND labels) 4. Reads and writes are separate steps (never interleave) 5. Idempotent operation is mandatory (safe to re-run) - 6. Only predefined project fields may be updated - 7. **Project Update Instructions take precedence for all project writes** - 8. **Campaign items MUST be labeled** for discovery and isolation - - --- - - ## Campaign Label Requirements - - **All campaign-related issues, PRs, and discussions MUST have two labels:** - - 1. **`agentic-campaign`** - Generic label marking content as part of ANY campaign - - Prevents other workflows from processing campaign items - - Enables campaign-wide queries and filters - - 2. **`z_campaign_security-alert-burndown`** - Campaign-specific label - - Enables precise discovery of items belonging to THIS campaign - - Format: `z_campaign_` (lowercase, hyphen-separated) - - Example: `z_campaign_security-q1-2025` - - **Worker Responsibilities:** - - Workers creating issues/PRs as campaign output MUST add both labels - - Workers SHOULD use `create-issue` or `create-pr` safe outputs with labels configuration - - If workers cannot add labels automatically, campaign orchestrator will attempt to add them during discovery - - **Non-Campaign Workflow Responsibilities:** - - Workflows triggered by issues/PRs SHOULD skip items with `agentic-campaign` label - - Use `skip-if-match` configuration to filter out campaign items: - ```yaml - on: - issues: - types: [opened, labeled] - skip-if-match: - query: "label:agentic-campaign" - max: 0 # Skip if ANY campaign items match - ``` + 6. Orchestrators do not write GitHub state directly --- ## Execution Steps (Required Order) - ### Step 0 — Epic Issue Initialization [FIRST RUN ONLY] - - **Campaign Epic Issue Requirements:** - - Each project board MUST have exactly ONE Epic issue representing the campaign - - The Epic serves as the parent for all campaign work issues - - The Epic is narrative-only and tracks overall campaign progress - - **On every run, before other steps:** - - 1) **Check for existing Epic issue** by searching the repository for: - - An open issue with label `epic` or `type:epic` - - Body text containing: `campaign_id: security-alert-burndown` - - 2) **If no Epic issue exists**, create it using `create-issue`: - ```yaml - create-issue: - title: "Security Alert Burndown" - body: | - ## Campaign Overview - - This Epic issue tracks the overall progress of the campaign. All work items are sub-issues of this Epic. - - **Campaign Details:** - - Campaign ID: `security-alert-burndown` - - Project Board: https://github.com/orgs/githubnext/projects/134 - - Worker Workflows: `code-scanning-fixer`, `security-fix-pr`, `dependabot-bundler`, `secret-scanning-triage` - - --- - `campaign_id: security-alert-burndown` - labels: - - agentic-campaign - - z_campaign_security-alert-burndown - - epic - - type:epic - ``` - - 3) **After creating the Epic** (or if Epic exists but not on board), add it to the project board: - ```yaml - update-project: - project: "https://github.com/orgs/githubnext/projects/134" - campaign_id: "security-alert-burndown" - content_type: "issue" - content_number: - fields: - status: "In Progress" - campaign_id: "security-alert-burndown" - worker_workflow: "unknown" - repository: "" - priority: "High" - size: "Large" - start_date: "" - end_date: "" - ``` - - 4) **Record the Epic issue number** in repo-memory for reference (e.g., in cursor file or metadata). - - **Note:** This step typically runs only on the first orchestrator execution. On subsequent runs, verify the Epic exists and is on the board, but do not recreate it. - - --- - ### Step 1 — Read State (Discovery) [NO WRITES] **IMPORTANT**: Discovery has been precomputed. Read the discovery manifest instead of performing GitHub-wide searches. 1) Read the precomputed discovery manifest: `./.gh-aw/campaign.discovery.json` - - This manifest contains all discovered worker outputs with normalized metadata - - Schema version: v1 - - Fields: campaign_id, generated_at, discovery (total_items, cursor info), summary (counts), items (array of normalized items) - - 2) Read current GitHub Project board state (items + required fields). - 3) Parse discovered items from the manifest: + 2) Parse discovered items from the manifest: - Each item has: url, content_type (issue/pull_request/discussion), number, repo, created_at, updated_at, state - Closed items have: closed_at (for issues) or merged_at (for PRs) - Items are pre-sorted by updated_at for deterministic processing - 4) Check the manifest summary for work counts: - - `needs_add_count`: Number of items that need to be added to the project - - `needs_update_count`: Number of items that need status updates - - If both are 0, you may skip to reporting step + 3) Check the manifest summary for work counts. - 5) Discovery cursor is maintained automatically in repo-memory; do not modify it manually. + 4) Discovery cursor is maintained automatically in repo-memory; do not modify it manually. ### Step 2 — Make Decisions (Planning) [NO WRITES] @@ -1375,68 +956,27 @@ jobs: - Closed (issue/discussion) → `Done` - Merged (PR) → `Done` - **Why use explicit GitHub state?** - GitHub is the source of truth for work status. Inferring status from other signals (labels, comments) would be unreliable and could cause incorrect tracking. - - 6) Calculate required date fields for each item (per Project Update Instructions): + 6) Calculate required date fields (for workers that sync Projects): - `start_date`: format `created_at` as `YYYY-MM-DD` - `end_date`: - if closed/merged → format `closed_at`/`merged_at` as `YYYY-MM-DD` - PROMPT_EOF - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - if open → **today's date** formatted `YYYY-MM-DD` (required for roadmap view) + - if open → **today's date** formatted `YYYY-MM-DD` - **Why use today for open items?** - GitHub Projects requires end_date for roadmap views. Using today's date shows the item is actively tracked and updates automatically each run until completion. + 7) Reads and writes are separate steps (never interleave). - 7) Do NOT implement idempotency by comparing against the board. You may compare for reporting only. + ### Step 3 — Dispatch Workers (Execution) [DISPATCH ONLY] - **Why no comparison for idempotency?** - The safe-output system handles deduplication. Comparing would add complexity and potential race conditions. Trust the infrastructure. + 8) For each selected unit of work, dispatch a worker workflow using `dispatch-workflow`. - 8) Apply write budget: - - If `MaxProjectUpdatesPerRun > 0`, select at most that many items this run using deterministic order - (e.g., oldest `updated_at` first; tie-break by ID/number). - - Defer remaining items to next run via cursor. + Constraints: + - Only dispatch allowlisted workflows. + - Keep within the dispatch-workflow max for this run. - **Why use deterministic order?** - Ensures predictable behavior and prevents starvation. Oldest items are processed first, ensuring fair treatment of all work items. The cursor saves progress for next run. + ### Step 4 — Report (No Writes) - ### Step 3 — Write State (Execution) [WRITES ONLY] + 9) Summarize what you dispatched, what remains, and what should run next. - 9) For each selected item, send an `update-project` request. - - Do NOT interleave reads. - - Do NOT pre-check whether the item is on the board. - - **All write semantics MUST follow Project Update Instructions**, including: - - first add → full required fields (status, campaign_id, worker_workflow, repo, priority, size, start_date, end_date) - - existing item → status-only update unless explicit backfill is required - - 10) Record per-item outcome: success/failure + error details. - - ### Step 4 — Report & Status Update - - 11) **REQUIRED: Create a project status update summarizing this run** - - Every campaign run MUST create a status update using `create-project-status-update` safe output. This is the primary communication mechanism for conveying campaign progress to stakeholders. - - **Required Sections:** - - - **Most Important Findings**: Highlight the 2-3 most critical discoveries, insights, or blockers from this run - - **What Was Learned**: Document key learnings, patterns observed, or insights gained during this run - - **Campaign Progress**: Report on campaign metrics and trends with baseline → current → target format, including direction and velocity - - **Campaign Summary**: Tasks completed, in progress, blocked, and overall completion percentage - - **Next Steps**: Clear action items and priorities for the next run - - **Configuration:** - - Set appropriate status: ON_TRACK, AT_RISK, OFF_TRACK, or COMPLETE - - Use today's date for start_date and target_date (or appropriate future date for target) - - Body must be comprehensive yet concise (target: 200-400 words) - - Example status update: - ```yaml - create-project-status-update: - project: "https://github.com/orgs/githubnext/projects/134" - status: "ON_TRACK" - start_date: "2026-01-06" - target_date: "2026-01-31" - body: | - ## Campaign Run Summary + If a status update is required on the GitHub Project, dispatch a dedicated reporting/sync worker to perform that write. **Discovered:** 25 items (15 issues, 10 PRs) **Processed:** 10 items added to project, 5 updated @@ -1506,7 +1046,8 @@ jobs: ## 0) Hard Requirements (Do Not Deviate) - - Writes MUST use only the `update-project` safe-output. + - Orchestrators are dispatch-only and MUST NOT perform project writes directly. + - Worker workflows performing project writes MUST use only the `update-project` safe-output. - All writes MUST target exactly: - **Project URL**: `https://github.com/orgs/githubnext/projects/134` - Every item MUST include: @@ -1531,6 +1072,8 @@ jobs: | `start_date` | date | `YYYY-MM-DD` | | `end_date` | date | `YYYY-MM-DD` | + PROMPT_EOF + cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" Field names are case-sensitive. --- @@ -1748,13 +1291,13 @@ jobs: 1. Read State (no writes) 2. Make Decisions (no writes) - 3. Write State (update-project only) + 3. Dispatch Workers (dispatch-workflow only) 4. Report The following rules are mandatory and override inferred behavior: - The GitHub Project board is the single source of truth. - - All project writes MUST comply with `project_update_instructions.md`. + - All project writes MUST comply with the Project Update Instructions (in workers). - State reads and state writes MUST NOT be interleaved. - Do NOT infer missing data or invent values. - Do NOT reorganize hierarchy. @@ -2355,18 +1898,13 @@ jobs: if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') runs-on: ubuntu-slim permissions: - contents: read - discussions: write - issues: write - pull-requests: write + actions: write timeout-minutes: 15 env: GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "security-alert-burndown.campaign.g" GH_AW_WORKFLOW_NAME: "Security Alert Burndown" outputs: - process_project_safe_outputs_processed_count: ${{ steps.process_project_safe_outputs.outputs.processed_count }} - process_project_safe_outputs_temporary_project_map: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -2391,27 +1929,12 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process Project-Related Safe Outputs - id: process_project_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: "{\"create_project_status_update\":{\"max\":1},\"update_project\":{\"max\":10}}" - GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_project_handler_manager.cjs'); - await main(); - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3},\"create_issue\":{\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"dispatch_workflow\":{\"max\":3,\"workflows\":[\"code-scanning-fixer\",\"security-fix-pr\",\"dependabot-bundler\",\"secret-scanning-triage\"]},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/docs/src/content/docs/examples/campaigns/security-audit.campaign.md b/docs/src/content/docs/examples/campaigns/security-audit.campaign.md index f12d160528..2cacf80f48 100644 --- a/docs/src/content/docs/examples/campaigns/security-audit.campaign.md +++ b/docs/src/content/docs/examples/campaigns/security-audit.campaign.md @@ -78,7 +78,7 @@ Generates weekly security status reports: The campaign orchestrator will: -1. **Discover** security issues created by worker workflows via tracker-id +1. **Discover** security issues created by worker workflows via tracker-label 2. **Coordinate** by adding discovered items to the project board 3. **Track Progress** using KPIs and project board status fields 4. **Dispatch** worker workflows as needed to maintain campaign momentum diff --git a/docs/src/content/docs/examples/campaigns/security-scanner.md b/docs/src/content/docs/examples/campaigns/security-scanner.md index 6290a8efb1..ce824ff6d0 100644 --- a/docs/src/content/docs/examples/campaigns/security-scanner.md +++ b/docs/src/content/docs/examples/campaigns/security-scanner.md @@ -3,8 +3,16 @@ title: Security Scanner Workflow Example name: Security Scanner description: Scan repositories for security vulnerabilities on: - schedule: - - cron: "0 9 * * 1" # Every Monday at 9 AM + workflow_dispatch: + inputs: + campaign_id: + description: "Campaign identifier" + required: true + type: string + payload: + description: "JSON payload with work details" + required: true + type: string permissions: contents: read security-events: write @@ -33,29 +41,18 @@ Scan the repository for security vulnerabilities and create issues for any findi - CVE ID (if available) - Recommended fix - References and resources - - Labels: security, - - Body can optionally include tracker-id marker for campaign discovery + - Labels: security, , plus the campaign tracker label (defaults to `z_campaign_`) 4. For medium and low-severity findings: - Group similar findings into a single issue - Include all details in the issue description 5. Add comments to existing security issues if new information is discovered -## Output Format (Optional) - -When creating issues, you can optionally include the tracker-id in the issue body to help campaign orchestrators discover and track work items: - -``` -tracker-id: security-scanner -``` - -Note: This is optional. Campaign orchestrators can also discover worker items using labels, so tracker-id is not required. - ## Example Issue **Title**: [Security] SQL Injection vulnerability in user authentication **Body**: -```markdown +````markdown ## Vulnerability Details **Severity**: High @@ -71,16 +68,14 @@ SQL injection vulnerability in user authentication logic allows attackers to byp Use parameterized queries instead of string concatenation: -\```javascript +```javascript const query = 'SELECT * FROM users WHERE username = ? AND password = ?'; db.query(query, [username, hashedPassword]); -\``` +``` ## References - https://cwe.mitre.org/data/definitions/89.html - https://owasp.org/www-community/attacks/SQL_Injection ---- -tracker-id: security-scanner -``` +```` diff --git a/docs/src/content/docs/guides/campaigns/creating-campaigns.md b/docs/src/content/docs/guides/campaigns/creating-campaigns.md index f0dc3c679d..0140f9744d 100644 --- a/docs/src/content/docs/guides/campaigns/creating-campaigns.md +++ b/docs/src/content/docs/guides/campaigns/creating-campaigns.md @@ -75,7 +75,7 @@ Here's what a campaign spec looks like after creation: **Generated [campaign spec](/gh-aw/guides/campaigns/specs/)**: -```yaml +```markdown --- id: security-alert-burndown name: "Security Alert Burndown" @@ -83,16 +83,6 @@ description: "Drive the code security alerts backlog to zero" # GitHub Project for tracking project-url: "https://github.com/orgs/ORG/projects/1" -tracker-label: "campaign:security-alert-burndown" - -# Strategic goals -objective: "Reduce open code security alerts to zero without breaking CI." -kpis: - - id: open_alerts - name: "Open alerts" - priority: primary - direction: "decrease" - target: 0 # Worker workflows to dispatch workflows: @@ -102,9 +92,30 @@ workflows: governance: max-project-updates-per-run: 10 max-comments-per-run: 10 + +owners: + - "@security-team" --- + +# Security Alert Burndown + +## Objective + +Reduce open code security alerts to zero without breaking CI. + +## Key Performance Indicators (KPIs) + +### Primary KPI: Open alerts +- **Baseline**: (fill in) +- **Target**: 0 +- **Time Window**: (fill in) +- **Direction**: Decrease ``` +Notes: +- `tracker-label` is optional; when omitted it defaults to `z_campaign_`. +- Campaign narrative (objective, KPIs, timeline) belongs in the markdown body. + The spec compiles into a campaign orchestrator workflow (`.campaign.lock.yml`) that GitHub Actions executes on schedule. The orchestrator [dispatches workers, tracks outputs, updates the Project board, and reports progress](/gh-aw/guides/campaigns/lifecycle/). ## Next steps diff --git a/docs/src/content/docs/guides/campaigns/getting-started.md b/docs/src/content/docs/guides/campaigns/getting-started.md index a687b3b6cd..18f1efa847 100644 --- a/docs/src/content/docs/guides/campaigns/getting-started.md +++ b/docs/src/content/docs/guides/campaigns/getting-started.md @@ -33,7 +33,7 @@ The pull request creates three components: **Project board** - GitHub Project for tracking campaign progress with custom fields and views. -**Campaign spec** - Configuration file at `.github/workflows/.campaign.md` defining goals, workers, and governance. +**Campaign spec** - Configuration file at `.github/workflows/.campaign.md` defining campaign configuration (project URL, workflows, scope, governance). The markdown body contains narrative goals and success criteria. **Orchestrator workflow** - Compiled workflow at `.github/workflows/.campaign.lock.yml` that executes the campaign logic. diff --git a/docs/src/content/docs/guides/campaigns/lifecycle.md b/docs/src/content/docs/guides/campaigns/lifecycle.md index 3fff82f05e..f27ea78024 100644 --- a/docs/src/content/docs/guides/campaigns/lifecycle.md +++ b/docs/src/content/docs/guides/campaigns/lifecycle.md @@ -50,7 +50,7 @@ Worker workflows in the campaign's `workflows` list must: - Accept `workflow_dispatch` as the **only** trigger - Remove all other triggers (`schedule`, `push`, `pull_request`) -- Label created items with `campaign:` +- Label created items with the campaign tracker label (defaults to `z_campaign_`) - Accept standardized inputs: `campaign_id` (string) and `payload` (string JSON) ```yaml @@ -74,12 +74,13 @@ Workflows not in the `workflows` list can keep their original triggers. The camp ```yaml # Campaign spec -tracker-label: "campaign:security-audit" workflows: - vulnerability-scanner # Orchestrator controls this one # dependency-check runs independently with its cron schedule ``` +`tracker-label` is optional; when omitted it defaults to `z_campaign_`. + ## Discovery and governance Discovery finds items created by workers based on tracker labels. Governance limits control the pace of work. diff --git a/docs/src/content/docs/guides/campaigns/specs.md b/docs/src/content/docs/guides/campaigns/specs.md index 2021603999..7211e3c8b7 100644 --- a/docs/src/content/docs/guides/campaigns/specs.md +++ b/docs/src/content/docs/guides/campaigns/specs.md @@ -9,9 +9,9 @@ Campaign specs are YAML frontmatter configuration files at `.github/workflows/.campaign.md` file with YAML frontmatter plus a markdown body. Most fields have sensible defaults. -```yaml +```markdown --- id: framework-upgrade name: "Framework Upgrade" @@ -53,6 +53,7 @@ Upgrade all services to Framework vNext with zero downtime. - Format: lowercase letters, digits, hyphens only - Example: `security-audit-2025` - Auto-generates defaults for: tracker-label, memory-paths, metrics-glob, cursor-glob +- If omitted, defaults to the filename basename (e.g. `security-audit.campaign.md` → `security-audit`) **name** - Human-friendly display name - Example: `"Security Audit 2025"` @@ -66,6 +67,8 @@ Upgrade all services to Framework vNext with zero downtime. - Format: List of workflow IDs (file names without .md extension) - Example: `["security-scanner", "dependency-fixer"]` +`workflows` is strongly recommended for most campaigns (and `gh aw campaign validate` will flag empty workflows). It can be omitted for campaigns that only do coordination/discovery work. + ## Fields with defaults Many fields have automatic defaults based on the campaign ID: @@ -124,12 +127,14 @@ Override discovery scope when operating across multiple repositories: - See [Governance fields](#governance-fields) below **allowed-orgs** - Organizations campaign can modify + - Format: List of organization names - Alternative to specifying individual `allowed-repos` -**project-github-token** - Custom token for Projects API -- Format: Token expression like `${{ secrets.TOKEN_NAME }}` -- Use when default `GITHUB_TOKEN` lacks Projects permissions +Campaign orchestrators are **dispatch-only by design**: +- The orchestrator can make decisions and coordinate work. +- The orchestrator may only *act* by dispatching allowlisted worker workflows via `safe-outputs.dispatch-workflow`. +- All side effects (Projects, issues/PRs, comments) happen in worker workflows with their own safe-outputs. ## Markdown body content @@ -243,7 +248,7 @@ Common validation errors: The simplest possible campaign: -```yaml +```markdown --- id: security-audit-q1 name: "Security Audit Q1 2025" diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md index 585b346866..d99e661c4d 100644 --- a/docs/src/content/docs/reference/glossary.md +++ b/docs/src/content/docs/reference/glossary.md @@ -285,7 +285,7 @@ See the [ProjectOps Guide](/gh-aw/examples/issue-pr-events/projectops/) for impl A finite, enterprise-scale initiative with explicit ownership, approval gates, and executive visibility. Agentic campaigns orchestrate business outcomes (security remediation, dependency updates, compliance enforcement) across multiple repositories with governance, accountability, and ROI tracking. -Campaigns use `.campaign.md` specification files to define objectives, KPIs, and governance. The orchestrator discovers and tracks work, executes workflows sequentially, and can create missing workflows if needed. +Campaigns use `.campaign.md` files where YAML frontmatter defines configuration (project URL, workflows, scope, governance) and the markdown body defines narrative context (objective, KPIs, timeline). The orchestrator discovers and tracks work, executes workflows sequentially, and can create missing workflows if needed. ```yaml # Campaign example @@ -294,10 +294,19 @@ project-url: "https://github.com/orgs/ORG/projects/1" workflows: - framework-scanner # Will be created if missing - framework-upgrader # Will be created if missing -objective: "Upgrade all services to Framework vNext" -kpis: - - name: "Services upgraded" - target: 50 +``` + +```markdown +# Framework Upgrade + +## Objective + +Upgrade all services to Framework vNext. + +## KPIs + +### Primary KPI: Services upgraded +- **Target**: 50 ``` **Key characteristics:** diff --git a/pkg/campaign/loader.go b/pkg/campaign/loader.go index ce1185faeb..8187703034 100644 --- a/pkg/campaign/loader.go +++ b/pkg/campaign/loader.go @@ -258,21 +258,11 @@ func CreateSpecSkeleton(rootDir, id string, force bool) (string, error) { buf.WriteString("# discovery-orgs:\n") buf.WriteString("# - myorg\n") buf.WriteString("\n") - buf.WriteString("objective: \"Reduce security vulnerabilities across all repositories\"\n") buf.WriteString("workflows:\n") buf.WriteString(" - vulnerability-scanner\n") buf.WriteString(" - dependency-updater\n") buf.WriteString("owners:\n") buf.WriteString(" - @security-team\n") - buf.WriteString("kpis:\n") - buf.WriteString(" - name: \"Critical vulnerabilities resolved\"\n") - buf.WriteString(" priority: primary\n") - buf.WriteString(" unit: count\n") - buf.WriteString(" baseline: 0\n") - buf.WriteString(" target: 50\n") - buf.WriteString(" time-window-days: 30\n") - buf.WriteString(" direction: increase\n") - buf.WriteString(" source: code_security\n") buf.WriteString("```\n") // Use restrictive permissions (0644) for proper git tracking diff --git a/pkg/campaign/orchestrator.go b/pkg/campaign/orchestrator.go index a30d5ccddf..49f9706689 100644 --- a/pkg/campaign/orchestrator.go +++ b/pkg/campaign/orchestrator.go @@ -370,46 +370,13 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W appendPromptSection(markdownBuilder, "CLOSING INSTRUCTIONS (HIGHEST PRIORITY)", closingInstructions) } - // Enable safe outputs needed for campaign coordination. + // Campaign orchestrators are dispatch-only: they may only dispatch allowlisted + // workflows via the dispatch-workflow safe output. All side effects (Projects, + // issues/PRs, comments) must be performed by dispatched worker workflows. + // // Note: Campaign orchestrators intentionally omit explicit `permissions:` from // the generated markdown; safe-output jobs have their own scoped permissions. - maxComments := 10 - maxProjectUpdates := 10 - if spec.Governance != nil { - if spec.Governance.MaxCommentsPerRun > 0 { - maxComments = spec.Governance.MaxCommentsPerRun - } - if spec.Governance.MaxProjectUpdatesPerRun > 0 { - maxProjectUpdates = spec.Governance.MaxProjectUpdatesPerRun - } - } - safeOutputs := &workflow.SafeOutputsConfig{} - // Allow creating the Epic issue for the campaign (max: 1, only created once). - safeOutputs.CreateIssues = &workflow.CreateIssuesConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 1}} - // Allow commenting on related issues/PRs as part of campaign coordination. - safeOutputs.AddComments = &workflow.AddCommentsConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: maxComments}} - // Allow updating the campaign's GitHub Project dashboard. - updateProjectConfig := &workflow.UpdateProjectConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: maxProjectUpdates}} - // If the campaign spec specifies a custom GitHub token for Projects v2 operations, - // pass it to the update-project configuration. - if strings.TrimSpace(spec.ProjectGitHubToken) != "" { - updateProjectConfig.GitHubToken = strings.TrimSpace(spec.ProjectGitHubToken) - orchestratorLog.Printf("Campaign orchestrator '%s' configured with custom GitHub token for update-project", spec.ID) - } - safeOutputs.UpdateProjects = updateProjectConfig - - // Allow creating project status updates for campaign summaries. - statusUpdateConfig := &workflow.CreateProjectStatusUpdateConfig{BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 1}} - // Use the same custom GitHub token for status updates as for project operations. - if strings.TrimSpace(spec.ProjectGitHubToken) != "" { - statusUpdateConfig.GitHubToken = strings.TrimSpace(spec.ProjectGitHubToken) - orchestratorLog.Printf("Campaign orchestrator '%s' configured with custom GitHub token for create-project-status-update", spec.ID) - } - safeOutputs.CreateProjectStatusUpdates = statusUpdateConfig - - // Add dispatch_workflow if workflows are configured - // This allows the orchestrator to dispatch worker workflows for the campaign if len(spec.Workflows) > 0 { dispatchWorkflowConfig := &workflow.DispatchWorkflowConfig{ BaseSafeOutputConfig: workflow.BaseSafeOutputConfig{Max: 3}, @@ -419,7 +386,7 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W orchestratorLog.Printf("Campaign orchestrator '%s' configured with dispatch_workflow for %d workflows", spec.ID, len(spec.Workflows)) } - orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with safe outputs enabled", spec.ID) + orchestratorLog.Printf("Campaign orchestrator '%s' built successfully with dispatch-workflow safe output", spec.ID) // Extract file-glob patterns from memory-paths or metrics-glob to support // multiple directory structures (e.g., both dated "campaign-id-*/**" and non-dated "campaign-id/**") @@ -438,6 +405,21 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W orchestratorLog.Printf("Campaign orchestrator '%s' using default engine: %s", spec.ID, engineID) } + tools := map[string]any{ + "repo-memory": []any{ + map[string]any{ + "id": "campaigns", + "branch-name": "memory/campaigns", + "file-glob": convertStringsToAny(fileGlobPatterns), + "campaign-id": spec.ID, + }, + }, + "bash": []any{"*"}, + "edit": nil, + } + // Deliberately omit GitHub tool access from orchestrators. All writes and GitHub + // API operations should be performed by dispatched worker workflows. + data := &workflow.WorkflowData{ Name: name, Description: description, @@ -454,21 +436,7 @@ func BuildOrchestrator(spec *CampaignSpec, campaignFilePath string) (*workflow.W EngineConfig: &workflow.EngineConfig{ ID: engineID, }, - Tools: map[string]any{ - "github": map[string]any{ - "toolsets": []any{"default", "actions", "code_security"}, - }, - "repo-memory": []any{ - map[string]any{ - "id": "campaigns", - "branch-name": "memory/campaigns", - "file-glob": convertStringsToAny(fileGlobPatterns), - "campaign-id": spec.ID, - }, - }, - "bash": []any{"*"}, - "edit": nil, - }, + Tools: tools, SafeOutputs: safeOutputs, } diff --git a/pkg/campaign/orchestrator_test.go b/pkg/campaign/orchestrator_test.go index c97d94a95f..16c77561d1 100644 --- a/pkg/campaign/orchestrator_test.go +++ b/pkg/campaign/orchestrator_test.go @@ -132,6 +132,45 @@ func TestBuildOrchestrator_WorkflowsInDiscovery(t *testing.T) { }) } +func TestBuildOrchestrator_DispatchOnlyPolicy(t *testing.T) { + withTempGitRepoWithInstalledCampaignPrompts(t, func(_ string) { + spec := &CampaignSpec{ + ID: "dispatch-only-campaign", + Name: "Dispatch Only Campaign", + Description: "Campaign orchestrator restricted to dispatch-workflow", + ProjectURL: "https://github.com/orgs/test/projects/1", + Workflows: []string{"worker-a", "worker-b"}, + MemoryPaths: []string{"memory/campaigns/dispatch-only-campaign/**"}, + } + + mdPath := ".github/workflows/dispatch-only-campaign.campaign.md" + data, _ := BuildOrchestrator(spec, mdPath) + if data == nil { + t.Fatalf("expected non-nil WorkflowData") + } + + if data.SafeOutputs == nil { + t.Fatalf("expected SafeOutputs to be set") + } + if data.SafeOutputs.DispatchWorkflow == nil { + t.Fatalf("expected dispatch-workflow safe output to be enabled") + } + if len(data.SafeOutputs.DispatchWorkflow.Workflows) != 2 { + t.Fatalf("expected 2 allowlisted workflows, got %d", len(data.SafeOutputs.DispatchWorkflow.Workflows)) + } + if data.SafeOutputs.CreateIssues != nil || data.SafeOutputs.AddComments != nil || data.SafeOutputs.UpdateProjects != nil || data.SafeOutputs.CreateProjectStatusUpdates != nil { + t.Fatalf("expected dispatch-only orchestrator to omit non-dispatch safe outputs") + } + + // Dispatch-only policy should not grant GitHub tool access to the agent. + if data.Tools != nil { + if _, ok := data.Tools["github"]; ok { + t.Fatalf("expected dispatch-only orchestrator to omit github tools") + } + } + }) +} + func TestBuildOrchestrator_TrackerIDMonitoring(t *testing.T) { withTempGitRepoWithInstalledCampaignPrompts(t, func(_ string) { spec := &CampaignSpec{ @@ -174,15 +213,15 @@ func TestBuildOrchestrator_TrackerIDMonitoring(t *testing.T) { t.Errorf("expected markdown to contain core principles section, got: %q", data.MarkdownContent) } - // Verify separation of steps (read / decide / write / report) + // Verify separation of steps (read / decide / dispatch / report) if !strings.Contains(data.MarkdownContent, "Step 1") || !strings.Contains(data.MarkdownContent, "Read State") { t.Errorf("expected markdown to contain Step 1 Read State, got: %q", data.MarkdownContent) } if !strings.Contains(data.MarkdownContent, "Step 2") || !strings.Contains(data.MarkdownContent, "Make Decisions") { t.Errorf("expected markdown to contain Step 2 Make Decisions, got: %q", data.MarkdownContent) } - if !strings.Contains(data.MarkdownContent, "Step 3") || !strings.Contains(data.MarkdownContent, "Write State") { - t.Errorf("expected markdown to contain Step 3 Write State, got: %q", data.MarkdownContent) + if !strings.Contains(data.MarkdownContent, "Step 3") || !strings.Contains(data.MarkdownContent, "Dispatch Workers") { + t.Errorf("expected markdown to contain Step 3 Dispatch Workers, got: %q", data.MarkdownContent) } if !strings.Contains(data.MarkdownContent, "Step 4") || !strings.Contains(data.MarkdownContent, "Report") { t.Errorf("expected markdown to contain Step 4 Report, got: %q", data.MarkdownContent) @@ -190,80 +229,7 @@ func TestBuildOrchestrator_TrackerIDMonitoring(t *testing.T) { }) } -func TestBuildOrchestrator_GitHubToken(t *testing.T) { - withTempGitRepoWithInstalledCampaignPrompts(t, func(_ string) { - t.Run("with custom github token", func(t *testing.T) { - spec := &CampaignSpec{ - ID: "test-campaign-with-token", - Name: "Test Campaign", - Description: "A test campaign with custom GitHub token", - ProjectURL: "https://github.com/orgs/test/projects/1", - Workflows: []string{"test-workflow"}, - ProjectGitHubToken: "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}", - } - - mdPath := ".github/workflows/test-campaign.campaign.md" - data, _ := BuildOrchestrator(spec, mdPath) - - if data == nil { - t.Fatalf("expected non-nil WorkflowData") - } - - // Verify that SafeOutputs is configured - if data.SafeOutputs == nil { - t.Fatalf("expected SafeOutputs to be configured") - } - - // Verify that UpdateProjects is configured - if data.SafeOutputs.UpdateProjects == nil { - t.Fatalf("expected UpdateProjects to be configured") - } - - // Verify that the GitHubToken is set - if data.SafeOutputs.UpdateProjects.GitHubToken != "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}" { - t.Errorf("expected GitHubToken to be %q, got %q", - "${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}", - data.SafeOutputs.UpdateProjects.GitHubToken) - } - }) - - t.Run("without custom github token", func(t *testing.T) { - spec := &CampaignSpec{ - ID: "test-campaign-no-token", - Name: "Test Campaign", - Description: "A test campaign without custom GitHub token", - ProjectURL: "https://github.com/orgs/test/projects/1", - Workflows: []string{"test-workflow"}, - // ProjectGitHubToken is intentionally omitted - } - - mdPath := ".github/workflows/test-campaign.campaign.md" - data, _ := BuildOrchestrator(spec, mdPath) - - if data == nil { - t.Fatalf("expected non-nil WorkflowData") - } - - // Verify that SafeOutputs is configured - if data.SafeOutputs == nil { - t.Fatalf("expected SafeOutputs to be configured") - } - - // Verify that UpdateProjects is configured - if data.SafeOutputs.UpdateProjects == nil { - t.Fatalf("expected UpdateProjects to be configured") - } - - // Verify that the GitHubToken is empty when not specified - if data.SafeOutputs.UpdateProjects.GitHubToken != "" { - t.Errorf("expected GitHubToken to be empty when not specified, got %q", - data.SafeOutputs.UpdateProjects.GitHubToken) - } - }) - }) -} - -func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) { +func TestBuildOrchestrator_GovernanceDoesNotGrantWriteSafeOutputs(t *testing.T) { withTempGitRepoWithInstalledCampaignPrompts(t, func(_ string) { spec := &CampaignSpec{ ID: "test-campaign", @@ -281,14 +247,14 @@ func TestBuildOrchestrator_GovernanceOverridesSafeOutputMaxima(t *testing.T) { if data == nil { t.Fatalf("expected non-nil WorkflowData") } - if data.SafeOutputs == nil || data.SafeOutputs.AddComments == nil || data.SafeOutputs.UpdateProjects == nil { - t.Fatalf("expected SafeOutputs add-comment and update-project to be configured") + if data.SafeOutputs == nil || data.SafeOutputs.DispatchWorkflow == nil { + t.Fatalf("expected dispatch-workflow safe output to be enabled") } - if data.SafeOutputs.AddComments.Max != 3 { - t.Fatalf("unexpected add-comment max: got %d, want %d", data.SafeOutputs.AddComments.Max, 3) + if data.SafeOutputs.DispatchWorkflow.Max != 3 { + t.Fatalf("unexpected dispatch-workflow max: got %d, want %d", data.SafeOutputs.DispatchWorkflow.Max, 3) } - if data.SafeOutputs.UpdateProjects.Max != 4 { - t.Fatalf("unexpected update-project max: got %d, want %d", data.SafeOutputs.UpdateProjects.Max, 4) + if data.SafeOutputs.CreateIssues != nil || data.SafeOutputs.AddComments != nil || data.SafeOutputs.UpdateProjects != nil || data.SafeOutputs.CreateProjectStatusUpdates != nil { + t.Fatalf("expected orchestrator to omit non-dispatch safe outputs regardless of governance") } }) } diff --git a/pkg/campaign/schemas/campaign_spec_schema.json b/pkg/campaign/schemas/campaign_spec_schema.json index 56a150129e..79d0a4bc46 100644 --- a/pkg/campaign/schemas/campaign_spec_schema.json +++ b/pkg/campaign/schemas/campaign_spec_schema.json @@ -27,11 +27,6 @@ "pattern": "/projects/", "minLength": 1 }, - "project-github-token": { - "type": "string", - "description": "Optional GitHub token expression used for GitHub Projects v2 operations (e.g., ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }})", - "minLength": 1 - }, "version": { "type": "string", "description": "Spec version (e.g., v1)", diff --git a/pkg/campaign/spec.go b/pkg/campaign/spec.go index 2321b01a26..17633a91ba 100644 --- a/pkg/campaign/spec.go +++ b/pkg/campaign/spec.go @@ -93,12 +93,6 @@ type CampaignSpec struct { // enforced by validation in the future. AllowedSafeOutputs []string `yaml:"allowed-safe-outputs,omitempty" json:"allowed_safe_outputs,omitempty" console:"header:Allowed Safe Outputs,omitempty,maxlen:30"` - // ProjectGitHubToken is an optional GitHub token expression (e.g., - // ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }}) used for GitHub Projects v2 - // operations. When specified, this token is passed to the update-project - // safe output configuration in the generated orchestrator workflow. - ProjectGitHubToken string `yaml:"project-github-token,omitempty" json:"project_github_token,omitempty" console:"header:Project Token,omitempty,maxlen:30"` - // Governance configures lightweight pacing and opt-out policies for campaign // orchestrator workflows. These guardrails are primarily enforced through // generated prompts and safe-output maxima. diff --git a/pkg/campaign/template_test.go b/pkg/campaign/template_test.go index 187d778e7d..3ad54a070d 100644 --- a/pkg/campaign/template_test.go +++ b/pkg/campaign/template_test.go @@ -28,7 +28,7 @@ func TestRenderOrchestratorInstructions(t *testing.T) { "Step 2", "Make Decisions", "Step 3", - "Write State", + "Dispatch Workers", "Step 4", "Report", }, @@ -37,7 +37,6 @@ func TestRenderOrchestratorInstructions(t *testing.T) { name: "explicit state management", data: CampaignPromptData{}, shouldContain: []string{ - "Read current GitHub Project board state", "Parse discovered items from the manifest", "Discovery cursor is maintained automatically", "Determine desired `status`", @@ -158,7 +157,7 @@ func TestRenderClosingInstructions(t *testing.T) { "Execute all four steps in strict order", "Read State (no writes)", "Make Decisions (no writes)", - "Write State (update-project only)", + "Dispatch Workers (dispatch-workflow only)", "Report", "Workers are immutable and campaign-agnostic", "GitHub Project board is the single source of truth", diff --git a/pkg/campaign/validation.go b/pkg/campaign/validation.go index d5365ee858..7d45a579e7 100644 --- a/pkg/campaign/validation.go +++ b/pkg/campaign/validation.go @@ -309,7 +309,6 @@ func ValidateSpecWithSchema(spec *CampaignSpec) []string { Name string `json:"name"` Description string `json:"description,omitempty"` ProjectURL string `json:"project-url,omitempty"` - ProjectGitHubToken string `json:"project-github-token,omitempty"` Version string `json:"version,omitempty"` Workflows []string `json:"workflows,omitempty"` DiscoveryRepos []string `json:"discovery-repos,omitempty"` @@ -334,7 +333,6 @@ func ValidateSpecWithSchema(spec *CampaignSpec) []string { Name: spec.Name, Description: spec.Description, ProjectURL: spec.ProjectURL, - ProjectGitHubToken: spec.ProjectGitHubToken, Version: spec.Version, Workflows: spec.Workflows, DiscoveryRepos: spec.DiscoveryRepos, diff --git a/pkg/cli/compile_campaign_orchestrator_test.go b/pkg/cli/compile_campaign_orchestrator_test.go index 156392d60f..eda2c9d90f 100644 --- a/pkg/cli/compile_campaign_orchestrator_test.go +++ b/pkg/cli/compile_campaign_orchestrator_test.go @@ -95,6 +95,14 @@ func TestGenerateAndCompileCampaignOrchestrator(t *testing.T) { t.Errorf("expected generated markdown to set engine: claude") } + // Verify dispatch-workflow safe output is rendered (used for orchestration) + if !strings.Contains(mdStr, "dispatch-workflow:") { + t.Errorf("expected generated markdown to include dispatch-workflow safe output") + } + if !strings.Contains(mdStr, "example-workflow") { + t.Errorf("expected generated markdown to include allowlisted workflow 'example-workflow'") + } + // Verify that the Source comment exists and contains a relative path (not absolute) if !strings.Contains(mdStr, "