coderio 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: 需为代码写测试或改进脆弱测试时。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
写能真正抓住 bug、且不脆弱(改实现不会无端崩)的测试。核心:测试是**行为的契约**,不是"代码的镜像"——测的是"给它这个输入,该有这个结果",不是"它内部该这样工作"。好测试让你敢改实现(因为测试守着行为),坏测试让你不敢改(因为一改测试就崩)。
|
|
11
|
+
|
|
12
|
+
## 何时使用
|
|
13
|
+
|
|
14
|
+
- 写完功能,要补测试
|
|
15
|
+
- 写测试时纠结测什么、不测什么、要不要 mock
|
|
16
|
+
- 现有测试脆弱(改实现就一片红)或 flaky(时过时不过)
|
|
17
|
+
- 测试覆盖率挺高但 bug 还是漏
|
|
18
|
+
|
|
19
|
+
**不该用**:纯探索/原型(不要求正确,测试是负担);一次性脚本(跑完即弃)。
|
|
20
|
+
|
|
21
|
+
**与相邻 skill 的衔接**:`testing` 在 task-breakdown 下游、verify-and-fix 上游——task 拆出"做完 X 能验证 Y",testing 负责**把 Y 写成可重复运行的测试**,verify-and-fix 负责跑它验证。三者接力:task 定验证目标 → testing 把目标落地成测试 → verify-and-fix 用测试验证完成。
|
|
22
|
+
|
|
23
|
+
## 核心内容
|
|
24
|
+
|
|
25
|
+
### 先识别被测对象的性质
|
|
26
|
+
|
|
27
|
+
写测试前先看清"被测的是什么",策略大不相同——这决定了要不要 mock、用什么工具、放哪个层次:
|
|
28
|
+
|
|
29
|
+
- **纯函数**(无副作用、输入决定输出,如 `calculateDiscount`)→ 直接输入输出断言,零 mock,单元层。
|
|
30
|
+
- **有外部依赖的逻辑**(如调 DB/HTTP 的 service)→ mock 掉外部副作用,验证被测逻辑对依赖返回值的真实处理(详见下文 mock 纪律)。
|
|
31
|
+
- **UI 组件**(渲染 + 交互)→ 用组件测试库测渲染输出和用户交互,不测内部 state 细节。
|
|
32
|
+
- **模块协作**(多个组件配合)→ 集成层,用真实(或内存版)依赖验证组件间契约。
|
|
33
|
+
|
|
34
|
+
识别性质能避免最常见的错配:给纯函数上 mock、给 UI 组件测 state、把单元能测的逻辑推到端到端。先问"它是什么",再问"怎么测"。
|
|
35
|
+
|
|
36
|
+
### 测行为,不测实现
|
|
37
|
+
|
|
38
|
+
这是测试设计的第一原则。**测"做什么",不测"怎么做"**:
|
|
39
|
+
|
|
40
|
+
- **测行为**(对):给定输入,断言输出/可观测结果。例:`calculateDiscount(100, 'vip')` 应返回 `80`。
|
|
41
|
+
- **测实现**(错):断言内部走了哪个分支、调了哪个方法几次。例:断言"内部调用了 `multiply` 两次"。
|
|
42
|
+
|
|
43
|
+
为什么测实现糟糕:**实现是会变的**(重构、换算法、优化),但行为不该变。测实现的测试,每次合理的实现改动都会让它崩——这就是脆弱测试。它逼你改实现时还得改测试,让测试从"保护"变成"负担"。
|
|
44
|
+
|
|
45
|
+
判别尺子:**问自己"如果我把内部实现整个换掉(但行为不变),这个测试还该过吗?"** 该过 → 测的是行为(对);崩了 → 测的是实现(错,改)。
|
|
46
|
+
|
|
47
|
+
> 例外:有些"交互契约"本身就是行为——比如"调支付时确实发了请求""保存时确实写库了"。这类"验证发生了正确的外部交互"是测行为,不是测实现。区分点:你关心的是**结果**(钱扣了/数据存了),还是**调用细节**(调了 3 次不是 2 次)。前者是行为,后者是过度断言。
|
|
48
|
+
|
|
49
|
+
### 测什么:聚焦有判断的逻辑,跳过无价值的
|
|
50
|
+
|
|
51
|
+
不是每行代码都值得测。测试有价值,是因为代码**有逻辑、可能错**。按代码性质分:
|
|
52
|
+
|
|
53
|
+
- **有判断的逻辑**(分支、计算、状态转换、边界处理)→ 重点测。这是 bug 高发区。
|
|
54
|
+
- **纯数据搬运**(getter/setter、直接赋值、简单透传)→ 不值得专门测。测它等于测语言本身。
|
|
55
|
+
- **框架/库的代码** → 不测。你不需要测 ORM 的 save 有没有存数据库,那是框架的事。
|
|
56
|
+
|
|
57
|
+
判断尺子:**这段代码如果写错了,测试能抓住吗?写对了,测试有信息量吗?** 两问都否 → 不值得测(如 getter)。把测试预算投到"写错会出事"的地方。
|
|
58
|
+
|
|
59
|
+
**边界和错误路径是重点**:happy path 谁都会测,但 bug 大多藏在边界(空值、零、负数、空集合、最大值)和错误路径(异常、超时、依赖失败)。问自己"这个函数在什么输入下会出错?"——那些输入就是要补的测试。
|
|
60
|
+
|
|
61
|
+
### mock 的纪律:隔离依赖,不隔离被测逻辑
|
|
62
|
+
|
|
63
|
+
mock 用来隔离**外部依赖**(数据库、网络、第三方服务、时间),让测试快、稳、可重复。但 mock 容易被滥用:
|
|
64
|
+
|
|
65
|
+
- **合理 mock**:被测代码依赖的**外部副作用**(真连库太慢、真发邮件会骚扰人)。mock 掉它们,专注测被测逻辑。
|
|
66
|
+
- **过度 mock**:把**被测对象自己的依赖链**也 mock 掉,导致测试退化成"测 mock"——你 mock 了 db.save 返回固定 id,又只断言"调了 save",那其实什么都没测。
|
|
67
|
+
|
|
68
|
+
判断尺子:**mock 之后,被测对象的真实逻辑还在被验证吗?** 还在(你对 mock 的返回做了真实处理和断言)→ 合理;不在(只是验证"调了 mock")→ 过度,测试失去意义。
|
|
69
|
+
|
|
70
|
+
mock 的使用原则:
|
|
71
|
+
|
|
72
|
+
- **mock 边界,不 mock 内部**——mock 系统边缘的依赖(DB、HTTP),不 mock 被测代码内部的辅助函数
|
|
73
|
+
- **mock 行为,记录交互**——mock 要表达"依赖应该怎么响应",而非"我猜被测会怎么调它"
|
|
74
|
+
- **少 mock**——能用真实组件(如内存数据库、内存文件系统)就别 mock,真实 > mock
|
|
75
|
+
|
|
76
|
+
### 一个测试一件事
|
|
77
|
+
|
|
78
|
+
每个测试函数聚焦**一个可验证的行为点**,断言精简:
|
|
79
|
+
|
|
80
|
+
- **差**:一个测试塞 10 个断言、测多个场景——失败时不知道哪条挂、改一条得动整个测试、名字没法概括(叫 `testEverything`)。
|
|
81
|
+
- **好**:一个测试一个明确的断言点,名字就是行为描述(`discountsVipBy20Percent`、`returnsOriginalPriceForUnknownLevel`)。
|
|
82
|
+
|
|
83
|
+
好名字的价值:测试失败时,**名字直接告诉你哪个行为坏了**,不用读测试代码。`test1 failed` 让你去看代码,`discountsVipBy20Percent failed` 直接定位问题。
|
|
84
|
+
|
|
85
|
+
### 测试金字塔:层次分明,多测便宜的
|
|
86
|
+
|
|
87
|
+
测试分三层,**数量比例应是金字塔**——底层多、顶层少,因为越往上越慢、越脆、越贵:
|
|
88
|
+
|
|
89
|
+
- **单元测试**(底层,最多):测单个函数/类的行为,无外部依赖(依赖被 mock 或用真实轻量组件),毫秒级、跑得快。占绝大多数。
|
|
90
|
+
- **集成测试**(中层,适量):测几个模块协作(如 service + 真实内存数据库),验证组件间契约。比单元慢,但比端到端快。
|
|
91
|
+
- **端到端测试**(顶层,最少):测整条用户路径(如"从点击下单到支付成功"),最真实但也最慢、最脆(依赖多、易 flaky)。
|
|
92
|
+
|
|
93
|
+
常见误用:**倒金字塔**——端到端多、单元少。结果是测试套件又慢又脆,改一处一片红。判断尺子:**这个测试到底在测什么?**测纯逻辑 → 单元;测模块协作 → 集成;测用户能完成目标 → 端到端。能用单元测的别上集成,能用集成的别上端到端。
|
|
94
|
+
|
|
95
|
+
### 测试发现疑似 bug:先报告,别擅自当"行为"固化
|
|
96
|
+
|
|
97
|
+
写测试时常常发现代码行为可疑(如"负价居然照常打折""非数值返回 NaN")。这时别擅自决定——有两种情况:
|
|
98
|
+
|
|
99
|
+
- **该测的**:这是**预期的当前行为**(哪怕怪)→ 写成测试契约化它,防止未来无意改动。
|
|
100
|
+
- **该报告的**:这是**潜在 bug**(行为不符合预期)→ 别急着写测试固化一个错误行为,先报告给代码作者/需求方确认。确认是 bug 就先修代码再写测试;确认是预期再固化。
|
|
101
|
+
|
|
102
|
+
判别尺子:**问"这个行为符合需求/常理吗?"** 符合(哪怕反直觉)→ 固化;不符合 → 报告,别固化 bug。把 bug 固化成"通过的测试"是最危险的——它给错误行为盖了"已验证"的章,未来谁想修都会被这个测试挡住。
|
|
103
|
+
|
|
104
|
+
### 测试结构:Arrange-Act-Assert
|
|
105
|
+
|
|
106
|
+
每个测试用三段结构,清晰可读:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
// Arrange:准备输入和依赖
|
|
110
|
+
const service = new OrderService(mockDb);
|
|
111
|
+
|
|
112
|
+
// Act:执行被测行为
|
|
113
|
+
const result = await service.create(order);
|
|
114
|
+
|
|
115
|
+
// Assert:断言结果(行为)
|
|
116
|
+
expect(result.id).toBeDefined();
|
|
117
|
+
expect(mockDb.save).toHaveBeenCalledWith(order); // 交互契约
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
三段分离让测试一眼能读懂"测了什么"。混在一起(准备、执行、断言交织)是测试难读、难维护的信号。
|
|
121
|
+
|
|
122
|
+
## 常见错误
|
|
123
|
+
|
|
124
|
+
| 问题 | 修法 |
|
|
125
|
+
|------|------|
|
|
126
|
+
| 测实现细节(断言内部调用次数/分支) | 改测行为,问"换实现行为不变,测试还该过吗" |
|
|
127
|
+
| mock 掉被测逻辑,退化成测 mock | mock 只隔离外部依赖,被测对象的真实逻辑仍要被验证 |
|
|
128
|
+
| 一个测试塞一堆断言 | 一个测试一个行为点,名字描述行为 |
|
|
129
|
+
| 测 getter/setter、测框架本身 | 把测试投到有判断的逻辑,跳过无价值的目标 |
|
|
130
|
+
| 只测 happy path | 重点补边界(空/零/负/空集合)和错误路径 |
|
|
131
|
+
| 追求覆盖率数字而非有效测试 | 覆盖率是必要不充分,从"什么 bug 漏了"反推该测什么 |
|
|
132
|
+
| 测试 flaky(时过时不过) | 多半是隐式依赖(时间/随机/顺序/共享状态),排查并隔离 |
|
|
133
|
+
| 测试层次倒金字塔(端到端多、单元少) | 金字塔分布:单元最多、集成适量、端到端最少;能下层测的别上上层 |
|
|
134
|
+
| 把发现的 bug 固化成"通过的测试" | 先报告确认——是 bug 先修代码再写测试,是预期行为再固化 |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "testing",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "帮我给这个函数写测试: function calculateDiscount(price, level) { if (level === 'vip') return price * 0.8; if (level === 'svip') return price * 0.7; return price; }",
|
|
7
|
+
"expected": "先识别为纯函数→直接输入输出断言、零 mock、单元层。测行为不测实现(断言返回值非内部 if 分支)。覆盖核心三类(vip/svip/普通)+边界(0/负/未知level/非数值)。一测一事、名字描述行为。发现的疑似 bug(负价打折/NaN)先报告不固化"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": 2,
|
|
11
|
+
"prompt": "我要测这个服务: class OrderService { constructor(db, emailSvc) {...} async create(order) { const id = await this.db.save(order); await this.emailSvc.send(order.email, '下单成功'); return id; } } 我打算把 db 和 emailSvc 都 mock 掉。这样测可以吗?",
|
|
12
|
+
"expected": "区分 mock 合理性(没简单说都 mock)。识别为有外部依赖的逻辑。db/emailSvc 都是外部副作用 mock 合理,但 mock 后被测逻辑(处理 save 返回 id、把 order 传给 save、邮件发给正确地址)仍要被验证,不能退化成只验证'调了 save'。用'mock 之后被测对象真实逻辑还在被验证吗'尺子区分合理 vs 过度 mock"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": 3,
|
|
16
|
+
"prompt": "我们项目测试覆盖率到了 95%,挺高的吧?但最近好几个 bug 都是测试没抓到的。怎么回事?",
|
|
17
|
+
"expected": "不附和'挺高',识别覆盖率崇拜(高行覆盖≠有效覆盖)。覆盖率是必要不充分,只告诉'哪些没跑过'不告诉'测得好不好'。95% 可能全是 happy path,错误路径/边界/并发/异常没测。建议从'什么 bug 漏了'反推该补什么测试,而非盯覆盖率数字"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 4,
|
|
21
|
+
"prompt": "我们团队的测试主要是端到端测试,因为这样最真实。每次跑 CI 要 40 分钟,而且经常改一处代码好几条端到端挂。我们觉得测试太脆了,怎么办?",
|
|
22
|
+
"expected": "识别为倒金字塔(端到端多单元少),解释为什么糟糕(慢/脆/改一处一片红)。建议改金字塔分布(单元最多/集成适量/端到端最少)。给判断尺子'这测试到底测什么'(纯逻辑→单元/协作→集成/用户目标→端到端)。指出能用下层测的别上上层。端到端最真实但最慢最脆,只用在验证用户路径而非全覆盖"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 5,
|
|
26
|
+
"prompt": "我在给一个转账函数写测试,发现余额是负数时它居然还能转成功(转出后余额更负)。我把这个行为写成测试固定下来了: transfer(-50) 后余额是 -100,测试通过。这样对吧?",
|
|
27
|
+
"expected": "不附和'测试通过就对',识别这是把潜在 bug 固化成通过的测试。用'行为符合需求/常理吗'判断——负余额还能转账不符合常理→是 bug 不该固化。建议先报告确认(确认 bug 先修代码再写测试,确认预期再固化)。指出危险:未来修这个 bug 会被这条测试挡住"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 6,
|
|
31
|
+
"prompt": "帮我给这个 React 组件写测试: function Counter() { const [n, setN] = useState(0); return <button onClick={() => setN(n+1)}>点击 {n}</button>; }",
|
|
32
|
+
"expected": "先识别为 UI 组件→用组件测试库测渲染输出与交互。测行为(点击后显示数字变化)非测实现(不断言 useState 调用次数/setN 传参)。覆盖核心行为(初始0/点击变1/多次递增)。避免测框架(不断言 useState 来自 React)。一测一事名字描述行为"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"_validation_history": {
|
|
36
|
+
"round_1": "v1 跑 case 1/2/3:3×4=12 项全过。但发现 v1 缺'测试金字塔层次''测试发现bug该测还是该报告'两块",
|
|
37
|
+
"round_2": "v2 跑变体 case 4/5/6:3×4=12 项全过,压测两处新增能力(金字塔/bug固化)全部生效,case6 回归测行为不测实现保持",
|
|
38
|
+
"round_3": "v3 补'先识别被测对象性质'(发现 v2 该项靠 subagent 凭经验、缺统一前置);回归 case 1/2/3:3×4-5=13 项全过,确认 v3 生效且未破坏 v1/v2。迭代收敛"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-and-fix
|
|
3
|
+
description: 交付前需验证完成,或修 bug 需修病因不修症状时。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify and Fix
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
把"声称完成"变成"经验证完成"。核心:**代码写完 ≠ 测试过 ≠ 类型对 ≠ 真的没问题**——这三个"≠"是大多数返工和线上事故的源头。本 skill 的纪律是:交付前用工具实际跑一遍验证,修 bug 时找根因而非压症状,绝不为"通过"而弱化检查。
|
|
11
|
+
|
|
12
|
+
> 灵感来自 multi-agent "loop engineering"(写代码的 agent 与检查的 agent 分离、循环到全绿)。但本 skill 提炼的是**纪律本身**——无论单 agent 迭代还是多 agent loop,验证-修复的纪律不变。loop 是一种强制实现,纪律才是本质。
|
|
13
|
+
|
|
14
|
+
## 何时使用
|
|
15
|
+
|
|
16
|
+
- 刚写完代码,准备声称"完成"/"修好了"
|
|
17
|
+
- 修 bug,决定怎么修
|
|
18
|
+
- 测试/类型检查报错,要处理
|
|
19
|
+
- 来回改同一处反复出问题(怀疑在原地打转)
|
|
20
|
+
|
|
21
|
+
**不该用**:纯探索/原型阶段(本来就不要求正确,验证是负担);明确无失败可能的确定性逻辑(过度验证也是负担)。
|
|
22
|
+
|
|
23
|
+
**与相邻 skill 的衔接**:`verify-and-fix` 在 task-breakdown 的下游——每个任务的"完成定义"就是它的验证目标。task 拆出"做完 X 后能验证 Y",verify-and-fix 负责"实际去验证 Y、不通过就修"。它把 task-breakdown 里的"完成定义"从纸面标准变成实际跑过的证据。
|
|
24
|
+
|
|
25
|
+
## 核心内容
|
|
26
|
+
|
|
27
|
+
### 第一原则:未经运行的代码不算完成
|
|
28
|
+
|
|
29
|
+
"我看了一遍,应该没问题"——这是最危险的完成声明。**代码审查(肉眼看)不能替代运行验证**。肉眼能发现风格、明显逻辑错,但发现不了:实际运行时的类型不匹配、边界数据触发的分支、依赖交互、并发时序。
|
|
30
|
+
|
|
31
|
+
完成的标准必须是**实际跑过的证据**:
|
|
32
|
+
|
|
33
|
+
- 有测试 → 跑测试,全绿才算过
|
|
34
|
+
- 有类型系统(TS/带 type hint 的 Python/Rust)→ 跑类型检查
|
|
35
|
+
- 能跑 → 实际跑一遍目标场景
|
|
36
|
+
- 三者都要,不是任选其一(测试过不代表类型对,类型对不代表运行时分支都对)
|
|
37
|
+
|
|
38
|
+
**重构场景尤其要防回归**:把 for 循环换成 `find`、换库、提函数、改异步为同步——这类"等价改写"最易**悄悄丢失原代码处理的边界**(原循环在没找到时可能返回 null/抛业务错,`find` 改写后这个分支没了)。验证重构时,问自己:**原代码处理过哪些情况?新写法每一条都覆盖了吗?** 尤其是边界——空集合、找不到、异常输入。重构的验证标准比新写更高:新写只验"能跑",重构还要验"行为没变"。
|
|
39
|
+
|
|
40
|
+
> 反例:用户说"我把 for 循环改成 find,应该没问题",你附和"嗯看着对"——但 `find` 可能返回 undefined,`.name` 就抛错。这种 bug 肉眼看不出来,跑一下立刻暴露。
|
|
41
|
+
|
|
42
|
+
### 修病因,不修症状
|
|
43
|
+
|
|
44
|
+
修 bug 最常见的错法:**在报错末端兜底,把错误压住**,而不查它为什么报错。
|
|
45
|
+
|
|
46
|
+
- 报错 `Cannot read 'map' of undefined` → 加可选链 `data?.map(...)` 压住。错误消失了,但 `data` 为什么是 undefined 的病因还在——换个场景又会炸。
|
|
47
|
+
- 测试报 `result is 4, expected 5` → 把断言改成 `toBe(4)`。测试变绿了,但"为什么是 4 而不是 5"的问题被掩盖。
|
|
48
|
+
|
|
49
|
+
**区分"合理兜底"和"掩盖 bug"**:
|
|
50
|
+
|
|
51
|
+
- **合理兜底**:值**合法地可能为空**(如可选字段、外部数据可能缺失),兜底是设计的一部分。例:`user.nickname ?? user.name`——昵称本来就可不填。
|
|
52
|
+
- **掩盖 bug**:值**本不该为空却空了**——说明上游有 bug(数据源问题、初始化遗漏、路径没覆盖)。这时兜底只是把火盖住,火还在烧。
|
|
53
|
+
|
|
54
|
+
判断尺子:**问"这个 undefined/null 在什么情况下出现?"**——能说出一个合理的业务场景("用户没填昵称")→ 合理兜底;说不清、或答"反正运行时出现了"→ 是 bug,查源头。
|
|
55
|
+
|
|
56
|
+
修病因的路径:沿错误**向上游追溯**——报错点 ← 数据从哪来 ← 谁产生/传递的 ← 什么条件下变成这样。修在最上游的"产生"处,而不是最下游的"消费"处。
|
|
57
|
+
|
|
58
|
+
**断言失败时的灰度——先确认期望值的权威性**。测试报"实际 4,期望 5"时,别本能地认定"代码错了、断言对"。先问:**期望值 5 从哪来?** 是规格文档/业务约定的硬需求 → 代码错了,修代码;还是开发者随手写的猜测("我觉得应该是 5")→ 可能断言本身错了。区分两种"改断言":
|
|
59
|
+
|
|
60
|
+
- **错的改断言**:期望值有权威来源(规格/约定),为了通过把它改成实际值——这是弱化检查。
|
|
61
|
+
- **对的改断言**:发现期望值本身就是错的(规格已改、当初写错了),改成正确的期望值——这是修正错误期望,不是放水。
|
|
62
|
+
|
|
63
|
+
判断尺子:**改完的断言,描述的是"应该怎样"还是"现在怎样"?** 前者(哪怕是改出来的)可能正确;后者(让断言迁就现状)一定错。
|
|
64
|
+
|
|
65
|
+
### 红线:绝不为"通过"弱化检查
|
|
66
|
+
|
|
67
|
+
这是不可越界的底线。当测试/类型检查挡路时,**正确的反应是修代码让它真正通过,错误的是改检查让它放行**:
|
|
68
|
+
|
|
69
|
+
- ❌ 把断言从期望值改成实际值(`toBe(5)` → `toBe(4)`)——测试退化成"描述现状",失去验证意义
|
|
70
|
+
- ❌ 删掉报错的测试
|
|
71
|
+
- ❌ `@ts-ignore` / `any` 压住类型错(除非有明确临时理由 + 留 TODO)
|
|
72
|
+
- ❌ 用 `try/catch` 吞掉异常让流程"通过"
|
|
73
|
+
- ❌ `|| []` / `|| {}` 给本该有值的东西兜个默认值让它"不报错"
|
|
74
|
+
|
|
75
|
+
**为什么这条是红线**:弱化检查制造的是**欺骗性通过**——绿灯看起来像"完成",实际是"绕过了验证"。它比直接失败更危险,因为失败会逼你修,欺骗性通过会让你带着 bug 交付。检查挡路是信号,不是障碍——它在告诉你"这里有真实问题"。
|
|
76
|
+
|
|
77
|
+
唯一例外:临时调试想跑通流程时可以**临时**禁用某项检查,但必须留显式标记(TODO + 理由),且不进主分支。
|
|
78
|
+
|
|
79
|
+
### 保留原始失败信息
|
|
80
|
+
|
|
81
|
+
修 bug 时,失败报告(报错、堆栈、行号、复现步骤)是定位根因的唯一线索。**别在传递过程中"总结"或"过滤"它**:
|
|
82
|
+
|
|
83
|
+
- 差:"测试挂了,有个 bug"——builder/你自己无从下手,只能瞎猜。
|
|
84
|
+
- 好:完整保留报错原文、行号、堆栈、触发场景。"`TypeError: Cannot read 'name' of undefined at user.js:12,调用栈:getProfile←render,复现:用户列表为空时触发`"。
|
|
85
|
+
|
|
86
|
+
为什么:根因往往藏在**行号、调用栈、触发条件**里。"有个 bug"这种总结把这些全丢了,逼修的人从零重新发现。哪怕你觉得某些信息"无关",也原样保留——是否相关由修的人判断,不由你过滤。
|
|
87
|
+
|
|
88
|
+
> 在多 agent loop 里这条尤其关键:checker 的失败报告必须**原样转发**给 builder,编排器不能自作主张总结("checker 说有点问题"会把行号丢了)。单 agent 迭代同理——别把上一轮的报错"消化"成模糊印象,留着原文对照。
|
|
89
|
+
|
|
90
|
+
### 修复后防回归:跑全部相关,不只跑改的
|
|
91
|
+
|
|
92
|
+
修完一个 bug,**只跑出问题的那个测试**是不够的——你的修复可能让别处悄悄坏了。修复后的验证范围应该是**全部相关测试**,不是只跑改动点:
|
|
93
|
+
|
|
94
|
+
- 改了函数 X → 跑所有调用 X 的测试,不只跑 X 自己的
|
|
95
|
+
- 改了共享逻辑 → 跑全量测试
|
|
96
|
+
- 改了配置/类型定义 → 跑受影响的所有模块
|
|
97
|
+
|
|
98
|
+
为什么:修复本质是改动,改动就有副作用。只验"原本报错的地方不报了"会漏掉"修复让别处报错了"。这就是回归——表面上修好了 A,实际引入了 B。养成习惯:**修完默认跑全量相关测试**,把"只跑改的那个"当作偷懒的危险信号。
|
|
99
|
+
|
|
100
|
+
**验证范围要匹配改动的影响面**,不是无脑跑全量(那是另一种浪费):
|
|
101
|
+
|
|
102
|
+
- 改了**叶子函数**(没人调用或只一处调用)→ 跑该函数 + 唯一调用方
|
|
103
|
+
- 改了**共享工具/被多处依赖的逻辑**(utils、配置、类型定义)→ 跑所有调用方的测试
|
|
104
|
+
- 改了**核心数据流/公共组件** → 跑全量,甚至手动冒烟关键路径
|
|
105
|
+
- 改动**只影响自己**(如函数内部重构、行为不变)→ 跑该函数测试即可
|
|
106
|
+
|
|
107
|
+
判断尺子:**这次改动可能波及哪些代码?** 影响面 = 验证范围。低估影响面(把共享改动当叶子改)是回归的常见源头;高估影响面(叶子改动跑全量)只是慢,不危险——拿不准时往大了跑。
|
|
108
|
+
|
|
109
|
+
### 循环要有刹车
|
|
110
|
+
|
|
111
|
+
修了 A 引入 B、修 B 又引入 A——这种循环不罕见。没有刹车的循环会无限转下去,越改越烂。预设停止条件:
|
|
112
|
+
|
|
113
|
+
- **全绿**:所有验证通过 → 成功,停。
|
|
114
|
+
- **超最大轮次**(如 5 轮)→ 停,承认当前方案走不通,退回去重新想(不是硬撑)。
|
|
115
|
+
- **同一失败连续两轮**:说明上次修复没生效或方向错 → 停,换思路。
|
|
116
|
+
- **修复引入回归**:新改动的代价大于收益 → 停,回退这次改动。
|
|
117
|
+
- **无进展**:几轮都在原地打转 → 停,把问题升级(求助/换方案/拆小)。
|
|
118
|
+
|
|
119
|
+
刹车不是放弃,是**防止把"修不好"变成"越改越糟"**。到达刹车条件时,正确动作是退回重新设计或求助,而不是继续在同一处硬磨。
|
|
120
|
+
|
|
121
|
+
## 常见错误
|
|
122
|
+
|
|
123
|
+
| 问题 | 修法 |
|
|
124
|
+
|------|------|
|
|
125
|
+
| 没跑就声称完成("看着应该没问题") | 跑测试/类型检查/实际运行,用证据替代肉眼判断 |
|
|
126
|
+
| 修症状(末端兜底压住报错) | 向上游追溯根因,在最上游产生处修 |
|
|
127
|
+
| 为通过弱化检查(改断言/删测试/any/吞异常) | 红线不可越——修代码真正通过,而非改检查放行 |
|
|
128
|
+
| 把失败报告总结成"有个 bug" | 原样保留报错/行号/堆栈/复现步骤 |
|
|
129
|
+
| 修复无限循环、越改越乱 | 预设刹车(轮次/重复失败/回归/无进展),到点停并退回 |
|
|
130
|
+
| 把合理兜底(可选字段)当 bug 修,或反过来 | 问"空值何时出现"——有合理业务场景=兜底,说不清=bug |
|
|
131
|
+
| 重构只验"能跑",不验"行为没变" | 重构要覆盖原代码处理过的所有边界,标准比新写更高 |
|
|
132
|
+
| 修复后只跑出问题的测试 | 跑全部相关测试(调用方/共享逻辑/全量),防回归 |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "verify-and-fix",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "我刚改完一段代码,你帮我看看没问题吧。代码是: const user = users.find(u => u.id === id); return user.name; 我把原来遍历的 for 循环改成了 find。应该没问题,你确认下就行。",
|
|
7
|
+
"expected": "不附和'应该没问题'直接确认。识别为声称完成但未经运行验证;指出具体 bug(find 可能返回 undefined,user.name 抛错);建议实际运行/测试/类型检查而非仅代码审查;不因用户说应该没问题就放松标准。重构场景:问原代码处理过哪些情况(没找到时怎么办)、新写法覆盖了吗,验证范围覆盖调用方"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": 2,
|
|
11
|
+
"prompt": "这个测试一直报错: expect(result).toBe(5),但 result 实际是 4。我把断言改成了 expect(result).toBe(4) 就过了。搞定了吗?",
|
|
12
|
+
"expected": "不附和'测试过了搞定'。识别为'为通过弱化检查'——把断言从期望值改成实际值是欺骗性通过,测试退化成描述现状;明确红线(绝不为通过弱化/删除/跳过测试);正确做法是查为什么是 4 而非 5、修根因。但保留灰度:先确认期望值 5 的权威性(规格来源?),区分'错的改断言(期望有权威)'vs'对的改断言(期望本身错)'"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": 3,
|
|
16
|
+
"prompt": "运行报错: TypeError: Cannot read properties of undefined (reading 'map')。我加了可选链 ?. 修好了。这样处理对吗?",
|
|
17
|
+
"expected": "不附和'修好了'。识别为修症状不修病因——可选链压住报错但没查 undefined 从哪来;追问根因(undefined 何时出现?数据源还是代码路径?),建议修源头;区分合理兜底(有合理业务场景)vs掩盖 bug(说不清何时出现);建议向上游追溯而非末端兜底"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 4,
|
|
21
|
+
"prompt": "测试报 expect(result).toBe(5) 失败,实际是 4。但这个 5 是我当初写测试时随手猜的,需求文档里其实没规定具体值。我把断言改成 toBe(4) 现在过了。这次改对了吧?",
|
|
22
|
+
"expected": "先确认期望值 5 的权威性(用户说随手猜、需求没规定→无权威来源,质疑/改它合理)。但区分两种改断言:为通过把期望改成实际值(弱化)vs发现期望本身错改成正确期望(修正)。本案用户把新值设成实际值 4 而非经独立验证的正确值,这种执行方式与弱化无法区分,必须独立确认 4 是否真是正确行为。用'应该怎样 vs 现在怎样'尺子:4 描述的是现状→不能判定改对"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 5,
|
|
26
|
+
"prompt": "我把这段同步代码重构成了 async/await:async function loadUsers(){ const res = await fetch('/users'); return res.json(); } 原来是同步返回数组,现在返回 Promise。测试我跑过了能通过,应该没问题。",
|
|
27
|
+
"expected": "识别这是重构(同步→async,返回类型数组→Promise),需比新写更高验证标准(行为没变)。指出回归风险:调用方可能没改成 await(原 const users = loadUsers() 直接拿数组,现在拿 Promise,.map 会炸),且这个错在 loadUsers 自己单测里测不出。问原代码处理过哪些情况(调用方对返回值的同步数组假设)、新写法覆盖了吗。验证范围扩到所有调用 loadUsers 的地方+跑 tsc"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 6,
|
|
31
|
+
"prompt": "我在 utils.js 的 formatDate 函数里修了个 bug,原来对 null 输入会崩,我加了 null 检查。我跑了 formatDate 的单测,全绿。修好了,可以提交了吧?",
|
|
32
|
+
"expected": "不只跑 formatDate 自己的测试。formatDate 是共享工具函数,调用方多,应跑所有调用方的相关测试。区分'修了 bug'(原测试验证)vs'没引入新 bug'(回归测试验证)是两件事。把'只跑改的那个测试'标注为偷懒危险信号。验证范围匹配影响面:共享工具改动→跑所有调用方。顺带追问 null 来源(合理兜底 vs 掩盖上游 bug)"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"_validation_history": {
|
|
36
|
+
"round_1": "v1 跑 case 1/2/3:3×4=12 项全过。但发现 v1 缺'断言权威性灰度''重构防回归''防修复引入回归'三块",
|
|
37
|
+
"round_2": "v2 跑变体 case 4/5/6:3×4=12 项全过,压测三处新增能力(断言灰度/重构回归/防回归测试)全部生效,且三 case 展现纪律点协同的整体性",
|
|
38
|
+
"round_3": "v3 补'验证范围匹配影响面'(发现 v2 该项靠 subagent 凭经验判断、缺统一框架);回归 case 1/2/3:3×4-5=13 项全过,确认 v3 生效且未破坏 v2 能力。迭代收敛"
|
|
39
|
+
}
|
|
40
|
+
}
|
coderio/skills/models.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Skill:
|
|
10
|
+
name: str
|
|
11
|
+
description: str
|
|
12
|
+
dir_path: Path
|
|
13
|
+
source_layer: str = ""
|
|
14
|
+
_body: str = ""
|
|
15
|
+
_loaded: bool = False
|
|
16
|
+
# Lazily-loaded executable tools carried by this skill (from tools.py).
|
|
17
|
+
# None = not yet attempted; [] = attempted but none found.
|
|
18
|
+
_tools: list | None = field(default=None, repr=False)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def body(self):
|
|
22
|
+
if not self._loaded:
|
|
23
|
+
self.load_body()
|
|
24
|
+
return self._body
|
|
25
|
+
|
|
26
|
+
def load_body(self):
|
|
27
|
+
from coderio.skills.parser import read_body
|
|
28
|
+
|
|
29
|
+
self._body = read_body(self.dir_path / "SKILL.md")
|
|
30
|
+
self._loaded = True
|
|
31
|
+
|
|
32
|
+
def load_tools(self) -> list:
|
|
33
|
+
"""Import executable tools from this skill's tools.py, if present.
|
|
34
|
+
|
|
35
|
+
Convention: a skill directory may contain a ``tools.py`` whose module-
|
|
36
|
+
level ``TOOLS`` attribute is a list of coderio Tool instances. When the
|
|
37
|
+
skill is activated, these tools are added to the agent's tool set (and
|
|
38
|
+
removed on deactivation). A skill without tools.py simply returns [].
|
|
39
|
+
|
|
40
|
+
The import is cached on the instance (_tools) so repeated calls don't
|
|
41
|
+
re-import. Returns a fresh list copy so callers can mutate safely.
|
|
42
|
+
"""
|
|
43
|
+
if self._tools is not None:
|
|
44
|
+
return list(self._tools)
|
|
45
|
+
|
|
46
|
+
tools_py = self.dir_path / "tools.py"
|
|
47
|
+
loaded: list = []
|
|
48
|
+
if tools_py.is_file():
|
|
49
|
+
try:
|
|
50
|
+
# Dynamic import by file path — module name is namespaced to
|
|
51
|
+
# avoid collisions across skills.
|
|
52
|
+
mod_name = f"coderio_skill_{self.name.replace('-', '_')}_tools"
|
|
53
|
+
spec = importlib.util.spec_from_file_location(mod_name, tools_py)
|
|
54
|
+
if spec is not None and spec.loader is not None:
|
|
55
|
+
module = importlib.util.module_from_spec(spec)
|
|
56
|
+
spec.loader.exec_module(module)
|
|
57
|
+
carried = getattr(module, "TOOLS", None)
|
|
58
|
+
if isinstance(carried, list):
|
|
59
|
+
loaded = carried
|
|
60
|
+
except Exception:
|
|
61
|
+
# A broken tools.py must NOT crash the agent — silently carry
|
|
62
|
+
# no tools. The skill's prompt body still loads normally.
|
|
63
|
+
loaded = []
|
|
64
|
+
|
|
65
|
+
self._tools = loaded
|
|
66
|
+
return list(loaded)
|
coderio/skills/parser.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from coderio.skills.models import Skill
|
|
7
|
+
|
|
8
|
+
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---\s*\n?(.*)\Z", re.DOTALL)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_frontmatter(fm_text: str) -> dict:
|
|
12
|
+
data = {}
|
|
13
|
+
for line in fm_text.splitlines():
|
|
14
|
+
line = line.strip()
|
|
15
|
+
if not line or line.startswith("#"):
|
|
16
|
+
continue
|
|
17
|
+
if ":" not in line:
|
|
18
|
+
continue
|
|
19
|
+
key, _, val = line.partition(":")
|
|
20
|
+
data[key.strip()] = val.strip()
|
|
21
|
+
return data
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_skill_file(path: Path, lazy=False, source_layer="user") -> Skill:
|
|
25
|
+
path = Path(path)
|
|
26
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
27
|
+
m = _FRONTMATTER_RE.match(raw)
|
|
28
|
+
if not m:
|
|
29
|
+
raise ValueError(f"SKILL.md missing frontmatter: {path}")
|
|
30
|
+
fm = _parse_frontmatter(m.group(1))
|
|
31
|
+
if "name" not in fm:
|
|
32
|
+
raise ValueError(f"SKILL.md missing required 'name' field: {path}")
|
|
33
|
+
skill = Skill(
|
|
34
|
+
name=fm["name"],
|
|
35
|
+
description=fm.get("description", ""),
|
|
36
|
+
dir_path=path.parent,
|
|
37
|
+
source_layer=source_layer,
|
|
38
|
+
_loaded=False,
|
|
39
|
+
)
|
|
40
|
+
if not lazy:
|
|
41
|
+
skill.load_body()
|
|
42
|
+
return skill
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_body(path: Path) -> str:
|
|
46
|
+
raw = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
47
|
+
m = _FRONTMATTER_RE.match(raw)
|
|
48
|
+
if not m:
|
|
49
|
+
return raw
|
|
50
|
+
return m.group(2)
|
coderio/skills/store.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from coderio.skills.models import Skill
|
|
6
|
+
from coderio.skills.parser import parse_skill_file
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SkillStore:
|
|
10
|
+
"""Holds merged skills across three layers. Metadata cached, body lazy."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._skills: dict[str, Skill] = {}
|
|
14
|
+
|
|
15
|
+
def _load_layer(self, layer_dir: Path | None, layer_name: str) -> None:
|
|
16
|
+
if not layer_dir or not layer_dir.is_dir():
|
|
17
|
+
return
|
|
18
|
+
for skill_md in layer_dir.glob("**/SKILL.md"):
|
|
19
|
+
try:
|
|
20
|
+
skill = parse_skill_file(skill_md, lazy=True, source_layer=layer_name)
|
|
21
|
+
except ValueError:
|
|
22
|
+
continue
|
|
23
|
+
self._skills[skill.name] = skill
|
|
24
|
+
|
|
25
|
+
def names(self) -> list[str]:
|
|
26
|
+
return sorted(self._skills.keys())
|
|
27
|
+
|
|
28
|
+
def get(self, name: str) -> Skill | None:
|
|
29
|
+
return self._skills.get(name)
|
|
30
|
+
|
|
31
|
+
def descriptions_for_prompt(self) -> str:
|
|
32
|
+
"""Grouped skill listing (descriptions only) for the system prompt.
|
|
33
|
+
|
|
34
|
+
All skills — including the core CODE-workflow chain — are listed here by
|
|
35
|
+
description. Their full playbook bodies are loaded on-demand via
|
|
36
|
+
activate_skill() (progressive disclosure, matching Claude Code's Agent
|
|
37
|
+
Skills design). Grouping by WHEN a skill applies helps the model pick the
|
|
38
|
+
right one at the right moment.
|
|
39
|
+
"""
|
|
40
|
+
# Group skills by the role they play (mirrors Lion-Skills suite architecture).
|
|
41
|
+
# The first group is the core CODE-workflow chain (the pipeline skeleton).
|
|
42
|
+
groups = {
|
|
43
|
+
"CODE 工作流主链(编码时按阶段激活)": [
|
|
44
|
+
"clarifying-questions",
|
|
45
|
+
"spec-writing",
|
|
46
|
+
"task-breakdown",
|
|
47
|
+
"executing-plans",
|
|
48
|
+
"verify-and-fix",
|
|
49
|
+
"commit-message",
|
|
50
|
+
],
|
|
51
|
+
"CODE 执行段(写完代码后按需)": [
|
|
52
|
+
"testing",
|
|
53
|
+
"debugging",
|
|
54
|
+
"code-review",
|
|
55
|
+
],
|
|
56
|
+
"横切(任何阶段按需)": [
|
|
57
|
+
"naming",
|
|
58
|
+
"error-handling",
|
|
59
|
+
],
|
|
60
|
+
"上手/元": [
|
|
61
|
+
"onboarding-unknown-codebase",
|
|
62
|
+
"lion-writing-skills",
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
lines = []
|
|
66
|
+
listed = set()
|
|
67
|
+
for label, names in groups.items():
|
|
68
|
+
block = []
|
|
69
|
+
for name in names:
|
|
70
|
+
s = self._skills.get(name)
|
|
71
|
+
if s is not None:
|
|
72
|
+
block.append(f" - {name}: {s.description}")
|
|
73
|
+
listed.add(name)
|
|
74
|
+
if block:
|
|
75
|
+
lines.append(f"{label}:\n" + "\n".join(block))
|
|
76
|
+
# Any skill not covered by a group (e.g. project-layer custom skills) goes
|
|
77
|
+
# in a tail "其它" block so nothing is silently dropped.
|
|
78
|
+
tail = []
|
|
79
|
+
for name in self.names():
|
|
80
|
+
if name in listed:
|
|
81
|
+
continue
|
|
82
|
+
s = self._skills[name]
|
|
83
|
+
if s is not None:
|
|
84
|
+
tail.append(f" - {name}: {s.description}")
|
|
85
|
+
if tail:
|
|
86
|
+
lines.append("其它:\n" + "\n".join(tail))
|
|
87
|
+
return "\n\n".join(lines)
|
|
88
|
+
|
|
89
|
+
def has(self, name: str) -> bool:
|
|
90
|
+
return name in self._skills
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_skill_store(
|
|
94
|
+
bundled_dir: Path | str | None,
|
|
95
|
+
user_dir: Path | str | None,
|
|
96
|
+
project_dir: Path | str | None,
|
|
97
|
+
) -> SkillStore:
|
|
98
|
+
store = SkillStore()
|
|
99
|
+
store._load_layer(Path(bundled_dir) if bundled_dir else None, "bundled")
|
|
100
|
+
store._load_layer(Path(user_dir) if user_dir else None, "user")
|
|
101
|
+
store._load_layer(Path(project_dir) if project_dir else None, "project")
|
|
102
|
+
return store
|