flower-trellis 0.6.0-beta.6 → 0.6.0-beta.7

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.
Files changed (25) hide show
  1. package/README.md +1 -1
  2. package/enhancements/0.6/.agents/skills/trellis-check-all/SKILL.md +3 -2
  3. package/enhancements/0.6/.agents/skills/trellis-check-all/references/fallback-findings.md +7 -3
  4. package/enhancements/0.6/.agents/skills/trellis-check-all/references/full-profile.md +2 -1
  5. package/enhancements/0.6/.agents/skills/trellis-check-all/references/light-profile.md +2 -1
  6. package/enhancements/0.6/.agents/skills/trellis-check-all/references/reporting-and-disposition.md +31 -16
  7. package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +3 -3
  8. package/enhancements/0.6/.agents/skills/trellis-route/references/check-all-agent-body.md +1 -1
  9. package/enhancements/0.6/.claude/skills/trellis-check-all/SKILL.md +3 -2
  10. package/enhancements/0.6/.claude/skills/trellis-check-all/references/fallback-findings.md +7 -3
  11. package/enhancements/0.6/.claude/skills/trellis-check-all/references/full-profile.md +2 -1
  12. package/enhancements/0.6/.claude/skills/trellis-check-all/references/light-profile.md +2 -1
  13. package/enhancements/0.6/.claude/skills/trellis-check-all/references/reporting-and-disposition.md +31 -16
  14. package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +3 -3
  15. package/enhancements/0.6/overrides/patches/workflow/phase-ownership/phase-2-check-content.md +1 -1
  16. package/enhancements/0.6/overrides/patches/workflow/state-untracked/content.md +1 -1
  17. package/enhancements/0.6/overrides/patches/workflow/states-in-progress/common-content.md +1 -1
  18. package/enhancements/MANIFEST.json +2 -2
  19. package/package.json +3 -3
  20. package/src/builtin-marketplaces/rd-guide.json +2 -2
  21. package/src/commands/plugin-interactive.js +1 -1
  22. package/src/plugin/gitlab/rest-client.js +88 -12
  23. package/src/plugin/sources/gitlab-provider.js +111 -12
  24. package/src/plugin/sources/remote-archive.js +10 -6
  25. package/src/plugin/sources/user-source-store.js +55 -14
package/README.md CHANGED
@@ -168,7 +168,7 @@ flower-trellis plugin
168
168
  | `.flower/state.json` | 本机;记录实际平台、生成路径、ownership 与 Patch provenance |
169
169
  | `.flower/cache/`、`.flower/transactions/` | 本机;可清理缓存与事务恢复证据 |
170
170
 
171
- `rd-guide` 是随包预注册、默认启用但惰性访问的 GitLab Marketplace。打开管理器后,`发现` 页会在已有凭据时读取远程目录;未登录时只展示授权入口,不会尝试读取仓库内容。普通交互默认使用 Device Flow,PKCE 浏览器登录保留为来源详情中的高级选项。OAuth 只申请 `read_api read_repository`,Application Secret 和 token 都不会写入项目文件。
171
+ `rd-guide` 是随包预注册、默认启用但惰性访问的 GitLab Marketplace。它的 GitLab 地址、项目和 ref 随 Flower 包升级,用户配置只保存启用或停用偏好,避免旧的用户级完整副本静默遮蔽包内修复;需要其它地址或分支时应新增独立 source ID。打开管理器后,`发现` 页会在已有凭据时读取远程目录;未登录时只展示授权入口,不会尝试读取仓库内容。普通交互默认使用 Device Flow,PKCE 浏览器登录保留为来源详情中的高级选项。OAuth 只申请 `read_api read_repository`,Application Secret 和 token 都不会写入项目文件。
172
172
 
173
173
  GitHub 首版只支持 `github.com` 公共仓库和匿名 REST,不保存 PAT 或其它凭据。来源会固定确认后的格式入口;安装 lock 固定完整 commit 与 canonical digest,通过 Marketplace 发现时还会固定索引仓库和索引 commit。匿名 API 可能受每小时 60 次/IP 的主要限额影响,限流会显示为 GitHub 诊断,不会转成登录提示。
174
174
 
@@ -18,7 +18,7 @@ description: "统一 Check-All 入口:确认范围与运行上下文,按 req
18
18
  4. 先按根因性质和可达证据把发现分为主路径 `CHK-*`、兜底 `FBK-*` 与文档漂移 `DOC-*`,再为 `CHK-*` 和 `FBK-*` 分配 P0/P1/P2。
19
19
  5. 在最终报告前处理允许自动修复的文档漂移,并把修复内容展示在报告里。
20
20
  6. 根据 interactive / validated auto-loop 边界输出下一步或完成 runner `record + next`。
21
- 7. untracked helper 只保存流程游标:findings 或新编辑设回 `implement`;只有严格通过且 disposition 确认继续时才 `advance --stage spec`。
21
+ 7. untracked helper 只保存流程游标:未处置 findings 或新编辑设回 `implement`;严格通过或已接受风险通过且 disposition 确认继续时才 `advance --stage spec`。
22
22
 
23
23
  ---
24
24
 
@@ -42,7 +42,7 @@ description: "统一 Check-All 入口:确认范围与运行上下文,按 req
42
42
  1. **默认 audit-only collect-all**:可以读文件、搜索、运行无业务写入副作用的 lint、typecheck 和测试;普通代码、配置、测试、任务规格语义问题不得在检查阶段直接修复。
43
43
  2. **唯一自修例外**:低风险文档漂移进入 `DOC-*` 通道,按 `references/document-drift-auto-remediation.md` 的白名单、黑名单和写入时机处理。
44
44
  3. **分类先于严重度**:读取 `references/fallback-findings.md`;主路径错误和非兜底契约违背进入 `CHK-*`,fail-closed、异常输入、失败降级和防御性保护缺口进入 `FBK-*`。契约证据影响严重度,不改变兜底根因归属。
45
- 4. **修改前只确认一次**:除 `DOC-*` 自动修复外,全部检查结束后通过统一报告让用户选择 `CHK-*` / `FBK-*` 修复范围;`修复全部` 默认覆盖两类问题。
45
+ 4. **处置只确认一次**:除 `DOC-*` 自动修复外,全部检查结束后通过统一报告让用户选择 `CHK-*` / `FBK-*` 修复范围或明确接受当前风险;`修复全部` 默认覆盖两类问题,接受风险不得隐藏发现。
46
46
  5. **委托规则不改变边界**:复用 `trellis-check` 时只复用检查清单、验证方法和命令发现,忽略其中任何“直接修复”“失败后先修复”的指令。
47
47
  6. **真正阻塞才中途暂停**:只有业务规划冲突、后续验证前提失效、生产或外部副作用、破坏性操作风险时提前停止。
48
48
 
@@ -114,6 +114,7 @@ check_profile:
114
114
  - 三个维度状态;
115
115
  - 自动修复的 `DOC-*` 内容;
116
116
  - 剩余 `CHK-*` 主路径问题与 `FBK-*` 兜底问题;
117
+ - 每个剩余问题的未处置或已接受风险状态;
117
118
  - 已执行验证和未覆盖风险;
118
119
  - 与当前结论匹配的唯一下一步。
119
120
 
@@ -1,6 +1,6 @@
1
1
  # Fallback Findings
2
2
 
3
- 本文件定义 `CHK-*` 主路径问题与 `FBK-*` 兜底问题的根因边界。分类发生在严重度评估之前;分类完成后,两类问题都根据当前实际影响分配 P0/P1/P2,并进入同一个修复与严格通过门禁。
3
+ 本文件定义 `CHK-*` 主路径问题与 `FBK-*` 兜底问题的根因边界。分类发生在严重度评估之前;分类完成后,两类问题都根据当前实际影响分配 P0/P1/P2,并进入同一个修复、风险接受与通过门禁。
4
4
 
5
5
  ---
6
6
 
@@ -65,5 +65,9 @@
65
65
  - `CHK-*` 与 `FBK-*` 分别独立编号,当前修复/重检循环保留原 ID。
66
66
  - 两类问题都按实际影响分配 P0/P1/P2;严重度不改变通道归属。
67
67
  - 两类问题都是修复项,都被 `修复全部` 覆盖,也都可以通过混合精确 ID 修复,例如 `修复 CHK-001,FBK-002`。
68
- - 任何剩余 `CHK-*` 或 `FBK-*` 都阻断 strict pass,并让 interactive、untracked、auto-loop、direct Git、Update-Spec 与 Push 进入未通过或 fix/recheck 路径。
69
- - `仅保留报告` 可以显式停止修复,但不能把未解决问题改写成通过。
68
+ - 用户可以明确接受当前报告中任一 `CHK-*` 或 `FBK-*` 的风险而不修复;接受风险不改变问题通道、严重度、证据或建议,也不能把问题从报告中删除。
69
+ - 风险接受必须能唯一对应当前报告的问题 ID;当前报告只有一个候选时,“这个问题”“不管这个”等明确指代可以解析为该 ID。P0 必须逐项写出精确 ID,不接受“全部”式概括授权。
70
+ - 风险接受只适用于当前问题证据和实际 diff;受影响代码、契约、验证结果或问题证据变化后立即失效,必须重新检查和接受。
71
+ - `strict pass` 仍要求剩余 `CHK-*` 与 `FBK-*` 均为 0;若所有剩余问题都已被用户有效接受,且无阻塞、无部分验证、无未接受的实质风险,则使用“已接受风险通过”。两种通过都可以进入 interactive、untracked、direct Git、Update-Spec 与 Push 的继续路径。
72
+ - 只有未处置的 `CHK-*` 或 `FBK-*` 阻断交互完成链。validated auto-loop 不能代表用户接受风险,仍要求两类问题均为 0 才能 `record ok`。
73
+ - `仅保留报告` 只表示停止修复,不构成风险接受,也不能把未处置问题改写成通过。
@@ -131,4 +131,5 @@ Full 通过必须同时满足:
131
131
  - 所有适用 PRD / design / implement 条目已映射到实现或明确 `N/A`;
132
132
  - 所有触发的假设 Dimension 已完成源码或真实契约核对;
133
133
  - 项目规范、复用、依赖、同层一致性和验证命令已覆盖实际变更范围;
134
- - 无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险。
134
+ - strict pass:无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险;或
135
+ - 已接受风险通过:所有剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受,且无阻塞、无部分验证、无未接受的实质剩余风险。
@@ -70,6 +70,7 @@ Light 通过必须同时满足:
70
70
  - 所有受影响规划条目已核对;
71
71
  - 所有触发的假设维度状态为 `通过` 或合理的 `N/A`;
72
72
  - 定向验证已运行并通过,或明确说明无需运行的原因;
73
- - 无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险。
73
+ - strict pass:无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险;或
74
+ - 已接受风险通过:所有剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受,且无阻塞、无部分验证、无未接受的实质剩余风险。
74
75
 
75
76
  存在未覆盖但不影响局部结论的内容时,必须在“未覆盖与风险”中说明。
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 统一问题模型
8
8
 
9
- 先按 `references/fallback-findings.md` 判定 `CHK-*` / `FBK-*`,再为两类问题分配 P0/P1/P2。分类表达根因性质,严重度描述当前实际影响;两者都不改变问题必须进入修复和 strict pass 门禁的处置规则。
9
+ 先按 `references/fallback-findings.md` 判定 `CHK-*` / `FBK-*`,再为两类问题分配 P0/P1/P2。分类表达根因性质,严重度描述当前实际影响;两者都不决定问题必须修复还是可以由用户接受风险。
10
10
 
11
11
  ### `CHK-*` 主路径问题
12
12
 
@@ -42,7 +42,19 @@
42
42
  | 位置 | 同一根因的全部受影响位置 |
43
43
  | 验证 | 修复后的测试、故障注入、命令或手动验证步骤 |
44
44
 
45
- `CHK-*` 与 `FBK-*` 分开编号。同一根因的多个位置合并到一个问题;报告按严重度排序,但不得因此重排已经分配的 ID。新根因使用对应通道的下一个 ID。
45
+ `CHK-*` 与 `FBK-*` 分开编号。同一根因的多个位置合并到一个问题;报告按严重度排序,但不得因此重排已经分配的 ID。新根因使用对应通道的下一个 ID。每个问题还必须标记 `未处置` 或 `已接受风险`;处置状态不改变 ID、通道和严重度。
46
+
47
+ ## 风险接受
48
+
49
+ 风险接受是用户对当前报告的显式处置,不是检查器对问题严重度的降级:
50
+
51
+ 1. 只有用户可以接受风险;主会话、subagent 和 validated auto-loop 都不得代替用户推断或授权。
52
+ 2. 接受必须唯一对应当前报告的问题 ID。用户可以写 `接受风险 CHK-001,FBK-002 并继续`;当前报告只有一个候选时,“这个问题”“不管这个”等明确指代可以解析为该 ID。P0 必须逐项写出精确 ID,不接受 `接受全部剩余风险`。
53
+ 3. 接受只绑定当前问题证据与实际 diff。受影响代码、契约、验证结果、问题内容或严重度变化后,原接受立即失效,问题恢复为 `未处置`。
54
+ 4. 已接受问题继续完整展示证据、影响、建议和验证,并标记 `处置:已接受风险`;不得删除、改列 `DOC-*` 或伪报已修复。
55
+ 5. `strict pass` 只用于剩余 `CHK-*` / `FBK-*` 均为 0。所有剩余问题均已被有效接受,且无 blocked、无部分验证、无未接受的实质剩余风险时,结论为 `通过·已接受风险`。
56
+ 6. blocked、部分验证和无法唯一对应到当前问题 ID 的实质剩余风险不是 `CHK-*` / `FBK-*` 处置状态,不能借风险接受绕过。
57
+ 7. `仅保留报告` 表示停止处置并等待,不等于接受风险;只有带明确接受语义的用户回复才改变问题处置状态。
46
58
 
47
59
  ---
48
60
 
@@ -53,7 +65,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
53
65
  ```markdown
54
66
  ## Trellis Check-All 结果
55
67
 
56
- [<通过/未通过/阻塞>] <N> 个维度 · CHK <N> · FBK <N> · 自动修复 DOC <N> · P0 <N> / P1 <N> / P2 <N> · 验证 <通过>/<总数>
68
+ [<通过/通过·已接受风险/未通过/阻塞>] <N> 个维度 · CHK <N>(接受 <N>)· FBK <N>(接受 <N>)· 自动修复 DOC <N> · P0 <N> / P1 <N> / P2 <N> · 验证 <通过>/<总数>
57
69
 
58
70
  工作:<任务名称 | Untracked work: work-id | 无活动工作>
59
71
  范围:<文件数与层级摘要;包含自动修复产生的文档 diff>
@@ -77,6 +89,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
77
89
  ### 主路径问题
78
90
 
79
91
  - [ ] `CHK-001` `[P1]` <标题>
92
+ - 处置:<未处置/已接受风险>
80
93
  - 来源:<来源>
81
94
  - 证据:<file:line / 契约 / 命令结果>
82
95
  - 影响:<影响>
@@ -87,6 +100,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
87
100
  ### 兜底问题
88
101
 
89
102
  - [ ] `FBK-001` `[P1]` <标题>
103
+ - 处置:<未处置/已接受风险>
90
104
  - 来源:<来源>
91
105
  - 证据:<file:line / 契约 / 命令结果>
92
106
  - 兜底场景:<可达异常或失败场景>
@@ -105,7 +119,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
105
119
  批次 1:<CHK/FBK 问题 ID> · <修复目标>
106
120
  修复后:定向验证 -> Check-All 重检
107
121
 
108
- 操作:`修复全部`、`修复 CHK-001,FBK-002`、`仅保留报告`
122
+ 操作:`修复全部`、`修复 CHK-001,FBK-002`、`接受风险 CHK-001,FBK-002 并继续`、`仅保留报告`
109
123
 
110
124
  ### 下一步
111
125
 
@@ -116,9 +130,10 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
116
130
 
117
131
  - 没有 `DOC-*` 自动修复时省略“自动修复”区。
118
132
  - 没有 `CHK-*` 时省略“主路径问题”区;没有 `FBK-*` 时省略“兜底问题”区。
119
- - `CHK-*` 或 `FBK-*` 任一存在时展示“修复批次”,并只在报告末尾提供一次修复范围选择,不再逐项提问。
133
+ - 存在未处置 `CHK-*` 或 `FBK-*` 时展示“修复批次”,并只在报告末尾提供一次处置选择,不再逐项提问。
120
134
  - `修复全部` 始终覆盖全部 `CHK-*` 与 `FBK-*`;精确修复可以混合两类 ID。
121
- - `仅保留报告` 只表示停止修复,不改变未通过结论或剩余风险。
135
+ - 风险接受可以混合两类 ID;只有全部剩余问题都已有效接受时才形成“通过·已接受风险”。
136
+ - `仅保留报告` 只表示停止处置,不改变未通过结论或剩余风险。
122
137
  - interactive 标准报告必须以“下一步”段结束;停止等待不等于省略引导。
123
138
  - 独立 `CHK-*` 或 `FBK-*` 不得因数量多而静默省略;先合并同根因重复项,再完整列出剩余项。
124
139
  - 报告不得包含 commit message、拟提交/暂存文件、commit-only 决策或提交确认。
@@ -160,9 +175,9 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
160
175
  <按下方 `Interactive Post-Check Stop Gate` 输出一个明确、可执行的主动作>
161
176
  ```
162
177
 
163
- 检查通过后的动作由下方 `Interactive Post-Check Stop Gate` 判断:普通交互停止等待,符合 direct Git 严格通过条件时同轮进入 Phase 3.3 `trellis-update-spec`,再到 Phase 3.4 `trellis-push`。仍有 `CHK-*` 或 `FBK-*` 时停留在修复/重检循环。
178
+ 检查通过后的动作由下方 `Interactive Post-Check Stop Gate` 判断:普通交互停止等待,符合 direct Git strict pass 或已接受风险通过条件时同轮进入 Phase 3.3 `trellis-update-spec`,再到 Phase 3.4 `trellis-push`。仍有未处置 `CHK-*` 或 `FBK-*` 时停留在处置/重检循环。
164
179
 
165
- untracked helper 不记录 Check-All 证据。普通严格通过但尚未继续时保持 `stage=check`;只有 direct Git 同轮继续或用户后续明确继续时才 `advance --stage spec`。有剩余 `CHK-*`、`FBK-*`、部分验证、阻塞或报告后的新编辑时,先 `advance --stage implement` 再返回实现。
180
+ untracked helper 不记录 Check-All 证据或风险接受。普通 strict pass / 已接受风险通过但尚未继续时保持 `stage=check`;只有 direct Git 同轮继续或用户后续明确继续时才 `advance --stage spec`。有未处置 `CHK-*`、`FBK-*`、部分验证、阻塞或报告后的新编辑时,先 `advance --stage implement` 再返回实现。
166
181
 
167
182
  ---
168
183
 
@@ -171,7 +186,7 @@ untracked helper 不记录 Check-All 证据。普通严格通过但尚未继续
171
186
  validated auto-loop 复用相同的画像、profile、`DOC-*` 通道和问题模型,但不展示普通模式的修复选择:
172
187
 
173
188
  - 有 `DOC-*` 且可自动修复:主会话先应用并验证;当前任务 `implement.md` / `brief.md` 的每个实际变化都追加精确 `--doc-remediation-file`,再决定最终 `ok|failed|blocked`。
174
- - 有剩余 `CHK-*` 或 `FBK-*`:向 runner `record --result failed --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含最高严重度、两类问题 ID、根因、受影响文件和已自动修复的 `DOC-*`。
189
+ - 有剩余 `CHK-*` 或 `FBK-*`:向 runner `record --result failed --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含最高严重度、两类问题 ID、根因、受影响文件和已自动修复的 `DOC-*`。validated auto-loop 不创建也不复用 interactive 风险接受。
175
190
  - 真正需要用户产品决策、越权、生产副作用或破坏性安全决策:使用同样深度字段 `record --result blocked`,随后按 runner 状态停止。
176
191
  - 无剩余 `CHK-*` 且无剩余 `FBK-*`:`record --result ok --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含自动修复数量;只有两类问题都为 0 才能进入通过路径。
177
192
  - record 成功后立即 `next`;若返回 `status=retryable reason=artifact-drift`,不得 `next`,先按 runner 指令在同一 outstanding action 内自纠并重录。validated auto-loop 不渲染交互式下一步段、不提示用户回复“继续”、不等待普通修复范围选择。
@@ -185,19 +200,19 @@ subagent 只返回结构化 `CHK-*`、`FBK-*`、`DOC-*` 候选、报告和 `chec
185
200
 
186
201
  非 validated auto-loop 先输出完整标准报告,再在本 Gate 内按以下顺序分流:
187
202
 
188
- 1. 只从触发本轮完成链的最新用户消息识别 direct Git intent:明确请求普通 push,或用户主动 `commit-only`。不得从历史消息、任务标题、摘要、dirty 状态或 auto-loop 内部 action 推断。
189
- 2. direct Git 只有在 Check-All 整体结论通过、剩余 `CHK-*` 和 `FBK-*` 均为 0、无阻塞、无部分验证、无待用户接受的实质剩余风险时才算严格通过。允许存在已成功验证的 `DOC-*` 自动修复;标准报告输出后,同一轮进入 Phase 3.3 `trellis-update-spec`;`no-op|written` 再由其加载 `trellis-push`,`needs-review` 停止。
190
- 3. 剩余 `CHK-*`、`FBK-*`、blocked、部分验证或实质剩余风险均不满足条件:输出标准报告并停止,不运行 Update-Spec,也不生成 Git 计划。原始 Git 请求不授权自动修复普通问题、忽略问题或扩大 Git 权限。
203
+ 1. 只从当前完成链证据识别 direct Git intent:触发检查的最新用户消息明确请求普通 push 或用户主动 `commit-only`;或者 Check-All 已因该 Git 请求报告并停止后,用户在当前报告上明确接受风险并要求继续。不得从任务标题、摘要、dirty 状态、无关历史或 auto-loop 内部 action 推断。
204
+ 2. direct Git 在以下任一条件成立时可以继续:Check-All strict pass;或全部剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受。两条路径还必须无阻塞、无部分验证、无未接受的实质剩余风险。允许存在已成功验证的 `DOC-*` 自动修复;标准报告输出后,同一轮进入 Phase 3.3 `trellis-update-spec`;`no-op|written` 再由其加载 `trellis-push`,`needs-review` 停止。
205
+ 3. 未处置 `CHK-*` / `FBK-*`、blocked、部分验证或未接受的实质剩余风险均不满足条件:输出标准报告并停止,不运行 Update-Spec,也不生成 Git 计划。原始 Git 请求不授权自动修复普通问题或替用户接受风险。
191
206
  4. 没有匹配 direct Git intent 的普通 interactive 检查保持原行为:报告后立即停止并等待用户选择。
192
207
 
193
208
  ### 交互式下一步引导
194
209
 
195
210
  所有 interactive 标准报告都必须在末尾输出 `### 下一步`,并按以下首个命中分支给出一个明确主动作:
196
211
 
197
- 1. 有剩余 `CHK-*` 或 `FBK-*`:提示用户回复 `修复全部`、混合精确问题 ID 或 `仅保留报告`;不得重复提出逐项确认。
212
+ 1. 有未处置 `CHK-*` 或 `FBK-*`:提示用户回复 `修复全部`、混合精确问题 ID、`接受风险 <问题 ID> 并继续` 或 `仅保留报告`;不得重复提出逐项确认。
198
213
  2. 有 blocked、部分验证或实质剩余风险:指出解除阻塞所需的精确决策、授权或验证,以及完成后重新运行 Check-All;涉及生产、外部系统或破坏性副作用时只引导用户授权,不自行执行。
199
- 3. direct Git 严格通过:说明本轮正在进入 `trellis-update-spec`,不要求用户再次回复“继续”或确认 Git 计划。
200
- 4. 无 direct Git intent 且严格通过:提示用户回复 `继续`,下一轮进入 `trellis-update-spec`,再由 `trellis-push` 生成提交计划。
214
+ 3. direct Git strict pass 或已接受风险通过:说明本轮正在进入 `trellis-update-spec`,不要求用户再次回复“继续”或确认 Git 计划。
215
+ 4. 无 direct Git intent 且 strict pass / 已接受风险通过:提示用户回复 `继续`,下一轮进入 `trellis-update-spec`,再由 `trellis-push` 生成提交计划。
201
216
 
202
217
  停止边界只控制是否自动推进,不能让报告在没有下一步提示的情况下结束。
203
218
 
@@ -209,6 +224,6 @@ subagent 只返回结构化 `CHK-*`、`FBK-*`、`DOC-*` 候选、报告和 `chec
209
224
  - 已执行验证及结果;
210
225
  - 未覆盖验证和剩余风险;
211
226
  - 总体结论;
212
- - 与当前结论匹配的唯一主动作引导;有 `CHK-*` 或 `FBK-*` 时是一次修复范围选择,部分验证/阻塞时是补充决策或验证,通过时是 Phase 3.3 / Phase 3.4 指向。
227
+ - 与当前结论匹配的唯一主动作引导;有未处置 `CHK-*` 或 `FBK-*` 时是一次修复或风险接受选择,部分验证/阻塞时是补充决策或验证,通过时是 Phase 3.3 / Phase 3.4 指向。
213
228
 
214
229
  Check-All 不新增 direct Git 专用摘要,也不得自行生成提交计划、commit message、拟提交文件或要求用户确认提交;strict pass 后的 Git 计划仍由 Update-Spec disposition 和 `trellis-push` owner 生成。
@@ -33,7 +33,7 @@ description: "按确认的精确文件范围提交普通变更或完成已就绪
33
33
 
34
34
  除 auto-loop 内部 `commit-only` 外,普通 push 或用户 `commit-only` 已经构成明确 Git 意图。本 skill 在读取 Git 提交计划前只记录当前可用的完成链证据,不补跑、不切换阶段,也不新增确认:
35
35
 
36
- - Check-All:根据当前标准报告与实际 diff 标记为 `通过`、`未运行`、`已失效`、`存在阻断 findings`、`blocked` 或 `部分验证`。只有剩余 `CHK-*` 与 `FBK-*` 均为 0 才能标记为 `通过`;没有可验证的当前报告时使用 `未运行`,不得从历史消息、摘要或 dirty 状态猜测通过。
36
+ - Check-All:根据当前标准报告与实际 diff 标记为 `通过`、`通过(已接受风险)`、`未运行`、`已失效`、`存在未处置 findings`、`blocked` 或 `部分验证`。剩余 `CHK-*` 与 `FBK-*` 均为 0 时标记为 `通过`;所有剩余问题都有当前有效的用户风险接受时标记为 `通过(已接受风险)`,并保留问题 ID 与严重度。没有可验证的当前报告时使用 `未运行`,不得从历史消息、摘要或 dirty 状态猜测通过或风险接受。
37
37
  - Update-Spec:根据当前 `spec_update_result` 与实际 diff 标记为 `no-op`、`written`、`needs-review`、`未运行` 或 `已失效`。结果缺失或无法证明仍适用于当前 diff 时使用 `未运行` / `已失效`。
38
38
 
39
39
  上述状态只进入 Step 3 的完成链证据与风险展示,不会阻止读取 Git 状态或生成提交计划。本步骤不得返回 Phase 2.2,不得加载 `trellis-check-all` 或 `trellis-update-spec`,也不得要求用户改写成“跳过检查后 push”。正常 workflow 的 Check-All -> Update-Spec -> Push 顺序仍由 Phase 2.2、Phase 3.3 和各自 owner 推进;`trellis-push` 不反向补做上游阶段。
@@ -136,7 +136,7 @@ auto-loop 内部 `commit-only` 也允许 retained dirty 存在,但每个生成
136
136
  顺序:<repo-a> [-> `<local generation command>`] -> <repo-b> [-> task progress]
137
137
 
138
138
  ### 完成链证据
139
- - Check-All:<通过 / 未运行 / 已失效 / 存在阻断 findings / blocked / 部分验证>
139
+ - Check-All:<通过 / 通过(已接受风险:CHK-001,FBK-002) / 未运行 / 已失效 / 存在未处置 findings / blocked / 部分验证>
140
140
  - Update-Spec:<no-op / written / needs-review / 未运行 / 已失效>
141
141
 
142
142
  ### 1. <repository-name>
@@ -176,7 +176,7 @@ Push:<执行 / 跳过(commit-only)>
176
176
  - 超过 8 个时按目录归组,最多 12 行;用户要求展开时展示同一 exact set。
177
177
  - 顶部仓库/commit/file 总数包含独立任务记录提交所在 Git root、该提交及其 exact files;任务记录文件使用相同的 8 文件展示阈值和展开规则。
178
178
  - 保留未提交的变更始终逐项标注 Git 状态;真正风险在独立“风险”区逐项展示。
179
- - 完成链证据始终显示当前状态,但不重复 Check-All 报告或 Spec review 正文;`未运行`、`已失效`、任一剩余 `CHK-*` / `FBK-*`、blocked、部分验证或 `needs-review` 同时计入风险区。
179
+ - 完成链证据始终显示当前状态,但不重复 Check-All 报告或 Spec review 正文;`未运行`、`已失效`、任一未处置 `CHK-*` / `FBK-*`、blocked、部分验证或 `needs-review` 同时计入风险区。已接受风险的问题也必须按 ID、严重度和影响进入风险区,但不得改标为阻断 finding。
180
180
  - 无活动 task、untracked 或 `commit-only` 时省略进度动作。
181
181
  - 不重复展示检查结果、规范复核、归档或其他阶段的详细信息。
182
182
  - 生成前无法确定的内容和增删行写“生成后计算”,不得填预测值。
@@ -22,4 +22,4 @@ The first dispatch line must be `Active task: <path>` for task work or `Untracke
22
22
 
23
23
  ## Return
24
24
 
25
- Return the complete Check-All report, `check_profile`, all `CHK-*` findings, all `FBK-*` findings, all `DOC-*` candidates, verification evidence, blocked checks, and residual risk. Any remaining `CHK-*` or `FBK-*` blocks strict pass. Do not output a commit or push plan.
25
+ Return the complete Check-All report, `check_profile`, all `CHK-*` findings, all `FBK-*` findings, all `DOC-*` candidates, verification evidence, blocked checks, and residual risk. Any remaining `CHK-*` or `FBK-*` blocks strict pass. The main session may separately record explicit user risk acceptance for current findings; do not infer, grant, or erase that acceptance yourself. Do not output a commit or push plan.
@@ -18,7 +18,7 @@ description: "统一 Check-All 入口:确认范围与运行上下文,按 req
18
18
  4. 先按根因性质和可达证据把发现分为主路径 `CHK-*`、兜底 `FBK-*` 与文档漂移 `DOC-*`,再为 `CHK-*` 和 `FBK-*` 分配 P0/P1/P2。
19
19
  5. 在最终报告前处理允许自动修复的文档漂移,并把修复内容展示在报告里。
20
20
  6. 根据 interactive / validated auto-loop 边界输出下一步或完成 runner `record + next`。
21
- 7. untracked helper 只保存流程游标:findings 或新编辑设回 `implement`;只有严格通过且 disposition 确认继续时才 `advance --stage spec`。
21
+ 7. untracked helper 只保存流程游标:未处置 findings 或新编辑设回 `implement`;严格通过或已接受风险通过且 disposition 确认继续时才 `advance --stage spec`。
22
22
 
23
23
  ---
24
24
 
@@ -42,7 +42,7 @@ description: "统一 Check-All 入口:确认范围与运行上下文,按 req
42
42
  1. **默认 audit-only collect-all**:可以读文件、搜索、运行无业务写入副作用的 lint、typecheck 和测试;普通代码、配置、测试、任务规格语义问题不得在检查阶段直接修复。
43
43
  2. **唯一自修例外**:低风险文档漂移进入 `DOC-*` 通道,按 `references/document-drift-auto-remediation.md` 的白名单、黑名单和写入时机处理。
44
44
  3. **分类先于严重度**:读取 `references/fallback-findings.md`;主路径错误和非兜底契约违背进入 `CHK-*`,fail-closed、异常输入、失败降级和防御性保护缺口进入 `FBK-*`。契约证据影响严重度,不改变兜底根因归属。
45
- 4. **修改前只确认一次**:除 `DOC-*` 自动修复外,全部检查结束后通过统一报告让用户选择 `CHK-*` / `FBK-*` 修复范围;`修复全部` 默认覆盖两类问题。
45
+ 4. **处置只确认一次**:除 `DOC-*` 自动修复外,全部检查结束后通过统一报告让用户选择 `CHK-*` / `FBK-*` 修复范围或明确接受当前风险;`修复全部` 默认覆盖两类问题,接受风险不得隐藏发现。
46
46
  5. **委托规则不改变边界**:复用 `trellis-check` 时只复用检查清单、验证方法和命令发现,忽略其中任何“直接修复”“失败后先修复”的指令。
47
47
  6. **真正阻塞才中途暂停**:只有业务规划冲突、后续验证前提失效、生产或外部副作用、破坏性操作风险时提前停止。
48
48
 
@@ -114,6 +114,7 @@ check_profile:
114
114
  - 三个维度状态;
115
115
  - 自动修复的 `DOC-*` 内容;
116
116
  - 剩余 `CHK-*` 主路径问题与 `FBK-*` 兜底问题;
117
+ - 每个剩余问题的未处置或已接受风险状态;
117
118
  - 已执行验证和未覆盖风险;
118
119
  - 与当前结论匹配的唯一下一步。
119
120
 
@@ -1,6 +1,6 @@
1
1
  # Fallback Findings
2
2
 
3
- 本文件定义 `CHK-*` 主路径问题与 `FBK-*` 兜底问题的根因边界。分类发生在严重度评估之前;分类完成后,两类问题都根据当前实际影响分配 P0/P1/P2,并进入同一个修复与严格通过门禁。
3
+ 本文件定义 `CHK-*` 主路径问题与 `FBK-*` 兜底问题的根因边界。分类发生在严重度评估之前;分类完成后,两类问题都根据当前实际影响分配 P0/P1/P2,并进入同一个修复、风险接受与通过门禁。
4
4
 
5
5
  ---
6
6
 
@@ -65,5 +65,9 @@
65
65
  - `CHK-*` 与 `FBK-*` 分别独立编号,当前修复/重检循环保留原 ID。
66
66
  - 两类问题都按实际影响分配 P0/P1/P2;严重度不改变通道归属。
67
67
  - 两类问题都是修复项,都被 `修复全部` 覆盖,也都可以通过混合精确 ID 修复,例如 `修复 CHK-001,FBK-002`。
68
- - 任何剩余 `CHK-*` 或 `FBK-*` 都阻断 strict pass,并让 interactive、untracked、auto-loop、direct Git、Update-Spec 与 Push 进入未通过或 fix/recheck 路径。
69
- - `仅保留报告` 可以显式停止修复,但不能把未解决问题改写成通过。
68
+ - 用户可以明确接受当前报告中任一 `CHK-*` 或 `FBK-*` 的风险而不修复;接受风险不改变问题通道、严重度、证据或建议,也不能把问题从报告中删除。
69
+ - 风险接受必须能唯一对应当前报告的问题 ID;当前报告只有一个候选时,“这个问题”“不管这个”等明确指代可以解析为该 ID。P0 必须逐项写出精确 ID,不接受“全部”式概括授权。
70
+ - 风险接受只适用于当前问题证据和实际 diff;受影响代码、契约、验证结果或问题证据变化后立即失效,必须重新检查和接受。
71
+ - `strict pass` 仍要求剩余 `CHK-*` 与 `FBK-*` 均为 0;若所有剩余问题都已被用户有效接受,且无阻塞、无部分验证、无未接受的实质风险,则使用“已接受风险通过”。两种通过都可以进入 interactive、untracked、direct Git、Update-Spec 与 Push 的继续路径。
72
+ - 只有未处置的 `CHK-*` 或 `FBK-*` 阻断交互完成链。validated auto-loop 不能代表用户接受风险,仍要求两类问题均为 0 才能 `record ok`。
73
+ - `仅保留报告` 只表示停止修复,不构成风险接受,也不能把未处置问题改写成通过。
@@ -131,4 +131,5 @@ Full 通过必须同时满足:
131
131
  - 所有适用 PRD / design / implement 条目已映射到实现或明确 `N/A`;
132
132
  - 所有触发的假设 Dimension 已完成源码或真实契约核对;
133
133
  - 项目规范、复用、依赖、同层一致性和验证命令已覆盖实际变更范围;
134
- - 无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险。
134
+ - strict pass:无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险;或
135
+ - 已接受风险通过:所有剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受,且无阻塞、无部分验证、无未接受的实质剩余风险。
@@ -70,6 +70,7 @@ Light 通过必须同时满足:
70
70
  - 所有受影响规划条目已核对;
71
71
  - 所有触发的假设维度状态为 `通过` 或合理的 `N/A`;
72
72
  - 定向验证已运行并通过,或明确说明无需运行的原因;
73
- - 无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险。
73
+ - strict pass:无 `CHK-*`、无 `FBK-*`、无阻塞、无部分验证、无实质剩余风险;或
74
+ - 已接受风险通过:所有剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受,且无阻塞、无部分验证、无未接受的实质剩余风险。
74
75
 
75
76
  存在未覆盖但不影响局部结论的内容时,必须在“未覆盖与风险”中说明。
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 统一问题模型
8
8
 
9
- 先按 `references/fallback-findings.md` 判定 `CHK-*` / `FBK-*`,再为两类问题分配 P0/P1/P2。分类表达根因性质,严重度描述当前实际影响;两者都不改变问题必须进入修复和 strict pass 门禁的处置规则。
9
+ 先按 `references/fallback-findings.md` 判定 `CHK-*` / `FBK-*`,再为两类问题分配 P0/P1/P2。分类表达根因性质,严重度描述当前实际影响;两者都不决定问题必须修复还是可以由用户接受风险。
10
10
 
11
11
  ### `CHK-*` 主路径问题
12
12
 
@@ -42,7 +42,19 @@
42
42
  | 位置 | 同一根因的全部受影响位置 |
43
43
  | 验证 | 修复后的测试、故障注入、命令或手动验证步骤 |
44
44
 
45
- `CHK-*` 与 `FBK-*` 分开编号。同一根因的多个位置合并到一个问题;报告按严重度排序,但不得因此重排已经分配的 ID。新根因使用对应通道的下一个 ID。
45
+ `CHK-*` 与 `FBK-*` 分开编号。同一根因的多个位置合并到一个问题;报告按严重度排序,但不得因此重排已经分配的 ID。新根因使用对应通道的下一个 ID。每个问题还必须标记 `未处置` 或 `已接受风险`;处置状态不改变 ID、通道和严重度。
46
+
47
+ ## 风险接受
48
+
49
+ 风险接受是用户对当前报告的显式处置,不是检查器对问题严重度的降级:
50
+
51
+ 1. 只有用户可以接受风险;主会话、subagent 和 validated auto-loop 都不得代替用户推断或授权。
52
+ 2. 接受必须唯一对应当前报告的问题 ID。用户可以写 `接受风险 CHK-001,FBK-002 并继续`;当前报告只有一个候选时,“这个问题”“不管这个”等明确指代可以解析为该 ID。P0 必须逐项写出精确 ID,不接受 `接受全部剩余风险`。
53
+ 3. 接受只绑定当前问题证据与实际 diff。受影响代码、契约、验证结果、问题内容或严重度变化后,原接受立即失效,问题恢复为 `未处置`。
54
+ 4. 已接受问题继续完整展示证据、影响、建议和验证,并标记 `处置:已接受风险`;不得删除、改列 `DOC-*` 或伪报已修复。
55
+ 5. `strict pass` 只用于剩余 `CHK-*` / `FBK-*` 均为 0。所有剩余问题均已被有效接受,且无 blocked、无部分验证、无未接受的实质剩余风险时,结论为 `通过·已接受风险`。
56
+ 6. blocked、部分验证和无法唯一对应到当前问题 ID 的实质剩余风险不是 `CHK-*` / `FBK-*` 处置状态,不能借风险接受绕过。
57
+ 7. `仅保留报告` 表示停止处置并等待,不等于接受风险;只有带明确接受语义的用户回复才改变问题处置状态。
46
58
 
47
59
  ---
48
60
 
@@ -53,7 +65,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
53
65
  ```markdown
54
66
  ## Trellis Check-All 结果
55
67
 
56
- [<通过/未通过/阻塞>] <N> 个维度 · CHK <N> · FBK <N> · 自动修复 DOC <N> · P0 <N> / P1 <N> / P2 <N> · 验证 <通过>/<总数>
68
+ [<通过/通过·已接受风险/未通过/阻塞>] <N> 个维度 · CHK <N>(接受 <N>)· FBK <N>(接受 <N>)· 自动修复 DOC <N> · P0 <N> / P1 <N> / P2 <N> · 验证 <通过>/<总数>
57
69
 
58
70
  工作:<任务名称 | Untracked work: work-id | 无活动工作>
59
71
  范围:<文件数与层级摘要;包含自动修复产生的文档 diff>
@@ -77,6 +89,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
77
89
  ### 主路径问题
78
90
 
79
91
  - [ ] `CHK-001` `[P1]` <标题>
92
+ - 处置:<未处置/已接受风险>
80
93
  - 来源:<来源>
81
94
  - 证据:<file:line / 契约 / 命令结果>
82
95
  - 影响:<影响>
@@ -87,6 +100,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
87
100
  ### 兜底问题
88
101
 
89
102
  - [ ] `FBK-001` `[P1]` <标题>
103
+ - 处置:<未处置/已接受风险>
90
104
  - 来源:<来源>
91
105
  - 证据:<file:line / 契约 / 命令结果>
92
106
  - 兜底场景:<可达异常或失败场景>
@@ -105,7 +119,7 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
105
119
  批次 1:<CHK/FBK 问题 ID> · <修复目标>
106
120
  修复后:定向验证 -> Check-All 重检
107
121
 
108
- 操作:`修复全部`、`修复 CHK-001,FBK-002`、`仅保留报告`
122
+ 操作:`修复全部`、`修复 CHK-001,FBK-002`、`接受风险 CHK-001,FBK-002 并继续`、`仅保留报告`
109
123
 
110
124
  ### 下一步
111
125
 
@@ -116,9 +130,10 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
116
130
 
117
131
  - 没有 `DOC-*` 自动修复时省略“自动修复”区。
118
132
  - 没有 `CHK-*` 时省略“主路径问题”区;没有 `FBK-*` 时省略“兜底问题”区。
119
- - `CHK-*` 或 `FBK-*` 任一存在时展示“修复批次”,并只在报告末尾提供一次修复范围选择,不再逐项提问。
133
+ - 存在未处置 `CHK-*` 或 `FBK-*` 时展示“修复批次”,并只在报告末尾提供一次处置选择,不再逐项提问。
120
134
  - `修复全部` 始终覆盖全部 `CHK-*` 与 `FBK-*`;精确修复可以混合两类 ID。
121
- - `仅保留报告` 只表示停止修复,不改变未通过结论或剩余风险。
135
+ - 风险接受可以混合两类 ID;只有全部剩余问题都已有效接受时才形成“通过·已接受风险”。
136
+ - `仅保留报告` 只表示停止处置,不改变未通过结论或剩余风险。
122
137
  - interactive 标准报告必须以“下一步”段结束;停止等待不等于省略引导。
123
138
  - 独立 `CHK-*` 或 `FBK-*` 不得因数量多而静默省略;先合并同根因重复项,再完整列出剩余项。
124
139
  - 报告不得包含 commit message、拟提交/暂存文件、commit-only 决策或提交确认。
@@ -160,9 +175,9 @@ interactive 模式完成所有可继续检查和允许的 `DOC-*` 自动修复
160
175
  <按下方 `Interactive Post-Check Stop Gate` 输出一个明确、可执行的主动作>
161
176
  ```
162
177
 
163
- 检查通过后的动作由下方 `Interactive Post-Check Stop Gate` 判断:普通交互停止等待,符合 direct Git 严格通过条件时同轮进入 Phase 3.3 `trellis-update-spec`,再到 Phase 3.4 `trellis-push`。仍有 `CHK-*` 或 `FBK-*` 时停留在修复/重检循环。
178
+ 检查通过后的动作由下方 `Interactive Post-Check Stop Gate` 判断:普通交互停止等待,符合 direct Git strict pass 或已接受风险通过条件时同轮进入 Phase 3.3 `trellis-update-spec`,再到 Phase 3.4 `trellis-push`。仍有未处置 `CHK-*` 或 `FBK-*` 时停留在处置/重检循环。
164
179
 
165
- untracked helper 不记录 Check-All 证据。普通严格通过但尚未继续时保持 `stage=check`;只有 direct Git 同轮继续或用户后续明确继续时才 `advance --stage spec`。有剩余 `CHK-*`、`FBK-*`、部分验证、阻塞或报告后的新编辑时,先 `advance --stage implement` 再返回实现。
180
+ untracked helper 不记录 Check-All 证据或风险接受。普通 strict pass / 已接受风险通过但尚未继续时保持 `stage=check`;只有 direct Git 同轮继续或用户后续明确继续时才 `advance --stage spec`。有未处置 `CHK-*`、`FBK-*`、部分验证、阻塞或报告后的新编辑时,先 `advance --stage implement` 再返回实现。
166
181
 
167
182
  ---
168
183
 
@@ -171,7 +186,7 @@ untracked helper 不记录 Check-All 证据。普通严格通过但尚未继续
171
186
  validated auto-loop 复用相同的画像、profile、`DOC-*` 通道和问题模型,但不展示普通模式的修复选择:
172
187
 
173
188
  - 有 `DOC-*` 且可自动修复:主会话先应用并验证;当前任务 `implement.md` / `brief.md` 的每个实际变化都追加精确 `--doc-remediation-file`,再决定最终 `ok|failed|blocked`。
174
- - 有剩余 `CHK-*` 或 `FBK-*`:向 runner `record --result failed --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含最高严重度、两类问题 ID、根因、受影响文件和已自动修复的 `DOC-*`。
189
+ - 有剩余 `CHK-*` 或 `FBK-*`:向 runner `record --result failed --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含最高严重度、两类问题 ID、根因、受影响文件和已自动修复的 `DOC-*`。validated auto-loop 不创建也不复用 interactive 风险接受。
175
190
  - 真正需要用户产品决策、越权、生产副作用或破坏性安全决策:使用同样深度字段 `record --result blocked`,随后按 runner 状态停止。
176
191
  - 无剩余 `CHK-*` 且无剩余 `FBK-*`:`record --result ok --effective-check-depth <light|full> --check-depth-reason <summary>`,摘要包含自动修复数量;只有两类问题都为 0 才能进入通过路径。
177
192
  - record 成功后立即 `next`;若返回 `status=retryable reason=artifact-drift`,不得 `next`,先按 runner 指令在同一 outstanding action 内自纠并重录。validated auto-loop 不渲染交互式下一步段、不提示用户回复“继续”、不等待普通修复范围选择。
@@ -185,19 +200,19 @@ subagent 只返回结构化 `CHK-*`、`FBK-*`、`DOC-*` 候选、报告和 `chec
185
200
 
186
201
  非 validated auto-loop 先输出完整标准报告,再在本 Gate 内按以下顺序分流:
187
202
 
188
- 1. 只从触发本轮完成链的最新用户消息识别 direct Git intent:明确请求普通 push,或用户主动 `commit-only`。不得从历史消息、任务标题、摘要、dirty 状态或 auto-loop 内部 action 推断。
189
- 2. direct Git 只有在 Check-All 整体结论通过、剩余 `CHK-*` 和 `FBK-*` 均为 0、无阻塞、无部分验证、无待用户接受的实质剩余风险时才算严格通过。允许存在已成功验证的 `DOC-*` 自动修复;标准报告输出后,同一轮进入 Phase 3.3 `trellis-update-spec`;`no-op|written` 再由其加载 `trellis-push`,`needs-review` 停止。
190
- 3. 剩余 `CHK-*`、`FBK-*`、blocked、部分验证或实质剩余风险均不满足条件:输出标准报告并停止,不运行 Update-Spec,也不生成 Git 计划。原始 Git 请求不授权自动修复普通问题、忽略问题或扩大 Git 权限。
203
+ 1. 只从当前完成链证据识别 direct Git intent:触发检查的最新用户消息明确请求普通 push 或用户主动 `commit-only`;或者 Check-All 已因该 Git 请求报告并停止后,用户在当前报告上明确接受风险并要求继续。不得从任务标题、摘要、dirty 状态、无关历史或 auto-loop 内部 action 推断。
204
+ 2. direct Git 在以下任一条件成立时可以继续:Check-All strict pass;或全部剩余 `CHK-*` / `FBK-*` 都有当前有效的用户风险接受。两条路径还必须无阻塞、无部分验证、无未接受的实质剩余风险。允许存在已成功验证的 `DOC-*` 自动修复;标准报告输出后,同一轮进入 Phase 3.3 `trellis-update-spec`;`no-op|written` 再由其加载 `trellis-push`,`needs-review` 停止。
205
+ 3. 未处置 `CHK-*` / `FBK-*`、blocked、部分验证或未接受的实质剩余风险均不满足条件:输出标准报告并停止,不运行 Update-Spec,也不生成 Git 计划。原始 Git 请求不授权自动修复普通问题或替用户接受风险。
191
206
  4. 没有匹配 direct Git intent 的普通 interactive 检查保持原行为:报告后立即停止并等待用户选择。
192
207
 
193
208
  ### 交互式下一步引导
194
209
 
195
210
  所有 interactive 标准报告都必须在末尾输出 `### 下一步`,并按以下首个命中分支给出一个明确主动作:
196
211
 
197
- 1. 有剩余 `CHK-*` 或 `FBK-*`:提示用户回复 `修复全部`、混合精确问题 ID 或 `仅保留报告`;不得重复提出逐项确认。
212
+ 1. 有未处置 `CHK-*` 或 `FBK-*`:提示用户回复 `修复全部`、混合精确问题 ID、`接受风险 <问题 ID> 并继续` 或 `仅保留报告`;不得重复提出逐项确认。
198
213
  2. 有 blocked、部分验证或实质剩余风险:指出解除阻塞所需的精确决策、授权或验证,以及完成后重新运行 Check-All;涉及生产、外部系统或破坏性副作用时只引导用户授权,不自行执行。
199
- 3. direct Git 严格通过:说明本轮正在进入 `trellis-update-spec`,不要求用户再次回复“继续”或确认 Git 计划。
200
- 4. 无 direct Git intent 且严格通过:提示用户回复 `继续`,下一轮进入 `trellis-update-spec`,再由 `trellis-push` 生成提交计划。
214
+ 3. direct Git strict pass 或已接受风险通过:说明本轮正在进入 `trellis-update-spec`,不要求用户再次回复“继续”或确认 Git 计划。
215
+ 4. 无 direct Git intent 且 strict pass / 已接受风险通过:提示用户回复 `继续`,下一轮进入 `trellis-update-spec`,再由 `trellis-push` 生成提交计划。
201
216
 
202
217
  停止边界只控制是否自动推进,不能让报告在没有下一步提示的情况下结束。
203
218
 
@@ -209,6 +224,6 @@ subagent 只返回结构化 `CHK-*`、`FBK-*`、`DOC-*` 候选、报告和 `chec
209
224
  - 已执行验证及结果;
210
225
  - 未覆盖验证和剩余风险;
211
226
  - 总体结论;
212
- - 与当前结论匹配的唯一主动作引导;有 `CHK-*` 或 `FBK-*` 时是一次修复范围选择,部分验证/阻塞时是补充决策或验证,通过时是 Phase 3.3 / Phase 3.4 指向。
227
+ - 与当前结论匹配的唯一主动作引导;有未处置 `CHK-*` 或 `FBK-*` 时是一次修复或风险接受选择,部分验证/阻塞时是补充决策或验证,通过时是 Phase 3.3 / Phase 3.4 指向。
213
228
 
214
229
  Check-All 不新增 direct Git 专用摘要,也不得自行生成提交计划、commit message、拟提交文件或要求用户确认提交;strict pass 后的 Git 计划仍由 Update-Spec disposition 和 `trellis-push` owner 生成。
@@ -33,7 +33,7 @@ description: "按确认的精确文件范围提交普通变更或完成已就绪
33
33
 
34
34
  除 auto-loop 内部 `commit-only` 外,普通 push 或用户 `commit-only` 已经构成明确 Git 意图。本 skill 在读取 Git 提交计划前只记录当前可用的完成链证据,不补跑、不切换阶段,也不新增确认:
35
35
 
36
- - Check-All:根据当前标准报告与实际 diff 标记为 `通过`、`未运行`、`已失效`、`存在阻断 findings`、`blocked` 或 `部分验证`。只有剩余 `CHK-*` 与 `FBK-*` 均为 0 才能标记为 `通过`;没有可验证的当前报告时使用 `未运行`,不得从历史消息、摘要或 dirty 状态猜测通过。
36
+ - Check-All:根据当前标准报告与实际 diff 标记为 `通过`、`通过(已接受风险)`、`未运行`、`已失效`、`存在未处置 findings`、`blocked` 或 `部分验证`。剩余 `CHK-*` 与 `FBK-*` 均为 0 时标记为 `通过`;所有剩余问题都有当前有效的用户风险接受时标记为 `通过(已接受风险)`,并保留问题 ID 与严重度。没有可验证的当前报告时使用 `未运行`,不得从历史消息、摘要或 dirty 状态猜测通过或风险接受。
37
37
  - Update-Spec:根据当前 `spec_update_result` 与实际 diff 标记为 `no-op`、`written`、`needs-review`、`未运行` 或 `已失效`。结果缺失或无法证明仍适用于当前 diff 时使用 `未运行` / `已失效`。
38
38
 
39
39
  上述状态只进入 Step 3 的完成链证据与风险展示,不会阻止读取 Git 状态或生成提交计划。本步骤不得返回 Phase 2.2,不得加载 `trellis-check-all` 或 `trellis-update-spec`,也不得要求用户改写成“跳过检查后 push”。正常 workflow 的 Check-All -> Update-Spec -> Push 顺序仍由 Phase 2.2、Phase 3.3 和各自 owner 推进;`trellis-push` 不反向补做上游阶段。
@@ -136,7 +136,7 @@ auto-loop 内部 `commit-only` 也允许 retained dirty 存在,但每个生成
136
136
  顺序:<repo-a> [-> `<local generation command>`] -> <repo-b> [-> task progress]
137
137
 
138
138
  ### 完成链证据
139
- - Check-All:<通过 / 未运行 / 已失效 / 存在阻断 findings / blocked / 部分验证>
139
+ - Check-All:<通过 / 通过(已接受风险:CHK-001,FBK-002) / 未运行 / 已失效 / 存在未处置 findings / blocked / 部分验证>
140
140
  - Update-Spec:<no-op / written / needs-review / 未运行 / 已失效>
141
141
 
142
142
  ### 1. <repository-name>
@@ -176,7 +176,7 @@ Push:<执行 / 跳过(commit-only)>
176
176
  - 超过 8 个时按目录归组,最多 12 行;用户要求展开时展示同一 exact set。
177
177
  - 顶部仓库/commit/file 总数包含独立任务记录提交所在 Git root、该提交及其 exact files;任务记录文件使用相同的 8 文件展示阈值和展开规则。
178
178
  - 保留未提交的变更始终逐项标注 Git 状态;真正风险在独立“风险”区逐项展示。
179
- - 完成链证据始终显示当前状态,但不重复 Check-All 报告或 Spec review 正文;`未运行`、`已失效`、任一剩余 `CHK-*` / `FBK-*`、blocked、部分验证或 `needs-review` 同时计入风险区。
179
+ - 完成链证据始终显示当前状态,但不重复 Check-All 报告或 Spec review 正文;`未运行`、`已失效`、任一未处置 `CHK-*` / `FBK-*`、blocked、部分验证或 `needs-review` 同时计入风险区。已接受风险的问题也必须按 ID、严重度和影响进入风险区,但不得改标为阻断 finding。
180
180
  - 无活动 task、untracked 或 `commit-only` 时省略进度动作。
181
181
  - 不重复展示检查结果、规范复核、归档或其他阶段的详细信息。
182
182
  - 生成前无法确定的内容和增删行写“生成后计算”,不得填预测值。
@@ -8,6 +8,6 @@ Before interactive Check-All begins, run `python3 ./.trellis/scripts/pre_check_s
8
8
 
9
9
  Check-All selects light/full depth from intent, actual diff, risk, and runtime context. It is audit-only and collect-all by default: classify main-path issues as `CHK-*`, fallback-path issues as `FBK-*`, and low-risk task-document drift as `DOC-*`. Assign P0/P1/P2 to both `CHK-*` and `FBK-*` after root-cause classification; severity does not choose the channel or make either channel optional. Report all items and stop before code changes only when `CHK-*` / `FBK-*` repair scope needs confirmation. The only write exception is low-risk `DOC-*` auto-remediation owned by Check-All and shown in the final report, unless a validated auto-loop owns the continuation.
10
10
 
11
- The existing `Interactive Post-Check Stop Gate` owns one narrow direct Git exception. Only when the latest user message that triggered this completion chain explicitly requested an ordinary push or user-initiated `commit-only`, and Check-All strictly passes with zero remaining `CHK-*` and zero remaining `FBK-*` findings, no blocker, no partial verification, and no material residual risk requiring user acceptance, show the existing standard report and continue in the same turn to Phase 3.3 `trellis-update-spec`. Any remaining finding, blocker, partial verification, or material residual risk reports and stops. Ordinary interactive checks still report and stop; Check-All never creates the Git plan itself.
11
+ The existing `Interactive Post-Check Stop Gate` owns one narrow direct Git exception. Continue when the current completion-chain evidence contains an ordinary push or user-initiated `commit-only` intent and Check-All either strictly passes with zero remaining `CHK-*` / `FBK-*`, or every remaining finding has current explicit user risk acceptance. Both paths require no blocker, no partial verification, and no unaccepted material residual risk. A user reply that explicitly accepts findings from the current stopped report and asks to continue may resume that pending direct Git chain; do not infer intent or acceptance from unrelated history, summaries, or dirty state. Keep accepted findings visible in the standard report and Push risk evidence. Any unaccepted finding, blocker, partial verification, or unaccepted material residual risk reports and stops. Ordinary interactive checks still report and stop; Check-All never creates the Git plan itself.
12
12
 
13
13
  After authorized repairs, return through the same route and re-run Check-All. The final pre-commit pass must cover the whole task and cannot be downgraded to light.
@@ -6,7 +6,7 @@ A different implementation request remains blocked by the single-active-work gua
6
6
 
7
7
  [workflow-state:untracked_check]
8
8
  The current-session untracked work item is at `check`. Run `python3 ./.trellis/scripts/untracked_flow.py status`, then enter Phase 2.2 through `trellis-route(target=check)` and execute `trellis-check-all`.
9
- If Check-All reports findings or a new edit is needed, run `python3 ./.trellis/scripts/untracked_flow.py advance --stage implement` before returning to implementation. On a strict pass, keep `check` while the interactive stop gate waits; only a same-turn direct Git continuation or a later explicit continuation advances to `spec`.
9
+ If Check-All reports unaccepted findings or a new edit is needed, run `python3 ./.trellis/scripts/untracked_flow.py advance --stage implement` before returning to implementation. On a strict pass or accepted-risk pass, keep `check` while the interactive stop gate waits; only a same-turn direct Git continuation or a later explicit continuation advances to `spec`. The helper does not persist finding details or risk acceptance.
10
10
  [/workflow-state:untracked_check]
11
11
 
12
12
  [workflow-state:untracked_spec]
@@ -2,5 +2,5 @@ Before the first implement route, restate `<task>/brief.md`; if it is missing, r
2
2
  Before routing or editing, apply the `Request Triage` Active Task Scope Guard. New implementation work outside the active task title/brief stops here until the user chooses a new task, updates this task's artifacts first, or explicitly proceeds untracked without reusing its progress.
3
3
  Enter Phase 2.1/2.2 through the target-matched `trellis-route`; a user route override wins over remembered evidence.
4
4
  After implementation and focused validation, return to the Phase 2.1 completion contract and resolve its Pre-Check action before ending the turn; the full hold/default policy remains owned by Phase 2.1.
5
- After Check-All, follow the `Interactive Post-Check Stop Gate`: a validated auto-loop immediately records and advances, a matching direct Git strict pass may continue to `trellis-update-spec`, and every other interactive result reports and stops. A later interactive next/continue runs `trellis-update-spec`; downstream disposition remains owned by Update-Spec and `trellis-push`.
5
+ After Check-All, follow the `Interactive Post-Check Stop Gate`: a validated auto-loop immediately records and advances, a matching direct Git strict pass or accepted-risk pass may continue to `trellis-update-spec`, and every other interactive result reports and stops. A later interactive next/continue, including explicit acceptance of the current report's findings followed by continue, runs `trellis-update-spec`; downstream disposition remains owned by Update-Spec and `trellis-push`.
6
6
  Run `/trellis:finish-work` only when explicitly requested after Phase 3.4 completes.
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-08-05T23:17:59.095Z",
2
+ "syncedAt": "2026-08-06T05:34:52.463Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "11ba49652bff38da5f3fe9366e8696d919908d87",
4
+ "sourceCommit": "ff6502fdecdab6f47b62587ae0da233a47268872",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "aliyun-ops",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.6.0-beta.6",
3
+ "version": "0.6.0-beta.7",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,9 +68,9 @@
68
68
  "commit-and-tag-version": "^12.7.3"
69
69
  },
70
70
  "flowerReleaseNotes": {
71
- "version": "0.6.0-beta.6",
71
+ "version": "0.6.0-beta.7",
72
72
  "source": "CHANGELOG.md",
73
- "body": "### ✨ 新功能 Features\n\n* **skill-garden:** 将 DMS、SLS、MSE/Nacos 整合为阿里云运维查询 Skill ([44ba195](https://github.com/SilentFlower/flower-trellis/commit/44ba195a5cbc8943fe0f38bbcff94ae4d940791e))\n* **skill:** 集成阿里云 DMS 查询 Skill ([a582d1c](https://github.com/SilentFlower/flower-trellis/commit/a582d1cc45ea38b8288b75bd4511f378eb5997a2))\n* **trellis:** 将 Check-All 可选问题收敛为统一兜底分类 ([eda90ec](https://github.com/SilentFlower/flower-trellis/commit/eda90ecc3fd3e92317cb15f054f837900fb2467e))\n* **trellis:** 新增 Check-All 可选问题分类 ([839904e](https://github.com/SilentFlower/flower-trellis/commit/839904e3d584f3f71190d95fb623612673f9b210))\n* **trellis:** 新增 Trellis 项目级关闭与恢复能力 ([a27b5c2](https://github.com/SilentFlower/flower-trellis/commit/a27b5c2966a29f394d1dc98341e4c3be5ac7a030))\n* **worktree:** 让 Trellis worktree 状态按分支隔离 ([dde2fbb](https://github.com/SilentFlower/flower-trellis/commit/dde2fbb792714f326d9f48a85e055229eb4bd4bb))",
73
+ "body": "### ✨ 新功能 Features\n\n* **trellis:** 支持接受 Check-All 检查风险 ([589dd9a](https://github.com/SilentFlower/flower-trellis/commit/589dd9ad14e0755ea47da46345f14567b1632285))\n\n\n### 🐛 修复 Bug Fixes\n\n* **plugin:** 修复 GitLab 私有来源安装 ([4ba3cf6](https://github.com/SilentFlower/flower-trellis/commit/4ba3cf610f8fef99ef0297654d1859510be8f07e))",
74
74
  "truncated": false
75
75
  },
76
76
  "optionalDependencies": {
@@ -4,9 +4,9 @@
4
4
  "type": "gitlab",
5
5
  "name": "研发指南",
6
6
  "enabled": true,
7
- "baseUrl": "http://gitlab.xhgjdev.com",
7
+ "baseUrl": "https://gitlab.xhgjdev.com",
8
8
  "project": "digital-rd-governance/rd-guide",
9
- "ref": "main",
9
+ "ref": "feat/flower-plugin-distribution",
10
10
  "marketplacePath": ".flower-marketplace/marketplace.json",
11
11
  "oauth": {
12
12
  "applicationId": "0f73e53d745450b6ab9596960b10a2ac1654d67c0941bae381f6dbbf6839ec04",
@@ -957,7 +957,7 @@ async function manageSource(context, sourceId) {
957
957
  section: "来源设置",
958
958
  icon: source.enabled ? "○" : "●",
959
959
  });
960
- choices.push({
960
+ if (!source.builtin) choices.push({
961
961
  name: "编辑来源",
962
962
  value: "edit",
963
963
  description: source.type === "github"
@@ -17,7 +17,7 @@ export class GitLabRestClient {
17
17
  /**
18
18
  * 创建 REST 客户端。
19
19
  *
20
- * @param {{source:object,credentialManager:object,fetch?:typeof fetch,timeoutMs?:number,maxArchiveBytes?:number}} options 依赖
20
+ * @param {{source:object,credentialManager:object,fetch?:typeof fetch,timeoutMs?:number,maxArchiveBytes?:number,maxFileBytes?:number,maxTreeEntries?:number}} options 依赖
21
21
  */
22
22
  constructor(options) {
23
23
  this.source = options.source;
@@ -25,6 +25,8 @@ export class GitLabRestClient {
25
25
  this.fetch = options.fetch || globalThis.fetch;
26
26
  this.timeoutMs = options.timeoutMs || 30_000;
27
27
  this.maxArchiveBytes = options.maxArchiveBytes || 100 * 1024 * 1024;
28
+ this.maxFileBytes = options.maxFileBytes || 25 * 1024 * 1024;
29
+ this.maxTreeEntries = options.maxTreeEntries || 10_000;
28
30
  }
29
31
 
30
32
  /**
@@ -53,11 +55,24 @@ export class GitLabRestClient {
53
55
  * @returns {Promise<string>} 文件内容
54
56
  */
55
57
  async readRawFile(project, filePath, ref) {
56
- const buffer = await this.getBuffer(
58
+ const buffer = await this.readRawBuffer(project, filePath, ref);
59
+ return buffer.toString("utf8");
60
+ }
61
+
62
+ /**
63
+ * 读取仓库文件原始字节。
64
+ *
65
+ * @param {string} project GitLab project path
66
+ * @param {string} filePath 仓库内路径
67
+ * @param {string} ref 固定 ref
68
+ * @returns {Promise<Buffer>} 文件原始字节
69
+ */
70
+ async readRawBuffer(project, filePath, ref) {
71
+ return this.getBuffer(
57
72
  `/projects/${encodeGitLabProject(project)}/repository/files/${encodeURIComponent(filePath)}/raw`,
58
73
  { ref },
74
+ { maxBytes: this.maxFileBytes, limitLabel: "GitLab repository 文件超过大小限制" },
59
75
  );
60
- return buffer.toString("utf8");
61
76
  }
62
77
 
63
78
  /**
@@ -76,6 +91,40 @@ export class GitLabRestClient {
76
91
  return payload;
77
92
  }
78
93
 
94
+ /**
95
+ * 递归读取固定 ref 下的完整 repository tree,并处理 GitLab 分页。
96
+ *
97
+ * @param {string} project GitLab project path
98
+ * @param {{path?:string,ref:string}} options 查询参数
99
+ * @returns {Promise<object[]>} 全部 tree 条目
100
+ */
101
+ async readRepositoryTree(project, options) {
102
+ const pathname = `/projects/${encodeGitLabProject(project)}/repository/tree`;
103
+ const entries = [];
104
+ let page = 1;
105
+ while (true) {
106
+ const response = await this.#request(pathname, {
107
+ ref: options.ref,
108
+ recursive: "true",
109
+ per_page: "100",
110
+ page: String(page),
111
+ ...(options.path ? { path: options.path } : {}),
112
+ });
113
+ const payload = await this.#readJsonResponse(response, project);
114
+ if (!Array.isArray(payload)) throw this.#remoteError("GitLab tree 响应无效", project);
115
+ entries.push(...payload);
116
+ if (entries.length > this.maxTreeEntries) {
117
+ throw this.#remoteError("GitLab repository tree 超过条目限制", project);
118
+ }
119
+ const nextPage = response.headers.get("x-next-page");
120
+ if (!nextPage) return entries;
121
+ if (!/^\d+$/.test(nextPage) || Number(nextPage) <= page) {
122
+ throw this.#remoteError("GitLab tree 分页响应无效", project);
123
+ }
124
+ page = Number(nextPage);
125
+ }
126
+ }
127
+
79
128
  /**
80
129
  * 下载固定 commit 的仓库归档。
81
130
  *
@@ -100,10 +149,15 @@ export class GitLabRestClient {
100
149
  */
101
150
  async getJson(pathname, query = {}) {
102
151
  const response = await this.#request(pathname, query);
152
+ return this.#readJsonResponse(response, this.source.id);
153
+ }
154
+
155
+ /** @param {Response} response @param {string} diagnosticPath @returns {Promise<any>} */
156
+ async #readJsonResponse(response, diagnosticPath) {
103
157
  try {
104
158
  return await response.json();
105
159
  } catch (error) {
106
- throw this.#remoteError("GitLab 返回了无效 JSON", this.source.id, error);
160
+ throw this.#remoteError("GitLab 返回了无效 JSON", diagnosticPath, error);
107
161
  }
108
162
  }
109
163
 
@@ -112,18 +166,20 @@ export class GitLabRestClient {
112
166
  *
113
167
  * @param {string} pathname API v4 相对路径
114
168
  * @param {Record<string,string>} [query] 查询参数
115
- * @param {{archive?:boolean}} [options] 响应限制
169
+ * @param {{archive?:boolean,maxBytes?:number,limitLabel?:string}} [options] 响应限制
116
170
  * @returns {Promise<Buffer>} 响应字节
117
171
  */
118
172
  async getBuffer(pathname, query = {}, options = {}) {
119
173
  const response = await this.#request(pathname, query);
120
174
  const contentLength = Number(response.headers.get("content-length"));
121
- if (options.archive && Number.isFinite(contentLength) && contentLength > this.maxArchiveBytes) {
122
- throw this.#remoteError("GitLab archive 超过大小限制", this.source.id);
175
+ const maxBytes = options.maxBytes || (options.archive ? this.maxArchiveBytes : null);
176
+ const limitLabel = options.limitLabel || "GitLab archive 超过大小限制";
177
+ if (maxBytes && Number.isFinite(contentLength) && contentLength > maxBytes) {
178
+ throw this.#remoteError(limitLabel, this.source.id);
123
179
  }
124
180
  const buffer = Buffer.from(await response.arrayBuffer());
125
- if (options.archive && buffer.length > this.maxArchiveBytes) {
126
- throw this.#remoteError("GitLab archive 超过大小限制", this.source.id);
181
+ if (maxBytes && buffer.length > maxBytes) {
182
+ throw this.#remoteError(limitLabel, this.source.id);
127
183
  }
128
184
  return buffer;
129
185
  }
@@ -140,11 +196,30 @@ export class GitLabRestClient {
140
196
  try {
141
197
  const response = await this.fetch(url, {
142
198
  headers: { authorization: `Bearer ${token}` },
199
+ redirect: "manual",
143
200
  signal: controller.signal,
144
201
  });
145
202
  if (response.ok) return response;
146
203
  if (attempt === 0 && response.status >= 500) continue;
147
- throw this.#remoteError(`GitLab REST 请求失败:${response.status}`, this.source.id);
204
+ const location = response.headers.get("location");
205
+ if (response.status >= 300 && response.status < 400) {
206
+ let locationOrigin = null;
207
+ try {
208
+ locationOrigin = location ? new URL(location, url).origin : null;
209
+ } catch {
210
+ // 非法 Location 仍按重定向错误报告,不能恢复携带凭据的自动跳转。
211
+ }
212
+ throw this.#remoteError(
213
+ `GitLab REST 请求发生重定向:${response.status},请检查 source baseUrl 是否应使用 HTTPS`,
214
+ this.source.id,
215
+ undefined,
216
+ { status: response.status, endpoint: pathname, locationOrigin },
217
+ );
218
+ }
219
+ throw this.#remoteError(`GitLab REST 请求失败:${response.status}`, this.source.id, undefined, {
220
+ status: response.status,
221
+ endpoint: pathname,
222
+ });
148
223
  } catch (error) {
149
224
  lastError = error;
150
225
  if (error instanceof PluginRuntimeError || attempt > 0) break;
@@ -156,12 +231,13 @@ export class GitLabRestClient {
156
231
  throw this.#remoteError("GitLab REST 请求失败", this.source.id, lastError);
157
232
  }
158
233
 
159
- /** @param {string} message @param {string} diagnosticPath @param {unknown} [cause] @returns {PluginRuntimeError} */
160
- #remoteError(message, diagnosticPath, cause) {
234
+ /** @param {string} message @param {string} diagnosticPath @param {unknown} [cause] @param {object} [details] @returns {PluginRuntimeError} */
235
+ #remoteError(message, diagnosticPath, cause, details = {}) {
161
236
  return new PluginRuntimeError(message, {
162
237
  code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_REQUEST_FAILED,
163
238
  path: diagnosticPath,
164
239
  cause,
240
+ details,
165
241
  });
166
242
  }
167
243
  }
@@ -6,10 +6,18 @@ import { hashCanonicalTree } from "../integrity/canonical-tree.js";
6
6
  import { PLUGIN_RUNTIME_ERROR_CODES, PluginRuntimeError } from "../runtime-errors.js";
7
7
  import { validateMarketplaceManifest } from "../schemas/marketplace-manifest.js";
8
8
  import { validatePluginManifest } from "../schemas/plugin-manifest.js";
9
- import { composeCanonicalPluginId, parseCanonicalPluginId } from "../schemas/shared.js";
9
+ import {
10
+ assertSafePosixRelativePath,
11
+ composeCanonicalPluginId,
12
+ parseCanonicalPluginId,
13
+ } from "../schemas/shared.js";
10
14
  import { compareUtf8 } from "../stable-order.js";
11
15
  import { verifyPluginPackage } from "./package-reader.js";
12
- import { copyOrdinaryDirectory, extractRemoteArchive } from "./remote-archive.js";
16
+ import {
17
+ copyOrdinaryDirectory,
18
+ extractRemoteArchive,
19
+ REMOTE_PACKAGE_LIMITS,
20
+ } from "./remote-archive.js";
13
21
 
14
22
  const PROFILE_RANK = Object.freeze({ standard: 0, integration: 1, system: 2 });
15
23
 
@@ -294,16 +302,21 @@ export class GitLabSourceProvider {
294
302
  const packageRoot = path.join(staging, "package");
295
303
  try {
296
304
  fs.mkdirSync(extractRoot);
297
- fs.writeFileSync(archiveFile, await this.client.downloadArchive(project, commit));
298
- const { selectedRoot } = await extractRemoteArchive({
299
- archiveFile,
300
- extractRoot,
301
- subdir,
302
- label: "GitLab Plugin",
303
- sourceId: this.id,
304
- extractArchive: this.extractArchive,
305
- });
306
- copyOrdinaryDirectory(selectedRoot, packageRoot, "GitLab Plugin");
305
+ try {
306
+ fs.writeFileSync(archiveFile, await this.client.downloadArchive(project, commit));
307
+ const { selectedRoot } = await extractRemoteArchive({
308
+ archiveFile,
309
+ extractRoot,
310
+ subdir,
311
+ label: "GitLab Plugin",
312
+ sourceId: this.id,
313
+ extractArchive: this.extractArchive,
314
+ });
315
+ copyOrdinaryDirectory(selectedRoot, packageRoot, "GitLab Plugin");
316
+ } catch (error) {
317
+ if (!this.#canFallbackToRepositoryTree(error)) throw error;
318
+ await this.#materializeRepositoryTree(project, commit, subdir, packageRoot);
319
+ }
307
320
  validatePluginManifest(JSON.parse(fs.readFileSync(path.join(packageRoot, "plugin.json"), "utf8")));
308
321
  const integrity = hashCanonicalTree(packageRoot);
309
322
  if (integrity !== version.integrity) {
@@ -337,6 +350,92 @@ export class GitLabSourceProvider {
337
350
  }
338
351
  }
339
352
 
353
+ /** @param {unknown} error @returns {boolean} */
354
+ #canFallbackToRepositoryTree(error) {
355
+ return (
356
+ error?.code === PLUGIN_RUNTIME_ERROR_CODES.REMOTE_REQUEST_FAILED &&
357
+ error?.details?.status === 406 &&
358
+ typeof this.client.readRepositoryTree === "function" &&
359
+ typeof this.client.readRawBuffer === "function"
360
+ );
361
+ }
362
+
363
+ /**
364
+ * 在 GitLab archive 对 OAuth 返回 406 时,用固定 commit 的 tree/raw API 重建选中目录。
365
+ *
366
+ * @param {string} project GitLab project path
367
+ * @param {string} commit 固定 commit
368
+ * @param {string|null} subdir 选中的仓库子目录
369
+ * @param {string} target 目标目录
370
+ * @returns {Promise<void>} 完成信号
371
+ */
372
+ async #materializeRepositoryTree(project, commit, subdir, target) {
373
+ const normalizedSubdir = subdir
374
+ ? assertSafePosixRelativePath(subdir, "GitLab Plugin subdir")
375
+ : null;
376
+ const entries = await this.client.readRepositoryTree(project, {
377
+ ref: commit,
378
+ ...(normalizedSubdir ? { path: normalizedSubdir } : {}),
379
+ });
380
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > REMOTE_PACKAGE_LIMITS.maxEntries) {
381
+ throw new PluginRuntimeError("GitLab Plugin repository tree 无效或超过条目限制", {
382
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
383
+ path: this.id,
384
+ });
385
+ }
386
+ const prefix = normalizedSubdir ? `${normalizedSubdir}/` : "";
387
+ const paths = new Set();
388
+ let totalBytes = 0;
389
+ fs.mkdirSync(target, { recursive: true });
390
+ for (const entry of entries.sort((left, right) => compareUtf8(String(left.path), String(right.path)))) {
391
+ const repositoryPath = assertSafePosixRelativePath(entry?.path, "GitLab repository tree 路径");
392
+ if (prefix && !repositoryPath.startsWith(prefix)) {
393
+ throw new PluginRuntimeError(`GitLab Plugin tree 条目逃逸选中目录:${repositoryPath}`, {
394
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
395
+ path: this.id,
396
+ });
397
+ }
398
+ const relativePath = assertSafePosixRelativePath(
399
+ prefix ? repositoryPath.slice(prefix.length) : repositoryPath,
400
+ "GitLab Plugin tree 路径",
401
+ );
402
+ if (paths.has(relativePath)) {
403
+ throw new PluginRuntimeError(`GitLab Plugin tree 包含重复路径:${relativePath}`, {
404
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
405
+ path: this.id,
406
+ });
407
+ }
408
+ paths.add(relativePath);
409
+ const destination = path.join(target, ...relativePath.split("/"));
410
+ if (entry.type === "tree" && entry.mode === "040000") {
411
+ fs.mkdirSync(destination, { recursive: true });
412
+ continue;
413
+ }
414
+ if (entry.type !== "blob" || !["100644", "100755"].includes(entry.mode)) {
415
+ throw new PluginRuntimeError(`GitLab Plugin tree 包含不安全条目:${repositoryPath}`, {
416
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
417
+ path: this.id,
418
+ });
419
+ }
420
+ const content = await this.client.readRawBuffer(project, repositoryPath, commit);
421
+ if (!Buffer.isBuffer(content) || content.length > REMOTE_PACKAGE_LIMITS.maxEntryBytes) {
422
+ throw new PluginRuntimeError(`GitLab Plugin 文件超过大小限制:${repositoryPath}`, {
423
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
424
+ path: this.id,
425
+ });
426
+ }
427
+ totalBytes += content.length;
428
+ if (totalBytes > REMOTE_PACKAGE_LIMITS.maxExtractedBytes) {
429
+ throw new PluginRuntimeError("GitLab Plugin repository tree 超过总大小限制", {
430
+ code: PLUGIN_RUNTIME_ERROR_CODES.REMOTE_ARCHIVE_INVALID,
431
+ path: this.id,
432
+ });
433
+ }
434
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
435
+ fs.writeFileSync(destination, content, { mode: entry.mode === "100755" ? 0o755 : 0o644 });
436
+ }
437
+ }
438
+
340
439
  /** @param {object} source @returns {string} */
341
440
  #sourceReference(source) {
342
441
  return source.type === "path" ? this.source.project : source.project;
@@ -7,9 +7,13 @@ import { assertSafePosixRelativePath } from "../schemas/shared.js";
7
7
  import { compareUtf8 } from "../stable-order.js";
8
8
 
9
9
  const ALLOWED_ARCHIVE_TYPES = new Set(["File", "Directory"]);
10
- const MAX_ENTRY_BYTES = 25 * 1024 * 1024;
11
- const MAX_ARCHIVE_ENTRIES = 10_000;
12
- const MAX_EXTRACTED_BYTES = 250 * 1024 * 1024;
10
+
11
+ /** 远程 Plugin 包在 archive 与 API tree 回退路径共用的资源上限。 */
12
+ export const REMOTE_PACKAGE_LIMITS = Object.freeze({
13
+ maxEntryBytes: 25 * 1024 * 1024,
14
+ maxEntries: 10_000,
15
+ maxExtractedBytes: 250 * 1024 * 1024,
16
+ });
13
17
 
14
18
  /**
15
19
  * 判断 archive 条目是否落在调用方明确选中的子目录内。
@@ -93,17 +97,17 @@ export async function extractRemoteArchive(options) {
93
97
  path.win32.isAbsolute(normalized) ||
94
98
  normalized.includes("\\") ||
95
99
  segments.some((segment) => !segment || segment === "." || segment === "..") ||
96
- archiveEntries > MAX_ARCHIVE_ENTRIES
100
+ archiveEntries > REMOTE_PACKAGE_LIMITS.maxEntries
97
101
  ) {
98
102
  unsafeEntry ||= entryPath;
99
103
  return false;
100
104
  }
101
- if (!ALLOWED_ARCHIVE_TYPES.has(entry.type) || entrySize > MAX_ENTRY_BYTES) {
105
+ if (!ALLOWED_ARCHIVE_TYPES.has(entry.type) || entrySize > REMOTE_PACKAGE_LIMITS.maxEntryBytes) {
102
106
  if (isInsideSelectedSubdir(normalized, options.subdir)) unsafeEntry ||= entryPath;
103
107
  return false;
104
108
  }
105
109
  extractedBytes += entrySize;
106
- if (extractedBytes > MAX_EXTRACTED_BYTES) {
110
+ if (extractedBytes > REMOTE_PACKAGE_LIMITS.maxExtractedBytes) {
107
111
  unsafeEntry ||= entryPath;
108
112
  return false;
109
113
  }
@@ -12,7 +12,8 @@ import {
12
12
  } from "../schemas/shared.js";
13
13
  import { compareUtf8 } from "../stable-order.js";
14
14
 
15
- const SOURCE_CONFIG_VERSION = 2;
15
+ const SOURCE_CONFIG_VERSION = 3;
16
+ const SOURCE_DESCRIPTOR_VERSION = 2;
16
17
  const LEGACY_SOURCE_CONFIG_VERSION = 1;
17
18
  const EXTERNAL_FORMATS = new Set(["auto", "flower", "codex", "claude-code", "skill-only"]);
18
19
  const BUILTIN_DESCRIPTOR_PATH = fileURLToPath(
@@ -79,7 +80,7 @@ export function validateGitLabSourceDescriptor(value) {
79
80
  const oauth = /** @type {Record<string,unknown>} */ (source?.oauth || {});
80
81
  const scopes = Array.isArray(oauth?.scopes) ? [...oauth.scopes].sort(compareUtf8) : [];
81
82
  if (
82
- ![LEGACY_SOURCE_CONFIG_VERSION, SOURCE_CONFIG_VERSION].includes(source.schemaVersion) ||
83
+ ![LEGACY_SOURCE_CONFIG_VERSION, SOURCE_DESCRIPTOR_VERSION].includes(source.schemaVersion) ||
83
84
  !isPluginId(source.id) ||
84
85
  source.type !== "gitlab" ||
85
86
  typeof source.name !== "string" || !source.name ||
@@ -102,7 +103,7 @@ export function validateGitLabSourceDescriptor(value) {
102
103
  baseUrl.search = "";
103
104
  baseUrl.hash = "";
104
105
  return {
105
- schemaVersion: SOURCE_CONFIG_VERSION,
106
+ schemaVersion: SOURCE_DESCRIPTOR_VERSION,
106
107
  id: source.id,
107
108
  type: "gitlab",
108
109
  name: source.name,
@@ -182,7 +183,7 @@ export function validateGitHubSourceDescriptor(value) {
182
183
  }
183
184
  const format = String(source?.format || "auto");
184
185
  if (
185
- Number(source?.schemaVersion) !== SOURCE_CONFIG_VERSION ||
186
+ Number(source?.schemaVersion) !== SOURCE_DESCRIPTOR_VERSION ||
186
187
  !isPluginId(source?.id) ||
187
188
  source?.type !== "github" ||
188
189
  typeof source?.name !== "string" || !source.name ||
@@ -208,7 +209,7 @@ export function validateGitHubSourceDescriptor(value) {
208
209
  });
209
210
  }
210
211
  return {
211
- schemaVersion: SOURCE_CONFIG_VERSION,
212
+ schemaVersion: SOURCE_DESCRIPTOR_VERSION,
212
213
  id: source.id,
213
214
  type: "github",
214
215
  name: source.name,
@@ -263,7 +264,13 @@ export class UserSourceStore {
263
264
  { ...validateSourceDescriptor(source), builtin: true },
264
265
  ]));
265
266
  for (const source of this.#readUserSources()) {
266
- merged.set(source.id, { ...source, builtin: merged.has(source.id) });
267
+ const builtin = merged.get(source.id);
268
+ if (builtin) {
269
+ // 内置来源的远程连接定义随包升级,用户层只保留显式启停偏好。
270
+ merged.set(source.id, { ...builtin, enabled: source.enabled });
271
+ } else {
272
+ merged.set(source.id, { ...source, builtin: false });
273
+ }
267
274
  }
268
275
  return [...merged.values()].sort((left, right) => compareUtf8(left.id, right.id));
269
276
  }
@@ -293,7 +300,13 @@ export class UserSourceStore {
293
300
  * @returns {object} 保存后的来源
294
301
  */
295
302
  set(source) {
296
- const normalized = validateSourceDescriptor({ schemaVersion: SOURCE_CONFIG_VERSION, ...source });
303
+ const normalized = validateSourceDescriptor({ schemaVersion: SOURCE_DESCRIPTOR_VERSION, ...source });
304
+ if (this.builtinDescriptors.some(({ id }) => id === normalized.id)) {
305
+ throw new PluginRuntimeError(`内置 Plugin source 仅支持启用或停用:${normalized.id}`, {
306
+ code: PLUGIN_RUNTIME_ERROR_CODES.SOURCE_CONFIG_INVALID,
307
+ path: normalized.id,
308
+ });
309
+ }
297
310
  const sources = this.#readUserSources().filter((entry) => entry.id !== normalized.id);
298
311
  sources.push(normalized);
299
312
  this.#writeUserSources(sources);
@@ -333,7 +346,15 @@ export class UserSourceStore {
333
346
  */
334
347
  setEnabled(id, enabled) {
335
348
  const source = this.get(id, { includeDisabled: true });
336
- return this.set({ ...source, enabled, builtin: undefined });
349
+ const sources = this.#readUserSources().filter((entry) => entry.id !== id);
350
+ if (source.builtin) {
351
+ const builtin = validateSourceDescriptor(this.builtinDescriptors.find((entry) => entry.id === id));
352
+ if (enabled !== builtin.enabled) sources.push({ id, enabled, builtinPreference: true });
353
+ } else {
354
+ sources.push({ ...source, enabled, builtin: undefined });
355
+ }
356
+ this.#writeUserSources(sources);
357
+ return this.get(id, { includeDisabled: true });
337
358
  }
338
359
 
339
360
  /** @returns {object[]} 用户配置中的来源 */
@@ -341,16 +362,31 @@ export class UserSourceStore {
341
362
  if (!fs.existsSync(this.configFile)) return [];
342
363
  try {
343
364
  const raw = JSON.parse(fs.readFileSync(this.configFile, "utf8"));
344
- if (![LEGACY_SOURCE_CONFIG_VERSION, SOURCE_CONFIG_VERSION].includes(raw.schemaVersion) || !Array.isArray(raw.sources)) {
365
+ if (![LEGACY_SOURCE_CONFIG_VERSION, SOURCE_DESCRIPTOR_VERSION, SOURCE_CONFIG_VERSION].includes(raw.schemaVersion) || !Array.isArray(raw.sources)) {
345
366
  throw new TypeError("用户 source 配置 schemaVersion 或 sources 无效");
346
367
  }
347
368
  if (raw.schemaVersion === LEGACY_SOURCE_CONFIG_VERSION && raw.sources.some(({ type }) => type !== "gitlab")) {
348
369
  throw new TypeError("schemaVersion 1 只允许旧 GitLab source");
349
370
  }
350
- const sources = raw.sources.map((source) => validateSourceDescriptor({
351
- ...source,
352
- schemaVersion: source.schemaVersion || raw.schemaVersion,
353
- }));
371
+ const builtinIds = new Set(this.builtinDescriptors.map(({ id }) => id));
372
+ const sources = raw.sources.map((source) => {
373
+ const fields = Object.keys(source || {});
374
+ if (
375
+ fields.length === 2 &&
376
+ fields.every((field) => ["id", "enabled"].includes(field)) &&
377
+ builtinIds.has(source.id) &&
378
+ typeof source.enabled === "boolean"
379
+ ) {
380
+ return { id: source.id, enabled: source.enabled, builtinPreference: true };
381
+ }
382
+ const normalized = validateSourceDescriptor({
383
+ ...source,
384
+ schemaVersion: source.schemaVersion || Math.min(raw.schemaVersion, SOURCE_DESCRIPTOR_VERSION),
385
+ });
386
+ return builtinIds.has(normalized.id)
387
+ ? { id: normalized.id, enabled: normalized.enabled, builtinPreference: true }
388
+ : normalized;
389
+ });
354
390
  if (new Set(sources.map(({ id }) => id)).size !== sources.length) {
355
391
  throw new TypeError("用户 source 配置包含重复 ID");
356
392
  }
@@ -375,9 +411,14 @@ export class UserSourceStore {
375
411
  const temporary = `${this.configFile}.${process.pid}.tmp`;
376
412
  try {
377
413
  fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
414
+ const serialized = sources.map((source) => (
415
+ source.builtinPreference
416
+ ? { id: source.id, enabled: source.enabled }
417
+ : source
418
+ ));
378
419
  fs.writeFileSync(temporary, `${JSON.stringify({
379
420
  schemaVersion: SOURCE_CONFIG_VERSION,
380
- sources: [...sources].sort((left, right) => compareUtf8(left.id, right.id)),
421
+ sources: serialized.sort((left, right) => compareUtf8(left.id, right.id)),
381
422
  }, null, 2)}\n`, { mode: 0o600 });
382
423
  fs.renameSync(temporary, this.configFile);
383
424
  } catch (error) {