Parent: #204 | Phase 1: Single Rig, Single Polecat
Note: This was previously part of #212, which has been repurposed as the Rig DO Alarm. The tRPC routes are now a separate issue.
Goal
Dashboard API for creating and managing towns and rigs. The sling mutation creates DO state and arms the alarm — the alarm handles dispatching to the container. All reads go through the Gastown worker HTTP API (DO SQLite), no Postgres.
New Router
src/server/routers/gastown.ts
export const gastownRouter = router({
// -- Towns --
createTown: protectedProcedure.input(z.object({
name: z.string().min(1).max(64),
})).mutation(async ({ ctx, input }) => { /* create town via gastown worker */ }),
listTowns: protectedProcedure
.query(async ({ ctx }) => { /* list towns for current user */ }),
getTown: protectedProcedure.input(z.object({ townId: z.string().uuid() }))
.query(async ({ ctx, input }) => { /* get town with rigs */ }),
// -- Rigs --
createRig: protectedProcedure.input(z.object({
townId: z.string().uuid(),
name: z.string().min(1).max(64),
gitUrl: z.string().url(),
defaultBranch: z.string().default('main'),
})).mutation(async ({ ctx, input }) => { /* create rig, initialize Rig DO */ }),
getRig: protectedProcedure.input(z.object({ rigId: z.string().uuid() }))
.query(async ({ ctx, input }) => { /* get rig with agents, active beads */ }),
// -- Beads (read from DO via worker API) --
listBeads: protectedProcedure.input(z.object({
rigId: z.string().uuid(),
status: z.enum(['open', 'in_progress', 'closed', 'cancelled']).optional(),
})).query(async ({ ctx, input }) => { /* list beads via gastown worker */ }),
// -- Agents --
listAgents: protectedProcedure.input(z.object({ rigId: z.string().uuid() }))
.query(async ({ ctx, input }) => { /* list agents via gastown worker */ }),
// -- Work Assignment --
sling: protectedProcedure.input(z.object({
rigId: z.string().uuid(),
title: z.string(),
body: z.string().optional(),
model: z.string().default('kilo/auto'),
})).mutation(async ({ ctx, input }) => {
// 1. Create bead in Rig DO (via internal auth HTTP call)
// 2. Register or pick an agent (Rig DO allocates name)
// 3. Hook bead to agent (Rig DO updates state)
// 4. Arm Rig DO alarm → alarm will dispatch agent to container
// 5. Return agent info (no stream URL yet — comes from container)
}),
// -- Send message to Mayor --
sendMessage: protectedProcedure.input(z.object({
townId: z.string().uuid(),
message: z.string(),
model: z.string().default('kilo/auto'),
})).mutation(async ({ ctx, input }) => {
// 1. Create a message bead assigned to the Mayor agent
// 2. Arm alarm → dispatches to container
}),
// -- Agent Streams --
getAgentStreamUrl: protectedProcedure.input(z.object({
agentId: z.string().uuid(),
townId: z.string().uuid(),
})).query(async ({ ctx, input }) => {
// Fetch stream ticket from container via TownContainer.fetch()
// Return WebSocket URL for dashboard to connect to
}),
});
Key difference from original #212: The sling mutation no longer creates a cloud-agent-next session. It creates state in the DO and arms the alarm. The alarm handles dispatching to the container. This decouples the API response time from container cold starts.
Dependencies
- PR 1 (Rig DO)
- PR 2 (HTTP API Layer)
- PR 4 (Town Container)
- PR 5 (Rig DO Alarm)
Acceptance Criteria
Parent: #204 | Phase 1: Single Rig, Single Polecat
Goal
Dashboard API for creating and managing towns and rigs. The
slingmutation creates DO state and arms the alarm — the alarm handles dispatching to the container. All reads go through the Gastown worker HTTP API (DO SQLite), no Postgres.New Router
src/server/routers/gastown.tsKey difference from original #212: The
slingmutation no longer creates a cloud-agent-next session. It creates state in the DO and arms the alarm. The alarm handles dispatching to the container. This decouples the API response time from container cold starts.Dependencies
Acceptance Criteria
gastownRouteradded to tRPC app routerslingmutation: creates bead → assigns agent → hooks bead → arms alarmsendMessagemutation for Mayor communication