Skip to content

Commit 7ee4861

Browse files
authored
fix(review): forward installation token, post inline findings, and stream progress (#57)
1 parent 817e9ae commit 7ee4861

7 files changed

Lines changed: 182 additions & 53 deletions

File tree

docs/BOT-WORKFLOWS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ Composite workflows like `ship` insert a child row per step. When the child comp
104104
- `[major]` — should fix before merge (likely bug, missing test)
105105
- `[minor]` — nice to fix (readability)
106106
- `[nit]` — taste, optional
107-
- **Outputs**: `state.head_sha`, `state.changed_files`, `state.additions`, `state.deletions`, `state.branch_state` (`{ commits_behind_base, commits_ahead_of_base, is_fork }`), `state.report` (full `REVIEW.md` — Summary / What was checked / Findings / Reasoning), `state.costUsd`, `state.turns`. The agent posts findings as a single GitHub Review (`event: "COMMENT"`) with inline `comments[]` via `gh api`.
107+
- **Outputs**: `state.head_sha`, `state.changed_files`, `state.additions`, `state.deletions`, `state.branch_state` (`{ commits_behind_base, commits_ahead_of_base, is_fork }`), `state.report` (full `REVIEW.md` — Summary / What was checked / Findings / Reasoning), `state.costUsd`, `state.turns`. The agent posts each finding as a separate inline comment via `mcp__github_inline_comment__create_inline_comment` (one MCP call per finding) — never as a single bundled review POST. This guarantees each finding lands on the right line with its own resolvable thread, instead of one wall-of-text comment at the top of the PR.
108+
- **Progress visibility**: the handler seeds the tracking comment (via `setState` + `tryReserveTrackingCommentId`) before invoking the pipeline and threads the reserved id into `RunPipelineOverrides.trackingCommentId`. The agent updates the same comment via `mcp__github_comment__update_claude_comment` at five `[update tracking comment]` checkpoints in the prompt — branch refresh, file walk, finding count, posting findings, final REVIEW.md — so the user sees live progress instead of waiting blind for a final report. Without the seed/handoff, `pipeline.ts` would create-and-finalize its own comment and the handler's mid-run updates would target the wrong id; with it, the handler owns the comment lifecycle end-to-end.
108109
- **No-findings case**: the agent MUST still post a top-level review body listing exactly what was checked (files read in full, classes of issue scanned, tests run) and why no issues were flagged. Silence looks indistinguishable from "didn't actually look."
109110
- **Branch refresh**: if the PR head is behind base AND not on a fork, the agent rebases onto base, resolves conflicts honestly (reads the surrounding code, runs typecheck + tests, doesn't take ours/theirs blindly), and force-pushes with `--force-with-lease`. Fork PRs get a comment asking the contributor to rebase. See `src/workflows/handlers/branch-refresh.ts`.
110-
- **Stop conditions**: agent writes `REVIEW.md`; pipeline reports success. Handler NEVER calls `pulls.merge` and NEVER posts an `APPROVE` or `REQUEST_CHANGES` review (FR-017 — those are human prerogatives).
111+
- **Stop conditions**: agent writes `REVIEW.md`; pipeline reports success. **Push policy**: the only acceptable push from `review` is `git push --force-with-lease` after a clean rebase onto base (same diff, fresh head SHA — see Branch refresh above). The handler never creates commits with code changes (those belong to `implement` / `resolve`), never calls `pulls.merge`, and never posts an `APPROVE` or `REQUEST_CHANGES` review (FR-017 — those are human prerogatives).
111112
- **Example trigger**: add label `bot:review` on the PR, or comment "`@chrisleekr-bot review this PR`"
112113

113114
### resolve
@@ -117,6 +118,7 @@ Composite workflows like `ship` insert a child row per step. When the child comp
117118
- **Inputs**: PR title, failing check names, count of open top-level review comments, branch-staleness diagnostics.
118119
- **Method**: classify each open reviewer comment (Valid / Partially Valid / Invalid / Needs Clarification), fix valid ones with new commits, reply to all four classes appropriately. Fix failing CI when there is a clear root cause. Refresh the branch first if it's stale (same logic as `review`).
119120
- **Outputs**: `state.failing_checks`, `state.top_level_comments`, `state.branch_state`, `state.report` (full `RESOLVE.md` body — Summary / CI status / Review comments / Commits pushed / Outstanding), `state.costUsd`, `state.turns`. The agent is asked to write `RESOLVE.md` before finishing; the handler captures it pre-cleanup and embeds it in the tracking comment.
121+
- **Progress visibility**: same seed/handoff pattern as `review` — the handler `setState`s a "Resolve starting" message before the pipeline, hands the reserved tracking-comment id to `RunPipelineOverrides.trackingCommentId`, and the agent posts `[update tracking comment]` checkpoints at branch-refresh, CI-fix, comment-classification, and final RESOLVE.md steps. Reviewer-thread replies are posted via `gh api repos/<owner>/<repo>/pulls/<num>/comments/<id>/replies -X POST` (the bot's `gh` and `git` calls authenticate via `GH_TOKEN` / `GITHUB_TOKEN` injected from the GitHub App installation token by `buildProviderEnv` in `src/core/executor.ts`).
120122
- **Stop conditions** (from `src/workflows/handlers/resolve.ts`):
121123
- `FIX_ATTEMPTS_CAP = 3` — max consecutive CI-fix attempts per PR
122124
- `POLL_WAIT_SECS_CAP = 900` — 15-minute reviewer-patience window

src/core/executor.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ function isResultMessage(msg: unknown): msg is SDKResultMessage {
2626
* The Claude CLI subprocess picks between them via its own documented auth
2727
* precedence chain (API key at position 3 beats OAuth token at position 5).
2828
* See https://code.claude.com/docs/en/authentication#authentication-precedence
29+
*
30+
* When `installationToken` is supplied, it is exported as both `GH_TOKEN` and
31+
* `GITHUB_TOKEN` so the agent's shell `gh`/`git` calls authenticate as the
32+
* GitHub App installation. MCP servers receive the same token via their own
33+
* env mapping in `src/mcp/registry.ts`.
2934
*/
30-
function buildProviderEnv(): Record<string, string | undefined> {
35+
function buildProviderEnv(installationToken?: string): Record<string, string | undefined> {
36+
const tokenEnv: Record<string, string> =
37+
installationToken !== undefined && installationToken !== ""
38+
? { GH_TOKEN: installationToken, GITHUB_TOKEN: installationToken }
39+
: {};
3140
if (config.provider === "bedrock") {
32-
return { ...process.env, CLAUDE_CODE_USE_BEDROCK: "1" };
41+
return { ...process.env, ...tokenEnv, CLAUDE_CODE_USE_BEDROCK: "1" };
3342
}
34-
return { ...process.env };
43+
return { ...process.env, ...tokenEnv };
3544
}
3645

3746
/**
@@ -51,6 +60,7 @@ export interface ExecuteAgentParams {
5160
workDir: string;
5261
allowedTools: string[];
5362
maxTurns?: number;
63+
installationToken?: string;
5464
}
5565

5666
export async function executeAgent({
@@ -60,6 +70,7 @@ export async function executeAgent({
6070
workDir,
6171
allowedTools,
6272
maxTurns,
73+
installationToken,
6374
}: ExecuteAgentParams): Promise<ExecutionResult> {
6475
const { log } = ctx;
6576

@@ -89,7 +100,7 @@ export async function executeAgent({
89100
allowedTools,
90101
mcpServers,
91102
systemPrompt: { type: "preset", preset: "claude_code" },
92-
env: buildProviderEnv(),
103+
env: buildProviderEnv(installationToken),
93104
};
94105
const resolvedMaxTurns = maxTurns ?? config.agentMaxTurns;
95106
if (resolvedMaxTurns !== undefined) {

src/core/pipeline.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ export interface RunPipelineOverrides {
117117
* tracking comment without duplicating the pipeline machinery.
118118
*/
119119
captureFiles?: string[];
120+
/**
121+
* Pre-existing tracking comment id (typically created by a workflow
122+
* handler via `setState` before invoking the pipeline). When set, the
123+
* pipeline does NOT call `createTrackingComment`/`finalizeTrackingComment`
124+
* — the orchestrator's tracking-mirror owns the comment lifecycle, and
125+
* the pipeline only wires the id into the prompt + MCP server so the
126+
* agent can post mid-run progress via `update_claude_comment`.
127+
*/
128+
trackingCommentId?: number;
120129
}
121130

122131
/**
@@ -158,9 +167,20 @@ export async function runPipeline(
158167
overrides: RunPipelineOverrides = {},
159168
): Promise<ExecutionResult> {
160169
let trackingCommentId: number | undefined;
170+
// When the caller (workflow handler) seeded the tracking comment, the
171+
// pipeline must NOT finalize it — the handler's terminal `setState` writes
172+
// the final body via tracking-mirror, and a pipeline finalize would
173+
// overwrite it with the legacy "completed" template.
174+
const callerOwnsTrackingComment = overrides.trackingCommentId !== undefined;
161175

162176
try {
163-
if (ctx.skipTrackingComments === true) {
177+
if (callerOwnsTrackingComment) {
178+
trackingCommentId = overrides.trackingCommentId;
179+
ctx.log.info(
180+
{ trackingCommentId },
181+
"Using caller-supplied tracking comment (workflow handler owns lifecycle)",
182+
);
183+
} else if (ctx.skipTrackingComments === true) {
164184
ctx.log.info("Skipping tracking comment (skipTrackingComments)");
165185
} else {
166186
trackingCommentId = await retryWithBackoff(() => createTrackingComment(ctx), {
@@ -222,10 +242,11 @@ export async function runPipeline(
222242
mcpServers,
223243
workDir,
224244
allowedTools,
245+
installationToken,
225246
...(overrides.maxTurns !== undefined ? { maxTurns: overrides.maxTurns } : {}),
226247
});
227248

228-
if (resolvedTrackingCommentId !== undefined) {
249+
if (resolvedTrackingCommentId !== undefined && !callerOwnsTrackingComment) {
229250
try {
230251
const finalOpts = buildFinalOpts(result);
231252
await retryWithBackoff(
@@ -279,7 +300,7 @@ export async function runPipeline(
279300
const err = error instanceof Error ? error : new Error(String(error));
280301
ctx.log.error({ err }, "Request processing failed");
281302

282-
if (trackingCommentId !== undefined) {
303+
if (trackingCommentId !== undefined && !callerOwnsTrackingComment) {
283304
const commentId = trackingCommentId;
284305
try {
285306
await retryWithBackoff(

src/workflows/handlers/resolve.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { runPipeline } from "../../core/pipeline";
22
import type { BotContext } from "../../types";
33
import type { WorkflowHandler } from "../registry";
4+
import { findById } from "../runs-store";
45
import { type BranchStaleness, formatRefreshDirective, getBranchStaleness } from "./branch-refresh";
56

67
/**
@@ -96,6 +97,22 @@ export const handler: WorkflowHandler = async (ctx) => {
9697

9798
const staleness = await getBranchStaleness(octokit, target.owner, target.repo, target.number);
9899

100+
// Seed the tracking comment up front so the agent can post mid-run
101+
// progress against it. See review.ts for the same pattern + rationale.
102+
await ctx.setState(
103+
{
104+
pr_number: target.number,
105+
failing_checks: failingChecks,
106+
top_level_comments: topLevelComments.length,
107+
},
108+
`🔎 **Resolve starting** — ${String(failingChecks.length)} failing checks, ${String(topLevelComments.length)} open comment threads. Refreshing branch and classifying feedback…`,
109+
);
110+
const seededRow = await findById(runId);
111+
const trackingCommentId = seededRow?.tracking_comment_id ?? undefined;
112+
if (trackingCommentId === undefined || trackingCommentId === null) {
113+
log.warn({ runId }, "resolve handler: tracking comment id not found after seed setState");
114+
}
115+
99116
const triggerBody = buildResolvePrompt({
100117
prNumber: target.number,
101118
prTitle: pr.title,
@@ -121,12 +138,16 @@ export const handler: WorkflowHandler = async (ctx) => {
121138
headBranch: pr.head.ref,
122139
baseBranch: pr.base.ref,
123140
labels: [],
124-
skipTrackingComments: true,
125141
octokit,
126142
log,
127143
};
128144

129-
const result = await runPipeline(botCtx, { captureFiles: ["RESOLVE.md"] });
145+
const result = await runPipeline(botCtx, {
146+
captureFiles: ["RESOLVE.md"],
147+
...(trackingCommentId !== undefined && trackingCommentId !== null
148+
? { trackingCommentId }
149+
: {}),
150+
});
130151
if (!result.success) {
131152
return { status: "failed", reason: "resolve pipeline execution failed" };
132153
}
@@ -202,20 +223,29 @@ function buildResolvePrompt(input: {
202223
``,
203224
formatRefreshDirective(input.staleness),
204225
``,
226+
`## Tools you MUST use`,
227+
`- \`mcp__github_comment__update_claude_comment\` — refresh the tracking comment at every checkpoint marked **[update tracking comment]** so the user sees live progress.`,
228+
`- \`Bash\` (\`gh\`, \`git\`) — \`gh\` and \`git\` are pre-authenticated as the GitHub App installation in this environment, so \`gh pr view\`, \`gh run view --log-failed\`, \`gh api .../pulls/comments/<id>/replies\`, \`git commit\`, \`git push\` all work without further setup.`,
229+
`- \`Read\`, \`Edit\`, \`Grep\`, \`Glob\` — for code edits and exploration.`,
230+
``,
205231
`Do the following in order:`,
206-
`0. **Refresh the branch first if needed** (see "Branch state" above). A senior engineer rebases before triaging anything; resolving feedback against a stale branch is wasted work.`,
207-
`1. If failing checks exist, fetch their logs (gh run view --log-failed) and classify the failure. If it's a test / lint / type / build failure with a clear root cause, attempt one fix — diagnose, edit, commit, push. Do NOT retry more than once per run (FIX_ATTEMPTS_CAP=3 is the per-iteration cap; cross-run enforcement is not yet wired).`,
208-
`2. For each open comment thread, classify into one of: Valid | Partially Valid | Invalid | Needs Clarification. The count above is an upper bound — some threads may already be resolved, in which case skip them. For Valid/Partially Valid: fix the code, commit, push, and reply to the comment with the commit SHA and a one-sentence explanation. For Invalid: reply with evidence-backed explanation, no code changes. For Needs Clarification: reply asking the specific question needed to proceed.`,
209-
`3. If all checks pass AND all comments resolved AND reviewDecision is APPROVED, post a one-line "review complete — ready to merge" comment.`,
210-
`4. NEVER call \`gh pr merge\` or \`octokit.pulls.merge\`. Merging is a human action (FR-017).`,
211-
`5. NEVER push to the base branch ${input.baseBranch}.`,
212-
`6. Before finishing, write \`RESOLVE.md\` at the repo root summarizing this resolve iteration.`,
232+
`0. **[update tracking comment]** Post: "🔎 Resolve — refreshing branch (if needed)."`,
233+
`1. **Refresh the branch first if needed** (see "Branch state" above). A senior engineer rebases before triaging anything; resolving feedback against a stale branch is wasted work.`,
234+
`2. **[update tracking comment]** Post: "🔎 Resolve — diagnosing N failing checks." (replace N; skip this step if N=0)`,
235+
`3. If failing checks exist, fetch their logs (\`gh run view --log-failed\`) and classify the failure. If it's a test / lint / type / build failure with a clear root cause, attempt one fix — diagnose, edit, commit, push. Do NOT retry more than once per run (FIX_ATTEMPTS_CAP=3 is the per-iteration cap; cross-run enforcement is not yet wired).`,
236+
`4. **[update tracking comment]** Post: "🔎 Resolve — classifying K open comment threads." (replace K; skip if K=0)`,
237+
`5. For each open comment thread, classify into one of: Valid | Partially Valid | Invalid | Needs Clarification. The count above is an upper bound — some threads may already be resolved, in which case skip them. For Valid/Partially Valid: fix the code, commit, push, and reply to the comment via \`gh api repos/OWNER/REPO/pulls/${String(input.prNumber)}/comments/<comment_id>/replies -X POST -f body="..."\` with the commit SHA and a one-sentence explanation. For Invalid: reply with evidence-backed explanation, no code changes. For Needs Clarification: reply asking the specific question needed to proceed.`,
238+
`6. If all checks pass AND all comments resolved AND reviewDecision is APPROVED, post a one-line "review complete — ready to merge" comment via \`update_claude_comment\`.`,
239+
`7. NEVER call \`gh pr merge\` or \`octokit.pulls.merge\`. Merging is a human action (FR-017).`,
240+
`8. NEVER push to the base branch ${input.baseBranch}.`,
241+
`9. **Before finishing**, write \`RESOLVE.md\` at the repo root summarizing this resolve iteration.`,
213242
` Required sections:`,
214243
` ## Summary — one paragraph: what state the PR is in now and what's left.`,
215244
` ## CI status — list each failing check and what you did about it.`,
216245
` ## Review comments — for each comment: classification, action taken, commit/reply link.`,
217246
` ## Commits pushed — sha · subject.`,
218247
` ## Outstanding — what still blocks merge (if anything).`,
219-
` This becomes the tracking comment body — be specific, cite files and links.`,
248+
` This becomes the final tracking comment body — be specific, cite files and links.`,
249+
`10. **[update tracking comment]** Final: paste the full RESOLVE.md contents.`,
220250
].join("\n");
221251
}

0 commit comments

Comments
 (0)