From 7b5e78116f061235023ae3c3cec9681292a0ced7 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 04:29:08 +0000 Subject: [PATCH 1/4] chore(harness): add SessionStart hook for PR template + review handling Injects the repo's PR template (or the org's `.github` repo template as a fallback) into agent context at session start, and adds guidance to always reply and resolve PR review threads when addressing review comments. Generated-By: PostHog Code Task-Id: 041ddd4c-ea89-4cd1-9697-cdae40e30a3f --- .claude/hooks/check-pr-template.sh | 77 ++++++++++++++++++++++++++++++ .claude/settings.json | 13 +++++ 2 files changed, 90 insertions(+) create mode 100755 .claude/hooks/check-pr-template.sh diff --git a/.claude/hooks/check-pr-template.sh b/.claude/hooks/check-pr-template.sh new file mode 100755 index 000000000..dda4bb185 --- /dev/null +++ b/.claude/hooks/check-pr-template.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# SessionStart hook: inject PR workflow guidance. +# - Always: instruct the agent to leave a reply and resolve each PR review +# conversation it deals with. +# - When detectable: inject the repo's PR template (or the org's `.github` +# repo template as a fallback) so PRs are opened with the right structure. + +set -uo pipefail + +cd "${CLAUDE_PROJECT_DIR:-$PWD}" 2>/dev/null || exit 0 +git rev-parse --git-dir >/dev/null 2>&1 || exit 0 + +template="" +src="" + +for path in \ + .github/pull_request_template.md \ + .github/PULL_REQUEST_TEMPLATE.md \ + .github/pull_request_template.txt \ + docs/pull_request_template.md \ + docs/PULL_REQUEST_TEMPLATE.md \ + pull_request_template.md \ + PULL_REQUEST_TEMPLATE.md +do + if [[ -f "$path" ]]; then + template=$(cat "$path") + src="repo: $path" + break + fi +done + +if [[ -z "$template" ]] && command -v gh >/dev/null 2>&1; then + remote=$(git config --get remote.origin.url 2>/dev/null || echo "") + owner=$(printf '%s' "$remote" | sed -nE 's|.*github\.com[:/]([^/]+)/.*|\1|p') + + if [[ -n "$owner" ]]; then + for path in \ + .github/pull_request_template.md \ + .github/PULL_REQUEST_TEMPLATE.md \ + profile/pull_request_template.md \ + pull_request_template.md \ + PULL_REQUEST_TEMPLATE.md + do + content=$(gh api "/repos/${owner}/.github/contents/${path}" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true) + if [[ -n "$content" ]]; then + template="$content" + src="org fallback: ${owner}/.github/${path}" + break + fi + done + fi +fi + +review_block="### PR review / comment handling +When addressing PR review comments or review threads, treat each thread as done only after BOTH of these: + 1. Reply to the thread with a short note describing what changed (and the commit SHA when useful) so the reviewer can see the resolution inline. + 2. Resolve the conversation thread. +Use the GitHub GraphQL API via \`gh\` to resolve threads, e.g.: + gh api graphql -f query='mutation(\$id:ID!){resolveReviewThread(input:{threadId:\$id}){thread{isResolved}}}' -f id=\"\" +Reply to a thread via \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`. Never silently push fixes without confirming each related thread is replied to and resolved." + +if [[ -n "$template" ]]; then + template_block=" + +### PR template +A PR template was detected (${src}). When opening a pull request with \`gh pr create\`, pass the template below as the \`--body\` argument — preserve the section headings exactly and fill each section based on the branch's actual changes. Do NOT fall back to the generic Summary/Test plan format. Keep any required PR footer (e.g. the PostHog Code attribution) appended after the template. + +--- BEGIN PR TEMPLATE --- +${template} +--- END PR TEMPLATE ---" +else + template_block="" +fi + +ctx="${review_block}${template_block}" + +jq -nc --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}' diff --git a/.claude/settings.json b/.claude/settings.json index e73b7b901..b025f55bb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,5 +6,18 @@ "Edit(./apps/cli/**)", "Write(./apps/cli/**)" ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-pr-template.sh", + "timeout": 15 + } + ] + } + ] } } From df1640642d1413877c2045f7ee68496b74f0e86b Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 04:31:52 +0000 Subject: [PATCH 2/4] chore(harness): instruct agent to link matching open issues in PR body Extend the SessionStart hook context with guidance to search for open issues that match the branch's work via `gh issue list --search` and include `Closes #N` / `Refs #N` links in the PR description. Generated-By: PostHog Code Task-Id: 041ddd4c-ea89-4cd1-9697-cdae40e30a3f --- .claude/hooks/check-pr-template.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.claude/hooks/check-pr-template.sh b/.claude/hooks/check-pr-template.sh index dda4bb185..7d4cf3a22 100755 --- a/.claude/hooks/check-pr-template.sh +++ b/.claude/hooks/check-pr-template.sh @@ -2,6 +2,8 @@ # SessionStart hook: inject PR workflow guidance. # - Always: instruct the agent to leave a reply and resolve each PR review # conversation it deals with. +# - Always: instruct the agent to search for matching open issues before +# opening a PR and link them via `Closes #N` / `Refs #N`. # - When detectable: inject the repo's PR template (or the org's `.github` # repo template as a fallback) so PRs are opened with the right structure. @@ -59,6 +61,9 @@ Use the GitHub GraphQL API via \`gh\` to resolve threads, e.g.: gh api graphql -f query='mutation(\$id:ID!){resolveReviewThread(input:{threadId:\$id}){thread{isResolved}}}' -f id=\"\" Reply to a thread via \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`. Never silently push fixes without confirming each related thread is replied to and resolved." +issue_block="### Related-issue linking +Before opening a pull request, search the repo for existing open issues that match the work in the branch. Use \`gh issue list --state open --search ''\` (and \`gh issue view \` to confirm relevance) to find candidates derived from the branch name, commit messages, and changed files. For every issue the PR would resolve, include a \`Closes #\` line in the PR body so GitHub auto-links and auto-closes it on merge. If the issue is related but the PR does not fully resolve it, use \`Refs #\` instead. Skip this only when there are clearly no matching issues." + if [[ -n "$template" ]]; then template_block=" @@ -72,6 +77,8 @@ else template_block="" fi -ctx="${review_block}${template_block}" +ctx="${review_block} + +${issue_block}${template_block}" jq -nc --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}' From be3f95b483cbdba0e667e9bbda5499036171f9b2 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 04:54:19 +0000 Subject: [PATCH 3/4] chore(harness): move PR template + review + issue-link guidance into the cloud system prompt Wrong target on the previous two commits: the agent harness is `packages/agent/src/server/agent-server.ts#buildCloudSystemPrompt`, not Claude Code's `.claude/settings.json`. Revert the Claude-Code hook attempt and put the guidance in the actual cloud task system prompt instead. - Auto-PR (no existing PR) path: instruct the agent to check `.github/pull_request_template.md` (and variants) for a body template, fall back to the org's `.github` repo via `gh api`, and search for matching open issues with `gh issue list --search` to include `Closes #N` / `Refs #N` links in the body. - Auto-PR (existing PR) path: instruct the agent to reply on each PR review thread it addressed and resolve the conversation via the `resolveReviewThread` GraphQL mutation, and to list unresolved threads first so it can target the ones it fixed. - No-repo path: add a brief reminder to apply both behaviors when the user explicitly asks for a PR from a freshly cloned repo. - Extend agent-server tests to lock in the new prompt content for each branch (`gh pr create` path, existing-PR path, no-repo path). Generated-By: PostHog Code Task-Id: 041ddd4c-ea89-4cd1-9697-cdae40e30a3f --- .claude/hooks/check-pr-template.sh | 84 ------------------- .claude/settings.json | 13 --- .../agent/src/server/agent-server.test.ts | 17 ++++ packages/agent/src/server/agent-server.ts | 13 ++- 4 files changed, 28 insertions(+), 99 deletions(-) delete mode 100755 .claude/hooks/check-pr-template.sh diff --git a/.claude/hooks/check-pr-template.sh b/.claude/hooks/check-pr-template.sh deleted file mode 100755 index 7d4cf3a22..000000000 --- a/.claude/hooks/check-pr-template.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# SessionStart hook: inject PR workflow guidance. -# - Always: instruct the agent to leave a reply and resolve each PR review -# conversation it deals with. -# - Always: instruct the agent to search for matching open issues before -# opening a PR and link them via `Closes #N` / `Refs #N`. -# - When detectable: inject the repo's PR template (or the org's `.github` -# repo template as a fallback) so PRs are opened with the right structure. - -set -uo pipefail - -cd "${CLAUDE_PROJECT_DIR:-$PWD}" 2>/dev/null || exit 0 -git rev-parse --git-dir >/dev/null 2>&1 || exit 0 - -template="" -src="" - -for path in \ - .github/pull_request_template.md \ - .github/PULL_REQUEST_TEMPLATE.md \ - .github/pull_request_template.txt \ - docs/pull_request_template.md \ - docs/PULL_REQUEST_TEMPLATE.md \ - pull_request_template.md \ - PULL_REQUEST_TEMPLATE.md -do - if [[ -f "$path" ]]; then - template=$(cat "$path") - src="repo: $path" - break - fi -done - -if [[ -z "$template" ]] && command -v gh >/dev/null 2>&1; then - remote=$(git config --get remote.origin.url 2>/dev/null || echo "") - owner=$(printf '%s' "$remote" | sed -nE 's|.*github\.com[:/]([^/]+)/.*|\1|p') - - if [[ -n "$owner" ]]; then - for path in \ - .github/pull_request_template.md \ - .github/PULL_REQUEST_TEMPLATE.md \ - profile/pull_request_template.md \ - pull_request_template.md \ - PULL_REQUEST_TEMPLATE.md - do - content=$(gh api "/repos/${owner}/.github/contents/${path}" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true) - if [[ -n "$content" ]]; then - template="$content" - src="org fallback: ${owner}/.github/${path}" - break - fi - done - fi -fi - -review_block="### PR review / comment handling -When addressing PR review comments or review threads, treat each thread as done only after BOTH of these: - 1. Reply to the thread with a short note describing what changed (and the commit SHA when useful) so the reviewer can see the resolution inline. - 2. Resolve the conversation thread. -Use the GitHub GraphQL API via \`gh\` to resolve threads, e.g.: - gh api graphql -f query='mutation(\$id:ID!){resolveReviewThread(input:{threadId:\$id}){thread{isResolved}}}' -f id=\"\" -Reply to a thread via \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`. Never silently push fixes without confirming each related thread is replied to and resolved." - -issue_block="### Related-issue linking -Before opening a pull request, search the repo for existing open issues that match the work in the branch. Use \`gh issue list --state open --search ''\` (and \`gh issue view \` to confirm relevance) to find candidates derived from the branch name, commit messages, and changed files. For every issue the PR would resolve, include a \`Closes #\` line in the PR body so GitHub auto-links and auto-closes it on merge. If the issue is related but the PR does not fully resolve it, use \`Refs #\` instead. Skip this only when there are clearly no matching issues." - -if [[ -n "$template" ]]; then - template_block=" - -### PR template -A PR template was detected (${src}). When opening a pull request with \`gh pr create\`, pass the template below as the \`--body\` argument — preserve the section headings exactly and fill each section based on the branch's actual changes. Do NOT fall back to the generic Summary/Test plan format. Keep any required PR footer (e.g. the PostHog Code attribution) appended after the template. - ---- BEGIN PR TEMPLATE --- -${template} ---- END PR TEMPLATE ---" -else - template_block="" -fi - -ctx="${review_block} - -${issue_block}${template_block}" - -jq -nc --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}' diff --git a/.claude/settings.json b/.claude/settings.json index b025f55bb..e73b7b901 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,18 +6,5 @@ "Edit(./apps/cli/**)", "Write(./apps/cli/**)" ] - }, - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-pr-template.sh", - "timeout": 15 - } - ] - } - ] } } diff --git a/packages/agent/src/server/agent-server.test.ts b/packages/agent/src/server/agent-server.test.ts index 88bae5505..72784f739 100644 --- a/packages/agent/src/server/agent-server.test.ts +++ b/packages/agent/src/server/agent-server.test.ts @@ -826,6 +826,9 @@ describe("AgentServer HTTP Mode", () => { "If the user explicitly asks you to open or update a pull request", "open a draft pull request", "unless the user explicitly asks", + ".github/pull_request_template.md", + "gh issue list --search", + "Closes #", "Generated-By: PostHog Code", "Task-Id: test-task-id", ], @@ -868,6 +871,13 @@ describe("AgentServer HTTP Mode", () => { expect(prompt).toContain("Generated-By: PostHog Code"); expect(prompt).toContain("Task-Id: test-task-id"); expect(prompt).toContain("Created with [PostHog Code]"); + // PR template detection (repo first, org `.github` fallback) + expect(prompt).toContain(".github/pull_request_template.md"); + expect(prompt).toContain("org's `.github` repo"); + // Related-issue linking + expect(prompt).toContain("gh issue list --state open --search"); + expect(prompt).toContain("Closes #"); + expect(prompt).toContain("Refs #"); delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN; }); @@ -895,6 +905,13 @@ describe("AgentServer HTTP Mode", () => { ); expect(prompt).toContain("Push to the existing PR branch"); expect(prompt).not.toContain("Create a draft pull request"); + // Review-comment thread handling: reply + resolve + expect(prompt).toContain("review thread"); + expect(prompt).toContain("/pulls/{n}/comments/{id}/replies"); + expect(prompt).toContain("resolveReviewThread"); + expect(prompt).toContain( + "Do NOT push fixes for review comments without replying to and resolving each related thread.", + ); delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN; }); diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 12ea84c47..428f1cd08 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -1633,9 +1633,14 @@ After completing the requested changes: 1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\` 2. Stage and commit all changes with a clear commit message 3. Push to the existing PR branch +4. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these: + - Reply on the thread with a short note describing what changed (reference the commit SHA when useful) using \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`. + - Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation(\\$id:ID!){resolveReviewThread(input:{threadId:\\$id}){thread{isResolved}}}' -f id=""\`. + List unresolved threads first with \`gh api graphql -f query='{repository(owner:"",name:""){pullRequest(number:){reviewThreads(first:100){nodes{id isResolved comments(first:1){nodes{body}}}}}}}'\` so you can resolve each one you fixed. Important: - Do NOT create a new branch or a new pull request. +- Do NOT push fixes for review comments without replying to and resolving each related thread. ${attributionInstructions} `; } @@ -1651,7 +1656,7 @@ When the user asks for code changes: When the user explicitly asks to clone or work in a GitHub repository: - Clone the repository into /tmp/workspace/repos// using \`gh repo clone / /tmp/workspace/repos//\` - Work from inside that cloned repository for follow-up code changes -- If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone +- If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone. Before opening the PR, check the cloned repo for a PR template at \`.github/pull_request_template.md\` (or variants; fall back to the org's \`.github\` repo via \`gh api\`) and use it as the body structure, and search for matching open issues with \`gh issue list --search\` to include \`Closes #\` / \`Refs #\` links. - Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`; return ` @@ -1694,7 +1699,11 @@ After completing the requested changes: 1. Create a new branch prefixed with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`) based on the work done 2. Stage and commit all changes with a clear commit message 3. Push the branch to origin -4. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and body. Add the following footer at the end of the PR description: +4. Before opening the PR, prepare the body: + - Check the repo for a PR template at \`.github/pull_request_template.md\` (also try \`.github/PULL_REQUEST_TEMPLATE.md\`, \`docs/pull_request_template.md\`, and root variants). If one exists, use its exact section headings as the PR body — do NOT fall back to a generic Summary/Test plan format. + - If no repo-level template exists, check the org's \`.github\` repo via \`gh api /repos//.github/contents/.github/pull_request_template.md\` (and other common paths) and use that as a fallback. + - Search for matching open issues with \`gh issue list --state open --search ''\` (derive keywords from the branch name, commits, and changed files; \`gh issue view \` to confirm relevance). For every issue this PR would resolve, include a \`Closes #\` line in the body so GitHub auto-links and auto-closes it on merge. For issues that are related but not fully resolved, use \`Refs #\` instead. +5. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and the body prepared above. Add the following footer at the end of the PR description: \`\`\` --- *Created with [PostHog Code](https://posthog.com/code?ref=pr)* From a4bece60a5816aa33d8f8e27126ac9738afd0aaf Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 05:06:55 +0000 Subject: [PATCH 4/4] fix(harness): drop unnecessary \\$id escape in resolveReviewThread snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A TypeScript template literal only interpolates ${...}; a bare $id is already literal text. The `\\$id` escape produced a stray backslash in the prompt — bash single quotes preserve it verbatim, so gh would send `mutation(\$id:ID!)` to GraphQL and fail with a parse error. Generated-By: PostHog Code Task-Id: 041ddd4c-ea89-4cd1-9697-cdae40e30a3f --- packages/agent/src/server/agent-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 428f1cd08..2d0ff1519 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -1635,7 +1635,7 @@ After completing the requested changes: 3. Push to the existing PR branch 4. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these: - Reply on the thread with a short note describing what changed (reference the commit SHA when useful) using \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`. - - Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation(\\$id:ID!){resolveReviewThread(input:{threadId:\\$id}){thread{isResolved}}}' -f id=""\`. + - Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=""\`. List unresolved threads first with \`gh api graphql -f query='{repository(owner:"",name:""){pullRequest(number:){reviewThreads(first:100){nodes{id isResolved comments(first:1){nodes{body}}}}}}}'\` so you can resolve each one you fixed. Important: