Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/docs/src/content/docs/extend/github-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export const plugins = defineJuniorPlugins([

`subscribeAfterCreate` creates a temporary watch after a successful `github_createPullRequest` call. It only runs in Slack conversations that can host watches, and only when GitHub webhooks are enabled. Forced events are removed from the tool result's suggested events so the model does not re-watch them. The watch still expires normally.

This watch always includes `pull_request.review.approved` and `pull_request.merged`, even when `events` omits them. When the pull request is approved or merged, Junior mentions the people involved in the thread. These are the pull request author and assignees, the commit authors, and the people in `Co-authored-by` commit trailers. Junior only mentions people whose GitHub account or email links to a Slack user. When the reviewer or commenter links to a Slack user, Junior knows that the GitHub login and the Slack user are the same person, so it does not name them twice.
This watch always includes `pull_request.review.approved` and `pull_request.merged`, even when `events` omits them. When the pull request is approved or merged, Junior mentions the people involved in the thread. These are the pull request author and assignees, the commit authors, and the people in `Co-authored-by` commit trailers. Junior only mentions people whose GitHub account or email links to a Slack user. When the reviewer or commenter links to a Slack user, Junior knows that the GitHub login and the Slack user are the same person, so it does not name them twice. When the merged pull request is in a stack, Junior also restacks the open pull requests above it whose branches it pushed, and resolves conflicts. GitHub rebases the next pull request in a stack after a merge, but it stops at a conflict and sends no event for it.

`guidance` adds short app guidance when a matching pull request event reaches the agent. It applies within the watch or event automation instruction. It cannot replace or expand that instruction, grant credentials, or bypass action review. Keep each value short.

Expand Down Expand Up @@ -532,7 +532,9 @@ read can be unknown, and a later event reads it again. A pull request that gets
no other events is not checked. Trusted data includes `baseBranch`, `headSha`,
and `url`. Filters can match only `repo` and `pullRequest`. The watch that
Junior creates after it opens a pull request always gets this event, and the
default guidance tells the agent to merge the base branch, not rebase.
default guidance tells the agent to merge the base branch, not rebase. For a
pull request in a stack, the guidance tells the agent to restack with
`gh stack rebase`, because a stack merges only with a linear history.

</details>

Expand Down
2 changes: 1 addition & 1 deletion packages/junior-github/skills/github-code/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Guide evidence-first GitHub repository work from inspection through a reviewable
- Treat shallow clones as inspection checkouts; deepen before history work; never force-push around missing ancestry.
- Install dependencies with the lockfile frozen mode before verification when needed.
- Finish every code change with a pushed PR, without asking, unless the user opts out or asked for investigation only. Open it ready when work and checks are done; open a draft when work, checks, or decisions remain. An open scope question goes in the PR body, not in a stop.
- After each push to a branch with an open PR, check mergeability. Resolve conflicts without asking, by merging the PR base branch (usually the default branch). Never rebase for this. GitHub sends no event for a new conflict, and conflicted PRs get no new checks. The plugin publishes `pull_request.merge_conflict` for a watched PR only when another webhook event arrives for it, so the post-push check stays.
- After each push to a branch with an open PR, check mergeability. Resolve conflicts without asking, by merging the PR base branch (usually the default branch). Never rebase for this, except in a stack: a stack merges only with a linear history, so stacked PRs restack with `gh stack`. When a stacked PR merges, restack the open PRs above it, because GitHub sends them no event when its own rebase stops at a conflict. GitHub sends no event for a new conflict, and conflicted PRs get no new checks. The plugin publishes `pull_request.merge_conflict` for a watched PR only when another webhook event arrives for it, so the post-push check stays.
- Write conventional PR titles and short plain-English bodies; keep check results out of the PR body.
- Report exact validation and permission failures.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Open this when making repository edits, not for read-only inspection.
- Run `gh pr view NUMBER --repo owner/repo --json mergeable,baseRefName`.
- `UNKNOWN`: GitHub is still calculating it. Wait about 5 seconds and check again, up to 3 times. If it stays `UNKNOWN`, report that and continue.
- `CONFLICTING`: resolve it now. Do not ask first. GitHub checks mergeability against the PR base (`baseRefName`), which is usually the default branch. Fetch the base and merge `origin/BASE` into the PR branch. Do not rebase. Resolve each conflict so that both the PR change and the base branch change stay intact. Run the checks again, push, and check mergeability again.
- Stacked PR (`gh api 'repos/owner/repo/stacks?pull_request=NUMBER'` returns a stack): a stack merges only with a linear history, so rebase instead of merge. Run `gh stack checkout NUMBER` and `gh stack rebase`, resolve each conflict, run `gh stack rebase --continue`, run the checks, and `gh stack push`.
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Resolved
4. Resolve the actual default branch.
5. Update an existing PR for the branch with `github_updatePullRequest`, or create with `github_createPullRequest` (`draft: true` only when work, checks, or decisions remain; otherwise ready for review).
6. PR title matches the current dominant change, same conventional form as commits.
Expand All @@ -48,4 +49,4 @@ Open this when making repository edits, not for read-only inspection.

## Follow

When PR creation returns a subscribable hint, subscribe to suggested review/CI events. Fix routine review feedback and failed checks without a thread update. Reply only when the requester needs to act, or when the PR merges or closes.
When PR creation returns a subscribable hint, subscribe to suggested review/CI events. Fix routine review feedback and failed checks without a thread update. When a PR in a stack merges, restack the open PRs above it with `gh stack sync` before you reply; GitHub sends them no event when its own rebase stops at a conflict. Reply only when the requester needs to act, or when the PR merges or closes.
18 changes: 14 additions & 4 deletions packages/junior-github/src/events/pull-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export const GITHUB_PULL_REQUEST_EVENTS = [
export type GitHubPullRequestEvent =
(typeof GITHUB_PULL_REQUEST_EVENTS)[number];

/**
* A stack merges only with a linear history, so a stacked pull request is
* rebased, never merged with its base. GitHub rebases the next pull request
* after a merge, but it stops at a conflict and sends no event for it.
*/
const STACK_CHECK =
"Check if the pull request is in a stack: `gh api 'repos/OWNER/REPO/stacks?pull_request=NUMBER'`.";
const STACK_CONFLICT_STEPS =
"run `gh stack rebase`, resolve each conflict so that both changes stay intact, stage the files, and run `gh stack rebase --continue`. Run the checks again, run `gh stack push`, and confirm that each open pull request in the stack can merge. Report the files that had conflicts.";

const GITHUB_PULL_REQUEST_EVENT_GUIDANCE: Partial<
Record<GitHubPullRequestEvent, string>
> = {
Expand All @@ -39,8 +49,7 @@ const GITHUB_PULL_REQUEST_EVENT_GUIDANCE: Partial<
'After acting on actionable feedback, call github_updatePullRequestFeedback with the exact repo, commentId, and commentKind from Verified details, and status "addressed" or "declined".',
"pull_request.review_comment.created":
'After acting on actionable feedback, call github_updatePullRequestFeedback with the exact repo, commentId, and commentKind from Verified details, and status "addressed" or "declined". Then resolve the thread with github_resolvePullRequestReviewThread.',
"pull_request.merge_conflict":
"Fetch baseBranch from Verified details and merge it into the pull request branch. Do not rebase. Resolve each conflict so that both the pull request change and the base branch change stay intact. Run the checks again, push, and confirm that the pull request can merge. Report the files that had conflicts.",
"pull_request.merge_conflict": `${STACK_CHECK} If it is, run \`gh stack checkout NUMBER\`, then ${STACK_CONFLICT_STEPS} If it is not, fetch baseBranch from Verified details and merge it into the pull request branch. Do not rebase. Resolve each conflict so that both the pull request change and the base branch change stay intact. Run the checks again, push, and confirm that the pull request can merge. Report the files that had conflicts.`,
};

/** Combine provider defaults with app guidance for pull request events. */
Expand Down Expand Up @@ -75,12 +84,13 @@ export interface GitHubPullRequestSubscriptionConfig {
* Watch guidance for outcomes that the people involved need to hear about.
* The Watch made after Junior creates a pull request always receives these
* events. The webhook adds the people involved in the pull request to them.
* A merge also restacks the open pull requests above it, because Junior owns
* that stack and GitHub sends them no event when its own rebase stops.
*/
const GITHUB_PULL_REQUEST_OUTCOME_GUIDANCE = {
"pull_request.review.approved":
"Mention each person in People involved, or the person who asked for the pull request if that line is missing. Say who approved it and link it. Do not merge it unless asked.",
"pull_request.merged":
"Mention each person in People involved, or the person who asked for the pull request if that line is missing. Say that it merged and link it.",
"pull_request.merged": `${STACK_CHECK} If open pull requests in the stack remain above it and Junior pushed their branches, restack them before you reply. Do not ask first. Run \`gh stack checkout NUMBER\` with the lowest open pull request, then \`gh stack sync\`. If sync reports a conflict, ${STACK_CONFLICT_STEPS} Then mention each person in People involved, or the person who asked for the pull request if that line is missing. Say that it merged and link it. When you restacked, say so in the same reply.`,
} as const satisfies Partial<Record<GitHubPullRequestEvent, string>>;

/** Build the Watch created after Junior creates a pull request. */
Expand Down
7 changes: 4 additions & 3 deletions packages/junior-github/tests/github-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,8 @@ Conversation: \`local:test:old-conversation\`
});
// Merge conflicts always reach the Watch, so Junior keeps its pull
// request ready to merge. Approval and merge always reach the people
// involved, so the Watch stores guidance for those outcomes.
// involved, so the Watch stores guidance for those outcomes. A merge
// restacks the open pull requests above it first.
expect(subscribe).toHaveBeenCalledWith({
events: [
"pull_request.checks.failed",
Expand All @@ -1640,8 +1641,8 @@ Conversation: \`local:test:old-conversation\`
"pull_request.review.approved": expect.stringContaining(
"Mention each person in People involved",
),
"pull_request.merged": expect.stringContaining(
"Mention each person in People involved",
"pull_request.merged": expect.stringMatching(
/gh stack sync[\s\S]*mention each person in People involved/,
),
},
intent: "Report failed checks and requested changes.",
Expand Down
Loading