Skip to content

Commit 20e3ee1

Browse files
authored
Merge branch 'main' into fix/posthog-rewrites-incorrect-order
2 parents 91203b9 + e06c8a4 commit 20e3ee1

63 files changed

Lines changed: 2786 additions & 106 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cloud-agent-next/wrangler.jsonc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@
1818
"ip": "0.0.0.0",
1919
},
2020
"observability": {
21-
"enabled": true,
21+
"enabled": false,
22+
"head_sampling_rate": 1,
23+
"logs": {
24+
"enabled": true,
25+
"head_sampling_rate": 1,
26+
"persist": true,
27+
"invocation_logs": true,
28+
},
29+
"traces": {
30+
"enabled": true,
31+
"persist": true,
32+
"head_sampling_rate": 1,
33+
},
2234
},
2335
"logpush": true,
2436
"routes": [

cloudflare-gastown/container/plugin/mayor-tools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,12 @@ export function createMayorTools(client: MayorGastownClient) {
7878
gt_list_beads: tool({
7979
description:
8080
'List beads (work items) in a specific rig. ' +
81-
'Optionally filter by status (open, in_progress, closed, failed) or type (issue, message, escalation, merge_request). ' +
81+
'Optionally filter by status (open, in_progress, in_review, closed, failed) or type (issue, message, escalation, merge_request). ' +
8282
'Use this to check what work exists in a rig, what is in progress, and what has been completed.',
8383
args: {
8484
rig_id: tool.schema.string().describe('The UUID of the rig to list beads from'),
8585
status: tool.schema
86-
.enum(['open', 'in_progress', 'closed', 'failed'])
86+
.enum(['open', 'in_progress', 'in_review', 'closed', 'failed'])
8787
.describe('Filter by bead status')
8888
.optional(),
8989
type: tool.schema

cloudflare-gastown/container/plugin/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Types mirroring the Town DO domain model.
22
// These are the API response shapes — the plugin never touches SQLite directly.
33

4-
export type BeadStatus = 'open' | 'in_progress' | 'closed' | 'failed';
4+
export type BeadStatus = 'open' | 'in_progress' | 'in_review' | 'closed' | 'failed';
55
export type BeadType =
66
| 'issue'
77
| 'message'

cloudflare-gastown/src/db/tables/bead-events.table.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const BeadEventType = z.enum([
1818
'pr_created',
1919
'pr_creation_failed',
2020
'agent_status',
21+
'triage_resolved',
2122
]);
2223

2324
export type BeadEventType = z.infer<typeof BeadEventType>;

cloudflare-gastown/src/db/tables/beads.table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const BeadType = z.enum([
1515
'agent',
1616
]);
1717

18-
export const BeadStatus = z.enum(['open', 'in_progress', 'closed', 'failed']);
18+
export const BeadStatus = z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']);
1919
export const BeadPriority = z.enum(['low', 'medium', 'high', 'critical']);
2020

2121
export const BeadRecord = z.object({

cloudflare-gastown/src/db/tables/rig-beads.table.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { z } from 'zod';
22
import { getTableFromZodSchema, getCreateTableQueryFromTable } from '../../util/table';
33

44
const BeadType = z.enum(['issue', 'message', 'escalation', 'merge_request']);
5-
const BeadStatus = z.enum(['open', 'in_progress', 'closed', 'failed']);
5+
const BeadStatus = z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']);
66
const BeadPriority = z.enum(['low', 'medium', 'high', 'critical']);
77

88
export const RigBeadRecord = z.object({
@@ -32,7 +32,7 @@ export function createTableRigBeads(): string {
3232
id: `text primary key`,
3333
rig_id: `text`,
3434
type: `text not null check(type in ('issue', 'message', 'escalation', 'merge_request'))`,
35-
status: `text not null default 'open' check(status in ('open', 'in_progress', 'closed', 'failed'))`,
35+
status: `text not null default 'open' check(status in ('open', 'in_progress', 'in_review', 'closed', 'failed'))`,
3636
title: `text not null`,
3737
body: `text`,
3838
assignee_agent_id: `text`,

cloudflare-gastown/src/dos/Town.do.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,36 @@ export class TownDO extends DurableObject<Env> {
757757
if (input.status === 'merged' && sourceBeadId) {
758758
this.dispatchUnblockedBeads(sourceBeadId);
759759
}
760+
761+
// When a review fails or conflicts (rework), the source bead was
762+
// returned to in_progress. Re-hook a polecat and re-dispatch so the
763+
// rework starts automatically. The original polecat may already be
764+
// working on something else, so fall back to getOrCreateAgent.
765+
if ((input.status === 'failed' || input.status === 'conflict') && sourceBeadId) {
766+
const sourceBead = beadOps.getBead(this.sql, sourceBeadId);
767+
if (sourceBead?.rig_id) {
768+
try {
769+
const reworkAgent = agents.getOrCreateAgent(
770+
this.sql,
771+
'polecat',
772+
sourceBead.rig_id,
773+
this.townId
774+
);
775+
agents.hookBead(this.sql, reworkAgent.id, sourceBeadId);
776+
this.dispatchAgent(reworkAgent, sourceBead).catch(err =>
777+
console.error(
778+
`${TOWN_LOG} completeReviewWithResult: fire-and-forget rework dispatch failed for bead=${sourceBeadId}`,
779+
err
780+
)
781+
);
782+
} catch (err) {
783+
console.warn(
784+
`${TOWN_LOG} completeReviewWithResult: could not dispatch rework for bead=${sourceBeadId}:`,
785+
err
786+
);
787+
}
788+
}
789+
}
760790
}
761791

762792
async agentDone(agentId: string, input: AgentDoneInput): Promise<void> {
@@ -966,6 +996,24 @@ export class TownDO extends DurableObject<Env> {
966996
},
967997
});
968998

999+
// Log a triage_resolved event on the target bead so the action shows
1000+
// up in the activity feed for the bead that was actually affected.
1001+
const targetBeadId = snapshotHookedBeadId ?? targetAgentId;
1002+
if (targetBeadId && targetBeadId !== input.triage_request_bead_id) {
1003+
beadOps.logBeadEvent(this.sql, {
1004+
beadId: targetBeadId,
1005+
agentId: input.agent_id,
1006+
eventType: 'triage_resolved',
1007+
newValue: action,
1008+
metadata: {
1009+
action,
1010+
resolution_notes: input.resolution_notes,
1011+
triage_request_bead_id: input.triage_request_bead_id,
1012+
target_agent_id: targetAgentId,
1013+
},
1014+
});
1015+
}
1016+
9691017
// If this triage request was created for an escalation, close the
9701018
// linked escalation bead too so it doesn't sit open indefinitely.
9711019
// The escalation_bead_id is nested under metadata.context (set by
@@ -3245,7 +3293,7 @@ export class TownDO extends DurableObject<Env> {
32453293
[
32463294
...query(
32473295
this.sql,
3248-
/* sql */ `SELECT COUNT(*) AS cnt FROM ${beads} WHERE ${beads.status} IN ('open', 'in_progress') AND ${beads.type} NOT IN ('agent', 'message')`,
3296+
/* sql */ `SELECT COUNT(*) AS cnt FROM ${beads} WHERE ${beads.status} IN ('open', 'in_progress', 'in_review') AND ${beads.type} NOT IN ('agent', 'message')`,
32493297
[]
32503298
),
32513299
][0]?.cnt ?? 0
@@ -3274,6 +3322,7 @@ export class TownDO extends DurableObject<Env> {
32743322
beads: {
32753323
open: number;
32763324
inProgress: number;
3325+
inReview: number;
32773326
failed: number;
32783327
triageRequests: number;
32793328
};
@@ -3328,12 +3377,13 @@ export class TownDO extends DurableObject<Env> {
33283377
[]
33293378
),
33303379
];
3331-
const beadCounts = { open: 0, inProgress: 0, failed: 0, triageRequests: 0 };
3380+
const beadCounts = { open: 0, inProgress: 0, inReview: 0, failed: 0, triageRequests: 0 };
33323381
for (const row of beadRows) {
33333382
const s = `${row.status as string}`;
33343383
const c = Number(row.cnt);
33353384
if (s === 'open') beadCounts.open = c;
33363385
else if (s === 'in_progress') beadCounts.inProgress = c;
3386+
else if (s === 'in_review') beadCounts.inReview = c;
33373387
else if (s === 'failed') beadCounts.failed = c;
33383388
}
33393389

cloudflare-gastown/src/dos/town/agents.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,32 @@ export function deleteAgent(sql: SqlStorage, agentId: string): void {
220220

221221
// ── Hooks (GUPP) ────────────────────────────────────────────────────
222222

223+
/** Bead types that are system-managed and should never be hooked to an agent. */
224+
const UNHOOKABLE_BEAD_TYPES = new Set(['escalation', 'convoy', 'agent', 'message']);
225+
223226
export function hookBead(sql: SqlStorage, agentId: string, beadId: string): void {
224227
const agent = getAgent(sql, agentId);
225228
if (!agent) throw new Error(`Agent ${agentId} not found`);
226229

227230
const bead = getBead(sql, beadId);
228231
if (!bead) throw new Error(`Bead ${beadId} not found`);
229232

233+
// Prevent hooking to system-managed bead types that no agent should
234+
// work on directly. Escalation beads are resolved by triage, convoy
235+
// beads are containers, agent/message beads are metadata records.
236+
if (UNHOOKABLE_BEAD_TYPES.has(bead.type)) {
237+
throw new Error(`Cannot hook agent to bead ${beadId}: type '${bead.type}' is not workable`);
238+
}
239+
240+
// Triage request beads are resolved by the triage agent via
241+
// gt_triage_resolve, not by hooking. Prevent polecats from
242+
// accidentally picking these up.
243+
if (bead.labels.includes('gt:triage-request')) {
244+
throw new Error(
245+
`Cannot hook agent to bead ${beadId}: triage requests are resolved via gt_triage_resolve`
246+
);
247+
}
248+
230249
// Already hooked to this bead — idempotent
231250
if (agent.current_hook_bead_id === beadId) return;
232251

cloudflare-gastown/src/dos/town/review-queue.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ export function completeReviewWithResult(
313313
conflict: true,
314314
},
315315
});
316+
// Return source bead to in_progress so the polecat can be re-dispatched
317+
// to resolve the conflict (in_review → in_progress rework flow).
318+
updateBeadStatus(sql, entry.bead_id, 'in_progress', entry.agent_id);
319+
} else if (input.status === 'failed') {
320+
// Review failed (rework requested): return source bead to in_progress
321+
// so it can be re-dispatched (in_review → in_progress rework flow).
322+
updateBeadStatus(sql, entry.bead_id, 'in_progress', entry.agent_id);
316323
}
317324
}
318325

@@ -556,11 +563,13 @@ export function agentDone(sql: SqlStorage, agentId: string, input: AgentDoneInpu
556563
default_branch: rig?.default_branch,
557564
});
558565

559-
// Close the source bead (matches upstream gt done behavior). The polecat's
560-
// work is done — the MR bead now tracks the merge lifecycle. The source
561-
// bead retains its assignee so we know which agent worked on it.
566+
// Transition the source bead to in_review — the polecat's work is done
567+
// but the refinery hasn't reviewed it yet. The MR bead tracks the merge
568+
// lifecycle. The source bead retains its assignee so we know which agent
569+
// worked on it. It will be closed (or returned to in_progress) by the
570+
// refinery after review.
562571
unhookBead(sql, agentId);
563-
closeBead(sql, sourceBead, agentId);
572+
updateBeadStatus(sql, sourceBead, 'in_review', agentId);
564573
}
565574

566575
/**

cloudflare-gastown/src/handlers/rig-triage.handler.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ const ResolveTriageBody = z.object({
2929
});
3030

3131
export async function handleResolveTriage(c: Context<GastownEnv>, _params: { rigId: string }) {
32-
const agentId = getEnforcedAgentId(c);
32+
// In production, agentId comes from the verified JWT. In development
33+
// (where authMiddleware is skipped), fall back to the identity header
34+
// the container client sends with every request. The fallback is gated
35+
// on ENVIRONMENT to prevent header spoofing in production.
36+
const agentId =
37+
getEnforcedAgentId(c) ||
38+
(c.env.ENVIRONMENT === 'development' ? c.req.header('X-Gastown-Agent-Id') : null);
3339
if (!agentId) {
3440
return c.json(resError('Agent authentication required'), 401);
3541
}

0 commit comments

Comments
 (0)