From 3ef07a9309577466d537fc47ba95282f21dc383f Mon Sep 17 00:00:00 2001 From: syn Date: Wed, 1 Jul 2026 16:03:35 -0500 Subject: [PATCH 1/5] docs(agents): consolidate AGENTS.md guidance into skills and rules Reduce always-loaded agent context by moving conditionally-relevant and duplicated guidance out of AGENTS.md files into the mechanisms that load on demand or by reference. - Move duplicated Cloudflare Worker/DO conventions (file naming, DO stub helpers, sub-module splitting, IO boundaries, SQL, HTTP routes, withDORetry) into a shared durable-objects skill reference; trim gastown/wasteland/ mcp-gateway/cloud-agent-next AGENTS.md to pointers + genuine deltas. - Move the module-scope DB-client-caching rule into workers-best-practices skill; drop the Workers & Durable Objects section from root AGENTS.md. - Consolidate coding standards into .kilo/rules/coding-style.md (reconciling the `as`-operator rule to the nuanced version); remove restatements from root, extension, mobile, and kiloclaw AGENTS.md. Fix stale .kilocode path. - Dedup GDPR/PII rule (kept in .kilo/rules/gdpr-pii.md). - Move PR conventions into a /cloud-pr command. - Move the spec index into a specs skill (rebuilt from disk, all specs except template.md); add per-service spec pointers to kiloclaw, kiloclaw-billing, and mcp-gateway. - Delete the CI-enforced Markdown Tables prose. - Rescope the Domain Context section to state CONTEXT.md covers only the Code Reviewer and Security Agent domains. --- .agents/skills/durable-objects/SKILL.md | 3 + .../references/repo-conventions.md | 189 ++++++++++++++++++ .agents/skills/specs/SKILL.md | 35 ++++ .../skills/workers-best-practices/SKILL.md | 1 + .../references/rules.md | 30 +++ .kilo/command/cloud-pr.md | 30 +++ .kilo/rules/coding-style.md | 8 +- AGENTS.md | 105 +--------- apps/extension/AGENTS.md | 3 +- apps/mobile/AGENTS.md | 1 - services/cloud-agent-next/AGENTS.md | 2 +- services/gastown/AGENTS.md | 97 +-------- services/kiloclaw-billing/AGENTS.md | 4 + services/kiloclaw/AGENTS.md | 11 +- services/mcp-gateway/AGENTS.md | 42 +--- services/wasteland/AGENTS.md | 96 +-------- 16 files changed, 329 insertions(+), 328 deletions(-) create mode 100644 .agents/skills/durable-objects/references/repo-conventions.md create mode 100644 .agents/skills/specs/SKILL.md create mode 100644 .kilo/command/cloud-pr.md diff --git a/.agents/skills/durable-objects/SKILL.md b/.agents/skills/durable-objects/SKILL.md index ad6ae1a438..6b8ae67af3 100644 --- a/.agents/skills/durable-objects/SKILL.md +++ b/.agents/skills/durable-objects/SKILL.md @@ -34,6 +34,9 @@ Fetch the relevant doc page when implementing features. - `./references/rules.md` - Core rules, storage, concurrency, RPC, alarms - `./references/testing.md` - Vitest setup, unit/integration tests, alarm testing - `./references/workers.md` - Workers handlers, types, wrangler config, observability +- `./references/repo-conventions.md` - This repo's file naming, DO stub helpers, + sub-module splitting, IO boundary/SQL/HTTP route conventions shared by + `services/gastown`, `services/wasteland`, and `services/mcp-gateway` Search: `blockConcurrencyWhile`, `idFromName`, `getByName`, `setAlarm`, `sql.exec` diff --git a/.agents/skills/durable-objects/references/repo-conventions.md b/.agents/skills/durable-objects/references/repo-conventions.md new file mode 100644 index 0000000000..c62eeb732d --- /dev/null +++ b/.agents/skills/durable-objects/references/repo-conventions.md @@ -0,0 +1,189 @@ +# Repo Conventions — Worker/DO Services in This Monorepo + +These conventions apply to Cloudflare Worker services in `services/` that use +Durable Objects. The file naming/sub-module/IO-boundary/SQL/HTTP-route sections +below are specific to the Hono + Zod service family (currently `services/gastown`, +`services/wasteland`, `services/mcp-gateway`); the DO call retry section applies +to every service in the repo that calls a DO stub. They are repo-specific patterns +layered on top of the general Durable Objects rules in `references/rules.md`; each +service's own `AGENTS.md` documents only its scope and genuine deltas from this +shared contract. + +## DO call retries + +Retry Durable Object stub calls with `withDORetry` from `@kilocode/worker-utils` +(`packages/worker-utils/src/do-retry.ts`) rather than hand-rolling retry/backoff +loops. It creates a fresh stub per attempt (required, since certain errors break a +stub), retries only errors with Cloudflare's documented `.retryable === true` +property, and applies jittered exponential backoff. + +```ts +const result = await withDORetry( + () => env.MY_DO.get(env.MY_DO.idFromName(key)), // fresh stub per attempt + stub => stub.getMetadata(), + 'getMetadata' +); +``` + +Services that need service-local log correlation wrap it with their own logger +bound in, e.g. `services/cloud-agent-next/src/utils/do-retry.ts` and +`services/webhook-agent-ingest/src/util/do-retry.ts` — both are thin +logger-binding adapters over the shared `withDORetry`, not reimplementations. +Prefer this pattern (a local `withDORetry` re-export bound to the service logger) +over calling the base helper directly with no logger, and over copying the retry +loop itself. Used today by `cloud-agent-next`, `webhook-agent-ingest`, +`session-ingest`, `kilo-chat`, `kiloclaw`, and `code-review-infra`. + +## File naming + +- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`, + `connect.handler.ts`, `routes.schema.ts`, `instances.table.ts`. +- Modules that predominantly export a class should be named after that class, e.g. + `AgentIdentity.do.ts` for `AgentIdentityDO`, `MCPGatewayInstance.do.ts` for + `MCPGatewayInstance`. +- Keep pure helpers in `lib/` and route handlers in `handlers/`. + +## DO stub helper + +Each DO module must export a `get{ClassName}Stub` helper function (e.g. +`getRigDOStub`) that centralizes how that DO namespace creates instances. Callers +use this helper instead of accessing the namespace binding directly. + +## Sub-modules for large DOs + +When a Durable Object grows beyond a few hundred lines, extract domain logic into +sub-modules under a `/` directory alongside the DO file. For example, +`Town.do.ts` delegates to modules in `town/`: + +``` +dos/ + Town.do.ts # Class definition, RPC methods, alarm loop + town/ + agents.ts # Agent CRUD, hook management + beads.ts # Bead CRUD, convoy progress + scheduling.ts # Agent dispatch, pending work scheduling + review-queue.ts # Review lifecycle, recovery + patrol.ts # Zombie detection, stale hook recovery + config.ts # Town configuration + rigs.ts # Rig registry + mail.ts # Inter-agent mail + container-dispatch.ts # Container start/stop/status +``` + +Each sub-module exports plain functions (not classes) that accept `SqlStorage` and +any other required context as arguments. The DO imports them with the +`import * as X` pattern: + +```ts +import * as beadOps from './town/beads'; +import * as agents from './town/agents'; +import * as scheduling from './town/scheduling'; + +// In the DO class: +beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId); +agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); +await scheduling.schedulePendingWork(this.schedulingCtx); +``` + +This keeps the DO class thin (RPC surface + orchestration) while sub-modules own +the business logic. The `import * as X` pattern makes call sites self-documenting — +you can always tell which domain a function belongs to. + +## IO boundaries + +- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE + event payloads, subprocess output, upstream responses, persisted session + records) with Zod schemas. Return `unknown` from raw fetch/parse helpers and + `.parse()` in the caller. +- Never use `as` to cast IO data. If the shape is known, define a Zod schema; if + not, use `.passthrough()` or a catch-all schema. +- Some services are stricter than others at these boundaries (e.g. `mcp-gateway` + validates MCP protocol messages, headers, and query params in addition to the + baseline above) — check the service's own `AGENTS.md` for boundary-specific + additions. + +## Column naming + +Never name a primary key column just `id`. Encode the entity in the column name, +e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes +grep-based navigation reliable. + +## 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: multi-line strings, 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. 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. +- **Do not alias tables in SQL queries.** Always use the full table name with the + qualified `${table.column}` interpolator. Aliases like `FROM beads b` combined + with the qualified interpolator produce double-qualified names + (`b.beads.bead_id`) that SQLite rejects. If a self-join requires + disambiguation, use a raw string alias only for the second copy and reference + its columns with `${table.columns.col}` (bare) prefixed manually. +- 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` 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)`. +- When a column has a SQL `CHECK` constraint restricting 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'])`. + +## HTTP routes + +- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). + Define all routes in the main worker entry point (e.g. `gastown.worker.ts`, + `mcp-gateway.worker.ts`) so a human can scan one file and see every exposed + route. +- Move handler logic into `handlers/*.handler.ts` modules. Each module owns + routes for a logical domain. Name the file after the domain, e.g. + `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. +- Each handler function takes two arguments: + 1. The Hono `Context` object (typed as the app's env type). + 2. A plain object containing the route params parsed from the path, e.g. + `{ rigId: string }` or `{ rigId: string; beadId: string }`. + + This keeps the handler's contract explicit and testable, while the route + definition in the entry point is the single source of truth for + path → param shape. + +```ts +// gastown.worker.ts — route definition +app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); + +// handlers/rig-agents.handler.ts — handler implementation +export async function handleRegisterAgent(c: Context, params: { rigId: string }) { + // Zod validation lives in the handler, not as route middleware + const parsed = RegisterAgentBody.safeParse(await c.req.json()); + if (!parsed.success) { + return c.json( + { success: false, error: 'Invalid request body', issues: parsed.error.issues }, + 400 + ); + } + const rig = getRigDOStub(c.env, params.rigId); + const agent = await rig.registerAgent(parsed.data); + return c.json(resSuccess(agent), 201); +} +``` + +## DB clients + +See `workers-best-practices` skill (`references/rules.md` → "Repo rule: never +cache DB clients/pools in module scope") for the Hyperdrive/`getWorkerDb` +lifecycle rule shared by all Workers in this repo. Durable Object instance fields +(e.g. a Drizzle/SQLite wrapper over `state.storage`) are exempt — that's +object-local state, not module-scope caching. diff --git a/.agents/skills/specs/SKILL.md b/.agents/skills/specs/SKILL.md new file mode 100644 index 0000000000..d402261588 --- /dev/null +++ b/.agents/skills/specs/SKILL.md @@ -0,0 +1,35 @@ +--- +name: specs +description: Business-rule specs in .specs/ govern KiloClaw billing/lifecycle/controller/data model/Composio, MCP Gateway auth, model experiments, Security Agent, subscription center, team/enterprise seat billing, Impact affiliate/referrals, Kilo Pass, organization SSO, Stripe early fraud warnings, and coding plans. Load before making ANY change (bug fix, feature, refactor, or review) to a domain covered by one of these specs, to read the authoritative rules first. +--- + +# Business-Rule Specs + +Specs in `.specs/` are the authoritative source of truth for the business rules and +invariants of the domains they cover. Before making **any** change to a covered +domain — including bug fixes, new features, refactors, or reviews — you **must** +first read the relevant spec. Implementation mechanics (route names, columns, retry +cadence) belong in plans and code; the spec owns the rules and invariants. + +## Index + +| Spec | Governs | +|---|---| +| `.specs/kiloclaw-billing.md` | KiloClaw billing, pricing, invoicing, usage metering, payment flows | +| `.specs/kiloclaw-billing-lifecycle.md` | KiloClaw billing lifecycle — credit-renewal orchestration safety | +| `.specs/kiloclaw-composio.md` | KiloClaw Composio credential provisioning, injection, and sharing | +| `.specs/kiloclaw-controller.md` | KiloClaw controller/machine lifecycle, bootstrap, Docker image | +| `.specs/kiloclaw-datamodel.md` | KiloClaw data model — instance/subscription tables, invariants | +| `.specs/mcp-gateway-auth.md` | Kilo MCP Gateway v1 — protocol surface, ownership, OAuth lifecycle, provider grants, runtime auth | +| `.specs/model-experiments.md` | Model experiment routing, bucketing, lifecycle, prompt retention, and reporting rules | +| `.specs/security-agent.md` | Security Agent Auto Remediation and finding/SLA notification guarantees | +| `.specs/subscription-center.md` | Subscription Center ownership, states, and user-facing behavior | +| `.specs/team-enterprise-seat-billing.md` | Team and Enterprise seat billing, subscription management | +| `.specs/impact-affiliate-tracking.md` | Impact.com affiliate conversion tracking | +| `.specs/impact-referrals.md` | Impact.com Advocate referral programs for KiloClaw and Kilo Pass | +| `.specs/kilo-pass.md` | Kilo Pass states, provider support, credit amounts, eligibility, lifecycle | +| `.specs/organization-sso.md` | Organization SSO enforcement — auth requirements, membership admission/removal, policy inheritance | +| `.specs/stripe-early-fraud-warnings.md` | Stripe Early Fraud Warning enforcement — scope, containment, financial unwinding, remediation | +| `.specs/coding-plans.md` | Coding Plans business rules (RFC 2119 normative language) | + +`.specs/template.md` is the authoring template for new specs, not a governed domain. diff --git a/.agents/skills/workers-best-practices/SKILL.md b/.agents/skills/workers-best-practices/SKILL.md index 6516c8c015..c0480adcfa 100644 --- a/.agents/skills/workers-best-practices/SKILL.md +++ b/.agents/skills/workers-best-practices/SKILL.md @@ -60,6 +60,7 @@ mkdir -p /tmp/workers-types-latest && \ | Queues & Workflows | Move async/background work off the critical path | | Service bindings | Use service bindings for Worker-to-Worker calls — not public HTTP | | Hyperdrive | Always use Hyperdrive for external PostgreSQL/MySQL connections | +| Repo: DB client lifecycle | Never cache DB clients/pools in module scope (Worker or DO); use `getWorkerDb(...)` per use. DO instance fields are fine for object-local state. See `references/rules.md` | ### Observability diff --git a/.agents/skills/workers-best-practices/references/rules.md b/.agents/skills/workers-best-practices/references/rules.md index 1e7b969581..b72924efef 100644 --- a/.agents/skills/workers-best-practices/references/rules.md +++ b/.agents/skills/workers-best-practices/references/rules.md @@ -268,6 +268,36 @@ async fetch(request: Request, env: Env): Promise { **Retrieve**: `/hyperdrive/` for current configuration and supported databases. +### Repo rule: never cache DB clients/pools in module scope (Workers or Durable Objects) + +This repo's Workers and Durable Objects must not cache database clients, pools, or other transport-owning/request-context-bound SDK objects in module scope. Workers reuse isolates across requests, and Durable Object classes in the same Worker can share module memory across object instances — stale module-scope I/O state causes cross-context runtime failures, not just the generic "stale request state" anti-pattern above. + +**Check**: no `const client = new Client(...)` (or Drizzle/pg pool equivalent) declared at module scope, even for "read-only" or "singleton" convenience. + +- Create external database clients through the approved per-use helper, `getWorkerDb(...)`; let Hyperdrive own pooling. +- Only cache pure data or context-independent values in module scope (e.g. static config objects, parsed constants). +- Durable Object **instance fields** are fine for object-local state created from constructor inputs — for example, SQLite/Drizzle wrappers over `state.storage`. That is per-object state, not shared module state. +- If an optimization appears to require module-scope client caching, stop and document why lifetime, transport ownership, binding freshness, and Cloudflare runtime behavior make it safe before implementing it. + +```ts +// Correct: per-request/per-call helper owns client lifecycle +export async function handler(env: Env) { + const db = getWorkerDb(env.HYPERDRIVE.connectionString); + return db.select().from(users); +} +``` + +Anti-pattern: + +```ts +// Module-scope client — shared across isolate reuse and (for DOs) across instances +const db = drizzle(new Client({ connectionString: env.HYPERDRIVE.connectionString })); + +export async function handler() { + return db.select().from(users); // stale/leaked connection risk +} +``` + --- ## Observability diff --git a/.kilo/command/cloud-pr.md b/.kilo/command/cloud-pr.md new file mode 100644 index 0000000000..e0f3e437b9 --- /dev/null +++ b/.kilo/command/cloud-pr.md @@ -0,0 +1,30 @@ +--- +description: Create a pull request following repo conventions +--- + +Create a pull request for the current branch following the repo conventions below. $ARGUMENTS + +Before opening the PR, inspect the branch: review all commits on it (not just the latest), `git status`, `git diff` against the base branch, and remote tracking state. + +## Titles + +- Format: `type(scope): ` (e.g., `feat(auth): add SSO login`) +- Common types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `ci`, `style`, `perf` +- Imperative mood, under 72 characters, no trailing period. + +## Descriptions + +Follow the PR template in `.github/pull_request_template.md`. Every description must include four sections in order: + +1. **`## Summary`** — What changed and why. Outcome-focused, call out architectural changes. +2. **`## Verification`** — Manual verification only. Do not list automated checks such as `pnpm typecheck`, `pnpm test`, `pnpm lint`, `pnpm validate`, CI, or formatting commands here. +3. **`## Visual Changes`** — Before/after screenshots, or `N/A`. +4. **`## Reviewer Notes`** — Risk areas, tricky logic, rollout notes, or `N/A`. + +Do not leave HTML comments from the template. Review all commits on the branch when writing the summary. + +## Workflow + +- Create PRs as **ready for review** by default. Only use `--draft` if explicitly requested. +- When assigning PRs or issues, resolve the GitHub username with `gh api user --jq '.login'`. Never guess usernames. +- Never use `--force`, `--no-verify`, or any other flag that bypasses git hooks without explicit user approval. If a hook or check fails, diagnose and fix it or ask how to proceed — do not silently skip it. diff --git a/.kilo/rules/coding-style.md b/.kilo/rules/coding-style.md index 71ffda8281..160d785ae9 100644 --- a/.kilo/rules/coding-style.md +++ b/.kilo/rules/coding-style.md @@ -5,9 +5,11 @@ - KISS: Be wary of over-abstracting code. Do report and ask about violations of DRY, but don't prematurely generalize. - If trivial, avoid TS classes; use e.g. closures instead - STRONGLY AVOID coding patterns that cannot be statically checked: - - AVOID typescript's "as" operator - - AVOID typescript's null-forgiving "!" - - INSTEAD TRY where possible typescript's "satisfies", or leverage flow-sensitive typing. + - Use `as` casts sparingly, but do not ban them outright. Prefer `satisfies`, discriminated unions, generics, or flow-sensitive narrowing when TypeScript can be made to understand the type naturally. + - A targeted `as` cast is acceptable when code is at a known boundary where TypeScript has lost information that the surrounding control flow guarantees. For example, inside a platform switch, casting `message` to `Message` or `Message` is preferable to adding generic `Record` property helpers just to read known adapter fields. + - Avoid broad casts that hide real uncertainty, especially `as any`, double casts through `unknown`, or casting external/untrusted data without validation. Use runtime validation when the data shape is genuinely unknown, user-controlled, persisted, or coming from an API contract we do not own. + - `as` casts are explicitly permitted inside test files (e.g. `*.test.ts`, `*.spec.ts`, files under `__tests__/`, and other test fixtures/helpers) — they are commonly needed for fixture construction, narrowing partial mocks, and exercising error paths. Production code conventions still apply to non-test code imported by tests. + - AVOID typescript's null-forgiving "!"; prefer explicit checks or flow-sensitive typing. - Prefer clear NAMES (for e.g. variables, functions and tests) over COMMENTS. - ONLY add comments about things that are NOT OBVIOUS in context. diff --git a/AGENTS.md b/AGENTS.md index 191eb43b3c..cdce2d39a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,14 +21,15 @@ scripts/ CI and one-off scripts ## Domain Context -Before changing domain behavior, read `CONTEXT.md`. -Use canonical terms from `CONTEXT.md` in code, docs, task descriptions, tests, and agent outputs. -Do not introduce synonyms for existing concepts unless updating `CONTEXT.md` first. -Do not duplicate the full context contract inside `AGENTS.md`. +`CONTEXT.md` is the domain-language and ownership contract for the **Code Reviewer** and **Security Agent** domains only (review execution/analytics, Security Findings, Security Sync, notifications, remediation, and their email delivery — mainly `apps/web/src/lib/{code-reviews,security-agent}/`, `services/security-sync/`). It does not cover other areas of the monorepo. + +When working in those domains, read `CONTEXT.md` first and use its canonical terms in code, docs, task descriptions, tests, and agent outputs. Do not introduce synonyms for its concepts without updating `CONTEXT.md` first, and do not duplicate the full contract inside `AGENTS.md`. ## Verification -After making changes, verify your work with the narrowest relevant checks. Avoid running the full `pnpm typecheck` by default; it is slow enough to make development environments unusable. Prefer targeted package checks or `scripts/typecheck-all.sh --changes-only`, and mention in your final response when the full typecheck was skipped for this reason. Run the full suite when appropriate. **Always run `pnpm format` before committing** — CI will reject unformatted code. +Verify your work with the narrowest relevant checks. Prefer targeted package checks or `scripts/typecheck-all.sh --changes-only`. + +**Always run `pnpm format` before committing** | Command | What it checks | |---|---| @@ -38,40 +39,14 @@ After making changes, verify your work with the narrowest relevant checks. Avoid | `pnpm validate` | All three above in sequence | | `pnpm format` | Auto-format with oxfmt | -Target a specific test file: `pnpm test -- `. Run tests for a specific service: `pnpm --filter test`. - **Before running tests**, ensure the test database is running. If there is no active Postgres instance, run `pnpm test:db` first — this starts the Postgres container and applies migrations. You can check whether Postgres is already running with `docker compose -f dev/docker-compose.yml ps postgres`. -## apps/web UI Work - -When editing UI files in `apps/web` — React components, pages, layouts, or styles (`.tsx`/`.css`) — use the `kilo-design` skill. - -## Coding Standards - -- Prefer `type` over `interface`. -- Use `as` casts sparingly, but do not ban them outright. Prefer `satisfies`, discriminated unions, generics, or flow-sensitive narrowing when TypeScript can be made to understand the type naturally. -- A targeted `as` cast is acceptable when code is at a known boundary where TypeScript has lost information that the surrounding control flow guarantees. For example, inside a platform switch, casting `message` to `Message` or `Message` is preferable to adding generic `Record` property helpers just to read known adapter fields. -- Avoid broad casts that hide real uncertainty, especially `as any`, double casts through `unknown`, or casting external/untrusted data without validation. Use runtime validation when the data shape is genuinely unknown, user-controlled, persisted, or coming from an API contract we do not own. -- The above restrictions on `as` do not apply inside test files (e.g. `*.test.ts`, `*.spec.ts`, files under `__tests__/`, and other test fixtures/helpers). `as` casts are explicitly permitted in tests, where they are commonly needed for fixture construction, narrowing partial mocks, and exercising error paths. Production code conventions still apply to non-test code imported by tests. -- Avoid `!` non-null assertions; prefer explicit checks or flow-sensitive typing. -- Avoid mocks in tests; assert on results or check the database for side effects. -- Prefer clear names over comments. Only comment things not obvious in context. -- When the linter flags an unused variable, investigate the root cause — do not blindly prefix with `_`. -- Use existing dependencies before implementing custom solutions. Check `package.json` for what's available. - ## Timestamp Serialization - Drizzle/Postgres `timestamp({ withTimezone: true, mode: 'string' })` rows may surface timestamp text like `2026-04-29 01:16:12.945+00`, which strict ISO validators such as `z.string().datetime()` reject. - Before putting DB-backed timestamp strings into HTTP bodies, queue messages, or other strict JSON contracts, normalize them to UTC ISO with an existing domain serializer or `new Date(value).toISOString()`. Do not forward raw DB timestamp text across contract boundaries. - Keep strict validators unless the receiving contract intentionally accepts a broader format. Add regression fixtures using production-shape Postgres timestamp text when fixing or extending these paths. -## Workers & Durable Objects - -- Do not cache database clients, pools, or other transport-owning/request-context-bound SDK objects in module scope for Cloudflare Workers or Durable Objects. Workers reuse isolates across requests, Durable Object classes in the same Worker can share module memory across object instances, and stale module-scope I/O state can cause cross-context runtime failures. -- Create external database clients through approved per-use helpers such as `getWorkerDb(...)`; let Hyperdrive own pooling. Only cache pure data or context-independent values in module scope. -- Durable Object instance fields are valid for object-local state created from constructor inputs, such as SQLite/Drizzle wrappers over `state.storage`. -- If optimization appears to require module-scope client caching, stop and document why lifetime, transport ownership, binding freshness, and Cloudflare runtime behavior make it safe before implementing it. - ## Database Migrations Schema is in `packages/db/src/schema.ts`. Migrations live in `packages/db/src/migrations/` and are generated by `drizzle-kit` via `pnpm drizzle generate`. @@ -81,78 +56,14 @@ Schema is in `packages/db/src/schema.ts`. Migrations live in `packages/db/src/mi - **After a rebase that conflicts on migration files:** delete all migration files, snapshots, and journal entries that were added on the branch, then re-run `pnpm drizzle generate` to regenerate a clean migration from the current schema diff. Re-append any backfill SQL afterward. - Prefer a single migration per feature branch when the code has not yet been deployed to production. If multiple migrations accumulated during development, squash them by deleting all branch-local migrations and regenerating. -## GDPR & PII - -When adding PII (email, name, IP address, etc.) to the database — whether as a new table or a new column — you **must** also update the GDPR soft delete flow in `softDeleteUser` (`apps/web/src/lib/user/index.ts`) and add a corresponding test in `apps/web/src/lib/user/index.test.ts`. - ## Logging & Sensitive Data Never log tokens, credentials, auth headers, cookies, or webhook secrets. Use `redactSensitiveHeaders` from `@kilocode/worker-utils/redact-headers` when headers must be stored or logged. Do not enable `sendDefaultPii` or `attachRpcInput` in Sentry config. -## Plans +## Stripe Subscription Schedules -When writing implementation plans, always save them to the `.plans/` directory at the repo root. This is the designated location for all planning documents — do not place them elsewhere in the repo. +When using `subscriptionSchedules.create()` with `from_subscription`, Stripe prohibits setting `metadata` in the same call (it copies metadata from the subscription automatically). Set custom metadata (e.g., `origin` tags) in the subsequent `subscriptionSchedules.update()` call instead. ## Git Safety - - **Never** use `--force`, `--no-verify`, or any other flag that bypasses git hooks or safety checks without explicit user approval. - If a hook or check fails, diagnose the issue and either fix it or ask the user how to proceed — do not silently skip it. - -## Pull Requests - -### Titles - -- Format: `type(scope): ` (e.g., `feat(auth): add SSO login`) -- Common types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `ci`, `style`, `perf` -- Imperative mood, under 72 characters, no trailing period. - -### Descriptions - -Follow the PR template in `.github/pull_request_template.md`. Every description must include four sections in order: - -1. **`## Summary`** — What changed and why. Outcome-focused, call out architectural changes. -2. **`## Verification`** — Manual verification only. Do not list automated checks such as `pnpm typecheck`, `pnpm test`, `pnpm lint`, `pnpm validate`, CI, or formatting commands here. -3. **`## Visual Changes`** — Before/after screenshots, or `N/A`. -4. **`## Reviewer Notes`** — Risk areas, tricky logic, rollout notes, or `N/A`. - -Do not leave HTML comments from the template. Review all commits on the branch when writing the summary. - -### Workflow - -- Create PRs as **ready for review** by default. Only use `--draft` if explicitly requested. -- When assigning PRs or issues, resolve the GitHub username with `gh api user --jq '.login'`. Never guess usernames. - -## Specs - -Business-rule specs live in `.specs/`. Before making **any** changes to a domain covered by a spec — including bug fixes, new features, refactors, or reviews — you **must** first read the relevant spec. - -| Spec | Governs | -|---|---| -| `.specs/kiloclaw-billing.md` | KiloClaw billing, pricing, invoicing, usage metering, payment flows | -| `.specs/kiloclaw-billing-lifecycle.md` | KiloClaw billing lifecycle — credit-renewal orchestration safety | -| `.specs/kiloclaw-composio.md` | KiloClaw Composio credential provisioning, injection, and sharing | -| `.specs/kiloclaw-controller.md` | KiloClaw controller/machine lifecycle, bootstrap, Docker image | -| `.specs/kiloclaw-datamodel.md` | KiloClaw data model — instance/subscription tables, invariants | -| `.specs/model-experiments.md` | Model experiment routing, bucketing, lifecycle, prompt retention, and reporting rules | -| `.specs/security-agent.md` | Security Agent Auto Remediation and finding/SLA notification guarantees | -| `.specs/subscription-center.md` | Subscription Center ownership, states, and user-facing behavior | -| `.specs/team-enterprise-seat-billing.md` | Team and Enterprise seat billing, subscription management | -| `.specs/impact-affiliate-tracking.md` | Impact.com affiliate conversion tracking | -| `.specs/impact-referrals.md` | Impact.com Advocate referral programs for KiloClaw and Kilo Pass | - -## Markdown Tables - -Use compact, non-padded markdown tables to avoid merge conflicts. Prettier is configured to skip `*.md` files so it won't re-pad tables. - -**Rules:** -- Separator rows: use `|---|---|` (no spaces around `---`, colons allowed for alignment: `:---`, `---:`, `:---:`) -- Content rows: single space of padding only — `| value |`, not `| value |` - -**Enforcement:** -- `script/check-md-table-padding.ts` checks all tracked `*.md` files -- CI runs this check on every PR that touches markdown files -- To auto-fix: `bun run script/check-md-table-padding.ts --fix` - -## Stripe Subscription Schedules - -When using `subscriptionSchedules.create()` with `from_subscription`, Stripe prohibits setting `metadata` in the same call (it copies metadata from the subscription automatically). Set custom metadata (e.g., `origin` tags) in the subsequent `subscriptionSchedules.update()` call instead. diff --git a/apps/extension/AGENTS.md b/apps/extension/AGENTS.md index f0ce99d0de..e76febd285 100644 --- a/apps/extension/AGENTS.md +++ b/apps/extension/AGENTS.md @@ -83,7 +83,6 @@ Use a narrower subset only when the change is clearly isolated, and say what was ## Code Style -- Prefer `type` over `interface` in new code unless an existing file already uses interface-heavy browser API shapes. -- Avoid `as any`, broad casts, and non-null assertions in production code. Validate extension/browser API responses at the boundary. +- Prefer `type` over `interface` in new code, unless an existing file already uses interface-heavy browser API shapes — validate extension/browser API responses at the boundary rather than casting them. - Do not log tokens, auth headers, cookies, or gateway request bodies that may contain user content. - Keep helpers boring and local until behavior is shared by real callers. diff --git a/apps/mobile/AGENTS.md b/apps/mobile/AGENTS.md index 68581660c8..c43986d272 100644 --- a/apps/mobile/AGENTS.md +++ b/apps/mobile/AGENTS.md @@ -99,7 +99,6 @@ rm -rf "$TMPDIR/metro-cache" "$TMPDIR"/metro-file-map-* ## Code Style - Expo Router requires default exports in `src/app/` — this is the only place default exports are allowed. -- Prefer `type` over `interface`. - Import `View`, `Text`, `ScrollView`, `Pressable`, `TextInput` from `react-native` — NativeWind's Metro plugin rewrites these imports to add `className` support automatically. - Import `Image` from `@/components/ui/image` (a `styled` wrapper around `expo-image`). Lint enforces this. - For UI components (Button, Text with variants, Card, etc.), import from `@/components/ui/`. These are from react-native-reusables (shadcn/ui for RN). diff --git a/services/cloud-agent-next/AGENTS.md b/services/cloud-agent-next/AGENTS.md index 006caea752..562b016820 100644 --- a/services/cloud-agent-next/AGENTS.md +++ b/services/cloud-agent-next/AGENTS.md @@ -131,7 +131,7 @@ This pattern blocks API endpoints from running for external contributors who don ### Runtime Guidelines -- Durable Object calls should be retried using `withDORetry` in `src/utils/do-retry.ts` +- Durable Object calls should be retried using `withDORetry` (this service's logger-bound wrapper is `src/utils/do-retry.ts`). See the `durable-objects` skill (`references/repo-conventions.md`) for the shared `withDORetry` pattern used across services. - Execute commands inside a session context (use `session.exec(...)`, not `sandbox.exec(...)`) ### Testing Standards diff --git a/services/gastown/AGENTS.md b/services/gastown/AGENTS.md index 28af50abbe..52cd7b57de 100644 --- a/services/gastown/AGENTS.md +++ b/services/gastown/AGENTS.md @@ -1,94 +1,9 @@ # Conventions -## File naming +File naming, Durable Object structure, IO boundary validation, column naming, SQL +query conventions, and HTTP route/handler patterns for this service are documented +in the `durable-objects` skill (`references/repo-conventions.md`) — shared with +`services/wasteland` and `services/mcp-gateway`. Load that skill before making +structural changes here. -- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`. -- Modules that predominantly export a class should be named after that class, e.g. `AgentIdentity.do.ts` for `AgentIdentityDO`. - -## Durable Objects - -- Each DO module must export a `get{ClassName}Stub` helper function (e.g. `getRigDOStub`) that centralizes how that DO namespace creates instances. Callers should use this helper instead of accessing the namespace binding directly. -- **Sub-modules for large DOs**: When a Durable Object grows beyond a few hundred lines, extract domain logic into sub-modules under a `/` directory alongside the DO file. For example, `Town.do.ts` delegates to modules in `town/`: - - ``` - dos/ - Town.do.ts # Class definition, RPC methods, alarm loop - town/ - agents.ts # Agent CRUD, hook management - beads.ts # Bead CRUD, convoy progress - scheduling.ts # Agent dispatch, pending work scheduling - review-queue.ts # Review lifecycle, recovery - patrol.ts # Zombie detection, stale hook recovery - config.ts # Town configuration - rigs.ts # Rig registry - mail.ts # Inter-agent mail - container-dispatch.ts # Container start/stop/status - ``` - - Each sub-module exports plain functions (not classes) that accept `SqlStorage` and any other required context as arguments. The DO imports them with the `import * as X` pattern: - - ```ts - import * as beadOps from './town/beads'; - import * as agents from './town/agents'; - import * as scheduling from './town/scheduling'; - - // In the DO class: - beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId); - agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); - await scheduling.schedulePendingWork(this.schedulingCtx); - ``` - - This keeps the DO class thin (RPC surface + orchestration) while sub-modules own the business logic. The `import * as X` pattern makes call sites self-documenting — you can always tell which domain a function belongs to. - -## IO boundaries - -- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE event payloads, subprocess output) with Zod schemas. Return `unknown` from raw fetch/parse helpers and `.parse()` in the caller. -- Never use `as` to cast IO data. If the shape is known, define a Zod schema; if not, use `.passthrough()` or a catch-all schema. - -## Column naming - -- Never name a primary key column just `id`. Encode the entity in the column name, e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes grep-based navigation reliable. - -## 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. -- **Do not alias tables in SQL queries.** Always use the full table name and the qualified `${table.column}` interpolator. Aliases like `FROM beads b` combined with the qualified interpolator produce double-qualified names (`b.beads.bead_id`) that SQLite rejects. If a self-join requires disambiguation, use a raw string alias only for the second copy and reference its columns with `${table.columns.col}` (bare) prefixed manually. -- 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` 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'])`. - -## HTTP routes - -- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). Define all routes in the main worker entry point (e.g. `gastown.worker.ts`) so a human can scan one file and immediately see every route the app exposes. -- Move handler logic into `handlers/*.handler.ts` modules. Each module owns routes for a logical domain. Name the file after the domain, e.g. `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. -- Each handler function takes two arguments: - 1. The Hono `Context` object (typed as the app's `HonoContext` / `GastownEnv`). - 2. A plain object containing the route params parsed from the path, e.g. `{ rigId: string }` or `{ rigId: string; beadId: string }`. - - This keeps the handler's contract explicit and testable, while the route definition in the entry point is the single source of truth for path → param shape. - - ```ts - // gastown.worker.ts — route definition - app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); - - // handlers/rig-agents.handler.ts — handler implementation - export async function handleRegisterAgent(c: Context, params: { rigId: string }) { - // Zod validation lives in the handler, not as route middleware - const parsed = RegisterAgentBody.safeParse(await c.req.json()); - if (!parsed.success) { - return c.json( - { success: false, error: 'Invalid request body', issues: parsed.error.issues }, - 400 - ); - } - const rig = getRigDOStub(c.env, params.rigId); - const agent = await rig.registerAgent(parsed.data); - return c.json(resSuccess(agent), 201); - } - ``` +This service has no deltas from the shared conventions. diff --git a/services/kiloclaw-billing/AGENTS.md b/services/kiloclaw-billing/AGENTS.md index ed052f3e45..ec913995f4 100644 --- a/services/kiloclaw-billing/AGENTS.md +++ b/services/kiloclaw-billing/AGENTS.md @@ -4,6 +4,10 @@ `services/kiloclaw-billing` owns the KiloClaw billing lifecycle worker. +## Specs + +This service is governed by `.specs/kiloclaw-billing.md` and `.specs/kiloclaw-billing-lifecycle.md`. Read them (and load the `specs` skill) before changing billing behavior here. + ## Allowed Writes - This worker is allowed to write KiloClaw billing state in Postgres via Hyperdrive. diff --git a/services/kiloclaw/AGENTS.md b/services/kiloclaw/AGENTS.md index 8d71d814bd..4cd7928b1b 100644 --- a/services/kiloclaw/AGENTS.md +++ b/services/kiloclaw/AGENTS.md @@ -4,6 +4,10 @@ KiloClaw is a Cloudflare Worker that runs per-user OpenClaw AI assistant instances on Fly.io Machines. The CF Worker handles auth, config management, and proxies HTTP/WebSocket traffic to each user's Fly Machine via Fly Proxy. +## Specs + +This service is governed by `.specs/kiloclaw-controller.md`, `.specs/kiloclaw-datamodel.md`, and `.specs/kiloclaw-composio.md`. Read the relevant spec (and load the `specs` skill) before changing controller/machine lifecycle, the instance/subscription data model, or Composio credential handling. + ## Hard Invariants These are non-negotiable. Do not reintroduce shared/fallback paths. @@ -260,13 +264,6 @@ KiloClaw is transitioning from one-instance-per-user to N-instances-per-owner (p - **KiloClawRegistry DO** — SQLite-backed DO that indexes instances per owner (`user:{userId}` or `org:{orgId}`) for routing. Fresh-provision admission reservations are being added as a separate non-routable state surface; never expose pending reservations from normal route-resolution methods. - **Org instances** — `organization_id` links instances to org contexts. Registry entries for an organization may contain several assigned users; any current one-active-instance admission rule must be qualified by assigned user rather than treating the whole organization as one slot. -## Code Style - -- See `/.kilocode/rules/coding-style.md` for project-wide rules -- Prefer `type` over `interface` -- Avoid `as` and `!` -- use `satisfies` or flow-sensitive typing -- No mocks where avoidable -- assert on results - ## Gateway Configuration OpenClaw configuration is built at machine startup by the controller's bootstrap module (`controller/src/bootstrap.ts`): diff --git a/services/mcp-gateway/AGENTS.md b/services/mcp-gateway/AGENTS.md index 59373d9e4a..2709147865 100644 --- a/services/mcp-gateway/AGENTS.md +++ b/services/mcp-gateway/AGENTS.md @@ -13,46 +13,23 @@ The Worker MUST NOT implement first-level OAuth authorization, token, registrati provider callback, JWKS, user-info, config CRUD, assignment CRUD, or app management routes in v1. -## File naming +## Specs -- Add a suffix matching the module type, for example `mcp-gateway.worker.ts`, - `MCPGatewayInstance.do.ts`, `connect.handler.ts`, `routes.schema.ts`, and - `instances.table.ts`. -- Modules that predominantly export a class should be named after that class. -- Keep pure helpers in `lib/` and keep route handlers in `handlers/`. +This service is governed by `.specs/mcp-gateway-auth.md` (the authoritative MCP +Gateway v1 spec — protocol surface, ownership, OAuth lifecycle, provider grants, +runtime auth). Read it (and load the `specs` skill) before changing gateway +behavior. -## HTTP routes +## HTTP routes (deltas) -- Define every exposed Hono route in `src/mcp-gateway.worker.ts` so the public - surface is visible in one file. -- Do not mount Hono sub-apps. -- Move route logic into `handlers/*.handler.ts` modules. -- Each handler takes the Hono context and a plain parsed params object. The route - declaration remains the source of truth for path-to-param shape. - Runtime routes are scoped connect resources only: - `/mcp-connect/user/{user_id}/{config_id}/{route_key}` - `/mcp-connect/org/{org_id}/{config_id}/{route_key}` - Protected-resource metadata is the only other public gateway surface owned by this Worker. -## IO boundaries - -- Validate every IO boundary with Zod: MCP messages, route params, query params, - behavior-affecting headers, upstream responses, JSON parse results, SSE payloads, - subprocess output, and persisted session records. -- Raw parse and fetch helpers return `unknown`; callers parse with the relevant - Zod schema. -- Do not use `as` casts for IO shapes. Use schemas, `.passthrough()`, or explicit - catch-all schemas when the shape is intentionally broad. -- The gateway is stricter than Gastown at MCP protocol, header, query, upstream - response, and persisted-session boundaries. ## Hyperdrive and Postgres - -- Use `getWorkerDb(env.HYPERDRIVE.connectionString, { statement_timeout: ... })` - per request or per Durable Object use. -- Never cache pg pools, Drizzle clients, transaction objects, request-scoped state, - or other transport-owning SDK objects in module scope. - Postgres remains the shared system of record for config, route, assignment, identity, instance, and grant state. - The Worker must re-check current Postgres state on every authenticated runtime @@ -64,17 +41,10 @@ routes in v1. deterministic key is `{owner_scope}:{owner_id}:{config_id}:{user_id}`. - Do not introduce a global gateway Durable Object or a config-level DO that serializes all users of a shared org config. -- Every DO module exports a `get{ClassName}Stub` helper, and callers use that - helper instead of accessing the namespace binding directly. -- Keep the DO class thin: RPC surface, alarms, and orchestration only. Move large - domain logic into plain-function submodules under a sibling directory when the - class grows beyond a few hundred lines. - DO cache state is never authoritative for config, assignment, identity, route, or grant eligibility. - If DO SQLite is used, use tracked schema migrations from day one instead of ad hoc `CREATE TABLE IF NOT EXISTS` drift. -- Use table interpolator objects and Zod row schemas for DO SQLite queries instead - of raw table or column strings and unsafe casts. ## Security and streaming diff --git a/services/wasteland/AGENTS.md b/services/wasteland/AGENTS.md index f06633c061..bfa34405cc 100644 --- a/services/wasteland/AGENTS.md +++ b/services/wasteland/AGENTS.md @@ -1,93 +1,9 @@ # Conventions -## File naming +File naming, Durable Object structure, IO boundary validation, column naming, SQL +query conventions, and HTTP route/handler patterns for this service are documented +in the `durable-objects` skill (`references/repo-conventions.md`) — shared with +`services/gastown` and `services/mcp-gateway`. Load that skill before making +structural changes here. -- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`. -- Modules that predominantly export a class should be named after that class, e.g. `AgentIdentity.do.ts` for `AgentIdentityDO`. - -## Durable Objects - -- Each DO module must export a `get{ClassName}Stub` helper function (e.g. `getRigDOStub`) that centralizes how that DO namespace creates instances. Callers should use this helper instead of accessing the namespace binding directly. -- **Sub-modules for large DOs**: When a Durable Object grows beyond a few hundred lines, extract domain logic into sub-modules under a `/` directory alongside the DO file. For example, `Town.do.ts` delegates to modules in `town/`: - - ``` - dos/ - Town.do.ts # Class definition, RPC methods, alarm loop - town/ - agents.ts # Agent CRUD, hook management - beads.ts # Bead CRUD, convoy progress - scheduling.ts # Agent dispatch, pending work scheduling - review-queue.ts # Review lifecycle, recovery - patrol.ts # Zombie detection, stale hook recovery - config.ts # Town configuration - rigs.ts # Rig registry - mail.ts # Inter-agent mail - container-dispatch.ts # Container start/stop/status - ``` - - Each sub-module exports plain functions (not classes) that accept `SqlStorage` and any other required context as arguments. The DO imports them with the `import * as X` pattern: - - ```ts - import * as beadOps from './town/beads'; - import * as agents from './town/agents'; - import * as scheduling from './town/scheduling'; - - // In the DO class: - beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId); - agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); - await scheduling.schedulePendingWork(this.schedulingCtx); - ``` - - This keeps the DO class thin (RPC surface + orchestration) while sub-modules own the business logic. The `import * as X` pattern makes call sites self-documenting — you can always tell which domain a function belongs to. - -## IO boundaries - -- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE event payloads, subprocess output) with Zod schemas. Return `unknown` from raw fetch/parse helpers and `.parse()` in the caller. -- Never use `as` to cast IO data. If the shape is known, define a Zod schema; if not, use `.passthrough()` or a catch-all schema. - -## Column naming - -- Never name a primary key column just `id`. Encode the entity in the column name, e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes grep-based navigation reliable. - -## 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` 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'])`. - -## HTTP routes - -- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). Define all routes in the main worker entry point (e.g. `gastown.worker.ts`) so a human can scan one file and immediately see every route the app exposes. -- Move handler logic into `handlers/*.handler.ts` modules. Each module owns routes for a logical domain. Name the file after the domain, e.g. `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. -- Each handler function takes two arguments: - 1. The Hono `Context` object (typed as the app's `HonoContext` / `GastownEnv`). - 2. A plain object containing the route params parsed from the path, e.g. `{ rigId: string }` or `{ rigId: string; beadId: string }`. - - This keeps the handler's contract explicit and testable, while the route definition in the entry point is the single source of truth for path → param shape. - - ```ts - // gastown.worker.ts — route definition - app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); - - // handlers/rig-agents.handler.ts — handler implementation - export async function handleRegisterAgent(c: Context, params: { rigId: string }) { - // Zod validation lives in the handler, not as route middleware - const parsed = RegisterAgentBody.safeParse(await c.req.json()); - if (!parsed.success) { - return c.json( - { success: false, error: 'Invalid request body', issues: parsed.error.issues }, - 400 - ); - } - const rig = getRigDOStub(c.env, params.rigId); - const agent = await rig.registerAgent(parsed.data); - return c.json(resSuccess(agent), 201); - } - ``` +This service has no deltas from the shared conventions. From 89d2d2b760ce8878b3f1e18422ac79599bda52a5 Mon Sep 17 00:00:00 2001 From: syn Date: Wed, 1 Jul 2026 16:27:18 -0500 Subject: [PATCH 2/5] docs(agents): scope DO conventions correctly; keep gastown/wasteland self-contained Follow-up to the consolidation: the shared durable-objects repo-conventions doc had over-generalized Gastown/Wasteland-specific patterns. - Trim repo-conventions.md to genuinely shared Worker/DO conventions (DO call retries, DO stub helper, sub-modules, IO boundaries, DB clients); drop the SQL/table and HTTP-route sections that are not universal. - Revert services/gastown/AGENTS.md and services/wasteland/AGENTS.md to their original main content (they keep their own raw-query() SQL conventions rather than pointing at a shared doc). - mcp-gateway: HTTP routes section lists only its own route surface. - Update durable-objects SKILL.md description to match repo-conventions.md. --- .agents/skills/durable-objects/SKILL.md | 6 +- .../references/repo-conventions.md | 113 +----------------- services/gastown/AGENTS.md | 97 ++++++++++++++- services/mcp-gateway/AGENTS.md | 2 +- services/wasteland/AGENTS.md | 96 ++++++++++++++- 5 files changed, 189 insertions(+), 125 deletions(-) diff --git a/.agents/skills/durable-objects/SKILL.md b/.agents/skills/durable-objects/SKILL.md index 6b8ae67af3..4d8e60cd12 100644 --- a/.agents/skills/durable-objects/SKILL.md +++ b/.agents/skills/durable-objects/SKILL.md @@ -34,9 +34,9 @@ Fetch the relevant doc page when implementing features. - `./references/rules.md` - Core rules, storage, concurrency, RPC, alarms - `./references/testing.md` - Vitest setup, unit/integration tests, alarm testing - `./references/workers.md` - Workers handlers, types, wrangler config, observability -- `./references/repo-conventions.md` - This repo's file naming, DO stub helpers, - sub-module splitting, IO boundary/SQL/HTTP route conventions shared by - `services/gastown`, `services/wasteland`, and `services/mcp-gateway` +- `./references/repo-conventions.md` - This repo's Worker/DO conventions: DO call + retries, DO stub helpers, sub-module splitting, IO boundaries, and DB-client + lifecycle Search: `blockConcurrencyWhile`, `idFromName`, `getByName`, `setAlarm`, `sql.exec` diff --git a/.agents/skills/durable-objects/references/repo-conventions.md b/.agents/skills/durable-objects/references/repo-conventions.md index c62eeb732d..b1625f1586 100644 --- a/.agents/skills/durable-objects/references/repo-conventions.md +++ b/.agents/skills/durable-objects/references/repo-conventions.md @@ -1,13 +1,7 @@ # Repo Conventions — Worker/DO Services in This Monorepo -These conventions apply to Cloudflare Worker services in `services/` that use -Durable Objects. The file naming/sub-module/IO-boundary/SQL/HTTP-route sections -below are specific to the Hono + Zod service family (currently `services/gastown`, -`services/wasteland`, `services/mcp-gateway`); the DO call retry section applies -to every service in the repo that calls a DO stub. They are repo-specific patterns -layered on top of the general Durable Objects rules in `references/rules.md`; each -service's own `AGENTS.md` documents only its scope and genuine deltas from this -shared contract. +Always bias towards following established patterns in existing services. These +are merely guidelines. ## DO call retries @@ -31,17 +25,7 @@ bound in, e.g. `services/cloud-agent-next/src/utils/do-retry.ts` and logger-binding adapters over the shared `withDORetry`, not reimplementations. Prefer this pattern (a local `withDORetry` re-export bound to the service logger) over calling the base helper directly with no logger, and over copying the retry -loop itself. Used today by `cloud-agent-next`, `webhook-agent-ingest`, -`session-ingest`, `kilo-chat`, `kiloclaw`, and `code-review-infra`. - -## File naming - -- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`, - `connect.handler.ts`, `routes.schema.ts`, `instances.table.ts`. -- Modules that predominantly export a class should be named after that class, e.g. - `AgentIdentity.do.ts` for `AgentIdentityDO`, `MCPGatewayInstance.do.ts` for - `MCPGatewayInstance`. -- Keep pure helpers in `lib/` and route handlers in `handlers/`. +loop itself. ## DO stub helper @@ -61,13 +45,6 @@ dos/ town/ agents.ts # Agent CRUD, hook management beads.ts # Bead CRUD, convoy progress - scheduling.ts # Agent dispatch, pending work scheduling - review-queue.ts # Review lifecycle, recovery - patrol.ts # Zombie detection, stale hook recovery - config.ts # Town configuration - rigs.ts # Rig registry - mail.ts # Inter-agent mail - container-dispatch.ts # Container start/stop/status ``` Each sub-module exports plain functions (not classes) that accept `SqlStorage` and @@ -91,94 +68,12 @@ you can always tell which domain a function belongs to. ## IO boundaries -- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE +- Validate data at IO boundaries (HTTP responses, JSON.parse results, SSE event payloads, subprocess output, upstream responses, persisted session records) with Zod schemas. Return `unknown` from raw fetch/parse helpers and `.parse()` in the caller. - Never use `as` to cast IO data. If the shape is known, define a Zod schema; if not, use `.passthrough()` or a catch-all schema. -- Some services are stricter than others at these boundaries (e.g. `mcp-gateway` - validates MCP protocol messages, headers, and query params in addition to the - baseline above) — check the service's own `AGENTS.md` for boundary-specific - additions. - -## Column naming - -Never name a primary key column just `id`. Encode the entity in the column name, -e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes -grep-based navigation reliable. - -## 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: multi-line strings, 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. 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. -- **Do not alias tables in SQL queries.** Always use the full table name with the - qualified `${table.column}` interpolator. Aliases like `FROM beads b` combined - with the qualified interpolator produce double-qualified names - (`b.beads.bead_id`) that SQLite rejects. If a self-join requires - disambiguation, use a raw string alias only for the second copy and reference - its columns with `${table.columns.col}` (bare) prefixed manually. -- 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` 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)`. -- When a column has a SQL `CHECK` constraint restricting 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'])`. - -## HTTP routes - -- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). - Define all routes in the main worker entry point (e.g. `gastown.worker.ts`, - `mcp-gateway.worker.ts`) so a human can scan one file and see every exposed - route. -- Move handler logic into `handlers/*.handler.ts` modules. Each module owns - routes for a logical domain. Name the file after the domain, e.g. - `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. -- Each handler function takes two arguments: - 1. The Hono `Context` object (typed as the app's env type). - 2. A plain object containing the route params parsed from the path, e.g. - `{ rigId: string }` or `{ rigId: string; beadId: string }`. - - This keeps the handler's contract explicit and testable, while the route - definition in the entry point is the single source of truth for - path → param shape. - -```ts -// gastown.worker.ts — route definition -app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); - -// handlers/rig-agents.handler.ts — handler implementation -export async function handleRegisterAgent(c: Context, params: { rigId: string }) { - // Zod validation lives in the handler, not as route middleware - const parsed = RegisterAgentBody.safeParse(await c.req.json()); - if (!parsed.success) { - return c.json( - { success: false, error: 'Invalid request body', issues: parsed.error.issues }, - 400 - ); - } - const rig = getRigDOStub(c.env, params.rigId); - const agent = await rig.registerAgent(parsed.data); - return c.json(resSuccess(agent), 201); -} -``` ## DB clients diff --git a/services/gastown/AGENTS.md b/services/gastown/AGENTS.md index 52cd7b57de..28af50abbe 100644 --- a/services/gastown/AGENTS.md +++ b/services/gastown/AGENTS.md @@ -1,9 +1,94 @@ # Conventions -File naming, Durable Object structure, IO boundary validation, column naming, SQL -query conventions, and HTTP route/handler patterns for this service are documented -in the `durable-objects` skill (`references/repo-conventions.md`) — shared with -`services/wasteland` and `services/mcp-gateway`. Load that skill before making -structural changes here. +## File naming -This service has no deltas from the shared conventions. +- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`. +- Modules that predominantly export a class should be named after that class, e.g. `AgentIdentity.do.ts` for `AgentIdentityDO`. + +## Durable Objects + +- Each DO module must export a `get{ClassName}Stub` helper function (e.g. `getRigDOStub`) that centralizes how that DO namespace creates instances. Callers should use this helper instead of accessing the namespace binding directly. +- **Sub-modules for large DOs**: When a Durable Object grows beyond a few hundred lines, extract domain logic into sub-modules under a `/` directory alongside the DO file. For example, `Town.do.ts` delegates to modules in `town/`: + + ``` + dos/ + Town.do.ts # Class definition, RPC methods, alarm loop + town/ + agents.ts # Agent CRUD, hook management + beads.ts # Bead CRUD, convoy progress + scheduling.ts # Agent dispatch, pending work scheduling + review-queue.ts # Review lifecycle, recovery + patrol.ts # Zombie detection, stale hook recovery + config.ts # Town configuration + rigs.ts # Rig registry + mail.ts # Inter-agent mail + container-dispatch.ts # Container start/stop/status + ``` + + Each sub-module exports plain functions (not classes) that accept `SqlStorage` and any other required context as arguments. The DO imports them with the `import * as X` pattern: + + ```ts + import * as beadOps from './town/beads'; + import * as agents from './town/agents'; + import * as scheduling from './town/scheduling'; + + // In the DO class: + beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId); + agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); + await scheduling.schedulePendingWork(this.schedulingCtx); + ``` + + This keeps the DO class thin (RPC surface + orchestration) while sub-modules own the business logic. The `import * as X` pattern makes call sites self-documenting — you can always tell which domain a function belongs to. + +## IO boundaries + +- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE event payloads, subprocess output) with Zod schemas. Return `unknown` from raw fetch/parse helpers and `.parse()` in the caller. +- Never use `as` to cast IO data. If the shape is known, define a Zod schema; if not, use `.passthrough()` or a catch-all schema. + +## Column naming + +- Never name a primary key column just `id`. Encode the entity in the column name, e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes grep-based navigation reliable. + +## 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. +- **Do not alias tables in SQL queries.** Always use the full table name and the qualified `${table.column}` interpolator. Aliases like `FROM beads b` combined with the qualified interpolator produce double-qualified names (`b.beads.bead_id`) that SQLite rejects. If a self-join requires disambiguation, use a raw string alias only for the second copy and reference its columns with `${table.columns.col}` (bare) prefixed manually. +- 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` 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'])`. + +## HTTP routes + +- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). Define all routes in the main worker entry point (e.g. `gastown.worker.ts`) so a human can scan one file and immediately see every route the app exposes. +- Move handler logic into `handlers/*.handler.ts` modules. Each module owns routes for a logical domain. Name the file after the domain, e.g. `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. +- Each handler function takes two arguments: + 1. The Hono `Context` object (typed as the app's `HonoContext` / `GastownEnv`). + 2. A plain object containing the route params parsed from the path, e.g. `{ rigId: string }` or `{ rigId: string; beadId: string }`. + + This keeps the handler's contract explicit and testable, while the route definition in the entry point is the single source of truth for path → param shape. + + ```ts + // gastown.worker.ts — route definition + app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); + + // handlers/rig-agents.handler.ts — handler implementation + export async function handleRegisterAgent(c: Context, params: { rigId: string }) { + // Zod validation lives in the handler, not as route middleware + const parsed = RegisterAgentBody.safeParse(await c.req.json()); + if (!parsed.success) { + return c.json( + { success: false, error: 'Invalid request body', issues: parsed.error.issues }, + 400 + ); + } + const rig = getRigDOStub(c.env, params.rigId); + const agent = await rig.registerAgent(parsed.data); + return c.json(resSuccess(agent), 201); + } + ``` diff --git a/services/mcp-gateway/AGENTS.md b/services/mcp-gateway/AGENTS.md index 2709147865..98a60cf88e 100644 --- a/services/mcp-gateway/AGENTS.md +++ b/services/mcp-gateway/AGENTS.md @@ -20,7 +20,7 @@ Gateway v1 spec — protocol surface, ownership, OAuth lifecycle, provider grant runtime auth). Read it (and load the `specs` skill) before changing gateway behavior. -## HTTP routes (deltas) +## HTTP routes - Runtime routes are scoped connect resources only: - `/mcp-connect/user/{user_id}/{config_id}/{route_key}` diff --git a/services/wasteland/AGENTS.md b/services/wasteland/AGENTS.md index bfa34405cc..f06633c061 100644 --- a/services/wasteland/AGENTS.md +++ b/services/wasteland/AGENTS.md @@ -1,9 +1,93 @@ # Conventions -File naming, Durable Object structure, IO boundary validation, column naming, SQL -query conventions, and HTTP route/handler patterns for this service are documented -in the `durable-objects` skill (`references/repo-conventions.md`) — shared with -`services/gastown` and `services/mcp-gateway`. Load that skill before making -structural changes here. +## File naming -This service has no deltas from the shared conventions. +- Add a suffix matching the module type, e.g. `agents.table.ts`, `gastown.worker.ts`. +- Modules that predominantly export a class should be named after that class, e.g. `AgentIdentity.do.ts` for `AgentIdentityDO`. + +## Durable Objects + +- Each DO module must export a `get{ClassName}Stub` helper function (e.g. `getRigDOStub`) that centralizes how that DO namespace creates instances. Callers should use this helper instead of accessing the namespace binding directly. +- **Sub-modules for large DOs**: When a Durable Object grows beyond a few hundred lines, extract domain logic into sub-modules under a `/` directory alongside the DO file. For example, `Town.do.ts` delegates to modules in `town/`: + + ``` + dos/ + Town.do.ts # Class definition, RPC methods, alarm loop + town/ + agents.ts # Agent CRUD, hook management + beads.ts # Bead CRUD, convoy progress + scheduling.ts # Agent dispatch, pending work scheduling + review-queue.ts # Review lifecycle, recovery + patrol.ts # Zombie detection, stale hook recovery + config.ts # Town configuration + rigs.ts # Rig registry + mail.ts # Inter-agent mail + container-dispatch.ts # Container start/stop/status + ``` + + Each sub-module exports plain functions (not classes) that accept `SqlStorage` and any other required context as arguments. The DO imports them with the `import * as X` pattern: + + ```ts + import * as beadOps from './town/beads'; + import * as agents from './town/agents'; + import * as scheduling from './town/scheduling'; + + // In the DO class: + beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId); + agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); + await scheduling.schedulePendingWork(this.schedulingCtx); + ``` + + This keeps the DO class thin (RPC surface + orchestration) while sub-modules own the business logic. The `import * as X` pattern makes call sites self-documenting — you can always tell which domain a function belongs to. + +## IO boundaries + +- Always validate data at IO boundaries (HTTP responses, JSON.parse results, SSE event payloads, subprocess output) with Zod schemas. Return `unknown` from raw fetch/parse helpers and `.parse()` in the caller. +- Never use `as` to cast IO data. If the shape is known, define a Zod schema; if not, use `.passthrough()` or a catch-all schema. + +## Column naming + +- Never name a primary key column just `id`. Encode the entity in the column name, e.g. `bead_id`, `bead_event_id`, `rig_id`. This avoids ambiguity in joins and makes grep-based navigation reliable. + +## 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` 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'])`. + +## HTTP routes + +- **Do not use Hono sub-app mounting** (e.g. `app.route('/prefix', subApp)`). Define all routes in the main worker entry point (e.g. `gastown.worker.ts`) so a human can scan one file and immediately see every route the app exposes. +- Move handler logic into `handlers/*.handler.ts` modules. Each module owns routes for a logical domain. Name the file after the domain, e.g. `handlers/rig-agents.handler.ts` for `/api/rigs/:rigId/agents/*` routes. +- Each handler function takes two arguments: + 1. The Hono `Context` object (typed as the app's `HonoContext` / `GastownEnv`). + 2. A plain object containing the route params parsed from the path, e.g. `{ rigId: string }` or `{ rigId: string; beadId: string }`. + + This keeps the handler's contract explicit and testable, while the route definition in the entry point is the single source of truth for path → param shape. + + ```ts + // gastown.worker.ts — route definition + app.post('/api/rigs/:rigId/agents', c => handleRegisterAgent(c, c.req.param())); + + // handlers/rig-agents.handler.ts — handler implementation + export async function handleRegisterAgent(c: Context, params: { rigId: string }) { + // Zod validation lives in the handler, not as route middleware + const parsed = RegisterAgentBody.safeParse(await c.req.json()); + if (!parsed.success) { + return c.json( + { success: false, error: 'Invalid request body', issues: parsed.error.issues }, + 400 + ); + } + const rig = getRigDOStub(c.env, params.rigId); + const agent = await rig.registerAgent(parsed.data); + return c.json(resSuccess(agent), 201); + } + ``` From 50325bf28c5b960e5fce82d614dda8b4a8da2803 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 2 Jul 2026 08:12:20 -0600 Subject: [PATCH 3/5] Update .agents/skills/specs/SKILL.md Co-authored-by: Jean du Plessis --- .agents/skills/specs/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/specs/SKILL.md b/.agents/skills/specs/SKILL.md index d402261588..113f694dfa 100644 --- a/.agents/skills/specs/SKILL.md +++ b/.agents/skills/specs/SKILL.md @@ -1,6 +1,6 @@ --- name: specs -description: Business-rule specs in .specs/ govern KiloClaw billing/lifecycle/controller/data model/Composio, MCP Gateway auth, model experiments, Security Agent, subscription center, team/enterprise seat billing, Impact affiliate/referrals, Kilo Pass, organization SSO, Stripe early fraud warnings, and coding plans. Load before making ANY change (bug fix, feature, refactor, or review) to a domain covered by one of these specs, to read the authoritative rules first. +description: Business-rule specs for KiloClaw billing/lifecycle/controller/data model/Composio, MCP Gateway auth, model experiments, Security Agent, subscription center, team/enterprise seat billing, Impact affiliate/referrals, Kilo Pass, organization SSO, Stripe early fraud warnings, and coding plans. Load when you need context about the business requirements that guided the implementation. --- # Business-Rule Specs From 27fee0845c31abaa0a250cf2cf2f302647035ee7 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 2 Jul 2026 08:12:29 -0600 Subject: [PATCH 4/5] Update .agents/skills/specs/SKILL.md Co-authored-by: Jean du Plessis --- .agents/skills/specs/SKILL.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.agents/skills/specs/SKILL.md b/.agents/skills/specs/SKILL.md index 113f694dfa..e4217c84e2 100644 --- a/.agents/skills/specs/SKILL.md +++ b/.agents/skills/specs/SKILL.md @@ -5,11 +5,9 @@ description: Business-rule specs for KiloClaw billing/lifecycle/controller/data # Business-Rule Specs -Specs in `.specs/` are the authoritative source of truth for the business rules and -invariants of the domains they cover. Before making **any** change to a covered -domain — including bug fixes, new features, refactors, or reviews — you **must** -first read the relevant spec. Implementation mechanics (route names, columns, retry -cadence) belong in plans and code; the spec owns the rules and invariants. +Specs in `.specs/` are context as to the original business intention, rules and +invariants of the domains they cover. Consult them for context and flag inconsistencies +to the user if instructions or changes will cause deviations from the original intent. ## Index From 83ad1df59f00722acb9de0434ebb8576248dd8e4 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 2 Jul 2026 08:12:40 -0600 Subject: [PATCH 5/5] Update services/kiloclaw-billing/AGENTS.md Co-authored-by: Jean du Plessis --- services/kiloclaw-billing/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/kiloclaw-billing/AGENTS.md b/services/kiloclaw-billing/AGENTS.md index ec913995f4..6149cc6791 100644 --- a/services/kiloclaw-billing/AGENTS.md +++ b/services/kiloclaw-billing/AGENTS.md @@ -6,7 +6,7 @@ ## Specs -This service is governed by `.specs/kiloclaw-billing.md` and `.specs/kiloclaw-billing-lifecycle.md`. Read them (and load the `specs` skill) before changing billing behavior here. +Load the `specs` skill to access the business rules and invariants of the KiloClaw billing service when you need context about the business requirements that guided the implementation. ## Allowed Writes