名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Memories 只适用于 Cascade(旧版智能体),Devin Local 用 Skills。配置目录优先用
.devin/,旧的.windsurf/仍会被读取。本文按当前文档写,界面细节以你安装的版本为准。
这篇给一个项目写一套规则:根目录
AGENTS.md(始终生效)、子目录api/AGENTS.md(只在改 api/ 时生效),以及.devin/rules/下四种激活方式各一个规则文件。再写一个lint_rules.py:检查 trigger 是否合法、glob 规则有没有 globs、model_decision 有没有 description、是否超过字数上限,并模拟“编辑某个文件时会带上哪些规则”。
先看清成品
- 检查脚本在示例项目上真实运行:6 个文件全部通过;在故意写错的项目上找出 5 个错误和 2 条提醒。
- 发现官方示例
globs: **/*.test.ts不是合法 YAML(PyYAML 报 ScannerError),所以脚本没有用 yaml 库解析。- 4 项 pytest 通过。
合规提醒:用你自己的账号和授权的套餐,不共享账号。规则、工作流和 MCP 配置会把指令和工具交给 AI,不要写进密钥;公司代码先确认组织政策是否允许使用。AI 运行命令前要看清楚,不要开启“自动执行所有命令”。
运行说明:box 是 Linux 服务器,没有图形界面,我没有安装或运行 Devin Desktop / Windsurf 编辑器,所以没有验证编辑器实际加载这些文件的效果。真实运行过的是:本文的检查脚本、示例项目和 MCP 服务器(2026-10-21,UTC+8)。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
demo/ |
示例项目的规则和 AGENTS.md |
broken/ |
故意写错的规则 |
lint_rules.py |
规则检查 |
test_lint_rules.py |
4 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| 编辑器 | Devin Desktop(原 Windsurf),从 https://windsurf.com 下载 |
| Python | 3.11 以上(用到 tomllib),pip install pytest |
关键事实
| 事实 | 来源 |
|---|---|
全局规则 ~/.codeium/windsurf/memories/global_rules.md,始终生效,最多 6000 字符 |
官方 Memories & Rules |
工作区规则 .devin/rules/*.md(优先)或 .windsurf/rules/*.md,一个文件一条规则,每个最多 12000 字符;旧的 .windsurfrules 仍会读取 |
同上 |
frontmatter 的 trigger:always_on(每条消息都带)、model_decision(只带 description,需要时读全文)、glob(读写匹配 globs 的文件时)、manual(输入 @规则名 时) |
同上 |
AGENTS.md 不需要 frontmatter:根目录 = 始终生效,子目录 = <目录>/** 的 glob 规则 |
官方 AGENTS.md |
| 自动生成的 Memories 只在本机、只对旧版 Cascade 智能体有效;需要长期保留的知识建议写成规则或 AGENTS.md | 官方 Memories & Rules |
文件结构
89-windsurf-A-src/
├── demo/
│ ├── AGENTS.md
│ ├── api/AGENTS.md
│ └── .devin/rules/
│ ├── security.md # always_on
│ ├── python-style.md # glob
│ ├── db-migrations.md # model_decision
│ └── release-notes.md # manual
├── broken/ # 故意写错
├── lint_rules.py
└── test_lint_rules.py
第 1 步:根目录 AGENTS.md
# 项目约定(根目录 AGENTS.md:始终生效)
- Python 3.11,依赖用 `uv` 管理,不要用 pip 直接装到系统里。
- 改完代码先跑 `uv run pytest -q`,全部通过再说“完成”。
- 不要读取或修改 `.env`、`secrets/` 下的任何文件。
第 2 步:api/AGENTS.md
# api/ 目录约定(子目录 AGENTS.md:只在读写 api/ 下的文件时生效)
- 所有接口返回 JSON,错误统一为 `{"error": {"code": ..., "message": ...}}`。
- 新接口必须同时写测试,放在 `api/tests/`。
第 3 步:四个规则文件
<!-- security.md -->
---
trigger: always_on
---
- 不要在代码、日志、提交信息中写入任何密钥或 Token。
- 运行会删除文件、改 git 历史或访问生产环境的命令前,先停下来问我。
<!-- python-style.md -->
---
trigger: glob
globs: **/*.py
---
- 函数和类都写 docstring(中文即可)。
- 能提前 return 就提前 return,少嵌套。
- 不要 `except Exception: pass`,至少记录日志。
<!-- db-migrations.md -->
---
trigger: model_decision
description: 修改数据库表结构、写迁移脚本或改 models.py 时使用
---
<migrations>
- 迁移只能新增,不能修改已经合并的迁移文件。
- 删除列分两步:先停止读写并发版,下一个版本再删除。
- 每个迁移都要能回滚,写好 downgrade。
</migrations>
<!-- release-notes.md -->
---
trigger: manual
---
写发布说明时:按“新增 / 修复 / 变更”三类列出,每条一句话,面向用户而不是开发者,不写内部工单号。
第 4 步:检查脚本
"""检查 Devin Desktop(原 Windsurf)工作区规则:.devin/rules/*.md、旧的 .windsurf/rules/*.md 和 AGENTS.md。
规则来自 docs.devin.ai/desktop/cascade/memories 与 agents-md:
- 每个规则文件的 frontmatter 用 trigger 声明激活方式:always_on / model_decision / glob / manual;
- glob 需要 globs,model_decision 靠 description 让模型决定是否读取全文;
- 工作区规则每个文件最多 12000 字符,全局 global_rules.md 最多 6000 字符;
- AGENTS.md 不需要 frontmatter:根目录始终生效,子目录相当于 <目录>/** 的 glob 规则。
注意:官方示例写的是 `globs: **/*.test.ts`(不加引号),这不是合法的 YAML(** 会被当成别名),
所以这里按“一行一个 key: value”解析,不用 yaml 库。"""
import fnmatch, sys
from pathlib import Path
TRIGGERS = {"always_on", "model_decision", "glob", "manual"}
WS_LIMIT, GLOBAL_LIMIT = 12000, 6000
def frontmatter(text):
if not text.startswith("---\n"):
return None, text
end = text.find("\n---", 4)
if end < 0:
return None, text
meta = {}
for line in text[4:end].splitlines():
if ":" in line:
k, v = line.split(":", 1); meta[k.strip()] = v.strip()
return meta, text[end + 4:].lstrip("\n")
def lint_rule(path):
text = Path(path).read_text(encoding="utf-8"); errs, warns = [], []
if len(text) > WS_LIMIT:
errs.append(f"{len(text)} 字符,超过工作区规则上限 {WS_LIMIT}")
meta, body = frontmatter(text)
if meta is None:
errs.append("没有 frontmatter(--- trigger: ... ---)"); return errs, warns, None
t = meta.get("trigger")
if t not in TRIGGERS:
errs.append(f"trigger={t!r},应为 {sorted(TRIGGERS)} 之一"); return errs, warns, t
if t == "glob" and not meta.get("globs"):
errs.append("trigger: glob 但没写 globs")
if t == "model_decision" and not meta.get("description"):
errs.append("trigger: model_decision 但没写 description,模型无从判断何时读取")
if t != "glob" and meta.get("globs"):
warns.append(f"写了 globs 但 trigger 是 {t},globs 不会起作用")
if not body.strip():
errs.append("正文为空")
if t == "always_on" and len(text) > 3000:
warns.append("always_on 规则每条消息都会占上下文,内容较长时考虑改成 model_decision 或 glob")
return errs, warns, t
def agents_scope(root, path):
rel = Path(path).parent.relative_to(root)
return "always_on" if str(rel) == "." else f"glob {rel.as_posix()}/**"
def lint_tree(root):
root = Path(root); report, bad = [], 0
files = sorted(list(root.rglob(".devin/rules/*.md")) + list(root.rglob(".windsurf/rules/*.md")))
names = {}
for f in files:
errs, warns, t = lint_rule(f); bad += len(errs)
report.append((f.relative_to(root).as_posix(), t, errs, warns))
names.setdefault(f.name, []).append(f.relative_to(root).as_posix())
for n, ps in names.items():
if len(ps) > 1 and any(".devin/" in p for p in ps) and any(".windsurf/" in p for p in ps):
report.append((n, None, [], [f"同名规则同时在 .devin 和 .windsurf 下:{ps},.devin 优先,旧文件可能已失效"]))
for f in sorted(list(root.rglob("AGENTS.md")) + list(root.rglob("agents.md"))):
t = f.read_text(encoding="utf-8"); errs = []
if t.startswith("---\n"):
errs.append("AGENTS.md 不需要 frontmatter,作用范围由所在目录决定")
if len(t) > WS_LIMIT:
errs.append(f"{len(t)} 字符,超过 {WS_LIMIT}")
bad += len(errs)
report.append((f.relative_to(root).as_posix(), agents_scope(root, f), errs, []))
if (root / ".windsurfrules").exists():
report.append((".windsurfrules", "always_on", [], ["旧的单文件规则仍会被读取,建议拆分到 .devin/rules/"]))
return report, bad
def which_apply(root, file_path):
"""模拟:编辑某个文件时,哪些 glob 规则和子目录 AGENTS.md 会被带上(always_on 总是带上)。"""
out = []
for f in sorted(Path(root).rglob(".devin/rules/*.md")):
meta, _ = frontmatter(f.read_text(encoding="utf-8"))
if meta and meta.get("trigger") == "glob":
pats = [p.strip() for p in meta.get("globs", "").split(",") if p.strip()]
if any(fnmatch.fnmatch(file_path, p) or fnmatch.fnmatch("x/" + file_path, p) for p in pats):
out.append(f.name)
for f in Path(root).rglob("AGENTS.md"):
rel = f.parent.relative_to(root).as_posix()
if rel != "." and file_path.startswith(rel + "/"):
out.append(f"{rel}/AGENTS.md")
return out
def main(argv=None):
argv = argv if argv is not None else sys.argv[1:]
root = argv[0] if argv else "."
report, bad = lint_tree(root)
for p, t, errs, warns in report:
print(f"{'✗' if errs else '✓'} {p:<36} {t or ''}")
for e in errs: print(" 错误:", e)
for w in warns: print(" 提醒:", w)
for fp in argv[1:]:
print(f"编辑 {fp} 时额外生效:{which_apply(root, fp) or '无'}")
print(f"共 {len(report)} 项,{bad} 个错误")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
第 5 步:测试
import lint_rules as L
def test_demo_clean():
report, bad = L.lint_tree("demo")
assert bad == 0 and len(report) == 6
def test_broken_found():
report, bad = L.lint_tree("broken")
msgs = " ".join(e for _, _, errs, _ in report for e in errs)
assert bad == 5
for k in ("description", "12000", "'always'", "没写 globs", "不需要 frontmatter"):
assert k in msgs
def test_frontmatter_unquoted_glob():
# 官方示例写法,yaml 库会报错,这里要能解析
meta, body = L.frontmatter("---\ntrigger: glob\nglobs: **/*.test.ts\n---\n\n正文")
assert meta == {"trigger": "glob", "globs": "**/*.test.ts"} and body == "正文"
def test_which_apply():
assert L.which_apply("demo", "api/routes.py") == ["python-style.md", "api/AGENTS.md"]
assert L.which_apply("demo", "main.py") == ["python-style.md"]
assert L.which_apply("demo", "web/app.js") == []
第 6 步:运行
$ python lint_rules.py demo api/routes.py web/app.js
✓ .devin/rules/db-migrations.md model_decision
✓ .devin/rules/python-style.md glob
✓ .devin/rules/release-notes.md manual
✓ .devin/rules/security.md always_on
✓ AGENTS.md always_on
✓ api/AGENTS.md glob api/**
编辑 api/routes.py 时额外生效:['python-style.md', 'api/AGENTS.md']
编辑 web/app.js 时额外生效:无
共 6 项,0 个错误
$ python lint_rules.py broken
✗ .devin/rules/deploy.md model_decision
错误: trigger: model_decision 但没写 description,模型无从判断何时读取
✗ .devin/rules/huge.md always_on
错误: 15427 字符,超过工作区规则上限 12000
提醒: always_on 规则每条消息都会占上下文,内容较长时考虑改成 model_decision 或 glob
✗ .devin/rules/lang.md always
错误: trigger='always',应为 ['always_on', 'glob', 'manual', 'model_decision'] 之一
✗ .devin/rules/tests.md glob
错误: trigger: glob 但没写 globs
✓ .windsurf/rules/tests.md manual
✓ tests.md
提醒: 同名规则同时在 .devin 和 .windsurf 下:['.devin/rules/tests.md', '.windsurf/rules/tests.md'],.devin 优先,旧文件可能已失效
✗ docs/AGENTS.md glob docs/**
错误: AGENTS.md 不需要 frontmatter,作用范围由所在目录决定
共 7 项,5 个错误
$ echo $?
1
$ python -m pytest -q
4 passed
怎么检查结果?
- 把
demo/里的文件放进你的项目,在编辑器的 Customizations → Rules 面板里应能看到 4 条工作区规则,激活方式和文件里写的一致。 - 在对话里问一个 Python 文件的问题,再问一个 JS 文件的问题,对比 Python 风格规则是否只在前者生效。
which_apply用 Python 的 fnmatch 模拟 glob,和编辑器的匹配实现不一定完全一样,只用来粗查。
调试示例
1. 官方示例的 frontmatter 不是合法 YAML。 文档里的例子是:
---
trigger: glob
globs: **/*.test.ts
---
我一开始想用 PyYAML 解析 frontmatter,结果:
>>> yaml.safe_load('trigger: glob\nglobs: **/*.test.ts')
ScannerError while scanning an alias
>>> yaml.safe_load('globs: "**/*.test.ts"')
{'globs': '**/*.test.ts'}
在 YAML 里 * 开头表示别名,所以不加引号会报错。编辑器显然不是用严格的 YAML 解析的(文档就是这么写的)。我的检查脚本因此按“一行一个 key: value”解析,并加了测试。影响:如果你的仓库里有 pre-commit 的 YAML 检查或其他工具扫描 frontmatter,可能会对这些规则文件报错。加引号能不能被编辑器正确识别,我没法在 box 上验证,所以示例保持和官方一致不加引号。
2. 同名规则同时出现在 .devin 和 .windsurf。 从旧版升级的项目可能两个目录都有。文档说 .devin/ 优先、.windsurf/ 作为兼容。脚本发现同名文件时会提醒,避免你改了旧目录里的规则却发现没生效。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 规则没生效 | trigger 写错(如 always) | 用 lint_rules.py 检查 |
| 规则太长被截断 | 超过 12000 / 6000 字符 | 拆成多个文件,或改成 model_decision |
| 每条消息都很慢、上下文不够 | always_on 规则太多太长 | 只把真正全局的放 always_on |
| 改了规则没变化 | 改的是 .windsurf 下的旧文件 | 以 .devin/rules 为准 |
模板规则
- always_on 只放安全和全局约定。
- 目录专属约定用子目录 AGENTS.md。
- model_decision 一定写好 description。
- 规则提交到仓库,和代码一起评审。
项目资源
| 用途 | 地址 |
|---|---|
| Devin Desktop 文档(原 docs.windsurf.com) | https://docs.windsurf.com |
| Memories & Rules(官方) | https://docs.devin.ai/desktop/cascade/memories |
| Devin Local 智能体(官方) | https://docs.devin.ai/desktop/devin-local |
| AGENTS.md(官方) | https://docs.devin.ai/desktop/cascade/agents-md |
| Devin Desktop 官网(原 windsurf.com) | https://windsurf.com |
规则写得少而准,比写一大篇更有用。