@godv61/dsh-task-engine 0.23.8 → 0.24.0

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.
package/defaults/eng.json CHANGED
@@ -1,10 +1,49 @@
1
1
  {
2
2
  "flow": "standard",
3
3
  "stage_bindings": {
4
- "需求评审": { "skills": ["requirement-analysis"], "rules": ["security-redlines"] },
5
- "设计": { "skills": ["solution-design"] },
6
- "开发": { "skills": ["code-implement"], "rules": ["coding-conventions"] },
7
- "交付": { "skills": ["code-verify"], "rules": ["coding-conventions"] },
8
- "代码审核": { "skills": ["code-review", "code-commit"], "rules": ["security-redlines", "commit-conventions"] }
4
+ "需求评审": {
5
+ "skills": [
6
+ {
7
+ "skill": { "source": "bundled", "name": "requirement-analysis" },
8
+ "rules": [{ "source": "bundled", "name": "security-redlines" }]
9
+ }
10
+ ]
11
+ },
12
+ "设计": {
13
+ "skills": [
14
+ { "skill": { "source": "bundled", "name": "solution-design" }, "rules": [] }
15
+ ]
16
+ },
17
+ "开发": {
18
+ "skills": [
19
+ {
20
+ "skill": { "source": "bundled", "name": "code-implement" },
21
+ "rules": [
22
+ { "source": "bundled", "name": "coding-conventions" },
23
+ { "source": "bundled", "name": "security-redlines" }
24
+ ]
25
+ }
26
+ ]
27
+ },
28
+ "交付": {
29
+ "skills": [
30
+ { "skill": { "source": "bundled", "name": "code-verify" }, "rules": [] }
31
+ ]
32
+ },
33
+ "代码审核": {
34
+ "skills": [
35
+ {
36
+ "skill": { "source": "bundled", "name": "code-review" },
37
+ "rules": [
38
+ { "source": "bundled", "name": "coding-conventions" },
39
+ { "source": "bundled", "name": "security-redlines" }
40
+ ]
41
+ },
42
+ {
43
+ "skill": { "source": "bundled", "name": "code-commit" },
44
+ "rules": [{ "source": "bundled", "name": "commit-conventions" }]
45
+ }
46
+ ]
47
+ }
9
48
  }
10
- }
49
+ }
@@ -0,0 +1,163 @@
1
+ # DSH Task Engine —— 技术简报(供评估与优化讨论)
2
+
3
+ > 用途:把事实交给 GPT 讨论时使用。以下内容均经代码/实测核对,不是印象。
4
+
5
+ ---
6
+
7
+ ## 1. 它是什么
8
+
9
+ DeepSeek Harness(DSH)的插件:给 AI 编码会话加一套**可检查的工程交付流程**。
10
+
11
+ - npm: `@godv61/dsh-task-engine`(`latest` = 0.23.9,MIT)
12
+ - GitHub: https://github.com/godv61/dsh-task-engine(**已被 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 收录**,PR #5681 一次通过)
13
+ - 规模:host 面 **4079 行** TS,client 面 **2173 行** TSX,测试 146 项 P0 断言 + 52 项 `node:test`
14
+
15
+ ### 核心机制
16
+
17
+ **一个工具 `dev_task`**,7 个操作:`status` / `config` / `init` / `create` / `commit` / `install_hook` / `verify_hook`。
18
+
19
+ 它持有任务状态机,**阶段流转是硬门禁**(模型无法绕过):
20
+
21
+ | 守卫 | 含义 |
22
+ | :--- | :--- |
23
+ | `requirement_confirmation` | 需求须经人确认 |
24
+ | `solution_confirmation` | 方案须经人确认 |
25
+ | `artifacts_present` | 阶段产物须齐备 |
26
+ | `todos_done` | 实施项须全部完成 |
27
+ | `verified` | 须有**真实命令回执**(退出码 0,未超时/中止/被沙箱拒) |
28
+ | `review_passed` | 须有审核结论 |
29
+
30
+ **三个内置流程**(阶段顺序与守卫由预设固化):
31
+
32
+ | 流程 | 阶段 |
33
+ | :--- | :--- |
34
+ | `standard` | 需求评审 → 设计 → 开发 → 交付 → 代码审核 → 完成 |
35
+ | `agile` | 需求 → 开发 → 交付 → 审查 |
36
+ | `minimal` | 开发 → 交付 |
37
+
38
+ **渐进式披露**:每个阶段挂「技能 + 规则」,进入该阶段才披露,避免一次性灌入上下文。
39
+
40
+ **工作台 5 个标签页**:项目初始化 / 流程配置 / 任务台账 / 技能 / 规则。
41
+
42
+ ---
43
+
44
+ ## 2. 已知的设计缺口(当前讨论起点)
45
+
46
+ ### 2.1 技能与规则是平级的,但存在**隐式依赖**
47
+
48
+ **数据模型**(`src/engine.ts`):
49
+
50
+ ```ts
51
+ interface StageBinding {
52
+ skills?: string[] // 独立数组
53
+ rules?: string[] // 独立数组
54
+ }
55
+ ```
56
+
57
+ 引擎分别解析:
58
+
59
+ ```ts
60
+ const rules = await resolveRules(binding?.rules ?? [], fs, cwd) // 不经技能
61
+ ```
62
+
63
+ **但技能文本里引用了具体规则**,例如 `code-implement/SKILL.md`:
64
+
65
+ > 代码质量:按内置规则 **coding-conventions** 查「做得好不好」
66
+
67
+ **问题**:这个依赖**只写在散文里,没有结构化**。因此:
68
+
69
+ - 用户在 UI 勾了 `code-implement` 但没勾 `coding-conventions` → 技能执行时引用一个不存在的规则
70
+ - **引擎不校验,UI 不提示**
71
+ - 默认配置碰巧配对,用户改绑定就可能拆散
72
+
73
+ **默认配置印证耦合确实存在**:规则 `coding-conventions` 同时挂在「开发」(`code-implement`)和「交付」(`code-verify`)两个阶段。
74
+
75
+ ### 2.2 待决策的四个方案
76
+
77
+ | 方案 | 做法 | 代价 |
78
+ | :--- | :--- | :--- |
79
+ | **A. 声明式依赖** | `SKILL.md` frontmatter 加 `requires_rules: [...]` | 是 B/C/D 的前置条件;需改 7 个内置技能 |
80
+ | **B. 软提示** | 规则列表全量显示,标注「此技能建议配合 X」 | 保留自由组合;仅提示 |
81
+ | **C. 强联动** | 勾技能 → 规则列表过滤到相关项 | ⚠️ 破坏自由组合(同一规则复用于多技能) |
82
+ | **D. 仅保存时校验** | 不联动 UI,保存时警告「技能 X 引用了未挂载的规则 Y」 | 最轻量;问题暴露较晚 |
83
+
84
+ **当前倾向**:A + D(先让关系可表达,再做校验),或 A + B。**C 需要在 A 的数据之上才成立。**
85
+
86
+ ---
87
+
88
+ ## 3. 其他可能值得评估的方向
89
+
90
+ ### 3.1 流程定制能力的边界
91
+
92
+ **现状**:阶段顺序与守卫由预设固化,**可视化自定义流程不在计划内**(曾评估,结论是不做——把流程设计负担转嫁给使用者)。
93
+
94
+ **可调部分**:项目根 `.dsh/eng.json` 只声明 `flow`(选哪套预设)+ `stage_bindings`(每阶段挂什么)。
95
+
96
+ **待评估**:这个边界是否过窄?例如是否该允许「加一个自定义阶段但保留守卫语义」?
97
+
98
+ ### 3.2 验证与审核的可信度边界
99
+
100
+ **现状**(README 与 FAQ 都声明):
101
+
102
+ > 命令覆盖是否充分、审核是否准确仍需判断,**不能把一个成功退出码当作全部需求已验证**。
103
+
104
+ - 验证需真实回执,但**回执不等于需求被完整覆盖**
105
+ - 审核/实施项结论**由模型记录**,非独立验证
106
+ - 任务 JSON **无签名**,有文件写权限者可同时改内容与摘要
107
+
108
+ **待评估**:对「成熟开源产品」而言,这个边界是否需要更强机制(如独立验收、产物哈希链)?
109
+
110
+ ### 3.3 提交门禁可被绕过
111
+
112
+ **现状**:`hooks/commit-msg` 检查任务、阶段、消息与文件范围。但:
113
+
114
+ > `--no-verify`、替换 `hooksPath` 或直接改本地数据**仍可能绕过**。
115
+
116
+ **待评估**:是否需要与 CI 结合的服务端校验?还是明确声明「个人本机工具,不提供防篡改」即可?
117
+
118
+ ### 3.4 多环境一致性
119
+
120
+ **现状问题**:同一插件需在**桌面版 DSH**(`0.1.2-rc.1`)与**源码版 DSH**(`0.1.7-alpha.1`)上工作,两者契约不同:
121
+
122
+ - persona 字段:桌面版要 `text`,源码版要 `prefix`(已在 0.23.7 通过「跟着运行环境走」解决)
123
+ - 插件管理:桌面版用 generation 机制,源码版用 pnpm
124
+
125
+ **已采取的防护**:CI 加 `dsh-contract` job(跑 `scripts/verify-dsh-compat.mjs`),peer 范围用显式 `||` 分支覆盖各版本。
126
+
127
+ **待评估**:DSH 处于 alpha、**1–3 天一个 tag**,这个跟踪成本是否可持续?
128
+
129
+ ### 3.5 其他
130
+
131
+ - **国际化**:目前中文为主,README 有中英双版。是否需要完整 i18n?
132
+ - **可访问性**:工作台介入了 `aria-label`,但未系统测试
133
+ - **性能**:任务台账在任务多时的表现未测
134
+ - **文档**:本次刚做过一次重组(发布说明归入 `docs/releases/`,索引分「当前/历史」)
135
+
136
+ ---
137
+
138
+ ## 4. 近期已完成的修复(供判断演进方向)
139
+
140
+ | 版本 | 内容 |
141
+ | :--- | :--- |
142
+ | 0.23.9 | 可搜索绑定选择器(独立搜索/仅看已选/计数/就地滚动/预设默认锁定);source map 停止内联源码(447→148 KB);修 10 处 UTF-8 字节损坏 |
143
+ | 0.23.8 | 文档修正(4 处与代码矛盾的陈述)+ 目录重组 |
144
+ | 0.23.7 | persona 字段兼容(新旧 DSH 都能用)——**读源预设用哪个字段就写哪个**,不再硬编码 |
145
+ | 0.23.6 | eng 预设每次启动从运行中 harness 重新派生(此前只在首次生成,DSH 升级后失效) |
146
+
147
+ ---
148
+
149
+ ## 5. 环境依赖(影响可测试性)
150
+
151
+ - **DSH 0.1.7-alpha.1** 曾有 bug 致**所有工具调用失败**(`ctx.tools[TOOL_RUNTIME_SCHEDULER]` 为 `undefined`),社区在 discussions #7035 / #7194 报告,**0.1.7 已修**
152
+ - 源码版用 `pnpm dsh web` 会触发模块双重加载;绕过方式:`node apps/cli/lib/bin.js <profile>`
153
+ - 桌面版插件由 `dshmarket` + generation 机制管理,**不能用 CLI 的 `dsh plugin` 直接操作**(会与它冲突)
154
+
155
+ ---
156
+
157
+ ## 6. 想请对方重点回答的问题
158
+
159
+ 1. **技能↔规则依赖**:A/B/C/D 选哪个?还是有更好的模型(如规则直接属于技能、互不独立)?
160
+ 2. **流程定制边界**:「不做自定义流程」对一个成熟产品是否合理?还是该提供受限的扩展点?
161
+ 3. **可信度声明**:当前「不防篡改」的边界,是明确声明即可,还是需要机制补强?
162
+ 4. **对 DSH alpha 的跟踪成本**:1–3 天一个 tag,插件该怎么定位(追最新?固定版本?)
163
+ 5. **还缺什么才能称得上「成熟开源产品」**:贡献指南、issue 模板、语义化版本、变更日志规范、安全策略、行为准则?
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,78 @@
4
4
 
5
5
  按版本查阅功能变化。当前使用方式以[项目首页](../README.md)和使用指南为准;历史条目中的实现方式、限制与测试数量可能已被后续版本替代。
6
6
 
7
+ ## 0.24.0
8
+
9
+ 针对 2026-09-23 外部深度评测的改造。**这是一个破坏性版本**:配置里的 `stage_bindings[*].rules` 不再作为节点级规则读取,`skills` 的格式也从字符串数组改为带来源的对象。已有配置会继续工作——旧的节点级规则以 `legacy_rules` 保留并**仍然生效**——但它们的归属需要你确认一次,见下面「规则改为只挂在技能下」。
10
+
11
+ 每个问题都先以独立复现确认存在,再修,并做正反两面验证。四个批次按实施顺序列在下面。
12
+
13
+ ### 返工、资源冻结与预设重新定位(第三批·续)
14
+
15
+ - **新增 `revise` 操作:受控返工**。流程图只有正向边,但工作不是——需求会在实现开始后变化,缺陷会在审核时发现。此前唯一的回头方式是手改记录,而那会让每一条下游结论**名义上依然成立**:确认、验证回执、审核结论都描述着一棵已被取代的代码树,却仍读作"通过"。
16
+ - **失效范围按声明的类型推导,不是一刀切**:`requirement` 清掉需求确认及其全部下游;`solution` 保留需求确认,清掉方案确认与实现相关证据;`defect` 保留需求与方案确认,只清掉"旧实现是正确的"这一批证据。**每次返工都清空一切会更简单,也会丢掉仍然成立的结论**,让工作无谓重做。
17
+ - 被清掉审核的实施项**退回未完成状态**——留着 `done` 会让 `todos_done` 在审核刚被作废的工作上通过。
18
+ - 返工历史记入 `revisions`,含类型、原因、从哪个阶段回到哪个阶段、以及本次失效了哪些结论。
19
+ - **任务创建时冻结技能与规则正文**。名字无法让进行中的任务保持稳定:改一条正在使用的规则会**无声改变该任务正在做的事**,删掉它则让一条已配置的约束凭空消失。现在每个解析到的正文都连同一个内容 hash 存入 `flow.resources`,**同一正文按 hash 只存一份**(`security-redlines` 被两个技能引用,正文不重复)。读不到正文的资源会被报为 `unreadable`,而不是假装冻结成功。
20
+ - **预设重新定位为三种任务复杂度**,而不是三种安全底线。名称与描述改为面向任务的表述:**完整研发**(新功能、架构或跨模块改动、高风险)、**日常迭代**(目标明确的常规功能与缺陷修复)、**快速修改**(局部、低风险、方案明确的改动)。三者都以同一个 `completed` 生命周期收尾。
21
+ - **修复 UI 的两个编辑丢失问题**:点击**当前已选中**的流程卡片会重新用预设默认值覆盖绑定——一次误点就会丢掉全部自定义;现在点击当前项无操作。有未保存修改时切换流程会**先确认**,因为绑定是整体替换、编辑无法恢复。保存按钮在有未保存修改时才可点,并显示状态。
22
+
23
+ ### 规则改为只挂在技能下(第三批,破坏性)
24
+
25
+ **规则改为只挂在技能下。** 此前 `StageBinding` 是 `{ skills: string[], rules: string[] }` 两个平级数组,于是「这个技能在什么规则下运行」这个问题,必须先把预设默认、项目追加、优先级解析全部算一遍才能回答——而且答案会随阶段变化。现在:
26
+
27
+ ```text
28
+ 流程 → 节点 → 技能 → 规则
29
+ ```
30
+
31
+ - `StageBinding` 只保留 `skills`,每项是 `SkillBinding { skill: ResourceRef, rules: ResourceRef[] }`。**节点不再有规则列表,也不提供追加、禁用或覆盖**;打开一个技能看到的就是它完整的规则列表,挂到任何节点都是同一套。
32
+ - 同一技能需要不同规则时**复制成另一个独立技能**,不建立隐式继承。测试里有一条断言专门锁住这一点:同一个技能引用在任何流程的任何节点都必须携带完全相同的规则集合。
33
+ - **资源引用带来源**(`bundled:` / `project:` / `user:`)。裸名字无法区分「项目里的 coding-conventions」和「用户目录里的 coding-conventions」——过去二者只能靠优先级隐式决定,现在引用本身说明去哪一层找,解析不再走优先级遍历。
34
+ - **规则可被多个技能引用**,正文不复制。`security-redlines` 同时被 `code-implement` 与 `code-review` 引用,就是这种共享(有断言覆盖)。同一阶段的重复引用会去重披露。
35
+ - **内置规则的归属按正文内容逐条审查,不按文件名机械搬迁**:`coding-conventions` 给 `code-implement`(写时遵循)与 `code-review`(审的就是这些);`commit-conventions` 只给 `code-commit`;`security-redlines` 给 `code-implement`、`code-review` 与 `requirement-analysis`——它的「服务端校验才是边界,前端校验不是」在需求阶段就要定。
36
+ - **旧配置的节点级规则不丢弃、不猜归属**。`legacy_rules` 保留原名,**仍然生效**,同时 `status.unassigned_legacy_rules` 列出它们等待归属;阶段披露文本也会说明这是待分配规则。`validateWorkflow` 会报出未分配的旧规则,让这个迁移状态保持可见而不是沉淀成两套并存的模型。
37
+ - `status` 新增 `unassigned_legacy_rules`,`rules[]` 现在带 `source`;`skill_obligations` 与 `skill_result` 按技能名比较(DSH 的 skill 工具按名寻址),来源只决定解析到哪一层。
38
+
39
+ **界面**:节点页只列技能;勾选技能后展开「规则设置」,在**技能**上增删规则。旧的平级「技能 / 规则」双选择器(`BindingPicker`)已删除——它表达的正是被取消的双层模型。技能卡片区分预设绑定(锁定)与项目追加,并显示该技能当前携带几条规则。
40
+
41
+ **关于测试**:`.p0-test.mjs` 与 `.workflow-test.mjs` 里各有一条断言依赖旧的节点级规则语义(「清空节点规则仍保留核心规则」「技能以字符串数组绑定」),已改为按新模型断言——不变量没有变(核心能力不可被覆盖取消),变的只是它落在技能上。新增 4 条第三批断言。
42
+
43
+ ### 显式完成与可配置的审查深度(第二批)
44
+
45
+ - **完成成为显式动作,不再由「站在最后一个阶段」推定**。`minimal` 的终态恰好又是它的提交检查点,而「离开检查点前必须提交」这条规则只在**离开**阶段时触发——终态没有出口,于是该流程能在 `commits` 为空时抵达终点,**没有任何交付记录**。新增 `complete` 操作与 `TaskCompletion` 记录:三个预设统一以同一生命周期收尾,`completionBlockers()` 在完成时检查「是否处于终态」「`todos_done` 条件是否满足」「交付方式是否要求提交」。
46
+ - **是否必须提交改为流程的交付方式**。`commit_required`(缺省 `true`,保持既有行为)允许非代码任务或非 Git 项目完成而不需要提交——Git 提交是一种交付方式,不是通用终点。
47
+ - **每项审查深度改为流程属性**。`review_depth`(`two-stage` / `single`,缺省 `two-stage` 保持既有行为)。此前 `todosBlockers` 被三个预设共用且硬编码要求 `spec` 与 `quality` **两个**结论,因此 `minimal` 虽只有两个阶段,逐项审查负担与完整流程完全相同——实测三者对同一状态返回**逐字节相同**的阻塞列表。现有 `minimal` 声明 `single`:**轻量是减少重复判定,不是跳过检查**——未审核的项、未通过的结论仍然阻塞。
48
+ - **`review` 结构改为防御性读取**。`todosBlockers` 原先假设 `quality` 必然存在;加载轻量深度或较早快照写下的记录时会崩溃,而不是报告问题。
49
+ - **未知的 `review_depth` 是配置错误**,不会静默退回最严档位把拼写错误藏在更严行为背后。`validateWorkflow` 会报出它。
50
+ - `minimal` 版本升到 2;`standard` / `agile` 的 v1 快照继续按原语义读取。
51
+
52
+ **关于测试**:第一批里那条针对 `minimal` 的断言是**空过的**——它检查 `status.skill_blockers`/`commit_blockers`,而 `todosBlockers` 的输出从不进入这两个字段,所以断言的失败分支不可达,对着完全未修复的引擎也能通过。现已改为**直接调用被测函数**,并加了一条「三个预设的审查负担不得完全相同」的断言。反向验证:临时移除 `review_depth` 声明,两条测试立即失败。
53
+
54
+ ### 门禁一致性修复(第一批)
55
+
56
+ 针对 2026-09-23 外部深度评测的第一批修复。全部问题先用独立复现确认存在(`.assessment-batch1.mjs`,8 项),再修,且每条都有正反两面验证。
57
+
58
+ - **验证失败不再能取得提交许可**。标准流程的 `交付 → 代码审核` 要求 `verified`,而 `代码审核 → 完成` 只要求 `review_passed`;`evidenceBlockers` 又把陈旧检查挂在 `verification.passed` 为真之下,于是**重新验证失败反而让检查彻底消失**——失败比从不验证更宽松。现在 `verified` 作为流程级约束计算:`verificationHeldStages()` 从所有 `verified` 边**向前闭包**推导出「该要求仍在生效的阶段集合」,使要求只在跨过验证门之后适用(任务在需求阶段尚无物可验,不应被阻塞),跨过之后则一直适用。提交与完成共用该判断。
59
+ - **敏捷流程的提交标签契约统一**。检查点阶段返回标签 `TASK`,而该预设的消息正则只接受 `T\d+`,导致引擎交给模型的状态行与校验器对同一份契约互相矛盾。正则改为同时接受 `TASK` 与 `T\d+`,覆盖 `item` 策略实际产生的两种标签形状(实施项提交与收尾提交)。
60
+ - **敏捷流程补齐 `review` 产物**。该预设的 `审查` 阶段绑定 `code-review`,而该技能指示模型 `record artifact=review`;预设却只声明了 `requirement`,于是被绑定技能的指令被引擎拒绝——预设自己制造了一个不可能满足的契约。现声明 `审查` 阶段的 `review` 产物。
61
+ - **缺失规则不再静默跳过**。`resolveRules` 在 bundled/project/user 三处都找不到时只是不加入结果,调用方无从察觉——被删除的规则文件与从未绑定的规则无法区分。现在解析结果区分 `resolved`/`missing`,并记录每条规则的**来源层**(内置规则按设计优先于同名项目/用户文件,不记录来源就无法判断哪一份在生效)。`status` 新增 `missing_rules` 与 `rules[]`(含来源),阶段披露文本也会明确列出找不到的绑定。
62
+ - **敏捷版本号提升到 2**。上一项改变了预设的产物契约与消息契约,因此按版本区分:新任务采用 v2,既有任务继续读取各自冻结的 v1 快照。
63
+
64
+ **关于既有测试**:`.workflow-test.mjs` 中一项用例从 `代码审核` 阶段起步却未给出验证状态,此前正是靠上述缺陷(`passed` 为假时陈旧检查不触发)才得以看到它真正要测的技能门禁。已为该项补上验证状态——被测意图不变(未执行的附加技能不得冒充完成),只是不再依赖缺陷的副作用。
65
+
66
+ ## 0.23.9
67
+
68
+ - **工作台的阶段绑定改为可搜索的选择器**。此前技能和规则各是一串可点击的胶囊标签:没有搜索、没有筛选、没有计数,列表也没有滚动容器,资源一多就把页面撑得很长。现在每类资源各有一个选择器:
69
+ - 技能与规则**各自独立搜索**,另有「仅看已选」筛选。
70
+ - 显示**已选/总数**、每项的资源来源,空态区分「尚未安装」和「没有匹配项」。
71
+ - 列表在 280px 内**就地滚动**,不再随目录增长而拉伸页面。
72
+ - **预设默认绑定保持勾选且不可取消**,与引擎的追加式覆盖语义一致——界面不会表达一个引擎会忽略的移除。
73
+ - 目录中找不到的绑定**仍然可见**并标注为未找到;目录加载失败不能静默隐藏已有选择。
74
+ - 切换阶段时**重置筛选但不改动绑定**。
75
+ - 顺带删除重复的只读流程节点列表和每个预设的说明文字——选择器已经展示了这些信息。
76
+ - **修复客户端 source map 内联源码**。`lib/client.js.map` 此前带有 `sourcesContent`,把整棵 `src/client` 源码原样以 `lib/` 名义打进包里(447 KB);基于路径的「tarball excludes src sources」检查因此形同虚设。改为只保留行映射后为 148 KB,并补上**按内容而非按文件名**断言的检查,防止退回。
77
+ - **修复 10 处 UTF-8 字节损坏**。四处文件里出现了「被截断的 UTF-8 首字节 + `?`」的序列——工具链某处把无法编码的字符替换掉了而没有报错,因此评审从未看到。最严重的一处在**预设名内部**:`工程化开发引擎` 的「擎」是 `E6 93 3F` 而非 `E6 93 8D`,而这正是预设选择器渲染的文字。其余为破折号和一个箭头。全部修复后,仓库现以严格的 UTF-8 解码器验证通过——此前用容错解码读取,正是它掩盖了这个问题。
78
+
7
79
  ## 0.23.8
8
80
 
9
81
  - **更正文档中与代码不符的陈述**,不改动任何运行时行为:
@@ -14,7 +14,7 @@
14
14
 
15
15
  项目没有配置文件时使用 `standard`;配置文件存在但 flow 缺失或无效时会报错,不会自动忽略错误。
16
16
 
17
- ## 追加技能与规则
17
+ ## 技能与规则
18
18
 
19
19
  最小配置如下:
20
20
 
@@ -24,15 +24,40 @@
24
24
  }
25
25
  ```
26
26
 
27
- `stage_bindings` 的键是当前流程中的阶段名称,值是该阶段需要的技能与规则名称。字段可省略;默认绑定仍然生效。[默认配置示例](../defaults/eng.json)列出了标准流程的绑定。
27
+ `stage_bindings` 的键是当前流程中的阶段名称,值只有 `skills`——**节点只选择技能,规则属于技能**。
28
28
 
29
- 当前绑定采用追加方式:项目增加的名称会与预设默认项合并去重;取消默认项的勾选不会移除其绑定。内置同名资源优先,使用不同名称来安装自己的资源。
29
+ ```json
30
+ {
31
+ "flow": "standard",
32
+ "stage_bindings": {
33
+ "开发": {
34
+ "skills": [
35
+ { "skill": { "source": "bundled", "name": "code-implement" },
36
+ "rules": [{ "source": "bundled", "name": "coding-conventions" },
37
+ { "source": "project", "name": "api-contract" }] }
38
+ ]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ **规则挂载在技能下,节点不提供规则追加、禁用或覆盖。** 打开一个技能就能看到它完整的规则列表;把它挂到任何节点,都是同一套规则。需要同一技能在不同节点用不同规则时,**复制成另一个独立技能**,再分别配置——不建立隐式继承关系。
45
+
46
+ 规则引用带来源(`bundled:` / `project:` / `user:`),因此同名资源不会被混淆。同一份规则可被多个技能引用,不需要复制正文;编辑共享规则时界面会显示受影响的技能。
47
+
48
+ [默认配置示例](../defaults/eng.json)列出了标准流程的完整绑定。
49
+
50
+ 当前绑定采用追加方式:项目增加的**技能**会与预设默认项合并去重,按来源标识比较;取消预设技能的勾选不会移除它。内置同名资源优先,自建资源请使用不同名称。
51
+
52
+ 进入阶段后,`dev_task` 披露该阶段全部技能的规则内容。新任务离开阶段前会检查 Harness 的 skill 工具成功加载记录;附加技能还须记录 `skill_result`(技能名、执行场景、真实验收命令回执)。`status.skill_obligations` 中的 `command_receipts_required` 列出需要回执的技能;七个内置技能使用已有的记录、验证、审核和提交操作,无需重复登记附加回执。安装一个技能不会自动执行其中的脚本。
53
+
54
+ 挂在终态(例如"完成")的技能在进入终态前执行。测试技能通常建议挂在"交付";现有"完成"绑定也会在审核阶段执行后才放行。标准流程 v2 在审核通过后提交,并核对真实 Git HEAD。修改文件或声明范围后,旧验证回执失效。
30
55
 
31
- 进入阶段后,`dev_task` 披露该阶段的技能名和规则内容。新任务离开阶段前会检查 Harness 的 skill 工具成功加载记录;附加技能还须记录 `skill_result`(技能名、执行场景、真实验收命令回执)。`status.skill_obligations` 中的 `command_receipts_required` 列出需要回执的技能;七个内置技能使用已有的记录、验证、审核和提交操作,无需重复登记附加回执。安装一个技能不会自动执行其中的脚本。
56
+ 资源管理保留项目和个人目录的同名条目,避免来源误标和误操作。技能最终由 Harness 的技能目录加载。
32
57
 
33
- 挂在终态(例如“完成”)的技能在进入终态前执行。测试技能通常建议挂在“交付”;现有“完成”绑定也会在审核阶段执行后才放行。标准流程 v2 在审核通过后提交,并核对真实 Git HEAD。修改文件或声明范围后,旧验证回执失效。
58
+ ### 旧配置迁移
34
59
 
35
- 资源管理保留项目和个人目录的同名条目,避免来源误标和误操作。绑定仍按名称选择,内置优先;自建同名资源按项目优先于个人解析。技能最终由 Harness 的技能目录加载。
60
+ 旧配置把规则写在**节点**下(`"开发": { "rules": [...] }`)。这类规则不记录它属于哪个技能,因此**不会被自动分配**:它们保留在 `legacy_rules` 中,**仍然生效**,同时 `status.unassigned_legacy_rules` 会列出它们等待归属。请把每条规则加到你希望承载它的技能下,再从 `legacy_rules` 中移除;若同一技能在不同节点需要不同规则,复制该技能再分别配置。
36
61
 
37
62
  ## 哪些内容暂时不能修改
38
63
 
package/hooks/commit-msg CHANGED
@@ -30,11 +30,15 @@ var import_node_fs = __toESM(require("node:fs"), 1);
30
30
  var import_node_path = __toESM(require("node:path"), 1);
31
31
 
32
32
  // src/engine.ts
33
+ function formatResourceRef(ref) {
34
+ return `${ref.source}:${ref.name}`;
35
+ }
33
36
  function artifactsForStage(stage, config2) {
34
37
  return config2.artifacts.filter((artifact) => artifact.stage === stage);
35
38
  }
36
- function todosBlockers(state2) {
39
+ function todosBlockers(state2, config2) {
37
40
  if (state2.items.length === 0) return ["no implementation items"];
41
+ const depth = config2?.review_depth ?? "two-stage";
38
42
  const blockers = [];
39
43
  for (const item of state2.items) {
40
44
  if (item.status !== "done") {
@@ -42,11 +46,13 @@ function todosBlockers(state2) {
42
46
  continue;
43
47
  }
44
48
  if (item.review === void 0) {
45
- blockers.push(`item "${item.id}" is done but has no two-stage review`);
49
+ blockers.push(`item "${item.id}" is done but has no review`);
46
50
  continue;
47
51
  }
48
- if (item.review.spec.outcome !== "pass") blockers.push(`item "${item.id}" spec review is not pass`);
49
- if (item.review.quality.outcome !== "pass") blockers.push(`item "${item.id}" quality review is not pass`);
52
+ if (item.review.spec?.outcome !== "pass") blockers.push(`item "${item.id}" spec review is not pass`);
53
+ if (depth === "two-stage" && item.review.quality?.outcome !== "pass") {
54
+ blockers.push(`item "${item.id}" quality review is not pass`);
55
+ }
50
56
  }
51
57
  return blockers;
52
58
  }
@@ -57,7 +63,7 @@ function guardSatisfied(guard, state2, config2) {
57
63
  case "solution_confirmation":
58
64
  return state2.solution_confirmed;
59
65
  case "todos_done":
60
- return todosBlockers(state2).length === 0;
66
+ return todosBlockers(state2, config2).length === 0;
61
67
  case "verified": {
62
68
  if (!state2.verification.passed) return false;
63
69
  if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
@@ -87,11 +93,45 @@ function validateCommitMessage(message2, config2) {
87
93
  errors: [`commit summary "${message2}" does not match ${config2.commit.message_hint}`]
88
94
  };
89
95
  }
96
+ function verificationHeldStages(config2) {
97
+ const held = /* @__PURE__ */ new Set();
98
+ const queue = config2.transitions.filter((transition) => (transition.requires ?? []).includes("verified")).map((transition) => transition.to);
99
+ while (queue.length > 0) {
100
+ const stage = queue.shift();
101
+ if (held.has(stage)) continue;
102
+ held.add(stage);
103
+ for (const transition of config2.transitions) {
104
+ if (transition.from === stage && !held.has(transition.to)) queue.push(transition.to);
105
+ }
106
+ }
107
+ return held;
108
+ }
109
+ function verificationBlockers(state2, config2) {
110
+ if (state2.execution_version !== 1) return [];
111
+ if (!verificationHeldStages(config2).has(state2.stage)) return [];
112
+ const blockers = [];
113
+ if (!state2.verification.passed) {
114
+ blockers.push("this flow requires a passing verification at this stage, and the current verification is not passing");
115
+ }
116
+ if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
117
+ const receipt = state2.verification.receipt;
118
+ if (receipt === void 0) {
119
+ blockers.push("a high-risk task needs a real command receipt, and none is recorded");
120
+ } else if (receipt.exit_code !== 0 || receipt.timed_out || receipt.aborted) {
121
+ blockers.push("a high-risk task needs a command that exited 0 without timing out or aborting");
122
+ }
123
+ }
124
+ return blockers;
125
+ }
90
126
  function commitCheckpoint(state2, config2) {
91
127
  const { policy, checkpoints } = config2.commit;
92
128
  if (policy === "manual") {
93
129
  return { allowed: false, reason: "commit policy is manual \u2014 await an explicit user instruction" };
94
130
  }
131
+ const verification = verificationBlockers(state2, config2);
132
+ if (verification.length > 0) {
133
+ return { allowed: false, reason: verification.join("; ") };
134
+ }
95
135
  if (checkpoints.includes(state2.stage)) {
96
136
  const ready = config2.transitions.filter((transition) => transition.from === state2.stage).every((transition) => (transition.requires ?? []).every((g) => guardSatisfied(g, state2, config2)));
97
137
  if (!ready) {
@@ -118,6 +158,10 @@ function checkFileScope(state2, committing, config2) {
118
158
  }
119
159
 
120
160
  // src/workflows.ts
161
+ function bundled(skill, ...rules) {
162
+ const ref = (name) => ({ source: "bundled", name });
163
+ return { skill: ref(skill), rules: rules.map(ref) };
164
+ }
121
165
  var STANDARD = {
122
166
  stages: ["\u9700\u6C42\u8BC4\u5BA1", "\u8BBE\u8BA1", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u4EE3\u7801\u5BA1\u6838", "\u5B8C\u6210"],
123
167
  start_stage: "\u9700\u6C42\u8BC4\u5BA1",
@@ -142,11 +186,16 @@ var STANDARD = {
142
186
  },
143
187
  high_risk_requires_verification: true,
144
188
  stage_bindings: {
145
- "\u9700\u6C42\u8BC4\u5BA1": { skills: ["requirement-analysis"], rules: ["security-redlines"] },
146
- "\u8BBE\u8BA1": { skills: ["solution-design"] },
147
- "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
148
- "\u4EA4\u4ED8": { skills: ["code-verify"], rules: ["coding-conventions"] },
149
- "\u4EE3\u7801\u5BA1\u6838": { skills: ["code-review", "code-commit"], rules: ["security-redlines", "commit-conventions"] }
189
+ "\u9700\u6C42\u8BC4\u5BA1": { skills: [bundled("requirement-analysis", "security-redlines")] },
190
+ "\u8BBE\u8BA1": { skills: [bundled("solution-design")] },
191
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
192
+ "\u4EA4\u4ED8": { skills: [bundled("code-verify")] },
193
+ "\u4EE3\u7801\u5BA1\u6838": {
194
+ skills: [
195
+ bundled("code-review", "coding-conventions", "security-redlines"),
196
+ bundled("code-commit", "commit-conventions")
197
+ ]
198
+ }
150
199
  }
151
200
  };
152
201
  var AGILE = {
@@ -158,21 +207,32 @@ var AGILE = {
158
207
  { from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
159
208
  ],
160
209
  artifacts: [
161
- { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] }
210
+ { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] },
211
+ // The 审查 stage binds code-review, whose instructions write this artifact with
212
+ // `record artifact=review`. Declaring it here keeps that binding executable:
213
+ // the engine refuses an undeclared artifact, so a bound skill pointing at one
214
+ // would be a contract the preset itself made impossible to satisfy.
215
+ { stage: "\u5BA1\u67E5", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
162
216
  ],
163
217
  commit: {
218
+ // `item` policy emits two label shapes: `T<n>` for a mid-flow item commit and
219
+ // `TASK` for the closing commit at the checkpoint. The pattern below accepts
220
+ // BOTH, so the label the engine hands the model always satisfies the preset's
221
+ // own rule. Previously it accepted only `T\d+` while the checkpoint stage
222
+ // returned `TASK`, which made the closing commit impossible to phrase: the
223
+ // status line and the validator disagreed about the contract.
164
224
  policy: "item",
165
- message_pattern: "^\u3010(\\S+)\u3011\u3010T\\d+\u3011.+",
166
- message_hint: "\u3010<task_id>\u3011\u3010T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id",
225
+ message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
226
+ message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF1B\u5B9E\u65BD\u9879\u63D0\u4EA4\u7528 T1\u3001T2\uFF0C\u6536\u5C3E\u63D0\u4EA4\u7528 TASK",
167
227
  checkpoints: ["\u4EA4\u4ED8"],
168
228
  file_scope: true
169
229
  },
170
230
  high_risk_requires_verification: false,
171
231
  stage_bindings: {
172
- "\u9700\u6C42": { skills: ["requirement-analysis"] },
173
- "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
174
- "\u4EA4\u4ED8": { skills: ["code-verify", "code-commit"], rules: ["commit-conventions"] },
175
- "\u5BA1\u67E5": { skills: ["code-review"] }
232
+ "\u9700\u6C42": { skills: [bundled("requirement-analysis", "security-redlines")] },
233
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
234
+ "\u4EA4\u4ED8": { skills: [bundled("code-verify"), bundled("code-commit", "commit-conventions")] },
235
+ "\u5BA1\u67E5": { skills: [bundled("code-review", "coding-conventions", "security-redlines")] }
176
236
  }
177
237
  };
178
238
  var MINIMAL = {
@@ -190,9 +250,14 @@ var MINIMAL = {
190
250
  file_scope: false
191
251
  },
192
252
  high_risk_requires_verification: false,
253
+ // A fast-change flow checks each change once, not twice under two headings a
254
+ // short change rarely distinguishes. The item still has to be reviewed — what
255
+ // drops is the duplicated verdict, which is where the weight actually was: this
256
+ // flow used to carry the same per-item audit burden as the full one.
257
+ review_depth: "single",
193
258
  stage_bindings: {
194
- "\u5F00\u53D1": { skills: ["code-implement"] },
195
- "\u4EA4\u4ED8": { skills: ["code-commit"], rules: ["commit-conventions"] }
259
+ "\u5F00\u53D1": { skills: [bundled("code-implement", "coding-conventions", "security-redlines")] },
260
+ "\u4EA4\u4ED8": { skills: [bundled("code-commit", "commit-conventions")] }
196
261
  }
197
262
  };
198
263
  function deriveCapabilities(config2) {
@@ -208,9 +273,16 @@ function preset(id, version, label, description, config2) {
208
273
  return { id, version, label, description, capabilities: deriveCapabilities(config2), config: config2 };
209
274
  }
210
275
  var FLOW_PRESETS = {
211
- standard: preset("standard", 2, "\u6807\u51C6\u7814\u53D1", "\u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838\uFF0C\u5BA1\u6838\u540E\u63D0\u4EA4", STANDARD),
212
- agile: preset("agile", 1, "\u654F\u6377\u8F7B\u91CF", "\u9700\u6C42 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u5BA1\u67E5\uFF0C\u56DB\u9636\u6BB5\u3001\u5C11\u4EA7\u7269", AGILE),
213
- minimal: preset("minimal", 1, "\u7EAF\u4EE3\u7801", "\u5F00\u53D1 \u2192 \u4EA4\u4ED8\uFF0C\u4E24\u9636\u6BB5\uFF0C\u53EA\u7559\u63D0\u4EA4\u95E8\u7981", MINIMAL)
276
+ standard: preset("standard", 2, "\u5B8C\u6574\u7814\u53D1", "\u65B0\u529F\u80FD\u3001\u67B6\u6784\u6216\u8DE8\u6A21\u5757\u6539\u52A8\u3001\u9AD8\u98CE\u9669\u4EFB\u52A1\uFF1A\u9700\u6C42\u786E\u8BA4 \u2192 \u65B9\u6848\u786E\u8BA4 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u8BC1 \u2192 \u5BA1\u6838 \u2192 \u63D0\u4EA4", STANDARD),
277
+ // Version 2: the closing-commit label shape was unified with the message pattern,
278
+ // and the `review` artifact the bound code-review skill writes was declared. Tasks
279
+ // created before this keep their frozen version 1 config and are unaffected.
280
+ agile: preset("agile", 2, "\u65E5\u5E38\u8FED\u4EE3", "\u76EE\u6807\u660E\u786E\u7684\u5E38\u89C4\u529F\u80FD\u4E0E\u7F3A\u9677\u4FEE\u590D\uFF1A\u76EE\u6807\u4E0E\u9A8C\u6536 \u2192 \u5B9E\u73B0 \u2192 \u9A8C\u6536\u4E0E\u5BA1\u67E5 \u2192 \u63D0\u4EA4", AGILE),
281
+ // Version 2: per-item review depth is declared as `single` instead of inheriting
282
+ // the full flow's two verdicts per item. Tasks created before this keep their
283
+ // frozen version 1 config, which has no `review_depth` and therefore reads as
284
+ // two-stage exactly as it behaved.
285
+ minimal: preset("minimal", 2, "\u5FEB\u901F\u4FEE\u6539", "\u5C40\u90E8\u3001\u4F4E\u98CE\u9669\u3001\u65B9\u6848\u660E\u786E\u7684\u6539\u52A8\uFF1A\u4FEE\u6539 \u2192 \u68C0\u67E5\u4E0E\u63D0\u4EA4\uFF0C\u6BCF\u9879\u4E00\u6B21\u68C0\u67E5", MINIMAL)
214
286
  };
215
287
  function dedupe(names) {
216
288
  return [...new Set(names)];
@@ -221,11 +293,18 @@ function mergeBindings(base, override) {
221
293
  const merged = { ...base.stage_bindings };
222
294
  for (const [stage, addition] of Object.entries(override)) {
223
295
  const existing = merged[stage];
224
- const skills = dedupe([...existing?.skills ?? [], ...addition.skills ?? []]);
225
- const rules = dedupe([...existing?.rules ?? [], ...addition.rules ?? []]);
296
+ const skills = [...existing?.skills ?? []];
297
+ const seen = new Set(skills.map((skill) => formatResourceRef(skill.skill)));
298
+ for (const skill of addition.skills ?? []) {
299
+ const key = formatResourceRef(skill.skill);
300
+ if (seen.has(key)) continue;
301
+ seen.add(key);
302
+ skills.push(skill);
303
+ }
304
+ const legacy = dedupe([...existing?.legacy_rules ?? [], ...addition.legacy_rules ?? []]);
226
305
  const binding = {};
227
306
  if (skills.length > 0) binding.skills = skills;
228
- if (rules.length > 0) binding.rules = rules;
307
+ if (legacy.length > 0) binding.legacy_rules = legacy;
229
308
  merged[stage] = binding;
230
309
  changed = true;
231
310
  }