diff --git a/cloudflare-gastown/container/plugin/mayor-tools.ts b/cloudflare-gastown/container/plugin/mayor-tools.ts index 6e47735af9..714ba3a02b 100644 --- a/cloudflare-gastown/container/plugin/mayor-tools.ts +++ b/cloudflare-gastown/container/plugin/mayor-tools.ts @@ -78,12 +78,12 @@ export function createMayorTools(client: MayorGastownClient) { gt_list_beads: tool({ description: 'List beads (work items) in a specific rig. ' + - 'Optionally filter by status (open, in_progress, closed, failed) or type (issue, message, escalation, merge_request). ' + + 'Optionally filter by status (open, in_progress, in_review, closed, failed) or type (issue, message, escalation, merge_request). ' + 'Use this to check what work exists in a rig, what is in progress, and what has been completed.', args: { rig_id: tool.schema.string().describe('The UUID of the rig to list beads from'), status: tool.schema - .enum(['open', 'in_progress', 'closed', 'failed']) + .enum(['open', 'in_progress', 'in_review', 'closed', 'failed']) .describe('Filter by bead status') .optional(), type: tool.schema diff --git a/cloudflare-gastown/container/plugin/types.ts b/cloudflare-gastown/container/plugin/types.ts index 7f83b5e132..0fb1998ec3 100644 --- a/cloudflare-gastown/container/plugin/types.ts +++ b/cloudflare-gastown/container/plugin/types.ts @@ -1,7 +1,7 @@ // Types mirroring the Town DO domain model. // These are the API response shapes — the plugin never touches SQLite directly. -export type BeadStatus = 'open' | 'in_progress' | 'closed' | 'failed'; +export type BeadStatus = 'open' | 'in_progress' | 'in_review' | 'closed' | 'failed'; export type BeadType = | 'issue' | 'message' diff --git a/cloudflare-gastown/src/db/tables/bead-events.table.ts b/cloudflare-gastown/src/db/tables/bead-events.table.ts index 1ea22b4c07..72c6905544 100644 --- a/cloudflare-gastown/src/db/tables/bead-events.table.ts +++ b/cloudflare-gastown/src/db/tables/bead-events.table.ts @@ -18,6 +18,7 @@ export const BeadEventType = z.enum([ 'pr_created', 'pr_creation_failed', 'agent_status', + 'triage_resolved', ]); export type BeadEventType = z.infer; diff --git a/cloudflare-gastown/src/db/tables/beads.table.ts b/cloudflare-gastown/src/db/tables/beads.table.ts index 9358791614..e629017a1c 100644 --- a/cloudflare-gastown/src/db/tables/beads.table.ts +++ b/cloudflare-gastown/src/db/tables/beads.table.ts @@ -15,7 +15,7 @@ export const BeadType = z.enum([ 'agent', ]); -export const BeadStatus = z.enum(['open', 'in_progress', 'closed', 'failed']); +export const BeadStatus = z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']); export const BeadPriority = z.enum(['low', 'medium', 'high', 'critical']); export const BeadRecord = z.object({ diff --git a/cloudflare-gastown/src/db/tables/rig-beads.table.ts b/cloudflare-gastown/src/db/tables/rig-beads.table.ts index 327af6d7cb..59c0c01587 100644 --- a/cloudflare-gastown/src/db/tables/rig-beads.table.ts +++ b/cloudflare-gastown/src/db/tables/rig-beads.table.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { getTableFromZodSchema, getCreateTableQueryFromTable } from '../../util/table'; const BeadType = z.enum(['issue', 'message', 'escalation', 'merge_request']); -const BeadStatus = z.enum(['open', 'in_progress', 'closed', 'failed']); +const BeadStatus = z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']); const BeadPriority = z.enum(['low', 'medium', 'high', 'critical']); export const RigBeadRecord = z.object({ @@ -32,7 +32,7 @@ export function createTableRigBeads(): string { id: `text primary key`, rig_id: `text`, type: `text not null check(type in ('issue', 'message', 'escalation', 'merge_request'))`, - status: `text not null default 'open' check(status in ('open', 'in_progress', 'closed', 'failed'))`, + status: `text not null default 'open' check(status in ('open', 'in_progress', 'in_review', 'closed', 'failed'))`, title: `text not null`, body: `text`, assignee_agent_id: `text`, diff --git a/cloudflare-gastown/src/dos/Town.do.ts b/cloudflare-gastown/src/dos/Town.do.ts index 45b00bf674..b8ba794f9a 100644 --- a/cloudflare-gastown/src/dos/Town.do.ts +++ b/cloudflare-gastown/src/dos/Town.do.ts @@ -757,6 +757,36 @@ export class TownDO extends DurableObject { if (input.status === 'merged' && sourceBeadId) { this.dispatchUnblockedBeads(sourceBeadId); } + + // When a review fails or conflicts (rework), the source bead was + // returned to in_progress. Re-hook a polecat and re-dispatch so the + // rework starts automatically. The original polecat may already be + // working on something else, so fall back to getOrCreateAgent. + if ((input.status === 'failed' || input.status === 'conflict') && sourceBeadId) { + const sourceBead = beadOps.getBead(this.sql, sourceBeadId); + if (sourceBead?.rig_id) { + try { + const reworkAgent = agents.getOrCreateAgent( + this.sql, + 'polecat', + sourceBead.rig_id, + this.townId + ); + agents.hookBead(this.sql, reworkAgent.id, sourceBeadId); + this.dispatchAgent(reworkAgent, sourceBead).catch(err => + console.error( + `${TOWN_LOG} completeReviewWithResult: fire-and-forget rework dispatch failed for bead=${sourceBeadId}`, + err + ) + ); + } catch (err) { + console.warn( + `${TOWN_LOG} completeReviewWithResult: could not dispatch rework for bead=${sourceBeadId}:`, + err + ); + } + } + } } async agentDone(agentId: string, input: AgentDoneInput): Promise { @@ -966,6 +996,24 @@ export class TownDO extends DurableObject { }, }); + // Log a triage_resolved event on the target bead so the action shows + // up in the activity feed for the bead that was actually affected. + const targetBeadId = snapshotHookedBeadId ?? targetAgentId; + if (targetBeadId && targetBeadId !== input.triage_request_bead_id) { + beadOps.logBeadEvent(this.sql, { + beadId: targetBeadId, + agentId: input.agent_id, + eventType: 'triage_resolved', + newValue: action, + metadata: { + action, + resolution_notes: input.resolution_notes, + triage_request_bead_id: input.triage_request_bead_id, + target_agent_id: targetAgentId, + }, + }); + } + // If this triage request was created for an escalation, close the // linked escalation bead too so it doesn't sit open indefinitely. // The escalation_bead_id is nested under metadata.context (set by @@ -3245,7 +3293,7 @@ export class TownDO extends DurableObject { [ ...query( this.sql, - /* sql */ `SELECT COUNT(*) AS cnt FROM ${beads} WHERE ${beads.status} IN ('open', 'in_progress') AND ${beads.type} NOT IN ('agent', 'message')`, + /* sql */ `SELECT COUNT(*) AS cnt FROM ${beads} WHERE ${beads.status} IN ('open', 'in_progress', 'in_review') AND ${beads.type} NOT IN ('agent', 'message')`, [] ), ][0]?.cnt ?? 0 @@ -3274,6 +3322,7 @@ export class TownDO extends DurableObject { beads: { open: number; inProgress: number; + inReview: number; failed: number; triageRequests: number; }; @@ -3328,12 +3377,13 @@ export class TownDO extends DurableObject { [] ), ]; - const beadCounts = { open: 0, inProgress: 0, failed: 0, triageRequests: 0 }; + const beadCounts = { open: 0, inProgress: 0, inReview: 0, failed: 0, triageRequests: 0 }; for (const row of beadRows) { const s = `${row.status as string}`; const c = Number(row.cnt); if (s === 'open') beadCounts.open = c; else if (s === 'in_progress') beadCounts.inProgress = c; + else if (s === 'in_review') beadCounts.inReview = c; else if (s === 'failed') beadCounts.failed = c; } diff --git a/cloudflare-gastown/src/dos/town/agents.ts b/cloudflare-gastown/src/dos/town/agents.ts index ad7fcfae92..e5a34166ba 100644 --- a/cloudflare-gastown/src/dos/town/agents.ts +++ b/cloudflare-gastown/src/dos/town/agents.ts @@ -220,6 +220,9 @@ export function deleteAgent(sql: SqlStorage, agentId: string): void { // ── Hooks (GUPP) ──────────────────────────────────────────────────── +/** Bead types that are system-managed and should never be hooked to an agent. */ +const UNHOOKABLE_BEAD_TYPES = new Set(['escalation', 'convoy', 'agent', 'message']); + export function hookBead(sql: SqlStorage, agentId: string, beadId: string): void { const agent = getAgent(sql, agentId); if (!agent) throw new Error(`Agent ${agentId} not found`); @@ -227,6 +230,22 @@ export function hookBead(sql: SqlStorage, agentId: string, beadId: string): void const bead = getBead(sql, beadId); if (!bead) throw new Error(`Bead ${beadId} not found`); + // Prevent hooking to system-managed bead types that no agent should + // work on directly. Escalation beads are resolved by triage, convoy + // beads are containers, agent/message beads are metadata records. + if (UNHOOKABLE_BEAD_TYPES.has(bead.type)) { + throw new Error(`Cannot hook agent to bead ${beadId}: type '${bead.type}' is not workable`); + } + + // Triage request beads are resolved by the triage agent via + // gt_triage_resolve, not by hooking. Prevent polecats from + // accidentally picking these up. + if (bead.labels.includes('gt:triage-request')) { + throw new Error( + `Cannot hook agent to bead ${beadId}: triage requests are resolved via gt_triage_resolve` + ); + } + // Already hooked to this bead — idempotent if (agent.current_hook_bead_id === beadId) return; diff --git a/cloudflare-gastown/src/dos/town/review-queue.ts b/cloudflare-gastown/src/dos/town/review-queue.ts index aca88240c2..f595d78c48 100644 --- a/cloudflare-gastown/src/dos/town/review-queue.ts +++ b/cloudflare-gastown/src/dos/town/review-queue.ts @@ -313,6 +313,13 @@ export function completeReviewWithResult( conflict: true, }, }); + // Return source bead to in_progress so the polecat can be re-dispatched + // to resolve the conflict (in_review → in_progress rework flow). + updateBeadStatus(sql, entry.bead_id, 'in_progress', entry.agent_id); + } else if (input.status === 'failed') { + // Review failed (rework requested): return source bead to in_progress + // so it can be re-dispatched (in_review → in_progress rework flow). + updateBeadStatus(sql, entry.bead_id, 'in_progress', entry.agent_id); } } @@ -556,11 +563,13 @@ export function agentDone(sql: SqlStorage, agentId: string, input: AgentDoneInpu default_branch: rig?.default_branch, }); - // Close the source bead (matches upstream gt done behavior). The polecat's - // work is done — the MR bead now tracks the merge lifecycle. The source - // bead retains its assignee so we know which agent worked on it. + // Transition the source bead to in_review — the polecat's work is done + // but the refinery hasn't reviewed it yet. The MR bead tracks the merge + // lifecycle. The source bead retains its assignee so we know which agent + // worked on it. It will be closed (or returned to in_progress) by the + // refinery after review. unhookBead(sql, agentId); - closeBead(sql, sourceBead, agentId); + updateBeadStatus(sql, sourceBead, 'in_review', agentId); } /** diff --git a/cloudflare-gastown/src/handlers/rig-triage.handler.ts b/cloudflare-gastown/src/handlers/rig-triage.handler.ts index 50f36558ef..0041f9bf0e 100644 --- a/cloudflare-gastown/src/handlers/rig-triage.handler.ts +++ b/cloudflare-gastown/src/handlers/rig-triage.handler.ts @@ -29,7 +29,13 @@ const ResolveTriageBody = z.object({ }); export async function handleResolveTriage(c: Context, _params: { rigId: string }) { - const agentId = getEnforcedAgentId(c); + // In production, agentId comes from the verified JWT. In development + // (where authMiddleware is skipped), fall back to the identity header + // the container client sends with every request. The fallback is gated + // on ENVIRONMENT to prevent header spoofing in production. + const agentId = + getEnforcedAgentId(c) || + (c.env.ENVIRONMENT === 'development' ? c.req.header('X-Gastown-Agent-Id') : null); if (!agentId) { return c.json(resError('Agent authentication required'), 401); } diff --git a/cloudflare-gastown/src/prompts/mayor-system.prompt.ts b/cloudflare-gastown/src/prompts/mayor-system.prompt.ts index 6e360ee67b..db25815191 100644 --- a/cloudflare-gastown/src/prompts/mayor-system.prompt.ts +++ b/cloudflare-gastown/src/prompts/mayor-system.prompt.ts @@ -137,6 +137,8 @@ When a user asks "how's X going?" or wants a progress update: Convoys land automatically when all tracked beads close — no manual management needed. +Bead lifecycle: \`open\` → \`in_progress\` (polecat working) → \`in_review\` (gt_done called, awaiting refinery) → \`closed\` (merged) or back to \`in_progress\` (rework requested). + ## Conversational Model - **Respond directly for questions.** If the user asks a question you can answer from context, respond conversationally. Don't delegate questions. diff --git a/cloudflare-gastown/src/prompts/polecat-system.prompt.ts b/cloudflare-gastown/src/prompts/polecat-system.prompt.ts index 508f1de80c..b47f755481 100644 --- a/cloudflare-gastown/src/prompts/polecat-system.prompt.ts +++ b/cloudflare-gastown/src/prompts/polecat-system.prompt.ts @@ -24,7 +24,7 @@ You have these tools available. Use them to coordinate with the Gastown orchestr - **gt_prime** — Call at the start of your session to get full context: your agent record, hooked bead, undelivered mail, and open beads. Your context is injected automatically on first message, but call this if you need to refresh. - **gt_bead_status** — Inspect the current state of any bead by ID. - **gt_bead_close** — Close a bead when its work is fully complete and merged. -- **gt_done** — Signal that you are done with your current hooked bead. This pushes your branch, submits it to the review queue, and unhooks you. Always push your branch before calling gt_done. +- **gt_done** — Signal that you are done with your current hooked bead. This pushes your branch, submits it to the review queue, transitions the bead to \`in_review\`, and unhooks you. Always push your branch before calling gt_done. - **gt_mail_send** — Send a message to another agent in the rig. Use this for coordination, questions, or status sharing. - **gt_mail_check** — Check for new mail from other agents. Call this periodically or when you suspect coordination messages. - **gt_escalate** — Escalate a problem you cannot solve. Creates an escalation bead. Use this when you are stuck, blocked, or need human intervention. @@ -37,7 +37,7 @@ You have these tools available. Use them to coordinate with the Gastown orchestr 2. **Work**: Implement the bead's requirements. Write code, tests, and documentation as needed. 3. **Commit frequently**: Make small, focused commits. Push often. The container's disk is ephemeral — if it restarts, unpushed work is lost. 4. **Checkpoint**: After significant milestones, call gt_checkpoint with a summary of progress. -5. **Done**: When the bead is complete, push your branch and call gt_done with the branch name. +5. **Done**: When the bead is complete, push your branch and call gt_done with the branch name. The bead transitions to \`in_review\` and the refinery picks it up for merge. If the review fails (rework), you will be re-dispatched with the bead back in \`in_progress\`. ## Commit & Push Hygiene diff --git a/cloudflare-gastown/src/prompts/triage-system.prompt.ts b/cloudflare-gastown/src/prompts/triage-system.prompt.ts index 946c7daea1..dc4feb8611 100644 --- a/cloudflare-gastown/src/prompts/triage-system.prompt.ts +++ b/cloudflare-gastown/src/prompts/triage-system.prompt.ts @@ -54,10 +54,12 @@ This will close the triage batch, unhook you, and return you to idle. - **Prefer least-disruptive actions.** RESTART over CLOSE_BEAD. NUDGE over ESCALATE. - **Escalate genuinely hard problems.** If a situation requires human context you don't have, escalate rather than guess. - **Never skip a triage request.** Every pending request must be resolved. +- **Post status updates.** Call gt_status before starting the batch (e.g. "Triaging 3 requests") and after finishing (e.g. "Triage complete — 2 restarted, 1 escalated"). This keeps the dashboard informed. ## Available Tools - **gt_triage_resolve** — Resolve a triage request. Provide the triage_request_bead_id, chosen action, and brief notes. +- **gt_status** — Post a plain-language status update visible on the dashboard. Call this at the start and end of your triage batch. - **gt_mail_send** — Send guidance to a stuck agent. - **gt_escalate** — Forward a problem to the Mayor or human operators. - **gt_bead_close** — Close your hooked bead when all triage requests have been processed. diff --git a/cloudflare-gastown/src/trpc/router.ts b/cloudflare-gastown/src/trpc/router.ts index 4fbe8a5909..32df123799 100644 --- a/cloudflare-gastown/src/trpc/router.ts +++ b/cloudflare-gastown/src/trpc/router.ts @@ -278,7 +278,7 @@ export const gastownRouter = router({ .input( z.object({ rigId: z.string().uuid(), - status: z.enum(['open', 'in_progress', 'closed', 'failed']).optional(), + status: z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']).optional(), }) ) .output(z.array(RpcBeadOutput)) diff --git a/cloudflare-gastown/src/trpc/schemas.ts b/cloudflare-gastown/src/trpc/schemas.ts index 3071ee4610..584d54817d 100644 --- a/cloudflare-gastown/src/trpc/schemas.ts +++ b/cloudflare-gastown/src/trpc/schemas.ts @@ -34,7 +34,7 @@ export const RigOutput = z.object({ export const BeadOutput = z.object({ bead_id: z.string(), type: z.enum(['issue', 'message', 'escalation', 'merge_request', 'convoy', 'molecule', 'agent']), - status: z.enum(['open', 'in_progress', 'closed', 'failed']), + status: z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']), title: z.string(), body: z.string().nullable(), rig_id: z.string().nullable(), @@ -204,6 +204,7 @@ const AlarmStatusOutput = z.object({ beads: z.object({ open: z.number(), inProgress: z.number(), + inReview: z.number(), failed: z.number(), triageRequests: z.number(), }), diff --git a/cloudflare-gastown/src/types.ts b/cloudflare-gastown/src/types.ts index 1340d16e95..f721a9463b 100644 --- a/cloudflare-gastown/src/types.ts +++ b/cloudflare-gastown/src/types.ts @@ -4,7 +4,7 @@ import type { AgentMetadataRecord } from './db/tables/agent-metadata.table'; // -- Beads -- -export const BeadStatus = z.enum(['open', 'in_progress', 'closed', 'failed']); +export const BeadStatus = z.enum(['open', 'in_progress', 'in_review', 'closed', 'failed']); export type BeadStatus = z.infer; export const BeadType = z.enum([ diff --git a/cloudflare-gastown/src/ui/dashboard.ui.ts b/cloudflare-gastown/src/ui/dashboard.ui.ts index 1fee579ed9..dc414b5da4 100644 --- a/cloudflare-gastown/src/ui/dashboard.ui.ts +++ b/cloudflare-gastown/src/ui/dashboard.ui.ts @@ -47,6 +47,7 @@ export function dashboardHtml(): string { .badge { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 11px; } .badge.open { background: #1f6feb33; color: #58a6ff; } .badge.in_progress { background: #d29922aa; color: #e3b341; } + .badge.in_review { background: #8957e533; color: #bc8cff; } .badge.closed { background: #3fb95033; color: #3fb950; } .badge.idle { background: #21262d; color: #8b949e; } .badge.working { background: #d29922aa; color: #e3b341; } diff --git a/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx b/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx index 226696ab9d..da8b911759 100644 --- a/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx +++ b/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx @@ -135,6 +135,7 @@ export function TownOverviewPageClient({ townId }: TownOverviewPageClientProps) const userBeads = allBeads.filter(b => b.type !== 'agent'); const openBeadCount = userBeads.filter(b => b.status === 'open').length; const inProgressBeadCount = userBeads.filter(b => b.status === 'in_progress').length; + const inReviewBeadCount = userBeads.filter(b => b.status === 'in_review').length; const closedBeadCount = userBeads.filter(b => b.status === 'closed').length; const escalationsCount = events.filter(e => e.event_type === 'escalated').length; @@ -221,7 +222,7 @@ export function TownOverviewPageClient({ townId }: TownOverviewPageClientProps) {/* Left column: activity feed */}
{/* Stats strip */} -
+
} color="text-violet-400" /> + } + color="text-purple-400" + /> b.status === 'in_progress' && b.type !== 'agent' ).length; + const inReviewBeads = beads.filter(b => b.status === 'in_review' && b.type !== 'agent').length; const closedBeads = beads.filter(b => b.status === 'closed' && b.type !== 'agent').length; return ( @@ -126,9 +127,10 @@ export function RigDetailPageClient({ townId, rigId }: RigDetailPageClientProps)
{/* Stats strip */} -
+
+
diff --git a/src/components/gastown/ActivityFeed.tsx b/src/components/gastown/ActivityFeed.tsx index 5dc5cbe140..f198df6554 100644 --- a/src/components/gastown/ActivityFeed.tsx +++ b/src/components/gastown/ActivityFeed.tsx @@ -15,6 +15,7 @@ import { Mail, MessageSquare, ChevronRight, + ShieldCheck, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; @@ -29,6 +30,7 @@ const EVENT_ICONS: Record = { review_completed: GitMerge, mail_sent: Mail, agent_status: MessageSquare, + triage_resolved: ShieldCheck, }; const EVENT_COLORS: Record = { @@ -42,6 +44,7 @@ const EVENT_COLORS: Record = { review_completed: 'text-green-600', mail_sent: 'text-sky-500', agent_status: 'text-white/50', + triage_resolved: 'text-amber-500', }; type TownEvent = GastownOutputs['gastown']['getTownEvents'][number]; @@ -76,6 +79,12 @@ function eventDescription(event: { return `${rigPrefix}Review ${event.new_value ?? 'completed'}`; case 'mail_sent': return `${rigPrefix}Mail sent`; + case 'triage_resolved': { + const action = event.new_value ?? (event.metadata?.action as string | undefined) ?? 'unknown'; + const notes = event.metadata?.resolution_notes as string | undefined; + const desc = `${rigPrefix}Triage: ${action}`; + return notes ? `${desc} — ${notes}` : desc; + } case 'agent_status': { const msg = event.new_value ?? (event.metadata?.message as string | undefined); const agentName = event.metadata?.agent_name as string | undefined; diff --git a/src/components/gastown/BeadBoard.tsx b/src/components/gastown/BeadBoard.tsx index fbf3bfa77f..eb2316f619 100644 --- a/src/components/gastown/BeadBoard.tsx +++ b/src/components/gastown/BeadBoard.tsx @@ -30,17 +30,19 @@ type BeadBoardProps = { agentNameById?: Record; }; -const statusColumns = ['open', 'in_progress', 'closed'] as const; +const statusColumns = ['open', 'in_progress', 'in_review', 'closed'] as const; const statusLabels: Record = { open: 'Open', in_progress: 'In Progress', + in_review: 'In Review', closed: 'Closed', }; const statusColors: Record = { open: 'bg-blue-500/10 text-blue-400 border-blue-500/20', in_progress: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', + in_review: 'bg-purple-500/10 text-purple-400 border-purple-500/20', closed: 'bg-green-500/10 text-green-400 border-green-500/20', }; @@ -160,7 +162,7 @@ export function BeadBoard({ }: BeadBoardProps) { if (isLoading) { return ( -
+
{statusColumns.map(status => (
@@ -175,7 +177,7 @@ export function BeadBoard({ } return ( -
+
{statusColumns.map((status, colIdx) => { const columnBeads = beads.filter(b => b.status === status && b.type !== 'agent'); return ( diff --git a/src/components/gastown/TerminalBar.tsx b/src/components/gastown/TerminalBar.tsx index 6078f0b9e7..8957a22e16 100644 --- a/src/components/gastown/TerminalBar.tsx +++ b/src/components/gastown/TerminalBar.tsx @@ -148,7 +148,13 @@ export function TerminalBar({ townId }: TerminalBarProps) { type AlarmStatus = { alarm: { nextFireAt: string | null; intervalMs: number; intervalLabel: string }; agents: { working: number; idle: number; stalled: number; dead: number; total: number }; - beads: { open: number; inProgress: number; failed: number; triageRequests: number }; + beads: { + open: number; + inProgress: number; + inReview: number; + failed: number; + triageRequests: number; + }; patrol: { guppWarnings: number; guppEscalations: number; @@ -372,6 +378,11 @@ function AlarmStatusPane({ townId }: { townId: string }) { value={data.beads.inProgress} highlight={data.beads.inProgress > 0} /> + 0} + /> 0} />