# Harness engineering: Preparing TypeScript codebases for coding agents
**作者**: zayne (zeyu) zhang
**日期**: 2026-05-03T02:07:46.000Z
**来源**: [https://x.com/zeyu1337/status/2050759187576021219](https://x.com/zeyu1337/status/2050759187576021219)
---

At @HacktronAI, I pushed our product team to fully embrace vibe coding with Claude Code and Codex, while enforcing guardrails and code quality standards.
Vibe coding works best when the codebase has strong affordances — a concept in design that describes the possible actions an actor (in this case, a coding agent) can take, in relation to an object (in this case, the codebase):
> Affordance: a use or purpose that a thing can have, that people notice as part of the way they see or experience it.
For a coding agent like Claude Code or Cursor to produce productive code instead of "AI slop" that becomes expensive to maintain and clean up later, building a codebase with obvious structure and automated guardrails becomes important.
Even the smartest models today can't possibly reason about every edge case without a good harness. And even with coding agents like Claude Code, designing repositories in a thoughtful way can go a long way in improving the quality of the code.
A repository should be treated less like a pile of code that can be executed, and more like an execution environment for agents. Good vibe coding, therefore, would mean that the environment provides:
- Fast validation against "bad engineering"
- A constrained blast radius
- Guardrails that enforce invariants before commiting
- Tests and scripts that the agent can use to "vibe-check" itself
## Make the repository legible to agents
Use pnpm and set up a monorepo. If you want to work across multiple repositories for frontend and various backend microservices, you'll need to either make your coding agent context switch across these repositories, or provide them with overly broad permissions so that they can access all repositories in the same session. This isn't nice. So just use a monorepo.
```
apps/
frontend/
backend/
docs/
architecture.md
conventions.md
packages/
eslint-config/
shared-utils/
shared-tyles/
typescript-config/
CLAUDE.md
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
turbo.json
```
The monorepo structure allows you to create multiple apps that use shared packages. These can be utilities and type definitions. In addition, I find it useful to standardise ESLint and TypeScript configurations in a shared package, so that they can be easily imported in new apps and packages.
For example, once you export an ESLint configuration like this in a shared package:
```
// packages/eslint-config/base.js
import js from '@eslint/js'
import eslintConfigPrettier from 'eslint-config-prettier'
import turboPlugin from 'eslint-plugin-turbo'
import tseslint from 'typescript-eslint'
import onlyWarn from 'eslint-plugin-only-warn'
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
'turbo/no-undeclared-env-vars': 'warn',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ['dist/**'],
},
]
```
Each app and package can simply import from this configuration.
```
// apps/frontend/eslint.config.mjs
import { config } from '@my-project/eslint-config/base'
export default config
```
## Skills encapsulate best practices
Skills like nestjs-best-practices and typescript-advanced-types can help coding agents produce idiomatic code. Of course, a lot of this is strongly opinionated, so we also write our own skills to encapsulate the best practices we've learnt over the years. If you don't know what those best practices should be, Google this or point AI at an example repository that demonstrates strong software engineering principles, and have it come up with its own skill.
We have engineers using all sorts of different agents: Claude, Codex, Cursor, …, so if we want these skills to be useful and shared across team members, we need every coding agent to use the same set of skills.
That's why skills are stored in .agents, and .codex, .claude, etc. symlink to the skills stored in the main .agents directory.
```
.agents/
skills/
typescript-expert/
SKILL.md
typescript-advanced-types/
SKILL.md
[...]
.codex/
skills/
typescript-expert -> ../../agents/skills/typescript-expert
typescript-advanced-types -> ../../agents/skills/typescript-advanced-types
[...]
.claude/
skills/
typescript-expert -> ../../agents/skills/typescript-expert
typescript-advanced-types -> ../../agents/skills/typescript-advanced-types
[...]
[...]
```
## Documentation that agents read and maintain
A CLAUDE.md (or equivalent), if written well, goes a long way in providing self-evolving documentation. This documentation can outline architecture, tech stack, but also more importantly, rules that the AI agent should abide by.
```
# VibeSlop - The Best Vibe Coded Application
## Overview
VibeSlop has a NestJS backend and a Nuxt frontend. It is a B2B AI SaaS.
[...]
## Notion Documentation
**IMPORTANT**: VibeSlop has comprehensive documentation in Notion that should be kept in sync with code changes.
**Main page**: https://notion.so/[...]
### Documentation Structure
| Section | Page ID | Description |
| -------------- | ---------- | ------------------------------ |
| Authentication | `DEADBEEF` | Auth guards, token types, RBAC |
| [...] | [...] | [...] |
### When to Update Notion Docs
Update the relevant Notion page when:
- Adding new API endpoints → Update API Reference
- Adding/modifying entities → Update Database & Entities
- Changing auth guards or token handling → Update Authentication
[...]
### How to Update
Use the Notion MCP tools:
- `mcp__notionMCP__notion-fetch` - Read existing page content
- `mcp__notionMCP__notion-update-page` - Update page content
- `mcp__notionMCP__notion-create-pages` - Create new nested pages
[...]
## AI Coding Rules (MANDATORY)
These rules are non-negotiable. Every code change — whether new feature, bugfix, or refactor — must comply. Violations must be fixed before committing.
### DTO & OpenAPI Contract
[...]
### TypeScript Strictness
- **No casting** except `as const`. No `as unknown as X`, `as any`, `as SomeType`, `@ts-ignore`, `// @ts-expect-error`.
- **Use enums** instead of magic strings. If a value has a fixed set of options, define an enum.
- **Use optional fields sparingly** — prefer union types (`string | null`) over optional (`string?`) when the field is semantically required but may be absent.
- **No re-declaring types** that already exist in `@my-project/shared-types`, entity definitions, or generated code.
- `pnpm check-types` must pass before committing.
### Architecture
[...]
### Minimal Changes / No Slop
AI-generated code accumulates: narration comments, single-use helpers, dead code from earlier iterations, error handling for cases that can't happen. Before declaring done, re-read your own diff with a hostile eye and cut everything the current implementation doesn't need. The principle is that a bug fix does not need surrounding cleanup, a one-shot change does not need a helper, and previous iterations are obsolete the moment a later iteration supersedes them.
- **Re-read the diff end-to-end before finishing.** After several iterations, files carry leftovers — replaced methods, unused imports, stale branches, helpers that nothing calls anymore. Delete them. Git has the history; the codebase does not need a tombstone.
- **No narration comments.** Don't explain WHAT (names do that) or reference the task ("added for X", "used by Y flow", "handles issue Z"). Only write a comment when the WHY is non-obvious: a hidden constraint, a workaround, a surprising invariant.
- ✗ `// Loop through findings and send feedback to Slack`
- ✗ `// Added for the unfurl flow` / `// TODO: remove old logic once migrated`
- ✓ `// Stripe retries webhooks on 5xx — dedupe on event.id before mutating state`
- **No commented-out code, no "removed X" tombstones, no backwards-compat shims for code you just deleted in the same PR.** If it's gone, it's gone. Don't keep a renamed `_oldMethod` "just in case".
- **No single-use abstractions.** Don't create a helper, wrapper, base class, or custom decorator until a second caller exists. Three similar lines beats a premature abstraction. `packages/shared-utils/src/status-mapper.ts` is what justified extraction looks like — used across `scan/`, `findings/`, and `cost-estimation/`. Don't manufacture that bar; let duplication prove it.
- **No speculative error handling.** Trust internal callers and framework guarantees. DTOs already validate controller input via `class-validator` — a service that receives a typed `SendFeedbackDto` (`src/findings/dto/send-feedback.dto.ts`) does not re-check that `reaction` is a string. Validate only at true boundaries: HTTP input, webhook payloads, external API responses, untyped env vars.
- ✗ `try { return await this.repo.findOne(...) } catch (e) { throw e }`
- ✗ `if (!user) throw new Error('user required')` where the parameter type is `User`, not `User | undefined`
- ✗ Wrapping a single `repo.save()` in a try/catch that logs and rethrows
- **Prefer editing existing files and reusing existing types.** Search `src/utils/`, `src/services/`, `src/dto/`, and `@my-project/shared-utils` before writing a new helper. Reuse `PaginationDto` (`src/dto/pagination.dto.ts`) for paginated endpoints instead of defining `page`/`limit` again. Reuse entity types from `@my-project/shared-types` instead of redeclaring shapes. Don't split a 200-line service into four files unless there's an actual reason.
- **Keep the shape minimal.** Controllers stay thin — validate → service → return, no branching, no queries (see `src/findings/findings.controller.ts`). DTOs carry request/response fields only, decorated with `@ApiProperty` + `class-validator` — nothing more (see `src/findings/dto/send-feedback.dto.ts`, `src/dto/pagination.dto.ts`). Entities stay as columns + relations — no computed getters or lifecycle hooks unless actually needed (see `src/seat/organization-developer.entity.ts`).
- **Frontend caveat:** UI iteration is where slop compounds fastest — unused props, stale Tailwind classes, dead conditional branches from designs two revs ago, state nothing reads. Same rule applies with more force: read the component top-to-bottom against the current design before declaring done, and delete anything the current design doesn't use.
### Quality Gates
- Tests must pass (`pnpm test`) before committing.
- Linter must pass (`pnpm lint`) before committing.
- Type-checker must pass (`pnpm check-types`) before committing.
```
A few things here:
1. We enforce self-documenting development by instructing the agent to update Notion documentation. This assumes that the Notion MCP is used.
2. We enforce AI coding rules based on behaviour we've observed in the past. For example, we saw that frontend code produced a lot of slop due to the nature of UI iteration: it's meant to produce lots of different variations until the developer is happy with the result. This means that often times, coding agents leave a lot of stale and dead code from previous iterations. We found that enforcing the "minimal changes" rule helped a lot.
## "Garbage collection" for slop
Even with our best efforts, "slop code" is inevitable. It's not like humans didn't produce slop code before. But AI allowed us to combat this by periodically auditing the codebase for things like dead code (functions with no references), outdated documentation, etc.
We did this by creating a GitHub Actions workflow that just runs Claude Code every 24 hours with prompts that ask it to:
1. Clean up poor code quality based on a set of rules we maintain in the repository under docs/.
2. Update the CLAUDE.md above based on the latest code changes.
```
name: Claude Garbage Collection
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
concurrency:
group: claude-garbage-collection
cancel-in-progress: false
jobs:
cleanup:
strategy:
fail-fast: false
matrix:
target_branch:
- staging
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ matrix.target_branch }}
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
token_format: access_token
- name: Set NPM_TOKEN for Artifact Registry
run: echo "NPM_TOKEN=${{ steps.auth.outputs.access_token }}" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Claude garbage collection task
id: claude-cleanup
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
base_branch: ${{ matrix.target_branch }}
prompt: |
Read `CLAUDE.md` and `docs/cleanup/README.md`. Use `docs/cleanup/` as the source of truth for this garbage collection pass.
Work only against `${{ matrix.target_branch }}` and keep the change scoped to that branch's current state.
You may make multiple improvements, but each PR must stay focused on one small, safe maintenance concern.
Leave the repository unchanged if there is no clear cleanup to make.
additional_permissions: |
actions: read
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(pnpm:*),Bash(gh:*)'"
sync-claude-md:
strategy:
fail-fast: false
matrix:
target_branch:
- staging
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ matrix.target_branch }}
- name: Sync CLAUDE.md with codebase
id: claude-md-sync
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
base_branch: ${{ matrix.target_branch }}
prompt: |
Your sole task is to update all `CLAUDE.md` files so they accurately reflect the current codebase on the `${{ matrix.target_branch }}` branch.
Steps:
1. Read every `CLAUDE.md` file in the repo (root `.claude/CLAUDE.md` and any nested ones like `apps/my-app/CLAUDE.md`, etc.).
2. Audit each section against the actual codebase:
- **Project structure**: list directories under `apps/my-app/src/` and update the tree if modules were added, renamed, or removed.
- **Key entities**: check `apps/my-app/src/**/entities/*.entity.ts` and update the entity table.
- **API namespaces**: check all `@Controller()` decorators and update the namespace table.
- **Key commands**: verify each command in `package.json` scripts still exists.
- **Environment variables**: check `.env.example` and update the env var list.
- **Path aliases**: check `tsconfig.json` path mappings.
- **Shared packages**: check `packages/*/package.json` names.
- **Guards & auth**: check `src/guards/` and `src/middleware/` for current guard list.
3. Remove references to files, modules, entities, or endpoints that no longer exist.
4. Add entries for new modules, entities, or endpoints that are missing from the docs.
5. Do NOT change style, tone, or conventions sections — only factual/structural sections.
6. If nothing is out of date, make no changes and do not open a PR.
Keep the PR focused: only `CLAUDE.md` file changes, nothing else.
additional_permissions: |
actions: read
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(pnpm:*),Bash(gh:*)'"
```
This produces easily mergable pull requests a lot of the time, and has saved us countless hours of manual refactoring and cleanup. It's almost like a garbage collection engine that cleans up dead code and stale documentation in the background, without needing much manual work from us apart from reviewing (mostly clean) PRs.
## Making bad code hard to commit
The shame of asking Claude Code to help you run git commit… well, it's a norm now and lots of people do it. So the best thing to do is to use hooks that enforce quality at commit-time.

You can set this up pretty easily:
```
pnpm add -D husky lint-staged
pnpm exec husky init
```
and in package.json:
```
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"]
}
}
```
This ensures that all code at least passes linting and formatting rules before hitting GitHub.
What about tests and typechecking? Now it's time to take this one step further and provide the agent with…
## One command to validate everything
Agents need a finish line. Once the feature is complete, functional testing can be easily done using Playwright or Cursor's built-in browser. But how does it know that the code is in good shape for review?
You can create a script like this that type checks, lints, runs unit tests, and produces a production build:
```
{
"scripts": {
"validate": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
[...]
}
}
```
then instruct the agent through e.g. CLAUDE.md to make use of this command.
```
Before considering a task complete, run:
pnpm validate
If it fails, fix the errors rather than working around the checks.
Do not remove tests or weaken types unless explicitly asked.
```
## Test-driven development, always
Agents are only good when they can complete the "code → test / validate → code again" loop with high confidence that the test / validate step actually reflects what the developer wants.
This is where the tried-and-tested TDD methodology really shines. First, you describe the intended spec to the agent. You can write a Markdown file for this. Next, the agent generates test cases. Now, you manually inspect those test cases to see if they reflect the behaviour that you want:
```
it('does not charge customers twice for the same billing period', () => {
// ...
})
```
If they don't, then the agent should change the tests. Once you're satisfied with the test spec, then (and only then) get the agent to start doing the real coding work.
For coding agents, a good test suite is not only good documentation, but also serve as great supervision.
## CI where local harness engineering isn't enough
Local hooks can only catch so many obvious problems. In the end, CI tests are where many bugs are found before they make it to production.
One example of where CI tests are most useful is for security. It's no secret that vibe coding has produced a lot more software vulnerabilities in recent months! When agents generate code quickly, they also generate more places for auth checks to be skipped, dependencies to sprawl, and business logic assumptions to break.
For example, tools like GitGuardian can catch accidentally-committed secrets, and Socket can catch vulnerable or suspicious dependencies to stop supply-chain attacks.
For deeper application security issues, especially the kinds generic scanners struggle with, you can also use AI-native tools like Hacktron in CI to review pull request for real code-level vulnerabilities: broken authorization, unsafe business logic, and other security regressions that require more context than simple pattern matching.
The advantage of tools like Hacktron is that unlike traditional scanners that still rely on known syntactic patterns and AI reviewers that provide only functional testing and code quality issues, Hacktron finds real security vulnerabilties that are introduced throughout the lifetime of your organisation using context-aware analysis to identify the security issues that Claude and Codex miss.
## Always think about affordance
I hope this article has been helpful to you. I've outlined some techniques and ways that we think about vibe coding while enforcing code quality and security.
The key thing to bear in mind is to always think about what your codebase and development environment is affording to the model. The output of your coding agent will depend heavily on that, because the environment dictates the constraints in which these agents operate.
## 相关链接
- [zayne (zeyu) zhang](https://x.com/zeyu1337)
- [@zeyu1337](https://x.com/zeyu1337)
- [3.3K](https://x.com/zeyu1337/status/2050759187576021219/analytics)
- [@HacktronAI](https://x.com/@HacktronAI)
- [pnpm](https://pnpm.io/)
- [nestjs-best-practices](https://github.com/Kadajett/agent-nestjs-skills)
- [typescript-advanced-types](https://github.com/sickn33/antigravity-awesome-skills/blob/main/plugins/antigravity-awesome-skills-claude/skills/typescript-advanced-types/SKILL.md)
- [GitGuardian](https://www.gitguardian.com/)
- [Socket](https://socket.dev/)
- [Hacktron](https://www.hacktron.ai/blog/introducing-hacktron-review)
- [Hacktron](https://www.hacktron.ai/blog/introducing-hacktron-review)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [10:07 AM · May 3, 2026](https://x.com/zeyu1337/status/2050759187576021219)
- [3,392 Views](https://x.com/zeyu1337/status/2050759187576021219/analytics)
---
*导出时间: 2026/5/3 19:42:49*
---
## 中文翻译
# 工程化管理:为编程 Agent 准备 TypeScript 代码库
**作者**: zayne (zeyu) zhang
**日期**: 2026-05-03T02:07:46.000Z
**来源**: [https://x.com/zeyu1337/status/2050759187576021219](https://x.com/zeyu1337/status/2050759187576021219)
---

在 @HacktronAI,我极力推动我们的产品团队全面拥抱使用 Claude Code 和 Codex 进行的“氛围编程(vibe coding)”,同时严格执行防护栏和代码质量标准。
当代码库具有很强的“可供性”时,氛围编程的效果最好——“可供性”是一个设计概念,描述了行动者(在此例中为编程 Agent)相对于对象(在此例中为代码库)可以采取的可能行动:
> 可供性:指事物可能具有的用途或目的,是人们观察或体验该事物时所感知到的一部分。
为了让像 Claude Code 或 Cursor 这样的编程 Agent 生成富有成效的代码,而不是生成日后维护和清理成本高昂的“AI 垃圾代码”,构建一个结构清晰且具有自动化防护栏的代码库变得至关重要。
即使是最聪明的模型,在没有良好的“约束”的情况下,也不可能考虑到每一个边缘情况。即使使用了像 Claude Code 这样的编程 Agent,以深思熟虑的方式设计仓库也能极大地提高代码质量。
仓库不应仅仅被视为一堆可执行的代码,而应被视为 Agent 的执行环境。因此,良好的氛围编程意味着该环境应提供:
- 针对“糟糕工程”的快速验证
- 受控的爆炸半径(影响范围)
- 在提交前强制执行不变量的防护栏
- Agent 可用于“自我氛围检查”的测试和脚本
## 让仓库对 Agent 来说清晰易读
使用 pnpm 并搭建 monorepo。如果你希望跨越多个仓库来处理前端和各种后端微服务,你需要么让你的编程 Agent 在这些仓库之间进行上下文切换,要么为它们提供过宽的权限,以便它们能在同一个会话中访问所有仓库。这并不友好。所以,直接使用 monorepo 吧。
```
apps/
frontend/
backend/
docs/
architecture.md
conventions.md
packages/
eslint-config/
shared-utils/
shared-tyles/
typescript-config/
CLAUDE.md
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
turbo.json
```
Monorepo 结构允许你创建多个使用共享包的应用。这些可以是工具和类型定义。此外,我发现将 ESLint 和 TypeScript 配置在共享包中标准化非常有用,这样就可以在新的应用和包中轻松导入它们。
例如,一旦你在共享包中导出了像这样的 ESLint 配置:
```
// packages/eslint-config/base.js
import js from '@eslint/js'
import eslintConfigPrettier from 'eslint-config-prettier'
import turboPlugin from 'eslint-plugin-turbo'
import tseslint from 'typescript-eslint'
import onlyWarn from 'eslint-plugin-only-warn'
/**
* A shared ESLint configuration for the repository.
* 仓库的共享 ESLint 配置。
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
'turbo/no-undeclared-env-vars': 'warn',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ['dist/**'],
},
]
```
每个应用和包都可以直接从该配置导入。
```
// apps/frontend/eslint.config.mjs
import { config } from '@my-project/eslint-config/base'
export default config
```
## 技能封装最佳实践
像 nestjs-best-practices 和 typescript-advanced-types 这样的技能可以帮助编程 Agent 生成地道的代码。当然,这其中很多都带有强烈的个人观点,所以我们也编写自己的技能来封装我们多年来学到的最佳实践。如果你不知道这些最佳实践应该是什么,可以去谷歌搜索,或者让 AI 参考一个展示了强大软件工程原则的示例仓库,让它自己想出技能。
我们的工程师使用各种不同的 Agent:Claude、Codex、Cursor……因此,如果我们希望这些技能有用并在团队成员之间共享,我们需要每个编程 Agent 都使用同一套技能。
这就是为什么技能存储在 `.agents` 中,而 `.codex`、`.claude` 等通过符号链接链接到存储在主 `.agents` 目录中的技能。
```
.agents/
skills/
typescript-expert/
SKILL.md
typescript-advanced-types/
SKILL.md
[...]
.codex/
skills/
typescript-expert -> ../../agents/skills/typescript-expert
typescript-advanced-types -> ../../agents/skills/typescript-advanced-types
[...]
.claude/
skills/
typescript-expert -> ../../agents/skills/typescript-expert
typescript-advanced-types -> ../../agents/skills/typescript-advanced-types
[...]
[...]
```
## Agent 阅读和维护的文档
如果写得好,CLAUDE.md(或等效文件)对于提供自我演进的文档大有裨益。该文档可以概述架构、技术栈,但更重要的是,概述 AI Agent 应遵守的规则。
```
# VibeSlop - 最佳氛围编码应用
## 概览
VibeSlop 拥有一个 NestJS 后端和一个 Nuxt 前端。它是一个 B2B AI SaaS。
[...]
## Notion 文档
**重要提示**:VibeSlop 在 Notion 中有详尽的文档,应与代码变更保持同步。
**主页**:https://notion.so/[...]
### 文档结构
| 部分 | 页面 ID | 描述 |
| -------------- | ---------- | ------------------------------ |
| Authentication | `DEADBEEF` | Auth guards, token types, RBAC |
| [...] | [...] | [...] |
### 何时更新 Notion 文档
在以下情况下更新相关的 Notion 页面:
- 添加新的 API 端点 → 更新 API 参考
- 添加/修改实体 → 更新数据库和实体
- 更改 auth guards 或 token 处理 → 更新身份验证
[...]
### 如何更新
使用 Notion MCP 工具:
- `mcp__notionMCP__notion-fetch` - 读取现有页面内容
- `mcp__notionMCP__notion-update-page` - 更新页面内容
- `mcp__notionMCP__notion-create-pages` - 创建新的嵌套页面
[...]
## AI 编码规则(强制性)
这些规则是不容协商的。每一次代码变更——无论是新功能、错误修复还是重构——都必须遵守。违规必须在提交前修复。
### DTO 和 OpenAPI 契约
[...]
### TypeScript 严格性
- **禁止类型转换**,除了 `as const`。禁止 `as unknown as X`、`as any`、`as SomeType`、`@ts-ignore`、`// @ts-expect-error`。
- **使用枚举** 代替魔法字符串。如果一个值有一组固定的选项,定义一个枚举。
- **谨慎使用可选字段** —— 当字段在语义上是必需的但可能不存在时,优先使用联合类型(`string | null`)而不是可选(`string?`)。
- **禁止重新声明** 已存在于 `@my-project/shared-types`、实体定义或生成的代码中的类型。
- `pnpm check-types` 必须在提交前通过。
### 架构
[...]
### 最小化变更 / 拒绝垃圾
AI 生成的代码会不断累积:叙述性注释、一次性辅助函数、早期迭代留下的死代码、针对不可能发生的情况的错误处理。在宣布完成之前,请以挑剔的眼光重新阅读你自己的 diff,并删除当前实现不需要的所有内容。原则是:错误修复不需要附带大扫除,一次性变更不需要辅助函数,当后续迭代取代先前迭代时,先前的迭代就过时了。
- **完成后从头到尾重新阅读 diff。** 经过多次迭代后,文件会残留遗留物 —— 被替换的方法、未使用的导入、过时的分支、不再被任何调用的辅助函数。删除它们。Git 拥有历史记录;代码库不需要墓碑。
- **禁止叙述性注释。** 不要解释 WHAT(命名已经做到了)或引用任务(“为 X 添加”、“被 Y 流程使用”、“处理问题 Z”)。只有当 WHY 不明显时才写注释:隐藏的约束、变通方法、令人惊讶的不变量。
- ✗ `// Loop through findings and send feedback to Slack`
- ✗ `// Added for the unfurl flow` / `// TODO: remove old logic once migrated`
- ✓ `// Stripe retries webhooks on 5xx — dedupe on event.id before mutating state`
- **禁止注释掉的代码,不保留“已删除 X”的墓碑,不保留刚刚在同一 PR 中删除的代码的向后兼容垫片。** 如果它消失了,那就是消失了。不要保留重命名的 `_oldMethod` “以防万一”。
- **禁止一次性抽象。** 在存在第二个调用者之前,不要创建辅助函数、包装器、基类或自定义装饰器。三行相似的代码胜过过早的抽象。`packages/shared-utils/src/status-mapper.ts` 就是证明提取合理性的样子 —— 它在 `scan/`、`findings/` 和 `cost-estimation/` 中被使用。不要人为制造那个门槛;让重复来证明它。
- **禁止推测性错误处理。** 信任内部调用者和框架保证。DTO 已经通过 `class-validator` 验证了控制器输入 —— 接收类型化 `SendFeedbackDto` (`src/findings/dto/send-feedback.dto.ts`) 的服务不会重新检查 `reaction` 是否为字符串。仅在真正的边界进行验证:HTTP 输入、webhook 负载、外部 API 响应、无类型的 env 变量。
- ✗ `try { return await this.repo.findOne(...) } catch (e) { throw e }`
- ✗ `if (!user) throw new Error('user required')`,其中参数类型是 `User`,而不是 `User | undefined`
- ✗ 将单个 `repo.save()` 包裹在记录并重新抛出错误的 try/catch 中
- **优先编辑现有文件和重用现有类型。** 在编写新的辅助函数之前,先搜索 `src/utils/`、`src/services/`、`src/dto/` 和 `@my-project/shared-utils`。对于分页端点,重用 `PaginationDto` (`src/dto/pagination.dto.ts`),而不是重新定义 `page`/`limit`。从 `@my-project/shared-types` 重用实体类型,而不是重新声明形状。除非有实际理由,否则不要将 200 行的服务拆分成四个文件。
- **保持形状最小化。** 控制器保持精简 —— 验证 → 服务 → 返回,无分支,无查询(参见 `src/findings/findings.controller.ts`)。DTO 仅携带请求/响应字段,用 `@ApiProperty` + `class-validator` 装饰 —— 仅此而已(参见 `src/findings/dto/send-feedback.dto.ts`、`src/dto/pagination.dto.ts`)。实体保持为列 + 关系 —— 除非真正需要,否则没有计算属性 getter 或生命周期钩子(参见 `src/seat/organization-developer.entity.ts`)。
- **前端注意事项:** UI 迭代是垃圾代码堆积最快的地方 —— 未使用的 props、过时的 Tailwind 类、两个版本前设计遗留的死条件分支、没有任何东西读取的状态。同样的规则适用,但力度更大:在宣布完成之前,对照当前设计从上到下阅读组件,并删除当前设计中不使用的任何内容。
### 质量门禁
- 测试必须在提交前通过(`pnpm test`)。
- Linter 必须在提交前通过(`pnpm lint`)。
- 类型检查器必须在提交前通过(`pnpm check-types`)。
```
这里有几件重要的事情:
1. 我们通过指示 Agent 更新 Notion 文档来强制执行自我文档化开发。这假设使用了 Notion MCP。
2. 我们根据过去观察到的行为强制执行 AI 编码规则。例如,我们看到前端代码由于 UI 迭代的性质产生了大量垃圾代码:它的目的是产生许多不同的变体,直到开发人员对结果满意为止。这意味着通常情况下,编程 Agent 会留下许多来自先前迭代的过时和死代码。我们发现执行“最小化变更”规则很有帮助。
## 针对“垃圾代码”的“垃圾回收”
即使我们尽了最大努力,“垃圾代码”也是不可避免的。这并不是说人类以前没有产生过垃圾代码。但 AI 使我们能够通过定期审计代码库来解决这个问题,例如查找死代码(没有引用的函数)、过时的文档等。
我们通过创建一个 GitHub Actions 工作流来做到这一点,它每 24 小时运行一次 Claude Code,并使用提示词要求它:
1. 根据我们在仓库下的 docs/ 中维护的一套规则清理低质量的代码。
2. 根据最新的代码变更更新上述的 CLAUDE.md。
```
name: Claude Garbage Collection
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
concurrency:
group: claude-garbage-collection
cancel-in-progress: false
jobs:
cleanup:
strategy:
fail-fast: false
matrix:
target_branch:
- staging
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ matrix.target_branch }}
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
token_format: access_token
- name: Set NPM_TOKEN for Artifact Registry
run: echo "NPM_TOKEN=${{ steps.auth.outputs.access_token }}" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Claude garbage collection task
id: claude-cleanup
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
base_branch: ${{ matrix.target_branch }}
prompt: |
Read `CLAUDE.md` and `docs/cleanup/README.md`. Use `docs/cleanup/` as the source of truth for this garbage collection pass.
Work only against `${{ matrix.target_branch }}` and keep the change scoped to that branch's current state.
You may make multiple improvements, but each PR must stay focused on one small, safe maintenance concern.
Leave the repository unchanged if there is no clear cleanup to make.
additional_permissions: |
actions: read
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(pnpm:*),Bash(gh:*)'"
sync-claude-md:
strategy:
fail-fast: false
matrix:
target_branch:
- staging
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ matrix.target_branch }}
- name: Sync CLAUDE.md with codebase
id: claude-md-sync
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
base_branch: ${{ matrix.target_branch }}
prompt: |
Your sole task is to update all `CLAUDE.md` files so they accurately reflect the current codebase on the `${{ matrix.target_branch }}` branch.
Steps:
1. Read every `CLAUDE.md` file in the repo (root `.claude/CLAUDE.md` and any nested ones like `apps/my-app/CLAUDE.md`, etc.).
2. Audit each section against the actual codebase:
- **Project structure**: list directories under `apps/my-app/src/` and update the tree if