diff --git a/.github/skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md b/.github/skills/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md new file mode 100644 index 0000000000..c7f5513881 --- /dev/null +++ b/.github/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/.github/skills/shepherd-task-approve-workflows-and-wait-for-completion/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md b/.github/skills/shepherd-task-approve-workflows-and-wait-for-completion/shepherd-task-approve-workflows-and-wait-for-completion/SKILL.md new file mode 100644 index 0000000000..c7f5513881 --- /dev/null +++ b/.github/skills/shepherd-task-approve-workflows-and-wait-for-completion/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/.github/skills/shepherd-task-from-assignment-to-ready/SKILL.md b/.github/skills/shepherd-task-from-assignment-to-ready/SKILL.md new file mode 100644 index 0000000000..c6b55d1cb9 --- /dev/null +++ b/.github/skills/shepherd-task-from-assignment-to-ready/SKILL.md @@ -0,0 +1,330 @@ +--- +name: shepherd-task-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 (`.github/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/.github/skills/shepherd-task-from-assignment-to-ready/shepherd-task-from-assignment-to-ready/SKILL.md b/.github/skills/shepherd-task-from-assignment-to-ready/shepherd-task-from-assignment-to-ready/SKILL.md new file mode 100644 index 0000000000..c6b55d1cb9 --- /dev/null +++ b/.github/skills/shepherd-task-from-assignment-to-ready/shepherd-task-from-assignment-to-ready/SKILL.md @@ -0,0 +1,330 @@ +--- +name: shepherd-task-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 (`.github/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/.github/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md b/.github/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md new file mode 100644 index 0000000000..75efb20c95 --- /dev/null +++ b/.github/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md @@ -0,0 +1,355 @@ +--- +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) + +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 (`.github/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 `.github/skills/` to know how to run tests. +- ❌❌❌ DO NOT RUN THE FULL TEST SUITE at this stage. ❌❌❌ Only run the tests directly related to the fix, in isolation. +- **If the commit has to do with Java, 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 (`.github/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 (`.github/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. diff --git a/.github/skills/shepherd-task-from-ready-to-merged-to-base/shepherd-task-from-ready-to-merged-to-base/SKILL.md b/.github/skills/shepherd-task-from-ready-to-merged-to-base/shepherd-task-from-ready-to-merged-to-base/SKILL.md new file mode 100644 index 0000000000..deca963b13 --- /dev/null +++ b/.github/skills/shepherd-task-from-ready-to-merged-to-base/shepherd-task-from-ready-to-merged-to-base/SKILL.md @@ -0,0 +1,356 @@ +--- +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`). + +## 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 (`.github/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 `.github/skills/` to know how to run tests. +- ❌❌❌ DO NOT RUN THE FULL TEST SUITE at this stage. ❌❌❌ Only run the tests directly related to the fix, in isolation. +- **If the commit has to do with Java, 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 upstream + +Once **all** N review comments have been addressed locally: + +```bash +# Push from the worktree to upstream (sibling directory) +cd "../review-copilot-pr-$PR_NUMBER" +git push upstream 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 (`.github/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 (`.github/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 upstream 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. diff --git a/ghcp-sdk-1893-remove-before-merge/20260707-prompts.md b/ghcp-sdk-1893-remove-before-merge/20260707-prompts.md new file mode 100644 index 0000000000..e285922158 --- /dev/null +++ b/ghcp-sdk-1893-remove-before-merge/20260707-prompts.md @@ -0,0 +1,207 @@ +### 13:23 Prompt + +### Context + +Work in this topic branch iteratively develops a system that facilitates using a combination of GitHub Coding Agent (CCA), `gh` CLI, Copilot CLI, and the locally configured dev system to shepherd sufficiently specified issues of type **Task** in the current repository's GitHub issue tracker through the steps listed below. + +#### Inputs to the system: + +- `REPO` the repo in which the work happens. +- `TASK_ISSUE` the issue number in the issue tracker for `REPO` +- `BASE_BRANCH` the base branch from which CCA should make its branch when it does its work. **This is never `main`**. +``` +#### Preconditions + +- CCA and code review agent fully installed and appropriately configured in `REPO`. +- Local system has `REPO` cloned and fully configured for local development using `copilot --yolo`. +- There exist N sufficiently specified issues in the issue tracker for `REPO`. + +#### High level system steps + +##### Skill `copilot-sdk-01/.github/skills/shepherd-task-from-assignment-to-ready/SKILL.md` + +1. `TASK_ISSUE` is unassigned. +2. `TASK_ISSUE` is assigned to `@Copilot`. +3. CCA creates a PR and topic branch from `BASE_BRANCH` for the issue. + **IMPORTANT: The topic branch created by CCA must be a branch from the specified `BASE_BRANCH`.** +4. Skill gets CCA to get a clean CI run. Loop until retries exceeded or clean run obtained. + a. CCA does work and pushes commits to the PR's topic branch. + b. This requires user action to press the **Approve workflows to run** button in the PR. + c. Skill causes the button to be pressed. +5. Skill addresses comments that appear before marking the issue as **Ready to review**. + +##### Skill `copilot-sdk-01/.github/skills/shepherd-task-from-ready-to-merged-to-base/SKILL.md` + +1. Skill causes the PR to be marked **Ready for review**. +2. Skill waits for copilot review agent to complete review and post its findings. +3. Skill uses local `copilot --yolo` with `git worktree` to attend to each review comment. Loop until retries exceeted or all issues addressed. + a. Evaluate the current comment. Discern if it needs action or not. + b. If action is necessary, make a git worktree, address the comment locally, and make a commit. Keep track of the commit hash. +4. Same loop as in the previous skill to get a clean CI run. +5. When a clean CI run is obtained, merge the PR to `BASE_BRANCH`. + +### Question: + +I need a 100% foolproof way to ensure the **IMPORTANT** comment in step 3 of the first skill happens. So far I have not found it. I'm about to file an issue in the issue tracker for `gh`. But before I do that, I need to do my homework. + +### 13:49 Response + +Thought for 3s +The user wants to ensure that when CCA (GitHub Coding Agent) creates a PR and topic branch, it branches from the specified `BASE_BRANCH` (not `main`). They want to understand what options exist before filing a `gh` CLI issue. +Let me look at the current skill implementation to understand what's being done, and then think about what options exist to enforce the base branch constraint. +Let me first look at the skill files to understand the current implementation. + +Let me look at the current skill implementation to understand what's in place. + +List directory .github/skills/shepherd-task-from-assignment-to-ready 1 file found + +Search "**/*.md" in .github/skills 8 files found + +Read SKILL.md 372 lines read + +Thought for 19s +Now I have a thorough understanding of what's in place. The user wants to find a "100% foolproof" way to ensure CCA creates its branch from `BASE_BRANCH`, not `main`. +The current approach uses: +1. Prepend a `[!IMPORTANT]` callout to the issue body before assignment +2. Assign to @Copilot +3. Post a reinforcing comment after assignment +4. After PR creation, detect if wrong base and use `gh pr edit --base` + `gh pr review --request-changes` +The user is asking what options exist from the `gh` CLI perspective before filing a `gh` issue. They want to know if there's a way to *force* CCA to use a specific base branch via the `gh` CLI itself. +Looking at the `gh` CLI capabilities, there's no way to directly specify a base branch when creating or editing an issue—that's a PR concept, not an issue one. When I assign to @Copilot via `gh issue edit`, CCA reads the issue content to decide what branch to create, but there's no programmatic flag to force a specific base branch. The `gh copilot` subcommand doesn't offer this control either. So before filing that issue, I should verify whether there's actually any programmatic way to control this. I'm checking if there's a `gh` CLI option, a GitHub API endpoint for CCA configuration, or a repository setting that could help, but based on what I know, there doesn't appear to be any way to specify the base branch when CCA creates a PR from an issue. any existing GitHub issues or discussions about this feature gap, then pull together what's currently available versus what's missing. + +Let me check the current `gh` CLI capabilities around this and look for any existing mechanisms. + +Shell Check gh issue and copilot subcommand options 75 lines… +gh issue --help 2>&1 | head -40; echo "---"; gh copilot --help 2>&1 | head -40 +Shell Check gh api and issue edit options 59 lines… +gh api --help 2>&1 | head -20; echo "---"; gh issue edit --help 2>&1 | head -40 +Shell Check gh issue develop - the linked branches command 36 lines… +gh issue develop --help 2>&1 + +Web Search (MCP: github-mcp-serve… {"type":"output_text","text":{"value":"It sounds like you want to **programma... + +Thought for 5s +The web search wasn't very helpful. Let me think about this more carefully and search specifically for GitHub's CCA (Coding Agent) documentation about base branch control. +Key findings so far: +1. `gh issue develop --base ` can create a branch linked to an issue from a specific base - this is potentially interesting! +2. There's no `gh issue edit` flag for specifying a base branch for CCA +3. The `gh copilot` subcommand isn't even installed in this environment +The interesting discovery is `gh issue develop --base `. If you pre-create the branch and link it to the issue via `gh issue develop`, CCA might pick up that linked branch and use it as its starting point rather than branching from `main`. +Now I need to dig deeper into how CCA actually determines which branch to use as its base when generating code. + +Web Search (MCP: github-mcp-serve… {"type":"output_text","text":{"value":"The GitHub Copilot **coding agent** ha... +Web Search (MCP: github-mcp-serve… {"type":"output_text","text":{"value":"Certainly! Here is a **feature request... + +Thought for 3s +The search results aren't giving me the ground truth about how CCA specifically behaves with `gh issue develop`. Let me look more specifically at: +1. What `gh issue develop` actually does in terms of GitHub's API +2. Whether CCA respects pre-linked branches created via `gh issue develop` +3. The GitHub docs for CCA +The key insight here is `gh issue develop --base `. If you pre-create a linked development branch from `BASE_BRANCH` using this command, GitHub's UI shows that branch as the "linked branch" for the issue. +The question is: does CCA (GitHub Coding Agent) respect this linked branch when creating its PR? Or does it always start fresh from the default branch? +I need to check the official GitHub documentation for CCA to see how it handles linked branches. + +Web Search (MCP: github-mcp-serve… {"type":"output_text","text":{"value":"It sounds like you're asking about **G... +Web Search (MCP: github-mcp-serve… {"type":"output_text","text":{"value":"To configure the **base branch** and P... + +Thought for 1s +Let me now look at the actual GitHub docs page for creating a PR with the Copilot coding agent to understand what's documented about base branch control. + +Fetching web content WebFetchRedirectError: web_fetch refused to follow redirect 302 from https://docs.github.com/ +Fetching web content https://docs… Contents of https://docs.github.com/en/copilot/how-tos/use-copilot-agents/clo... +Fetching web content https://docs… Contents of https://docs.github.com/en/copilot/how-tos/use-copilot-agents/clo... +Fetching web content https://docs… Contents of https://docs.github.com/en/copilot/how-tos/use-copilot-agents/clo... + +Thought for 15s +This is the key finding. There IS a 100% foolproof way to control the base branch when assigning CCA to an issue — the `agent_assignment` parameter with `base_branch` field in the REST API, and the `agentAssignment.baseRef` in the GraphQL API. +So the user doesn't need to file a `gh` issue about this! The capability already exists — it's just: +1. REST API: `PATCH /repos/OWNER/REPO/issues/ISSUE_NUMBER` with `agent_assignment.base_branch` +2. GraphQL API: mutations with `agentAssignment.baseRef` +3. `gh agent-task create --base `: A newer CLI command available in gh v2.80.0 and later +The real issue is that the current skill uses `gh issue edit --add-assignee "@copilot"`, which doesn't pass the base branch parameter at all. I need to replace that command with a proper REST API call that includes `agent_assignment.base_branch` to actually control which branch CCA operates on. +Writing the API call... +I'm realizing there's actually a newer `gh agent-task create` command available in version 2.80.0 and above that handles this directly, so the user might not need to manually construct these API calls at all. Let me show them this discovery and offer to update their skill with the right approach. + +The capability already exists — no `gh` issue needed. The GitHub docs reveal two 100% reliable approaches: + +──────────────────── + +Root cause of the current failure + +`gh issue edit --add-assignee "@copilot"` uses the plain issues assignee endpoint, which has no `base_branch` parameter. CCA then defaults to `main`. + +──────────────────── + +Solution 1: REST API `agent_assignment.base_branch` (available now) + +Replace the current `gh issue edit --add-assignee` call with: + +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\" + } + }" + +This is documented at use-cloud-agent-via-the-api. + +Solution 2: `gh agent-task create --base` (requires gh ≥ 2.80.0) + +gh agent-task create "Fix issue #$TASK_ISSUE" \ + --base "$BASE_BRANCH" \ + --repo "$REPO" + +Documented at use-cloud-agent-from-cli. + +──────────────────── + +Recommendation: Use Solution 1 (REST API) in the skill — it works with any `gh` version and is the canonical "assign existing issue to Copilot with `base_branch`" pattern. Would you like me to update `SKILL.md` to replace step 1b with this call (and simplify/remove the now-redundant issue-body prepend and fallback rebase logic)? + +### 13:49 Prompt + +Yes, please fix the `SKILL.md` to do this. + +### 13:51 Response + +Thought for 13s +Let me update the SKILL.md to use the REST API `agent_assignment.base_branch` approach. I need to: +1. Replace step 1b (the `gh issue edit --add-assignee "@copilot"` call) with the proper REST API call +2. Simplify/remove the now-redundant issue-body prepend (step 1a) and reinforcing comment (step 1c) - these are workarounds that are no longer needed +3. Remove/simplify the fallback rebase logic in Step 2 +Let me think about what to keep: +- Step 1a (prepend IMPORTANT to issue body): This was purely a workaround. We can remove it. +- Step 1b (assign): Replace with the REST API call with `agent_assignment.base_branch` +- Step 1c (reinforcing comment): Also a workaround. Remove it. +- Step 2 fallback (detect wrong base and fix): This can now be simplified since CCA will use the right base, but keeping it as a safety check might still be useful. +Actually, let me reconsider what needs to stay. The user wants me to update SKILL.md to use the new API, so I should think strategically about what to keep and what to cut. Step 1a was purely to influence CCA's base branch choice, and now that we have the API, I can remove it or at least simplify it to just update the issue body without the base branch instruction header. Step 1b becomes the REST API call, step 1c was also just influencing CCA so that goes, and step 2 becomes a simpler safety verification since the API guarantees the base branch—no need for the rebase instruction anymore. +Updating SKILL.md... + +I'll make surgical edits: remove the now-redundant workaround steps (1a, 1c, and the fallback rebase) and replace step 1b with the authoritative REST API call. + +Read SKILL.md L28:115 (87 lines read) +Read SKILL.md L178:200 (23 lines read) + +Now I have everything I need. Making the three targeted edits: + +Edit SKILL.md +28 -70 + +Now replace the fallback rebase block in Step 2 with a lightweight sanity assertion: + +Edit SKILL.md +7 -7 + +Let me verify the result looks clean: + +Read SKILL.md L28:130 (103 lines read) +Read SKILL.md L130:175 (45 lines read) + +Looks clean. The changes are: + +1. Step 1 — Replaced the entire 3-sub-step workaround (prepend IMPORTANT body, plain  gh issue edit --add-assignee , reinforcing comment) with a single  gh api  REST call that passes  agent_assignment.base_branch  directly to CCA. Includes a  [!NOTE]  warning against the old  gh issue edit  approach. +2. Step 2 sanity check — Replaced the "fix and request rebase" fallback (which was the last line of defense against the wrong-base problem) with a hard  exit 1  assertion. Since Step 1 now guarantees the right base, a mismatch here indicates a genuine problem requiring manual intervention, not something to silently patch over. + diff --git a/scripts/agentic-development/agentic-development/shepherd-task-given-list.ps1 b/scripts/agentic-development/agentic-development/shepherd-task-given-list.ps1 new file mode 100644 index 0000000000..2364559723 --- /dev/null +++ b/scripts/agentic-development/agentic-development/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/scripts/agentic-development/agentic-development/shepherd-task-given-list.sh b/scripts/agentic-development/agentic-development/shepherd-task-given-list.sh new file mode 100644 index 0000000000..de1382993b --- /dev/null +++ b/scripts/agentic-development/agentic-development/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/scripts/agentic-development/agentic-development/shepherd-task-inspect-json.ps1 b/scripts/agentic-development/agentic-development/shepherd-task-inspect-json.ps1 new file mode 100644 index 0000000000..5ad36ebb14 --- /dev/null +++ b/scripts/agentic-development/agentic-development/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/scripts/agentic-development/agentic-development/shepherd-task-inspect-json.sh b/scripts/agentic-development/agentic-development/shepherd-task-inspect-json.sh new file mode 100644 index 0000000000..ae2ab86653 --- /dev/null +++ b/scripts/agentic-development/agentic-development/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/scripts/agentic-development/agentic-development/shepherd-task.ps1 b/scripts/agentic-development/agentic-development/shepherd-task.ps1 new file mode 100644 index 0000000000..dd68367456 --- /dev/null +++ b/scripts/agentic-development/agentic-development/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-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/scripts/agentic-development/agentic-development/shepherd-task.sh b/scripts/agentic-development/agentic-development/shepherd-task.sh new file mode 100644 index 0000000000..6503258c81 --- /dev/null +++ b/scripts/agentic-development/agentic-development/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-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/scripts/agentic-development/shepherd-task-given-list.ps1 b/scripts/agentic-development/shepherd-task-given-list.ps1 new file mode 100644 index 0000000000..2364559723 --- /dev/null +++ b/scripts/agentic-development/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/scripts/agentic-development/shepherd-task-given-list.sh b/scripts/agentic-development/shepherd-task-given-list.sh new file mode 100644 index 0000000000..de1382993b --- /dev/null +++ b/scripts/agentic-development/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/scripts/agentic-development/shepherd-task-inspect-json.ps1 b/scripts/agentic-development/shepherd-task-inspect-json.ps1 new file mode 100644 index 0000000000..5ad36ebb14 --- /dev/null +++ b/scripts/agentic-development/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/scripts/agentic-development/shepherd-task-inspect-json.sh b/scripts/agentic-development/shepherd-task-inspect-json.sh new file mode 100644 index 0000000000..ae2ab86653 --- /dev/null +++ b/scripts/agentic-development/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/scripts/agentic-development/shepherd-task.ps1 b/scripts/agentic-development/shepherd-task.ps1 new file mode 100644 index 0000000000..dd68367456 --- /dev/null +++ b/scripts/agentic-development/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-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/scripts/agentic-development/shepherd-task.sh b/scripts/agentic-development/shepherd-task.sh new file mode 100644 index 0000000000..6503258c81 --- /dev/null +++ b/scripts/agentic-development/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-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