Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions cloudflare-gastown/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@

## SQL queries

- Use the type-safe `query()` helper from `util/query.util.ts` for all SQL queries.
- Prefix SQL template strings with `/* sql */` for syntax highlighting and to signal intent, e.g. `query(this.sql, /* sql */ \`SELECT ...\`, [...])`.
- Format queries for human readability: use multi-line strings with one clause per line (`SELECT`, `FROM`, `WHERE`, `SET`, etc.).
- Reference tables and columns via the table interpolator objects exported from `db/tables/*.table.ts` (created with `getTableFromZodSchema` from `util/table.ts`). Never use raw table/column name strings in queries. The interpolator has three access patterns — use the right one for context:
- `${beads}` → bare table name. Use for `FROM`, `INSERT INTO`, `DELETE FROM`.
- `${beads.columns.status}` → bare column name. Use for `SET` clauses and `INSERT` column lists where the table is already implied.
- `${beads.status}` → qualified `table.column`. Use for `SELECT`, `WHERE`, `JOIN ON`, `ORDER BY`, and anywhere a column could be ambiguous.
- Prefer static queries over dynamically constructed ones. Move conditional logic into the query itself using SQL constructs like `COALESCE`, `CASE`, `NULLIF`, or `WHERE (? IS NULL OR col = ?)` patterns so the full query is always visible as a single readable string.
- Always parse query results with the Zod `Record` schemas from `db/tables/*.table.ts`. Never use ad-hoc `as Record<string, unknown>` casts or `String(row.col)` to extract fields — use `.pick()` for partial selects and `.array()` for lists, e.g. `BeadRecord.pick({ bead_id: true }).array().parse(rows)`. This keeps row parsing type-safe and co-located with the schema definition.
- When a column has a SQL `CHECK` constraint that restricts it to a set of values (i.e. an enum), mirror that in the Record schema using `z.enum()` rather than `z.string()`, e.g. `role: z.enum(['polecat', 'refinery', 'mayor', 'witness'])`.
- Use the Drizzle query builder (`db.select()`, `db.insert()`, `db.update()`, `db.delete()`) for all database operations. Do not use raw SQL strings.
- Import table objects from `db/sqlite-schema.ts`. Reference columns via the table object (e.g. `beads.bead_id`, `agent_metadata.status`).
- Use `$inferSelect` / `$inferInsert` types from `db/sqlite-schema.ts` for row types. Do not define ad-hoc row types or use Zod schemas for DB result parsing.
- For JSON columns stored as `text` (`labels`, `metadata`, `config`, `checkpoint`, `data`), parse with `JSON.parse()` after reading and serialize with `JSON.stringify()` before writing.
- Use `.get()` for single-row results, `.all()` for multi-row results, `.run()` for write operations.
- Use `eq()`, `and()`, `or()`, `inArray()`, `gt()`, `lt()`, `isNull()`, `isNotNull()` from `drizzle-orm` for WHERE conditions.
- Use `.innerJoin(table, condition)` for joins.
- For conditional filters, build a `conditions: SQL[]` array and pass to `and(...conditions)`.
- Reference `docs/do-sqlite-drizzle.md` (repo root) for the drizzle migration workflow (schema changes, generating migrations).

## HTTP routes

Expand Down
8 changes: 8 additions & 0 deletions cloudflare-gastown/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
out: './drizzle',
schema: './src/db/sqlite-schema.ts',
dialect: 'sqlite',
driver: 'durable-sqlite',
});
135 changes: 135 additions & 0 deletions cloudflare-gastown/drizzle/0000_mushy_elektra.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
CREATE TABLE IF NOT EXISTS `agent_metadata` (
`bead_id` text PRIMARY KEY NOT NULL,
`role` text NOT NULL,
`identity` text NOT NULL,
`container_process_id` text,
`status` text DEFAULT 'idle' NOT NULL,
`current_hook_bead_id` text,
`dispatch_attempts` integer DEFAULT 0 NOT NULL,
`checkpoint` text,
`last_activity_at` text,
FOREIGN KEY (`bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`current_hook_bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
CONSTRAINT "check_agent_metadata_role" CHECK("agent_metadata"."role" in ('polecat', 'refinery', 'mayor', 'witness')),
CONSTRAINT "check_agent_metadata_status" CHECK("agent_metadata"."status" in ('idle', 'working', 'stalled', 'dead'))
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `agent_metadata_identity_unique` ON `agent_metadata` (`identity`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `bead_dependencies` (
`bead_id` text NOT NULL,
`depends_on_bead_id` text NOT NULL,
`dependency_type` text DEFAULT 'blocks' NOT NULL,
FOREIGN KEY (`bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`depends_on_bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
CONSTRAINT "check_bead_deps_type" CHECK("bead_dependencies"."dependency_type" in ('blocks', 'tracks', 'parent-child'))
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `idx_bead_deps_pk` ON `bead_dependencies` (`bead_id`,`depends_on_bead_id`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_bead_deps_depends_on` ON `bead_dependencies` (`depends_on_bead_id`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `bead_events` (
`bead_event_id` text PRIMARY KEY NOT NULL,
`bead_id` text NOT NULL,
`agent_id` text,
`event_type` text NOT NULL,
`old_value` text,
`new_value` text,
`metadata` text DEFAULT '{}',
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_bead_events_bead` ON `bead_events` (`bead_id`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_bead_events_created` ON `bead_events` (`created_at`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_bead_events_type` ON `bead_events` (`event_type`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `beads` (
`bead_id` text PRIMARY KEY NOT NULL,
`type` text NOT NULL,
`status` text DEFAULT 'open' NOT NULL,
`title` text NOT NULL,
`body` text,
`rig_id` text,
`parent_bead_id` text,
`assignee_agent_bead_id` text,
`priority` text DEFAULT 'medium',
`labels` text DEFAULT '[]',
`metadata` text DEFAULT '{}',
`created_by` text,
`created_at` text NOT NULL,
`updated_at` text NOT NULL,
`closed_at` text,
FOREIGN KEY (`parent_bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
CONSTRAINT "check_beads_type" CHECK("beads"."type" in ('issue', 'message', 'escalation', 'merge_request', 'convoy', 'molecule', 'agent')),
CONSTRAINT "check_beads_status" CHECK("beads"."status" in ('open', 'in_progress', 'closed', 'failed')),
CONSTRAINT "check_beads_priority" CHECK("beads"."priority" in ('low', 'medium', 'high', 'critical'))
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_beads_type_status` ON `beads` (`type`,`status`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_beads_parent` ON `beads` (`parent_bead_id`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_beads_rig_status` ON `beads` (`rig_id`,`status`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_beads_assignee` ON `beads` (`assignee_agent_bead_id`,`type`,`status`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `convoy_metadata` (
`bead_id` text PRIMARY KEY NOT NULL,
`total_beads` integer DEFAULT 0 NOT NULL,
`closed_beads` integer DEFAULT 0 NOT NULL,
`landed_at` text,
FOREIGN KEY (`bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `escalation_metadata` (
`bead_id` text PRIMARY KEY NOT NULL,
`severity` text NOT NULL,
`category` text,
`acknowledged` integer DEFAULT 0 NOT NULL,
`re_escalation_count` integer DEFAULT 0 NOT NULL,
`acknowledged_at` text,
FOREIGN KEY (`bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action,
CONSTRAINT "check_escalation_severity" CHECK("escalation_metadata"."severity" in ('low', 'medium', 'high', 'critical'))
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `review_metadata` (
`bead_id` text PRIMARY KEY NOT NULL,
`branch` text NOT NULL,
`target_branch` text DEFAULT 'main' NOT NULL,
`merge_commit` text,
`pr_url` text,
`retry_count` integer DEFAULT 0,
FOREIGN KEY (`bead_id`) REFERENCES `beads`(`bead_id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `rig_agent_events` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`agent_id` text NOT NULL,
`event_type` text NOT NULL,
`data` text DEFAULT '{}' NOT NULL,
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_rig_agent_events_agent_id` ON `rig_agent_events` (`agent_id`);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_rig_agent_events_agent_created` ON `rig_agent_events` (`agent_id`,`id`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `rigs` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`git_url` text DEFAULT '' NOT NULL,
`default_branch` text DEFAULT 'main' NOT NULL,
`config` text DEFAULT '{}',
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `idx_rigs_name` ON `rigs` (`name`);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `user_rigs` (
`id` text PRIMARY KEY NOT NULL,
`town_id` text NOT NULL,
`name` text NOT NULL,
`git_url` text NOT NULL,
`default_branch` text DEFAULT 'main' NOT NULL,
`platform_integration_id` text,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `user_towns` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`owner_user_id` text NOT NULL,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
Loading