理论 21 篇讲完了,本章从零写一个真实 Skill,让你看一遍从想法 → 写 SKILL.md → references 拆解 → 测试 → 装入 OpenCode → 上调用的全过程。我们要写的 Skill 叫 tdd-reminder:让主 Agent 在写新代码前先提示用户走 TDD。

22.1 Skill 想法从哪里来

三步法挖掘想法:

  1. 找你过去 7 天的 OpenCode session log

  2. grep 模式 “你忘了 / 你没做 / 应该先”

    rg "忘了\|应该\|未做\|先\|要是之前" .opencode/log/ | head -20
    
  3. 凡是出现 3 次以上的模式,就是候选 Skill

我自己日志里有一条:“每次写完 Python 函数才发现没写测试,又往回加 test_test_xxx,凑合跑”——这就是 tdd-reminder 要解决的:让 Agent 在写实现代码前主动说一句"咱们先写 test 行吗"

22.2 拆解 Skill 行为需求

维度 spec
Trigger 主 Agent 即将写 Python / TS / Go / Rust 代码,且函数 / class 存在
不要触发 1)用户问解释性问题 2)改一行小 bug 3)已有现成测试
行为 提醒想先写 unit test;提供一次 brainstorm 机会,而非强制
DONE 用户明确说 “不用 test” 或 用户已提供 test 或 单元 test 跑通
失败模式 1)用户说"快点别叨叨" → 跳过加 [tdd-reminder skipped] 自报\n2)该语言无 pytest 等价物 → 改用最简化 shell test

22.3 主 SKILL.md

---
name: tdd-reminder
description: |
  在写新 production 代码前显式提醒用户走 TDD。
  Provides an opportunity (not enforcement) to write tests first.
---

# tdd-reminder

You MUST read this skill when:
  - About to write production code in {Python, TypeScript, Go, Rust}
  - The code adds a new function / method / class (≥ 10 lines)
  - No existing test for the entity being written

You MUST NOT read this skill when:
  - User is asking an explanatory question (e.g., "how does X work")
  - The change is a one-line bug fix
  - An existing test already covers the entity

## What to do

When triggered, BEFORE writing the production code:

1. Ask in one short line:
   > "要不要先写个 test? (yes / skip)"
2. If user says "yes": write the test first, run it (it should fail), then implement.
3. If user says "skip" or "no": continue, but log:
   `[tdd-reminder skipped] reason: user-declined`

## DONE

When either:
  - Test passes after implementation, or
  - User skipped and `[tdd-reminder skipped]` logged

## Failure Modes

- User is in a hurry ("快点别叨叨"):
  - Skip and log `[tdd-reminder skipped] reason: user-hurry`
- Language has no pytest-equivalent available:
  - Allow fallback to shell-print-based ad-hoc test
  - Log `[tdd-reminder partial] reason: no-test-framework`
- User provides an existing test (points you at tests/foo.py):
  - Skip the prompt; do not duplicate.

## Output prefix

When triggered, prefix your first line with `[tdd-reminder]` for grep-ability.
When user accepts and you write test, prefix with `[tdd-reminder active]`.
When skipped, prefix with `[tdd-reminder skipped]`.

## References

- See `references/which-test-framework.md` for language-specific frameworks
- See `references/edge-cases.md` for boundary examples (CLI, fixtures, mocks)

把这段 SKILL.md 写到 .opencode/skills/tdd-reminder/SKILL.md

22.4 references/:拆细节

主 SKILL.md 不写长细节——细节放 references/:

.opencode/skills/tdd-reminder/
├── SKILL.md
└── references/
    ├── which-test-framework.md
    └── edge-cases.md

references/which-test-framework.md

# Test Framework Quick Pick

| Language    | Framework      | Single-file command                  |
|-------------|----------------|--------------------------------------|
| Python      | pytest         | `pytest -x tests/test_foo.py::test_a`|
| TypeScript  | vitest / bun test | `bun test path/to/test.ts`        |
| Go          | go test        | `go test ./pkg/... -run ^TestFoo$`   |
| Rust        | cargo          | `cargo test pkg_name::test_foo`      |
| Hono/Node   | vitest / jest  | `bun test`                           |

If the user's project lacks any of these, fall back to:
  - Node: `node --test`
  - Python: built-in `unittest` stdlib module
  - Go: stdlib testing
  - Rust: stdlib `#[test]`

Only fall back when confirms by checking `Taskfile.yml` / `pyproject.toml` / `go.mod` / `Cargo.toml`.

references/edge-cases.md

# Edge Cases

## CLI commands
If writing a CLI tool, test with:
  - single-command: `subprocess.run(["my-cli", "arg"])` + assert exit code
  - long-running: don't test long runs; test setup / parse logic separately

## HTTP handlers
Use `httpx.AsyncClient` (Python) or `supertest` (Node).  DO NOT start real server in unit tests.

## Mocks vs fakes
Prefer fakes (in-memory impl) over mocks (patched functions).  Mocks hide behavior; fakes reveal it.

22.5 把它 fork 到 OpenCode,跑通

OpenCode 会自动 monitor skills 目录。第一次启动后:

# 测试 Skill 被加载
opencode
> 给 lib/validators.py 的 is_alpha 加一个空字符串返回 true 的分支

# Expected:
[tdd-reminder] About to write new code for is_alpha.
要不要先写个 test? (yes / skip)

如果没触发:

  1. 检查文件位置:必须是 .opencode/skills/tdd-reminder/SKILL.md~/.config/opencode/skills/...
  2. 检查 name: 与文件夹名相符
  3. 看 OpenCode session 启动日志是否有 “loaded skill: tdd-reminder”

22.6 测试 trigger 稳定性

跑 5 个 probe prompt:

A. 解释性的:为什么 Python 不需要类型?
B. 加功能:  给 utils.py 加一个 is_alpha 函数,空字符串返回 true
C. 改 bug:  把 src/foo.ts 第 12 行的 == 改成 === 
D. 加 feature:写一个新的 SQL 解析器
E. 一次性补全: 把 print(x) 改成 print(x, end="")

预期:

Prompt 触发?
A ❌(解释性跳过)
B ✅(新函数、可能 skip)
C ❌(一行改 bug)
D
E

5 个测试都通过,trigger 信号噪比 acceptable。

22.7 加 self-check:Skill 内自报

让 skill-reminder 自报"我触发了",方便你日后 grep session log:

修改 SKILL.md 平行:

When the skill loaded, your next message MUST start with one of:
  - `[tdd-reminder engaged]` (user about to write new code, you offered)
  - `[tdd-reminder skipped] reason: <reason>` (you decided not to engage)

7 天后跑:

rg "\[tdd-reminder" .opencode/log/

统计 engaged vs skipped。有数据,才能改 trigger 优化——这就是 14 章 Agent 可观测性 的开始:

本周统计:
  engaged          : 14 次
  skipped - user-skip : 3 次
  skipped - haste    : 1 次
  Skill 沉淀有效

22.8 跨 harness 兼容

复制到 Claude Code:

mkdir -p .claude/skills/tdd-reminder/references
cp .opencode/skills/tdd-reminder/{SKILL.md,references/*} .claude/skills/tdd-reminder/

绝大多数 prompt-only Skill 都能直接复用——SKILL.md 是 markdown。

可能差异的元字段:

  • OpenCode 偏好 <trigger> 区块
  • Claude Code 2026 下半年加了 priority field
  • Codex 二开版用 lifecycle 标 BEFORE/AFTER

跨 harness 迁移时只改 meta,主内容不动

22.9 跨项目共享:把它升 user skill

写好一个 skill 自己用,意识到团队也能用:移到 ~/.config/opencode/skills/tdd-reminder/,所有项目可见。

mv .opencode/skills/tdd-reminder ~/.config/opencode/skills/

更进一步开放:

  1. 把 skill 单独 git 仓库化:github.com/you/opencode-skill-tdd-reminder
  2. 用 plugin 机制安装:
opencode plugin install github:you/opencode-skill-tdd-reminder

社区发现:

  • 仓库 readme 写 Skill 能力 + 触发 demo + 故意失败 demo
  • 加 LICENSE、CHANGELOG、版本标签
  • OpenCode plugin 注册中心预计 2026 下半年 acceptance

22.10 一个反面教材:糟糕的 Skill 长什么样

# ❌ 反例
name: vague-skill
description: |
 帮助写好代码
trigger: 看情况
flow: 想想,然后写
done: 写完了就 done
failure: 失败就再改

问题:

  • trigger 不可机器判别
  • flow 无步骤
  • done 无标准
  • failure 模式省略
  • 没 self-check
  • 没有 references

段错误的 Skill 会整周把 Agent 误导。低质量 skill 比没 skill 还糟——因为它"有形"但不准。

22.11 小结

  • Skill 从 grep 自己 log 找模式挖掘,values 3 次以上的值得沉淀
  • SKILL.md 短 + references/ 拆细节 + 输出自报 + 失败模式列
  • 5 个 probe 验证 trigger;7 天统计 engaged vs skipped 调
  • 跨 harness 复制 paste friendly,跨团队 install 走 plugin

下一篇: 《23 Skill 测试与演化》——把 Skill 当作 software:有 unit test、有 regression test、有 changelog。

Summary: 从 grep 找模式到 trigger 5-probe 验证到 self-report log 反馈,自写第一个 Skill 的完整 workflow。