diff --git a/.agents/skills/chat-sdk/SKILL.md b/.agents/skills/chat-sdk/SKILL.md index 22ab7e5893..6d6db7050a 100644 --- a/.agents/skills/chat-sdk/SKILL.md +++ b/.agents/skills/chat-sdk/SKILL.md @@ -75,16 +75,16 @@ bot.onSubscribedMessage(async (thread, message) => { ## Event handlers -| Handler | Trigger | -| -------------------------- | ------------------------------------------------- | -| `onNewMention` | Bot @-mentioned in unsubscribed thread | -| `onSubscribedMessage` | Any message in subscribed thread | -| `onNewMessage(regex)` | Messages matching pattern in unsubscribed threads | -| `onSlashCommand("/cmd")` | Slash command invocations | -| `onReaction(emojis)` | Emoji reactions added/removed | -| `onAction(actionId)` | Button clicks and dropdown selections | -| `onAssistantThreadStarted` | Slack Assistants API thread opened | -| `onAppHomeOpened` | Slack App Home tab opened | +| Handler | Trigger | +|---|---| +| `onNewMention` | Bot @-mentioned in unsubscribed thread | +| `onSubscribedMessage` | Any message in subscribed thread | +| `onNewMessage(regex)` | Messages matching pattern in unsubscribed threads | +| `onSlashCommand("/cmd")` | Slash command invocations | +| `onReaction(emojis)` | Emoji reactions added/removed | +| `onAction(actionId)` | Button clicks and dropdown selections | +| `onAssistantThreadStarted` | Slack Assistants API thread opened | +| `onAppHomeOpened` | Slack App Home tab opened | ## Streaming @@ -122,18 +122,18 @@ await thread.post( ## Packages -| Package | Purpose | -| ----------------------------- | ----------------------------- | -| `chat` | Core SDK | -| `@chat-adapter/slack` | Slack | -| `@chat-adapter/teams` | Microsoft Teams | -| `@chat-adapter/gchat` | Google Chat | -| `@chat-adapter/discord` | Discord | -| `@chat-adapter/github` | GitHub Issues | -| `@chat-adapter/linear` | Linear Issues | -| `@chat-adapter/state-redis` | Redis state (production) | -| `@chat-adapter/state-ioredis` | ioredis state (alternative) | -| `@chat-adapter/state-memory` | In-memory state (development) | +| Package | Purpose | +|---|---| +| `chat` | Core SDK | +| `@chat-adapter/slack` | Slack | +| `@chat-adapter/teams` | Microsoft Teams | +| `@chat-adapter/gchat` | Google Chat | +| `@chat-adapter/discord` | Discord | +| `@chat-adapter/github` | GitHub Issues | +| `@chat-adapter/linear` | Linear Issues | +| `@chat-adapter/state-redis` | Redis state (production) | +| `@chat-adapter/state-ioredis` | ioredis state (alternative) | +| `@chat-adapter/state-memory` | In-memory state (development) | ## Changesets (Release Flow) diff --git a/.agents/skills/durable-objects/SKILL.md b/.agents/skills/durable-objects/SKILL.md index 44d69d5c47..ad6ae1a438 100644 --- a/.agents/skills/durable-objects/SKILL.md +++ b/.agents/skills/durable-objects/SKILL.md @@ -11,12 +11,12 @@ Build stateful, coordinated applications on Cloudflare's edge using Durable Obje Your knowledge of Durable Objects APIs and configuration may be outdated. **Prefer retrieval over pre-training** for any Durable Objects task. -| Resource | URL | -| -------------- | ----------------------------------------------------------------- | -| Docs | https://developers.cloudflare.com/durable-objects/ | -| API Reference | https://developers.cloudflare.com/durable-objects/api/ | +| Resource | URL | +|---|---| +| Docs | https://developers.cloudflare.com/durable-objects/ | +| API Reference | https://developers.cloudflare.com/durable-objects/api/ | | Best Practices | https://developers.cloudflare.com/durable-objects/best-practices/ | -| Examples | https://developers.cloudflare.com/durable-objects/examples/ | +| Examples | https://developers.cloudflare.com/durable-objects/examples/ | Fetch the relevant doc page when implementing features. @@ -41,13 +41,13 @@ Search: `blockConcurrencyWhile`, `idFromName`, `getByName`, `setAlarm`, `sql.exe ### Use Durable Objects For -| Need | Example | -| ------------------------- | ------------------------------------------------- | -| Coordination | Chat rooms, multiplayer games, collaborative docs | -| Strong consistency | Inventory, booking systems, turn-based games | -| Per-entity storage | Multi-tenant SaaS, per-user data | -| Persistent connections | WebSockets, real-time notifications | -| Scheduled work per entity | Subscription renewals, game timeouts | +| Need | Example | +|---|---| +| Coordination | Chat rooms, multiplayer games, collaborative docs | +| Strong consistency | Inventory, booking systems, turn-based games | +| Per-entity storage | Multi-tenant SaaS, per-user data | +| Persistent connections | WebSockets, real-time notifications | +| Scheduled work per entity | Subscription renewals, game timeouts | ### Do NOT Use For diff --git a/.agents/skills/durable-objects/references/rules.md b/.agents/skills/durable-objects/references/rules.md index c5ca75f30a..776266f611 100644 --- a/.agents/skills/durable-objects/references/rules.md +++ b/.agents/skills/durable-objects/references/rules.md @@ -112,11 +112,11 @@ For production apps, consider [`durable-utils`](https://github.com/lambrospetrou ### State Types -| Type | Speed | Persistence | Use Case | -| ----------------- | -------- | ----------------- | --------------------------- | -| Class properties | Fastest | Lost on eviction | Caching, active connections | -| SQLite storage | Fast | Durable | Primary data | -| External (R2, D1) | Variable | Durable, cross-DO | Large files, shared data | +| Type | Speed | Persistence | Use Case | +|---|---|---|---| +| Class properties | Fastest | Lost on eviction | Caching, active connections | +| SQLite storage | Fast | Durable | Primary data | +| External (R2, D1) | Variable | Durable, cross-DO | Large files, shared data | **Rule**: Always persist critical state to SQLite first, then update in-memory cache. diff --git a/.agents/skills/kilo-design/NOTICE.md b/.agents/skills/kilo-design/NOTICE.md index 375a74c8c1..05fe0c03c7 100644 --- a/.agents/skills/kilo-design/NOTICE.md +++ b/.agents/skills/kilo-design/NOTICE.md @@ -14,15 +14,15 @@ This skill contains content adapted from the Impeccable project. The following files under `reference/` are derivative works based on corresponding files in `pbakaus/impeccable` at `source/skills/impeccable/reference/`: -| Local file | Upstream source | -| --------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `reference/typography.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/typography.md | +| Local file | Upstream source | +|---|---| +| `reference/typography.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/typography.md | | `reference/color-and-contrast.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/color-and-contrast.md | -| `reference/spatial-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/spatial-design.md | -| `reference/motion-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/motion-design.md | +| `reference/spatial-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/spatial-design.md | +| `reference/motion-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/motion-design.md | | `reference/interaction-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/interaction-design.md | -| `reference/responsive-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/responsive-design.md | -| `reference/ux-writing.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/ux-writing.md | +| `reference/responsive-design.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/responsive-design.md | +| `reference/ux-writing.md` | https://github.com/pbakaus/impeccable/blob/main/source/skills/impeccable/reference/ux-writing.md | Each of these files has been modified to: diff --git a/.agents/skills/kilo-design/SKILL.md b/.agents/skills/kilo-design/SKILL.md index fb07e50d95..82a5f88772 100644 --- a/.agents/skills/kilo-design/SKILL.md +++ b/.agents/skills/kilo-design/SKILL.md @@ -49,16 +49,16 @@ Given a user prompt that invokes this skill: 2. Identify the dominant concern from the prompt and load the matching reference(s): -| Prompt signal | Load | -| ---------------------------------------------------------------- | --------------------------------- | -| typography, fonts, type scale, hierarchy, readability | `reference/typography.md` | +| Prompt signal | Load | +|---|---| +| typography, fonts, type scale, hierarchy, readability | `reference/typography.md` | | color, palette, contrast, accent, theming, gradient, a11y colors | `reference/color-and-contrast.md` | -| spacing, layout, grid, rhythm, padding, alignment, cards | `reference/spatial-design.md` | -| motion, animation, transitions, easing, micro-interactions | `reference/motion-design.md` | -| forms, focus, hover, states, dialog, dropdown, keyboard nav | `reference/interaction-design.md` | -| responsive, mobile, breakpoints, touch, tablet, adapt | `reference/responsive-design.md` | -| copy, microcopy, error messages, labels, empty state copy | `reference/ux-writing.md` | -| audit / critique / polish / general redesign | all references above | +| spacing, layout, grid, rhythm, padding, alignment, cards | `reference/spatial-design.md` | +| motion, animation, transitions, easing, micro-interactions | `reference/motion-design.md` | +| forms, focus, hover, states, dialog, dropdown, keyboard nav | `reference/interaction-design.md` | +| responsive, mobile, breakpoints, touch, tablet, adapt | `reference/responsive-design.md` | +| copy, microcopy, error messages, labels, empty state copy | `reference/ux-writing.md` | +| audit / critique / polish / general redesign | all references above | 3. If the prompt targets a specific file, component, or route, **read that file first** before proposing or making changes. Do not guess @@ -155,15 +155,15 @@ This skill is intentionally scoped. It does **not**: ## Reference map -| File | What it covers | -| --------------------------------- | --------------------------------------------------------------- | -| `reference/kilo-brand.md` | Kilo-specific tokens, components, rules. Load first, always. | -| `reference/typography.md` | Inter/mono usage, hierarchy, tabular nums, OpenType polish. | +| File | What it covers | +|---|---| +| `reference/kilo-brand.md` | Kilo-specific tokens, components, rules. Load first, always. | +| `reference/typography.md` | Inter/mono usage, hierarchy, tabular nums, OpenType polish. | | `reference/color-and-contrast.md` | OKLCH tokens, brand vs action color, dark-first contrast rules. | -| `reference/spatial-design.md` | Spacing, radius scale, grid patterns, optical alignment. | -| `reference/motion-design.md` | Durations, easings, reduced motion, Kilo brand flourishes. | -| `reference/interaction-design.md` | Focus, forms, overlays, destructive actions, keyboard nav. | -| `reference/responsive-design.md` | Breakpoints, input-method queries, safe areas, images. | -| `reference/ux-writing.md` | Kilo voice, labels, error copy, empty states, i18n. | +| `reference/spatial-design.md` | Spacing, radius scale, grid patterns, optical alignment. | +| `reference/motion-design.md` | Durations, easings, reduced motion, Kilo brand flourishes. | +| `reference/interaction-design.md` | Focus, forms, overlays, destructive actions, keyboard nav. | +| `reference/responsive-design.md` | Breakpoints, input-method queries, safe areas, images. | +| `reference/ux-writing.md` | Kilo voice, labels, error copy, empty states, i18n. | See `NOTICE.md` for Impeccable attribution and licensing. diff --git a/.agents/skills/kilo-design/reference/color-and-contrast.md b/.agents/skills/kilo-design/reference/color-and-contrast.md index 0230b3c30b..1094ccfd21 100644 --- a/.agents/skills/kilo-design/reference/color-and-contrast.md +++ b/.agents/skills/kilo-design/reference/color-and-contrast.md @@ -51,13 +51,13 @@ reserved for inline links and historical references, not action fills. ### Palette structure (how Kilo's tokens map) -| Role | Purpose | Kilo tokens | -| -------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| Brand | Rare, load-bearing accent | `brand-primary` | -| Action | Primary CTAs in product/marketing | `primary` / `primary-foreground` | -| Neutral | Text, backgrounds, borders | `background`, `foreground`, `muted`, `accent`, `secondary`, `card`, `popover`, `border`, `input`, `ring`, `kilo-gray` | -| Semantic | Destructive, success pills | `destructive`, badge variants `beta`/`new` | -| Surface | Sidebar, charts, Kilo gray | `sidebar-*`, `chart-1`..`chart-5`, `kilo-gray` | +| Role | Purpose | Kilo tokens | +|---|---|---| +| Brand | Rare, load-bearing accent | `brand-primary` | +| Action | Primary CTAs in product/marketing | `primary` / `primary-foreground` | +| Neutral | Text, backgrounds, borders | `background`, `foreground`, `muted`, `accent`, `secondary`, `card`, `popover`, `border`, `input`, `ring`, `kilo-gray` | +| Semantic | Destructive, success pills | `destructive`, badge variants `beta`/`new` | +| Surface | Sidebar, charts, Kilo gray | `sidebar-*`, `chart-1`..`chart-5`, `kilo-gray` | ### Theming discipline @@ -104,13 +104,13 @@ create a new branded surface, lean on `kilo-gray` rather than a pure gray. A complete system needs: -| Role | Purpose | Example | -| ------------ | ----------------------------- | ------------------------- | -| **Brand** | Rare, voice-carrying accent | 1 color, 1–2 shades | -| **Action** | Primary call-to-action | 1 color, 3 states | -| **Neutral** | Text, backgrounds, borders | 9–11 shade scale | +| Role | Purpose | Example | +|---|---|---| +| **Brand** | Rare, voice-carrying accent | 1 color, 1–2 shades | +| **Action** | Primary call-to-action | 1 color, 3 states | +| **Neutral** | Text, backgrounds, borders | 9–11 shade scale | | **Semantic** | Success, error, warning, info | 4 colors, 2–3 shades each | -| **Surface** | Cards, modals, overlays | 2–3 elevation levels | +| **Surface** | Cards, modals, overlays | 2–3 elevation levels | Skip secondary/tertiary unless you need them. Most apps work fine with one accent and one action color. @@ -131,11 +131,11 @@ That shared role should still stay near 10% visual weight. ### WCAG Requirements -| Content type | AA minimum | AAA target | -| ------------------------------- | ---------- | ---------- | -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | +| Content type | AA minimum | AAA target | +|---|---|---| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | The gotcha: placeholder text still needs 4.5:1. Check Kilo's `placeholder:text-muted-foreground` against `bg-input/30` on real screens @@ -170,12 +170,12 @@ first and "flip" it, you'll introduce bad shadows, under-contrast accents, and oversaturated hues. Design on the real `background` / `card` / `muted` surfaces, not on `#fff`. -| Light mode principle | Dark mode behavior | -| -------------------- | ------------------------------------------- | -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black — dark gray (OKLCH 12–18%) | +| Light mode principle | Dark mode behavior | +|---|---| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Never pure black — dark gray (OKLCH 12–18%) | Depth in dark mode comes from surface lightness, not shadow. Kilo's scale is already: `background` → `card`/`popover` → `muted`/`secondary`/`accent`. diff --git a/.agents/skills/kilo-design/reference/interaction-design.md b/.agents/skills/kilo-design/reference/interaction-design.md index 86ce9ed1e5..a259ad9da7 100644 --- a/.agents/skills/kilo-design/reference/interaction-design.md +++ b/.agents/skills/kilo-design/reference/interaction-design.md @@ -60,16 +60,16 @@ any interactive control from scratch, check these locations first: Every interactive element needs these states designed: -| State | When | Visual treatment | -| ------------ | ----------------------------- | --------------------------- | -| **Default** | At rest | Base styling | -| **Hover** | Pointer over (not touch) | Subtle lift, color shift | -| **Focus** | Keyboard / programmatic focus | Visible ring (see below) | -| **Active** | Being pressed | Pressed in, darker | -| **Disabled** | Not interactive | Reduced opacity, no pointer | -| **Loading** | Processing | Spinner, skeleton | -| **Error** | Invalid state | Red border, icon, message | -| **Success** | Completed | Green check, confirmation | +| State | When | Visual treatment | +|---|---|---| +| **Default** | At rest | Base styling | +| **Hover** | Pointer over (not touch) | Subtle lift, color shift | +| **Focus** | Keyboard / programmatic focus | Visible ring (see below) | +| **Active** | Being pressed | Pressed in, darker | +| **Disabled** | Not interactive | Reduced opacity, no pointer | +| **Loading** | Processing | Spinner, skeleton | +| **Error** | Invalid state | Red border, icon, message | +| **Success** | Completed | Green check, confirmation | Common miss: designing hover without focus, or vice versa. Keyboard users never see hover states. diff --git a/.agents/skills/kilo-design/reference/kilo-brand.md b/.agents/skills/kilo-design/reference/kilo-brand.md index e96e0bb364..7540e89e6c 100644 --- a/.agents/skills/kilo-design/reference/kilo-brand.md +++ b/.agents/skills/kilo-design/reference/kilo-brand.md @@ -13,15 +13,15 @@ generic AI-flavored "modern SaaS." Before changing tokens, colors, type, radius, or animation, consult these files directly: -| Concern | File | -| ------------------------ | ---------------------------------------------------------- | -| Web tokens & base theme | `apps/web/src/app/globals.css` | -| Font loading & variables | `apps/web/src/app/layout.tsx` | -| shadcn config | `apps/web/components.json` | -| Core UI primitives | `apps/web/src/components/ui/*.tsx` | -| Brand lockup | `apps/web/src/components/HeaderLogo.tsx` | -| Storybook canvas | `apps/storybook/.storybook/preview.ts` and `storybook.css` | -| Mobile tokens | `apps/mobile/src/global.css` | +| Concern | File | +|---|---| +| Web tokens & base theme | `apps/web/src/app/globals.css` | +| Font loading & variables | `apps/web/src/app/layout.tsx` | +| shadcn config | `apps/web/components.json` | +| Core UI primitives | `apps/web/src/components/ui/*.tsx` | +| Brand lockup | `apps/web/src/components/HeaderLogo.tsx` | +| Storybook canvas | `apps/storybook/.storybook/preview.ts` and `storybook.css` | +| Mobile tokens | `apps/mobile/src/global.css` | If a token or component already exists, **use it**. Do not reintroduce a parallel system. @@ -31,10 +31,10 @@ parallel system. Kilo has two surfaces with slightly different design rules. Identify which one you are designing for before picking colors, type scale, or motion. -| Register | Scope | -| --------------------- | --------------------------------------------------------------------------------------- | -| **Product UI** | Web app, dashboards, settings, billing, admin, Storybook components, mobile app screens | -| **Brand / Marketing** | Landing pages, docs, pricing, hero surfaces, on-brand campaign moments | +| Register | Scope | +|---|---| +| **Product UI** | Web app, dashboards, settings, billing, admin, Storybook components, mobile app screens | +| **Brand / Marketing** | Landing pages, docs, pricing, hero surfaces, on-brand campaign moments | Both use the same tokens and fonts. Brand permits more visual expression (hero type, animation, committed color, imagery). Product UI stays calm, @@ -60,40 +60,40 @@ These are declared as CSS variables in `globals.css` and surfaced to Tailwind via `@theme inline`. Prefer the Tailwind utility that maps to the token (e.g. `bg-background`, `text-foreground`, `border-border`) over hex. -| Token | Role | -| ------------------------------ | ------------------------------------------------------- | -| `background` | Page/body surface. Near-black `oklch(0.145 0 0)`. | -| `foreground` | Default text. Near-white `oklch(0.985 0 0)`. | -| `card`, `popover` | Elevated dark surface `oklch(0.205 0 0)`. | -| `card-foreground` | Text on card. | -| `primary` | Brand yellow-green primary CTA token. | -| `primary-foreground` | Near-black text on primary yellow-green. | +| Token | Role | +|---|---| +| `background` | Page/body surface. Near-black `oklch(0.145 0 0)`. | +| `foreground` | Default text. Near-white `oklch(0.985 0 0)`. | +| `card`, `popover` | Elevated dark surface `oklch(0.205 0 0)`. | +| `card-foreground` | Text on card. | +| `primary` | Brand yellow-green primary CTA token. | +| `primary-foreground` | Near-black text on primary yellow-green. | | `secondary`, `muted`, `accent` | Mid-dark surfaces `oklch(0.269 0 0)` for chips, hovers. | -| `muted-foreground` | Secondary text `oklch(0.708 0 0)`. | -| `border`, `input`, `ring` | Hairline borders and focus rings. | -| `destructive` | Red error/danger state. | -| `sidebar-*` | Sidebar app-shell tokens. | -| `chart-1`..`chart-5` | Data viz palette. | +| `muted-foreground` | Secondary text `oklch(0.708 0 0)`. | +| `border`, `input`, `ring` | Hairline borders and focus rings. | +| `destructive` | Red error/danger state. | +| `sidebar-*` | Sidebar app-shell tokens. | +| `chart-1`..`chart-5` | Data viz palette. | ### Kilo-specific primitives -| Token | Value | Use | -| ----------------------------------- | ----------------------------------------------- | -------------------------------- | -| `--brand-primary` / `brand-primary` | `oklch(95% 0.15 108)` (electric yellow-green) | Alias of primary for brand roles | -| `--color-kilo-gray` | `oklch(0.24 0.007 1)` | Kilo-branded neutral surface | -| `--color-kilo-gray-lighter` | Derived via `oklch(from ... calc(l + 0.1) c h)` | Paired with `kilo-gray` | -| `--ease-out-strong` | `cubic-bezier(0.23, 1, 0.32, 1)` | Preferred easing for transitions | +| Token | Value | Use | +|---|---|---| +| `--brand-primary` / `brand-primary` | `oklch(95% 0.15 108)` (electric yellow-green) | Alias of primary for brand roles | +| `--color-kilo-gray` | `oklch(0.24 0.007 1)` | Kilo-branded neutral surface | +| `--color-kilo-gray-lighter` | Derived via `oklch(from ... calc(l + 0.1) c h)` | Paired with `kilo-gray` | +| `--ease-out-strong` | `cubic-bezier(0.23, 1, 0.32, 1)` | Preferred easing for transitions | ### Primary action color The product primary CTA is the Kilo brand yellow-green, exposed through the semantic `primary` token and `--brand-primary` alias: -| Role | Value | -| ---------- | ---------------------------------------- | -| Background | `oklch(95% 0.15 108)` (`#EDFF00`-ish) | -| Hover | Slightly darker yellow-green | -| Text | Near-black via `primary-foreground` | +| Role | Value | +|---|---| +| Background | `oklch(95% 0.15 108)` (`#EDFF00`-ish) | +| Hover | Slightly darker yellow-green | +| Text | Near-black via `primary-foreground` | | Focus ring | Low-alpha yellow-green / semantic `ring` | Use it for the main action on a surface, exactly once. Blue is no longer a @@ -124,10 +124,10 @@ Avoid: Font loading is in `apps/web/src/app/layout.tsx`: -| Family | CSS variable | Use | -| -------------- | ------------------ | ---------------------------------------------------------- | -| Inter | `--font-sans` | Default UI text | -| Roboto Mono | `--font-mono` | Code, identifiers, metadata | +| Family | CSS variable | Use | +|---|---|---| +| Inter | `--font-sans` | Default UI text | +| Roboto Mono | `--font-mono` | Code, identifiers, metadata | | JetBrains Mono | `--font-jetbrains` | Terminal-like and code-editor surfaces (`.font-jetbrains`) | Known issue to be aware of (do not fix casually): `globals.css` currently @@ -152,13 +152,13 @@ Type scale rules for product UI: Base radius: `--radius: 0.625rem` in `globals.css`. Derived tokens: -| Token | Value | Typical use | -| ------------- | --------------------------- | ----------------------------- | -| `--radius-sm` | `calc(var(--radius) - 4px)` | Tight inline chips | -| `--radius-md` | `calc(var(--radius) - 2px)` | Buttons, inputs | -| `--radius-lg` | `var(--radius)` | Popovers, medium containers | -| `--radius-xl` | `calc(var(--radius) + 4px)` | Cards, dialogs | -| (pill) | `rounded-full` | Badges, avatars, status pills | +| Token | Value | Typical use | +|---|---|---| +| `--radius-sm` | `calc(var(--radius) - 4px)` | Tight inline chips | +| `--radius-md` | `calc(var(--radius) - 2px)` | Buttons, inputs | +| `--radius-lg` | `var(--radius)` | Popovers, medium containers | +| `--radius-xl` | `calc(var(--radius) + 4px)` | Cards, dialogs | +| (pill) | `rounded-full` | Badges, avatars, status pills | Follow existing shadcn primitives. Buttons/inputs `rounded-md`, cards `rounded-xl`, badges full-pill. Do not introduce new radius values. diff --git a/.agents/skills/kilo-design/reference/motion-design.md b/.agents/skills/kilo-design/reference/motion-design.md index 363244f6b6..00a973bb1c 100644 --- a/.agents/skills/kilo-design/reference/motion-design.md +++ b/.agents/skills/kilo-design/reference/motion-design.md @@ -45,12 +45,12 @@ Kilo motion is **purposeful and subtle** in product UI, with occasional ### Durations -| Duration | Use | Examples | -| --------- | ------------------------------------ | -------------------------------------- | -| 100–150ms | Instant feedback | Button press, toggle, color shift | -| 200–300ms | State changes | Menu open, tooltip, hover, focus bloom | -| 300–500ms | Layout changes | Accordion, drawer open, sheet open | -| 500–800ms | Entrance animations (brand surfaces) | Hero reveal, first-paint choreography | +| Duration | Use | Examples | +|---|---|---| +| 100–150ms | Instant feedback | Button press, toggle, color shift | +| 200–300ms | State changes | Menu open, tooltip, hover, focus bloom | +| 300–500ms | Layout changes | Accordion, drawer open, sheet open | +| 500–800ms | Entrance animations (brand surfaces) | Hero reveal, first-paint choreography | Exit animations are faster than entrances (≈75% of enter duration). @@ -70,12 +70,12 @@ Exit animations are faster than entrances (≈75% of enter duration). Timing matters more than easing. These durations feel right for most UI: -| Duration | Use | Examples | -| --------- | ------------------- | ---------------------------------- | -| 100–150ms | Instant feedback | Button press, toggle, color change | -| 200–300ms | State changes | Menu open, tooltip, hover states | -| 300–500ms | Layout changes | Accordion, modal, drawer | -| 500–800ms | Entrance animations | Page load, hero reveals | +| Duration | Use | Examples | +|---|---|---| +| 100–150ms | Instant feedback | Button press, toggle, color change | +| 200–300ms | State changes | Menu open, tooltip, hover states | +| 300–500ms | Layout changes | Accordion, modal, drawer | +| 500–800ms | Entrance animations | Page load, hero reveals | Exit animations are faster than entrances — use ~75% of enter duration. @@ -83,10 +83,10 @@ Exit animations are faster than entrances — use ~75% of enter duration. Don't use `ease` — it's a compromise that's rarely optimal. Instead: -| Curve | Use for | CSS | -| --------------- | ---------------------------- | -------------------------------- | -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | +| Curve | Use for | CSS | +|---|---|---| +| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | +| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | | **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | For micro-interactions, exponential curves feel natural (friction, diff --git a/.agents/skills/kilo-design/reference/spatial-design.md b/.agents/skills/kilo-design/reference/spatial-design.md index 52e424e4a4..0c7f034891 100644 --- a/.agents/skills/kilo-design/reference/spatial-design.md +++ b/.agents/skills/kilo-design/reference/spatial-design.md @@ -26,14 +26,14 @@ app shell with marketing-scale spacing. Kilo defines a radius scale: -| Token | Value | Use | -| ------------- | --------------------------- | ----------------------------- | -| `--radius` | `0.625rem` | Base | -| `--radius-sm` | `calc(var(--radius) - 4px)` | Tight inline chips | -| `--radius-md` | `calc(var(--radius) - 2px)` | Buttons, inputs | -| `--radius-lg` | `var(--radius)` | Popovers, medium containers | -| `--radius-xl` | `calc(var(--radius) + 4px)` | Cards, dialogs | -| (full) | `rounded-full` | Badges, avatars, status pills | +| Token | Value | Use | +|---|---|---| +| `--radius` | `0.625rem` | Base | +| `--radius-sm` | `calc(var(--radius) - 4px)` | Tight inline chips | +| `--radius-md` | `calc(var(--radius) - 2px)` | Buttons, inputs | +| `--radius-lg` | `var(--radius)` | Popovers, medium containers | +| `--radius-xl` | `calc(var(--radius) + 4px)` | Cards, dialogs | +| (full) | `rounded-full` | Badges, avatars, status pills | Do not introduce new radius values. @@ -96,13 +96,13 @@ If everything looks the same weight blurred, you have a hierarchy problem. Do not rely on size alone. Combine: -| Tool | Strong hierarchy | Weak hierarchy | -| ------------ | ------------------------ | ----------------- | -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top / left (primary) | Bottom / right | -| **Space** | Surrounded by whitespace | Crowded | +| Tool | Strong hierarchy | Weak hierarchy | +|---|---|---| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top / left (primary) | Bottom / right | +| **Space** | Surrounded by whitespace | Crowded | The best hierarchy uses 2–3 dimensions at once: a heading that's larger, bolder, AND has more space above it. diff --git a/.agents/skills/kilo-design/reference/typography.md b/.agents/skills/kilo-design/reference/typography.md index cca5b39578..76877d4bb0 100644 --- a/.agents/skills/kilo-design/reference/typography.md +++ b/.agents/skills/kilo-design/reference/typography.md @@ -7,10 +7,10 @@ Kilo's fonts are already chosen and loaded. Do not introduce new families. -| Family | CSS variable | Use | -| -------------- | ------------------ | ---------------------------------------------------------- | -| Inter | `--font-sans` | Default product UI, buttons, labels, body | -| Roboto Mono | `--font-mono` | Code tokens, identifiers, metadata | +| Family | CSS variable | Use | +|---|---|---| +| Inter | `--font-sans` | Default product UI, buttons, labels, body | +| Roboto Mono | `--font-mono` | Code tokens, identifiers, metadata | | JetBrains Mono | `--font-jetbrains` | Terminal and code-editor surfaces (class `font-jetbrains`) | Kilo-specific rules: @@ -62,13 +62,13 @@ The common mistake: too many font sizes that are too close together (14, 15, **Use fewer sizes with more contrast.** A five-size system covers most product UI: -| Role | Typical ratio | Kilo Tailwind | Use case | -| ---- | ------------- | ------------- | ----------------------- | -| xs | 0.75rem | `text-xs` | Captions, legal, helper | -| sm | 0.875rem | `text-sm` | Secondary UI, metadata | -| base | 1rem | `text-base` | Body text | -| lg | 1.125–1.25rem | `text-lg` | Subheadings, lead text | -| xl+ | 1.5rem and up | `text-xl`+ | Page titles, hero | +| Role | Typical ratio | Kilo Tailwind | Use case | +|---|---|---|---| +| xs | 0.75rem | `text-xs` | Captions, legal, helper | +| sm | 0.875rem | `text-sm` | Secondary UI, metadata | +| base | 1rem | `text-base` | Body text | +| lg | 1.125–1.25rem | `text-lg` | Subheadings, lead text | +| xl+ | 1.5rem and up | `text-xl`+ | Page titles, hero | Pick a ratio (1.25 major third, 1.333 perfect fourth, 1.5 perfect fifth) and commit. Do not mix multiple scales. diff --git a/.agents/skills/kilo-design/reference/ux-writing.md b/.agents/skills/kilo-design/reference/ux-writing.md index debb54e63b..7c1cc5ce62 100644 --- a/.agents/skills/kilo-design/reference/ux-writing.md +++ b/.agents/skills/kilo-design/reference/ux-writing.md @@ -19,14 +19,14 @@ Prefer concrete verbs and specific nouns. Pick one term per concept and hold it across web, mobile, docs, and UI: -| Use | Not | -| ------------------ | ---------------------------------------------------------------- | -| Sign in / Sign out | Log in, Log out, Enter, Exit | -| Delete | Remove, Trash, Clear (for destructive ops) | -| Settings | Preferences, Options, Configuration | -| Create | Add, New (for creating a persistent thing) | -| Workspace | Team, Account, Org (if "workspace" is what the product calls it) | -| Kilo Code | KiloCode, kilo-code (product name) | +| Use | Not | +|---|---| +| Sign in / Sign out | Log in, Log out, Enter, Exit | +| Delete | Remove, Trash, Clear (for destructive ops) | +| Settings | Preferences, Options, Configuration | +| Create | Add, New (for creating a persistent thing) | +| Workspace | Team, Account, Org (if "workspace" is what the product calls it) | +| Kilo Code | KiloCode, kilo-code (product name) | If this repo has a newer glossary, prefer that. Do not invent synonyms to add variety. @@ -35,12 +35,12 @@ to add variety. Use verb + object. Specific over generic. Kilo-flavored examples: -| Bad | Good | -| ---------- | -------------------- | -| OK | Save changes | -| Submit | Create workspace | -| Yes | Delete project | -| Cancel | Keep editing | +| Bad | Good | +|---|---| +| OK | Save changes | +| Submit | Create workspace | +| Yes | Delete project | +| Cancel | Keep editing | | Click here | View billing history | For destructive actions, name the destruction and the count: @@ -128,13 +128,13 @@ action in both buttons: Never use `OK`, `Submit`, or `Yes`/`No`. Use specific verb + object: -| Bad | Good | Why | -| ---------- | -------------- | ----------------------------- | -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | +| Bad | Good | Why | +|---|---|---| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | For destructive actions, name the destruction: @@ -150,13 +150,13 @@ Every error should answer: (1) what happened, (2) why, (3) how to fix it. ### Error message templates -| Situation | Template | -| ----------------- | --------------------------------------------------------------- | -| Format error | "[Field] needs to be [format]. Example: [example]" | -| Missing required | "Please enter [what's missing]" | -| Permission denied | "You don't have access to [thing]. [What to do instead]" | -| Network error | "We couldn't reach [thing]. Check your connection and [action]" | -| Server error | "Something went wrong on our end. We're looking into it." | +| Situation | Template | +|---|---| +| Format error | "[Field] needs to be [format]. Example: [example]" | +| Missing required | "Please enter [what's missing]" | +| Permission denied | "You don't have access to [thing]. [What to do instead]" | +| Network error | "We couldn't reach [thing]. Check your connection and [action]" | +| Server error | "Something went wrong on our end. We're looking into it." | ### Don't blame the user @@ -179,11 +179,11 @@ Empty states are onboarding moments: **Voice** is your brand's personality — consistent everywhere. **Tone** adapts to the moment. -| Moment | Tone shift | -| ------------------- | ------------------------------------------------------------ | -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try…" | -| Loading | Reassuring: "Saving your work…" | +| Moment | Tone shift | +|---|---| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try…" | +| Loading | Reassuring: "Saving your work…" | | Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | Never use humor for errors. @@ -203,12 +203,12 @@ Never use humor for errors. German text is ~30% longer than English. Allocate space: -| Language | Expansion | -| -------- | ------------------------------ | -| German | +30% | -| French | +20% | -| Finnish | +30–40% | -| Chinese | -30% (fewer chars, same width) | +| Language | Expansion | +|---|---| +| German | +30% | +| French | +20% | +| Finnish | +30–40% | +| Chinese | -30% (fewer chars, same width) | ### Translation-friendly patterns diff --git a/.agents/skills/workers-best-practices/SKILL.md b/.agents/skills/workers-best-practices/SKILL.md index 6350ccbf79..6516c8c015 100644 --- a/.agents/skills/workers-best-practices/SKILL.md +++ b/.agents/skills/workers-best-practices/SKILL.md @@ -9,12 +9,12 @@ Your knowledge of Cloudflare Workers APIs, types, and configuration may be outda Fetch the **latest** versions before writing or reviewing Workers code. Do not rely on baked-in knowledge for API signatures, config fields, or binding shapes. -| Source | How to retrieve | Use for | -| ---------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------- | -| Workers best practices | Fetch `https://developers.cloudflare.com/workers/best-practices/workers-best-practices/` | Canonical rules, patterns, anti-patterns | -| Workers types | See `references/review.md` for retrieval steps | API signatures, handler types, binding types | -| Wrangler config schema | `node_modules/wrangler/config-schema.json` | Config fields, binding shapes, allowed values | -| Cloudflare docs | Search tool or `https://developers.cloudflare.com/workers/` | API reference, compatibility dates/flags | +| Source | How to retrieve | Use for | +|---|---|---| +| Workers best practices | Fetch `https://developers.cloudflare.com/workers/best-practices/workers-best-practices/` | Canonical rules, patterns, anti-patterns | +| Workers types | See `references/review.md` for retrieval steps | API signatures, handler types, binding types | +| Wrangler config schema | `node_modules/wrangler/config-schema.json` | Config fields, binding shapes, allowed values | +| Cloudflare docs | Search tool or `https://developers.cloudflare.com/workers/` | API reference, compatibility dates/flags | ## FIRST: Fetch Latest References @@ -37,68 +37,68 @@ mkdir -p /tmp/workers-types-latest && \ ### Configuration -| Rule | Summary | -| ------------------ | --------------------------------------------------------------------------------------- | +| Rule | Summary | +|---|---| | Compatibility date | Set `compatibility_date` to today on new projects; update periodically on existing ones | -| nodejs_compat | Enable the `nodejs_compat` flag — many libraries depend on Node.js built-ins | -| wrangler types | Run `wrangler types` to generate `Env` — never hand-write binding interfaces | -| Secrets | Use `wrangler secret put`, never hardcode secrets in config or source | -| wrangler.jsonc | Use JSONC config for non-secret settings — newer features are JSON-only | +| nodejs_compat | Enable the `nodejs_compat` flag — many libraries depend on Node.js built-ins | +| wrangler types | Run `wrangler types` to generate `Env` — never hand-write binding interfaces | +| Secrets | Use `wrangler secret put`, never hardcode secrets in config or source | +| wrangler.jsonc | Use JSONC config for non-secret settings — newer features are JSON-only | ### Request & Response Handling -| Rule | Summary | -| --------- | ------------------------------------------------------------------------------- | +| Rule | Summary | +|---|---| | Streaming | Stream large/unknown payloads — never `await response.text()` on unbounded data | -| waitUntil | Use `ctx.waitUntil()` for post-response work; do not destructure `ctx` | +| waitUntil | Use `ctx.waitUntil()` for post-response work; do not destructure `ctx` | ### Architecture -| Rule | Summary | -| ------------------ | -------------------------------------------------------------------------- | +| Rule | Summary | +|---|---| | Bindings over REST | Use in-process bindings (KV, R2, D1, Queues) — not the Cloudflare REST API | -| 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 | +| 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 | ### Observability -| Rule | Summary | -| ------------- | --------------------------------------------------------------------------------------- | +| Rule | Summary | +|---|---| | Logs & Traces | Enable `observability` in config with `head_sampling_rate`; use structured JSON logging | ### Code Patterns -| Rule | Summary | -| ----------------------- | ------------------------------------------------------------------------------------- | -| No global request state | Never store request-scoped data in module-level variables | -| Floating promises | Every Promise must be `await`ed, `return`ed, `void`ed, or passed to `ctx.waitUntil()` | +| Rule | Summary | +|---|---| +| No global request state | Never store request-scoped data in module-level variables | +| Floating promises | Every Promise must be `await`ed, `return`ed, `void`ed, or passed to `ctx.waitUntil()` | ### Security -| Rule | Summary | -| ------------------------- | ------------------------------------------------------------------------------------------- | -| Web Crypto | Use `crypto.randomUUID()` / `crypto.getRandomValues()` — never `Math.random()` for security | -| No passThroughOnException | Use explicit try/catch with structured error responses | +| Rule | Summary | +|---|---| +| Web Crypto | Use `crypto.randomUUID()` / `crypto.getRandomValues()` — never `Math.random()` for security | +| No passThroughOnException | Use explicit try/catch with structured error responses | ## Anti-Patterns to Flag -| Anti-pattern | Why it matters | -| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -| `await response.text()` on unbounded data | Memory exhaustion — 128 MB limit | -| Hardcoded secrets in source or config | Credential leak via version control | -| `Math.random()` for tokens/IDs | Predictable, not cryptographically secure | -| Bare `fetch()` without `await` or `waitUntil` | Floating promise — dropped result, swallowed error | -| Module-level mutable variables for request state | Cross-request data leaks, stale state, I/O errors | -| Cloudflare REST API from inside a Worker | Unnecessary network hop, auth overhead, added latency | -| `ctx.passThroughOnException()` as error handling | Hides bugs, makes debugging impossible | -| Hand-written `Env` interface | Drifts from actual wrangler config bindings | -| Direct string comparison for secret values | Timing side-channel — use `crypto.subtle.timingSafeEqual` | -| Destructuring `ctx` (`const { waitUntil } = ctx`) | Loses `this` binding — throws "Illegal invocation" at runtime | -| `any` on `Env` or handler params | Defeats type safety for all binding access | -| `as unknown as T` double-cast | Hides real type incompatibilities — fix the design | +| Anti-pattern | Why it matters | +|---|---| +| `await response.text()` on unbounded data | Memory exhaustion — 128 MB limit | +| Hardcoded secrets in source or config | Credential leak via version control | +| `Math.random()` for tokens/IDs | Predictable, not cryptographically secure | +| Bare `fetch()` without `await` or `waitUntil` | Floating promise — dropped result, swallowed error | +| Module-level mutable variables for request state | Cross-request data leaks, stale state, I/O errors | +| Cloudflare REST API from inside a Worker | Unnecessary network hop, auth overhead, added latency | +| `ctx.passThroughOnException()` as error handling | Hides bugs, makes debugging impossible | +| Hand-written `Env` interface | Drifts from actual wrangler config bindings | +| Direct string comparison for secret values | Timing side-channel — use `crypto.subtle.timingSafeEqual` | +| Destructuring `ctx` (`const { waitUntil } = ctx`) | Loses `this` binding — throws "Illegal invocation" at runtime | +| `any` on `Env` or handler params | Defeats type safety for all binding access | +| `as unknown as T` double-cast | Hides real type incompatibilities — fix the design | | `implements` on platform base classes (instead of `extends`) | Legacy — loses `this.ctx`, `this.env`. Applies to DurableObject, WorkerEntrypoint, Workflow | -| `env.X` inside platform base class | Should be `this.env.X` in classes extending DurableObject, WorkerEntrypoint, etc. | +| `env.X` inside platform base class | Should be `this.env.X` in classes extending DurableObject, WorkerEntrypoint, etc. | ## Review Workflow diff --git a/.agents/skills/workers-best-practices/references/review.md b/.agents/skills/workers-best-practices/references/review.md index fe33bc0bcc..a75b589c85 100644 --- a/.agents/skills/workers-best-practices/references/review.md +++ b/.agents/skills/workers-best-practices/references/review.md @@ -68,13 +68,13 @@ Flag `env.X` inside a class extending a platform base class. Flag `this.env.X` i ### Type integrity rules -| Rule | Detail | -| ----------------------- | --------------------------------------------------------------------------- | -| No `any` | Never on binding types, handler params, or API responses | -| No double-casting | `as unknown as T` hides real incompatibilities — fix the underlying design | -| Justify suppressions | `@ts-ignore`/`@ts-expect-error` must include a comment explaining why | -| Prefer `satisfies` | Use `satisfies ExportedHandler` over `as` — validates without widening | -| Validate, do not assert | Schema or type guard for untyped data (JSON, parsed bodies), not `as` | +| Rule | Detail | +|---|---| +| No `any` | Never on binding types, handler params, or API responses | +| No double-casting | `as unknown as T` hides real incompatibilities — fix the underlying design | +| Justify suppressions | `@ts-ignore`/`@ts-expect-error` must include a comment explaining why | +| Prefer `satisfies` | Use `satisfies ExportedHandler` over `as` — validates without widening | +| Validate, do not assert | Schema or type guard for untyped data (JSON, parsed bodies), not `as` | ### Stale class patterns @@ -108,14 +108,14 @@ For executable examples, verify: `name`, `compatibility_date`, `main`. Check the ### Common config mistakes -| Check | What to look for | -| -------------------------- | -------------------------------------------------- | +| Check | What to look for | +|---|---| | Stale `compatibility_date` | Should be recent; use `$today` placeholder in docs | -| Missing DO migrations | Every new DO class needs a migration entry | -| Binding name mismatch | Config `binding`/`name` must match `env.X` in code | -| Secrets in config | Never in `vars` — use `wrangler secret put` | -| Wrong binding key | Verify top-level key name against the schema | -| Missing entrypoint | `main` required for executable Workers | +| Missing DO migrations | Every new DO class needs a migration entry | +| Binding name mismatch | Config `binding`/`name` must match `env.X` in code | +| Secrets in config | Never in `vars` — use `wrangler secret put` | +| Wrong binding key | Verify top-level key name against the schema | +| Missing entrypoint | `main` required for executable Workers | --- diff --git a/.agents/skills/wrangler/SKILL.md b/.agents/skills/wrangler/SKILL.md index 6a164132f1..cd04503633 100644 --- a/.agents/skills/wrangler/SKILL.md +++ b/.agents/skills/wrangler/SKILL.md @@ -11,11 +11,11 @@ Your knowledge of Wrangler CLI flags, config fields, and subcommands may be outd Fetch the **latest** information before writing or reviewing Wrangler commands and config. Do not rely on baked-in knowledge for CLI flags, config fields, or binding shapes. -| Source | How to retrieve | Use for | -| ---------------------- | ----------------------------------------------------------- | --------------------------------------------- | -| Wrangler docs | `https://developers.cloudflare.com/workers/wrangler/` | CLI commands, flags, config reference | -| Wrangler config schema | `node_modules/wrangler/config-schema.json` | Config fields, binding shapes, allowed values | -| Cloudflare docs | Search tool or `https://developers.cloudflare.com/workers/` | API reference, compatibility dates/flags | +| Source | How to retrieve | Use for | +|---|---|---| +| Wrangler docs | `https://developers.cloudflare.com/workers/wrangler/` | CLI commands, flags, config reference | +| Wrangler config schema | `node_modules/wrangler/config-schema.json` | Config fields, binding shapes, allowed values | +| Cloudflare docs | Search tool or `https://developers.cloudflare.com/workers/` | API reference, compatibility dates/flags | ## FIRST: Verify Wrangler Installation @@ -50,16 +50,16 @@ npx create-cloudflare@latest my-app ## Quick Reference: Core Commands -| Task | Command | -| --------------------------- | --------------------------- | -| Start local dev server | `wrangler dev` | -| Deploy to Cloudflare | `wrangler deploy` | -| Deploy dry run | `wrangler deploy --dry-run` | -| Generate TypeScript types | `wrangler types` | -| Profile Worker startup time | `wrangler check startup` | -| View live logs | `wrangler tail` | -| Delete Worker | `wrangler delete` | -| Auth status | `wrangler whoami` | +| Task | Command | +|---|---| +| Start local dev server | `wrangler dev` | +| Deploy to Cloudflare | `wrangler deploy` | +| Deploy dry run | `wrangler deploy --dry-run` | +| Generate TypeScript types | `wrangler types` | +| Profile Worker startup time | `wrangler check startup` | +| View live logs | `wrangler tail` | +| Delete Worker | `wrangler delete` | +| Auth status | `wrangler whoami` | --- @@ -835,14 +835,14 @@ curl http://localhost:8787/__scheduled ### Common Issues -| Issue | Solution | -| ------------------------------- | ------------------------------------------------------------------------- | -| `command not found: wrangler` | Install: `npm install -D wrangler` | -| Auth errors | Run `wrangler login` | -| Startup time limit exceeded | Run `wrangler check startup` to profile startup and generate CPU profiles | -| Type errors after config change | Run `wrangler types` | -| Local storage not persisting | Check `.wrangler/state` directory | -| Binding undefined in Worker | Verify binding name matches config exactly | +| Issue | Solution | +|---|---| +| `command not found: wrangler` | Install: `npm install -D wrangler` | +| Auth errors | Run `wrangler login` | +| Startup time limit exceeded | Run `wrangler check startup` to profile startup and generate CPU profiles | +| Type errors after config change | Run `wrangler types` | +| Local storage not persisting | Check `.wrangler/state` directory | +| Binding undefined in Worker | Verify binding name matches config exactly | ### Debug Commands diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 394e5b9928..ebf7f79cc5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,8 +18,8 @@ | Before | After | -| ------ | ----- | -| | | +|---|---| +| | | ## Reviewer Notes diff --git a/.github/workflows/check-md-table-padding.yml b/.github/workflows/check-md-table-padding.yml new file mode 100644 index 0000000000..54165d5bc9 --- /dev/null +++ b/.github/workflows/check-md-table-padding.yml @@ -0,0 +1,19 @@ +name: Check markdown table padding +on: + pull_request: + paths: + - '**/*.md' + - 'script/check-md-table-padding.ts' + - '.github/workflows/check-md-table-padding.yml' + workflow_dispatch: +jobs: + check: + name: Check markdown table padding + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: oven-sh/setup-bun@v2 + - name: Run check + run: bun run script/check-md-table-padding.ts diff --git a/.plans/contributor-champion-credits.md b/.plans/contributor-champion-credits.md index 9897a5c5a0..77635cc1bb 100644 --- a/.plans/contributor-champion-credits.md +++ b/.plans/contributor-champion-credits.md @@ -6,11 +6,11 @@ Add monthly Kilo Credits to the contributor champions feature, modeled after the ### Tier Credit Amounts -| Tier | Monthly Credits | Auto-upgrade threshold | -| ----------- | --------------- | ----------------------- | -| Contributor | $0/month | 1+ merged PR in 90 days | -| Ambassador | $50/month | 5+ merged PRs all-time | -| Champion | $150/month | 15+ merged PRs all-time | +| Tier | Monthly Credits | Auto-upgrade threshold | +|---|---|---| +| Contributor | $0/month | 1+ merged PR in 90 days | +| Ambassador | $50/month | 5+ merged PRs all-time | +| Champion | $150/month | 15+ merged PRs all-time | --- @@ -246,10 +246,10 @@ If the contributor has no linked Kilo account, show a warning: Add columns to the Enrolled table: -| Column | Description | -| ------------ | --------------------------------------------------------------- | -| Credits/mo | e.g. "$50" or "—" for contributor tier | -| Last Grant | e.g. "2025-02-15 12:00" or "Never" | +| Column | Description | +|---|---| +| Credits/mo | e.g. "$50" or "—" for contributor tier | +| Last Grant | e.g. "2025-02-15 12:00" or "Never" | | Kilo Account | Already effectively shown via the email link + AlertCircle icon | ### 6c. Tier selector credit preview in review queue diff --git a/.plans/db-perf-improvements.md b/.plans/db-perf-improvements.md index a031b11f00..6aa7af4b35 100644 --- a/.plans/db-perf-improvements.md +++ b/.plans/db-perf-improvements.md @@ -35,12 +35,12 @@ packages/db/src/schema.ts (lines 566-600) **Indexes:** -| Name | Columns | Condition | -| --------------------------------------- | -------------------------- | ----------------------------------- | -| `idx_created_at` | `created_at` | - | -| `idx_abuse_classification` | `abuse_classification` | - | -| `idx_kilo_user_id_created_at2` | `kilo_user_id, created_at` | - | -| `idx_microdollar_usage_organization_id` | `organization_id` | `WHERE organization_id IS NOT NULL` | +| Name | Columns | Condition | +|---|---|---| +| `idx_created_at` | `created_at` | - | +| `idx_abuse_classification` | `abuse_classification` | - | +| `idx_kilo_user_id_created_at2` | `kilo_user_id, created_at` | - | +| `idx_microdollar_usage_organization_id` | `organization_id` | `WHERE organization_id IS NOT NULL` | ## Current Hotspots @@ -57,19 +57,19 @@ packages/db/src/schema.ts (lines 566-600) ### Slow query sources -| File | Query Pattern | Date Filter? | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------- | ------------------- | -| `src/app/api/profile/usage/route.ts:63-68` | SUM(cost), COUNT(\*), SUM(tokens) grouped by DATE(created_at) | **None** | -| `src/routers/user-router.ts:162-169` | SUM(cost), COUNT(\*), SUM(tokens) for autocomplete model | **None** | -| `src/routers/kilo-pass-router.ts:190-202` | SUM(cost) for billing period | Bounded range | -| `src/routers/kilo-pass-router.ts:268-279` | SUM(cost) last 3 months | Lower bound only | -| `src/routers/organizations/organization-router.ts:282-295` | SUM(cost), COUNT(id), SUM(tokens) last 30 days | Lower bound only | -| `src/routers/organizations/organization-usage-details-router.ts:158-188` | SUM/COUNT grouped by time bucket, model, provider, project + JOIN | Bounded range | -| `src/routers/organizations/organization-usage-details-router.ts:296-319` | SUM/COUNT grouped by date, user, model + JOIN | Lower bound or none | -| `src/routers/organizations/organization-usage-details-router.ts:345-357` | SUM(cost), COUNT(\*), SUM(tokens) for org autocomplete model | **None** | -| `src/app/admin/api/abuse/daily-stats/route.ts:38-45` | SUM(CASE WHEN abuse) grouped by day, 7-day window | Lower bound only | -| `src/app/admin/api/abuse/stats/route.ts:35-54` | SUM/COUNT 1h and 24h windows | Lower bound only | -| `src/app/admin/api/abuse/hourly-stats/route.ts:36-45` | SUM(CASE WHEN abuse) grouped by hour, 12h window | Lower bound only | +| File | Query Pattern | Date Filter? | +|---|---|---| +| `src/app/api/profile/usage/route.ts:63-68` | SUM(cost), COUNT(\*), SUM(tokens) grouped by DATE(created_at) | **None** | +| `src/routers/user-router.ts:162-169` | SUM(cost), COUNT(\*), SUM(tokens) for autocomplete model | **None** | +| `src/routers/kilo-pass-router.ts:190-202` | SUM(cost) for billing period | Bounded range | +| `src/routers/kilo-pass-router.ts:268-279` | SUM(cost) last 3 months | Lower bound only | +| `src/routers/organizations/organization-router.ts:282-295` | SUM(cost), COUNT(id), SUM(tokens) last 30 days | Lower bound only | +| `src/routers/organizations/organization-usage-details-router.ts:158-188` | SUM/COUNT grouped by time bucket, model, provider, project + JOIN | Bounded range | +| `src/routers/organizations/organization-usage-details-router.ts:296-319` | SUM/COUNT grouped by date, user, model + JOIN | Lower bound or none | +| `src/routers/organizations/organization-usage-details-router.ts:345-357` | SUM(cost), COUNT(\*), SUM(tokens) for org autocomplete model | **None** | +| `src/app/admin/api/abuse/daily-stats/route.ts:38-45` | SUM(CASE WHEN abuse) grouped by day, 7-day window | Lower bound only | +| `src/app/admin/api/abuse/stats/route.ts:35-54` | SUM/COUNT 1h and 24h windows | Lower bound only | +| `src/app/admin/api/abuse/hourly-stats/route.ts:36-45` | SUM(CASE WHEN abuse) grouped by hour, 12h window | Lower bound only | ## Success Criteria @@ -440,21 +440,21 @@ Documented for reference if growth continues beyond the rollup threshold: ## Rollout Summary -| Step | Phase | Deploy independently? | Requires migration? | Risk | -| ---- | ------------------------------------------------------------- | ------------------------- | ------------------- | -------- | -| 1 | 1a: Add query timing and tagging | Yes | No | None | -| 2 | 1b: Enforce scoped statement timeouts | Yes | No | Low | -| 3 | 1c: Route read-only microdollar_usage aggregations to replica | Yes | No | Very low | -| 4 | 2a: Profile usage period selector (API + UI) | Yes (behind flag) | No | Medium | -| 5 | 2b: User autocomplete period param | With step 4 (behind flag) | No | Medium | -| 6 | 2c: Org autocomplete date bound | Yes | No | Low | -| 7 | 2d: Standardize ranged queries | Yes | No | Very low | -| 8 | 2e: Feature flag rollout | After validating 4-7 | No | Low | -| 9 | 3a: User covering index | Yes | Yes (CONCURRENTLY) | Low | -| 10 | 3b: Org covering index | Yes | Yes (CONCURRENTLY) | Low | -| 11 | 3c: Measure write amplification | After 9+10 | No | N/A | -| 12 | 3d: Drop old indexes | After verifying 9+10+11 | Yes | Low | -| 13 | Phase 4: Reassess | After measuring 1-3 | No | N/A | -| 14 | Phase 5: Rollups | Only if justified | Yes | High | +| Step | Phase | Deploy independently? | Requires migration? | Risk | +|---|---|---|---|---| +| 1 | 1a: Add query timing and tagging | Yes | No | None | +| 2 | 1b: Enforce scoped statement timeouts | Yes | No | Low | +| 3 | 1c: Route read-only microdollar_usage aggregations to replica | Yes | No | Very low | +| 4 | 2a: Profile usage period selector (API + UI) | Yes (behind flag) | No | Medium | +| 5 | 2b: User autocomplete period param | With step 4 (behind flag) | No | Medium | +| 6 | 2c: Org autocomplete date bound | Yes | No | Low | +| 7 | 2d: Standardize ranged queries | Yes | No | Very low | +| 8 | 2e: Feature flag rollout | After validating 4-7 | No | Low | +| 9 | 3a: User covering index | Yes | Yes (CONCURRENTLY) | Low | +| 10 | 3b: Org covering index | Yes | Yes (CONCURRENTLY) | Low | +| 11 | 3c: Measure write amplification | After 9+10 | No | N/A | +| 12 | 3d: Drop old indexes | After verifying 9+10+11 | Yes | Low | +| 13 | Phase 4: Reassess | After measuring 1-3 | No | N/A | +| 14 | Phase 5: Rollups | Only if justified | Yes | High | Each step can be deployed, measured, and verified before proceeding to the next. diff --git a/.plans/experimental-models-1.md b/.plans/experimental-models-1.md index 579beec106..8b1694a4ef 100644 --- a/.plans/experimental-models-1.md +++ b/.plans/experimental-models-1.md @@ -10,14 +10,14 @@ [todo] not started --> -| Phase | Status | Current State | -| ----------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Phase 1 — Schema + Migration | [done] | Experiment tables exist. `model_experiment_request` is monthly range-partitioned on `created_at`, uses primary key `(usage_id, created_at)`, and stores one full-body prompt hash plus `request_kind`. | -| Phase 2 — Gateway Header Capture | [done] | Gateway captures `x-kilo-request`, `x-kilo-session`, and `x-kilocode-machineid`, and passes the client request id, session id, machine id, and client IP into routing/usage context. | -| Phase 3 — Variant Picker + Routing | [done] | Experimented public ids route through the deterministic picker, load routing details from Postgres after Redis membership pre-check, and go directly to the selected partner upstream. | -| Phase 4 — Usage, Metrics, Reporting | [done-core] | Attribution rows and R2 prompt bodies are written after microdollar usage. Admin request log reads the rows inline. Live aggregate reporting and `model_experiment_request_stats` are deferred until a report consumer needs them. | -| Phase 5 — Admin tRPC + UI | [done-core] | Admin CRUD, state transitions, variant version hot-swap, key rotation, UI tab, and request log exist. Request log shows existing prompt-prefix metadata; `getLiveStats` and full prompt inflation via `getPromptByHash` are still deferred. | -| Phase 6 — Specs + Tests | [partial] | Router, picker, prompt persistence, partitioning, and soft-delete policy tests exist. Durable spec file `.specs/model-experiments.md` and AGENTS registration are still owed. | +| Phase | Status | Current State | +|---|---|---| +| Phase 1 — Schema + Migration | [done] | Experiment tables exist. `model_experiment_request` is monthly range-partitioned on `created_at`, uses primary key `(usage_id, created_at)`, and stores one full-body prompt hash plus `request_kind`. | +| Phase 2 — Gateway Header Capture | [done] | Gateway captures `x-kilo-request`, `x-kilo-session`, and `x-kilocode-machineid`, and passes the client request id, session id, machine id, and client IP into routing/usage context. | +| Phase 3 — Variant Picker + Routing | [done] | Experimented public ids route through the deterministic picker, load routing details from Postgres after Redis membership pre-check, and go directly to the selected partner upstream. | +| Phase 4 — Usage, Metrics, Reporting | [done-core] | Attribution rows and R2 prompt bodies are written after microdollar usage. Admin request log reads the rows inline. Live aggregate reporting and `model_experiment_request_stats` are deferred until a report consumer needs them. | +| Phase 5 — Admin tRPC + UI | [done-core] | Admin CRUD, state transitions, variant version hot-swap, key rotation, UI tab, and request log exist. `getLiveStats` and prompt inflation via `getPromptByHash` are still deferred. | +| Phase 6 — Specs + Tests | [partial] | Router, picker, prompt persistence, partitioning, and soft-delete policy tests exist. Durable spec file `.specs/model-experiments.md` and AGENTS registration are still owed. | **Current schema:** @@ -72,7 +72,7 @@ Operational consequence: admin mutations that move experiments into or out of ro - `apps/web/src/lib/ai-gateway/experiments/upstream-schema.ts` — `ExperimentUpstreamSchema` (strict subset of `CustomLlmDefinitionSchema`, no `api_key`, no `extra_headers`). - `apps/web/src/lib/redis-keys.ts` — `EXPERIMENTED_PUBLIC_IDS_REDIS_KEY` helper used by Phase 3 membership checks and admin recomputation on routing-affecting status changes. - `apps/web/src/lib/redis.ts` — includes `redisDel(key)` helper. -- `apps/web/src/routers/admin/model-experiments-router.ts` — full CRUD + state machine (`activate`, `pause`, `complete`, `setArchived`, `delete`-on-draft) + variant ops (`addVariant`, `removeVariant`, `updateVariantLabel`, `swapVariantVersion`, `rotateApiKey`). All routing-affecting mutations invalidate per-public-id cache and recompute the membership set. Request-log rows include the existing `microdollar_usage_metadata.user_prompt_prefix`, `system_prompt_prefix.system_prompt_prefix`, and `system_prompt_length` fields for compact admin previews. `encrypted_api_key` is **never** selected by `list`/`get`/`swapVariantVersion`/`rotateApiKey` — admin response shapers explicitly enumerate non-key columns. `BYOK_ENCRYPTION_KEY` missing → `INTERNAL_SERVER_ERROR` on key-touching ops. +- `apps/web/src/routers/admin/model-experiments-router.ts` — full CRUD + state machine (`activate`, `pause`, `complete`, `setArchived`, `delete`-on-draft) + variant ops (`addVariant`, `removeVariant`, `updateVariantLabel`, `swapVariantVersion`, `rotateApiKey`). All routing-affecting mutations invalidate per-public-id cache and recompute the membership set. `encrypted_api_key` is **never** selected by `list`/`get`/`swapVariantVersion`/`rotateApiKey` — admin response shapers explicitly enumerate non-key columns. `BYOK_ENCRYPTION_KEY` missing → `INTERNAL_SERVER_ERROR` on key-touching ops. - Wired into `apps/web/src/routers/admin-router.ts` as `trpc.admin.modelExperiments.*`. - `apps/web/src/app/admin/api/model-experiments/hooks.ts` — react-query hooks for every procedure. - `apps/web/src/app/admin/model-experiments/ModelExperimentsContent.tsx` — list + detail (inline) + create dialog + add-variant dialog + Monaco-based hot-swap dialog (validates `ExperimentUpstreamSchema` strict before submit) + rotate-key dialog. Status badges, share = `weight / sum(weights)`, structural-edit lock for non-draft. @@ -82,7 +82,7 @@ Operational consequence: admin mutations that move experiments into or out of ro **Phase 5 — deferred:** - `getLiveStats(id)` tRPC procedure — still deferred until a real aggregate reporting consumer needs a stable query/result shape. -- `getPromptByHash(sha)` tRPC procedure and full admin prompt inflation — R2 helpers exist, but the admin UI only renders existing prompt-prefix metadata plus the captured-body download action. +- `getPromptByHash(sha)` tRPC procedure and admin prompt inflation — R2 helpers exist, but the admin UI renders hashes/sentinels rather than inflating prompt content. > **Scope: preview/experimental models only.** This system exists to A/B test > unreleased model checkpoints in partnership with model providers. It is **not** @@ -102,17 +102,17 @@ Run A/B tests against model checkpoints in partnership with model providers, esp ### Accepted Design -| Area | Decision | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Experiment scope | One experiment targets one public model id (`public_id`) and swaps the upstream checkpoint (`internal_id`) behind it. Clients keep sending the same public model id. | -| Allocation | N variants with positive integer weights (no sum constraint). Bucketing is deterministic on the first available subject: `kilo_user_id` → `machine_id` → `client_ip`. Missing all allocation subjects is an invariant violation and returns temporarily unavailable. | -| Anonymous traffic | Anonymous / free-tier traffic is bucketed when `machine_id` or IP is available. `machine` and `ip` cohorts are less stable than authenticated `user` cohorts, so every request records `allocation_subject` for reporting filters. A request with no IP is treated as a gateway bug and fails closed. | -| Client blinding | Variant id is not disclosed to the client. No `x-kilo-experiment`, no `x-kilo-variant`, and no payload field. Provider reports receive aggregate variant/checkpoint labels only. | -| Checkpoint replacement | A provider may replace the upstream config (`internal_id`, `base_url`, `api_key`, transforms) on a live variant without ending the experiment, as long as variant slots and weights are unchanged. Users stay pinned to the same variant slot. | -| Structural edits | Adding/removing variants or changing weights is allowed only before activation. After activation, structural changes require a new experiment because they shift bucket ranges and corrupt longitudinal cohorts. Hot-swapping a checkpoint is not structural; it is a new `model_experiment_variant_version` row under an existing variant slot. | -| Per-request snapshot | Experimented requests get one row in `model_experiment_request`, keyed by `usage_id`. That row stores the exact checkpoint selected at routing time. Users are pinned to a variant slot, not necessarily to the same checkpoint forever; if variant A moves from `rc1` to `rc2`, old rows remain attributable to `rc1` and new rows to `rc2`. | -| Feedback attribution | Gateway stores `x-kilo-request` as `model_experiment_request.client_request_id`. PostHog `Feedback Submitted.parentMessageID` joins to that value, and the experiment request row carries the variant/checkpoint snapshot. | -| Storage | Experiment definitions and routing details live in Postgres. Gateway hot-path pre-checks use an admin-maintained Redis membership key plus a short in-process cache. | +| Area | Decision | +|---|---| +| Experiment scope | One experiment targets one public model id (`public_id`) and swaps the upstream checkpoint (`internal_id`) behind it. Clients keep sending the same public model id. | +| Allocation | N variants with positive integer weights (no sum constraint). Bucketing is deterministic on the first available subject: `kilo_user_id` → `machine_id` → `client_ip`. Missing all allocation subjects is an invariant violation and returns temporarily unavailable. | +| Anonymous traffic | Anonymous / free-tier traffic is bucketed when `machine_id` or IP is available. `machine` and `ip` cohorts are less stable than authenticated `user` cohorts, so every request records `allocation_subject` for reporting filters. A request with no IP is treated as a gateway bug and fails closed. | +| Client blinding | Variant id is not disclosed to the client. No `x-kilo-experiment`, no `x-kilo-variant`, and no payload field. Provider reports receive aggregate variant/checkpoint labels only. | +| Checkpoint replacement | A provider may replace the upstream config (`internal_id`, `base_url`, `api_key`, transforms) on a live variant without ending the experiment, as long as variant slots and weights are unchanged. Users stay pinned to the same variant slot. | +| Structural edits | Adding/removing variants or changing weights is allowed only before activation. After activation, structural changes require a new experiment because they shift bucket ranges and corrupt longitudinal cohorts. Hot-swapping a checkpoint is not structural; it is a new `model_experiment_variant_version` row under an existing variant slot. | +| Per-request snapshot | Experimented requests get one row in `model_experiment_request`, keyed by `usage_id`. That row stores the exact checkpoint selected at routing time. Users are pinned to a variant slot, not necessarily to the same checkpoint forever; if variant A moves from `rc1` to `rc2`, old rows remain attributable to `rc1` and new rows to `rc2`. | +| Feedback attribution | Gateway stores `x-kilo-request` as `model_experiment_request.client_request_id`. PostHog `Feedback Submitted.parentMessageID` joins to that value, and the experiment request row carries the variant/checkpoint snapshot. | +| Storage | Experiment definitions and routing details live in Postgres. Gateway hot-path pre-checks use an admin-maintained Redis membership key plus a short in-process cache. | ### Existing Building Blocks diff --git a/.plans/experimental-models-2.md b/.plans/experimental-models-2.md index 03bbe2558f..ce5da74e01 100644 --- a/.plans/experimental-models-2.md +++ b/.plans/experimental-models-2.md @@ -51,13 +51,13 @@ Consent handling: Replay bundles, SWE-bench/OpenHands adapters, and held-out replay-eval are out of scope for v1 because key capture artifacts do not exist yet. Required follow-on work lives mostly in the kilocode CLI repo: -| Gap | Required capture | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | -| Resolved agent config | `agent_snapshot` ingest item per user-message turn with canonical hash and `Agent.Info`. | -| Lossless part bodies | `part_raw` ingest path with checksum and no read-path stripping. | +| Gap | Required capture | +|---|---| +| Resolved agent config | `agent_snapshot` ingest item per user-message turn with canonical hash and `Agent.Info`. | +| Lossless part bodies | `part_raw` ingest path with checksum and no read-path stripping. | | Workspace reconstruction | `workspace_ref` at session start with sanitized git remote, base commit, branch, dirty-start diff, and touched paths. | -| Per-step replay | `step_diff` items with full file diffs, including patch text. | -| Retry analytics | Structured `RetryPart` producer rather than only session-level `"retry"` status. | +| Per-step replay | `step_diff` items with full file diffs, including patch text. | +| Retry analytics | Structured `RetryPart` producer rather than only session-level `"retry"` status. | Future packages/services: diff --git a/.plans/gastown-cloud-proposal-d.md b/.plans/gastown-cloud-proposal-d.md index 81968271a7..0b8ee01842 100644 --- a/.plans/gastown-cloud-proposal-d.md +++ b/.plans/gastown-cloud-proposal-d.md @@ -23,11 +23,11 @@ Gastown solves four problems that arise when deploying AI agents at engineering Gastown's design is governed by three foundational principles: -| Principle | Acronym | Meaning | -| ------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Molecular Expression of Work** | MEOW | Breaking large goals into detailed, trackable, atomic instructions for agents. Supported by Beads, Formulas, and Molecules. | -| **Gas Town Universal Propulsion Principle** | GUPP | "If there is work on your Hook, YOU MUST RUN IT." Agents autonomously proceed with available work without waiting for external input. The hook is your assignment — execute immediately. | -| **Nondeterministic Idempotence** | NDI | Useful outcomes are achieved through orchestration of potentially unreliable processes. Persistent Beads and oversight agents (Witness, Deacon) guarantee eventual workflow completion even when individual operations may fail or produce varying results. | +| Principle | Acronym | Meaning | +|---|---|---| +| **Molecular Expression of Work** | MEOW | Breaking large goals into detailed, trackable, atomic instructions for agents. Supported by Beads, Formulas, and Molecules. | +| **Gas Town Universal Propulsion Principle** | GUPP | "If there is work on your Hook, YOU MUST RUN IT." Agents autonomously proceed with available work without waiting for external input. The hook is your assignment — execute immediately. | +| **Nondeterministic Idempotence** | NDI | Useful outcomes are achieved through orchestration of potentially unreliable processes. Persistent Beads and oversight agents (Witness, Deacon) guarantee eventual workflow completion even when individual operations may fail or produce varying results. | Additionally: @@ -40,19 +40,19 @@ The fundamental data model is **Beads** — git-backed atomic work units stored Gastown uses a **two-level Beads architecture**: -| Level | Location | Prefix | Purpose | -| -------- | ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| **Town** | `~/gt/.beads/` | `hq-*` | Cross-rig coordination: Mayor mail, convoy tracking, strategic decisions, town-level agent beads, role definitions | -| **Rig** | `/mayor/rig/.beads/` | project prefix (e.g. `gt-`, `bd-`) | Implementation work: bugs, features, tasks, merge requests, project-specific molecules, rig-level agent beads | +| Level | Location | Prefix | Purpose | +|---|---|---|---| +| **Town** | `~/gt/.beads/` | `hq-*` | Cross-rig coordination: Mayor mail, convoy tracking, strategic decisions, town-level agent beads, role definitions | +| **Rig** | `/mayor/rig/.beads/` | project prefix (e.g. `gt-`, `bd-`) | Implementation work: bugs, features, tasks, merge requests, project-specific molecules, rig-level agent beads | **Beads routing** is prefix-based. The file `~/gt/.beads/routes.jsonl` maps issue ID prefixes to rig locations. When you run `bd show gt-xyz`, the prefix `gt-` routes to the gastown rig's beads database. This is transparent — agents don't need to know which database to use. ### 4. Environments: Towns and Rigs -| Concept | Description | -| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Town** | The management headquarters (e.g., `~/gt/`). A town coordinates all workers across multiple rigs. It houses town-level agents (Mayor, Deacon) and the town-level Beads database. | -| **Rig** | A project-specific git repository under Gastown management. Each rig has its own Polecats, Refinery, Witness, and Crew members. Rigs are where actual development work happens. The rig root is a _container directory_, not a git clone itself — it holds a bare repo (`.repo.git/`) from which worktrees are created. | +| Concept | Description | +|---|---| +| **Town** | The management headquarters (e.g., `~/gt/`). A town coordinates all workers across multiple rigs. It houses town-level agents (Mayor, Deacon) and the town-level Beads database. | +| **Rig** | A project-specific git repository under Gastown management. Each rig has its own Polecats, Refinery, Witness, and Crew members. Rigs are where actual development work happens. The rig root is a _container directory_, not a git clone itself — it holds a bare repo (`.repo.git/`) from which worktrees are created. | #### Directory Structure @@ -90,20 +90,20 @@ Gastown has seven distinct agent roles organized into two tiers: #### Town-Level Agents (Cross-Rig) -| Role | Description | Lifecycle | Location | -| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------- | -| **Mayor** | Global coordinator. Initiates convoys, distributes work across rigs, handles escalations, coordinates cross-rig communication. | Singleton, persistent | `~/gt/mayor/` | -| **Deacon** | Daemon beacon. Background supervisor running continuous patrol cycles. Monitors system health, ensures worker activity, triggers recovery. | Singleton, persistent | `~/gt/deacon/` | -| **Dogs** | The Deacon's helper agents for infrastructure tasks (NOT project work). Example: Boot (health triage dog). Dogs are lightweight Go routines or ephemeral AI sessions for narrow tasks. | Ephemeral, Deacon-managed | `~/gt/deacon/dogs/` | +| Role | Description | Lifecycle | Location | +|---|---|---|---| +| **Mayor** | Global coordinator. Initiates convoys, distributes work across rigs, handles escalations, coordinates cross-rig communication. | Singleton, persistent | `~/gt/mayor/` | +| **Deacon** | Daemon beacon. Background supervisor running continuous patrol cycles. Monitors system health, ensures worker activity, triggers recovery. | Singleton, persistent | `~/gt/deacon/` | +| **Dogs** | The Deacon's helper agents for infrastructure tasks (NOT project work). Example: Boot (health triage dog). Dogs are lightweight Go routines or ephemeral AI sessions for narrow tasks. | Ephemeral, Deacon-managed | `~/gt/deacon/dogs/` | #### Rig-Level Agents (Per-Project) -| Role | Description | Lifecycle | Location | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------------------------- | -| **Witness** | Per-rig polecat lifecycle manager. Monitors polecat health, nudges stuck workers, handles cleanup, triggers escalations. | One per rig, persistent | `/witness/` | -| **Refinery** | Per-rig merge queue processor. Intelligently merges changes from polecats, handles conflicts, runs quality gates, ensures code quality before changes reach main. | One per rig, persistent | `/refinery/rig/` | -| **Polecat** | Ephemeral worker agents that produce merge requests. Spawned for specific tasks, work in isolated git worktrees, submit to merge queue when done, then self-clean. There is **no idle state** — polecats are either working, stalled (crashed), or zombie (`gt done` failed). | Transient, Witness-managed | `/polecats//rig/` | -| **Crew** | Persistent worker agents for long-lived collaboration. Human-managed, no automatic monitoring. Push to main directly (no merge queue). | Long-lived, user-managed | `/crew//rig/` | +| Role | Description | Lifecycle | Location | +|---|---|---|---| +| **Witness** | Per-rig polecat lifecycle manager. Monitors polecat health, nudges stuck workers, handles cleanup, triggers escalations. | One per rig, persistent | `/witness/` | +| **Refinery** | Per-rig merge queue processor. Intelligently merges changes from polecats, handles conflicts, runs quality gates, ensures code quality before changes reach main. | One per rig, persistent | `/refinery/rig/` | +| **Polecat** | Ephemeral worker agents that produce merge requests. Spawned for specific tasks, work in isolated git worktrees, submit to merge queue when done, then self-clean. There is **no idle state** — polecats are either working, stalled (crashed), or zombie (`gt done` failed). | Transient, Witness-managed | `/polecats//rig/` | +| **Crew** | Persistent worker agents for long-lived collaboration. Human-managed, no automatic monitoring. Push to main directly (no merge queue). | Long-lived, user-managed | `/crew//rig/` | #### Key Distinctions @@ -114,21 +114,21 @@ Gastown has seven distinct agent roles organized into two tiers: Polecats have three distinct lifecycle layers that operate independently: -| Layer | Component | Lifecycle | Persistence | -| ----------- | -------------------------------------------- | ---------- | ----------------------- | -| **Session** | AI agent instance (e.g., Claude in tmux) | Ephemeral | Cycles per step/handoff | -| **Sandbox** | Git worktree (the working directory) | Persistent | Until nuke | -| **Slot** | Name from pool (Toast, Shadow, Copper, etc.) | Persistent | Until nuke | +| Layer | Component | Lifecycle | Persistence | +|---|---|---|---| +| **Session** | AI agent instance (e.g., Claude in tmux) | Ephemeral | Cycles per step/handoff | +| **Sandbox** | Git worktree (the working directory) | Persistent | Until nuke | +| **Slot** | Name from pool (Toast, Shadow, Copper, etc.) | Persistent | Until nuke | **Session cycling is normal operation**, not failure. A polecat may cycle through many sessions while working on a single task (via `gt handoff` between molecule steps, compaction triggers, or crash recovery). The sandbox and slot persist across all session cycles. **Polecat states** (there are exactly three — no idle state): -| State | Description | -| ----------- | ------------------------------------------------------------------- | -| **Working** | Actively doing assigned work | +| State | Description | +|---|---| +| **Working** | Actively doing assigned work | | **Stalled** | Session stopped mid-work (crashed/interrupted without being nudged) | -| **Zombie** | Completed work but failed to die (`gt done` failed during cleanup) | +| **Zombie** | Completed work but failed to die (`gt done` failed during cleanup) | **Lifecycle flow**: `gt sling` → allocate slot → create worktree → start session → hook molecule → work happens (with session cycling) → `gt done` → push branch → submit to merge queue → request self-nuke → polecat is gone. @@ -138,15 +138,15 @@ Polecats have three distinct lifecycle layers that operate independently: #### Work Units -| Concept | Description | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Bead** | Git-backed atomic work unit (JSONL). The fundamental tracking primitive. Can represent issues, tasks, messages, escalations, MRs, agent identity records. | -| **Hook** | A special pinned Bead for each agent — their current assignment. GUPP: if work is on your hook, you run it immediately. | -| **Formula** | TOML-based workflow source template. Defines reusable patterns for multi-step operations (e.g., polecat work, patrol cycles, code review). | -| **Protomolecule** | A frozen template created from a formula via `bd cook`. Ready for instantiation. | -| **Molecule** | A durable, active workflow instance with trackable steps. Each step is a Bead. Molecules survive agent restarts and ensure complex workflows complete. Created via `bd mol pour`. | -| **Wisp** | An ephemeral molecule for patrol cycles and operational loops. Never synced to persistent storage. Created via `bd mol wisp`. Used by patrol agents to avoid accumulating data. | -| **Convoy** | A persistent tracking unit that monitors related beads across multiple rigs. Convoys group related tasks and track progress to "landing" (all tracked issues closed). | +| Concept | Description | +|---|---| +| **Bead** | Git-backed atomic work unit (JSONL). The fundamental tracking primitive. Can represent issues, tasks, messages, escalations, MRs, agent identity records. | +| **Hook** | A special pinned Bead for each agent — their current assignment. GUPP: if work is on your hook, you run it immediately. | +| **Formula** | TOML-based workflow source template. Defines reusable patterns for multi-step operations (e.g., polecat work, patrol cycles, code review). | +| **Protomolecule** | A frozen template created from a formula via `bd cook`. Ready for instantiation. | +| **Molecule** | A durable, active workflow instance with trackable steps. Each step is a Bead. Molecules survive agent restarts and ensure complex workflows complete. Created via `bd mol pour`. | +| **Wisp** | An ephemeral molecule for patrol cycles and operational loops. Never synced to persistent storage. Created via `bd mol wisp`. Used by patrol agents to avoid accumulating data. | +| **Convoy** | A persistent tracking unit that monitors related beads across multiple rigs. Convoys group related tasks and track progress to "landing" (all tracked issues closed). | #### Molecule Lifecycle @@ -177,16 +177,16 @@ Agents navigate molecules with `bd mol current` (where am I?), `bd close Agents coordinate via typed mail messages routed through the Beads system. Key message types: -| Type | Route | Purpose | -| ---------------- | ---------------------------- | ------------------------------------------------ | -| `POLECAT_DONE` | Polecat → Witness | Signal work completion, trigger cleanup | -| `MERGE_READY` | Witness → Refinery | Branch ready for merge queue processing | -| `MERGED` | Refinery → Witness | Branch merged successfully, safe to nuke polecat | -| `MERGE_FAILED` | Refinery → Witness | Merge failed (tests/build), needs rework | -| `REWORK_REQUEST` | Refinery → Witness → Polecat | Rebase needed due to merge conflicts | -| `WITNESS_PING` | Witness → Deacon | Second-order monitoring (ensure Deacon is alive) | -| `HELP` | Any → Mayor | Request intervention for stuck/blocked work | -| `HANDOFF` | Agent → self | Session continuity data across context limits | +| Type | Route | Purpose | +|---|---|---| +| `POLECAT_DONE` | Polecat → Witness | Signal work completion, trigger cleanup | +| `MERGE_READY` | Witness → Refinery | Branch ready for merge queue processing | +| `MERGED` | Refinery → Witness | Branch merged successfully, safe to nuke polecat | +| `MERGE_FAILED` | Refinery → Witness | Merge failed (tests/build), needs rework | +| `REWORK_REQUEST` | Refinery → Witness → Polecat | Rebase needed due to merge conflicts | +| `WITNESS_PING` | Witness → Deacon | Second-order monitoring (ensure Deacon is alive) | +| `HELP` | Any → Mayor | Request intervention for stuck/blocked work | +| `HANDOFF` | Agent → self | Session continuity data across context limits | Mail addresses use slash-separated path format: `gastown/witness`, `gastown/polecats/toast`, `mayor/`, `deacon/`. @@ -206,14 +206,14 @@ Advanced messaging primitives: All work is attributed to the agent who performed it via the `BD_ACTOR` environment variable: -| Role | BD_ACTOR Format | Example | -| -------- | ----------------------- | ------------------------ | -| Mayor | `mayor` | `mayor` | -| Deacon | `deacon` | `deacon` | -| Witness | `{rig}/witness` | `gastown/witness` | -| Refinery | `{rig}/refinery` | `gastown/refinery` | -| Crew | `{rig}/crew/{name}` | `gastown/crew/joe` | -| Polecat | `{rig}/polecats/{name}` | `gastown/polecats/toast` | +| Role | BD_ACTOR Format | Example | +|---|---|---| +| Mayor | `mayor` | `mayor` | +| Deacon | `deacon` | `deacon` | +| Witness | `{rig}/witness` | `gastown/witness` | +| Refinery | `{rig}/refinery` | `gastown/refinery` | +| Crew | `{rig}/crew/{name}` | `gastown/crew/joe` | +| Polecat | `{rig}/polecats/{name}` | `gastown/polecats/toast` | Attribution flows through: @@ -245,12 +245,12 @@ Daemon (Go process) ← Dumb transport, 3-min heartbeat tick **Boot decision matrix**: -| Condition | Action | -| --------------------------------- | -------------------------------------------------- | -| Deacon session dead | START (exit; daemon calls `ensureDeaconRunning()`) | -| Heartbeat > 15 min | WAKE (nudge Deacon) | -| Heartbeat 5–15 min + pending mail | NUDGE (send check-in) | -| Heartbeat fresh | NOTHING (exit silently) | +| Condition | Action | +|---|---| +| Deacon session dead | START (exit; daemon calls `ensureDeaconRunning()`) | +| Heartbeat > 15 min | WAKE (nudge Deacon) | +| Heartbeat 5–15 min + pending mail | NUDGE (send check-in) | +| Heartbeat fresh | NOTHING (exit silently) | **Why two AI agents?** The Deacon can't observe itself (a hung Deacon can't detect it's hung). Boot provides an external observer with fresh context each tick. @@ -270,11 +270,11 @@ Convoys are the primary unit for tracking batched work across rigs. Even a singl Severity-routed escalation with tiered routing and auto-re-escalation: -| Severity | Default Route | -| ---------- | ------------------------------------- | -| `low` | Bead only (record) | -| `medium` | Bead + mail Mayor | -| `high` | Bead + mail Mayor + email human | +| Severity | Default Route | +|---|---| +| `low` | Bead only (record) | +| `medium` | Bead + mail Mayor | +| `high` | Bead + mail Mayor + email human | | `critical` | Bead + mail Mayor + email + SMS human | **Escalation categories**: `decision`, `help`, `blocked`, `failed`, `emergency`, `gate_timeout`, `lifecycle`. @@ -338,9 +338,9 @@ Event types include: `patrol.muted`, `patrol.unmuted`, `agent.started`, `agent.s Agent context (role instructions, environment) is delivered via two mechanisms: -| Method | Roles | How | -| -------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------- | -| **On-disk CLAUDE.md** | Mayor, Refinery | Written to the agent's working directory inside the git worktree | +| Method | Roles | How | +|---|---|---| +| **On-disk CLAUDE.md** | Mayor, Refinery | Written to the agent's working directory inside the git worktree | | **Ephemeral injection via `gt prime`** | Deacon, Witness, Crew, Polecats | Injected at `SessionStart` hook. Not persisted to disk to avoid polluting source repos. | **Sparse checkout** is used to exclude context files (`.claude/`, `CLAUDE.md`, `.mcp.json`) from source repos, ensuring agents use Gastown's context rather than the project's. @@ -1012,24 +1012,24 @@ cloud/cloudflare-gastown/plugin/ #### Tools (Phase 1 — minimum viable set) -| Tool | Description | Rig DO Method | -| ---------------- | ------------------------------------------------------------------------ | -------------------------------- | -| `gt_prime` | Get full role context: identity, hooked work, instructions, pending mail | `prime(agentId)` | -| `gt_bead_status` | Read the status of a bead | `getBeadAsync(beadId)` | -| `gt_bead_close` | Close current bead or molecule step | `closeBead(beadId)` | -| `gt_done` | Signal work complete — push branch, submit to review queue | `agentDone(agentId, ...)` | -| `gt_mail_send` | Send a typed message to another agent | `sendMail(...)` | -| `gt_mail_check` | Read and acknowledge pending mail | `checkMail(agentId)` | -| `gt_escalate` | Escalate an issue with severity and category | `createBead(type: 'escalation')` | -| `gt_checkpoint` | Write crash-recovery data | `writeCheckpoint(agentId, ...)` | +| Tool | Description | Rig DO Method | +|---|---|---| +| `gt_prime` | Get full role context: identity, hooked work, instructions, pending mail | `prime(agentId)` | +| `gt_bead_status` | Read the status of a bead | `getBeadAsync(beadId)` | +| `gt_bead_close` | Close current bead or molecule step | `closeBead(beadId)` | +| `gt_done` | Signal work complete — push branch, submit to review queue | `agentDone(agentId, ...)` | +| `gt_mail_send` | Send a typed message to another agent | `sendMail(...)` | +| `gt_mail_check` | Read and acknowledge pending mail | `checkMail(agentId)` | +| `gt_escalate` | Escalate an issue with severity and category | `createBead(type: 'escalation')` | +| `gt_checkpoint` | Write crash-recovery data | `writeCheckpoint(agentId, ...)` | #### Plugin Event Hooks -| Event | Action | -| ------------------- | -------------------------------------------------------------------- | -| `session.created` | Auto-call `gt_prime` and inject result into session context | -| `session.compacted` | Re-call `gt_prime` to restore context after compaction | -| `session.deleted` | Notify Rig DO that the session has ended (for cleanup/cost tracking) | +| Event | Action | +|---|---| +| `session.created` | Auto-call `gt_prime` and inject result into session context | +| `session.compacted` | Re-call `gt_prime` to restore context after compaction | +| `session.deleted` | Notify Rig DO that the session has ended (for cleanup/cost tracking) | #### Changes from original proposal @@ -1037,13 +1037,13 @@ The plugin is unchanged in its tool definitions and event hooks. The difference #### Environment Variables (set by the container's control server when spawning a Kilo CLI process) -| Var | Value | -| ----------------------- | --------------------------------------------------- | -| `GASTOWN_API_URL` | Worker URL: `https://gastown..workers.dev` | -| `GASTOWN_SESSION_TOKEN` | Short-lived JWT for this agent session | -| `GASTOWN_AGENT_ID` | This agent's UUID | -| `GASTOWN_RIG_ID` | This rig's UUID | -| `KILO_API_URL` | Kilo gateway URL (for LLM calls) | +| Var | Value | +|---|---| +| `GASTOWN_API_URL` | Worker URL: `https://gastown..workers.dev` | +| `GASTOWN_SESSION_TOKEN` | Short-lived JWT for this agent session | +| `GASTOWN_AGENT_ID` | This agent's UUID | +| `GASTOWN_RIG_ID` | This rig's UUID | +| `KILO_API_URL` | Kilo gateway URL (for LLM calls) | --- @@ -1445,14 +1445,14 @@ Each agent becomes a **session** within a `kilo serve` instance rather than its #### Component Changes -| Component | Current | After | -| ----------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------- | -| `process-manager.ts` | Raw `Bun.spawn` child process management | `kilo serve` instances via SDK, session-based agent tracking | -| `agent-runner.ts` | Builds CLI args for `kilo code --non-interactive` | Creates sessions on running server, sends initial prompt via HTTP | -| `control-server.ts` `/agents/start` | Spawns a process | Creates a session on an existing (or new) server instance | -| `control-server.ts` `/agents/:id/message` | Writes to stdin pipe | `POST /session/:id/message` | -| `control-server.ts` `/agents/:id/status` | Process lifecycle (pid, exit code) | Session-level status with tool/message detail | -| `heartbeat.ts` | Reports process alive/dead | Reports session status + active tool calls from SSE events | +| Component | Current | After | +|---|---|---| +| `process-manager.ts` | Raw `Bun.spawn` child process management | `kilo serve` instances via SDK, session-based agent tracking | +| `agent-runner.ts` | Builds CLI args for `kilo code --non-interactive` | Creates sessions on running server, sends initial prompt via HTTP | +| `control-server.ts` `/agents/start` | Spawns a process | Creates a session on an existing (or new) server instance | +| `control-server.ts` `/agents/:id/message` | Writes to stdin pipe | `POST /session/:id/message` | +| `control-server.ts` `/agents/:id/status` | Process lifecycle (pid, exit code) | Session-level status with tool/message detail | +| `heartbeat.ts` | Reports process alive/dead | Reports session status + active tool calls from SSE events | #### What Stays the Same @@ -1588,10 +1588,10 @@ export const gastownRouter = router({ #### Pages -| Route | Component | Purpose | -| -------------------------------- | ---------- | --------------------------------------------------------- | -| `/gastown` | Town list | List user's towns, create new town | -| `/gastown/[townId]` | Town home | Split view: Mayor chat (left) + town dashboard (right) | +| Route | Component | Purpose | +|---|---|---| +| `/gastown` | Town list | List user's towns, create new town | +| `/gastown/[townId]` | Town home | Split view: Mayor chat (left) + town dashboard (right) | | `/gastown/[townId]/rigs/[rigId]` | Rig detail | Bead board, agent roster, merge queue, agent stream panel | #### Town Home — Mayor Chat + Dashboard @@ -1648,15 +1648,15 @@ New tRPC subscription or SSE endpoint: `GET /api/towns/:townId/events` #### New tRPC Procedures Needed -| Procedure | Type | Purpose | -| ----------------------- | ---------------- | ------------------------------------------ | -| `getConvoys` | query | List convoys for a town (with bead counts) | -| `getConvoy` | query | Single convoy with all tracked beads | -| `getBeadEvents` | query | Append-only event history for a bead | -| `getAgentHistory` | query | Completed beads for an agent (CV) | -| `getAgentMail` | query | Recent mail for an agent | -| `getTownEvents` | subscription/SSE | Real-time event stream for the town | -| `acknowledgeEscalation` | mutation | Mark escalation as acknowledged | +| Procedure | Type | Purpose | +|---|---|---| +| `getConvoys` | query | List convoys for a town (with bead counts) | +| `getConvoy` | query | Single convoy with all tracked beads | +| `getBeadEvents` | query | Append-only event history for a bead | +| `getAgentHistory` | query | Completed beads for an agent (CV) | +| `getAgentMail` | query | Recent mail for an agent | +| `getTownEvents` | subscription/SSE | Real-time event stream for the town | +| `acknowledgeEscalation` | mutation | Mark escalation as acknowledged | --- @@ -1831,12 +1831,12 @@ A new **Settings** page in the town sidebar (`/gastown/[townId]/settings`): #### tRPC Procedures -| Procedure | Type | Purpose | -| ------------------- | -------- | ---------------------------------------- | -| `getTownConfig` | query | Read town configuration | -| `updateTownConfig` | mutation | Update town-level config (partial merge) | -| `getAgentConfig` | query | Read agent-level overrides | -| `updateAgentConfig` | mutation | Update per-agent overrides | +| Procedure | Type | Purpose | +|---|---|---| +| `getTownConfig` | query | Read town configuration | +| `updateTownConfig` | mutation | Update town-level config (partial merge) | +| `getAgentConfig` | query | Read agent-level overrides | +| `updateAgentConfig` | mutation | Update per-agent overrides | #### Security @@ -1991,14 +1991,14 @@ New DO binding `MAYOR` for `MayorDO`, new migration tag `v3`. #### Tools -| Tool | Description | Proxies to | -| ------------------ | ------------------------------------------- | ----------------------------- | -| `gt_sling` | Sling a task to a polecat in a specific rig | `RigDO.slingBead(rigId, ...)` | -| `gt_list_rigs` | List all rigs in the town | `GastownUserDO.listRigs()` | -| `gt_list_beads` | List beads in a rig (filterable by status) | `RigDO.listBeads(filter)` | -| `gt_list_agents` | List agents in a rig | `RigDO.listAgents(filter)` | -| `gt_mail_send` | Send mail to an agent in any rig | `RigDO.sendMail(...)` | -| `gt_convoy_create` | Create a convoy tracking multiple beads | Future — convoy system | +| Tool | Description | Proxies to | +|---|---|---| +| `gt_sling` | Sling a task to a polecat in a specific rig | `RigDO.slingBead(rigId, ...)` | +| `gt_list_rigs` | List all rigs in the town | `GastownUserDO.listRigs()` | +| `gt_list_beads` | List beads in a rig (filterable by status) | `RigDO.listBeads(filter)` | +| `gt_list_agents` | List agents in a rig | `RigDO.listAgents(filter)` | +| `gt_mail_send` | Send mail to an agent in any rig | `RigDO.sendMail(...)` | +| `gt_convoy_create` | Create a convoy tracking multiple beads | Future — convoy system | Tools are HTTP endpoints on the Gastown worker, called by the mayor's kilo serve process using `GASTOWN_SESSION_TOKEN` for auth. The mayor's system prompt describes available tools and when to use them. @@ -2048,10 +2048,10 @@ The refinery agent can reason about test failures — if tests fail, it can exam #### New Tools (added to plugin) -| Tool | Description | -| ---------------- | ------------------------------------------------------------ | +| Tool | Description | +|---|---| | `gt_mol_current` | Get current molecule step (title, instructions, step N of M) | -| `gt_mol_advance` | Complete current step with summary, advance to next | +| `gt_mol_advance` | Complete current step with summary, advance to next | --- @@ -2075,11 +2075,11 @@ The refinery agent can reason about test failures — if tests fail, it can exam #### Severity Routing -| Severity | Action | -| ---------- | ----------------------------------------- | -| `low` | Record in bead events only | -| `medium` | + send mail to Mayor agent | -| `high` | + webhook to user (email/Slack) | +| Severity | Action | +|---|---| +| `low` | Record in bead events only | +| `medium` | + send mail to Mayor agent | +| `high` | + webhook to user (email/Slack) | | `critical` | + mark convoy as blocked, alert dashboard | #### Auto-Re-Escalation @@ -2394,23 +2394,23 @@ The product vision requires a live activity feed showing "beads created, agents ### Assessment Summary -| Aspect | Status | Verdict | -| --------------------------------------------------------- | -------------- | -------------------------------------------------------------- | -| Core loop (sling → alarm → dispatch → agent works → done) | ✅ Implemented | Works, needs Refinery endpoint to close the loop | -| Rig DO state machine | ✅ Solid | Production-quality, well-tested | -| Container + kilo serve | ✅ Solid | Fully adopted, clean architecture | -| Tool plugin | ✅ Complete | 8 tools, good tests, security boundaries | -| Mayor persistent session | ✅ Working | Session lifecycle, health monitoring | -| Mayor tools (delegation) | ❌ Missing | #339 — highest priority for product vision | -| Agent streaming to browser | ❌ Incomplete | Stream tickets exist but no WebSocket/SSE endpoint serves them | -| Refinery / merge flow | ❌ Missing | Container has no `/merge` endpoint; review queue is a dead end | -| Witness as agent | ⚠️ Alarm-only | Works mechanically but not transparent/observable | -| Town DO / convoys | ❌ Missing | No cross-rig coordination, no convoy tracking | -| Event log for dashboard | ❌ Missing | No append-only event stream for real-time feed | -| Postgres read replica | ❌ Not started | All reads go through DO RPCs | -| Molecules | ⚠️ Schema only | Table exists, no business logic | -| Polecat system prompt | ⚠️ Not wired | Detailed prompt exists but isn't used | -| Identity / attribution | ⚠️ Partial | `GIT_AUTHOR_NAME` is set but no CV / AgentIdentity tracking | +| Aspect | Status | Verdict | +|---|---|---| +| Core loop (sling → alarm → dispatch → agent works → done) | ✅ Implemented | Works, needs Refinery endpoint to close the loop | +| Rig DO state machine | ✅ Solid | Production-quality, well-tested | +| Container + kilo serve | ✅ Solid | Fully adopted, clean architecture | +| Tool plugin | ✅ Complete | 8 tools, good tests, security boundaries | +| Mayor persistent session | ✅ Working | Session lifecycle, health monitoring | +| Mayor tools (delegation) | ❌ Missing | #339 — highest priority for product vision | +| Agent streaming to browser | ❌ Incomplete | Stream tickets exist but no WebSocket/SSE endpoint serves them | +| Refinery / merge flow | ❌ Missing | Container has no `/merge` endpoint; review queue is a dead end | +| Witness as agent | ⚠️ Alarm-only | Works mechanically but not transparent/observable | +| Town DO / convoys | ❌ Missing | No cross-rig coordination, no convoy tracking | +| Event log for dashboard | ❌ Missing | No append-only event stream for real-time feed | +| Postgres read replica | ❌ Not started | All reads go through DO RPCs | +| Molecules | ⚠️ Schema only | Table exists, no business logic | +| Polecat system prompt | ⚠️ Not wired | Detailed prompt exists but isn't used | +| Identity / attribution | ⚠️ Partial | `GIT_AUTHOR_NAME` is set but no CV / AgentIdentity tracking | ### Recommended Priority Adjustments diff --git a/.plans/gastown-town-centric-refactor.md b/.plans/gastown-town-centric-refactor.md index 5034a4b2e8..56949c70eb 100644 --- a/.plans/gastown-town-centric-refactor.md +++ b/.plans/gastown-town-centric-refactor.md @@ -102,23 +102,23 @@ Everything currently in the Rig DO's SQLite moves to Town DO's SQLite, scoped by #### From Rig DO → Town DO -| Table | Key Change | -| ------------------ | -------------------------------------------------------------- | -| `rig_beads` | Add `rig_id TEXT NOT NULL` column, index on `(rig_id, status)` | -| `rig_agents` | Add `rig_id TEXT NOT NULL` column, index on `(rig_id, role)` | -| `rig_mail` | Already scoped via agent FKs — no change needed | -| `rig_review_queue` | Add `rig_id TEXT NOT NULL` for queries | -| `rig_molecules` | Already scoped via bead FK — no change needed | -| `rig_bead_events` | Already scoped via bead FK — no change needed | -| `rig_agent_events` | **Moves to AgentDO** (see below) — not in Town DO | +| Table | Key Change | +|---|---| +| `rig_beads` | Add `rig_id TEXT NOT NULL` column, index on `(rig_id, status)` | +| `rig_agents` | Add `rig_id TEXT NOT NULL` column, index on `(rig_id, role)` | +| `rig_mail` | Already scoped via agent FKs — no change needed | +| `rig_review_queue` | Add `rig_id TEXT NOT NULL` for queries | +| `rig_molecules` | Already scoped via bead FK — no change needed | +| `rig_bead_events` | Already scoped via bead FK — no change needed | +| `rig_agent_events` | **Moves to AgentDO** (see below) — not in Town DO | The Town DO already has `town_convoys`, `town_convoy_beads`, `town_escalations`. These stay. #### From Mayor DO → Town DO -| Data | Migration | -| ----------------- | --------------------------------------------------------------------------------------------------------------- | -| `mayorConfig` KV | Merged into town config KV (it's mostly redundant — townId, gitUrl, etc.) | +| Data | Migration | +|---|---| +| `mayorConfig` KV | Merged into town config KV (it's mostly redundant — townId, gitUrl, etc.) | | `mayorSession` KV | New `mayor_session` KV key in Town DO, or just tracked as a special agent in `rig_agents` with `role = 'mayor'` | The Mayor becomes just another agent row in `rig_agents` with `role = 'mayor'` and a synthetic `rig_id` (e.g., `mayor-{townId}`). Its session state (agentId, sessionId, status, lastActivityAt) maps directly to the agent table's existing columns. @@ -331,28 +331,28 @@ No buffering needed. Late-joining clients get a backfill from the AgentDO (which ### Files to Delete -| File | Reason | -| ------------------------------- | ----------------------------------- | -| `container/src/kilo-server.ts` | Replaced by SDK `createOpencode()` | -| `container/src/kilo-client.ts` | Replaced by SDK client | +| File | Reason | +|---|---| +| `container/src/kilo-server.ts` | Replaced by SDK `createOpencode()` | +| `container/src/kilo-client.ts` | Replaced by SDK client | | `container/src/sse-consumer.ts` | Replaced by SDK `event.subscribe()` | ### Files to Heavily Refactor -| File | Changes | -| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `container/src/process-manager.ts` | Remove ring buffers, SSE consumers, event buffering. Becomes a thin map of agentId → { client, server, session } | -| `container/src/agent-runner.ts` | Use `createOpencode()` instead of `ensureServer()`. Simplify `buildAgentEnv()` since config is passed to SDK directly | -| `container/src/control-server.ts` | Remove `/agents/:id/events` polling endpoint, stream-ticket endpoints. Add WebSocket endpoint | -| `container/src/types.ts` | Remove SSE event Zod schemas (`SSESessionEvent`, `SSEMessageEvent`, etc.), `KiloServerInstance`, `BufferedEvent` | +| File | Changes | +|---|---| +| `container/src/process-manager.ts` | Remove ring buffers, SSE consumers, event buffering. Becomes a thin map of agentId → { client, server, session } | +| `container/src/agent-runner.ts` | Use `createOpencode()` instead of `ensureServer()`. Simplify `buildAgentEnv()` since config is passed to SDK directly | +| `container/src/control-server.ts` | Remove `/agents/:id/events` polling endpoint, stream-ticket endpoints. Add WebSocket endpoint | +| `container/src/types.ts` | Remove SSE event Zod schemas (`SSESessionEvent`, `SSEMessageEvent`, etc.), `KiloServerInstance`, `BufferedEvent` | ### Files to Keep (mostly unchanged) -| File | Notes | -| ------------------------------ | ------------------------------------------------------------------- | -| `container/src/git-manager.ts` | Git operations don't change | -| `container/src/heartbeat.ts` | Simplify — may not need per-agent heartbeats if events flow over WS | -| `container/src/main.ts` | Still starts control server | +| File | Notes | +|---|---| +| `container/src/git-manager.ts` | Git operations don't change | +| `container/src/heartbeat.ts` | Simplify — may not need per-agent heartbeats if events flow over WS | +| `container/src/main.ts` | Still starts control server | --- @@ -486,16 +486,16 @@ export class TownContainerDO extends Container { ### What Gets Eliminated -| Component | Status | -| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | -| Stream ticket system (`streamTickets` map, `consumeStreamTicket()`, `POST /stream-ticket`) | **Deleted** | -| `GET /agents/:id/events?after=N` polling endpoint | **Deleted** | -| Ring buffer in `process-manager.ts` (`agentEventBuffers`, `MAX_BUFFERED_EVENTS`, `bufferAgentEvent()`) | **Deleted** | -| `TownContainerDO.pollEvents()` (500ms setInterval) | **Deleted** | -| `TownContainerDO.backfillEvents()` via HTTP | **Replaced** with historical query from AgentDO | -| `gastown-router.ts` `getAgentStreamUrl` (ticket fetching) | **Replaced** with direct WS URL | -| `gastown-client.ts` `getStreamTicket()` | **Deleted** | -| `town-container.handler.ts` `handleContainerStreamTicket()` | **Deleted** | +| Component | Status | +|---|---| +| Stream ticket system (`streamTickets` map, `consumeStreamTicket()`, `POST /stream-ticket`) | **Deleted** | +| `GET /agents/:id/events?after=N` polling endpoint | **Deleted** | +| Ring buffer in `process-manager.ts` (`agentEventBuffers`, `MAX_BUFFERED_EVENTS`, `bufferAgentEvent()`) | **Deleted** | +| `TownContainerDO.pollEvents()` (500ms setInterval) | **Deleted** | +| `TownContainerDO.backfillEvents()` via HTTP | **Replaced** with historical query from AgentDO | +| `gastown-router.ts` `getAgentStreamUrl` (ticket fetching) | **Replaced** with direct WS URL | +| `gastown-client.ts` `getStreamTicket()` | **Deleted** | +| `town-container.handler.ts` `handleContainerStreamTicket()` | **Deleted** | ### Browser Connection diff --git a/.plans/kiloclaw-billing-credits.md b/.plans/kiloclaw-billing-credits.md index 33abb1985e..8a62a60784 100644 --- a/.plans/kiloclaw-billing-credits.md +++ b/.plans/kiloclaw-billing-credits.md @@ -6,12 +6,12 @@ Every KiloClaw instance is funded by credits. A KiloClaw subscription is a recurring credit deduction tied to a specific instance. Credits reach the user's balance through one of three funding sources: -| Funding source | Mechanism | Kilo Pass bonuses? | -| ----------------------- | ------------------------------------------------------ | ------------------------------- | -| **Kilo Pass** (primary) | Kilo Pass subscription adds credits via Stripe | Yes | -| **Standalone hosting** | Stripe subscription ($9/mo or $48/6mo), routed through | Only if user also has Kilo Pass | -| | credits internally (deposit + deduct, balance neutral) | | -| **Manual top-up** | User buys credits ad hoc | Only if user also has Kilo Pass | +| Funding source | Mechanism | Kilo Pass bonuses? | +|---|---|---| +| **Kilo Pass** (primary) | Kilo Pass subscription adds credits via Stripe | Yes | +| **Standalone hosting** | Stripe subscription ($9/mo or $48/6mo), routed through | Only if user also has Kilo Pass | +| | credits internally (deposit + deduct, balance neutral) | | +| **Manual top-up** | User buys credits ad hoc | Only if user also has Kilo Pass | The KiloClaw subscription itself does not know or care where credits came from. It deducts from the balance on each renewal. diff --git a/.plans/kiloclaw-billing-promo-codes.md b/.plans/kiloclaw-billing-promo-codes.md index 4ef07f76c1..b99e859454 100644 --- a/.plans/kiloclaw-billing-promo-codes.md +++ b/.plans/kiloclaw-billing-promo-codes.md @@ -301,12 +301,12 @@ The existing test file's setup and mocks must be updated for the new exports, or ## Files Changed -| File | Change | -| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/lib/config.server.ts` | Add intro price env var, remove coupon env var | -| `src/lib/kiloclaw/stripe-price-ids.server.ts` | Add intro price to metadata map, new `getStripePriceIdForClawPlanIntro`, new `isIntroPriceId` | -| `src/routers/kiloclaw-router.ts` | Checkout: intro price + `allow_promotion_codes`. switchPlan: handle auto schedule with stale-schedule defense. cancelPlanSwitch: restore auto schedule on intro price. reactivateSubscription: recreate auto schedule | -| `src/lib/kiloclaw/stripe-handlers.ts` | New `ensureAutoIntroSchedule` shared idempotent helper (DB + live Stripe fetch guard). Called from `handleKiloClawSubscriptionCreated`, `reactivateSubscription`, and `cancelPlanSwitch`. Update comments | -| `src/lib/kiloclaw/billing-lifecycle-cron.ts` | New sweep: detect and repair stranded intro-price subscriptions with no attached schedule | -| `src/app/(app)/claw/components/billing/SubscriptionCard.tsx` | Gate switch button on `scheduledBy === 'user'` not `scheduledPlan` presence | -| `src/routers/kiloclaw-billing-router.test.ts` | Update checkout assertions, add lifecycle tests for auto-schedule, cancel/reactivate, switchPlan, cancelPlanSwitch, invoice classification, reconciliation sweep | +| File | Change | +|---|---| +| `src/lib/config.server.ts` | Add intro price env var, remove coupon env var | +| `src/lib/kiloclaw/stripe-price-ids.server.ts` | Add intro price to metadata map, new `getStripePriceIdForClawPlanIntro`, new `isIntroPriceId` | +| `src/routers/kiloclaw-router.ts` | Checkout: intro price + `allow_promotion_codes`. switchPlan: handle auto schedule with stale-schedule defense. cancelPlanSwitch: restore auto schedule on intro price. reactivateSubscription: recreate auto schedule | +| `src/lib/kiloclaw/stripe-handlers.ts` | New `ensureAutoIntroSchedule` shared idempotent helper (DB + live Stripe fetch guard). Called from `handleKiloClawSubscriptionCreated`, `reactivateSubscription`, and `cancelPlanSwitch`. Update comments | +| `src/lib/kiloclaw/billing-lifecycle-cron.ts` | New sweep: detect and repair stranded intro-price subscriptions with no attached schedule | +| `src/app/(app)/claw/components/billing/SubscriptionCard.tsx` | Gate switch button on `scheduledBy === 'user'` not `scheduledPlan` presence | +| `src/routers/kiloclaw-billing-router.test.ts` | Update checkout assertions, add lifecycle tests for auto-schedule, cancel/reactivate, switchPlan, cancelPlanSwitch, invoice classification, reconciliation sweep | diff --git a/.plans/kiloclaw-credits-testing.md b/.plans/kiloclaw-credits-testing.md index 320a210c89..8ec1ecb4e9 100644 --- a/.plans/kiloclaw-credits-testing.md +++ b/.plans/kiloclaw-credits-testing.md @@ -2,15 +2,15 @@ ## Prerequisites -| Requirement | Details | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| **Environment** | Local dev (`localhost:3000`) with Stripe test mode | -| **Billing enforcement** | `KILOCLAW_BILLING_ENFORCEMENT=true` | -| **Stripe price IDs** | `STRIPE_KILOCLAW_COMMIT_PRICE_ID`, `STRIPE_KILOCLAW_STANDARD_PRICE_ID`, `STRIPE_KILOCLAW_STANDARD_INTRO_PRICE_ID` all configured | -| **Test card** | Stripe test card `4242 4242 4242 4242` (success), `4000 0000 0000 0341` (decline) | -| **Fake users** | Create via `/users/sign_in?fakeUser=...` per the dev login flow | -| **DB access** | Direct DB access to inspect `kiloclaw_subscriptions`, `credit_transactions`, `kiloclaw_email_log` tables | -| **Cron endpoint** | Ability to manually trigger the billing lifecycle cron (POST to the cron endpoint with the auth secret) | +| Requirement | Details | +|---|---| +| **Environment** | Local dev (`localhost:3000`) with Stripe test mode | +| **Billing enforcement** | `KILOCLAW_BILLING_ENFORCEMENT=true` | +| **Stripe price IDs** | `STRIPE_KILOCLAW_COMMIT_PRICE_ID`, `STRIPE_KILOCLAW_STANDARD_PRICE_ID`, `STRIPE_KILOCLAW_STANDARD_INTRO_PRICE_ID` all configured | +| **Test card** | Stripe test card `4242 4242 4242 4242` (success), `4000 0000 0000 0341` (decline) | +| **Fake users** | Create via `/users/sign_in?fakeUser=...` per the dev login flow | +| **DB access** | Direct DB access to inspect `kiloclaw_subscriptions`, `credit_transactions`, `kiloclaw_email_log` tables | +| **Cron endpoint** | Ability to manually trigger the billing lifecycle cron (POST to the cron endpoint with the auth secret) | For each test, use a fresh fake user unless noted. Name users descriptively (e.g., `kilo-trial-test-...@example.com`). @@ -190,17 +190,17 @@ For each test, use a fresh fake user unless noted. Name users descriptively (e.g For each user state, verify `getBillingStatus` includes the correct fields: -| State | Expected | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Trial active** | `hasAccess: true`, `accessReason: 'trial'`, trial data present | -| **Trial expired** | `hasAccess: false`, `trial.expired: true` | -| **Active subscription (pure credit)** | `hasAccess: true`, `accessReason: 'subscription'`, subscription data includes `paymentSource: 'credits'`, `hasStripeFunding: false`, `creditRenewalAt` set, `renewalCostMicrodollars` set | -| **Active subscription (hybrid)** | `hasStripeFunding: true`, `paymentSource: 'credits'` | -| **Active subscription (legacy Stripe)** | `hasStripeFunding: true`, `paymentSource: 'stripe'` | -| **Past-due** | `hasAccess: true` (within 14 days), subscription `status: 'past_due'` | -| **Canceled** | `hasAccess: false`, subscription `status: 'canceled'` | -| **Earlybird** | `hasAccess: true`, `accessReason: 'earlybird'`, earlybird data present | -| **No access** | `hasAccess: false`, `accessReason: null` | +| State | Expected | +|---|---| +| **Trial active** | `hasAccess: true`, `accessReason: 'trial'`, trial data present | +| **Trial expired** | `hasAccess: false`, `trial.expired: true` | +| **Active subscription (pure credit)** | `hasAccess: true`, `accessReason: 'subscription'`, subscription data includes `paymentSource: 'credits'`, `hasStripeFunding: false`, `creditRenewalAt` set, `renewalCostMicrodollars` set | +| **Active subscription (hybrid)** | `hasStripeFunding: true`, `paymentSource: 'credits'` | +| **Active subscription (legacy Stripe)** | `hasStripeFunding: true`, `paymentSource: 'stripe'` | +| **Past-due** | `hasAccess: true` (within 14 days), subscription `status: 'past_due'` | +| **Canceled** | `hasAccess: false`, subscription `status: 'canceled'` | +| **Earlybird** | `hasAccess: true`, `accessReason: 'earlybird'`, earlybird data present | +| **No access** | `hasAccess: false`, `accessReason: null` | ### 5.2 — Stripe-funding indicator @@ -459,16 +459,16 @@ For each user state, verify `getBillingStatus` includes the correct fields: ### 12.1 — Access gate hierarchy -| User State | Expected | -| ----------------------------------- | -------------------------------------------------------- | -| Active subscription | ✅ Access | -| Past-due (< 14 days, not suspended) | ✅ Access | -| Trialing (not expired) | ✅ Access | -| Earlybird (not expired) | ✅ Access | -| Expired trial | ❌ Forbidden — "Your Trial Has Ended" | -| Expired earlybird | ❌ Forbidden — "Earlybird Hosting Expired" | -| Canceled subscription | ❌ Forbidden — "Subscription Ended" | -| Past-due + suspended | ❌ Forbidden — "Payment Issue" or "Insufficient Credits" | +| User State | Expected | +|---|---| +| Active subscription | ✅ Access | +| Past-due (< 14 days, not suspended) | ✅ Access | +| Trialing (not expired) | ✅ Access | +| Earlybird (not expired) | ✅ Access | +| Expired trial | ❌ Forbidden — "Your Trial Has Ended" | +| Expired earlybird | ❌ Forbidden — "Earlybird Hosting Expired" | +| Canceled subscription | ❌ Forbidden — "Subscription Ended" | +| Past-due + suspended | ❌ Forbidden — "Payment Issue" or "Insufficient Credits" | ### 12.2 — AccessLockedDialog shows correct remediation diff --git a/.plans/migrate-email-to-mailgun.md b/.plans/migrate-email-to-mailgun.md index 8351f51bef..6e3a7583d4 100644 --- a/.plans/migrate-email-to-mailgun.md +++ b/.plans/migrate-email-to-mailgun.md @@ -85,39 +85,39 @@ function buildCreditsSection(monthlyCreditsUsd: number): string { Customer.io stores subjects in the remote template. Mailgun needs them passed explicitly. Rather than adding a subject argument to every `send*Email` call site, subjects are stored in a `subjects` map in `email.ts` keyed by `TemplateName`. The Mailgun branch of `send()` looks up `subjects[templateName]` internally — call sites are unchanged. -| Template name | Subject | -| --------------------------- | --------------------------------------------- | -| `orgSubscription` | "Welcome to Kilo for Teams!" | -| `orgRenewed` | "Kilo: Your Teams Subscription Renewal" | -| `orgCancelled` | "Kilo: Your Teams Subscription is Cancelled" | -| `orgSSOUserJoined` | "Kilo: New SSO User Joined Your Organization" | -| `orgInvitation` | "Kilo: Teams Invitation" | -| `magicLink` | "Sign in to Kilo Code" | -| `balanceAlert` | "Kilo: Low Balance Alert" | -| `autoTopUpFailed` | "Kilo: Auto Top-Up Failed" | -| `ossInviteNewUser` | "Kilo: OSS Sponsorship Offer" | -| `ossInviteExistingUser` | "Kilo: OSS Sponsorship Offer" | -| `ossExistingOrgProvisioned` | "Kilo: OSS Sponsorship Offer" | -| `deployFailed` | "Kilo: Your Deployment Failed" | +| Template name | Subject | +|---|---| +| `orgSubscription` | "Welcome to Kilo for Teams!" | +| `orgRenewed` | "Kilo: Your Teams Subscription Renewal" | +| `orgCancelled` | "Kilo: Your Teams Subscription is Cancelled" | +| `orgSSOUserJoined` | "Kilo: New SSO User Joined Your Organization" | +| `orgInvitation` | "Kilo: Teams Invitation" | +| `magicLink` | "Sign in to Kilo Code" | +| `balanceAlert` | "Kilo: Low Balance Alert" | +| `autoTopUpFailed` | "Kilo: Auto Top-Up Failed" | +| `ossInviteNewUser` | "Kilo: OSS Sponsorship Offer" | +| `ossInviteExistingUser` | "Kilo: OSS Sponsorship Offer" | +| `ossExistingOrgProvisioned` | "Kilo: OSS Sponsorship Offer" | +| `deployFailed` | "Kilo: Your Deployment Failed" | ### Template Variables All templates use `{{ variable }}` interpolation. `year` is always `String(new Date().getFullYear())`. `credits_section` is built in JS (HTML snippet or empty string). -| Template file | Variables | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `orgSubscription.html` | `seats`, `organization_url`, `invoices_url`, `year` | -| `orgRenewed.html` | `seats`, `invoices_url`, `year` | -| `orgCancelled.html` | `invoices_url`, `year` | -| `orgSSOUserJoined.html` | `new_user_email`, `organization_url`, `year` | -| `orgInvitation.html` | `organization_name`, `inviter_name`, `accept_invite_url`, `year` | -| `magicLink.html` | `magic_link_url`, `email`, `expires_in`, `year` | -| `balanceAlert.html` | `minimum_balance`, `organization_url`, `year` | -| `autoTopUpFailed.html` | `reason`, `credits_url`, `year` | -| `ossInviteNewUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `accept_invite_url`, `integrations_url`, `code_reviews_url`, `year` | -| `ossInviteExistingUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | -| `ossExistingOrgProvisioned.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | -| `deployFailed.html` | `deployment_name`, `deployment_url`, `repository`, `year` | +| Template file | Variables | +|---|---| +| `orgSubscription.html` | `seats`, `organization_url`, `invoices_url`, `year` | +| `orgRenewed.html` | `seats`, `invoices_url`, `year` | +| `orgCancelled.html` | `invoices_url`, `year` | +| `orgSSOUserJoined.html` | `new_user_email`, `organization_url`, `year` | +| `orgInvitation.html` | `organization_name`, `inviter_name`, `accept_invite_url`, `year` | +| `magicLink.html` | `magic_link_url`, `email`, `expires_in`, `year` | +| `balanceAlert.html` | `minimum_balance`, `organization_url`, `year` | +| `autoTopUpFailed.html` | `reason`, `credits_url`, `year` | +| `ossInviteNewUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `accept_invite_url`, `integrations_url`, `code_reviews_url`, `year` | +| `ossInviteExistingUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | +| `ossExistingOrgProvisioned.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | +| `deployFailed.html` | `deployment_name`, `deployment_url`, `repository`, `year` | ### Admin Email Testing Page @@ -199,16 +199,16 @@ After the provider switch is confirmed stable: ## Files Changed -| File | PR | Change | -| ----------------------------------------------------------- | --- | --------------------------------------------------------------------------------------------------- | -| `src/lib/email.ts` | 1 | Routing via `send({ templateName, templateVars })`; exports `templates`, `subjects`, `TemplateName` | -| `src/lib/email-customerio.ts` | 1 | Extracted `sendViaCustomerIo()`; minimal PII-free logging | -| `src/lib/email-mailgun.ts` | 1→2 | Stub in PR 1 (throws); full `sendViaMailgun({ to, subject, html })` implementation in PR 2 | -| `src/lib/config.server.ts` | 1 | Add `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, `EMAIL_PROVIDER` with runtime validation guard | -| `src/routers/admin/email-testing-router.ts` | 1 | New tRPC router with `getTemplates`, `getProviders`, `getPreview`, `sendTest` | -| `src/app/admin/email-testing/page.tsx` | 1 | New admin page; Customer.io variable preview; mailgun iframe preview added in PR 2 | -| `src/app/admin/components/AppSidebar.tsx` | 1 | Add Email Testing nav link | -| `src/routers/admin-router.ts` | 1 | Register `emailTestingRouter` | -| `.env.local`, `.env.test`, `.env.development.local.example` | 1 | Add `EMAIL_PROVIDER=customerio` | -| `src/emails/*.html` | 2 | OSS templates: replace Liquid credits conditional with `{{ credits_section }}` | -| `package.json` | 2 | Add `mailgun.js` + `form-data` (keep `customerio-node` until PR 4) | +| File | PR | Change | +|---|---|---| +| `src/lib/email.ts` | 1 | Routing via `send({ templateName, templateVars })`; exports `templates`, `subjects`, `TemplateName` | +| `src/lib/email-customerio.ts` | 1 | Extracted `sendViaCustomerIo()`; minimal PII-free logging | +| `src/lib/email-mailgun.ts` | 1→2 | Stub in PR 1 (throws); full `sendViaMailgun({ to, subject, html })` implementation in PR 2 | +| `src/lib/config.server.ts` | 1 | Add `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, `EMAIL_PROVIDER` with runtime validation guard | +| `src/routers/admin/email-testing-router.ts` | 1 | New tRPC router with `getTemplates`, `getProviders`, `getPreview`, `sendTest` | +| `src/app/admin/email-testing/page.tsx` | 1 | New admin page; Customer.io variable preview; mailgun iframe preview added in PR 2 | +| `src/app/admin/components/AppSidebar.tsx` | 1 | Add Email Testing nav link | +| `src/routers/admin-router.ts` | 1 | Register `emailTestingRouter` | +| `.env.local`, `.env.test`, `.env.development.local.example` | 1 | Add `EMAIL_PROVIDER=customerio` | +| `src/emails/*.html` | 2 | OSS templates: replace Liquid credits conditional with `{{ credits_section }}` | +| `package.json` | 2 | Add `mailgun.js` + `form-data` (keep `customerio-node` until PR 4) | diff --git a/.plans/monorepo-migration-prompt.md b/.plans/monorepo-migration-prompt.md index c73585c7c8..7a66f7a390 100644 --- a/.plans/monorepo-migration-prompt.md +++ b/.plans/monorepo-migration-prompt.md @@ -48,36 +48,36 @@ git diff ...HEAD --name-status Save this file list — these are the only files you need to migrate. The status codes you may see: -| Status | Meaning | How to handle | -| ------ | -------- | -------------------------------------------------------------------------------------------- | -| `M` | Modified | Apply the diff to the file at its new path | -| `A` | Added | Create at the mapped new path (see safety rule below) | -| `D` | Deleted | Delete at the mapped new path | -| `R###` | Renamed | Treat as a delete of the old path + add of the new path, both mapped through the table below | -| `C###` | Copied | Treat as an add of the destination path, mapped through the table below | +| Status | Meaning | How to handle | +|---|---|---| +| `M` | Modified | Apply the diff to the file at its new path | +| `A` | Added | Create at the mapped new path (see safety rule below) | +| `D` | Deleted | Delete at the mapped new path | +| `R###` | Renamed | Treat as a delete of the old path + add of the new path, both mapped through the table below | +| `C###` | Copied | Treat as an add of the destination path, mapped through the table below | ## 2. Path mapping The restructure renamed paths as follows. For `cloudflare-*` workers, the `cloudflare-` prefix was stripped: -| Old path (this branch) | New path (main) | -| ----------------------------------- | ----------------------------------- | -| `src/` | `apps/web/src/` | -| `public/` | `apps/web/public/` | -| `dev/` | `apps/web/dev/` | -| `tests/` | `apps/web/tests/` | -| `storybook/` | `apps/storybook/` | -| `kilo-app/` | `apps/mobile/` | -| `cloud-agent/` | `services/cloud-agent/` | -| `cloud-agent-next/` | `services/cloud-agent-next/` | -| `cloudflare-gastown/` | `services/gastown/` | -| `cloudflare-deploy-infra/` | `services/deploy-infra/` | -| `cloudflare-o11y/` | `services/o11y/` | -| `cloudflare-/` | `services//` | -| `kiloclaw/` | `services/kiloclaw/` | +| Old path (this branch) | New path (main) | +|---|---| +| `src/` | `apps/web/src/` | +| `public/` | `apps/web/public/` | +| `dev/` | `apps/web/dev/` | +| `tests/` | `apps/web/tests/` | +| `storybook/` | `apps/storybook/` | +| `kilo-app/` | `apps/mobile/` | +| `cloud-agent/` | `services/cloud-agent/` | +| `cloud-agent-next/` | `services/cloud-agent-next/` | +| `cloudflare-gastown/` | `services/gastown/` | +| `cloudflare-deploy-infra/` | `services/deploy-infra/` | +| `cloudflare-o11y/` | `services/o11y/` | +| `cloudflare-/` | `services//` | +| `kiloclaw/` | `services/kiloclaw/` | | `kiloclaw/packages/secret-catalog/` | `packages/kiloclaw-secret-catalog/` | -| `packages/*` | `packages/*` (unchanged) | +| `packages/*` | `packages/*` (unchanged) | Many root-level Next.js config files (`next.config.mjs`, `tsconfig.json`, `jest.config.ts`, `playwright.config.ts`, `vercel.json`, etc.) moved into @@ -94,13 +94,13 @@ The root `package.json` was split: it is now a lean workspace root names. When running `pnpm --filter`, always use the name from the workspace's `package.json`, not the directory name. Known divergences: -| Directory | `package.json` name | -| ------------------- | --------------------- | -| `apps/web/` | `web` | -| `apps/mobile/` | `kilo-app` | -| `apps/storybook/` | `@kilocode/storybook` | -| `services/o11y/` | `cloudflare-o11y` | -| `services/gastown/` | `cloudflare-gastown` | +| Directory | `package.json` name | +|---|---| +| `apps/web/` | `web` | +| `apps/mobile/` | `kilo-app` | +| `apps/storybook/` | `@kilocode/storybook` | +| `services/o11y/` | `cloudflare-o11y` | +| `services/gastown/` | `cloudflare-gastown` | ## 3. Create migration branch diff --git a/.plans/monorepo-restructure-summary.md b/.plans/monorepo-restructure-summary.md index 8c81004971..3a320ec3e6 100644 --- a/.plans/monorepo-restructure-summary.md +++ b/.plans/monorepo-restructure-summary.md @@ -4,18 +4,18 @@ Executed on 2026-04-05. Implements the plan in `plans/monorepo-restructure.md`. ## Commits -| Commit | Phase | Description | -| --------- | ----- | ----------------------------------------------------------------------------- | -| `358df11` | 1 | Create directory scaffolding (`apps/`, `services/`) | -| `ca0a269` | 2 + 8 | Move Next.js app to `apps/web/`, split package.json, create lean root | -| `20cd5df` | 3 | Move Storybook to `apps/storybook/`, kilo-app to `apps/mobile/` | -| `3668663` | 4 | Move 19 workers to `services/`, strip `cloudflare-` prefix | -| `fa4bd0f` | 5 | Move `kiloclaw/packages/secret-catalog` to `packages/kiloclaw-secret-catalog` | -| `4096f4f` | 6 | Switch `pnpm-workspace.yaml` to globs, rewrite shell scripts | -| `3da92cf` | 7 | Update all 8 GitHub Actions workflows | -| `09cf1f7` | 9 | Update `.prettierignore`, `.gitignore`, `.kilocodeignore` | -| `97e6678` | 10 | Add `@typescript/native-preview` to root devDeps (verification fix) | -| `bc35ced` | 10 | Fix lint: `@jest/globals` in `@kilocode/db`, `.oxlintrc.json` ignorePatterns | +| Commit | Phase | Description | +|---|---|---| +| `358df11` | 1 | Create directory scaffolding (`apps/`, `services/`) | +| `ca0a269` | 2 + 8 | Move Next.js app to `apps/web/`, split package.json, create lean root | +| `20cd5df` | 3 | Move Storybook to `apps/storybook/`, kilo-app to `apps/mobile/` | +| `3668663` | 4 | Move 19 workers to `services/`, strip `cloudflare-` prefix | +| `fa4bd0f` | 5 | Move `kiloclaw/packages/secret-catalog` to `packages/kiloclaw-secret-catalog` | +| `4096f4f` | 6 | Switch `pnpm-workspace.yaml` to globs, rewrite shell scripts | +| `3da92cf` | 7 | Update all 8 GitHub Actions workflows | +| `09cf1f7` | 9 | Update `.prettierignore`, `.gitignore`, `.kilocodeignore` | +| `97e6678` | 10 | Add `@typescript/native-preview` to root devDeps (verification fix) | +| `bc35ced` | 10 | Fix lint: `@jest/globals` in `@kilocode/db`, `.oxlintrc.json` ignorePatterns | Phase 8 (lean root `package.json`) was done atomically with Phase 2 since both must happen together. Phase 11 (external configuration) is manual post-merge work. @@ -103,14 +103,14 @@ All 8 workflows updated (trufflehog needed no changes): ## Verification results -| Check | Result | -| -------------------------------------------------- | ------------------------------------ | -| `pnpm install` | Pass | -| `pnpm --filter web typecheck` | Pass | -| `pnpm --filter kilo-app typecheck` | Pass | -| `pnpm --filter cloud-agent-next typecheck` | Pass | -| `pnpm -r lint` | Pass (0 errors across 31 workspaces) | -| `scripts/changed-workspaces.sh --exclude apps/web` | Pass (correct JSON output) | +| Check | Result | +|---|---| +| `pnpm install` | Pass | +| `pnpm --filter web typecheck` | Pass | +| `pnpm --filter kilo-app typecheck` | Pass | +| `pnpm --filter cloud-agent-next typecheck` | Pass | +| `pnpm -r lint` | Pass (0 errors across 31 workspaces) | +| `scripts/changed-workspaces.sh --exclude apps/web` | Pass (correct JSON output) | ## Post-merge manual steps (Phase 11) diff --git a/.plans/monorepo-restructure.md b/.plans/monorepo-restructure.md index c33a28ef88..d179921aa0 100644 --- a/.plans/monorepo-restructure.md +++ b/.plans/monorepo-restructure.md @@ -387,21 +387,21 @@ These cannot be done in code and must be done manually: ## Risks and Mitigations -| Risk | Mitigation | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Git history fragmentation from `git mv` | Use `git mv` for all moves to preserve history tracking. Git is good at detecting renames. | -| Vercel deployment breaks | Update Vercel root directory setting IMMEDIATELY after merge. Consider coordinating merge timing. | -| pnpm resolution breaks | Run `pnpm install` after every structural change to catch issues early | -| Import paths break | The `@/*` alias stays the same (relative to tsconfig). Cross-package `workspace:*` deps use package names, not paths. | -| CI workflows fail on first run | Have the workflow updates in the same commit as the directory moves | -| `.env` not found by Next.js | Next.js loads `.env` from its project root -- moving it to `apps/web/.env` is correct | -| EAS builds break for kilo-app | Coordinate with mobile team; update EAS config post-merge if needed | -| `scripts/` shell scripts have hardcoded paths | Rewrite `changed-workspaces.sh` and `lint-all.sh` to expand workspace globs (Phase 6). Update `typecheck-all.sh` root tsgo path (Phase 2). | -| Lint scripts break across all workspaces | Every workspace's `lint` script hardcodes its root-relative path. Rewrite all 20+ `package.json` lint entries and `scripts/lint-all.sh` in the same commit as the directory moves | -| Root CI entrypoints disappear | Keep root aliases (`drizzle`, `test:e2e`, `dependency-cycle-check`) that delegate to the correct workspace, so workflows don't need simultaneous updates | -| Merge conflict volume at execution time | Execute the restructure on a fresh branch from main, not incrementally on a long-lived branch | -| `@kilocode/trpc` build ordering | `scripts/typecheck-all.sh` conditionally rebuilds trpc before workspace typechecks. This ordering must be preserved. | -| `typecheck-all.sh` hardcoded package name | The script excludes `kilocode-backend` in 4 places. Phase 2 renames this to `web` — both must be updated in the same commit. | +| Risk | Mitigation | +|---|---| +| Git history fragmentation from `git mv` | Use `git mv` for all moves to preserve history tracking. Git is good at detecting renames. | +| Vercel deployment breaks | Update Vercel root directory setting IMMEDIATELY after merge. Consider coordinating merge timing. | +| pnpm resolution breaks | Run `pnpm install` after every structural change to catch issues early | +| Import paths break | The `@/*` alias stays the same (relative to tsconfig). Cross-package `workspace:*` deps use package names, not paths. | +| CI workflows fail on first run | Have the workflow updates in the same commit as the directory moves | +| `.env` not found by Next.js | Next.js loads `.env` from its project root -- moving it to `apps/web/.env` is correct | +| EAS builds break for kilo-app | Coordinate with mobile team; update EAS config post-merge if needed | +| `scripts/` shell scripts have hardcoded paths | Rewrite `changed-workspaces.sh` and `lint-all.sh` to expand workspace globs (Phase 6). Update `typecheck-all.sh` root tsgo path (Phase 2). | +| Lint scripts break across all workspaces | Every workspace's `lint` script hardcodes its root-relative path. Rewrite all 20+ `package.json` lint entries and `scripts/lint-all.sh` in the same commit as the directory moves | +| Root CI entrypoints disappear | Keep root aliases (`drizzle`, `test:e2e`, `dependency-cycle-check`) that delegate to the correct workspace, so workflows don't need simultaneous updates | +| Merge conflict volume at execution time | Execute the restructure on a fresh branch from main, not incrementally on a long-lived branch | +| `@kilocode/trpc` build ordering | `scripts/typecheck-all.sh` conditionally rebuilds trpc before workspace typechecks. This ordering must be preserved. | +| `typecheck-all.sh` hardcoded package name | The script excludes `kilocode-backend` in 4 places. Phase 2 renames this to `web` — both must be updated in the same commit. | ## Out of Scope (for future PRs) diff --git a/.plans/product-analytics-improvements.md b/.plans/product-analytics-improvements.md index f515fef65f..68edafcef2 100644 --- a/.plans/product-analytics-improvements.md +++ b/.plans/product-analytics-improvements.md @@ -22,12 +22,12 @@ Do **not** try to instrument every possible interaction. Instrument the decision ### What is missing -| Gap | Impact | -| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -| **No activation funnel events**: welcome pages, repo connection, first session have no tracking | Cannot measure or optimize signup-to-value path | -| **Cloud Agent, Code Review, App Builder have zero client events** | Three priority products are invisible in product analytics | -| **Inconsistent `organizationId`** on client events | Cannot segment product usage by org without server-side joins | -| **Inconsistent `distinctId`**: email in some server events, `kilo_user_id` in others | Identity fragmentation in PostHog | +| Gap | Impact | +|---|---| +| **No activation funnel events**: welcome pages, repo connection, first session have no tracking | Cannot measure or optimize signup-to-value path | +| **Cloud Agent, Code Review, App Builder have zero client events** | Three priority products are invisible in product analytics | +| **Inconsistent `organizationId`** on client events | Cannot segment product usage by org without server-side joins | +| **Inconsistent `distinctId`**: email in some server events, `kilo_user_id` in others | Identity fragmentation in PostHog | --- @@ -141,14 +141,14 @@ This 900-line component is the primary Cloud Agent interaction surface. Zero Pos Add: | Event | Trigger | Properties | -|-------|---------|------------| +|---|---|---| | `cloud_agent_session_started` | User submits a new session (form submit) | `model`, `mode`, `has_repo`, `organizationId` | | `cloud_agent_repo_selected` | User selects a repository in the session form | `repo_source` (github/gitlab), `organizationId` | | `cloud_agent_model_changed` | User changes model selection | `model`, `previous_model` | On the sessions list page (`src/app/(app)/cloud/sessions/`): | Event | Trigger | Properties | -|-------|---------|------------| +|---|---|---| | `cloud_agent_session_opened` | User clicks into an existing session | `session_id`, `organizationId` | ### 3b. Code Review — `src/app/(app)/code-reviews/ReviewAgentPageClient.tsx` @@ -157,7 +157,7 @@ On the sessions list page (`src/app/(app)/cloud/sessions/`): Add: | Event | Trigger | Properties | -|-------|---------|------------| +|---|---|---| | `code_review_configured` | User saves review configuration | `platform` (github/gitlab), `organizationId` | | `code_review_integration_connected` | User completes GitHub/GitLab integration setup | `platform`, `organizationId` | | `code_review_job_viewed` | User opens a specific review result | `review_id`, `organizationId` | @@ -168,7 +168,7 @@ Add: Add: | Event | Trigger | Properties | -|-------|---------|------------| +|---|---|---| | `app_builder_project_created` | User starts a new project | `organizationId` | | `app_builder_message_sent` | User sends a prompt in the chat pane | `organizationId`, `has_existing_project: boolean` | | `app_builder_deployed` | User deploys the app | `organizationId` | @@ -185,13 +185,13 @@ The sidebar (`PersonalAppSidebar.tsx`, `OrganizationAppSidebar.tsx`) has zero cl These are valuable but lower priority. Defer until Phases 1–3 are validated. -| Improvement | Rationale | -| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Worker-level lifecycle events** (cloud-agent, app-builder, code-review-infra workers) | 15+ workers have zero analytics. Even `task_started` / `task_completed` / `task_failed` with duration would close major server-side visibility gaps. Requires adding PostHog HTTP calls to Cloudflare Workers (same pattern as KiloClaw controller in `kiloclaw/src/routes/controller.ts:149`). | -| **Event naming taxonomy** | Current names mix conventions (`start_free_trial` vs `claw_trial_started`). Adopt `product_action` format consistently (e.g., `cloud_agent_session_started`, `code_review_configured`). Phase 2 and 3 events above already follow this convention. | -| **Cross-domain identity resolution** | Marketing landing pages (kilo.ai) and app (app.kilo.ai) use separate PostHog anonymous IDs. PostHog supports cross-subdomain tracking via `cross_subdomain_cookie: true`, but this only works for subdomains of the same root domain. If marketing pages are on a different domain entirely, server-side identity linking via the backend (e.g., passing a token through the redirect URL) is required. | -| **Expansion tracking** | Events for invite accepted, seat count changed, plan upgrade completed (not just clicked), BYOK key added. These answer "how do orgs grow?" | -| **DataLayer enrichment** | `src/components/DataLayerProvider.tsx` pushes email/name/is_new_user only. Add `organizationId`, `plan`, `created_at` for richer GTM attribution. | +| Improvement | Rationale | +|---|---| +| **Worker-level lifecycle events** (cloud-agent, app-builder, code-review-infra workers) | 15+ workers have zero analytics. Even `task_started` / `task_completed` / `task_failed` with duration would close major server-side visibility gaps. Requires adding PostHog HTTP calls to Cloudflare Workers (same pattern as KiloClaw controller in `kiloclaw/src/routes/controller.ts:149`). | +| **Event naming taxonomy** | Current names mix conventions (`start_free_trial` vs `claw_trial_started`). Adopt `product_action` format consistently (e.g., `cloud_agent_session_started`, `code_review_configured`). Phase 2 and 3 events above already follow this convention. | +| **Cross-domain identity resolution** | Marketing landing pages (kilo.ai) and app (app.kilo.ai) use separate PostHog anonymous IDs. PostHog supports cross-subdomain tracking via `cross_subdomain_cookie: true`, but this only works for subdomains of the same root domain. If marketing pages are on a different domain entirely, server-side identity linking via the backend (e.g., passing a token through the redirect URL) is required. | +| **Expansion tracking** | Events for invite accepted, seat count changed, plan upgrade completed (not just clicked), BYOK key added. These answer "how do orgs grow?" | +| **DataLayer enrichment** | `src/components/DataLayerProvider.tsx` pushes email/name/is_new_user only. Add `organizationId`, `plan`, `created_at` for richer GTM attribution. | ## Success Criteria diff --git a/.plans/standard-first-month-credit-discount.md b/.plans/standard-first-month-credit-discount.md index 727a3b6980..0fb820fba9 100644 --- a/.plans/standard-first-month-credit-discount.md +++ b/.plans/standard-first-month-credit-discount.md @@ -169,24 +169,24 @@ Add and update tests: ## Files Changed (Summary) -| File | Change | -| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| `.specs/kiloclaw-billing.md` | Add credit enrollment intro pricing rule | -| `src/app/(app)/claw/components/billing/billing-types.ts` | Add `STANDARD_FIRST_MONTH_MICRODOLLARS` constant, add `creditIntroEligible` to type | -| `src/lib/kiloclaw/credit-billing.ts` | Add `KILOCLAW_STANDARD_FIRST_MONTH_MICRODOLLARS`, add `hadPaidSubscription` param to `enrollWithCredits`, use intro cost | -| `src/routers/kiloclaw-router.ts` | Query existing sub for intro eligibility in `enrollWithCredits` handler; add `creditIntroEligible` to billing status | -| `src/app/(app)/claw/components/billing/PlanSelectionDialog.tsx` | Update `CreditEnrollmentSection` to show intro price when eligible | -| `src/app/(app)/claw/components/billing/WelcomePage.tsx` | Update `CreditEnrollmentBanner` to show intro price when eligible | -| `src/routers/kiloclaw-billing-router.test.ts` | Add/update tests for intro pricing | +| File | Change | +|---|---| +| `.specs/kiloclaw-billing.md` | Add credit enrollment intro pricing rule | +| `src/app/(app)/claw/components/billing/billing-types.ts` | Add `STANDARD_FIRST_MONTH_MICRODOLLARS` constant, add `creditIntroEligible` to type | +| `src/lib/kiloclaw/credit-billing.ts` | Add `KILOCLAW_STANDARD_FIRST_MONTH_MICRODOLLARS`, add `hadPaidSubscription` param to `enrollWithCredits`, use intro cost | +| `src/routers/kiloclaw-router.ts` | Query existing sub for intro eligibility in `enrollWithCredits` handler; add `creditIntroEligible` to billing status | +| `src/app/(app)/claw/components/billing/PlanSelectionDialog.tsx` | Update `CreditEnrollmentSection` to show intro price when eligible | +| `src/app/(app)/claw/components/billing/WelcomePage.tsx` | Update `CreditEnrollmentBanner` to show intro price when eligible | +| `src/routers/kiloclaw-billing-router.test.ts` | Add/update tests for intro pricing | ## Files NOT Changed -| File | Why | -| --------------------------------------------- | ----------------------------------------------------------------------------------------- | -| `src/lib/kiloclaw/billing-lifecycle-cron.ts` | Renewals always use full price — no intro discount on renewals | -| `src/lib/kiloclaw/stripe-handlers.ts` | Stripe intro price logic unchanged | -| `src/lib/kiloclaw/stripe-price-ids.server.ts` | No new Stripe prices needed | -| `KiloPassAwardingCreditsClient.tsx` | Auto-enrollment callback calls same `enrollWithCredits` mutation — benefits automatically | +| File | Why | +|---|---| +| `src/lib/kiloclaw/billing-lifecycle-cron.ts` | Renewals always use full price — no intro discount on renewals | +| `src/lib/kiloclaw/stripe-handlers.ts` | Stripe intro price logic unchanged | +| `src/lib/kiloclaw/stripe-price-ids.server.ts` | No new Stripe prices needed | +| `KiloPassAwardingCreditsClient.tsx` | Auto-enrollment callback calls same `enrollWithCredits` mutation — benefits automatically | ## Verification diff --git a/.prettierignore b/.prettierignore index 2e85725cc0..f7c835c026 100644 --- a/.prettierignore +++ b/.prettierignore @@ -36,6 +36,11 @@ pnpm-lock.yaml .kilocode/ .kilo +# Markdown is excluded because prettier re-pads markdown table cells for column +# alignment, which creates large spurious diffs on any unrelated content change. +# See AGENTS.md "Markdown Tables" and script/check-md-table-padding.ts. +*.md + # Misc .DS_Store .env* diff --git a/.specs/impact-affiliate-tracking.md b/.specs/impact-affiliate-tracking.md index c68a088063..a568dc9f5f 100644 --- a/.specs/impact-affiliate-tracking.md +++ b/.specs/impact-affiliate-tracking.md @@ -126,12 +126,12 @@ after the winning attribution is established. 12. The system MUST report only these conversion event types under this spec: - | Event | Trigger | - | ----------- | ----------------------------------------------------------------------- | - | SIGNUP | User's first attributed association for a provider | - | TRIAL_START | Attributed personal KiloClaw trial subscription becomes active | - | TRIAL_END | Attributed personal KiloClaw trial subscription ends by product flow | - | SALE | Affiliate-eligible KiloClaw payment period or Kilo Pass invoice settles | + | Event | Trigger | + |---|---| + | SIGNUP | User's first attributed association for a provider | + | TRIAL_START | Attributed personal KiloClaw trial subscription becomes active | + | TRIAL_END | Attributed personal KiloClaw trial subscription ends by product flow | + | SALE | Affiliate-eligible KiloClaw payment period or Kilo Pass invoice settles | 13. Every conversion event MUST carry its conversion occurrence time as a UTC instant. @@ -160,14 +160,14 @@ after the winning attribution is established. 20. Kilo Pass SALE reporting MUST use this tier/cadence classification and MUST NOT derive classification from invoice display text: - | Tier | Cadence | Reporting category | Reporting name | - | ---- | ------- | ---------------------------- | ---------------------------- | - | 19 | monthly | `kilo-pass-tier-19-monthly` | `Kilo Pass Tier 19 Monthly` | - | 19 | yearly | `kilo-pass-tier-19-yearly` | `Kilo Pass Tier 19 Yearly` | - | 49 | monthly | `kilo-pass-tier-49-monthly` | `Kilo Pass Tier 49 Monthly` | - | 49 | yearly | `kilo-pass-tier-49-yearly` | `Kilo Pass Tier 49 Yearly` | - | 199 | monthly | `kilo-pass-tier-199-monthly` | `Kilo Pass Tier 199 Monthly` | - | 199 | yearly | `kilo-pass-tier-199-yearly` | `Kilo Pass Tier 199 Yearly` | + | Tier | Cadence | Reporting category | Reporting name | + |---|---|---|---| + | 19 | monthly | `kilo-pass-tier-19-monthly` | `Kilo Pass Tier 19 Monthly` | + | 19 | yearly | `kilo-pass-tier-19-yearly` | `Kilo Pass Tier 19 Yearly` | + | 49 | monthly | `kilo-pass-tier-49-monthly` | `Kilo Pass Tier 49 Monthly` | + | 49 | yearly | `kilo-pass-tier-49-yearly` | `Kilo Pass Tier 49 Yearly` | + | 199 | monthly | `kilo-pass-tier-199-monthly` | `Kilo Pass Tier 199 Monthly` | + | 199 | yearly | `kilo-pass-tier-199-yearly` | `Kilo Pass Tier 199 Yearly` | 21. Kilo Pass SALE reporting SHOULD include the resolved Stripe price identifier as the provider SKU when it is available. If tier and cadence are resolved but the SKU is unavailable, the SALE MUST still be reported without a diff --git a/.specs/impact-referrals.md b/.specs/impact-referrals.md index 026cf9b9f8..68bbf3ed2e 100644 --- a/.specs/impact-referrals.md +++ b/.specs/impact-referrals.md @@ -161,13 +161,13 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin 5. The system MUST use existing Impact Performance conversion action tracker IDs for lifecycle reporting where applicable: - | Event | ActionTrackerId | Referral use | - | ----------- | --------------- | ------------------------------------------------- | - | VISIT | 71668 | Affiliate/visit reporting where applicable | - | SIGNUP | 71655 | New user creation with attribution | - | TRIAL_START | 71656 | KiloClaw trial subscription becomes active | - | TRIAL_END | 71658 | KiloClaw trial subscription ends | - | SALE | 71659 | KiloClaw monetized period or Kilo Pass conversion | + | Event | ActionTrackerId | Referral use | + |---|---|---| + | VISIT | 71668 | Affiliate/visit reporting where applicable | + | SIGNUP | 71655 | New user creation with attribution | + | TRIAL_START | 71656 | KiloClaw trial subscription becomes active | + | TRIAL_END | 71658 | KiloClaw trial subscription ends | + | SALE | 71659 | KiloClaw monetized period or Kilo Pass conversion | 6. Impact Advocate API credentials MUST remain server-side and MUST NOT be exposed to the browser. @@ -296,14 +296,14 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin 47. The system MUST implement at least these attribution outcomes: -| Scenario | Expected winner | -| ------------------------------------------------------------------------- | --------------- | -| Affiliate first, referral second, both valid, no prior affiliate SALE | Referral | -| Affiliate first, referral second, both valid, affiliate SALE before touch | Affiliate | -| Referral first, affiliate second, both valid, no prior affiliate SALE | Referral | -| Only affiliate valid | Affiliate | -| Only referral valid | Referral | -| All touches expired or invalid | None | +| Scenario | Expected winner | +|---|---| +| Affiliate first, referral second, both valid, no prior affiliate SALE | Referral | +| Affiliate first, referral second, both valid, affiliate SALE before touch | Affiliate | +| Referral first, affiliate second, both valid, no prior affiliate SALE | Referral | +| Only affiliate valid | Affiliate | +| Only referral valid | Referral | +| All touches expired or invalid | None | 48. Attribution resolution for referral rewards MUST happen at conversion time, not only at signup time. diff --git a/.specs/kiloclaw-billing.md b/.specs/kiloclaw-billing.md index 8a82b9b4c4..78ba3b9262 100644 --- a/.specs/kiloclaw-billing.md +++ b/.specs/kiloclaw-billing.md @@ -187,10 +187,10 @@ lapses, with email notifications at each stage. price version. 3. The catalog MUST include these self-service price versions: - | Price version | Effective timestamp | Standard first paid month | Standard recurring | Commit | Trial | Default/max self-service instance | - | ------------- | ------------------------ | ----------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------- | ------ | --------------------------------- | - | `2026-03-19` | 2026-03-19T00:00:00.000Z | $4 (4,000,000 microdollars) when eligible for pre-rollout lineage | $9/month (9,000,000 microdollars) | $48 upfront for 6 months (48,000,000 microdollars) | 7 days | `perf-1-3` | - | `2026-05-10` | 2026-05-10T00:00:00.000Z | $55/month; no first-month discount | $55/month (55,000,000 microdollars) | $306 upfront for 6 months (306,000,000 microdollars) | 1 day | `perf-1-3` | + | Price version | Effective timestamp | Standard first paid month | Standard recurring | Commit | Trial | Default/max self-service instance | + |---|---|---|---|---|---|---| + | `2026-03-19` | 2026-03-19T00:00:00.000Z | $4 (4,000,000 microdollars) when eligible for pre-rollout lineage | $9/month (9,000,000 microdollars) | $48 upfront for 6 months (48,000,000 microdollars) | 7 days | `perf-1-3` | + | `2026-05-10` | 2026-05-10T00:00:00.000Z | $55/month; no first-month discount | $55/month (55,000,000 microdollars) | $306 upfront for 6 months (306,000,000 microdollars) | 1 day | `perf-1-3` | 4. The current commit plan MAY be described as $51/month, but billing MUST charge $306 upfront for each six-month commit period. @@ -271,11 +271,11 @@ requirements defined below. valid combinations of payment source and payment provider subscription ID: - | State | payment_source | provider subscription ID | - | ------------- | -------------- | ------------------------ | - | Legacy Stripe | `stripe` | non-null | - | Hybrid | `credits` | non-null | - | Pure credit | `credits` | null | + | State | payment_source | provider subscription ID | + |---|---|---| + | Legacy Stripe | `stripe` | non-null | + | Hybrid | `credits` | non-null | + | Pure credit | `credits` | null | A subscription with payment source `stripe` MUST have a non-null payment provider subscription ID. A subscription with payment source diff --git a/.specs/kiloclaw-controller.md b/.specs/kiloclaw-controller.md index d6b2faa9ee..0af540a417 100644 --- a/.specs/kiloclaw-controller.md +++ b/.specs/kiloclaw-controller.md @@ -128,12 +128,12 @@ health handler can process pending HTTP requests during bootstrap. ### State Values -| State | Meaning | -| --------------- | ------------------------------------------------ | +| State | Meaning | +|---|---| | `bootstrapping` | Bootstrap is in progress; `phase` indicates step | -| `starting` | Bootstrap complete, gateway is starting | -| `ready` | Controller startup completed successfully | -| `degraded` | A startup phase failed; `error` explains which | +| `starting` | Bootstrap complete, gateway is starting | +| `ready` | Controller startup completed successfully | +| `degraded` | A startup phase failed; `error` explains which | Note: `ready` means the controller startup sequence completed and the gateway was started. It does NOT mean the gateway is currently healthy. @@ -146,33 +146,33 @@ endpoint. The `phase` field during `bootstrapping` progresses through: -| Phase | What is happening | -| ------------------------------ | ------------------------------------------------- | -| `init` | HTTP server started, bootstrap not yet begun | -| `decrypting` | Decrypting `KILOCLAW_ENC_*` env vars | -| `directories` | Creating config/workspace dirs, setting env vars | -| `feature-flags` | Applying instance feature flags | -| `github` | Configuring GitHub access (best-effort) | -| `linear` | Configuring Linear MCP availability | +| Phase | What is happening | +|---|---| +| `init` | HTTP server started, bootstrap not yet begun | +| `decrypting` | Decrypting `KILOCLAW_ENC_*` env vars | +| `directories` | Creating config/workspace dirs, setting env vars | +| `feature-flags` | Applying instance feature flags | +| `github` | Configuring GitHub access (best-effort) | +| `linear` | Configuring Linear MCP availability | | `gateway-client-device-scopes` | Remediating gateway-client device approval scopes | -| `onboard` | Running `openclaw onboard` (first boot) | -| `doctor` | Running `openclaw doctor --fix` (subsequent boot) | -| `tools-md` | Synchronizing managed `TOOLS.md` sections | -| `mcporter` | Writing managed MCP server config | +| `onboard` | Running `openclaw onboard` (first boot) | +| `doctor` | Running `openclaw doctor --fix` (subsequent boot) | +| `tools-md` | Synchronizing managed `TOOLS.md` sections | +| `mcporter` | Writing managed MCP server config | ### Endpoint Availability by Phase -| Phase | `/_kilo/health` | `/_kilo/*` routes | User traffic (proxy) | WebSocket | -| ------------------------------ | ------------------------------- | ----------------- | ----------------------- | ----------------------- | -| Critical bootstrap running | Inline: `bootstrapping` + phase | 503 | 503 | 503 reject | -| Critical bootstrap failed | Inline: `degraded` | 503 | 503 | 503 reject | -| Runtime config failed | Inline: `degraded` | 503 | 503 | 503 reject | -| Routes registered | Hono: `bootstrapping` + phase | Auth-gated | 503 "Gateway not ready" | 503 reject | -| Non-critical bootstrap failed | Hono: `degraded` | Auth-gated | 503 "Gateway not ready" | 503 reject | -| Routes registered, gw starting | Hono: `starting` | Auth-gated | 503 "Gateway not ready" | Auth-gated | -| Gateway start failed | Hono: `degraded` | Auth-gated | 503 "Gateway not ready" | Auth-gated | -| Fully operational | Hono: `ready` | Auth-gated | Proxied to gateway | Proxied to gateway | -| Gateway crashes at runtime | Hono: `ready` (unchanged) | Auth-gated | 503 "Gateway not ready" | 503 "Gateway not ready" | +| Phase | `/_kilo/health` | `/_kilo/*` routes | User traffic (proxy) | WebSocket | +|---|---|---|---|---| +| Critical bootstrap running | Inline: `bootstrapping` + phase | 503 | 503 | 503 reject | +| Critical bootstrap failed | Inline: `degraded` | 503 | 503 | 503 reject | +| Runtime config failed | Inline: `degraded` | 503 | 503 | 503 reject | +| Routes registered | Hono: `bootstrapping` + phase | Auth-gated | 503 "Gateway not ready" | 503 reject | +| Non-critical bootstrap failed | Hono: `degraded` | Auth-gated | 503 "Gateway not ready" | 503 reject | +| Routes registered, gw starting | Hono: `starting` | Auth-gated | 503 "Gateway not ready" | Auth-gated | +| Gateway start failed | Hono: `degraded` | Auth-gated | 503 "Gateway not ready" | Auth-gated | +| Fully operational | Hono: `ready` | Auth-gated | Proxied to gateway | Proxied to gateway | +| Gateway crashes at runtime | Hono: `ready` (unchanged) | Auth-gated | 503 "Gateway not ready" | 503 "Gateway not ready" | The first observable state over HTTP is `bootstrapping` with phase `init`. There is no separately observable "HTTP server starting" state @@ -190,32 +190,32 @@ gateway supervisor handles crash recovery independently. Use 1. The endpoint MUST require bearer token authentication. 2. The response MUST include the following fields: -| Field | Type | Description | -| ---------- | -------------- | ----------------------------------------------------------- | -| `state` | string | Supervisor state (see below) | -| `pid` | number \| null | OS process ID of the gateway, null if not running | -| `uptime` | number | Seconds since the current process started, 0 if not running | -| `restarts` | number | Total restart count since controller boot | -| `lastExit` | object \| null | Last exit information, null if never exited | +| Field | Type | Description | +|---|---|---| +| `state` | string | Supervisor state (see below) | +| `pid` | number \| null | OS process ID of the gateway, null if not running | +| `uptime` | number | Seconds since the current process started, 0 if not running | +| `restarts` | number | Total restart count since controller boot | +| `lastExit` | object \| null | Last exit information, null if never exited | 3. The `lastExit` object, when present, MUST include: -| Field | Type | Description | -| -------- | -------------- | ------------------------------------------------------ | -| `code` | number \| null | Exit code, null if killed by signal | +| Field | Type | Description | +|---|---|---| +| `code` | number \| null | Exit code, null if killed by signal | | `signal` | string \| null | Signal name (e.g., `SIGTERM`), null if exited normally | -| `at` | string | ISO 8601 timestamp of the exit | +| `at` | string | ISO 8601 timestamp of the exit | ### Supervisor States -| State | Meaning | -| --------------- | ------------------------------------------------------- | -| `stopped` | Gateway is not running (manual stop or not yet started) | -| `starting` | Gateway process is spawning | -| `running` | Gateway process has been spawned and has not exited | -| `stopping` | SIGTERM sent, waiting for exit | -| `crashed` | Gateway exited unexpectedly, restart pending | -| `shutting_down` | Controller is shutting down, gateway being terminated | +| State | Meaning | +|---|---| +| `stopped` | Gateway is not running (manual stop or not yet started) | +| `starting` | Gateway process is spawning | +| `running` | Gateway process has been spawned and has not exited | +| `stopping` | SIGTERM sent, waiting for exit | +| `crashed` | Gateway exited unexpectedly, restart pending | +| `shutting_down` | Controller is shutting down, gateway being terminated | ### Gateway Lifecycle Endpoints @@ -554,11 +554,11 @@ patches to `openclaw.json`. The patches MUST include: The controller uses three authentication mechanisms: -| Auth type | Header | Used by | -| ------------ | ------------------------------- | ------------------------------------- | -| Bearer token | `Authorization: Bearer ` | All `/_kilo/*` routes except health | -| Proxy token | `x-kiloclaw-proxy-token` | Catch-all HTTP proxy, WebSocket proxy | -| None | — | `/health`, `/_kilo/health` | +| Auth type | Header | Used by | +|---|---|---| +| Bearer token | `Authorization: Bearer ` | All `/_kilo/*` routes except health | +| Proxy token | `x-kiloclaw-proxy-token` | Catch-all HTTP proxy, WebSocket proxy | +| None | — | `/health`, `/_kilo/health` | 1. Bearer token comparisons MUST be timing-safe. 2. Proxy token enforcement is controlled by `REQUIRE_PROXY_TOKEN`. @@ -569,30 +569,30 @@ The controller uses three authentication mechanisms: #### Health (unauthenticated) -| Method | Path | Description | -| ------ | --------------- | ------------------------------------------- | -| GET | `/health` | Bare Fly probe — always `{"status":"ok"}` | -| GET | `/_kilo/health` | Controller lifecycle state with phase/error | +| Method | Path | Description | +|---|---|---| +| GET | `/health` | Bare Fly probe — always `{"status":"ok"}` | +| GET | `/_kilo/health` | Controller lifecycle state with phase/error | #### Version (bearer token) -| Method | Path | Description | -| ------ | ---------------- | ----------------------------------------------------------------------------- | -| GET | `/_kilo/version` | Controller version, commit, openclaw version, gateway stats, controller state | +| Method | Path | Description | +|---|---|---| +| GET | `/_kilo/version` | Controller version, commit, openclaw version, gateway stats, controller state | The response fields are: -| Field | Type | Required | Description | -| ----------------- | -------------- | -------- | ---------------------------------------------------------------------------------- | -| `version` | string | Yes | Controller version (calver) | -| `commit` | string | Yes | Controller build commit hash | -| `openclawVersion` | string \| null | Yes | Installed openclaw version | -| `openclawCommit` | string \| null | Yes | Installed openclaw commit hash | -| `apiVersion` | number | No | Controller response/protocol envelope shape version | -| `capabilities` | string[] | No | Sorted capability hints for controller routes/behaviors registered in this build | -| `gateway` | object \| null | Yes | Supervisor stats (same as `/_kilo/gateway/status`), null if supervisor not created | -| `controllerState` | object | No | Current controller lifecycle state, present when state ref is wired | -| `kiloChatHealth` | object | No | Kilo Chat health probe summary, present when Kilo Chat probing is active | +| Field | Type | Required | Description | +|---|---|---|---| +| `version` | string | Yes | Controller version (calver) | +| `commit` | string | Yes | Controller build commit hash | +| `openclawVersion` | string \| null | Yes | Installed openclaw version | +| `openclawCommit` | string \| null | Yes | Installed openclaw commit hash | +| `apiVersion` | number | No | Controller response/protocol envelope shape version | +| `capabilities` | string[] | No | Sorted capability hints for controller routes/behaviors registered in this build | +| `gateway` | object \| null | Yes | Supervisor stats (same as `/_kilo/gateway/status`), null if supervisor not created | +| `controllerState` | object | No | Current controller lifecycle state, present when state ref is wired | +| `kiloChatHealth` | object | No | Kilo Chat health probe summary, present when Kilo Chat probing is active | Version capability hint rules: @@ -635,54 +635,54 @@ Version capability hint rules: #### Gateway (bearer token) -| Method | Path | Description | -| ------ | ------------------------ | ------------------------------------------ | -| GET | `/_kilo/gateway/status` | Gateway supervisor state and stats | -| POST | `/_kilo/gateway/start` | Start the gateway (409 if running) | -| POST | `/_kilo/gateway/stop` | Stop the gateway gracefully | -| GET | `/_kilo/gateway/ready` | Proxy the gateway readiness endpoint | -| POST | `/_kilo/gateway/restart` | Restart the gateway (409 if shutting down) | +| Method | Path | Description | +|---|---|---| +| GET | `/_kilo/gateway/status` | Gateway supervisor state and stats | +| POST | `/_kilo/gateway/start` | Start the gateway (409 if running) | +| POST | `/_kilo/gateway/stop` | Stop the gateway gracefully | +| GET | `/_kilo/gateway/ready` | Proxy the gateway readiness endpoint | +| POST | `/_kilo/gateway/restart` | Restart the gateway (409 if shutting down) | #### Config (bearer token) -| Method | Path | Description | -| ------ | ----------------------------------------- | ---------------------------------------------------------- | -| GET | `/_kilo/config/read` | Read openclaw.json with MD5 etag | -| POST | `/_kilo/config/restore/base` | Regenerate config from env vars, signal gateway reload | -| POST | `/_kilo/config/replace` | Atomically replace openclaw.json (etag concurrency) | -| POST | `/_kilo/config/patch` | Deep-merge a JSON patch into openclaw.json | -| POST | `/_kilo/config/tools-md/google-workspace` | Enable/disable managed Google Workspace `TOOLS.md` section | +| Method | Path | Description | +|---|---|---| +| GET | `/_kilo/config/read` | Read openclaw.json with MD5 etag | +| POST | `/_kilo/config/restore/base` | Regenerate config from env vars, signal gateway reload | +| POST | `/_kilo/config/replace` | Atomically replace openclaw.json (etag concurrency) | +| POST | `/_kilo/config/patch` | Deep-merge a JSON patch into openclaw.json | +| POST | `/_kilo/config/tools-md/google-workspace` | Enable/disable managed Google Workspace `TOOLS.md` section | The restore endpoint only accepts `base` as the version parameter. Other values MUST return 400. #### Environment (bearer token) -| Method | Path | Description | -| ------ | ------------------ | ---------------------------------------------------- | -| POST | `/_kilo/env/patch` | Hot-patch allowed env vars and signal gateway reload | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/env/patch` | Hot-patch allowed env vars and signal gateway reload | #### Google OAuth (bearer token) -| Method | Path | Description | -| ------ | ---------------------------- | ------------------------------------------------ | -| POST | `/_kilo/google-oauth/token` | Resolve a brokered Google OAuth access token | -| POST | `/_kilo/google-oauth/status` | Report broker/legacy Google credential readiness | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/google-oauth/token` | Resolve a brokered Google OAuth access token | +| POST | `/_kilo/google-oauth/status` | Report broker/legacy Google credential readiness | #### Pairing (bearer token) -| Method | Path | Description | -| ------ | --------------------------------- | ---------------------------------------- | -| GET | `/_kilo/pairing/channels` | Channel pairing state (optional refresh) | -| GET | `/_kilo/pairing/devices` | Device pairing state (optional refresh) | -| POST | `/_kilo/pairing/channels/approve` | Approve a channel pairing request | -| POST | `/_kilo/pairing/devices/approve` | Approve a device pairing request | +| Method | Path | Description | +|---|---|---| +| GET | `/_kilo/pairing/channels` | Channel pairing state (optional refresh) | +| GET | `/_kilo/pairing/devices` | Device pairing state (optional refresh) | +| POST | `/_kilo/pairing/channels/approve` | Approve a channel pairing request | +| POST | `/_kilo/pairing/devices/approve` | Approve a device pairing request | #### Gmail Push (bearer token) -| Method | Path | Description | -| ------ | --------------------- | ----------------------------------------------------- | -| POST | `/_kilo/gmail-pubsub` | Forward Google Pub/Sub push to gog watch on port 3002 | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/gmail-pubsub` | Forward Google Pub/Sub push to gog watch on port 3002 | The Gmail push endpoint MUST return 404 when Gmail watch is not configured and 503 when the watch supervisor is configured but not @@ -691,49 +691,49 @@ running. When forwarding, it MUST authenticate to gog with #### Inbound Hooks (bearer token) -| Method | Path | Description | -| ------ | -------------------- | ------------------------------------------------ | -| POST | `/_kilo/hooks/email` | Forward inbound email payloads to OpenClaw hooks | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/hooks/email` | Forward inbound email payloads to OpenClaw hooks | #### Files and Profiles (bearer token) -| Method | Path | Description | -| ------ | ---------------------------------------- | --------------------------------------------- | -| POST | `/_kilo/bot-identity` | Write bot identity metadata | -| POST | `/_kilo/user-profile` | Write user profile metadata | -| GET | `/_kilo/files/tree` | List the safe file tree under controller root | -| GET | `/_kilo/files/read` | Read a safe file path | -| POST | `/_kilo/files/import-openclaw-workspace` | Import OpenClaw workspace files | -| POST | `/_kilo/files/write` | Write a safe file path | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/bot-identity` | Write bot identity metadata | +| POST | `/_kilo/user-profile` | Write user profile metadata | +| GET | `/_kilo/files/tree` | List the safe file tree under controller root | +| GET | `/_kilo/files/read` | Read a safe file path | +| POST | `/_kilo/files/import-openclaw-workspace` | Import OpenClaw workspace files | +| POST | `/_kilo/files/write` | Write a safe file path | #### Doctor (bearer token) -| Method | Path | Description | -| ------ | ---------------------- | ------------------------------------ | -| POST | `/_kilo/doctor/start` | Start an async `openclaw doctor` run | -| GET | `/_kilo/doctor/status` | Inspect current doctor run status | -| POST | `/_kilo/doctor/cancel` | Cancel the active doctor run | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/doctor/start` | Start an async `openclaw doctor` run | +| GET | `/_kilo/doctor/status` | Inspect current doctor run status | +| POST | `/_kilo/doctor/cancel` | Cancel the active doctor run | #### Kilo CLI Run (bearer token) -| Method | Path | Description | -| ------ | ----------------------- | --------------------------- | -| POST | `/_kilo/cli-run/start` | Start an async Kilo CLI run | -| GET | `/_kilo/cli-run/status` | Inspect current run status | -| POST | `/_kilo/cli-run/cancel` | Cancel the active Kilo run | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/cli-run/start` | Start an async Kilo CLI run | +| GET | `/_kilo/cli-run/status` | Inspect current run status | +| POST | `/_kilo/cli-run/cancel` | Cancel the active Kilo run | #### Morning Briefing (bearer token) -| Method | Path | Description | -| ------ | ---------------------------------------- | ------------------------------------- | -| GET | `/_kilo/morning-briefing/status` | Proxy morning briefing plugin status | -| POST | `/_kilo/morning-briefing/enable` | Enable the morning briefing schedule | -| POST | `/_kilo/morning-briefing/disable` | Disable the morning briefing schedule | -| POST | `/_kilo/morning-briefing/run` | Run briefing generation immediately | -| POST | `/_kilo/morning-briefing/interests` | Update briefing interest topics | -| POST | `/_kilo/morning-briefing/user-location` | Update briefing user location | -| GET | `/_kilo/morning-briefing/read/today` | Read today's briefing markdown | -| GET | `/_kilo/morning-briefing/read/yesterday` | Read yesterday's briefing markdown | +| Method | Path | Description | +|---|---|---| +| GET | `/_kilo/morning-briefing/status` | Proxy morning briefing plugin status | +| POST | `/_kilo/morning-briefing/enable` | Enable the morning briefing schedule | +| POST | `/_kilo/morning-briefing/disable` | Disable the morning briefing schedule | +| POST | `/_kilo/morning-briefing/run` | Run briefing generation immediately | +| POST | `/_kilo/morning-briefing/interests` | Update briefing interest topics | +| POST | `/_kilo/morning-briefing/user-location` | Update briefing user location | +| GET | `/_kilo/morning-briefing/read/today` | Read today's briefing markdown | +| GET | `/_kilo/morning-briefing/read/yesterday` | Read yesterday's briefing markdown | #### Kilo Chat (bearer token) @@ -742,32 +742,32 @@ Kilo Chat routes are registered only when both `KILOCLAW_SANDBOX_ID` and to `${KILOCHAT_BASE_URL}/bot/v1/sandboxes/:sandboxId` using the per-sandbox gateway token. -| Method | Path | Description | -| ------ | ------------------------------------------------------------------------------------ | ------------------------------- | -| POST | `/_kilo/kilo-chat/send` | Send a Kilo Chat message | -| PATCH | `/_kilo/kilo-chat/messages/:messageId` | Edit a Kilo Chat message | -| DELETE | `/_kilo/kilo-chat/messages/:messageId` | Delete a Kilo Chat message | -| POST | `/_kilo/kilo-chat/messages/:messageId/reactions` | Add a message reaction | -| DELETE | `/_kilo/kilo-chat/messages/:messageId/reactions` | Delete a message reaction | -| POST | `/_kilo/kilo-chat/typing` | Start typing indicator | -| POST | `/_kilo/kilo-chat/typing/stop` | Stop typing indicator | -| GET | `/_kilo/kilo-chat/conversations/:conversationId/messages` | List conversation messages | -| PATCH | `/_kilo/kilo-chat/conversations/:conversationId` | Rename a conversation | -| POST | `/_kilo/kilo-chat/bot-status` | Set bot status | -| POST | `/_kilo/kilo-chat/conversations/:conversationId/conversation-status` | Set conversation status | -| POST | `/_kilo/kilo-chat/conversations` | Create a conversation | -| GET | `/_kilo/kilo-chat/conversations` | List conversations | -| GET | `/_kilo/kilo-chat/conversations/:conversationId/members` | List conversation members | -| POST | `/_kilo/kilo-chat/conversations/:conversationId/messages/:messageId/delivery-failed` | Report message delivery failure | -| POST | `/_kilo/kilo-chat/conversations/:conversationId/actions/:groupId/delivery-failed` | Report action delivery failure | -| POST | `/_kilo/kilo-chat/attachments/init` | Initialize an attachment upload | -| GET | `/_kilo/kilo-chat/attachments/:attachmentId/url` | Resolve an attachment URL | +| Method | Path | Description | +|---|---|---| +| POST | `/_kilo/kilo-chat/send` | Send a Kilo Chat message | +| PATCH | `/_kilo/kilo-chat/messages/:messageId` | Edit a Kilo Chat message | +| DELETE | `/_kilo/kilo-chat/messages/:messageId` | Delete a Kilo Chat message | +| POST | `/_kilo/kilo-chat/messages/:messageId/reactions` | Add a message reaction | +| DELETE | `/_kilo/kilo-chat/messages/:messageId/reactions` | Delete a message reaction | +| POST | `/_kilo/kilo-chat/typing` | Start typing indicator | +| POST | `/_kilo/kilo-chat/typing/stop` | Stop typing indicator | +| GET | `/_kilo/kilo-chat/conversations/:conversationId/messages` | List conversation messages | +| PATCH | `/_kilo/kilo-chat/conversations/:conversationId` | Rename a conversation | +| POST | `/_kilo/kilo-chat/bot-status` | Set bot status | +| POST | `/_kilo/kilo-chat/conversations/:conversationId/conversation-status` | Set conversation status | +| POST | `/_kilo/kilo-chat/conversations` | Create a conversation | +| GET | `/_kilo/kilo-chat/conversations` | List conversations | +| GET | `/_kilo/kilo-chat/conversations/:conversationId/members` | List conversation members | +| POST | `/_kilo/kilo-chat/conversations/:conversationId/messages/:messageId/delivery-failed` | Report message delivery failure | +| POST | `/_kilo/kilo-chat/conversations/:conversationId/actions/:groupId/delivery-failed` | Report action delivery failure | +| POST | `/_kilo/kilo-chat/attachments/init` | Initialize an attachment upload | +| GET | `/_kilo/kilo-chat/attachments/:attachmentId/url` | Resolve an attachment URL | #### Catch-All Proxy (proxy token) -| Method | Path | Description | -| ------ | ---- | ------------------------------------------ | -| ALL | `*` | Reverse-proxy to gateway at 127.0.0.1:3001 | +| Method | Path | Description | +|---|---|---| +| ALL | `*` | Reverse-proxy to gateway at 127.0.0.1:3001 | 1. The catch-all MUST be registered last so all `/_kilo/*` routes take priority. diff --git a/AGENTS.md b/AGENTS.md index e075d41ed9..fc0850aff1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,13 +23,13 @@ scripts/ CI and one-off scripts 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. -| Command | What it checks | -| ---------------- | -------------------------------------------- | +| Command | What it checks | +|---|---| | `pnpm typecheck` | TypeScript type checking across all packages | -| `pnpm lint` | Lint all source files | -| `pnpm test` | Jest test suite | -| `pnpm validate` | All three above in sequence | -| `pnpm format` | Auto-format with oxfmt | +| `pnpm lint` | Lint all source files | +| `pnpm test` | Jest test suite | +| `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`. @@ -119,17 +119,30 @@ Do not leave HTML comments from the template. Review all commits on the branch w 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/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 | +| 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/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 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 395d99aaf8..c8aa0b119c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -205,23 +205,23 @@ All tests should pass against the local PostgreSQL database. ## Common Development Commands -| Command | Description | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `pnpm dev:start` | Start all local services in a tmux dashboard | -| `pnpm dev:stop` | Stop the tmux session and all services | -| `pnpm dev:env` | Sync `.dev.vars` files from `.env.local` (see [Worker `.dev.vars` setup](#worker-dev-vars-setup)) | -| `pnpm test` | Run the Jest test suite | -| `pnpm typecheck` | Run the TypeScript type checker | -| `pnpm lint` | Lint all source files | -| `pnpm format` | Format all supported files with oxfmt | -| `pnpm format:changed` | Format only files changed since `main` | -| `pnpm validate` | Run typecheck, lint, and tests | -| `pnpm drizzle migrate` | Apply pending database migrations | -| `pnpm drizzle generate` | Generate a new migration after schema changes | -| `pnpm drizzle:verify-bootstrap` | Create a temporary empty database and verify `pnpm drizzle migrate` bootstraps it cleanly | -| `pnpm dev:db:reset` | Drop all app-owned schemas in the local dev database, recreate `public`, and leave the DB truly empty before re-migrating | -| `pnpm --filter web stripe` | Start Stripe webhook forwarding to localhost | -| `pnpm test:e2e` | Run Playwright end-to-end tests | +| Command | Description | +|---|---| +| `pnpm dev:start` | Start all local services in a tmux dashboard | +| `pnpm dev:stop` | Stop the tmux session and all services | +| `pnpm dev:env` | Sync `.dev.vars` files from `.env.local` (see [Worker `.dev.vars` setup](#worker-dev-vars-setup)) | +| `pnpm test` | Run the Jest test suite | +| `pnpm typecheck` | Run the TypeScript type checker | +| `pnpm lint` | Lint all source files | +| `pnpm format` | Format all supported files with oxfmt | +| `pnpm format:changed` | Format only files changed since `main` | +| `pnpm validate` | Run typecheck, lint, and tests | +| `pnpm drizzle migrate` | Apply pending database migrations | +| `pnpm drizzle generate` | Generate a new migration after schema changes | +| `pnpm drizzle:verify-bootstrap` | Create a temporary empty database and verify `pnpm drizzle migrate` bootstraps it cleanly | +| `pnpm dev:db:reset` | Drop all app-owned schemas in the local dev database, recreate `public`, and leave the DB truly empty before re-migrating | +| `pnpm --filter web stripe` | Start Stripe webhook forwarding to localhost | +| `pnpm test:e2e` | Run Playwright end-to-end tests | ## Git Workflow @@ -321,11 +321,11 @@ UPDATE organizations SET require_seats = false WHERE id = ''; Trial status is checked at three layers: -| Layer | Mechanism | Bypassed by `require_seats = false` | -| -------------- | --------------------------------------------------------------------------------- | ----------------------------------- | -| tRPC mutations | `requireActiveSubscriptionOrTrial()` middleware throws `FORBIDDEN` on hard expiry | Yes | -| Login redirect | `isOrganizationHardLocked()` redirects to `/profile` | Yes | -| Client UI | `OrganizationTrialWrapper` shows banners and lock dialogs | Yes | +| Layer | Mechanism | Bypassed by `require_seats = false` | +|---|---|---| +| tRPC mutations | `requireActiveSubscriptionOrTrial()` middleware throws `FORBIDDEN` on hard expiry | Yes | +| Login redirect | `isOrganizationHardLocked()` redirects to `/profile` | Yes | +| Client UI | `OrganizationTrialWrapper` shows banners and lock dialogs | Yes | ### Test organizations with various trial states @@ -372,12 +372,12 @@ The script (`dev/local/env-sync/`) scans every `.dev.vars.example` in the repo, Values are resolved using annotations in `.dev.vars.example` comment lines: -| Annotation | What it does | Example | -| ------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------- | -| _(none)_ | Copies the value from `.env.local` if the key matches, otherwise keeps the default | `INTERNAL_API_SECRET=your-secret-here` | -| `# @url ` | Builds `http://localhost:` from the service's dev port in `wrangler.jsonc` | `# @url nextjs` → `http://localhost:3000` | -| `# @from ` | Copies the value of a _different_ key from `.env.local` | `# @from CODE_REVIEW_WORKER_AUTH_TOKEN` | -| `# @pkcs8` | Copies from `.env.local` and converts PKCS#1 PEM keys to PKCS#8 format | `# @pkcs8` above a private key var | +| Annotation | What it does | Example | +|---|---|---| +| _(none)_ | Copies the value from `.env.local` if the key matches, otherwise keeps the default | `INTERNAL_API_SECRET=your-secret-here` | +| `# @url ` | Builds `http://localhost:` from the service's dev port in `wrangler.jsonc` | `# @url nextjs` → `http://localhost:3000` | +| `# @from ` | Copies the value of a _different_ key from `.env.local` | `# @from CODE_REVIEW_WORKER_AUTH_TOKEN` | +| `# @pkcs8` | Copies from `.env.local` and converts PKCS#1 PEM keys to PKCS#8 format | `# @pkcs8` above a private key var | For example, in a `.dev.vars.example`: diff --git a/apps/web/src/docs/network-compliance-diagram.md b/apps/web/src/docs/network-compliance-diagram.md index 07428e2586..342bf57340 100644 --- a/apps/web/src/docs/network-compliance-diagram.md +++ b/apps/web/src/docs/network-compliance-diagram.md @@ -274,14 +274,14 @@ sequenceDiagram ### **Data Protection & Privacy** -| Service | Data Type | Location | Compliance | -| --------------- | -------------------------------------------- | ------------- | --------------------- | -| **Supabase** | User profiles, usage data, organization data | EU/US regions | GDPR, SOC 2 | -| **Stytch** | Device fingerprints, fraud scores | US | SOC 2, Privacy Shield | -| **Stripe** | Payment data, customer billing info | Global | PCI DSS, GDPR | -| **Sentry** | Error logs, performance data | US/EU | GDPR, SOC 2 | -| **PostHog** | Analytics events, feature flags | US/EU | GDPR | -| **Customer.io** | Email addresses, communication preferences | US | GDPR, CAN-SPAM | +| Service | Data Type | Location | Compliance | +|---|---|---|---| +| **Supabase** | User profiles, usage data, organization data | EU/US regions | GDPR, SOC 2 | +| **Stytch** | Device fingerprints, fraud scores | US | SOC 2, Privacy Shield | +| **Stripe** | Payment data, customer billing info | Global | PCI DSS, GDPR | +| **Sentry** | Error logs, performance data | US/EU | GDPR, SOC 2 | +| **PostHog** | Analytics events, feature flags | US/EU | GDPR | +| **Customer.io** | Email addresses, communication preferences | US | GDPR, CAN-SPAM | ### **Security Measures** @@ -359,14 +359,14 @@ HTTPS Endpoints: ### **Risk Assessment** -| Risk Level | Component | Mitigation | -| ---------- | ------------------- | ------------------------------------------- | -| **HIGH** | Payment Processing | Stripe PCI DSS compliance, no local storage | -| **HIGH** | AI Model Access | Rate limiting, usage quotas, authentication | -| **MEDIUM** | User Authentication | Multi-factor options, fraud detection | -| **MEDIUM** | Data Storage | Encrypted connections, regular backups | -| **LOW** | Analytics | Anonymized data, GDPR compliance | -| **LOW** | Error Reporting | Sanitized logs, no PII in errors | +| Risk Level | Component | Mitigation | +|---|---|---| +| **HIGH** | Payment Processing | Stripe PCI DSS compliance, no local storage | +| **HIGH** | AI Model Access | Rate limiting, usage quotas, authentication | +| **MEDIUM** | User Authentication | Multi-factor options, fraud detection | +| **MEDIUM** | Data Storage | Encrypted connections, regular backups | +| **LOW** | Analytics | Anonymized data, GDPR compliance | +| **LOW** | Error Reporting | Sanitized logs, no PII in errors | --- diff --git a/apps/web/src/emails/AGENTS.md b/apps/web/src/emails/AGENTS.md index be444e0fb0..bc5093a842 100644 --- a/apps/web/src/emails/AGENTS.md +++ b/apps/web/src/emails/AGENTS.md @@ -6,23 +6,23 @@ These HTML files are server-rendered transactional email templates, sent via Mai All templates use a light-mode design system aligned with the Customer.io marketing template: -| Property | Value | -| ----------------------------- | ------------------------------------------------------------------------------------- | -| Font | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif` | -| Page background | `#ffffff` | -| Content width | 520px max | -| H1 / strong / emphasis | `#1a1a1a`, 24px/700 | -| Body text | `#333`, 14–15px, line-height 1.5–1.7 | -| Secondary text (inside boxes) | `#555`, 13–14px | -| Inline links | `#1a1a1a` underlined | -| Info-box background | `#f6f6f4` | -| Info-box border | `1px solid #ebebea` | -| Info-box border-radius | `10px` | -| Primary CTA button background | `#1a1a1a` | -| Primary CTA button text | `#ffffff`, 13px/600, `border-radius: 7px`, `padding: 10px 20px` | -| Section divider | `1px solid #ebebea` | -| Footer divider | `1px solid #eee` | -| Footer legal / address | `#ccc`, 11px | +| Property | Value | +|---|---| +| Font | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif` | +| Page background | `#ffffff` | +| Content width | 520px max | +| H1 / strong / emphasis | `#1a1a1a`, 24px/700 | +| Body text | `#333`, 14–15px, line-height 1.5–1.7 | +| Secondary text (inside boxes) | `#555`, 13–14px | +| Inline links | `#1a1a1a` underlined | +| Info-box background | `#f6f6f4` | +| Info-box border | `1px solid #ebebea` | +| Info-box border-radius | `10px` | +| Primary CTA button background | `#1a1a1a` | +| Primary CTA button text | `#ffffff`, 13px/600, `border-radius: 7px`, `padding: 10px 20px` | +| Section divider | `1px solid #ebebea` | +| Footer divider | `1px solid #eee` | +| Footer legal / address | `#ccc`, 11px | ## Content Guidelines @@ -61,37 +61,37 @@ Every template must include this branding footer below the content table: ## Template Variables -| Template file | Variables | Customer.io ID (crosswalk) | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -| `orgSubscription.html` | `seats`, `organization_url`, `invoices_url`, `year` | `10` | -| `orgRenewed.html` | `seats`, `invoices_url`, `year` | `11` | -| `orgCancelled.html` | `invoices_url`, `year` | `12` | -| `orgSSOUserJoined.html` | `new_user_email`, `organization_url`, `year` | `13` | -| `orgInvitation.html` | `organization_name`, `inviter_name`, `accept_invite_url`, `year` | `6` | -| `magicLink.html` | `magic_link_url`, `email`, `expires_in`, `year` | `14` | -| `balanceAlert.html` | `minimum_balance`, `organization_url`, `year` | `16` | -| `autoTopUpFailed.html` | `reason`, `credits_url`, `year` | `17` | -| `ossInviteNewUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `accept_invite_url`, `integrations_url`, `code_reviews_url`, `year` | `18` | -| `ossInviteExistingUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | `19` | -| `ossExistingOrgProvisioned.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | `20` | -| `deployFailed.html` | `deployment_name`, `deployment_url`, `repository`, `year` | `21` | -| `clawTrialEndingSoon.html` | `days_remaining`, `claw_url`, `year` | `22` | -| `clawTrialExpiresTomorrow.html` | `claw_url`, `year` | `23` | -| `clawSuspendedTrial.html` | `destruction_date`, `claw_url`, `year` | `24` | -| `clawSuspendedSubscription.html` | `destruction_date`, `claw_url`, `year` | `25` | -| `clawSuspendedPayment.html` | `destruction_date`, `claw_url`, `year` | `26` | -| `clawDestructionWarning.html` | `destruction_date`, `claw_url`, `instance_label`, `instance_id_short`, `year` | `27` | -| `clawInstanceReady.html` | `claw_url`, `year` | — | -| `clawInstanceDestroyed.html` | `claw_url`, `year` | `28` | -| `clawOrganizationTrialSuspendedBillingAuthority.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_billing_url`, `year` | — | -| `clawOrganizationTrialSuspendedUser.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_claw_url`, `year` | — | -| `clawOrganizationDestructionWarningBillingAuthority.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_billing_url`, `year` | — | -| `clawOrganizationDestructionWarningUser.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_claw_url`, `year` | — | -| `clawOrganizationInstanceDestroyedBillingAuthority.html` | `organization_name`, `instance_label`, `organization_billing_url`, `year` | — | -| `clawOrganizationInstanceDestroyedUser.html` | `organization_name`, `instance_label`, `organization_claw_url`, `year` | — | -| `clawEarlybirdEndingSoon.html` | `days_remaining`, `expiry_date`, `claw_url`, `year` | `29` | -| `clawEarlybirdExpiresTomorrow.html` | `expiry_date`, `claw_url`, `year` | `30` | -| `clawComplementaryInferenceEnded.html` | `claw_url`, `year` | — | -| `accountDeletionRequest.html` | `email`, `year` | — | -| `creditsTopUp.html` | `heading`, `intro`, `amount_usd`, `credits_usd`, `purchase_date`, `credits_url`, `receipt_section`, `year`. Org variants render org-specific copy into `intro` before template rendering; when provided, the organization name is interpolated there rather than passed as a separate template variable. | — | -| `kiloClawSubscriptionStarted.html` | `plan_name`, `price_usd`, `billing_period`, `next_billing_date`, `manage_url`, `year` | — | +| Template file | Variables | Customer.io ID (crosswalk) | +|---|---|---| +| `orgSubscription.html` | `seats`, `organization_url`, `invoices_url`, `year` | `10` | +| `orgRenewed.html` | `seats`, `invoices_url`, `year` | `11` | +| `orgCancelled.html` | `invoices_url`, `year` | `12` | +| `orgSSOUserJoined.html` | `new_user_email`, `organization_url`, `year` | `13` | +| `orgInvitation.html` | `organization_name`, `inviter_name`, `accept_invite_url`, `year` | `6` | +| `magicLink.html` | `magic_link_url`, `email`, `expires_in`, `year` | `14` | +| `balanceAlert.html` | `minimum_balance`, `organization_url`, `year` | `16` | +| `autoTopUpFailed.html` | `reason`, `credits_url`, `year` | `17` | +| `ossInviteNewUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `accept_invite_url`, `integrations_url`, `code_reviews_url`, `year` | `18` | +| `ossInviteExistingUser.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | `19` | +| `ossExistingOrgProvisioned.html` | `tier_name`, `seats`, `seat_value`, `credits_section`, `organization_url`, `integrations_url`, `code_reviews_url`, `year` | `20` | +| `deployFailed.html` | `deployment_name`, `deployment_url`, `repository`, `year` | `21` | +| `clawTrialEndingSoon.html` | `days_remaining`, `claw_url`, `year` | `22` | +| `clawTrialExpiresTomorrow.html` | `claw_url`, `year` | `23` | +| `clawSuspendedTrial.html` | `destruction_date`, `claw_url`, `year` | `24` | +| `clawSuspendedSubscription.html` | `destruction_date`, `claw_url`, `year` | `25` | +| `clawSuspendedPayment.html` | `destruction_date`, `claw_url`, `year` | `26` | +| `clawDestructionWarning.html` | `destruction_date`, `claw_url`, `instance_label`, `instance_id_short`, `year` | `27` | +| `clawInstanceReady.html` | `claw_url`, `year` | — | +| `clawInstanceDestroyed.html` | `claw_url`, `year` | `28` | +| `clawOrganizationTrialSuspendedBillingAuthority.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_billing_url`, `year` | — | +| `clawOrganizationTrialSuspendedUser.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_claw_url`, `year` | — | +| `clawOrganizationDestructionWarningBillingAuthority.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_billing_url`, `year` | — | +| `clawOrganizationDestructionWarningUser.html` | `organization_name`, `instance_label`, `destruction_date`, `organization_claw_url`, `year` | — | +| `clawOrganizationInstanceDestroyedBillingAuthority.html` | `organization_name`, `instance_label`, `organization_billing_url`, `year` | — | +| `clawOrganizationInstanceDestroyedUser.html` | `organization_name`, `instance_label`, `organization_claw_url`, `year` | — | +| `clawEarlybirdEndingSoon.html` | `days_remaining`, `expiry_date`, `claw_url`, `year` | `29` | +| `clawEarlybirdExpiresTomorrow.html` | `expiry_date`, `claw_url`, `year` | `30` | +| `clawComplementaryInferenceEnded.html` | `claw_url`, `year` | — | +| `accountDeletionRequest.html` | `email`, `year` | — | +| `creditsTopUp.html` | `heading`, `intro`, `amount_usd`, `credits_usd`, `purchase_date`, `credits_url`, `receipt_section`, `year`. Org variants render org-specific copy into `intro` before template rendering; when provided, the organization name is interpolated there rather than passed as a separate template variable. | — | +| `kiloClawSubscriptionStarted.html` | `plan_name`, `price_usd`, `billing_period`, `next_billing_date`, `manage_url`, `year` | — | diff --git a/apps/web/src/lib/security-agent/README.md b/apps/web/src/lib/security-agent/README.md index 3ce727466e..74b5f20907 100644 --- a/apps/web/src/lib/security-agent/README.md +++ b/apps/web/src/lib/security-agent/README.md @@ -122,16 +122,16 @@ src/lib/security-reviews/ ### tRPC Router (`src/routers/security-reviews-router.ts`) -| Procedure | Type | Description | -| --------------------- | -------- | --------------------------------------------------------- | -| `getPermissionStatus` | Query | Check if GitHub App has `vulnerability_alerts` permission | -| `getConfig` | Query | Get SLA configuration | -| `saveConfig` | Mutation | Save SLA configuration | -| `setEnabled` | Mutation | Enable/disable security reviews | -| `listFindings` | Query | List findings with filters | -| `getFinding` | Query | Get single finding by ID | -| `getStats` | Query | Get summary statistics | -| `triggerSync` | Mutation | **Manual trigger** to sync a specific repository | +| Procedure | Type | Description | +|---|---|---| +| `getPermissionStatus` | Query | Check if GitHub App has `vulnerability_alerts` permission | +| `getConfig` | Query | Get SLA configuration | +| `saveConfig` | Mutation | Save SLA configuration | +| `setEnabled` | Mutation | Enable/disable security reviews | +| `listFindings` | Query | List findings with filters | +| `getFinding` | Query | Get single finding by ID | +| `getStats` | Query | Get summary statistics | +| `triggerSync` | Mutation | **Manual trigger** to sync a specific repository | ### Cron Job (`src/app/api/cron/sync-security-alerts/route.ts`) @@ -145,33 +145,33 @@ src/lib/security-reviews/ ### Table: `security_findings` -| Column | Type | Description | -| -------------------------- | --------- | ----------------------------------------- | -| `id` | UUID | Primary key | -| `owned_by_organization_id` | UUID | Organization owner (nullable) | -| `owned_by_user_id` | TEXT | User owner (nullable) | -| `platform_integration_id` | UUID | Reference to platform_integrations | -| `repo_full_name` | TEXT | Repository full name (e.g., "owner/repo") | -| `source` | TEXT | Source type: 'dependabot' | -| `source_id` | TEXT | Alert number from source | -| `severity` | TEXT | 'critical', 'high', 'medium', 'low' | -| `ghsa_id` | TEXT | GitHub Security Advisory ID | -| `cve_id` | TEXT | CVE ID (nullable) | -| `package_name` | TEXT | Vulnerable package name | -| `package_ecosystem` | TEXT | Package ecosystem (npm, pip, etc.) | -| `vulnerable_version_range` | TEXT | Affected versions | -| `patched_version` | TEXT | Fixed version (nullable) | -| `manifest_path` | TEXT | Path to manifest file | -| `title` | TEXT | Finding title | -| `description` | TEXT | Full description | -| `status` | TEXT | 'open', 'fixed', 'ignored' | -| `ignored_reason` | TEXT | Reason for dismissal | -| `fixed_at` | TIMESTAMP | When fixed | -| `sla_due_at` | TIMESTAMP | SLA deadline | -| `dependabot_html_url` | TEXT | Link to Dependabot alert | -| `raw_data` | JSONB | Original API response | -| `first_detected_at` | TIMESTAMP | When first seen | -| `last_synced_at` | TIMESTAMP | Last sync time | +| Column | Type | Description | +|---|---|---| +| `id` | UUID | Primary key | +| `owned_by_organization_id` | UUID | Organization owner (nullable) | +| `owned_by_user_id` | TEXT | User owner (nullable) | +| `platform_integration_id` | UUID | Reference to platform_integrations | +| `repo_full_name` | TEXT | Repository full name (e.g., "owner/repo") | +| `source` | TEXT | Source type: 'dependabot' | +| `source_id` | TEXT | Alert number from source | +| `severity` | TEXT | 'critical', 'high', 'medium', 'low' | +| `ghsa_id` | TEXT | GitHub Security Advisory ID | +| `cve_id` | TEXT | CVE ID (nullable) | +| `package_name` | TEXT | Vulnerable package name | +| `package_ecosystem` | TEXT | Package ecosystem (npm, pip, etc.) | +| `vulnerable_version_range` | TEXT | Affected versions | +| `patched_version` | TEXT | Fixed version (nullable) | +| `manifest_path` | TEXT | Path to manifest file | +| `title` | TEXT | Finding title | +| `description` | TEXT | Full description | +| `status` | TEXT | 'open', 'fixed', 'ignored' | +| `ignored_reason` | TEXT | Reason for dismissal | +| `fixed_at` | TIMESTAMP | When fixed | +| `sla_due_at` | TIMESTAMP | SLA deadline | +| `dependabot_html_url` | TEXT | Link to Dependabot alert | +| `raw_data` | JSONB | Original API response | +| `first_detected_at` | TIMESTAMP | When first seen | +| `last_synced_at` | TIMESTAMP | Last sync time | **Constraints:** @@ -185,11 +185,11 @@ src/lib/security-reviews/ Default SLAs stored in `agent_configs` table with `agent_type: 'security_scan'`: | Severity | Default SLA (days) | -| -------- | ------------------ | -| Critical | 15 | -| High | 30 | -| Medium | 45 | -| Low | 90 | +|---|---| +| Critical | 15 | +| High | 30 | +| Medium | 45 | +| Low | 90 | --- @@ -228,11 +228,11 @@ The cron job at `/api/cron/sync-security-alerts` runs every 6 hours and: ## State Mapping | Dependabot State | Our Status | -| ---------------- | ---------- | -| `open` | `open` | -| `fixed` | `fixed` | -| `dismissed` | `ignored` | -| `auto_dismissed` | `ignored` | +|---|---| +| `open` | `open` | +| `fixed` | `fixed` | +| `dismissed` | `ignored` | +| `auto_dismissed` | `ignored` | --- diff --git a/design.md b/design.md index 6e8edeb850..ceed1d04a9 100644 --- a/design.md +++ b/design.md @@ -282,16 +282,16 @@ The single concession to atmosphere is the cloud-agent chat surface — terminal **Status colors follow one rigid pattern.** Every status badge is `bg-{color}-500/20 text-{color}-400 ring-1 ring-{color}-500/20`. The translucent fill + matching ring + brighter foreground is the system's most recognizable micro-pattern. Color assignments are fixed by domain, not by mood: -| Color | Domain | -| ------- | -------------------------------- | -| Blue | Cloud sessions (neutral default) | -| Purple | VS Code Extension | -| Zinc | CLI | -| Emerald | Slack | -| Orange | Agent Manager | -| Green | Success, "new" badges | -| Yellow | Warnings | -| Red | Destructive, errors | +| Color | Domain | +|---|---| +| Blue | Cloud sessions (neutral default) | +| Purple | VS Code Extension | +| Zinc | CLI | +| Emerald | Slack | +| Orange | Agent Manager | +| Green | Success, "new" badges | +| Yellow | Warnings | +| Red | Destructive, errors | Do not invent new status hues. Do not use status colors outside this badge pattern (e.g. don't use `red-500` as a button background — use `destructive` semantics through the dialog system). diff --git a/docs/do-sqlite-drizzle.md b/docs/do-sqlite-drizzle.md index 6c711b56a9..2ce00be755 100644 --- a/docs/do-sqlite-drizzle.md +++ b/docs/do-sqlite-drizzle.md @@ -36,12 +36,12 @@ Each Durable Object instance has its own isolated SQLite database. Migrations ap ## Workers using this pattern -| Worker | Description | -| --------------------------------- | ---------------------------- | -| `cloud-agent` | Agent orchestration | -| `cloud-agent-next` | Next-gen agent orchestration | -| `cloudflare-ai-attribution` | AI code attribution | -| `cloudflare-app-builder` | App builder | -| `cloudflare-o11y` | Observability | -| `cloudflare-session-ingest` | Session ingestion | -| `cloudflare-webhook-agent-ingest` | Webhook agent ingestion | +| Worker | Description | +|---|---| +| `cloud-agent` | Agent orchestration | +| `cloud-agent-next` | Next-gen agent orchestration | +| `cloudflare-ai-attribution` | AI code attribution | +| `cloudflare-app-builder` | App builder | +| `cloudflare-o11y` | Observability | +| `cloudflare-session-ingest` | Session ingestion | +| `cloudflare-webhook-agent-ingest` | Webhook agent ingestion | diff --git a/script/check-md-table-padding.ts b/script/check-md-table-padding.ts new file mode 100644 index 0000000000..523f8b1fd1 --- /dev/null +++ b/script/check-md-table-padding.ts @@ -0,0 +1,227 @@ +#!/usr/bin/env bun + +/** + * Enforces the "no padded markdown tables" rule from AGENTS.md. + * + * Prettier pads markdown table cells for column alignment. Any content change + * then re-pads every row, which pollutes diffs on untouched lines. Markdown is + * in .prettierignore, but hand-written padded tables still sneak in — this + * check catches them. + * + * Usage: + * bun run script/check-md-table-padding.ts # check all tracked *.md + * bun run script/check-md-table-padding.ts path/to/file.md [more.md ...] + * bun run script/check-md-table-padding.ts --fix [paths…] # rewrite in place + * + * What counts as a failure: + * - Separator row cells are anything other than `---`, `:---`, `---:`, + * `:---:` (after trimming a single optional leading/trailing space). + * - Content row cells have more than one space of padding between the + * content and the enclosing pipes. + */ + +import { spawnSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +const ok = new Set(['---', ':---', '---:', ':---:']); + +function tracked() { + const r = spawnSync('git', ['ls-files', '*.md'], { cwd: ROOT, encoding: 'utf8' }); + if (r.status !== 0) { + console.error(r.stderr?.trim() || 'git ls-files failed'); + process.exit(1); + } + return r.stdout.split('\n').filter(Boolean); +} + +function skip(file: string) { + const norm = file.replaceAll('\\', '/').toLowerCase(); + if (norm.startsWith('.changeset/')) return true; + if (norm === 'changelog.md' || norm.endsWith('/changelog.md')) return true; + if (norm.includes('node_modules/')) return true; + return false; +} + +type Issue = { file: string; line: number; kind: 'separator' | 'content'; detail: string }; + +function split(row: string) { + // Split on unescaped pipes, drop the empty leading/trailing cells that come + // from rows starting and ending with a pipe. + const cells: string[] = []; + let buf = ''; + for (let i = 0; i < row.length; i++) { + const c = row[i]; + if (c === '\\' && row[i + 1] === '|') { + buf += '\\|'; + i++; + continue; + } + if (c === '|') { + cells.push(buf); + buf = ''; + continue; + } + buf += c; + } + cells.push(buf); + if (cells.length >= 2 && cells[0].trim() === '') cells.shift(); + if (cells.length >= 1 && cells[cells.length - 1].trim() === '') cells.pop(); + return cells; +} + +function isSep(row: string) { + // A separator row contains only pipes, dashes, colons, and spaces, and has + // at least one dash. + if (!/\|/.test(row)) return false; + if (!/-/.test(row)) return false; + return /^[\s|:\-]+$/.test(row); +} + +function check(file: string): Issue[] { + const src = readFileSync(path.join(ROOT, file), 'utf8'); + const lines = src.split('\n'); + const issues: Issue[] = []; + let fence: string | null = null; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trim = line.trim(); + // Track fenced code blocks so we don't lint tables inside them. + const fenceMatch = trim.match(/^(`{3,}|~{3,})/); + if (fenceMatch) { + const marker = fenceMatch[1][0]; + if (fence === null) fence = marker; + else if (marker === fence) fence = null; + continue; + } + if (fence !== null) continue; + if (!line.trimStart().startsWith('|')) continue; + + if (isSep(line)) { + const cells = split(line.trim()); + for (const cell of cells) { + // Cells must be exactly ---, :---, ---: or :---: with no surrounding + // whitespace. Anything longer (or with space padding) is column-width + // alignment and re-pads on every content change. + if (!ok.has(cell)) { + issues.push({ + file, + line: i + 1, + kind: 'separator', + detail: `separator cell "${cell}" is padded or extended — use ---, :---, ---: or :---: with no surrounding spaces`, + }); + break; + } + } + continue; + } + + // Content row: detect padding (>1 space between content and pipe). + // Only inspect lines that look like table rows: starts and ends with `|`. + if (!line.trimStart().startsWith('|') || !line.trimEnd().endsWith('|')) continue; + const cells = split(line.trim()); + for (const cell of cells) { + if (cell.trim() === '') continue; + const leading = cell.match(/^ */)![0].length; + const trailing = cell.match(/ *$/)![0].length; + if (leading > 1 || trailing > 1) { + issues.push({ + file, + line: i + 1, + kind: 'content', + detail: `content cell "${cell}" has extra padding — use a single space on each side`, + }); + break; + } + } + } + return issues; +} + +function fixSepCell(raw: string) { + const t = raw.trim(); + const left = t.startsWith(':'); + const right = t.endsWith(':'); + if (left && right) return ':---:'; + if (left) return ':---'; + if (right) return '---:'; + return '---'; +} + +function rewriteRow(row: string, separator: boolean) { + // Preserve leading whitespace of the row itself (table indentation). + const indent = row.match(/^\s*/)![0]; + const body = row.slice(indent.length); + const cells = split(body); + if (cells.length === 0) return row; + if (separator) return `${indent}|${cells.map(fixSepCell).join('|')}|`; + return `${indent}| ${cells.map(c => c.trim()).join(' | ')} |`; +} + +function fix(file: string) { + const src = readFileSync(path.join(ROOT, file), 'utf8'); + const lines = src.split('\n'); + let changed = false; + let fence: string | null = null; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trim = line.trim(); + const fenceMatch = trim.match(/^(`{3,}|~{3,})/); + if (fenceMatch) { + const marker = fenceMatch[1][0]; + if (fence === null) fence = marker; + else if (marker === fence) fence = null; + continue; + } + if (fence !== null) continue; + if (!line.trimStart().startsWith('|')) continue; + if (!line.trimEnd().endsWith('|')) continue; + + const sep = isSep(line); + const next = rewriteRow(line, sep); + if (next !== line) { + lines[i] = next; + changed = true; + } + } + if (changed) writeFileSync(path.join(ROOT, file), lines.join('\n')); + return changed; +} + +const argv = process.argv.slice(2); +const fixFlag = argv.includes('--fix'); +const paths = argv.filter(a => a !== '--fix'); +const files = (paths.length > 0 ? paths : tracked()).filter(f => !skip(f)); + +if (fixFlag) { + let n = 0; + for (const f of files) if (fix(f)) n++; + console.log(`check-md-table-padding --fix: rewrote ${n} file(s).`); + process.exit(0); +} + +const all: Issue[] = []; +for (const f of files) { + for (const issue of check(f)) all.push(issue); +} + +if (all.length === 0) { + console.log(`check-md-table-padding: ${files.length} file(s) checked, no padded tables found.`); + process.exit(0); +} + +for (const i of all) { + console.error(`${i.file}:${i.line} [${i.kind}] ${i.detail}`); +} +console.error(''); +console.error( + `Found ${all.length} padded table row(s) across ${new Set(all.map(i => i.file)).size} file(s).` +); +console.error( + 'Fix: rewrite the table separator as |---|---| and use single-space padding on content cells.' +); +console.error('Or run: bun run script/check-md-table-padding.ts --fix'); +console.error('See AGENTS.md > Markdown Tables.'); +process.exit(1); diff --git a/services/cloud-agent-next/test/e2e/README.md b/services/cloud-agent-next/test/e2e/README.md index 7b1f3399bc..33de321e7a 100644 --- a/services/cloud-agent-next/test/e2e/README.md +++ b/services/cloud-agent-next/test/e2e/README.md @@ -96,13 +96,13 @@ family created for that scenario. Per-run overrides via env vars: -| Var | Default | -| -------------- | ------------------------------------------------------------- | -| `WORKER_URL` | `http://localhost:8794` | -| `FAKE_LLM_URL` | `http://localhost:8811` (host-side view) | -| `E2E_GIT_URL` | `https://github.com/octocat/Hello-World.git` | -| `E2E_MODEL` | `kilo/fake-deterministic` (the only model the fake serves) | -| `DATABASE_URL` | Optional direct database URL override for this harness | +| Var | Default | +|---|---| +| `WORKER_URL` | `http://localhost:8794` | +| `FAKE_LLM_URL` | `http://localhost:8811` (host-side view) | +| `E2E_GIT_URL` | `https://github.com/octocat/Hello-World.git` | +| `E2E_MODEL` | `kilo/fake-deterministic` (the only model the fake serves) | +| `DATABASE_URL` | Optional direct database URL override for this harness | | `POSTGRES_URL` | Repo database fallback loaded from root `.env.local` / `.env` | If `DATABASE_URL` is unset, the standalone TSX driver loads root `.env.local` @@ -122,14 +122,14 @@ A conversation directive is embedded in the user-visible prompt as from the last user message and dispatches the matching scenario. The source of directive truth is `test/e2e/fake-llm-server.ts`. -| Directive | Behavior | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| `echo:` | One SSE chunk with `delta.content = `, then `finish_reason: stop`, then `[DONE]`. | -| `slow::` | `n` content chunks `` apart, then stop + `[DONE]`. Used for pacing/timing probes. | -| `idle` | One empty-delta chunk, then stop + `[DONE]`. | -| `hang` | Opens the SSE stream but emits nothing and never closes. Drives abort/timeout paths. | -| `error:` | HTTP 402 with OpenAI-shaped error body carrying ``. Exercises kilo's error propagation. | -| `gate:` | Opens the SSE stream, emits no chunks, blocks until the driver calls `POST /test/release?tag=`. On release, emits `"done"` + stop + `[DONE]`. | +| Directive | Behavior | +|---|---| +| `echo:` | One SSE chunk with `delta.content = `, then `finish_reason: stop`, then `[DONE]`. | +| `slow::` | `n` content chunks `` apart, then stop + `[DONE]`. Used for pacing/timing probes. | +| `idle` | One empty-delta chunk, then stop + `[DONE]`. | +| `hang` | Opens the SSE stream but emits nothing and never closes. Drives abort/timeout paths. | +| `error:` | HTTP 402 with OpenAI-shaped error body carrying ``. Exercises kilo's error propagation. | +| `gate:` | Opens the SSE stream, emits no chunks, blocks until the driver calls `POST /test/release?tag=`. On release, emits `"done"` + stop + `[DONE]`. | Unknown or missing directives produce HTTP 402 with `unknown fake scenario: ` — easy to spot in fake-LLM logs. @@ -152,27 +152,27 @@ These are wrapped by `releaseGate()`, `waitForGateEngaged()`, and ## Lifecycle scenarios -| Lifecycle | What it does | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cold` | Fresh session; verify a new per-session sandbox appears and the conversation completes. | -| `hot` | Warmup with `echo:warmup`, then send the real prompt on the same session. Same container. | -| `followup` | Same as `hot` today; kept distinct for future resume-path splits. | -| `cold-hot` | One cold turn plus `echo:hot`, `slow:3:50`, and `echo:followup` hot turns on the same session/sandbox. | -| `external-kill` | Warmup, `docker kill` the sandbox, send another prompt, verify recovery/failure. | -| `kill-mid-flight` | Cold `hang`, kill while pending, verify DO surfaces disconnect/error. | -| `queue-while-busy` | Block on `gate:`, enqueue two echoes, release the gate, assert FIFO delivery through `cloud.message.*` events. | -| `queue-rapid-fire-no-gate` | Send immediate follow-ups behind `echo:first` and assert they reach their terminal FIFO state without gate coordination. | -| `queue-overflow` | Block on `gate:overflow`, fill the pending queue until enqueue fails with HTTP 429, release gate, drain. | -| `queue-interrupt-clears` | Block on `gate:`, enqueue two, `interruptSession`, assert `cloud.message.failed` with `reason: 'interrupted'` for each. | -| `llm-error` | Return fake provider HTTP 402 and assert the turn reaches a failed terminal event instead of hanging. | -| `chunked-streaming` | Stream delayed fake chunks and assert multiple downstream `message.part.delta` events survive. | -| `empty-response` | Run `idle`, assert completion, and assert no downstream `message.part.delta` is emitted. | -| `interrupt-mid-stream` | Interrupt an actively gated fake request and assert the active message is interrupted, not a queued message. | -| `unknown-model` | Use a model absent from the fake catalogue and require both the model error event and a terminal event. | -| `waiters-clean` | Complete a normal fake turn, then assert the fake server has no parked waiters or live responses. | -| `callback-completion` | Stand up local HTTP sink, register `callbackTarget.url`, run `echo:done`, assert the sink received `status: 'completed'`. | -| `callback-batch-followup` | Queue two turns behind a gated callback session, assert one callback for the final queued turn, then assert a later hot turn emits a fresh callback. | -| `callback-interrupt` | Local HTTP sink + gated active turn + `interruptSession`, assert callback fires with `status: 'interrupted'`. | +| Lifecycle | What it does | +|---|---| +| `cold` | Fresh session; verify a new per-session sandbox appears and the conversation completes. | +| `hot` | Warmup with `echo:warmup`, then send the real prompt on the same session. Same container. | +| `followup` | Same as `hot` today; kept distinct for future resume-path splits. | +| `cold-hot` | One cold turn plus `echo:hot`, `slow:3:50`, and `echo:followup` hot turns on the same session/sandbox. | +| `external-kill` | Warmup, `docker kill` the sandbox, send another prompt, verify recovery/failure. | +| `kill-mid-flight` | Cold `hang`, kill while pending, verify DO surfaces disconnect/error. | +| `queue-while-busy` | Block on `gate:`, enqueue two echoes, release the gate, assert FIFO delivery through `cloud.message.*` events. | +| `queue-rapid-fire-no-gate` | Send immediate follow-ups behind `echo:first` and assert they reach their terminal FIFO state without gate coordination. | +| `queue-overflow` | Block on `gate:overflow`, fill the pending queue until enqueue fails with HTTP 429, release gate, drain. | +| `queue-interrupt-clears` | Block on `gate:`, enqueue two, `interruptSession`, assert `cloud.message.failed` with `reason: 'interrupted'` for each. | +| `llm-error` | Return fake provider HTTP 402 and assert the turn reaches a failed terminal event instead of hanging. | +| `chunked-streaming` | Stream delayed fake chunks and assert multiple downstream `message.part.delta` events survive. | +| `empty-response` | Run `idle`, assert completion, and assert no downstream `message.part.delta` is emitted. | +| `interrupt-mid-stream` | Interrupt an actively gated fake request and assert the active message is interrupted, not a queued message. | +| `unknown-model` | Use a model absent from the fake catalogue and require both the model error event and a terminal event. | +| `waiters-clean` | Complete a normal fake turn, then assert the fake server has no parked waiters or live responses. | +| `callback-completion` | Stand up local HTTP sink, register `callbackTarget.url`, run `echo:done`, assert the sink received `status: 'completed'`. | +| `callback-batch-followup` | Queue two turns behind a gated callback session, assert one callback for the final queued turn, then assert a later hot turn emits a fresh callback. | +| `callback-interrupt` | Local HTTP sink + gated active turn + `interruptSession`, assert callback fires with `status: 'interrupted'`. | ### API dimension diff --git a/services/deploy-infra/dispatcher/http-requests/README.md b/services/deploy-infra/dispatcher/http-requests/README.md index 0424e877f5..8ecda28854 100644 --- a/services/deploy-infra/dispatcher/http-requests/README.md +++ b/services/deploy-infra/dispatcher/http-requests/README.md @@ -21,8 +21,8 @@ The `.env` file is gitignored so your credentials won't be committed. ## Files -| File | Purpose | -| ----------------- | ------------------------------------ | +| File | Purpose | +|---|---| | `dispatcher.http` | All API requests (management + auth) | -| `.env.example` | Template for credentials | -| `.env` | Your credentials (gitignored) | +| `.env.example` | Template for credentials | +| `.env` | Your credentials (gitignored) | diff --git a/services/gastown/docs/e2e-pr-feedback-testing.md b/services/gastown/docs/e2e-pr-feedback-testing.md index febd3be03a..cbd4ff09f5 100644 --- a/services/gastown/docs/e2e-pr-feedback-testing.md +++ b/services/gastown/docs/e2e-pr-feedback-testing.md @@ -324,14 +324,14 @@ gh api repos/$REPO/pulls/$PR_NUMBER/comments \ ### Expected Timeline -| Step | Duration | -| ---------------------------- | ------------ | -| Mayor slings bead | ~30s | -| Polecat works + pushes code | 2-5 min | -| Refinery reviews, creates PR | 1-3 min | -| Rework cycle (if needed) | 2-5 min | -| Auto-merge grace period | 2 min | -| **Total** | **6-15 min** | +| Step | Duration | +|---|---| +| Mayor slings bead | ~30s | +| Polecat works + pushes code | 2-5 min | +| Refinery reviews, creates PR | 1-3 min | +| Rework cycle (if needed) | 2-5 min | +| Auto-merge grace period | 2 min | +| **Total** | **6-15 min** | --- @@ -485,15 +485,15 @@ gh api repos/$REPO/pulls/$PR_NUMBER/comments \ ### Expected Timeline -| Step | Duration | -| ------------------------------ | ----------------- | -| Mayor slings bead | ~30s | -| Polecat works + creates PR | 2-5 min | -| Human adds review comment | manual | -| Feedback detected by poll_pr | ~30s (poll cycle) | -| Polecat resolves feedback | 1-3 min | -| Auto-merge grace period | 2 min | -| **Total (from human comment)** | **4-6 min** | +| Step | Duration | +|---|---| +| Mayor slings bead | ~30s | +| Polecat works + creates PR | 2-5 min | +| Human adds review comment | manual | +| Feedback detected by poll_pr | ~30s (poll cycle) | +| Polecat resolves feedback | 1-3 min | +| Auto-merge grace period | 2 min | +| **Total (from human comment)** | **4-6 min** | --- @@ -776,11 +776,11 @@ Unlike Test B (which goes through the mayor's `gt_sling_batch` tool via a chat m These dev-only endpoints are used by this test (all `application/json`, no auth in dev): -| Method | Path | Description | -| ------ | ----------------------------------- | ---------------------------------------------------------------- | -| GET | `/debug/towns/:townId/rigs` | List rigs registered with the town (returns `{ rigs: [...] }`) | -| POST | `/debug/towns/:townId/sling-convoy` | Directly call `Town.slingConvoy()` — bypasses the mayor | -| GET | `/debug/towns/:townId/convoys` | List active convoys with progress (`closed_beads`/`total_beads`) | +| Method | Path | Description | +|---|---|---| +| GET | `/debug/towns/:townId/rigs` | List rigs registered with the town (returns `{ rigs: [...] }`) | +| POST | `/debug/towns/:townId/sling-convoy` | Directly call `Town.slingConvoy()` — bypasses the mayor | +| GET | `/debug/towns/:townId/convoys` | List active convoys with progress (`closed_beads`/`total_beads`) | The `sling-convoy` body matches `Town.slingConvoy()` input: @@ -935,15 +935,15 @@ After the convoy closes, check: ### Expected Timeline (review-then-land, 3 beads) -| Step | Duration | -| --------------------------------------------------- | ------------- | -| Sling-convoy creates 3 issue beads + convoy bead | ~1s | -| Bead 1 polecat work + sub-PR + refinery merge | 3-5 min | -| Bead 2 polecat work + sub-PR + refinery merge | 3-5 min | -| Bead 3 polecat work + sub-PR + refinery merge | 3-5 min | -| Landing MR created, refinery reviews PR into `main` | 2-3 min | -| Auto-merge grace period | 2 min | -| **Total** | **15-25 min** | +| Step | Duration | +|---|---| +| Sling-convoy creates 3 issue beads + convoy bead | ~1s | +| Bead 1 polecat work + sub-PR + refinery merge | 3-5 min | +| Bead 2 polecat work + sub-PR + refinery merge | 3-5 min | +| Bead 3 polecat work + sub-PR + refinery merge | 3-5 min | +| Landing MR created, refinery reviews PR into `main` | 2-3 min | +| Auto-merge grace period | 2 min | +| **Total** | **15-25 min** | ### Known Issues Observed in This Flow @@ -957,46 +957,46 @@ After the convoy closes, check: ### Scenario 1 (No Review + Auto-Merge) -| Step | Duration | -| -------------------------- | ----------- | -| Mayor slings bead | ~30s | -| Polecat works + creates PR | 2-5 min | -| Auto-merge grace period | 2 min | -| **Total** | **5-8 min** | +| Step | Duration | +|---|---| +| Mayor slings bead | ~30s | +| Polecat works + creates PR | 2-5 min | +| Auto-merge grace period | 2 min | +| **Total** | **5-8 min** | ### Scenario 2 (Refinery Review + Auto-Merge) -| Step | Duration | -| ----------------------------- | ------------ | -| Mayor slings bead | ~30s | -| Polecat works + pushes code | 2-5 min | -| Refinery reviews + creates PR | 1-3 min | -| Rework cycle (if needed) | 2-5 min | -| Auto-merge grace period | 2 min | -| **Total** | **6-15 min** | +| Step | Duration | +|---|---| +| Mayor slings bead | ~30s | +| Polecat works + pushes code | 2-5 min | +| Refinery reviews + creates PR | 1-3 min | +| Rework cycle (if needed) | 2-5 min | +| Auto-merge grace period | 2 min | +| **Total** | **6-15 min** | ### Scenario 3 (Human Feedback + Auto-Resolve + Auto-Merge) -| Step | Duration | -| -------------------------- | ------------ | -| Mayor slings bead | ~30s | -| Polecat works + creates PR | 2-5 min | -| Human adds review comment | manual | -| Feedback detected | ~30s | -| Polecat resolves feedback | 1-3 min | -| Auto-merge grace period | 2 min | +| Step | Duration | +|---|---| +| Mayor slings bead | ~30s | +| Polecat works + creates PR | 2-5 min | +| Human adds review comment | manual | +| Feedback detected | ~30s | +| Polecat resolves feedback | 1-3 min | +| Auto-merge grace period | 2 min | | **Total (from task sent)** | **8-12 min** | ### 3-Bead Convoy (review-and-merge) -| Step | Duration | -| ---------------------------------------- | ------------------------ | -| Mayor creates convoy + 3 beads | ~1 min | -| 3 polecats work in parallel + create PRs | 2-5 min | -| 3 refinery reviews (sequential per rig) | 5-15 min | -| Feedback resolution cycles | 2-5 min each (if needed) | -| Auto-merge per PR | 2 min grace each | -| **Total** | **15-30 min** | +| Step | Duration | +|---|---| +| Mayor creates convoy + 3 beads | ~1 min | +| 3 polecats work in parallel + create PRs | 2-5 min | +| 3 refinery reviews (sequential per rig) | 5-15 min | +| Feedback resolution cycles | 2-5 min each (if needed) | +| Auto-merge per PR | 2 min grace each | +| **Total** | **15-30 min** | --- @@ -1057,21 +1057,21 @@ The wrangler container runtime occasionally fails to route DO `container.fetch() All `/debug/` endpoints are unauthenticated in development. In production, they're protected by Cloudflare Access. -| Method | Path | Description | -| ------- | ---------------------------------------- | -------------------------------------------------------- | -| `GET` | `/debug/towns/:townId/status` | Primary status: agents, beads, alarm, patrol, reconciler | -| `GET` | `/debug/towns/:townId/config` | Read town config (dev only) | -| `PATCH` | `/debug/towns/:townId/config` | Update town config (dev only, partial update) | -| `POST` | `/debug/towns/:townId/reconcile-dry-run` | Run reconciler without applying actions | -| `POST` | `/debug/towns/:townId/replay-events` | Replay events from time range | -| `GET` | `/debug/towns/:townId/drain-status` | Drain flag + nonce | -| `GET` | `/debug/towns/:townId/nudges` | Pending agent nudges | -| `POST` | `/debug/towns/:townId/send-message` | Send message to mayor (dev only) | -| `GET` | `/debug/towns/:townId/beads/:beadId` | Full bead details + review metadata + dependencies | -| `POST` | `/debug/towns/:townId/graceful-stop` | Trigger SIGTERM on container (dev only) | -| `GET` | `/debug/towns/:townId/rigs` | List rigs registered with the town (dev only) | -| `POST` | `/debug/towns/:townId/sling-convoy` | Directly call `Town.slingConvoy()` (dev only) | -| `GET` | `/debug/towns/:townId/convoys` | List active convoys with progress counts (dev only) | +| Method | Path | Description | +|---|---|---| +| `GET` | `/debug/towns/:townId/status` | Primary status: agents, beads, alarm, patrol, reconciler | +| `GET` | `/debug/towns/:townId/config` | Read town config (dev only) | +| `PATCH` | `/debug/towns/:townId/config` | Update town config (dev only, partial update) | +| `POST` | `/debug/towns/:townId/reconcile-dry-run` | Run reconciler without applying actions | +| `POST` | `/debug/towns/:townId/replay-events` | Replay events from time range | +| `GET` | `/debug/towns/:townId/drain-status` | Drain flag + nonce | +| `GET` | `/debug/towns/:townId/nudges` | Pending agent nudges | +| `POST` | `/debug/towns/:townId/send-message` | Send message to mayor (dev only) | +| `GET` | `/debug/towns/:townId/beads/:beadId` | Full bead details + review metadata + dependencies | +| `POST` | `/debug/towns/:townId/graceful-stop` | Trigger SIGTERM on container (dev only) | +| `GET` | `/debug/towns/:townId/rigs` | List rigs registered with the town (dev only) | +| `POST` | `/debug/towns/:townId/sling-convoy` | Directly call `Town.slingConvoy()` (dev only) | +| `GET` | `/debug/towns/:townId/convoys` | List active convoys with progress counts (dev only) | ### Inspect a Bead @@ -1130,11 +1130,11 @@ print(f'parent_bead_id: {bead.get(\"parent_bead_id\", \"NULL\")}') ### Summary -| Scenario | Config | Result | PR | Notes | -| -------------------------------- | ---------------------------------------------------- | ------------------- | ------------ | ------------------------------------------------------------------------------------------ | -| 1: No review + auto-merge | `code_review=false`, `auto_merge=true` | **PASS (with bug)** | #38 MERGED | Refinery dispatched despite `code_review=false` (bug persists). PR merged after rework. | -| 2: Refinery review (rework mode) | `code_review=true`, `review_mode=rework` | **FAIL** | None created | Polecat pushed branch but never created PR. Refinery stuck in rework-request loop 35+ min. | -| 3: Human feedback + auto-resolve | `code_review=false`, `auto_resolve_pr_feedback=true` | **FAIL** | None created | Same as Scenario 2: polecat pushed branch but no PR. MR bead stuck at `open`. | +| Scenario | Config | Result | PR | Notes | +|---|---|---|---|---| +| 1: No review + auto-merge | `code_review=false`, `auto_merge=true` | **PASS (with bug)** | #38 MERGED | Refinery dispatched despite `code_review=false` (bug persists). PR merged after rework. | +| 2: Refinery review (rework mode) | `code_review=true`, `review_mode=rework` | **FAIL** | None created | Polecat pushed branch but never created PR. Refinery stuck in rework-request loop 35+ min. | +| 3: Human feedback + auto-resolve | `code_review=false`, `auto_resolve_pr_feedback=true` | **FAIL** | None created | Same as Scenario 2: polecat pushed branch but no PR. MR bead stuck at `open`. | ### Bugs Found @@ -1229,20 +1229,20 @@ Task: "Add src/utils/set-helpers.ts with union, intersection, difference, symmetricDifference, isSubset" ``` -| Time (UTC) | Event | -| ---------- | ------------------------------------------------------------- | -| 19:38:42 | Task sent to mayor | -| 19:38:47 | Issue bead created, mayor hooks it | -| 19:43:51 | Mayor unhooks, polecat dispatched | -| 19:43:56 | Polecat hooks bead, starts implementing | -| 19:44:28 | Polecat: "Set helper file added; committing and pushing" | -| 19:44:43 | Polecat: "Creating pull request" | -| 19:44:59 | MR bead created (`pr_url=null`). **BUG: refinery dispatched** | -| 19:45:42 | Refinery requests rework, rework bead created | -| 19:45:57 | Polecat dispatched for rework | -| 19:46:58 | Rework bead: refinery calls `gt_done`, `poll_pr` starts | -| 19:48:02 | **PR #38 merged** by kiloconnect-development bot | -| 19:48:13 | All beads closed | +| Time (UTC) | Event | +|---|---| +| 19:38:42 | Task sent to mayor | +| 19:38:47 | Issue bead created, mayor hooks it | +| 19:43:51 | Mayor unhooks, polecat dispatched | +| 19:43:56 | Polecat hooks bead, starts implementing | +| 19:44:28 | Polecat: "Set helper file added; committing and pushing" | +| 19:44:43 | Polecat: "Creating pull request" | +| 19:44:59 | MR bead created (`pr_url=null`). **BUG: refinery dispatched** | +| 19:45:42 | Refinery requests rework, rework bead created | +| 19:45:57 | Polecat dispatched for rework | +| 19:46:58 | Rework bead: refinery calls `gt_done`, `poll_pr` starts | +| 19:48:02 | **PR #38 merged** by kiloconnect-development bot | +| 19:48:13 | All beads closed | **Duration:** ~9.5 min (would be ~5 min without unnecessary refinery rework) **PR:** #38 on branch `gt/toast/38cba536` — MERGED @@ -1257,17 +1257,17 @@ Task: "Add src/utils/promise-helpers.ts with delay, retry, timeout, allSettledWithErrors, race" ``` -| Time (UTC) | Event | -| ---------- | ---------------------------------------------------------------- | -| 19:49:20 | Task sent to mayor | -| 19:49:35 | Issue bead created, polecat dispatched | -| 19:49:44 | Polecat: "Implementing promise helper utilities" | -| 19:50:03 | MR bead created (`pr_url=null`). Refinery dispatched (expected). | -| 19:51:00 | Refinery: "Code review found gaps in test coverage" | -| 19:51:13 | First rework request + escalation beads (2 each) | -| 20:02:01 | **Second rework request** (duplicate) — refinery still working | -| 20:12:35 | Third rework request (scope mismatch) | -| 20:25+ | **Refinery still stuck** on MR bead after 35+ min | +| Time (UTC) | Event | +|---|---| +| 19:49:20 | Task sent to mayor | +| 19:49:35 | Issue bead created, polecat dispatched | +| 19:49:44 | Polecat: "Implementing promise helper utilities" | +| 19:50:03 | MR bead created (`pr_url=null`). Refinery dispatched (expected). | +| 19:51:00 | Refinery: "Code review found gaps in test coverage" | +| 19:51:13 | First rework request + escalation beads (2 each) | +| 20:02:01 | **Second rework request** (duplicate) — refinery still working | +| 20:12:35 | Third rework request (scope mismatch) | +| 20:25+ | **Refinery still stuck** on MR bead after 35+ min | **Duration:** 35+ min (never completed) **PR:** None created on GitHub @@ -1283,14 +1283,14 @@ Task: "Add src/utils/regex-helpers.ts with escapeRegex, isValidRegex, matchAll, replaceAll, extractGroups" ``` -| Time (UTC) | Event | -| ---------- | ------------------------------------------------ | -| 20:13:09 | Task sent to mayor | -| 20:13:25 | Issue bead created, polecat dispatched | -| 20:14:01 | Polecat: "Implementing regex helper utilities" | -| 20:14:05 | MR bead created (`pr_url=null`), polecat unhooks | -| 20:14:17 | MR bead status=`open`, no assignee, no dispatch | -| 20:25+ | **MR bead still `open`** — no one picks it up | +| Time (UTC) | Event | +|---|---| +| 20:13:09 | Task sent to mayor | +| 20:13:25 | Issue bead created, polecat dispatched | +| 20:14:01 | Polecat: "Implementing regex helper utilities" | +| 20:14:05 | MR bead created (`pr_url=null`), polecat unhooks | +| 20:14:17 | MR bead status=`open`, no assignee, no dispatch | +| 20:25+ | **MR bead still `open`** — no one picks it up | **Duration:** 10+ min (never completed) **PR:** None created on GitHub diff --git a/services/gastown/docs/post-deploy-monitoring.md b/services/gastown/docs/post-deploy-monitoring.md index fe3626ab83..a577cc7ef1 100644 --- a/services/gastown/docs/post-deploy-monitoring.md +++ b/services/gastown/docs/post-deploy-monitoring.md @@ -251,16 +251,16 @@ done ## 5. Key Metrics to Watch -| Metric | Healthy | Unhealthy | -| --------------------- | --------------------------------------- | ------------------------------------- | -| Working agents | >0 when beads exist | 0 for >5 min with open beads | -| Failed bead count | Stable | Increasing rapidly | -| Invariant violations | 0 | >0 (check reconciler logs) | -| Refinery status | `working` during review, `idle` between | `idle` with in_progress MR for >5 min | -| Review outcomes | `merged` | `Refinery container failed to start` | -| Alarm interval | `active (5s)` with work | Stuck at same `nextFireAt` | -| Reconciler wall clock | <100ms | >500ms consistently | -| Pending event count | 0 between ticks | Growing (events not draining) | +| Metric | Healthy | Unhealthy | +|---|---|---| +| Working agents | >0 when beads exist | 0 for >5 min with open beads | +| Failed bead count | Stable | Increasing rapidly | +| Invariant violations | 0 | >0 (check reconciler logs) | +| Refinery status | `working` during review, `idle` between | `idle` with in_progress MR for >5 min | +| Review outcomes | `merged` | `Refinery container failed to start` | +| Alarm interval | `active (5s)` with work | Stuck at same `nextFireAt` | +| Reconciler wall clock | <100ms | >500ms consistently | +| Pending event count | 0 between ticks | Growing (events not draining) | ## 6. Querying Cloudflare Logs (Workers Observability) @@ -351,26 +351,26 @@ The `gastown_events` Analytics Engine dataset stores all lifecycle events (bead The `gastown_events` dataset maps fields as follows: -| Column | Field | Description | -| ------- | ------------------- | ------------------------------------------------- | -| blob1 | event | Event name (e.g. `bead.created`, `agent.spawned`) | -| blob2 | userId | User who triggered the event | -| blob3 | delivery | `http`, `trpc`, or `internal` | -| blob4 | route | HTTP route pattern (for HTTP events) | -| blob5 | error | Error message (if any) | -| blob6 | townId | Town ID | -| blob7 | rigId | Rig ID | -| blob8 | agentId | Agent ID | -| blob9 | beadId | Bead ID | -| blob10 | label | Free-form label (e.g. actionsByType JSON) | -| blob11 | convoyId | Convoy ID | -| blob12 | role | Agent role (`polecat`, `refinery`, `mayor`) | -| blob13 | beadType | Bead type | -| double1 | durationMs | Duration in milliseconds | -| double2 | value | Generic numeric value | -| double3 | actionsEmitted | (reconciler_tick) actions count | -| double7 | invariantViolations | (reconciler_tick) violation count | -| double8 | pendingEventCount | (reconciler_tick) pending event count | +| Column | Field | Description | +|---|---|---| +| blob1 | event | Event name (e.g. `bead.created`, `agent.spawned`) | +| blob2 | userId | User who triggered the event | +| blob3 | delivery | `http`, `trpc`, or `internal` | +| blob4 | route | HTTP route pattern (for HTTP events) | +| blob5 | error | Error message (if any) | +| blob6 | townId | Town ID | +| blob7 | rigId | Rig ID | +| blob8 | agentId | Agent ID | +| blob9 | beadId | Bead ID | +| blob10 | label | Free-form label (e.g. actionsByType JSON) | +| blob11 | convoyId | Convoy ID | +| blob12 | role | Agent role (`polecat`, `refinery`, `mayor`) | +| blob13 | beadType | Bead type | +| double1 | durationMs | Duration in milliseconds | +| double2 | value | Generic numeric value | +| double3 | actionsEmitted | (reconciler_tick) actions count | +| double7 | invariantViolations | (reconciler_tick) violation count | +| double8 | pendingEventCount | (reconciler_tick) pending event count | ### Example AE queries @@ -426,14 +426,14 @@ curl -s "$AE_API" \ Worker logs now emit JSON-structured entries. Key fields available for filtering in Workers Observability: -| JSON field | Description | Example filter | -| ---------- | --------------------------- | ------------------------------- | -| `source` | Module that emitted the log | `"Town.do"`, `"gastown-worker"` | -| `townId` | Town UUID | `message : "townId":""` | -| `rigId` | Rig UUID | `message : "rigId":""` | -| `userId` | User UUID | `message : "userId":""` | -| `orgId` | Organization UUID | `message : "orgId":""` | -| `agentId` | Agent UUID | `message : "agentId":""` | -| `level` | `info`, `warn`, or `error` | `message : "level":"error"` | +| JSON field | Description | Example filter | +|---|---|---| +| `source` | Module that emitted the log | `"Town.do"`, `"gastown-worker"` | +| `townId` | Town UUID | `message : "townId":""` | +| `rigId` | Rig UUID | `message : "rigId":""` | +| `userId` | User UUID | `message : "userId":""` | +| `orgId` | Organization UUID | `message : "orgId":""` | +| `agentId` | Agent UUID | `message : "agentId":""` | +| `level` | `info`, `warn`, or `error` | `message : "level":"error"` | These fields are embedded in the `message` text as JSON, so use the `contains` operator or `:` in the dashboard query language to filter by them. diff --git a/services/gmail-push/README.md b/services/gmail-push/README.md index 3f426c813f..cb6698640c 100644 --- a/services/gmail-push/README.md +++ b/services/gmail-push/README.md @@ -18,10 +18,10 @@ Push requests are authenticated via **Google OIDC JWT** (mandatory). The Pub/Sub ## API Endpoints -| Endpoint | Method | Auth | Description | -| -------------------- | ------ | -------- | -------------------- | -| `/health` | GET | None | Health check | -| `/push/user/:userId` | POST | OIDC JWT | Receive Pub/Sub push | +| Endpoint | Method | Auth | Description | +|---|---|---|---| +| `/health` | GET | None | Health check | +| `/push/user/:userId` | POST | OIDC JWT | Receive Pub/Sub push | ## Development @@ -114,18 +114,18 @@ Deploys to: `cloudflare-gmail-push` ## Secrets (via Secrets Store) -| Secret | Description | -| --------------------- | -------------------------------------- | +| Secret | Description | +|---|---| | `INTERNAL_API_SECRET` | Shared secret for service binding auth | ## Environment Variables -| Variable | Description | -| -------------------- | ---------------------------------------------------- | +| Variable | Description | +|---|---| | `OIDC_AUDIENCE_BASE` | Base URL for per-user OIDC audience claim validation | ## Service Bindings -| Binding | Target Worker | Environment | -| ---------- | ------------- | ----------- | -| `KILOCLAW` | `kiloclaw` | Production | +| Binding | Target Worker | Environment | +|---|---|---| +| `KILOCLAW` | `kiloclaw` | Production | diff --git a/services/kiloclaw-billing/docs/observability.md b/services/kiloclaw-billing/docs/observability.md index a321279e07..20ac838706 100644 --- a/services/kiloclaw-billing/docs/observability.md +++ b/services/kiloclaw-billing/docs/observability.md @@ -8,18 +8,18 @@ Use this filter for every billing lifecycle query in Axiom: Important dimensions: -| Field | Meaning | -| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `billingComponent` | `worker`, `side_effects`, `kiloclaw_platform`, or `snowflake_sql_api` | -| `billingRunId` | One hourly billing run across all sweeps | -| `billingSweep` | The current sweep name, including `trial_inactivity_stop` for daily coordination and `trial_inactivity_stop_candidate` for per-instance stop work | -| `billingCallId` | One downstream call from the worker | -| `billingAttempt` | Queue delivery attempt number | -| `event` | `run_started`, `sweep_started`, `sweep_completed`, `sweep_failed`, `queue_retry`, `run_completed`, `run_failed`, `downstream_call`, `downstream_action`, `request_rejected`, `subscription_row_skipped`, and trial-inactivity-specific events such as `trial_inactivity_stop` | -| `outcome` | `started`, `completed`, `failed`, `retry`, `discarded`, or `skipped` | -| `durationMs` | Elapsed time for a sweep or downstream request | -| `snowflakeCode` | Snowflake SQL API error code on failed submit/poll requests | -| `snowflakeMessage` | Snowflake SQL API error message on failed submit/poll requests | +| Field | Meaning | +|---|---| +| `billingComponent` | `worker`, `side_effects`, `kiloclaw_platform`, or `snowflake_sql_api` | +| `billingRunId` | One hourly billing run across all sweeps | +| `billingSweep` | The current sweep name, including `trial_inactivity_stop` for daily coordination and `trial_inactivity_stop_candidate` for per-instance stop work | +| `billingCallId` | One downstream call from the worker | +| `billingAttempt` | Queue delivery attempt number | +| `event` | `run_started`, `sweep_started`, `sweep_completed`, `sweep_failed`, `queue_retry`, `run_completed`, `run_failed`, `downstream_call`, `downstream_action`, `request_rejected`, `subscription_row_skipped`, and trial-inactivity-specific events such as `trial_inactivity_stop` | +| `outcome` | `started`, `completed`, `failed`, `retry`, `discarded`, or `skipped` | +| `durationMs` | Elapsed time for a sweep or downstream request | +| `snowflakeCode` | Snowflake SQL API error code on failed submit/poll requests | +| `snowflakeMessage` | Snowflake SQL API error message on failed submit/poll requests | ## Saved Queries @@ -189,17 +189,17 @@ Base filter: Dimensions: -| Field | Meaning | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `billingComponent` | `web_trpc` (the enrollWithCredits TRPC mutation) | -| `event` | `credit_enrollment.attempted`, `credit_enrollment.succeeded`, `credit_enrollment.failed` | -| `outcome` | `started`, `completed`, `failed` | -| `failureReason` | `insufficient_credits`, `duplicate_enrollment`, `active_subscription_exists`, `no_instance`, `user_not_found`, `precondition_failed`, `internal_error` | -| `plan` | `commit` or `standard` | -| `userId` | KiloCode user id | -| `instanceId` | KiloClaw instance id (omitted on `no_instance` failures when the user never resolved to an instance) | -| `durationMs` | Time from mutation entry to success/failure | -| `error` | Error message on failures (truncated to 500 chars; may include upstream ORM/driver text on `internal_error` — treat as semi-sensitive) | +| Field | Meaning | +|---|---| +| `billingComponent` | `web_trpc` (the enrollWithCredits TRPC mutation) | +| `event` | `credit_enrollment.attempted`, `credit_enrollment.succeeded`, `credit_enrollment.failed` | +| `outcome` | `started`, `completed`, `failed` | +| `failureReason` | `insufficient_credits`, `duplicate_enrollment`, `active_subscription_exists`, `no_instance`, `user_not_found`, `precondition_failed`, `internal_error` | +| `plan` | `commit` or `standard` | +| `userId` | KiloCode user id | +| `instanceId` | KiloClaw instance id (omitted on `no_instance` failures when the user never resolved to an instance) | +| `durationMs` | Time from mutation entry to success/failure | +| `error` | Error message on failures (truncated to 500 chars; may include upstream ORM/driver text on `internal_error` — treat as semi-sensitive) | Funnel shape: every attempt emits exactly one `credit_enrollment.attempted`, followed by exactly one of `credit_enrollment.succeeded` or `credit_enrollment.failed` (with a `failureReason`). The enclosing try/catch guarantees this even if upstream helpers (anchor resolution, prior-subscription lookup) throw. diff --git a/services/kiloclaw/AGENTS.md b/services/kiloclaw/AGENTS.md index cdf654b394..01bb0ed99e 100644 --- a/services/kiloclaw/AGENTS.md +++ b/services/kiloclaw/AGENTS.md @@ -101,14 +101,14 @@ src/ ## Instance Statuses -| Status | Meaning | -| ------------- | -------------------------------------------------------------------- | -| `provisioned` | Config stored, volume created, no machine yet | -| `starting` | startAsync() fired; start() running in background via waitUntil | -| `running` | Machine is started and healthy | -| `stopped` | Machine is stopped, volume persists | -| `restoring` | Snapshot restore in progress via CF Queue; all lifecycle ops blocked | -| `destroying` | Two-phase destroy in progress, pending resource deletion | +| Status | Meaning | +|---|---| +| `provisioned` | Config stored, volume created, no machine yet | +| `starting` | startAsync() fired; start() running in background via waitUntil | +| `running` | Machine is started and healthy | +| `stopped` | Machine is stopped, volume persists | +| `restoring` | Snapshot restore in progress via CF Queue; all lifecycle ops blocked | +| `destroying` | Two-phase destroy in progress, pending resource deletion | The alarm runs for ALL statuses (not just `running`). `destroying` short-circuits reconciliation -- only retries pending deletes, never recreates resources. `starting` uses a 1-min alarm cadence; reconcileStarting() checks Fly machine state and transitions to `running` or `stopped`. If `startingAt` is set and more than 5 minutes have elapsed, it falls back to `stopped` automatically. `restoring` skips reconciliation entirely (the CF Queue worker owns the lifecycle); the alarm detects stuck restores (>30 min) and resets to `stopped`. @@ -116,28 +116,28 @@ The alarm runs for ALL statuses (not just `running`). `destroying` short-circuit ### Required (set via `wrangler secret put`) -| Variable | Purpose | -| ---------------------- | ---------------------------------------------------------------- | -| `FLY_API_TOKEN` | Bearer token for Fly Machines API (org-scoped) | -| `FLY_ORG_SLUG` | Fly org for creating per-user apps (e.g., `kilo-679`) | -| `FLY_REGISTRY_APP` | Shared app for Docker image registry (e.g., `kiloclaw-machines`) | -| `FLY_APP_NAME` | Legacy fallback for existing instances without per-user apps | -| `NEXTAUTH_SECRET` | JWT verification secret (shared with Next.js) | -| `INTERNAL_API_SECRET` | Platform API auth key | -| `GATEWAY_TOKEN_SECRET` | HMAC secret for per-user gateway tokens | +| Variable | Purpose | +|---|---| +| `FLY_API_TOKEN` | Bearer token for Fly Machines API (org-scoped) | +| `FLY_ORG_SLUG` | Fly org for creating per-user apps (e.g., `kilo-679`) | +| `FLY_REGISTRY_APP` | Shared app for Docker image registry (e.g., `kiloclaw-machines`) | +| `FLY_APP_NAME` | Legacy fallback for existing instances without per-user apps | +| `NEXTAUTH_SECRET` | JWT verification secret (shared with Next.js) | +| `INTERNAL_API_SECRET` | Platform API auth key | +| `GATEWAY_TOKEN_SECRET` | HMAC secret for per-user gateway tokens | ### Optional -| Variable | Purpose | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `FLY_REGION` | Default region for new volumes/machines. Comma-separated priority list: `us,eu` tries US first, falls back to EU. (default: `us,eu`) | -| `KILOCODE_API_BASE_URL` | Override KiloCode API URL | -| `AGENT_ENV_VARS_PRIVATE_KEY` | RSA private key for decrypting user secrets | -| `TELEGRAM_DM_POLICY` | Telegram DM policy (passed through to machine) | -| `DISCORD_DM_POLICY` | Discord DM policy (passed through to machine) | -| `OPENCLAW_ALLOWED_ORIGINS` | Comma-separated origins for Control UI WebSocket (e.g., `http://localhost:3000,http://localhost:8795`). Production: `https://claw.kilo.ai,https://claw.kilosessions.ai`. Per-instance `://