From c31e75f1a10f13293cecc97010cdee2fd1fc152f Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Fri, 3 Apr 2026 14:53:59 -0500 Subject: [PATCH 1/3] feat(gastown): skip review queue for gt:pr-fixup beads (#1983) * feat(gastown): skip review queue for gt:pr-fixup beads in agentDone() * style: fix formatting in local-debug-testing.md --------- Co-authored-by: John Fawcett --- cloudflare-gastown/docs/local-debug-testing.md | 6 ++++++ cloudflare-gastown/src/dos/town/review-queue.ts | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/cloudflare-gastown/docs/local-debug-testing.md b/cloudflare-gastown/docs/local-debug-testing.md index 7ea1af6cc7..7d795592f0 100644 --- a/cloudflare-gastown/docs/local-debug-testing.md +++ b/cloudflare-gastown/docs/local-debug-testing.md @@ -249,6 +249,7 @@ grep "container\|startAgent\|eviction" /tmp/gastown-wrangler.log | tail -20 ### Drain Flag Clearing The TownDO's `_draining` flag is cleared by whichever happens first: + - **Heartbeat instance ID change** (~30s): each container has a UUID. When a new container's heartbeat arrives with a different ID, the drain clears. - **Nonce handshake**: the new container calls `/container-ready` with the drain nonce. - **Hard timeout** (7 min): safety net if no heartbeat or handshake arrives. @@ -276,6 +277,7 @@ docker kill $(docker ps -q) 2>/dev/null ### Drain Stuck "Waiting for N agents" Agents show as `running` but aren't doing work. Common causes: + - **`fetchPendingNudges` hanging**: should be skipped during drain (check `_draining` flag) - **`server.heartbeat` clearing idle timer**: these events should be in `IDLE_TIMER_IGNORE_EVENTS` - **Agent in `starting` status**: `session.prompt()` blocking. Status is set to `running` before the prompt now. @@ -283,6 +285,7 @@ Agents show as `running` but aren't doing work. Common causes: ### Drain Flag Persists After Restart The drain banner stays visible after the container restarted. Causes: + - **No heartbeats arriving**: container failed to start, no agents registered - **`_containerInstanceId` not persisted**: should be in `ctx.storage` - Fallback: wait for the 7-minute hard timeout @@ -290,14 +293,17 @@ The drain banner stays visible after the container restarted. Causes: ### Git Credential Errors Container starts but agents fail at `git clone`: + ``` Error checking if container is ready: Invalid username ``` + This means the git token is stale/expired. Refresh credentials in the town settings. ### Accumulating Escalation Beads Triage/escalation beads pile up with `rig_id=NULL`. These are by design: + - `type=escalation` beads surface for human attention (merge conflicts, rework) - `type=issue` triage beads are handled by `maybeDispatchTriageAgent` - GUPP force-stop beads are created by the patrol system for stuck agents diff --git a/cloudflare-gastown/src/dos/town/review-queue.ts b/cloudflare-gastown/src/dos/town/review-queue.ts index 44c7e26bff..73cf004a29 100644 --- a/cloudflare-gastown/src/dos/town/review-queue.ts +++ b/cloudflare-gastown/src/dos/town/review-queue.ts @@ -577,6 +577,17 @@ export function agentDone(sql: SqlStorage, agentId: string, input: AgentDoneInpu return; } + // PR-fixup beads skip the review queue. The polecat pushed fixup commits + // to an existing PR branch — no separate review is needed. + if (hookedBead?.labels.includes('gt:pr-fixup')) { + console.log( + `[review-queue] agentDone: pr-fixup bead ${agent.current_hook_bead_id} — closing directly (skip review)` + ); + closeBead(sql, agent.current_hook_bead_id, agentId); + unhookBead(sql, agentId); + return; + } + if (agent.role === 'refinery') { // The refinery handles merging (direct strategy) or PR creation (pr strategy) // itself. When it calls gt_done: From a933abd38e8dfd37a6f78a26ba2ee7e0b806a1e2 Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Fri, 3 Apr 2026 19:56:50 +0000 Subject: [PATCH 2/3] feat(gastown): thread labels parameter through gt_sling tool chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional labels[] parameter to gt_sling so the mayor can tag beads at creation time. The parameter flows through: mayor-tools.ts → client.ts → mayor-tools.handler.ts → Town.do.ts slingBead() → createBead() createBead() already accepts labels, so no further changes needed. --- cloudflare-gastown/container/plugin/client.ts | 1 + cloudflare-gastown/container/plugin/mayor-tools.ts | 5 +++++ cloudflare-gastown/src/dos/Town.do.ts | 2 ++ cloudflare-gastown/src/handlers/mayor-tools.handler.ts | 1 + 4 files changed, 9 insertions(+) diff --git a/cloudflare-gastown/container/plugin/client.ts b/cloudflare-gastown/container/plugin/client.ts index 93b972854d..6a222b7b41 100644 --- a/cloudflare-gastown/container/plugin/client.ts +++ b/cloudflare-gastown/container/plugin/client.ts @@ -310,6 +310,7 @@ export class MayorGastownClient { title: string; body?: string; metadata?: Record; + labels?: string[]; }): Promise { return this.request(this.mayorPath('/sling'), { method: 'POST', diff --git a/cloudflare-gastown/container/plugin/mayor-tools.ts b/cloudflare-gastown/container/plugin/mayor-tools.ts index e8c9929de5..48ad586449 100644 --- a/cloudflare-gastown/container/plugin/mayor-tools.ts +++ b/cloudflare-gastown/container/plugin/mayor-tools.ts @@ -67,6 +67,10 @@ export function createMayorTools(client: MayorGastownClient) { .string() .describe('JSON-encoded metadata object for additional context') .optional(), + labels: tool.schema + .array(tool.schema.string()) + .describe('Labels to attach to the bead (e.g. ["gt:pr-fixup"])') + .optional(), }, async execute(args) { const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined; @@ -75,6 +79,7 @@ export function createMayorTools(client: MayorGastownClient) { title: args.title, body: args.body, metadata, + labels: args.labels, }); return [ `Task slung successfully.`, diff --git a/cloudflare-gastown/src/dos/Town.do.ts b/cloudflare-gastown/src/dos/Town.do.ts index 87b9496aee..1cf66dc2fd 100644 --- a/cloudflare-gastown/src/dos/Town.do.ts +++ b/cloudflare-gastown/src/dos/Town.do.ts @@ -2074,6 +2074,7 @@ export class TownDO extends DurableObject { body?: string; priority?: string; metadata?: Record; + labels?: string[]; }): Promise<{ bead: Bead; agent: Agent }> { const createdBead = beadOps.createBead(this.sql, { type: 'issue', @@ -2082,6 +2083,7 @@ export class TownDO extends DurableObject { priority: BeadPriority.catch('medium').parse(input.priority ?? 'medium'), rig_id: input.rigId, metadata: input.metadata, + labels: input.labels, }); events.insertEvent(this.sql, 'bead_created', { diff --git a/cloudflare-gastown/src/handlers/mayor-tools.handler.ts b/cloudflare-gastown/src/handlers/mayor-tools.handler.ts index 6f778d45d0..fcfe207ce3 100644 --- a/cloudflare-gastown/src/handlers/mayor-tools.handler.ts +++ b/cloudflare-gastown/src/handlers/mayor-tools.handler.ts @@ -24,6 +24,7 @@ const MayorSlingBody = z.object({ title: z.string().min(1), body: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional(), + labels: z.array(z.string()).optional(), }); const MayorSlingBatchBody = z From b3d3c6216e6549a8e1b8c8ff55a41692b8cd9a85 Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Fri, 3 Apr 2026 20:14:03 +0000 Subject: [PATCH 3/3] feat(gastown): add pr_fixup_context to PrimeContext and buildPrimeContext Add pr_fixup_context field to PrimeContext type and populate it in buildPrimeContext when the hooked bead has the gt:pr-fixup label, mirroring the existing rework_context pattern. Extracts pr_url, branch, and target_branch from bead metadata. --- cloudflare-gastown/src/dos/town/agents.ts | 12 ++++++++++++ cloudflare-gastown/src/types.ts | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/cloudflare-gastown/src/dos/town/agents.ts b/cloudflare-gastown/src/dos/town/agents.ts index a723ce6d97..21c12673e6 100644 --- a/cloudflare-gastown/src/dos/town/agents.ts +++ b/cloudflare-gastown/src/dos/town/agents.ts @@ -493,12 +493,24 @@ export function prime(sql: SqlStorage, agentId: string): PrimeContext { }; } + // Build PR fixup context if the hooked bead is a PR fixup request + let pr_fixup_context: PrimeContext['pr_fixup_context'] = null; + if (hookedBead?.labels.includes('gt:pr-fixup') && hookedBead.metadata) { + const meta = hookedBead.metadata as Record; + pr_fixup_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, + }; + } + return { agent, hooked_bead: hookedBead, undelivered_mail: undeliveredMail, open_beads: openBeads, rework_context, + pr_fixup_context, }; } diff --git a/cloudflare-gastown/src/types.ts b/cloudflare-gastown/src/types.ts index 2450970358..2c0844ecfc 100644 --- a/cloudflare-gastown/src/types.ts +++ b/cloudflare-gastown/src/types.ts @@ -171,6 +171,12 @@ export type PrimeContext = { original_bead_title: string | null; mr_bead_id: string | null; } | null; + /** Present when the hooked bead is a PR fixup (gt:pr-fixup label). */ + pr_fixup_context: { + pr_url: string | null; + branch: string | null; + target_branch: string | null; + } | null; }; // -- Agent done --