diff --git a/services/gastown/src/dos/town/agents.ts b/services/gastown/src/dos/town/agents.ts index 8b6c2765dc..cbcefc468f 100644 --- a/services/gastown/src/dos/town/agents.ts +++ b/services/gastown/src/dos/town/agents.ts @@ -519,6 +519,33 @@ export function prime(sql: SqlStorage, agentId: string): PrimeContext { }; } + // Build PR conflict context if the hooked bead is a PR conflict resolution request, + // or if it is a PR feedback bead that has also accumulated merge conflicts. + let pr_conflict_context: PrimeContext['pr_conflict_context'] = null; + if (hookedBead?.labels.includes('gt:pr-conflict') && hookedBead.metadata) { + const meta = hookedBead.metadata as Record; + pr_conflict_context = { + pr_url: typeof meta.pr_url === 'string' ? meta.pr_url : null, + branch: typeof meta.branch === 'string' ? meta.branch : null, + target_branch: typeof meta.target_branch === 'string' ? meta.target_branch : null, + has_feedback: meta.has_feedback === true || meta.has_feedback === 1, + }; + } else if (hookedBead?.labels.includes('gt:pr-feedback') && hookedBead.metadata) { + // A feedback bead can also have has_conflicts: true when a conflict was detected + // after the feedback bead was already created. Surface the conflict context so the + // agent resolves conflicts first, then addresses review feedback. + const meta = hookedBead.metadata as Record; + if (meta.has_conflicts === true || meta.has_conflicts === 1) { + pr_conflict_context = { + pr_url: typeof meta.pr_url === 'string' ? meta.pr_url : null, + branch: typeof meta.branch === 'string' ? meta.branch : null, + target_branch: + typeof meta.conflict_target_branch === 'string' ? meta.conflict_target_branch : null, + has_feedback: true, + }; + } + } + return { agent, hooked_bead: hookedBead, @@ -526,6 +553,7 @@ export function prime(sql: SqlStorage, agentId: string): PrimeContext { open_beads: openBeads, rework_context, pr_fixup_context, + pr_conflict_context, }; } diff --git a/services/gastown/src/dos/town/reconciler.ts b/services/gastown/src/dos/town/reconciler.ts index a67bfde950..d3737da7f9 100644 --- a/services/gastown/src/dos/town/reconciler.ts +++ b/services/gastown/src/dos/town/reconciler.ts @@ -403,6 +403,27 @@ export function applyEvent(sql: SqlStorage, event: TownEventRecord): void { const hasFailingChecks = payload.has_failing_checks === true; const hasUncheckedRuns = payload.has_unchecked_runs === true; + // Consolidation: if there's already an open gt:pr-conflict bead for this MR, + // add has_feedback: true to it instead of creating a separate feedback bead. + // The agent resolving conflicts will then also address review feedback afterward. + const existingConflictBeadId = getExistingPrConflictBeadId(sql, mrBeadId); + if (existingConflictBeadId) { + query( + sql, + /* sql */ ` + UPDATE ${beads} + SET ${beads.columns.metadata} = json_set(COALESCE(${beads.metadata}, '{}'), '$.has_feedback', 1), + ${beads.columns.updated_at} = ? + WHERE ${beads.bead_id} = ? + `, + [new Date().toISOString(), existingConflictBeadId] + ); + console.log( + `${LOG} pr_feedback_detected: merged into existing conflict bead ${existingConflictBeadId} (mrBeadId=${mrBeadId})` + ); + return; + } + const feedbackBead = beadOps.createBead(sql, { type: 'issue', title: buildFeedbackBeadTitle( @@ -482,6 +503,28 @@ export function applyEvent(sql: SqlStorage, event: TownEventRecord): void { : true; if (autoResolveConflicts) { + // Consolidation: if there's already an open gt:pr-feedback bead for this MR, + // add has_conflicts: true to it instead of creating a separate conflict bead. + // The agent handling the feedback bead will resolve conflicts first, then + // address review comments. + const existingFeedbackBeadId = getExistingPrFeedbackBeadId(sql, mrBeadId); + if (existingFeedbackBeadId) { + query( + sql, + /* sql */ ` + UPDATE ${beads} + SET ${beads.columns.metadata} = json_set(COALESCE(${beads.metadata}, '{}'), '$.has_conflicts', 1, '$.conflict_target_branch', ?), + ${beads.columns.updated_at} = ? + WHERE ${beads.bead_id} = ? + `, + [targetBranch, new Date().toISOString(), existingFeedbackBeadId] + ); + console.log( + `${LOG} pr_conflict_detected: merged into existing feedback bead ${existingFeedbackBeadId} (mrBeadId=${mrBeadId})` + ); + return; + } + const conflictBead = beadOps.createBead(sql, { type: 'issue', title: `Resolve merge conflicts on PR: ${branch}`, @@ -2250,42 +2293,60 @@ function hasRecentNudge(sql: SqlStorage, agentId: string, tier: string): boolean /** Check if an MR bead has a non-terminal conflict bead (gt:pr-conflict) blocking it. */ function hasExistingPrConflictBead(sql: SqlStorage, mrBeadId: string): boolean { - const rows = [ - ...query( - sql, - /* sql */ ` - SELECT 1 FROM ${bead_dependencies} bd - INNER JOIN ${beads} fb ON fb.${beads.columns.bead_id} = bd.${bead_dependencies.columns.depends_on_bead_id} - WHERE bd.${bead_dependencies.columns.bead_id} = ? - AND bd.${bead_dependencies.columns.dependency_type} = 'blocks' - AND fb.${beads.columns.labels} LIKE '%gt:pr-conflict%' - AND fb.${beads.columns.status} NOT IN ('closed', 'failed') - LIMIT 1 - `, - [mrBeadId] - ), - ]; - return rows.length > 0; + return getExistingPrConflictBeadId(sql, mrBeadId) !== null; +} + +/** Return the bead_id of a non-terminal conflict bead (gt:pr-conflict) blocking the MR, or null. */ +function getExistingPrConflictBeadId(sql: SqlStorage, mrBeadId: string): string | null { + const rows = z + .object({ bead_id: z.string() }) + .array() + .parse([ + ...query( + sql, + /* sql */ ` + SELECT fb.${beads.columns.bead_id} + FROM ${bead_dependencies} bd + INNER JOIN ${beads} fb ON fb.${beads.columns.bead_id} = bd.${bead_dependencies.columns.depends_on_bead_id} + WHERE bd.${bead_dependencies.columns.bead_id} = ? + AND bd.${bead_dependencies.columns.dependency_type} = 'blocks' + AND fb.${beads.columns.labels} LIKE '%gt:pr-conflict%' + AND fb.${beads.columns.status} NOT IN ('closed', 'failed') + LIMIT 1 + `, + [mrBeadId] + ), + ]); + return rows.length > 0 ? rows[0].bead_id : null; } /** Check if an MR bead has a non-terminal feedback bead (gt:pr-feedback) blocking it. */ function hasExistingPrFeedbackBead(sql: SqlStorage, mrBeadId: string): boolean { - const rows = [ - ...query( - sql, - /* sql */ ` - SELECT 1 FROM ${bead_dependencies} bd - INNER JOIN ${beads} fb ON fb.${beads.columns.bead_id} = bd.${bead_dependencies.columns.depends_on_bead_id} - WHERE bd.${bead_dependencies.columns.bead_id} = ? - AND bd.${bead_dependencies.columns.dependency_type} = 'blocks' - AND fb.${beads.columns.labels} LIKE '%gt:pr-feedback%' - AND fb.${beads.columns.status} NOT IN ('closed', 'failed') - LIMIT 1 - `, - [mrBeadId] - ), - ]; - return rows.length > 0; + return getExistingPrFeedbackBeadId(sql, mrBeadId) !== null; +} + +/** Return the bead_id of a non-terminal feedback bead (gt:pr-feedback) blocking the MR, or null. */ +function getExistingPrFeedbackBeadId(sql: SqlStorage, mrBeadId: string): string | null { + const rows = z + .object({ bead_id: z.string() }) + .array() + .parse([ + ...query( + sql, + /* sql */ ` + SELECT fb.${beads.columns.bead_id} + FROM ${bead_dependencies} bd + INNER JOIN ${beads} fb ON fb.${beads.columns.bead_id} = bd.${bead_dependencies.columns.depends_on_bead_id} + WHERE bd.${bead_dependencies.columns.bead_id} = ? + AND bd.${bead_dependencies.columns.dependency_type} = 'blocks' + AND fb.${beads.columns.labels} LIKE '%gt:pr-feedback%' + AND fb.${beads.columns.status} NOT IN ('closed', 'failed') + LIMIT 1 + `, + [mrBeadId] + ), + ]); + return rows.length > 0 ? rows[0].bead_id : null; } /** Build a human-readable title for the feedback bead. */ diff --git a/services/gastown/src/prompts/polecat-system.prompt.ts b/services/gastown/src/prompts/polecat-system.prompt.ts index 8815dea0e0..1dd3efc0f3 100644 --- a/services/gastown/src/prompts/polecat-system.prompt.ts +++ b/services/gastown/src/prompts/polecat-system.prompt.ts @@ -82,6 +82,29 @@ After all gates pass and your work is complete, create a pull request before cal ` : '' } +## PR Conflict Resolution Workflow + +When your hooked bead has the \`gt:pr-conflict\` label, **or** when it has the \`gt:pr-feedback\` label and \`pr_conflict_context\` is present in your context, you are resolving merge conflicts on an existing PR branch. **This is an exception to the "do not switch branches" rule.** You MUST check out the PR branch from your bead metadata (\`pr_conflict_context.branch\`). + +1. Check out the PR branch: \`git fetch origin && git checkout \` +2. Rebase onto the target branch to incorporate its latest changes: + ``` + git rebase origin/ + ``` +3. If there are conflicts during rebase, resolve them: + - Edit conflicting files to resolve conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) + - Stage the resolved files: \`git add \` + - Continue the rebase: \`git rebase --continue\` + - Repeat until the rebase completes +4. Push the rebased branch: + ``` + git push --force-with-lease origin + ``` +5. If the bead metadata has `has_feedback: true`, also address the PR review feedback (see PR Fixup Workflow below) before calling gt_done. +6. Call \`gt_done\` with the PR URL once all conflicts are resolved (and feedback addressed if applicable). + +Do NOT create a new PR. Push to the existing branch. + ## PR Fixup Workflow When your hooked bead has the \`gt:pr-fixup\` label, you are fixing an existing PR rather than creating new work. **This is the ONE exception to the "do not switch branches" rule.** You MUST check out the PR branch from your bead metadata instead of using the default worktree branch. @@ -101,7 +124,7 @@ Do NOT create a new PR. Push to the existing branch. - Commit after every meaningful unit of work (new function, passing test, config change). - Push after every commit. Do not batch pushes. - Use descriptive commit messages referencing the bead if applicable. -- Branch naming: your branch is pre-configured in your worktree. Do not switch branches — **unless** your bead has the \`gt:pr-fixup\` label (see PR Fixup Workflow above). +- Branch naming: your branch is pre-configured in your worktree. Do not switch branches — **unless** your bead has the \`gt:pr-fixup\` or \`gt:pr-conflict\` label (see workflows above). ## Escalation diff --git a/services/gastown/src/types.ts b/services/gastown/src/types.ts index 654be91810..73b7bf604b 100644 --- a/services/gastown/src/types.ts +++ b/services/gastown/src/types.ts @@ -177,6 +177,14 @@ export type PrimeContext = { branch: string | null; target_branch: string | null; } | null; + /** Present when the hooked bead is a PR conflict resolution (gt:pr-conflict label). */ + pr_conflict_context: { + pr_url: string | null; + branch: string | null; + target_branch: string | null; + /** When true, the bead also has pending review feedback to address after resolving conflicts. */ + has_feedback: boolean; + } | null; }; // -- Agent done --