Skip to content

Commit b64ff93

Browse files
chore(agents): streamline skills, add a skills validator, tighten public error contract (#1110)
Trim the `.agents/skills/` runbooks to what agents actually need at read time and add `scripts/check-skills.mjs` so malformed skills fail `pnpm lint` instead of shipping silently. - Condense 14 SKILL.md runbooks (-875 lines); drop the two `source-command-*` skills that only mirrored slash commands - Add `check-skills.mjs`: frontmatter, directory/name match, dangling symlinks, and CLAUDE.md routing-table coverage; wire into `check:agent-instructions` - Remove the stale `.claude/skills/shadcn` and `.cursor/skills` symlinks - CLAUDE.md routing table gains `chatbotx-basecode` and `builder-ui-i18n`; AGENTS.md now points at that table as the single inventory - Sync invariant mirrors: correct the `ChannelType` source of truth to `packages/utils/src/channel.ts`, add the structured-logging (`err`, not `error`) invariant - Public API: move `invalidRequestData` + `validation` into `commonApiErrors` so every input-taking route documents its 422, mark the per-router sets `satisfies ErrorMap`, and fix contact-notes create to declare `notFound` (its identifier lookup can 404) - Cover both with tests that read each procedure's real `errorMap` rather than the rendered spec Claude-Session: https://claude.ai/code/session_012bCkSZqKnTc47yX9qFKY3M Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent be8a0b3 commit b64ff93

29 files changed

Lines changed: 549 additions & 911 deletions

File tree

.agents/skills/builder-ui-i18n/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ All user-facing strings must use translations. Do not hardcode labels,
2727
placeholders, button text, empty states, tab names, toasts, or dialog copy in
2828
builder UI.
2929

30-
Primary files:
31-
32-
- `apps/builder/messages/en.json`
33-
- `apps/builder/messages/vi.json`
30+
Source of truth: `apps/builder/messages/en.json`. **All 20 locale files in
31+
`apps/builder/messages/` must carry every key**`apps/builder`'s own `lint` script is
32+
`i18n:check --source en --locales messages` (`apps/builder/package.json:11-12`), which runs
33+
in CI's Lint job. Adding a key to `en.json` + `vi.json` only will fail the parity check.
3434

3535
Before adding keys, check existing `fields.*`, common actions, table labels, and
36-
feature namespaces. Add both English and Vietnamese values for new keys.
36+
feature namespaces.
3737

3838
Typical component pattern:
3939

.agents/skills/chatbotx-basecode/SKILL.md

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,26 @@ not a replacement for reading adjacent code.
1414

1515
## Project Shape
1616

17-
ChatbotX is a pnpm workspace + Turborepo monorepo.
17+
ChatbotX is a pnpm workspace + Turborepo monorepo. The authoritative layout table is
18+
**`AGENTS.md` → "Repository layout"** — read it there rather than trusting a second copy.
19+
To see what actually exists right now:
1820

19-
```
20-
apps/
21-
builder/ Next.js app: product UI, oRPC/OpenAPI, route handlers
22-
worker/ BullMQ/Kafka background jobs
23-
realtime/ PartyKit realtime server
24-
cli/ chatbotx-cli
25-
mcp-server/ MCP tools generated from OpenAPI
26-
27-
packages/
28-
business/ service layer and business orchestration
29-
database/ Drizzle schema, relations, repositories, migrations
30-
ui/ shared UI components
31-
public-apis/ typed public API client
32-
sdk/ integration contracts and shared schemas
33-
worker-config/ queue names, job payloads, BullMQ queues
34-
flow-config/ flow/node/step config schemas
35-
ai, events, redis, kafka, filesystem, mail, imports, analytics, ...
36-
37-
integrations/
38-
messenger, whatsapp, zalo, tiktok, telegram, webchat, smtp, openai, google-sheets, ...
21+
```bash
22+
ls apps packages integrations
3923
```
4024

4125
## Skill Router
4226

43-
- Builder feature, page, action, query, or public route: use `feature-scaffold`.
44-
- oRPC or OpenAPI endpoint: use `orpc-api`.
45-
- Database schema, relations, migration, repository: use `drizzle-database`.
46-
- Service layer, app data-access boundary: use `business-data-access`.
47-
- UI component work, forms, tables, translations: use `builder-ui-i18n`.
48-
- Worker, BullMQ, Kafka, scheduled job: use `worker-development`.
49-
- Channel integration or webhook behavior: use `integration-channel`.
50-
- Flow step or state-based routing: use `flow-step-development`.
51-
- CLI, MCP server, generated public client: use `orpc-api`.
52-
- Dev server, build, lint, package management: use `turborepo-workflow`.
27+
The canonical task → skill routing table is **`CLAUDE.md` → "Skill → task mapping"**. Read
28+
it and pick the skill that matches the task; it lists every skill in `.agents/skills/`.
29+
30+
Two routing notes that table does not spell out:
31+
32+
- CLI, MCP server, and the generated public client all follow the public oRPC surface — use
33+
`orpc-api`.
34+
- A broad request usually decomposes into several skills (e.g. a new feature with a table and
35+
a queue = `feature-scaffold` + `drizzle-database` + `worker-development`). Read each before
36+
writing that layer, not all of them up front.
5337

5438
## Basecode Scan Checklist
5539

.agents/skills/contact-filter/SKILL.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,26 @@ Lives in two packages:
1717

1818
- **Frontend feature**`apps/builder/src/features/contact-filter/` (Zod schemas,
1919
UI config, React components). Barrel: `index.ts`.
20-
- **Backend query builder**`packages/database/src/queries/contact-filter.ts`
20+
- **Backend query builder**`packages/database/src/queries/contact-filter/`
2121
(`@chatbotx.io/database/queries`). Shared by the builder app **and** the worker
22-
so both resolve the same contacts.
22+
so both resolve the same contacts. **`queries/contact-filter.ts` is now a one-line
23+
re-export barrel** (`export * from "./contact-filter/index"`) — the real code is the
24+
14-file directory beside it:
25+
26+
| Concern | File |
27+
|---|---|
28+
| Per-field dispatch (`buildConditionWhere`) | `contact-filter/index.ts:412` |
29+
| Negation/NULL predicates, 24h window | `contact-filter/predicates.ts` (`COLUMN_NEGATION_OPERATORS:21`, `contactInboxInteractedWithin24hSQL:27`) |
30+
| Operator inversion map | `contact-filter/field-value-predicates.ts` (`NEGATION_TO_POSITIVE:41`) |
31+
| Relation EXISTS subqueries | `contact-filter/relation-sets.ts` (`RELATION_SET_FILTERS:76`, `buildRelationSetWhere:131`) |
32+
| Custom / bot fields, CTWA, timezone, continent | `custom-field-predicates.ts`, `bot-field-predicates.ts`, `ctwa-retarget.ts`, `timezone.ts`, `continent.ts` |
2333

2434
## Architecture
2535

2636
```
2737
contactFilterFields enum (partials/contact.ts) ← the field "universe" (~90)
2838
29-
CONTACT_FILTER_FIELD_DEFINITIONS (schema/definitions.ts) ← 22 ACTIVE fields = single source of truth
39+
CONTACT_FILTER_FIELD_DEFINITIONS (schema/definitions.ts) ← ~49 ACTIVE fields = single source of truth
3040
│ (each: { field, schemaKind, optionSource })
3141
├──► Zod condition schemas (schema/*.ts, via staticFieldFilter)
3242
└──► UI FieldConfig[] (components/contact-filter-config.ts, getFieldConfigs)
@@ -60,15 +70,16 @@ Operators + form-field types: `packages/database/src/partials/custom-field.ts`
6070
- Zod validation: `STATIC_OPERATOR_RULES` in `schema/static-field-filter.ts`
6171
- UI enablement: `staticFieldRules` in `components/static-field-filter-config.ts`
6272
4. **Backend SQL** — add a `case` to `buildConditionWhere`
63-
(`packages/database/src/queries/contact-filter.ts`). Without it the field
64-
silently produces **no condition** (the `default: return {}` branch).
73+
(`packages/database/src/queries/contact-filter/index.ts:412`; it takes
74+
`(condition, context)`). Without it the field silently produces **no condition**
75+
(the `default: return {}` branch).
6576
5. **Options / group** (if not `none`) — wire the option source in
6677
`use-contact-filter-configs.ts` / `contact-filter-config.ts`; group is assigned
6778
by `getContactFilterFieldGroup`.
6879

6980
## Where filter application lives
7081

71-
`contactRepository.buildListWhere` (`packages/database/src/repositories/contact/list-where.ts`)
82+
`contactRepository.buildListWhere` (aliases `buildContactListWhere`, `packages/database/src/repositories/contact/list-where.ts:42`)
7283
is the one place `applyContactFilter` is called to build the where clause for
7384
a contacts list/count — used by both the builder (private RSC) and the public
7485
API, via `contactService.list`/`count` (`packages/business/src/contact/list.ts`).
@@ -78,10 +89,10 @@ split exists to prevent (see `.agents/rules/data-access.md`). The worker's
7889
`export-contacts.ts` still hand-builds its own where clause — a known
7990
follow-up, not a pattern to extend.
8091

81-
## Backend query builder (`packages/database/src/queries/contact-filter.ts`)
92+
## Backend query builder (`packages/database/src/queries/contact-filter/`)
8293

8394
- `applyContactFilter(criteria)` → maps `conditions` to `{ AND: [...] }` or
84-
`{ OR: [...] }`; `buildConditionWhere(condition)` switches on `field`.
95+
`{ OR: [...] }`; `buildConditionWhere(condition, context)` switches on `field`.
8596
- `buildContactWhere({ workspaceId, keyword?, contactFilter? })` → relational
8697
where for `contactModel`.
8798
- `buildContactInboxContactFilterSQL({ contactIdColumn, workspaceId, contactFilter })`
@@ -131,7 +142,7 @@ Messenger/WhatsApp subactions).
131142

132143
## Shared 24h window predicate
133144

134-
`contactInboxInteractedWithin24hSQL()` (in `contact-filter.ts`) is the single
145+
`contactInboxInteractedWithin24hSQL()` (`contact-filter/predicates.ts:27`) is the single
135146
source for `lastIncomingMessageAt >= NOW() - INTERVAL '24 hours'`, used by:
136147
- the `interactedInLast24h` filter case (wrapped in a contact-level EXISTS), and
137148
- the broadcast audience (`apps/worker/src/schedule/handlers/prepare-broadcast.ts`)

.agents/skills/drizzle-database/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ Package: `packages/database` (`@chatbotx.io/database`)
1212

1313
## Table Definition
1414

15-
Tables use `pgTable` with `sharedColumns` spread for consistent `id`, `createdAt`, `updatedAt`:
15+
Tables use `pgTable` with `sharedColumns` spread for consistent `id`, `createdAt`,
16+
`updatedAt`. For a real workspace-scoped table to copy, read
17+
`packages/database/src/schema/minigame.ts` or any sibling in `schema/`; the shape is:
1618

1719
```typescript
1820
import { pgTable, text, index, uniqueIndex } from "drizzle-orm/pg-core"

.agents/skills/fb-comment-automation/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ Read it before non-trivial changes. This skill is the quick map + the traps.
102102
3. Add the guard inside the loop in `processCommentAutomation`, **with a
103103
`logAutomationSkipped(..., reason)` before `continue`**.
104104
4. Surface the toggle in `apps/builder/src/features/fb-comments/components/fb-comment-form.tsx`
105-
and add i18n keys to `apps/builder/messages/en.json` + `vi.json`.
105+
and add i18n keys to **every** locale file in `apps/builder/messages/` (the i18n parity
106+
check in `pnpm lint` fails on a missing key in any of the 20 locales).
106107
5. Extend `apps/worker/__tests__/comment-automation.test.ts`.
107108

108109
## Adding a new reply type (recipe)

0 commit comments

Comments
 (0)