diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index f94592e76..ef6728a5d 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -996,6 +996,12 @@ "description": "Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications.", "version": "1.0.0" }, + { + "name": "shepherd-task", + "source": "plugins/shepherd-task", + "description": "End-to-end task shepherding system that automates the full lifecycle of GitHub issues through Copilot coding agent assignment, CI approval, code review resolution, and merge. Includes orchestration scripts for both Bash and PowerShell.", + "version": "1.0.0" + }, { "name": "site-studio", "source": "extensions/site-studio", diff --git a/docs/README.plugins.md b/docs/README.plugins.md index bff4a4b4d..9714d77c0 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -87,6 +87,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [rust-mcp-development](../plugins/rust-mcp-development/README.md) | Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations. | 2 items | rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp | | [salesforce-development](../plugins/salesforce-development/README.md) | Complete Salesforce agentic development environment covering Apex & Triggers, Flow automation, Lightning Web Components, Aura components, and Visualforce pages. | 7 items | salesforce, apex, triggers, lwc, aura, flow, visualforce, crm, salesforce-dx | | [security-best-practices](../plugins/security-best-practices/README.md) | Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications. | 1 items | security, accessibility, performance, code-quality, owasp, a11y, optimization, best-practices | +| [shepherd-task](../plugins/shepherd-task/README.md) | End-to-end task shepherding system that automates the full lifecycle of GitHub issues through Copilot coding agent assignment, CI approval, code review resolution, and merge. Includes orchestration scripts for both Bash and PowerShell. | 3 items | shepherd, task-automation, copilot-coding-agent, ci-cd, code-review, github-actions, workflow-approval, yolo | | [skill-image-gen](../plugins/skill-image-gen/README.md) | Generate images using AI directly from your coding workflow. Supports OpenAI (gpt-image-2) and Google Gemini. BYO API key — the skill guides you through setup on first use. | 1 items | image-generation, openai, gemini, ai, art, sprites, textures, icons | | [software-engineering-team](../plugins/software-engineering-team/README.md) | 7 specialized agents covering the full software development lifecycle from UX design and architecture to security and DevOps. | 7 items | team, enterprise, security, devops, ux, architecture, product, ai-ethics | | [structured-autonomy](../plugins/structured-autonomy/README.md) | Premium planning, thrifty implementation | 3 items | | diff --git a/docs/README.skills.md b/docs/README.skills.md index fb94c86e3..37853f425 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -356,6 +356,9 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [security-review](../skills/security-review/SKILL.md)
`gh skills install github/awesome-copilot security-review` | AI-powered codebase security scanner that reasons about code like a security researcher — tracing data flows, understanding component interactions, and catching vulnerabilities that pattern-matching tools miss. Use this skill when asked to scan code for security vulnerabilities, find bugs, check for SQL injection, XSS, command injection, exposed API keys, hardcoded secrets, insecure dependencies, access control issues, or any request like "is my code secure?", "review for security issues", "audit this codebase", or "check for vulnerabilities". Covers injection flaws, authentication and access control bugs, secrets exposure, weak cryptography, insecure dependencies, and business logic issues across JavaScript, TypeScript, Python, Java, PHP, Go, Ruby, and Rust. | `references/language-patterns.md`
`references/report-format.md`
`references/secret-patterns.md`
`references/vuln-categories.md`
`references/vulnerable-packages.md` | | [semantic-kernel](../skills/semantic-kernel/SKILL.md)
`gh skills install github/awesome-copilot semantic-kernel` | Create, update, refactor, explain, or review Semantic Kernel solutions using shared guidance plus language-specific references for .NET and Python. | `references/dotnet.md`
`references/python.md` | | [setup-my-iq](../skills/setup-my-iq/SKILL.md)
`gh skills install github/awesome-copilot setup-my-iq` | Create, set up, or update the personal context portfolio: structured markdown files describing
who you are, how you work, your teams, and your tool/ADO configuration. Runs the interview
workflow for first-time setup and targeted edits for updates.

Trigger this skill when the user asks to: set up their context, create or update their context
portfolio, "create my IQ", "set up my IQ", edit their profile, add/remove a stakeholder,
update ADO config, change team info, update pillars, or set up any plugin configuration.
Trigger when another skill fails to find context (missing files or TODO markers) and needs
context populated. Also trigger when the user mentions a context change in passing
(e.g., "my manager changed", "we added someone to the team") to offer a context file update.

Do NOT trigger for read-only questions like "who's on my team?" or "what's my ADO config?".
Those are answered directly from the context files referenced in the loaded custom
instructions; no skill is needed. | `assets/templates` | +| [shepherd-task-approve-workflows-and-wait-for-completion](../skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md)
`gh skills install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion` | Use this skill to approve pending workflow runs on a PR branch and wait for them to complete. | None | +| [shepherd-task-from-assignment-to-ready](../skills/shepherd-task-from-assignment-to-ready/SKILL.md)
`gh skills install github/awesome-copilot shepherd-task-from-assignment-to-ready` | Use this skill to shepherd a child Task issue from 'assigned to Copilot' through CI approval and review-agent feedback resolution, stopping just before marking the PR as **Ready for review**. | None | +| [shepherd-task-from-ready-to-merged-to-base](../skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md)
`gh skills install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base` | Use this skill to shepherd a task PR from 'Ready for review' through Copilot code review, local comment resolution, and merge to the specified base branch. | None | | [shuffle-json-data](../skills/shuffle-json-data/SKILL.md)
`gh skills install github/awesome-copilot shuffle-json-data` | Shuffle repetitive JSON objects safely by validating schema consistency before randomising entries. | None | | [slang-shader-engineer](../skills/slang-shader-engineer/SKILL.md)
`gh skills install github/awesome-copilot slang-shader-engineer` | Use when working with Slang shaders, shader modules, HLSL-compatible GPU code, graphics pipelines, compute shaders, tessellation, ray tracing, parameter blocks, generics, interfaces, capabilities, cross-compilation, shader optimization, shader review, or C++ engine integration for Slang. Trigger on any mention of Slang, .slang files, slangc, SPIR-V from Slang, Slang modules, [shader("compute")], [shader("vertex")], or requests to write/review/refactor shader code with modern language features. Also trigger for Slang-to-HLSL/GLSL/Metal/CUDA cross-compile questions, or when the user says "shader" alongside "generics", "interfaces", "parameter blocks", "autodiff", or "capabilities". | `references/language-reference.md`
`references/rules-and-patterns.md`
`references/slang-documentation-full.md` | | [snowflake-semanticview](../skills/snowflake-semanticview/SKILL.md)
`gh skills install github/awesome-copilot snowflake-semanticview` | Create, alter, and validate Snowflake semantic views using Snowflake CLI (snow). Use when asked to build or troubleshoot semantic views/semantic layer definitions with CREATE/ALTER SEMANTIC VIEW, to validate semantic-view DDL against Snowflake via CLI, or to guide Snowflake CLI installation and connection setup. | None | diff --git a/plugins/shepherd-task/.github/plugin/plugin.json b/plugins/shepherd-task/.github/plugin/plugin.json new file mode 100644 index 000000000..9f6747dfb --- /dev/null +++ b/plugins/shepherd-task/.github/plugin/plugin.json @@ -0,0 +1,25 @@ +{ + "name": "shepherd-task", + "description": "End-to-end task shepherding system that automates the full lifecycle of GitHub issues through Copilot coding agent assignment, CI approval, code review resolution, and merge. Includes orchestration scripts for both Bash and PowerShell.", + "version": "1.0.0", + "author": { + "name": "Ed Burns" + }, + "repository": "https://github.com/github/awesome-copilot", + "license": "MIT", + "keywords": [ + "shepherd", + "task-automation", + "copilot-coding-agent", + "ci-cd", + "code-review", + "github-actions", + "workflow-approval", + "yolo" + ], + "skills": [ + "./skills/shepherd-task-approve-workflows-and-wait-for-completion/", + "./skills/shepherd-task-from-assignment-to-ready/", + "./skills/shepherd-task-from-ready-to-merged-to-base/" + ] +} diff --git a/plugins/shepherd-task/README.md b/plugins/shepherd-task/README.md new file mode 100644 index 000000000..2d9614f63 --- /dev/null +++ b/plugins/shepherd-task/README.md @@ -0,0 +1,232 @@ +## Summary + +This PR adds a new **shepherd-task** plugin that provides an end-to-end system for implementing a specification described as a well-specified list of GitHub issues using an opinionated choice of the following agents and tools. + +1. Copilot Coding Agent (CCA). +2. Copilot Code Review Agent (CCRA). +3. GitHub CLI `gh`. +4. GitHub Copilot CLI `copilot`. + +It shepherds tasks from assignment through CI approval, code review resolution, and merge — all orchestrated via `copilot --yolo` sessions driven by shell scripts. + +## Enabling assumptions + +1. The work will happen within a single GitHub repository. +1. The repository has GitHub Copilot Coding Agent and Code Review Agent installed and properly configured. +1. User has `gh` CLI installed at version 2.45.0 or later and is signed in. +1. USer has permissions on the repository to: + 1. Assign issues to `Copilot`. + 1. Push to the repository. +1. User has `copilot` CLI installed at version `1.0.72-0` or later and is signed in. +1. User has a local development environment suitable for using Copilot CLI and `git worktree` to do development work, including running tests. +1. User accepts that all of the work is to be done on a non-`main` base branch in the repository. +1. User accepts this plugin will invoke `copilot --yolo`. +1. The specification for the job to be done is encoded in an ordered set of GitHub issues in the issue tracker of the GitHub repository. +1. User has `jq` installed and accessible to Copilot CLI. + +## Motivation + +When using the Copilot coding agent to implement tasks from a backlog, there is a significant amount of repetitive supervisory work: assigning the issue, waiting for the PR, approving workflow runs, interpreting CI failures, requesting changes, waiting for fixes, resolving code review comments, and finally merging. This system automates that entire loop, allowing a developer to hand off a list of issue numbers and walk away. + +A successful run of the system will result in the work being merged to the specified non-`main` base branch, so that a further PR can be made from that branch with greater human oversight. + +This plugin was developed and battle-tested in the `github/copilot-sdk` repository, where it has successfully shepherded dozens of tasks through the full lifecycle. It is being contributed here so the broader community can adopt, adapt, and improve it. + +## Architecture + +The system has two layers: + +1. **Skills** (Copilot-invocable instructions) — three SKILL.md files that Copilot reads during `--yolo` sessions to know what to do at each phase: + - `shepherd-task-from-assignment-to-ready`: Assigns an issue to @copilot with a specific base branch, waits for PR creation, iterates through CI approval and review-agent feedback (up to 20 iterations). + - `shepherd-task-from-ready-to-merged-to-base`: Marks the PR as ready, waits for Copilot code review, resolves comments locally in a git worktree, pushes fixes, and merges. + - `shepherd-task-approve-workflows-and-wait-for-completion`: Reusable sub-skill that approves pending `action_required` workflow runs and blocks until completion. + +2. **Orchestration scripts** (user-facing entry points) — shell scripts that launch `copilot --yolo` sessions, verify outcomes with `gh` CLI between phases, and provide idempotent retry semantics. Both Bash and PowerShell variants are included for cross-platform support. + +The plugin manifest (`plugin.json`) ties these together as a discoverable unit in the marketplace. + +## Sample invocation + +Bash + +```bash +. ./plugins/shepherd-task/scripts/shepherd-task-given-list.sh 13,4,5,6,7,8,9,10,11 edburns/2-build-out-demo edburns/Build26-BRK206-your-agent-anywhere-multiclient-multidevice-with-github-copilot-sdk +``` + +PowerShell + +```powershell +. .\plugins\shepherd-task\scripts\shepherd-task-given-list.ps1 13,4,5,6,7,8,9,10,11 edburns/2-build-out-demo edburns/Build26-BRK206-your-agent-anywhere-multiclient-multidevice-with-github-copilot-sdk +``` + +This invocation was used to implement the issues in this Epic: https://github.com/edburns/Build26-BRK206-your-agent-anywhere-multiclient-multidevice-with-github-copilot-sdk/issues/2 . + +## Installation + +### Step 1: Install skills (via `gh skill`) + +```bash +gh skill install github/awesome-copilot shepherd-task-from-assignment-to-ready +gh skill install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base +gh skill install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion +``` + +Requires GitHub CLI v2.90.0+. + +### Step 2: Install orchestration scripts + +The scripts are not installable via `gh skill`. Either: +- Clone this repository and run from `plugins/shepherd-task/scripts/`, or +- Use the bundled installer to copy only the scripts into your repo: + + Bash + + ```bash + . ./plugins/shepherd-task/scripts/install-task-shepherd.sh /path/to/your/repo + ``` + + PowerShell + + ```powershell + . .\plugins\shepherd-task\scripts\install-task-shepherd.ps1 \path\to\your\repo + ``` + +The installer copies **only** the orchestration scripts — it does not duplicate the skills (those are managed by `gh skill install` above). + +## Uninstallation + +There is no `gh skill uninstall` command. To remove installed skills, manually delete the skill directories from your agent's skills location. + +- Use the bundled uninstaller to remove the scripts from your repo. + + Bash + + ```bash + . ./plugins/shepherd-task/scripts/uninstall-task-shepherd.sh /path/to/your/repo + ``` + + PowerShell + + ```powershell + . .\plugins\shepherd-task\scripts\uninstall-task-shepherd.ps1 \path\to\your\repo + ``` + + +## How it works + +``` +User runs: shepherd-task-given-list.sh "1841,1842,1843" feature-branch owner/repo + └─ For each issue, calls shepherd-task.sh + ├─ Phase 1: echo prompt | copilot --yolo (invokes shepherd-task-from-assignment-to-ready) + │ └─ skill loops: assign → wait for PR → approve workflows → fix CI → repeat + ├─ Verify: gh pr checks, no unresolved reviews + ├─ Phase 2: echo prompt | copilot --yolo (invokes shepherd-task-from-ready-to-merged-to-base) + │ └─ skill loops: mark ready → resolve review comments locally → push → merge + └─ Verify: PR merged to non-main base branch, issue closed +``` + +## Key design decisions + +- **Scripts verify state independently** — they don't trust Copilot exit codes; they use `gh` CLI to confirm PR existence, CI status, and merge state between phases. +- **Idempotent** — if a PR already exists for the issue, Phase 1 is skipped. If already merged, Phase 2 is skipped. Safe to re-run after failures. +- **Local review resolution** — Phase 2 resolves Copilot code review comments in a local git worktree rather than asking the remote agent to fix itself, giving more reliable results. +- **Cross-platform** — every script has both `.sh` and `.ps1` variants. + +## Per-file manifest + +### Plugin + +| File | Purpose | +|------|---------| +| `plugins/shepherd-task/.github/plugin/plugin.json` | Plugin manifest grouping the three skills; includes metadata, keywords, and version | +| `plugins/shepherd-task/README.md` | User-facing documentation for the plugin | + +### Orchestration scripts + +| File | Purpose | +|------|---------| +| `plugins/shepherd-task/scripts/shepherd-task-given-list.sh` | Bash: iterates a comma-separated list of issue numbers, invoking shepherd-task.sh for each | +| `plugins/shepherd-task/scripts/shepherd-task-given-list.ps1` | PowerShell equivalent | +| `plugins/shepherd-task/scripts/shepherd-task.sh` | Bash: orchestrates Phase 1 + Phase 2 for a single task issue with state verification | +| `plugins/shepherd-task/scripts/shepherd-task.ps1` | PowerShell equivalent | +| `plugins/shepherd-task/scripts/shepherd-task-inspect-json.sh` | Bash: debug utility to inspect copilot JSON session logs | +| `plugins/shepherd-task/scripts/shepherd-task-inspect-json.ps1` | PowerShell equivalent | +| `plugins/shepherd-task/scripts/install-task-shepherd.sh` | Bash: copies the full shepherd system into another repository | +| `plugins/shepherd-task/scripts/install-task-shepherd.ps1` | PowerShell equivalent | + +### Skills + +| File | Purpose | +|------|---------| +| `skills/shepherd-task-from-assignment-to-ready/SKILL.md` | Skill instructions for Phase 1: assign to Copilot → PR created → CI passing → no unresolved reviews | +| `skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md` | Skill instructions for Phase 2: mark ready → resolve code review → merge to base branch | +| `skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md` | Reusable sub-skill: approve `action_required` workflow runs and wait for completion | + +### Generated files (updated by `npm run build`) + +| File | Purpose | +|------|---------| +| `.github/plugin/marketplace.json` | Marketplace registry updated with new shepherd-task plugin entry | +| `docs/README.plugins.md` | Plugin listing updated to include shepherd-task | +| `docs/README.skills.md` | Skills listing updated with the three new shepherd-task skills | + +## Validation + +``` +$ npm run skill:validate +✅ shepherd-task-approve-workflows-and-wait-for-completion is valid +✅ shepherd-task-from-assignment-to-ready is valid +✅ shepherd-task-from-ready-to-merged-to-base is valid + +$ npm run plugin:validate +✅ shepherd-task is valid +``` + +## Testing + +This system has been used in production on `github/copilot-sdk` to shepherd implementation tasks for Java SDK development. It handles the common failure modes: workflow approval gates, CI flakiness, Copilot code review comment iteration, merge conflicts, and base branch drift. + +## Pull Request Checklist + +- [x] I have read and followed the [CONTRIBUTING.md](https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md) guidelines. +- [x] I have read and followed the [Guidance for submissions involving paid services](https://github.com/github/awesome-copilot/discussions/968). +- [x] My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory. +- [x] The file follows the required naming convention. +- [x] The content is clearly structured and follows the example format. +- [x] I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot. +- [x] I have run `npm start` and verified that `README.md` is up to date. +- [ ] I am targeting the `main` branch for this pull request. + +--- + +## Description + + + +--- + +## Type of Contribution + +- [ ] New instruction file. +- [ ] New prompt file. +- [ ] New agent file. +- [x] New plugin. +- [x] New skill file. +- [ ] New agentic workflow. +- [ ] New canvas extension. +- [ ] Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension. +- [ ] Other (please specify): + +--- + +## Additional Notes + + + +--- + +By submitting this pull request, I confirm that my contribution abides by the [Code of Conduct](../CODE_OF_CONDUCT.md) and will be licensed under the MIT License. + + + + diff --git a/plugins/shepherd-task/scripts/install-task-shepherd.ps1 b/plugins/shepherd-task/scripts/install-task-shepherd.ps1 new file mode 100644 index 000000000..ba3088834 --- /dev/null +++ b/plugins/shepherd-task/scripts/install-task-shepherd.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Copies the orchestration scripts into another repository. + +.DESCRIPTION + Copies the following directory from this repository to the target: + plugins/shepherd-task/scripts + + Skills should be installed separately via: + gh skill install github/awesome-copilot shepherd-task-from-assignment-to-ready + gh skill install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base + gh skill install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion + +.PARAMETER TargetRepoPath + Relative path to the target repository root (must exist). + +.EXAMPLE + ./install-task-shepherd.ps1 ../my-other-repo +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$TargetRepoPath +) + +$ErrorActionPreference = 'Stop' + +# Validate target path exists. +if (-not (Test-Path $TargetRepoPath -PathType Container)) { + Write-Error "Target repository path does not exist: $TargetRepoPath" +} + +$TargetRepo = (Resolve-Path $TargetRepoPath).Path +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition + +# Copy orchestration scripts. +$dest = Join-Path $TargetRepo 'plugins/shepherd-task/scripts' +if (-not (Test-Path $dest)) { + New-Item -ItemType Directory -Path $dest -Force | Out-Null +} +Copy-Item -Path (Join-Path $ScriptDir '*') -Destination $dest -Recurse -Force +Write-Host "Copied plugins/shepherd-task/scripts" + +Write-Host "" +Write-Host "Orchestration scripts installed into $TargetRepo" +Write-Host "" +Write-Host "Next, install the skills via gh CLI:" +Write-Host " gh skill install github/awesome-copilot shepherd-task-from-assignment-to-ready" +Write-Host " gh skill install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base" +Write-Host " gh skill install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion" diff --git a/plugins/shepherd-task/scripts/install-task-shepherd.sh b/plugins/shepherd-task/scripts/install-task-shepherd.sh new file mode 100755 index 000000000..55b8f2fc2 --- /dev/null +++ b/plugins/shepherd-task/scripts/install-task-shepherd.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# install-task-shepherd.sh — Copies the orchestration scripts into another repository. +# +# Copies the following directory from this repository to the target: +# plugins/shepherd-task/scripts +# +# Skills should be installed separately via: +# gh skill install github/awesome-copilot shepherd-task-from-assignment-to-ready +# gh skill install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base +# gh skill install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion +# +# Usage: ./install-task-shepherd.sh +# TARGET_REPO_PATH: relative path to the target repository root (must exist) + +set -euo pipefail + +TARGET_REPO="${1:?Usage: $0 }" + +# Resolve the source repo root (three levels up from plugins/shepherd-task/scripts/). +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SOURCE_REPO="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Validate target path exists. +if [ ! -d "$TARGET_REPO" ]; then + echo "ERROR: Target repository path does not exist: $TARGET_REPO" >&2 + exit 1 +fi + +# Copy orchestration scripts. +dest="$TARGET_REPO/plugins/shepherd-task/scripts" +mkdir -p "$dest" +cp -R "$SCRIPT_DIR/." "$dest/" +echo "Copied plugins/shepherd-task/scripts" + +echo "" +echo "Orchestration scripts installed into $TARGET_REPO" +echo "" +echo "Next, install the skills via gh CLI:" +echo " gh skill install github/awesome-copilot shepherd-task-from-assignment-to-ready" +echo " gh skill install github/awesome-copilot shepherd-task-from-ready-to-merged-to-base" +echo " gh skill install github/awesome-copilot shepherd-task-approve-workflows-and-wait-for-completion" diff --git a/plugins/shepherd-task/scripts/shepherd-task-given-list.ps1 b/plugins/shepherd-task/scripts/shepherd-task-given-list.ps1 new file mode 100644 index 000000000..236455972 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task-given-list.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Shepherds a list of child Task issues end-to-end by invoking shepherd-task.ps1 for each. + +.DESCRIPTION + Takes a comma-separated list of issue numbers and invokes shepherd-task.ps1 + sequentially for each one. + +.PARAMETER TaskIssues + Comma-separated list of issue numbers (e.g., "1841,1842,1843"). + +.PARAMETER BaseBranch + The base branch the task PRs should target. This is never main. + +.PARAMETER Repo + Repository in OWNER/REPO format. +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$TaskIssues, + + [Parameter(Mandatory = $true, Position = 1)] + [string]$BaseBranch, + + [Parameter(Mandatory = $true, Position = 2)] + [string]$Repo +) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$shepherdScript = Join-Path $scriptDir 'shepherd-task.ps1' + +$logDir = "shepherd-tasks-$(Get-Date -Format 'yyyyMMdd-HHmm')" +if (-not (Test-Path $logDir)) { + New-Item -ItemType Directory -Path $logDir | Out-Null +} + +$issues = $TaskIssues -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + +foreach ($issue in $issues) { + Write-Output "=== Shepherding task issue #$issue ===" + & $shepherdScript -TaskIssue $issue -BaseBranch $BaseBranch -Repo $Repo -LogDir $logDir + if ($LASTEXITCODE -ne 0) { + Write-Error "shepherd-task.ps1 failed for issue #$issue (exit code $LASTEXITCODE)" + exit $LASTEXITCODE + } +} + +Write-Output "=== All tasks shepherded successfully ===" diff --git a/plugins/shepherd-task/scripts/shepherd-task-given-list.sh b/plugins/shepherd-task/scripts/shepherd-task-given-list.sh new file mode 100755 index 000000000..de1382993 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task-given-list.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# shepherd-task-given-list.sh — Shepherds a list of child Task issues end-to-end +# by invoking shepherd-task.sh sequentially for each one. +# +# Usage: ./shepherd-task-given-list.sh +# TASK_ISSUES: comma-separated list of issue numbers (e.g., "1841,1842,1843") +# BASE_BRANCH: the base branch the task PRs should target (never main) +# REPO: repository in OWNER/REPO format + +set -euo pipefail + +if [[ $# -lt 3 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +TASK_ISSUES="$1" +BASE_BRANCH="$2" +REPO="$3" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +LOG_DIR="shepherd-tasks-$(date +%Y%m%d-%H%M)" +mkdir -p "$LOG_DIR" + +IFS=',' read -ra ISSUES <<< "$TASK_ISSUES" + +for issue in "${ISSUES[@]}"; do + issue="$(echo "$issue" | tr -d '[:space:]')" + [[ -z "$issue" ]] && continue + echo "=== Shepherding task issue #${issue} ===" + "$SCRIPT_DIR/shepherd-task.sh" "$issue" "$BASE_BRANCH" "$REPO" "$LOG_DIR" +done + +echo "=== All tasks shepherded successfully ===" diff --git a/plugins/shepherd-task/scripts/shepherd-task-inspect-json.ps1 b/plugins/shepherd-task/scripts/shepherd-task-inspect-json.ps1 new file mode 100644 index 000000000..5ad36ebb1 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task-inspect-json.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + Inspect a copilot JSON log file, showing the last N meaningful events. + +.PARAMETER JsonFile + Path to the JSON log file (relative or absolute). + +.PARAMETER Count + Number of messages to show (default: 20). +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$JsonFile, + + [Parameter(Mandatory = $false, Position = 1)] + [int]$Count = 20 +) + +$events = Get-Content $JsonFile | + Where-Object { $_ -notmatch '"ephemeral":true' -and $_ -notmatch '"tool.execution_partial_result"' } | + ForEach-Object { + try { $_ | ConvertFrom-Json } catch { $null } + } | + Where-Object { $_ -ne $null } | + Select-Object -Last $Count + +$events | ForEach-Object { + $evt = $_ + $ts = if ($evt.timestamp) { ([datetime]$evt.timestamp).ToString("HH:mm:ss") } else { "--------" } + $summary = switch -Wildcard ($evt.type) { + "user.message" { $txt = $evt.data.content; "USER: " + $txt.Substring(0, [Math]::Min(120, $txt.Length)) } + "assistant.message" { + if ($evt.data.content) { + $txt = $evt.data.content; "ASST: " + $txt.Substring(0, [Math]::Min(120, $txt.Length)) + } else { + $names = ($evt.data.toolRequests | ForEach-Object { $_.name }) -join ", " + "ASST: [tool calls: $names]" + } + } + "tool.execution_start" { + $args_summary = ($evt.data.arguments | ConvertTo-Json -Compress -Depth 1) -replace '[\r\n]','' + if ($args_summary.Length -gt 80) { $args_summary = $args_summary.Substring(0,77) + "..." } + "TOOL> " + $evt.data.toolName + " :: " + $args_summary + } + "tool.execution_complete" { "TOOL< " + $(if ($evt.data.success) {"OK"} else {"FAIL"}) } + "assistant.reasoning" { $txt = $evt.data.content; "THINK: " + $txt.Substring(0, [Math]::Min(120, $txt.Length)) } + "assistant.turn_start" { $null } + "assistant.turn_end" { $null } + default { $evt.type } + } + if ($summary) { "$ts | $summary" } + } diff --git a/plugins/shepherd-task/scripts/shepherd-task-inspect-json.sh b/plugins/shepherd-task/scripts/shepherd-task-inspect-json.sh new file mode 100755 index 000000000..ae2ab8665 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task-inspect-json.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# shepherd-task-inspect-json.sh — Show the last N meaningful events from a copilot JSON log. +# +# Usage: ./shepherd-task-inspect-json.sh [count] +# json-file: path to the JSON log file +# count: number of messages to show (default: 20) + +set -euo pipefail + +JSON_FILE="${1:?Usage: $0 [count]}" +COUNT="${2:-20}" + +grep -v '"ephemeral":true' "$JSON_FILE" | tail -n "$COUNT" | while IFS= read -r line; do + ts=$(echo "$line" | jq -r '.timestamp // empty' | xargs -I{} date -d {} +%H:%M:%S 2>/dev/null || echo "$line" | jq -r '.timestamp // "--------" | .[11:19]') + type=$(echo "$line" | jq -r '.type') + + case "$type" in + user.message) + content=$(echo "$line" | jq -r '.data.content[0:80]') + echo "$ts | USER: $content" + ;; + assistant.message) + content=$(echo "$line" | jq -r 'if .data.content != "" then .data.content[0:80] else "[tool calls: " + ([.data.toolRequests[].name] | join(", ")) + "]" end') + echo "$ts | ASST: $content" + ;; + tool.execution_start) + tool=$(echo "$line" | jq -r '.data.toolName') + desc=$(echo "$line" | jq -r '.data.arguments.description // ""') + echo "$ts | TOOL> $tool :: $desc" + ;; + tool.execution_complete) + status=$(echo "$line" | jq -r 'if .data.success then "OK" else "FAIL" end') + echo "$ts | TOOL< $status" + ;; + assistant.reasoning) + content=$(echo "$line" | jq -r '.data.content[0:80]') + echo "$ts | THINK: $content" + ;; + *) + echo "$ts | $type" + ;; + esac +done diff --git a/plugins/shepherd-task/scripts/shepherd-task.ps1 b/plugins/shepherd-task/scripts/shepherd-task.ps1 new file mode 100644 index 000000000..7a2cdd484 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task.ps1 @@ -0,0 +1,218 @@ +<# +.SYNOPSIS + Shepherds a child Task issue end-to-end: from Copilot assignment through merge. + +.DESCRIPTION + Orchestrates two phases by launching separate `copilot --yolo` sessions: + Phase 1: Assignment to Ready for Review + Phase 2: Ready for Review to Merged + + Between phases, the script verifies state using gh CLI (not copilot exit codes). + +.PARAMETER TaskIssue + The issue number (e.g., 1841) or URL of the child task to shepherd. + +.PARAMETER BaseBranch + The base branch the task PR should target. + +.PARAMETER Repo + Repository in OWNER/REPO format. +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$TaskIssue, + + [Parameter(Mandatory = $true, Position = 1)] + [string]$BaseBranch, + + [Parameter(Mandatory = $true, Position = 2)] + [string]$Repo, + + [Parameter(Mandatory = $false, Position = 3)] + [string]$LogDir = "shepherd-tasks-$(Get-Date -Format 'yyyyMMdd-HHmm')" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $LogDir)) { + New-Item -ItemType Directory -Path $LogDir | Out-Null +} + +function Write-Status($msg) { + Write-Output "[shepherd-task] $msg" +} + +function Write-Fail($msg) { + Write-Output "[shepherd-task] FAILED: $msg" +} + +function Write-Ok($msg) { + Write-Output "[shepherd-task] $msg" +} + +# --- Helper: Find the PR linked to the task issue --- +function Find-LinkedPR { + # Strategy A: Issue timeline for cross-referenced PRs + $prNumber = gh api "/repos/$Repo/issues/$TaskIssue/timeline" ` + --jq '.[] | select(.event == "cross-referenced") | select(.source.issue.pull_request != null) | select(.source.issue.state == "open") | .source.issue.number' 2>$null | + Select-Object -First 1 + + if ($prNumber) { return $prNumber.Trim() } + + # Strategy B: Search PR bodies for the issue number + $prNumber = gh pr list -R $Repo --state open --json number,body ` + --jq ".[] | select(.body | test(`"#$TaskIssue`")) | .number" 2>$null | + Select-Object -First 1 + + if ($prNumber) { return $prNumber.Trim() } + + # Strategy C: Title or branch name match + $prNumber = gh pr list -R $Repo --state open --json number,title,headRefName ` + --jq ".[] | select((.title | test(`"$TaskIssue`"; `"i`")) or (.headRefName | test(`"$TaskIssue`"))) | .number" 2>$null | + Select-Object -First 1 + + if ($prNumber) { return $prNumber.Trim() } + + return $null +} + +# --- Helper: Verify all CI checks pass (excluding expected failure) --- +function Test-CIPassing { + param([string]$PRNumber) + + $failures = gh pr checks $PRNumber -R $Repo --json name,state,bucket ` + --jq '.[] | select(.bucket == "fail") | select(.name != "No remove-before-merge directories") | .name' 2>$null + + return [string]::IsNullOrWhiteSpace($failures) +} + +# --- Helper: Check for unresolved bot review comments --- +function Test-NoUnresolvedReviews { + param([string]$PRNumber) + + $repoOwner = ($Repo -split '/')[0] + $repoName = ($Repo -split '/')[1] + + $unresolved = gh api graphql -F owner=$repoOwner -F name=$repoName -F number=$PRNumber -f query=' + query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reviewThreads(first: 100) { + nodes { isResolved comments(first: 1) { nodes { author { login } } } } + } + } + } + }' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0].author.login' 2>$null + + return [string]::IsNullOrWhiteSpace($unresolved) +} + +# ============================================================================= +# PHASE 1: Assignment to Ready for Review +# ============================================================================= + +# Idempotency: skip Phase 1 if a PR already exists for this issue +$prNumber = Find-LinkedPR +if ($prNumber) { + Write-Status "PR #$prNumber already exists for issue #$TaskIssue — skipping Phase 1." +} else { + Write-Status "Phase 1: Launching copilot --yolo for task #$TaskIssue" + + $phase1Prompt = @" +Invoke skill ``shepherd-task-from-assignment-to-ready`` with these inputs: + +- TASK_ISSUE: $TaskIssue +- BASE_BRANCH: $BaseBranch +- REPO: $Repo +"@ + + Write-Status "Phase 1 prompt: $phase1Prompt" + $phase1Share = Join-Path $LogDir "phase1-task-$(Get-Date -Format 'yyyyMMdd-HHmm')-$TaskIssue.md" + $phase1Json = Join-Path $LogDir "phase1-task-$(Get-Date -Format 'yyyyMMdd-HHmm')-$TaskIssue.json" + $phase1Prompt | copilot --yolo --output-format json --share $phase1Share > $phase1Json + + Write-Status "Phase 1: copilot exited. Verifying state..." + + # --- Verify Phase 1 outcome --- + $prNumber = Find-LinkedPR + if (-not $prNumber) { + Write-Fail "No open PR found linked to issue #$TaskIssue after Phase 1." + exit 1 + } +} +Write-Status "Found PR #$prNumber" + +# Verify base branch +$actualBase = gh pr view $prNumber -R $Repo --json baseRefName --jq '.baseRefName' +if ($actualBase -ne $BaseBranch) { + Write-Status "PR base is '$actualBase', fixing to '$BaseBranch'..." + gh pr edit $prNumber -R $Repo --base $BaseBranch +} + +# Verify CI passing +if (-not (Test-CIPassing $prNumber)) { + Write-Fail "CI checks not passing on PR #$prNumber after Phase 1." + exit 1 +} + +# Verify no unresolved reviews +if (-not (Test-NoUnresolvedReviews $prNumber)) { + Write-Fail "Unresolved review comments remain on PR #$prNumber after Phase 1." + exit 1 +} + +Write-Ok "Phase 1 VERIFIED: PR #$prNumber is ready. CI passing, no unresolved comments." + +# ============================================================================= +# PHASE 2: Ready for Review to Merged +# ============================================================================= + +# Idempotency: skip Phase 2 if PR is already merged +$prState = gh pr view $prNumber -R $Repo --json state --jq '.state' +if ($prState -eq "MERGED") { + Write-Ok "PR #$prNumber already merged — skipping Phase 2." +} else { + Write-Status "Phase 2: Launching copilot --yolo for PR #$prNumber" + + $phase2Prompt = @" +Invoke skill ``shepherd-task-from-ready-to-merged-to-base`` with these inputs: + +- TASK_ISSUE: $TaskIssue +- BASE_BRANCH: $BaseBranch +- REPO: $Repo +- PR_NUMBER: $prNumber +"@ + + Write-Status "Phase 2 prompt: $phase2Prompt" + $phase2Share = Join-Path $LogDir "phase2-task-$(Get-Date -Format 'yyyyMMdd-HHmm')-$TaskIssue.md" + $phase2Json = Join-Path $LogDir "phase2-task-$(Get-Date -Format 'yyyyMMdd-HHmm')-$TaskIssue.json" + $phase2Prompt | copilot --yolo --output-format json --share $phase2Share > $phase2Json + + Write-Status "Phase 2: copilot exited. Verifying state..." + + # --- Verify Phase 2 outcome --- + $prState = gh pr view $prNumber -R $Repo --json state --jq '.state' + if ($prState -ne "MERGED") { + Write-Fail "PR #$prNumber is in state '$prState', expected MERGED." + exit 1 + } +} + +# Verify merged into correct branch +$mergedBase = gh pr view $prNumber -R $Repo --json baseRefName --jq '.baseRefName' +if ($mergedBase -ne $BaseBranch) { + Write-Fail "PR #$prNumber was merged into '$mergedBase', expected '$BaseBranch'." + exit 1 +} + +# Verify issue is closed +$issueState = gh issue view $TaskIssue -R $Repo --json state --jq '.state' +if ($issueState -ne "CLOSED") { + Write-Status "Issue #$TaskIssue still open, closing..." + gh issue close $TaskIssue -R $Repo +} + +Write-Ok "SHEPHERD TASK COMPLETE: Task #$TaskIssue has been fully shepherded." +Write-Ok "PR #$prNumber merged to $BaseBranch." +exit 0 diff --git a/plugins/shepherd-task/scripts/shepherd-task.sh b/plugins/shepherd-task/scripts/shepherd-task.sh new file mode 100755 index 000000000..a2a4c9207 --- /dev/null +++ b/plugins/shepherd-task/scripts/shepherd-task.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# +# shepherd-task.sh — Shepherds a child Task issue end-to-end: +# from Copilot assignment through merge. +# +# Orchestrates two phases by launching separate `copilot --yolo` sessions. +# Between phases, verifies state using gh CLI (not copilot exit codes). +# +# Usage: ./shepherd-task.sh + +set -euo pipefail + +TASK_ISSUE="${1:?Usage: $0 [LOG_DIR]}" +BASE_BRANCH="${2:?Usage: $0 [LOG_DIR]}" +REPO="${3:?Usage: $0 [LOG_DIR]}" +LOG_DIR="${4:-shepherd-tasks-$(date +%Y%m%d-%H%M)}" + +mkdir -p "$LOG_DIR" + +# --- Helpers --- + +status() { echo "[shepherd-task] $*"; } +fail() { echo "[shepherd-task] FAILED: $*"; exit 1; } +ok() { echo "[shepherd-task] $*"; } + +# Find the PR linked to the task issue using three strategies. +find_linked_pr() { + local pr_number="" + + # Strategy A: Issue timeline for cross-referenced PRs + pr_number=$(gh api "/repos/$REPO/issues/$TASK_ISSUE/timeline" \ + --jq '.[] | select(.event == "cross-referenced") | select(.source.issue.pull_request != null) | select(.source.issue.state == "open") | .source.issue.number' 2>/dev/null | head -1) + + if [[ -n "$pr_number" ]]; then echo "$pr_number"; return 0; fi + + # Strategy B: Search PR bodies for the issue number + pr_number=$(gh pr list -R "$REPO" --state open --json number,body \ + --jq ".[] | select(.body | test(\"#$TASK_ISSUE\")) | .number" 2>/dev/null | head -1) + + if [[ -n "$pr_number" ]]; then echo "$pr_number"; return 0; fi + + # Strategy C: Title or branch name match + pr_number=$(gh pr list -R "$REPO" --state open --json number,title,headRefName \ + --jq ".[] | select((.title | test(\"$TASK_ISSUE\"; \"i\")) or (.headRefName | test(\"$TASK_ISSUE\"))) | .number" 2>/dev/null | head -1) + + if [[ -n "$pr_number" ]]; then echo "$pr_number"; return 0; fi + + return 1 +} + +# Verify all CI checks pass (excluding expected failure). +ci_passing() { + local pr_number="$1" + local failures + failures=$(gh pr checks "$pr_number" -R "$REPO" --json name,state,bucket \ + --jq '.[] | select(.bucket == "fail") | select(.name != "No remove-before-merge directories") | .name' 2>/dev/null) + + [[ -z "$failures" ]] +} + +# Check for unresolved bot review comments. +no_unresolved_reviews() { + local pr_number="$1" + local repo_owner="${REPO%%/*}" + local repo_name="${REPO##*/}" + local unresolved + unresolved=$(gh api graphql -F owner="$repo_owner" -F name="$repo_name" -F number="$pr_number" -f query=' + query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reviewThreads(first: 100) { + nodes { isResolved comments(first: 1) { nodes { author { login } } } } + } + } + } + }' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0].author.login' 2>/dev/null) + + [[ -z "$unresolved" ]] +} + +# ============================================================================= +# PHASE 1: Assignment to Ready for Review +# ============================================================================= + +# Idempotency: skip Phase 1 if a PR already exists for this issue +PR_NUMBER=$(find_linked_pr) || true +if [[ -n "$PR_NUMBER" ]]; then + status "PR #$PR_NUMBER already exists for issue #$TASK_ISSUE — skipping Phase 1." +else + status "Phase 1: Launching copilot --yolo for task #$TASK_ISSUE" + + PHASE1_PROMPT="Invoke skill \`shepherd-task-from-assignment-to-ready\` with these inputs: + +- TASK_ISSUE: $TASK_ISSUE +- BASE_BRANCH: $BASE_BRANCH +- REPO: $REPO" + + status "Phase 1 prompt:" + echo "$PHASE1_PROMPT" + PHASE1_SHARE="$LOG_DIR/phase1-task-$(date +%Y%m%d-%H%M)-$TASK_ISSUE.md" + PHASE1_JSON="$LOG_DIR/phase1-task-$(date +%Y%m%d-%H%M)-$TASK_ISSUE.json" + echo "$PHASE1_PROMPT" | copilot --yolo --output-format json --share "$PHASE1_SHARE" > "$PHASE1_JSON" + + status "Phase 1: copilot exited. Verifying state..." + + # --- Verify Phase 1 outcome --- + PR_NUMBER=$(find_linked_pr) || fail "No open PR found linked to issue #$TASK_ISSUE after Phase 1." +fi +status "Found PR #$PR_NUMBER" + +# Verify base branch +ACTUAL_BASE=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName') +if [[ "$ACTUAL_BASE" != "$BASE_BRANCH" ]]; then + status "PR base is '$ACTUAL_BASE', fixing to '$BASE_BRANCH'..." + gh pr edit "$PR_NUMBER" -R "$REPO" --base "$BASE_BRANCH" +fi + +# Verify CI passing +ci_passing "$PR_NUMBER" || fail "CI checks not passing on PR #$PR_NUMBER after Phase 1." + +# Verify no unresolved reviews +no_unresolved_reviews "$PR_NUMBER" || fail "Unresolved review comments remain on PR #$PR_NUMBER after Phase 1." + +ok "Phase 1 VERIFIED: PR #$PR_NUMBER is ready. CI passing, no unresolved comments." + +# ============================================================================= +# PHASE 2: Ready for Review to Merged +# ============================================================================= + +# Idempotency: skip Phase 2 if PR is already merged +PR_STATE=$(gh pr view "$PR_NUMBER" -R "$REPO" --json state --jq '.state') +if [[ "$PR_STATE" == "MERGED" ]]; then + ok "PR #$PR_NUMBER already merged — skipping Phase 2." +else + status "Phase 2: Launching copilot --yolo for PR #$PR_NUMBER" + + PHASE2_PROMPT="Invoke skill \`shepherd-task-from-ready-to-merged-to-base\` with these inputs: + +- TASK_ISSUE: $TASK_ISSUE +- BASE_BRANCH: $BASE_BRANCH +- REPO: $REPO +- PR_NUMBER: $PR_NUMBER" + + status "Phase 2 prompt:" + echo "$PHASE2_PROMPT" + PHASE2_SHARE="$LOG_DIR/phase2-task-$(date +%Y%m%d-%H%M)-$TASK_ISSUE.md" + PHASE2_JSON="$LOG_DIR/phase2-task-$(date +%Y%m%d-%H%M)-$TASK_ISSUE.json" + echo "$PHASE2_PROMPT" | copilot --yolo --output-format json --share "$PHASE2_SHARE" > "$PHASE2_JSON" + + status "Phase 2: copilot exited. Verifying state..." + + # --- Verify Phase 2 outcome --- + PR_STATE=$(gh pr view "$PR_NUMBER" -R "$REPO" --json state --jq '.state') + if [[ "$PR_STATE" != "MERGED" ]]; then + fail "PR #$PR_NUMBER is in state '$PR_STATE', expected MERGED." + fi +fi + +# Verify merged into correct branch +MERGED_BASE=$(gh pr view "$PR_NUMBER" -R "$REPO" --json baseRefName --jq '.baseRefName') +if [[ "$MERGED_BASE" != "$BASE_BRANCH" ]]; then + fail "PR #$PR_NUMBER was merged into '$MERGED_BASE', expected '$BASE_BRANCH'." +fi + +# Verify issue is closed +ISSUE_STATE=$(gh issue view "$TASK_ISSUE" -R "$REPO" --json state --jq '.state') +if [[ "$ISSUE_STATE" != "CLOSED" ]]; then + status "Issue #$TASK_ISSUE still open, closing..." + gh issue close "$TASK_ISSUE" -R "$REPO" +fi + +ok "SHEPHERD TASK COMPLETE: Task #$TASK_ISSUE has been fully shepherded." +ok "PR #$PR_NUMBER merged to $BASE_BRANCH." +exit 0 diff --git a/plugins/shepherd-task/scripts/uninstall-task-shepherd.ps1 b/plugins/shepherd-task/scripts/uninstall-task-shepherd.ps1 new file mode 100644 index 000000000..7a22376fe --- /dev/null +++ b/plugins/shepherd-task/scripts/uninstall-task-shepherd.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Removes the orchestration scripts from a target repository. + +.DESCRIPTION + Removes the following directory from the target: + plugins/shepherd-task/scripts + + Skills must be removed manually by deleting them from your agent's skills location. + +.PARAMETER TargetRepoPath + Relative path to the target repository root (must exist). + +.EXAMPLE + ./uninstall-task-shepherd.ps1 ../my-other-repo +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$TargetRepoPath +) + +$ErrorActionPreference = 'Stop' + +# Validate target path exists. +if (-not (Test-Path $TargetRepoPath -PathType Container)) { + Write-Error "Target repository path does not exist: $TargetRepoPath" +} + +$TargetRepo = (Resolve-Path $TargetRepoPath).Path +$ScriptsDir = Join-Path $TargetRepo 'plugins' 'shepherd-task' 'scripts' + +if (-not (Test-Path $ScriptsDir)) { + Write-Host "Nothing to remove: $ScriptsDir does not exist." + exit 0 +} + +Remove-Item -Path $ScriptsDir -Recurse -Force +Write-Host "Removed $ScriptsDir" + +# Clean up empty parent directories. +$parentDir = Join-Path $TargetRepo 'plugins' 'shepherd-task' +if ((Test-Path $parentDir) -and ((Get-ChildItem $parentDir -Force | Measure-Object).Count -eq 0)) { + Remove-Item -Path $parentDir -Force + Write-Host "Removed plugins/shepherd-task/" +} + +$pluginsDir = Join-Path $TargetRepo 'plugins' +if ((Test-Path $pluginsDir) -and ((Get-ChildItem $pluginsDir -Force | Measure-Object).Count -eq 0)) { + Remove-Item -Path $pluginsDir -Force + Write-Host "Removed plugins/" +} + +Write-Host "" +Write-Host "Orchestration scripts removed from $TargetRepo" +Write-Host "" +Write-Host "To also remove the skills, manually delete them from your agent's skills location:" +Write-Host " Remove-Item -Recurse /shepherd-task-from-assignment-to-ready" +Write-Host " Remove-Item -Recurse /shepherd-task-from-ready-to-merged-to-base" +Write-Host " Remove-Item -Recurse /shepherd-task-approve-workflows-and-wait-for-completion" diff --git a/plugins/shepherd-task/scripts/uninstall-task-shepherd.sh b/plugins/shepherd-task/scripts/uninstall-task-shepherd.sh new file mode 100755 index 000000000..b6b7b27e6 --- /dev/null +++ b/plugins/shepherd-task/scripts/uninstall-task-shepherd.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# uninstall-task-shepherd.sh — Removes the orchestration scripts from a target repository. +# +# Removes the following directory from the target: +# plugins/shepherd-task/scripts +# +# Skills must be removed manually by deleting them from your agent's skills location. +# +# Usage: ./uninstall-task-shepherd.sh +# TARGET_REPO_PATH: relative path to the target repository root (must exist) + +set -euo pipefail + +TARGET_REPO="${1:?Usage: $0 }" + +# Validate target path exists. +if [ ! -d "$TARGET_REPO" ]; then + echo "ERROR: Target repository path does not exist: $TARGET_REPO" >&2 + exit 1 +fi + +SCRIPTS_DIR="$TARGET_REPO/plugins/shepherd-task/scripts" + +if [ ! -d "$SCRIPTS_DIR" ]; then + echo "Nothing to remove: $SCRIPTS_DIR does not exist." + exit 0 +fi + +rm -rf "$SCRIPTS_DIR" +echo "Removed $SCRIPTS_DIR" + +# Clean up empty parent directories. +rmdir "$TARGET_REPO/plugins/shepherd-task" 2>/dev/null && echo "Removed plugins/shepherd-task/" || true +rmdir "$TARGET_REPO/plugins" 2>/dev/null && echo "Removed plugins/" || true + +echo "" +echo "Orchestration scripts removed from $TARGET_REPO" +echo "" +echo "To also remove the skills, manually delete them from your agent's skills location:" +echo " rm -rf /shepherd-task-from-assignment-to-ready" +echo " rm -rf /shepherd-task-from-ready-to-merged-to-base" +echo " rm -rf /shepherd-task-approve-workflows-and-wait-for-completion" diff --git a/skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md b/skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md new file mode 100644 index 000000000..c7f551388 --- /dev/null +++ b/skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md @@ -0,0 +1,68 @@ +--- +name: shepherd-task-approve-workflows-and-wait-for-completion +description: "Use this skill to approve pending workflow runs on a PR branch and wait for them to complete." +--- + +# Skill: Approve Workflows and Wait for Completion + +## Purpose + +Approve all pending workflow runs (`action_required` status) on a PR's topic branch and wait for them to complete. This is a reusable sub-skill invoked by other shepherd skills whenever workflow approval is needed. + +## Inputs + +- `REPO`: Repository in `OWNER/REPO` format (e.g., `github/copilot-sdk`). +- `JTBDTASK_BRANCH`: The topic branch name associated with the PR (used to find workflow runs). +- `PR_NUMBER`: The PR number (used for `gh pr checks --watch`). + +## Prerequisites + +- `gh` CLI authenticated with sufficient permissions (actions, PRs). +- The PR exists and has workflow runs triggered on `JTBDTASK_BRANCH`. + +--- + +## Procedure + +### Step 1: Approve pending workflow runs + +For each run in `action_required` status on the PR's branch, re-run it. The correct mechanism is `gh run rerun` (the `POST .../actions/runs/{id}/approve` endpoint is for fork PRs only and will return HTTP 403 here). + +```bash +# Get all action_required runs for the PR branch +PENDING_RUNS=$(gh run list -R $REPO --branch "$JTBDTASK_BRANCH" \ + --json databaseId,conclusion --jq '.[] | select(.conclusion == "action_required") | .databaseId') + +for RUN_ID in $PENDING_RUNS; do + gh run rerun $RUN_ID -R $REPO +done +``` + +### Step 2: Wait for workflow runs to complete + +```bash +# Watch all runs on the branch until they complete +# Use gh pr checks with --watch for convenience +gh pr checks $PR_NUMBER -R $REPO --watch --fail-fast +``` + +Alternatively, poll with: + +```bash +gh run list -R $REPO --branch "$JTBDTASK_BRANCH" \ + --json databaseId,status,conclusion,name \ + --jq '.[] | select(.status != "completed")' +``` + +--- + +## Error handling + +- **No pending runs found**: This is not an error — it means runs were already approved (possibly manually). Proceed directly to waiting for completion. +- **`gh run rerun` fails**: Retry up to 3 times with 10-second backoff, then report and stop. +- **Runs do not complete within a reasonable time**: The `--watch` flag on `gh pr checks` will block until completion or failure. If it times out, report and stop. + +## Notes + +- This skill is extracted from Steps 4 and 5 of `shepherd-task-from-assignment-to-ready` for reuse across multiple shepherd skills. +- The `gh api .../actions/runs/{id}/approve` endpoint does NOT work for same-repo PRs (returns HTTP 403 "This run is not from a fork pull request"). Always use `gh run rerun` instead. diff --git a/skills/shepherd-task-from-assignment-to-ready/SKILL.md b/skills/shepherd-task-from-assignment-to-ready/SKILL.md new file mode 100644 index 000000000..a61ce9a73 --- /dev/null +++ b/skills/shepherd-task-from-assignment-to-ready/SKILL.md @@ -0,0 +1,330 @@ +--- +name: shepherd-task-from-assignment-to-ready +description: "Use this skill to shepherd a child Task issue from 'assigned to Copilot' through CI approval and review-agent feedback resolution, stopping just before marking the PR as **Ready for review**." +--- + +# Skill: Shepherd Task to Ready for Review + +## Purpose + +Automate the lifecycle of a child **Task** issue from "assigned to Copilot" through CI passing and review-agent feedback resolution, stopping just before marking the PR as **Ready for review**. + +## Inputs + +- `TASK_ISSUE`: The issue number (e.g., `1850`) or URL of the child task to shepherd. +- `BASE_BRANCH`: The base branch the task PR should target (default: `upstream/edburns/1810-java-tool-ergonomics-tool-as-lambda`). +- `REPO`: Repository in `OWNER/REPO` format (default: `github/copilot-sdk`). + +## Prerequisites + +- `gh` CLI authenticated with sufficient permissions (issues, PRs, actions, reviews). +- The task issue already exists and has a clear description of work to do. +- The base branch exists in the repository. + +--- + +## Procedure + +### Step 1: Assign the task to @Copilot + +Use the GitHub Issues REST API with the `agent_assignment.base_branch` parameter. This is the **only 100% reliable method** — it passes `BASE_BRANCH` directly to CCA as a first-class input, so it cannot default to `main`. + +> [!NOTE] +> Do **not** use `gh issue edit --add-assignee "@copilot"` here. That command uses the plain assignees endpoint which has no `base_branch` parameter; CCA will default to `main`. + +```bash +gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/$REPO/issues/$TASK_ISSUE/assignees \ + --input - <<< "{ + \"assignees\": [\"copilot-swe-agent[bot]\"], + \"agent_assignment\": { + \"target_repo\": \"$REPO\", + \"base_branch\": \"$BASE_BRANCH\" + } + }" +``` + +> **PowerShell equivalent** (when running on Windows): +> ```powershell +> $body = @{ +> assignees = @("copilot-swe-agent[bot]") +> agent_assignment = @{ +> target_repo = $REPO +> base_branch = $BASE_BRANCH +> } +> } | ConvertTo-Json -Depth 3 +> gh api ` +> --method POST ` +> -H "Accept: application/vnd.github+json" ` +> -H "X-GitHub-Api-Version: 2022-11-28" ` +> /repos/$REPO/issues/$TASK_ISSUE/assignees ` +> --input - <<< $body +> ``` + +This triggers Copilot to: +1. Create a topic branch from `$BASE_BRANCH`. +2. Open a draft PR targeting `$BASE_BRANCH`. +3. Push initial commits. + +### Step 2: Find the corresponding PR + +Use **all three** of the following strategies (in order) each polling iteration. Copilot often creates PRs whose title or branch name does NOT contain the issue number — it may use a descriptive name instead. Therefore, relying on title/branch regex alone is insufficient. + +#### Strategy A: Query the issue timeline for linked PRs + +The GitHub timeline API shows PRs linked via "Fixes #N" or the UI link feature. This is the most reliable signal. + +```bash +# Query issue timeline for cross-referenced or connected PRs +PR_NUMBER=$(gh api "/repos/$REPO/issues/$TASK_ISSUE/timeline" \ + --jq '.[] | select(.event == "cross-referenced") | select(.source.issue.pull_request != null) | select(.source.issue.state == "open") | .source.issue.number' | head -1) +``` + +#### Strategy B: Search PR bodies for "Fixes #N" or "#N" + +Copilot PRs typically include "Fixes #1876" in the body even when the title is descriptive. + +```bash +# Search open PR bodies for the issue number +PR_NUMBER=$(gh pr list -R $REPO --state open --json number,body \ + --jq ".[] | select(.body | test(\"#$TASK_ISSUE\")) | .number" | head -1) +``` + +#### Strategy C: Match title or branch name (original approach) + +```bash +PR_NUMBER=$(gh pr list -R $REPO --state open --json number,title,headRefName \ + --jq ".[] | select((.title | test(\"$TASK_ISSUE\"; \"i\")) or (.headRefName | test(\"$TASK_ISSUE\"))) | .number" | head -1) +``` + +#### Polling loop + +Try all three strategies each iteration. Poll every 30 seconds for up to 15 minutes (Copilot coding agent can take 5-12 minutes to produce a PR). + +```bash +TIMEOUT=900 +INTERVAL=30 +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + # Strategy A: issue timeline + PR_NUMBER=$(gh api "/repos/$REPO/issues/$TASK_ISSUE/timeline" \ + --jq '.[] | select(.event == "cross-referenced") | select(.source.issue.pull_request != null) | select(.source.issue.state == "open") | .source.issue.number' 2>/dev/null | head -1) + + # Strategy B: PR body search + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list -R $REPO --state open --json number,body \ + --jq ".[] | select(.body | test(\"#$TASK_ISSUE\")) | .number" | head -1) + fi + + # Strategy C: title/branch match + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list -R $REPO --state open --json number,title,headRefName \ + --jq ".[] | select((.title | test(\"$TASK_ISSUE\"; \"i\")) or (.headRefName | test(\"$TASK_ISSUE\"))) | .number" | head -1) + fi + + if [ -n "$PR_NUMBER" ]; then + break + fi + + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) +done +``` + +If no PR is found after timeout, report failure and stop. + +Once the PR is found, verify the base branch as a sanity check (the `agent_assignment.base_branch` API call in Step 1 guarantees this, but confirm): + +```bash +# Sanity-check: confirm PR targets the correct base branch +ACTUAL_BASE=$(gh pr view $PR_NUMBER -R $REPO --json baseRefName --jq '.baseRefName') +if [ "$ACTUAL_BASE" != "$BASE_BRANCH" ]; then + echo "ERROR: PR #$PR_NUMBER targets '$ACTUAL_BASE' instead of '$BASE_BRANCH'." + echo "This should not happen when Step 1 used the agent_assignment.base_branch API." + echo "Manual intervention required — stop here." + exit 1 +fi +echo "Base branch confirmed: $ACTUAL_BASE" +``` + +### Step 3: Wait for initial commits and workflow trigger + +After the PR is created, Copilot pushes commits which trigger workflow runs. These runs require approval because every Copilot push triggers the "Approve workflows to run" gate. + +You may be coming to this PR after all the runs have been manually approved. In that case, you need to wait for the runs to complete, then, skip to step 6. Here is how you wait for the runs to complete. + +```bash +gh pr checks $PR_NUMBER -R $REPO --watch +``` + +Otherwise, wait for runs to appear in `action_required` status: + +```bash +# Wait for workflow runs needing approval +gh run list -R $REPO --branch "$JTBDTASK_BRANCH" --status action_required \ + --json databaseId,name,status --jq '.[].databaseId' +``` + +### Steps 4–5: Approve pending workflow runs and wait for completion + +Invoke the **`shepherd-task-approve-workflows-and-wait-for-completion`** skill (`skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md`) with: + +- `REPO` = `$REPO` +- `JTBDTASK_BRANCH` = the PR's topic branch +- `PR_NUMBER` = `$PR_NUMBER` + +This sub-skill approves all `action_required` runs via `gh run rerun` and waits for completion via `gh pr checks --watch`. + +### Step 6: Evaluate workflow results + +**Note:** Ignore failures from the "Block remove-before-merge paths" / "No remove-before-merge directories" workflow. This failure is expected on feature branches and is not a real problem. + +```bash +# Get check results, excluding the expected "Block remove-before-merge paths" failure +RESULTS=$(gh pr checks $PR_NUMBER -R $REPO --json name,state,bucket \ + --jq '.[] | select(.bucket == "fail") | select(.name != "No remove-before-merge directories")') +``` + +If there are real failures (after excluding the expected one), proceed to Step 7. If all pass, proceed to Step 8. + +### Step 7: Request changes from Copilot (iteration loop) + +**Max iterations: 20** + +When CI fails or review agents flag problems: + +#### 7.1: Gather failure details + +```bash +# Get failed run IDs +FAILED_RUNS=$(gh run list -R $REPO --branch "$JTBDTASK_BRANCH" \ + --status completed --json databaseId,conclusion,name \ + --jq '.[] | select(.conclusion == "failure") | .databaseId') + +# Get logs for failed runs (only failed steps) +for RUN_ID in $FAILED_RUNS; do + gh run view $RUN_ID -R $REPO --log-failed +done +``` + +#### 7.2: Gather review agent comments + +```bash +# Get review comments on the PR +gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" \ + --jq '.[] | select(.user.type == "Bot") | {user: .user.login, body: .body}' + +# Also get issue-level comments (review agents sometimes post there) +gh pr view $PR_NUMBER -R $REPO --comments --json comments \ + --jq '.comments[] | select(.author.login | test("bot|copilot|agent"; "i")) | {author: .author.login, body: .body}' +``` + +#### 7.3: Compose and submit a "Request changes" review + +Analyze the failures and compose a hybrid message: relevant log excerpts plus a short targeted instruction for Copilot. + +```bash +# Submit review requesting changes, @mentioning Copilot +gh pr review $PR_NUMBER -R $REPO --request-changes --body "$REVIEW_BODY" +``` + +The `$REVIEW_BODY` should follow this format: + +``` +@copilot Please fix the following issues: + +## CI Failure: [workflow name] + + + +**Fix:** [Short, specific instruction on what to change] + +## Review Comment from [bot name] + +> [quoted comment] + +**Fix:** [Short, specific instruction on what to change] +``` + +#### 7.4: Wait for Copilot to push fixes + +After submitting the review, wait for new commits on the branch: + +```bash +# Get current HEAD SHA +CURRENT_SHA=$(gh pr view $PR_NUMBER -R $REPO --json headRefOid --jq '.headRefOid') + +# Poll for new commits (up to 10 minutes) +TIMEOUT=600 +INTERVAL=30 +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + NEW_SHA=$(gh pr view $PR_NUMBER -R $REPO --json headRefOid --jq '.headRefOid') + if [ "$NEW_SHA" != "$CURRENT_SHA" ]; then + break + fi + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) +done +``` + +#### 7.5: Loop back + +Return to **Step 4** (approve workflows) and repeat. Track iteration count. If 20 iterations are exhausted without all checks passing, stop and report: + +``` +SHEPHERD FAILED: Exhausted 20 iterations on PR #$PR_NUMBER for task #$TASK_ISSUE. +Manual intervention required. +``` + +### Step 8: Address pre-Ready-for-Review comments + +Even when CI passes, review agents (e.g., "Copilot code review", "SDK Consistency Review Agent") may leave comments that should be addressed before marking ready. + +#### 8.1: Check for unresolved review comments + +```bash +# Get all review comments that haven't been resolved +gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.state == "CHANGES_REQUESTED") | {user: .user.login, body: .body}' + +# Get pending review threads +gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" \ + --jq '.[] | select(.user.type == "Bot") | {id: .id, user: .user.login, body: .body, path: .path, line: .line}' +``` + +#### 8.2: If unresolved comments exist, iterate + +Use the same pattern as Step 7: compose a review requesting changes with specific instructions, wait for Copilot to push, approve workflows, and check results. This shares the same 20-iteration budget. + +### Step 9: Final status report + +When all checks pass and no unresolved review comments remain: + +``` +SHEPHERD COMPLETE: PR #$PR_NUMBER for task #$TASK_ISSUE is ready to review for marking as **Ready to review**. +All CI checks pass. No unresolved review comments. +Next step: Mark as Ready for Review (use separate skill). +``` + +--- + +## Error handling + +- **PR not created within 10 minutes**: Report and stop. +- **Copilot doesn't push after review request within 10 minutes**: Report and stop. +- **20 iterations exhausted**: Report and stop. +- **API errors**: Retry up to 3 times with 10-second backoff, then report and stop. + +## Notes + +- This skill runs in a `copilot --yolo` session on a Dev Box, executing as the authenticated user. +- The skill does NOT mark the PR as "Ready for review" — that is a separate skill. +- The `gh api .../actions/runs/{id}/approve` endpoint is the programmatic equivalent of the "Approve and run" button in the GitHub UI. +- Review comments from bots/agents are treated the same as CI failures for iteration purposes. +- **Do NOT edit any plan/checklist files** (e.g., `1810-ignorance-reduction-for-implementation-plan.md`) to mark tasks as complete. Marking checklist items is outside the scope of this skill. diff --git a/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md b/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md new file mode 100644 index 000000000..e3106baae --- /dev/null +++ b/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md @@ -0,0 +1,361 @@ +--- +name: shepherd-task-from-ready-to-merged-to-base +description: "Use this skill to shepherd a task PR from 'Ready for review' through Copilot code review, local comment resolution, and merge to the specified base branch." +--- + +# Skill: Shepherd Task from Ready for Review to Merged + +## Purpose + +Automate the lifecycle of a task PR from marking as **Ready for review** through Copilot code review comment resolution and merge to the specified base branch. This is a follow-up skill intended to be run after `shepherd-task-from-assignment-to-ready`. + +## Inputs + +- `TASK_ISSUE`: The issue number (e.g., `1850`) or URL of the child task. +- `BASE_BRANCH`: The base branch the task PR should target (e.g., `edburns/1810-java-tool-ergonomics-tool-as-lambda`). +- `REPO`: Repository in `OWNER/REPO` format (default: `github/copilot-sdk`). +- `REMOTE`: Git remote to push to (default: `upstream`). + +## Prerequisites + +- The `shepherd-task-from-assignment-to-ready` skill has completed successfully for this task. +- `PR_NUMBER` is known (the PR created by Copilot for this task). For discussion: `jtbdtask-pr`. +- `gh` CLI authenticated with sufficient permissions. +- The PR is currently in draft state with all CI checks passing. + +--- + +## Procedure + +### Step 0: Find the PR + +Use the same multi-strategy approach as the assignment skill: + +1. **Issue timeline** — query `gh api "/repos/$REPO/issues/$TASK_ISSUE/timeline"` for cross-referenced open PRs. +2. **PR body search** — search open PR bodies for `#$TASK_ISSUE`. +3. **Title/branch match** — regex match on title or headRefName. + +If none of these find the PR, fail the skill and report the error. + +### Step 1: Mark the PR as Ready for Review + +```bash +gh pr ready $PR_NUMBER -R $REPO +``` + +### Step 2: Wait for Copilot code review agent to complete + +The act of marking as Ready for Review triggers the Copilot code review agent. Wait for it to post its findings. + +Poll the PR reviews and comments using **multiple detection strategies** (any match is sufficient): + +**Strategy A:** A review whose body matches `"Copilot.s findings"` (original format). + +**Strategy B:** A review whose body matches `"Pull request overview"` (alternate format). + +**Strategy C:** A review from a user whose login contains `"copilot-pull-request-reviewer"` (handles `[bot]` suffix). + +**Strategy D:** Line-level review comments from user `Copilot` on the PR. + +```bash +# Poll every 30 seconds for up to 10 minutes +TIMEOUT=600 +INTERVAL=30 +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + # Strategy A: Original "Copilot's findings" header + FINDINGS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.body | test("Copilot.s findings")) | {id: .id, body: .body}' 2>/dev/null | tail -1) + + # Strategy B: Alternate "Pull request overview" header + if [ -z "$FINDINGS" ]; then + FINDINGS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.body | test("Pull request overview")) | {id: .id, body: .body}' 2>/dev/null | tail -1) + fi + + # Strategy C: Any review from the copilot-pull-request-reviewer bot + if [ -z "$FINDINGS" ]; then + FINDINGS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.user.login | test("copilot-pull-request-reviewer")) | {id: .id, body: .body}' 2>/dev/null | tail -1) + fi + + # Strategy D: Line-level comments from user "Copilot" + if [ -z "$FINDINGS" ]; then + FINDINGS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" \ + --jq '.[] | select(.user.login == "Copilot") | {id: .id, body: .body}' 2>/dev/null | head -1) + fi + + if [ -n "$FINDINGS" ]; then + break + fi + + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) +done +``` + +Search for similar text to identify the batch of review findings (`jtbdtask-pr-comments`). + +If **Comments generated: 0** (or no comments for this round), skip to **Step 15**. + +When `jtbdtask-pr-comments` has been identified, proceed. + +### Step 3: Determine N (number of comments) + +❌❌❌ DO NOT TAKE ANY ACTION ON COMMENTS ALREADY MARKED **Resolved**. ❌❌❌ + +Extract the number of comments from the **Comments generated:** line in the findings header. There will be exactly N individual review comments in this batch to address. + +### Step 4: Fetch upstream and set up local worktree + +❌❌❌ This part of the work does not use the remote agent. All comment resolution is done locally. ❌❌❌ + +```bash +# Fetch upstream to get the topic branch +git fetch upstream + +# Get the currently logged in username +GH_CURRENT_USER=$(gh api /user --jq '.login') + +# Get the topic branch name for the PR +JTBDTASK_BRANCH=$(gh pr view $PR_NUMBER -R $REPO --json headRefName --jq '.headRefName') + +# Create a worktree for local review work — as a SIBLING of the current repo clone, not inside it. +git worktree add "../review-copilot-pr-$PR_NUMBER" "upstream/$JTBDTASK_BRANCH" +``` + +For discussion, this worktree is the `jtbdtask-pr-comments-comment-worktree`. + +### Step 5: Approve workflows and wait for completion + +Invoke the **`shepherd-task-approve-workflows-and-wait-for-completion`** skill (`skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md`) with: + +- `REPO` = `$REPO` +- `JTBDTASK_BRANCH` = the PR's topic branch +- `PR_NUMBER` = `$PR_NUMBER` + +This ensures any pending workflow runs triggered by prior pushes are approved and complete before gathering review comments. + +### Step 6: Gather all review comments + +```bash +# Get all review comments from the Copilot code review batch. +# The reviewer may appear as "copilot-pull-request-reviewer[bot]" or "Copilot" depending on the repo. +gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" \ + --jq '.[] | select(.user.login | test("copilot-pull-request-reviewer|Copilot")) | {id: .id, path: .path, line: .line, body: .body, in_reply_to_id: .in_reply_to_id}' +``` + +Identify each individual comment. Each has a unique `id` (e.g., `discussion_r3456155645`-style reference). For discussion, each is a `jtbdtask-pr-comments-comment`. + +### Step 7: Address each review comment locally + +For each review comment (`jtbdtask-pr-comments-comment`), working in the `jtbdtask-pr-comments-comment-worktree`: + +#### 7.1: Evaluate the comment + +- Carefully consider the comment and judge its merit. +- **If there is no merit:** mark the comment as resolved with an explanatory note (defer the resolution reply until Step 9). +- **If there is merit:** evaluate the suggested remedy. + - If you agree with the suggested remedy, proceed with it. + - If you disagree with the suggested remedy, devise a better remedy and proceed with that. + +#### 7.2: Implement the fix + +- Implement the remedy in the `jtbdtask-pr-comments-comment-worktree`. +- Use the appropriate language coding skill in `skills/` to know how to run tests. +- If there are a large number of tests: + - ❌❌❌ DO NOT RUN THE FULL TEST SUITE at this stage. ❌❌❌ . + - ✅✅✅ Only run the tests directly related to the fix, in isolation. ✅✅✅ . +- Otherwise, if there is not a large number of tests: + - Run all the tests. +- **If the commit touches any Java code, YOU MUST ALWAYS RUN `mvn spotless:apply` in the java directory before each commit.** + +#### 7.3: Commit locally (do not push) + +- Once the relevant tests pass, commit the fix. +- ❌❌❌ Do NOT push yet. ❌❌❌ +- Keep track of the commit hash — you will need it when replying to the review comment. + +### Step 8: Push all fixes to `$REMOTE` + +Once **all** N review comments have been addressed locally: + +```bash +# Push from the worktree to the configured remote (sibling directory) +cd "../review-copilot-pr-$PR_NUMBER" +git push "$REMOTE" HEAD:$JTBDTASK_BRANCH +``` + +### Step 9: Reply to each review comment and resolve the thread + +For each `jtbdtask-pr-comments-comment`: + +1. State what you did to address the comment. If the action corresponds to a commit, include the hash: "Fixed in ``". +2. Reply to the comment. +3. Resolve the review thread. + +To reply to the comment: + +```bash +# Reply to a specific review comment +gh api --method POST "/repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \ + -f "body=Fixed in $COMMIT_HASH. [explanation of the fix]" +``` + +To resolve the thread, use the GraphQL API (the REST API does not support thread resolution): + +```bash +# 1. Get the GraphQL thread node ID for the comment +THREAD_ID=$(gh api graphql -F number=$PR_NUMBER -f query=' +query($number: Int!) { + repository(owner: "github", name: "copilot-sdk") { + pullRequest(number: $number) { + reviewThreads(first: 100) { + nodes { + id + isResolved + comments(first: 1) { nodes { databaseId } } + } + } + } + } +}' --jq ".data.repository.pullRequest.reviewThreads.nodes[] | select(.comments.nodes[0].databaseId == $COMMENT_ID) | .id") + +# 2. Resolve the thread +gh api graphql -f query=" +mutation { + resolveReviewThread(input: {threadId: \"$THREAD_ID\"}) { + thread { id isResolved } + } +}" +``` + +### Step 10: Wait for CI to run + +The push triggers CI/CD. Use the same approach as `shepherd-task-from-assignment-to-ready` to: + +1. Wait for workflow runs to complete (`gh pr checks $PR_NUMBER -R $REPO --watch`). +2. Evaluate results (excluding the expected "Block remove-before-merge paths" / "No remove-before-merge directories" failure). +3. If there are real CI failures, gather logs and fix locally, commit, and push again. Repeat until CI passes. + +**Note:** Ignore failures from the "Block remove-before-merge paths" / "No remove-before-merge directories" workflow. This failure is expected on feature branches and is not a real problem. + +### Step 11: Approve workflows and wait for completion + +Invoke the **`shepherd-task-approve-workflows-and-wait-for-completion`** skill (`skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md`) with: + +- `REPO` = `$REPO` +- `JTBDTASK_BRANCH` = the PR's topic branch +- `PR_NUMBER` = `$PR_NUMBER` + +This ensures any pending workflow runs triggered by the push in Step 8 are approved and complete before re-requesting review. + +### Step 12: Re-request Copilot review + +```bash +gh pr edit $PR_NUMBER -R $REPO --add-reviewer "copilot-pull-request-reviewer" +``` + +### Step 13: Loop back + +Go back to **Step 2**. Wait for the Copilot code review agent to post new findings. + +**Max iterations: 8.** If exhausted, report failure and stop: + +``` +SHEPHERD FAILED: Exhausted 20 iterations on PR #$PR_NUMBER for task #$TASK_ISSUE. +Manual intervention required. +``` + +### Step 14: Approve workflows and wait for completion + +Invoke the **`shepherd-task-approve-workflows-and-wait-for-completion`** skill (`skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md`) with: + +- `REPO` = `$REPO` +- `JTBDTASK_BRANCH` = the PR's topic branch +- `PR_NUMBER` = `$PR_NUMBER` + +This ensures any pending workflow runs are approved and complete before performing final checks. + +### Step 15: Final checks before merge + +Verify: + +- The only failed check is "Block remove-before-merge paths" / "No remove-before-merge directories". +- All other checks pass. + +### Step 16: Clean up worktree + +```bash +# Remove the worktree (sibling directory) +git worktree remove "../review-copilot-pr-$PR_NUMBER" + +# Remove the local branch tracking the PR topic branch (if created) +git branch -D "$JTBDTASK_BRANCH" 2>/dev/null || true +``` + +### Step 17: Verify base branch + +❌❌❌ Ensure the base branch is NEVER `main` ❌❌❌ and always the `BASE_BRANCH` from this invocation. + +```bash +ACTUAL_BASE=$(gh pr view $PR_NUMBER -R $REPO --json baseRefName --jq '.baseRefName') +if [ "$ACTUAL_BASE" = "main" ]; then + echo "ERROR: PR base is 'main' — must be '$BASE_BRANCH'. Fixing..." + gh pr edit $PR_NUMBER -R $REPO --base "$BASE_BRANCH" +fi +``` + +### Step 18: Handle merge conflicts + +If there are conflicts between the PR branch and `BASE_BRANCH`: + +```bash +# Check for merge conflicts +MERGEABLE=$(gh pr view $PR_NUMBER -R $REPO --json mergeable --jq '.mergeable') +if [ "$MERGEABLE" = "CONFLICTING" ]; then + # Resolve conflicts locally in the worktree (sibling directory) + cd "../review-copilot-pr-$PR_NUMBER" + git fetch upstream + git rebase "upstream/$BASE_BRANCH" + # Resolve conflicts, then: + git rebase --continue + git push "$REMOTE" HEAD:$JTBDTASK_BRANCH --force-with-lease +fi +``` + +### Step 19: Merge the PR + +```bash +gh pr merge $PR_NUMBER -R $REPO --merge --delete-branch +``` + +This merges the work to `BASE_BRANCH`. + +### Step 20: Close the corresponding issue + +```bash +gh issue close $TASK_ISSUE -R $REPO +``` + +### Step 21: Final status report + +``` +SHEPHERD COMPLETE: PR #$PR_NUMBER for task #$TASK_ISSUE has been merged to $BASE_BRANCH. +``` + +--- + +## Error handling + +- **Copilot review agent doesn't post within 10 minutes**: Report and stop. +- **8 iterations exhausted**: Report and stop. +- **Merge conflicts that cannot be auto-resolved**: Report and stop. +- **API errors**: Retry up to 3 times with 10-second backoff, then report and stop. + +## Notes + +- This skill runs in a `copilot --yolo` session on a Dev Box, executing as the authenticated user. +- All review comment resolution is done **locally** — not via the remote Copilot coding agent. +- **Do NOT edit any plan/checklist files** (e.g., `1810-ignorance-reduction-for-implementation-plan.md`) to mark tasks as complete. Marking checklist items is outside the scope of this skill.