flower-trellis 0.6.10-beta.6 → 0.6.10-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.
- package/README.md +8 -6
- package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +17 -4
- package/enhancements/0.6/.agents/skills/trellis-push/references/completed-task-recovery.md +4 -2
- package/enhancements/0.6/.agents/skills/trellis-push/references/output-templates.md +4 -0
- package/enhancements/0.6/.agents/skills/trellis-push/scripts/verify_gc_commit.py +203 -0
- package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +17 -4
- package/enhancements/0.6/.claude/skills/trellis-push/references/completed-task-recovery.md +4 -2
- package/enhancements/0.6/.claude/skills/trellis-push/references/output-templates.md +4 -0
- package/enhancements/0.6/.claude/skills/trellis-push/scripts/verify_gc_commit.py +203 -0
- package/enhancements/MANIFEST.json +2 -2
- package/package.json +3 -3
- package/src/assets/flower_session_start.py +56 -23
- package/src/cli.js +2 -1
- package/src/constants.js +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ flower-trellis -v
|
|
|
117
117
|
| `--variant <old\|0.5\|0.6>` | 强制指定强化包变体(默认按 `.trellis/.version` 自动选) |
|
|
118
118
|
| `--target <dir>` | 目标目录(默认当前目录) |
|
|
119
119
|
| `--no-update-check` | 本次跳过 flower-trellis 新版本检测(等价环境变量 `FLOWER_NO_UPDATE_CHECK=1`) |
|
|
120
|
-
| `--backup-retention <n>` | `update` 成功后保留最近 n 份 `.trellis/.backup-<timestamp>` 快照(默认
|
|
120
|
+
| `--backup-retention <n>` | `update` 成功后保留最近 n 份 `.trellis/.backup-<timestamp>` 快照(默认 1,`0` 表示本次不清理) |
|
|
121
121
|
|
|
122
122
|
未指定平台时,交互模式会弹出多选菜单(默认勾选 Claude Code + Codex);也可直接传 `--claude` / `--codex` / `--cursor` / `--devin` / `--zcode` / `--trae` / `--omp` / `--grok` / `--kimi` / `--snow` 等指定,或用 `-y` 跳过菜单。`--windsurf` 仍作为 Devin 的旧别名透传给 Trellis。其余未识别的 flag(如 `-u`、`-f`、`--template`、`--with-statusline`)一律透传给 Trellis。
|
|
123
123
|
|
|
@@ -145,20 +145,21 @@ flower-trellis enable --target .
|
|
|
145
145
|
|
|
146
146
|
项目处于 disabled 时,`flower-trellis update`、`self-update` 的项目更新链,以及 `plugin add/update/remove/replay` 会临时恢复必要入口,完成写操作后再次 detach,并校验最终仍为 disabled;外部 Plugin 内容不会被当作 Trellis 入口删除。直接运行上游 `trellis update` 不经过这一控制面,可能重新生成入口,此时 `flower-trellis status` 会报告 `drifted`。`disable` 也不等同于 `uninstall`:历史数据和恢复能力会继续保留。
|
|
147
147
|
|
|
148
|
-
## Astra 工作流提示
|
|
148
|
+
## Astra 与 Sol 工作流提示
|
|
149
149
|
|
|
150
|
-
Codex 的 SessionStart 在 `startup`、`clear`、`compact`
|
|
150
|
+
Codex 的 SessionStart 在 `startup`、`clear`、`compact` 时,事件输入的当前模型精确为 `gpt-6-astra` 或 `gpt-6-sol`,才会在 state 分段追加对应模型的一份英文工作流提示。两者使用相同的工作流正文和各自的模型标识;其他模型(包括 5.5)、缺失模型和未知别名不会追加;普通用户轮次不重复注入。
|
|
151
151
|
|
|
152
152
|
提示用于辅助遵循技能、工作流、模板和证据陈述,效果仍需行为对照验证。它不能删除宿主或历史指令,也不保证覆盖更高优先级要求。会话中切换模型后,要等下次 SessionStart 才重新判断是否追加。
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
两个模型默认分别开启。要单独关闭某个模型的提示,在项目 `.trellis/config.yaml` 已有的 `codex` 配置下合并对应字段,保留原有字段:
|
|
155
155
|
|
|
156
156
|
```yaml
|
|
157
157
|
codex:
|
|
158
158
|
astra_workflow_hint: false
|
|
159
|
+
sol_workflow_hint: false
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
任一字段设置 `true` 或删除可恢复该模型的默认值。开关只影响后续新增提示;做无历史干扰的对照时应使用新会话。原工作流上下文继续注入,Trellis 全局禁用、resume 和非交互跳过规则保持原有行为。`no-trellis` 仍只跳过本轮 UserPromptSubmit。
|
|
162
163
|
|
|
163
164
|
## Flower Plugin
|
|
164
165
|
|
|
@@ -227,11 +228,12 @@ flower-trellis plugin add flower/flower-plugin-author --platform codex --json
|
|
|
227
228
|
### 升级备份保留
|
|
228
229
|
|
|
229
230
|
上游 `trellis update` 会在写入前创建 `.trellis/.backup-<timestamp>/` 完整快照。
|
|
230
|
-
`flower-trellis update` 在 Trellis 更新、强化包叠加和本地配置恢复流程完成后,默认只保留最近
|
|
231
|
+
`flower-trellis update` 在 Trellis 更新、强化包叠加和本地配置恢复流程完成后,默认只保留最近 1 份:
|
|
231
232
|
|
|
232
233
|
- 只有名称严格符合时间戳格式的直接子目录会参与清理;`.trellis/.backup-flower/`、普通文件、
|
|
233
234
|
软链接和相似名称目录不会被删除。
|
|
234
235
|
- 更新失败时不清理,并保留本轮上游已经创建的备份。
|
|
236
|
+
- 本轮新建的备份始终受保护;若一次更新新建多份,可能临时超过默认保留数量。
|
|
235
237
|
- `--dry-run` 只展示预计保留和删除的备份,不修改文件系统。
|
|
236
238
|
- `--backup-retention 0` 可关闭本次自动清理;该设置只影响当前命令,不写入项目配置。
|
|
237
239
|
- `self-update` 可通过 `--` 传入覆盖值,例如
|
|
@@ -88,16 +88,29 @@ git ls-files -u
|
|
|
88
88
|
git rev-parse --verify MERGE_HEAD 2>/dev/null || true
|
|
89
89
|
git log --oneline -5
|
|
90
90
|
git log @{u}..HEAD --oneline 2>/dev/null || true
|
|
91
|
+
git rev-list --reverse @{u}..HEAD 2>/dev/null || true
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
停止条件:
|
|
94
95
|
|
|
95
96
|
- detached HEAD、分支不可读、未解决冲突、rebase、cherry-pick、revert 或其它非 merge 的未完成 Git 集成状态。
|
|
96
97
|
- `MERGE_HEAD` 存在时仅普通模式可继续,并且必须固定当前 `HEAD` / `MERGE_HEAD`、确认 `git ls-files -u` 为空、全部 staged paths 都属于 planned 且没有 retained staged;否则停止。用户/auto-loop `commit-only` 不适用。
|
|
97
|
-
-
|
|
98
|
+
- 普通推送会携带既无法归属本次任务、也未通过下述自动 GC 审计的历史 ahead commits。
|
|
98
99
|
- 无法确定 planned file 是否属于当前请求或活动任务。
|
|
99
100
|
- 内部 `commit-only` 发现 staged 区非空。
|
|
100
101
|
|
|
102
|
+
### 历史自动 GC 提交
|
|
103
|
+
|
|
104
|
+
仅普通推送及其任务记录发布恢复使用这一默认许可。对 `@{u}..HEAD` 的每个完整提交哈希逐个判断:已由当前任务证据归属的提交沿用原规则;候选自动 GC 提交调用当前平台 `trellis-push` skill 目录中的只读脚本:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python3 <skill-dir>/scripts/verify_gc_commit.py --repo <repository-root> --commit <full-sha>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`<skill-dir>` 是当前实际加载的 `.agents/skills/trellis-push` 或 `.claude/skills/trellis-push`,不能从另一平台借用脚本。只有退出码为 0、JSON `status=verified` 且返回的完整 `commit` 与请求哈希一致,才把该提交标为“已验证自动 GC”。脚本逐项验证单父和完整固定消息、无重命名推断的 Git 树、顶层 closed 任务与 UTC 月份归档文件的同 blob/模式映射;支持同批多任务和已有相同归档的去重。它不读取或修改工作区,不凭提交消息单独放行。脚本缺失、执行失败、JSON 损坏、内容改写或任一证据不符都保持未知 ahead 并停止普通推送。
|
|
111
|
+
|
|
112
|
+
记录已验证 GC 的完整哈希及仓库,作为计划和执行前复核基线。它是用户对纯自动 GC 的默认许可,不属于业务 `planned` 或任务记录 exact files,不增加专项确认,也不自行触发 GC 或单独 push。正常业务提交仍须展示并确认一次 Push 计划;用户 `commit-only` 和 auto-loop 内部 `commit-only` 不因本规则扩大授权。
|
|
113
|
+
|
|
101
114
|
业务 Git 文件分为两组;普通模式的当前任务记录 exact files 按下方独立提交规则处理:
|
|
102
115
|
|
|
103
116
|
- `planned`:本轮明确归属且准备提交的 exact files。
|
|
@@ -142,7 +155,7 @@ auto-loop 内部 `commit-only` 不渲染交互式计划或结果,也不再次
|
|
|
142
155
|
|
|
143
156
|
## Step 4:精确提交与推送
|
|
144
157
|
|
|
145
|
-
每个仓库按计划顺序执行。执行前重新检查 planned files、当前分支、HEAD、upstream、冲突状态、staged、全部 dirty paths 和 retained
|
|
158
|
+
每个仓库按计划顺序执行。执行前重新检查 planned files、当前分支、HEAD、upstream、冲突状态、staged、全部 dirty paths 和 retained 摘要;普通推送还须逐个重验计划中已验证 GC 的完整哈希与当前 `@{u}..HEAD`。任一关键条件变化都停止当前执行并重新规划。普通模式仅 `retained` 内容变化时可更新说明;auto-loop 内部模式的 retained 内容必须保持不变。
|
|
146
159
|
|
|
147
160
|
计划包含本地生成命令时,前置仓成功后按计划执行命令,再复用本节现有预检。命令成功、后续仓全部 dirty paths(auto-loop 内部 `commit-only` 扣除已登记且验证未变化的 retained paths)都在预计 exact files 内且 retained 摘要未漂移时直接继续;否则停止并重新生成计划。预计文件最终 clean 时不强行提交。
|
|
148
161
|
|
|
@@ -220,7 +233,7 @@ auto-loop 内部链失败时向调用方返回全部已完成仓库提交和失
|
|
|
220
233
|
|
|
221
234
|
- 当前任务 exact files 与首次确认的路径集合一致,没有新增当前任务路径或无法归属的 dirty 内容。
|
|
222
235
|
- 父仓分支、upstream 和冲突状态安全。
|
|
223
|
-
-
|
|
236
|
+
- 对当前 `@{u}..HEAD` 重做 Step 2 归属检查;已验证的自动 GC 可随任务记录推送,其它无法归属的历史 ahead commits 仍停止。
|
|
224
237
|
|
|
225
238
|
全部业务 commit/push 成功时,通过 helper 用同一份最终 progress 原子写入 `progress`、`status=completed`、`completedAt` 与确定性 `closeout`:
|
|
226
239
|
|
|
@@ -251,7 +264,7 @@ git push origin <current-branch>
|
|
|
251
264
|
- 任务记录 commit 成功但 push 失败:任务目录应为 clean,并保留可归属的 ahead commit;后续只重试该 commit 的 push,不重复业务提交、helper 写入或任务记录 commit。
|
|
252
265
|
- 任务记录 push 成功:本任务产生的当前任务目录变更必须 clean;不得再次写入完成态或 Close。
|
|
253
266
|
|
|
254
|
-
任何恢复都必须验证当前分支、upstream、HEAD、`@{u}..HEAD`、任务记录 commit message 与 exact file set,以及 `task.json`
|
|
267
|
+
任何恢复都必须验证当前分支、upstream、HEAD、`@{u}..HEAD`、任务记录 commit message 与 exact file set,以及 `task.json` 的最终完成态。已验证自动 GC 可按 Step 2 默认许可归属;其它未知 ahead 或 dirty 不能当作可恢复任务记录。
|
|
255
268
|
|
|
256
269
|
## Step 6:结果
|
|
257
270
|
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
固定当前任务路径与 `task.json`,读取文件级任务状态、`closeout`、当前分支、upstream、`HEAD`、`@{u}..HEAD` 的提交消息与文件集合,以及 `python3 ./.trellis/scripts/auto_loop.py status --verbose`。所有恢复都必须验证 exact task、最终 progress、`status=completed`、Close 状态、分支和提交归属;progress 文本不能替代 runtime 或 Git 证据。
|
|
8
8
|
|
|
9
|
+
普通发布恢复逐个审计 ahead 提交。除本任务可归属的业务或任务记录提交外,只有按同目录 `SKILL.md` Step 2 及 `scripts/verify_gc_commit.py` 验证的纯自动 GC 提交可使用默认许可;仅凭 GC 消息或路径名不构成证据。
|
|
10
|
+
|
|
9
11
|
## Outcomes
|
|
10
12
|
|
|
11
13
|
按以下优先级只返回一个结果:
|
|
12
14
|
|
|
13
15
|
1. **Auto-loop 本地完成,无需 Push**:健康的终态或 recent auto-loop run 精确包含当前任务,记录的本地提交仍可验证,且任务 dirty 仅为 runner 在提交后写入的 `<task-dir>/task.json` progress/lifecycle bookkeeping。停止 Push;`closeout.status=closed` 时无需其它生命周期动作。
|
|
14
16
|
2. **任务记录 commit + push 恢复计划**:当前任务 exact files 仍 dirty,`task.json` 已包含合法最终 progress 与 `status=completed`,且文件集合可由首次确认或重新确认闭合。不得重复业务提交或 helper 写入;即使 Close 已成功,也只恢复已确认的任务记录发布。
|
|
15
|
-
3. **任务记录 push-only 恢复计划**:当前任务目录 clean;upstream 存在;`@{u}..HEAD` 中存在消息、exact file set 和完成态均可归属的任务记录 commit
|
|
17
|
+
3. **任务记录 push-only 恢复计划**:当前任务目录 clean;upstream 存在;`@{u}..HEAD` 中存在消息、exact file set 和完成态均可归属的任务记录 commit;其余 ahead 仅含已验证自动 GC 或其它可归属本任务的提交。只推送这些已存在提交,不创建新 commit。
|
|
16
18
|
4. **Close 待解阻**:任务记录已经同步,`closeout.status=blocked|pending`,且 blockers 证据一致。报告结构化 blockers;条件解决后只重试 `task.py close <task>`,持久化语义 blocker 由所属 owner 完成后追加 `--resolve-blocker <code>`,不重复业务 Git 动作或 progress 写入。
|
|
17
19
|
5. **无需动作**:任务记录已经同步且 `closeout.status=closed`。任务已退出活跃视图,物理 GC 由后续 SessionStart 在三天阈值后处理。
|
|
18
|
-
6. **阻断**:runtime 与 Git 矛盾、auto-loop marker 无健康完成证据、缺少普通路径 upstream、任务 dirty 超出 exact files
|
|
20
|
+
6. **阻断**:runtime 与 Git 矛盾、auto-loop marker 无健康完成证据、缺少普通路径 upstream、任务 dirty 超出 exact files、存在未通过 GC 审计且无法归属的 ahead,或提交消息/文件集合/分支无法闭合。报告具体证据缺口,不 push、不 Close,也不猜测完成来源。
|
|
19
21
|
|
|
20
22
|
恢复计划仍使用 `trellis-push` 的既有一次确认、执行前漂移检查和结果模板。这里只决定完成态恢复范围,不新增状态、持久化字段或自动确认。
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- **变更**:<N> 个文件 · `+<adds> -<deletes>`
|
|
24
24
|
- **父提交**:`<pre-merge-head>` + `<merge-head>`(仅已有 merge 时显示)
|
|
25
25
|
- **Push**:<执行 / 跳过(commit-only)>
|
|
26
|
+
- **已验证自动 GC**:<随本次推送的历史 GC short hashes>(仅存在时显示)
|
|
26
27
|
|
|
27
28
|
计划提交:
|
|
28
29
|
- <exact files 或分组摘要>
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
- 计划中单仓 `planned` 不超过 8 个文件时完整列出。
|
|
55
56
|
- 超过 8 个时按目录归组,最多 12 行;用户要求展开时展示同一 exact set。
|
|
56
57
|
- 顶部仓库/commit/file 总数包含独立任务记录提交所在 Git root、该提交及其 exact files;任务记录文件使用相同的 8 文件展示阈值和展开规则。
|
|
58
|
+
- 已验证自动 GC 是已有的历史 ahead,不计入顶部本轮新建 commit/file 数、业务 planned 或任务记录 exact files;仅在实际将随普通推送发布时展示其仓库和 short hashes,不作为风险或第二次确认项。
|
|
57
59
|
- 计划中的保留变更按仓库计数:不超过 8 项时逐项标注 `[untracked]`、`[unstaged]`、`[staged]`;超过 8 项时将非 staged 项按目录与 Git 状态汇总数量,每仓最多 12 行,必要时合并到上级目录。同一路径计数一次,兼有 staged/unstaged 时同时标注。
|
|
58
60
|
- 计划中的计划外 staged 项始终逐项单列,不计入分组摘要;计划和结果中的真正风险均在独立“风险”区逐项展示,不受行数限制。分组、展开均只改变展示,不改变 exact set 或确认范围。
|
|
59
61
|
- 用户要求“展开保留变更”时在对话中列出同一 exact set 与 Git 状态,不生成清单附件;“展开文件”仍指 planned files。
|
|
@@ -73,6 +75,7 @@
|
|
|
73
75
|
[推送成功 / 仅本地提交成功 / 部分完成 / 失败]
|
|
74
76
|
|
|
75
77
|
- **<repository-name>**:`<branch> → <upstream>` · `<short-hash[, short-hash...]>` · <已推送 / 仅本地 / 失败 / 未执行>
|
|
78
|
+
- **已验证自动 GC**:<repository-name> · <short hashes> · <已推送 / 未推送待恢复>(仅存在时显示)
|
|
76
79
|
- **任务记录**:`<task-record-hash>` · <completed + Close 已同步 / completed 但 Close blocked / partial 已同步,仍为 in_progress / commit 待恢复 / push 待恢复 / 同步失败>
|
|
77
80
|
- **保留未提交的变更(dirty)**:<每仓数量与实际核对结论>
|
|
78
81
|
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
- 仅普通模式且存在活动任务时显示“任务记录”行;没有实际 commit hash 时省略 hash,不填占位值。用户 `commit-only` 的仓库行只写当前分支与本地提交,不显示推送箭头或暗示已同步远端。
|
|
93
96
|
- untracked 结果用“无任务状态”行替代“任务记录”,展示 work id 与 `<已清理/保留待恢复>`;不生成或暗示 task progress commit。没有保留变更、失败或风险时省略对应行或章节。
|
|
94
97
|
- 部分完成时必须明确列出已成功仓库、失败仓库/步骤、当前分支和下一恢复动作。业务结果与 progress sync 状态不得合并成一个模糊结论。
|
|
98
|
+
- 结果中的已验证自动 GC 只报告实际随对应仓库推送的历史提交;push 失败时标明待恢复,不把本地 commit 误报为已推送,也不重复为 GC 生成提交。
|
|
95
99
|
- 普通成功结果必须确认本任务产生的当前任务目录变更 clean。其它 retained dirty(含计划外 staged)仍逐项核验,已核对保持原状时每仓只报告数量与结论,不重复清单。异常或未核验项列出路径、实际状态和处理情况,不得笼统声称全部保持原状;用户要求详情时再展示实际文件、message、命令或进度,展开文件仍沿用共用规则。
|
|
96
100
|
- Git 成功不消除现有风险;成功结果省略未变化且接受仍有效的问题,不重复接受数量或原影响说明。结果的“风险”区保留新增、变化、接受失效或无法验证的事项,以及仍适用的其它完成链风险与 `[上线后验证]`;用户要求详情时再展开原问题与处置。
|
|
97
101
|
- helper 成功但任务记录 commit 失败时,结果写“任务记录 commit 待恢复”,说明本地 `completed`、Close 结果与 exact task dirty 已保留;任务记录 commit 成功但 push 失败时写“任务记录 push 待恢复”,说明 clean ahead commit 已保留。两种情况都不得暗示需要重复业务提交、helper 写入或 Close。
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""只读核验 Trellis 物理 GC 提交是否为纯归档移动。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
GC_MESSAGE = b"chore(task): gc closed tasks\n"
|
|
17
|
+
TASK_ROOT = b".trellis/tasks/"
|
|
18
|
+
ARCHIVE_ROOT = TASK_ROOT + b"archive/"
|
|
19
|
+
COMMIT_RE = re.compile(r"[0-9a-fA-F]{40}(?:[0-9a-fA-F]{24})?\Z")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GcCommitAuditError(Exception):
|
|
23
|
+
"""表示提交缺少默认放行所需的确定性证据。"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _git(repo_root: Path, *args: str) -> bytes:
|
|
27
|
+
"""以字节形式读取 Git,保留路径中的制表符及非 ASCII 字节。"""
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["git", "-C", str(repo_root), *args],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
check=False,
|
|
32
|
+
)
|
|
33
|
+
if result.returncode != 0:
|
|
34
|
+
detail = result.stderr.decode("utf-8", errors="replace").strip()
|
|
35
|
+
raise GcCommitAuditError(f"Git 查询失败:{detail or '未知错误'}")
|
|
36
|
+
return result.stdout
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tree(repo_root: Path, commit: str) -> dict[bytes, tuple[bytes, bytes]]:
|
|
40
|
+
"""读取任务目录的路径、文件模式和 blob ID。"""
|
|
41
|
+
raw = _git(repo_root, "ls-tree", "-r", "-z", commit, "--", ".trellis/tasks")
|
|
42
|
+
entries: dict[bytes, tuple[bytes, bytes]] = {}
|
|
43
|
+
for record in raw.split(b"\0"):
|
|
44
|
+
if not record:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
header, path = record.split(b"\t", 1)
|
|
48
|
+
mode, kind, oid = header.split(b" ")
|
|
49
|
+
except ValueError as error:
|
|
50
|
+
raise GcCommitAuditError("Git 树条目格式损坏") from error
|
|
51
|
+
if kind != b"blob" or path in entries:
|
|
52
|
+
raise GcCommitAuditError("任务树包含非文件或重复路径")
|
|
53
|
+
entries[path] = (mode, oid)
|
|
54
|
+
return entries
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _changes(repo_root: Path, parent: str, commit: str) -> tuple[set[bytes], set[bytes]]:
|
|
58
|
+
"""读取无重命名推断的新增和删除路径。"""
|
|
59
|
+
raw = _git(
|
|
60
|
+
repo_root,
|
|
61
|
+
"diff-tree",
|
|
62
|
+
"--no-commit-id",
|
|
63
|
+
"--no-renames",
|
|
64
|
+
"--ignore-submodules=none",
|
|
65
|
+
"--name-status",
|
|
66
|
+
"-r",
|
|
67
|
+
"-z",
|
|
68
|
+
parent,
|
|
69
|
+
commit,
|
|
70
|
+
)
|
|
71
|
+
records = raw.split(b"\0")
|
|
72
|
+
if not records or records[-1] != b"" or len(records[:-1]) % 2:
|
|
73
|
+
raise GcCommitAuditError("Git 差异记录格式损坏")
|
|
74
|
+
deleted: set[bytes] = set()
|
|
75
|
+
added: set[bytes] = set()
|
|
76
|
+
for index in range(0, len(records) - 1, 2):
|
|
77
|
+
status, path = records[index:index + 2]
|
|
78
|
+
if not path or path in deleted or path in added:
|
|
79
|
+
raise GcCommitAuditError("Git 差异包含空路径或重复路径")
|
|
80
|
+
if status == b"D":
|
|
81
|
+
deleted.add(path)
|
|
82
|
+
elif status == b"A":
|
|
83
|
+
added.add(path)
|
|
84
|
+
else:
|
|
85
|
+
raise GcCommitAuditError("提交包含非归档移动的文件修改")
|
|
86
|
+
if not deleted:
|
|
87
|
+
raise GcCommitAuditError("提交没有顶层任务文件删除")
|
|
88
|
+
return deleted, added
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _source_name(path: bytes) -> bytes:
|
|
92
|
+
"""从被删除文件中提取顶层任务名。"""
|
|
93
|
+
if not path.startswith(TASK_ROOT):
|
|
94
|
+
raise GcCommitAuditError("提交修改了任务目录以外的路径")
|
|
95
|
+
remainder = path[len(TASK_ROOT):]
|
|
96
|
+
name, separator, relative = remainder.partition(b"/")
|
|
97
|
+
if not separator or not name or name == b"archive" or not relative:
|
|
98
|
+
raise GcCommitAuditError("被删除文件不属于顶层任务")
|
|
99
|
+
return name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _bucket(repo_root: Path, task_json: tuple[bytes, bytes]) -> bytes:
|
|
103
|
+
"""校验关闭态,并返回生成器使用的 UTC 归档月份。"""
|
|
104
|
+
mode, oid = task_json
|
|
105
|
+
if mode not in {b"100644", b"100755"}:
|
|
106
|
+
raise GcCommitAuditError("task.json 不是普通文件")
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(_git(repo_root, "cat-file", "blob", oid.decode("ascii")).decode("utf-8"))
|
|
109
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
110
|
+
raise GcCommitAuditError("task.json 不是有效的 UTF-8 JSON") from error
|
|
111
|
+
if not isinstance(data, dict) or data.get("status") != "completed":
|
|
112
|
+
raise GcCommitAuditError("归档任务不是 completed")
|
|
113
|
+
closeout = data.get("closeout")
|
|
114
|
+
if not isinstance(closeout, dict) or closeout.get("status") != "closed" or closeout.get("blockers") != []:
|
|
115
|
+
raise GcCommitAuditError("归档任务没有合法的 closed 状态")
|
|
116
|
+
closed_at = closeout.get("closedAt")
|
|
117
|
+
if not isinstance(closed_at, str):
|
|
118
|
+
raise GcCommitAuditError("归档任务缺少关闭时间")
|
|
119
|
+
try:
|
|
120
|
+
parsed = datetime.fromisoformat(closed_at.strip().replace("Z", "+00:00"))
|
|
121
|
+
except ValueError as error:
|
|
122
|
+
raise GcCommitAuditError("归档任务的关闭时间无效") from error
|
|
123
|
+
if parsed.tzinfo is None:
|
|
124
|
+
raise GcCommitAuditError("归档任务的关闭时间缺少时区")
|
|
125
|
+
return parsed.astimezone(timezone.utc).strftime("%Y-%m").encode("ascii")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def verify_gc_commit(repo_root: Path, commit: str) -> dict[str, Any]:
|
|
129
|
+
"""验证单个提交仅将已关闭任务原字节移入归档。
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
repo_root: 待检查的 Git 仓库根目录。
|
|
133
|
+
commit: 完整的 40 或 64 位提交哈希。
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
已验证提交、任务名与移动文件数的结构化摘要。
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
GcCommitAuditError: 提交不满足纯 GC 归档契约或 Git 证据不可读。
|
|
140
|
+
"""
|
|
141
|
+
if not COMMIT_RE.fullmatch(commit):
|
|
142
|
+
raise GcCommitAuditError("必须提供完整提交哈希")
|
|
143
|
+
resolved = _git(repo_root, "rev-parse", "--verify", f"{commit}^{{commit}}").strip().decode("ascii")
|
|
144
|
+
header, separator, message = _git(repo_root, "cat-file", "-p", resolved).partition(b"\n\n")
|
|
145
|
+
parents = [line[7:].decode("ascii") for line in header.splitlines() if line.startswith(b"parent ")]
|
|
146
|
+
if not separator or len(parents) != 1 or message != GC_MESSAGE:
|
|
147
|
+
raise GcCommitAuditError("提交父节点或消息不符合自动 GC 契约")
|
|
148
|
+
parent = parents[0]
|
|
149
|
+
deleted, added = _changes(repo_root, parent, resolved)
|
|
150
|
+
before = _tree(repo_root, parent)
|
|
151
|
+
after = _tree(repo_root, resolved)
|
|
152
|
+
names = {_source_name(path) for path in deleted}
|
|
153
|
+
expected_added: set[bytes] = set()
|
|
154
|
+
reported_names: list[str] = []
|
|
155
|
+
for name in sorted(names):
|
|
156
|
+
source_prefix = TASK_ROOT + name + b"/"
|
|
157
|
+
source_files = {path for path in before if path.startswith(source_prefix)}
|
|
158
|
+
if not source_files or deleted.intersection(source_files) != source_files:
|
|
159
|
+
raise GcCommitAuditError("顶层任务未完整移走")
|
|
160
|
+
task_json = before.get(source_prefix + b"task.json")
|
|
161
|
+
if task_json is None:
|
|
162
|
+
raise GcCommitAuditError("顶层任务缺少 task.json")
|
|
163
|
+
destination_prefix = ARCHIVE_ROOT + _bucket(repo_root, task_json) + b"/" + name + b"/"
|
|
164
|
+
if any(path.startswith(source_prefix) for path in after):
|
|
165
|
+
raise GcCommitAuditError("提交后仍残留顶层任务文件")
|
|
166
|
+
for source in source_files:
|
|
167
|
+
destination = destination_prefix + source[len(source_prefix):]
|
|
168
|
+
if after.get(destination) != before[source]:
|
|
169
|
+
raise GcCommitAuditError("归档文件与原文件内容或模式不同")
|
|
170
|
+
if destination not in before:
|
|
171
|
+
expected_added.add(destination)
|
|
172
|
+
try:
|
|
173
|
+
reported_names.append(name.decode("utf-8"))
|
|
174
|
+
except UnicodeDecodeError as error:
|
|
175
|
+
raise GcCommitAuditError("任务名不是 UTF-8") from error
|
|
176
|
+
if added != expected_added:
|
|
177
|
+
raise GcCommitAuditError("提交包含未归属的新增路径")
|
|
178
|
+
return {"status": "verified", "commit": resolved, "tasks": reported_names, "files": len(deleted)}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(argv: list[str] | None = None) -> int:
|
|
182
|
+
"""解析只读校验参数并输出机器可读的结果。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
argv: 测试可传入的命令行参数;缺省使用进程参数。
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
验证通过返回 0,证据不足或 Git 查询失败返回 1。
|
|
189
|
+
"""
|
|
190
|
+
parser = argparse.ArgumentParser(description="核验 Trellis 纯归档 GC 提交")
|
|
191
|
+
parser.add_argument("--repo", type=Path, required=True)
|
|
192
|
+
parser.add_argument("--commit", required=True)
|
|
193
|
+
args = parser.parse_args(argv)
|
|
194
|
+
try:
|
|
195
|
+
result = verify_gc_commit(args.repo, args.commit)
|
|
196
|
+
except GcCommitAuditError as error:
|
|
197
|
+
result = {"status": "rejected", "commit": args.commit, "reason": str(error)}
|
|
198
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
199
|
+
return 0 if result["status"] == "verified" else 1
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|
|
@@ -88,16 +88,29 @@ git ls-files -u
|
|
|
88
88
|
git rev-parse --verify MERGE_HEAD 2>/dev/null || true
|
|
89
89
|
git log --oneline -5
|
|
90
90
|
git log @{u}..HEAD --oneline 2>/dev/null || true
|
|
91
|
+
git rev-list --reverse @{u}..HEAD 2>/dev/null || true
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
停止条件:
|
|
94
95
|
|
|
95
96
|
- detached HEAD、分支不可读、未解决冲突、rebase、cherry-pick、revert 或其它非 merge 的未完成 Git 集成状态。
|
|
96
97
|
- `MERGE_HEAD` 存在时仅普通模式可继续,并且必须固定当前 `HEAD` / `MERGE_HEAD`、确认 `git ls-files -u` 为空、全部 staged paths 都属于 planned 且没有 retained staged;否则停止。用户/auto-loop `commit-only` 不适用。
|
|
97
|
-
-
|
|
98
|
+
- 普通推送会携带既无法归属本次任务、也未通过下述自动 GC 审计的历史 ahead commits。
|
|
98
99
|
- 无法确定 planned file 是否属于当前请求或活动任务。
|
|
99
100
|
- 内部 `commit-only` 发现 staged 区非空。
|
|
100
101
|
|
|
102
|
+
### 历史自动 GC 提交
|
|
103
|
+
|
|
104
|
+
仅普通推送及其任务记录发布恢复使用这一默认许可。对 `@{u}..HEAD` 的每个完整提交哈希逐个判断:已由当前任务证据归属的提交沿用原规则;候选自动 GC 提交调用当前平台 `trellis-push` skill 目录中的只读脚本:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python3 <skill-dir>/scripts/verify_gc_commit.py --repo <repository-root> --commit <full-sha>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`<skill-dir>` 是当前实际加载的 `.agents/skills/trellis-push` 或 `.claude/skills/trellis-push`,不能从另一平台借用脚本。只有退出码为 0、JSON `status=verified` 且返回的完整 `commit` 与请求哈希一致,才把该提交标为“已验证自动 GC”。脚本逐项验证单父和完整固定消息、无重命名推断的 Git 树、顶层 closed 任务与 UTC 月份归档文件的同 blob/模式映射;支持同批多任务和已有相同归档的去重。它不读取或修改工作区,不凭提交消息单独放行。脚本缺失、执行失败、JSON 损坏、内容改写或任一证据不符都保持未知 ahead 并停止普通推送。
|
|
111
|
+
|
|
112
|
+
记录已验证 GC 的完整哈希及仓库,作为计划和执行前复核基线。它是用户对纯自动 GC 的默认许可,不属于业务 `planned` 或任务记录 exact files,不增加专项确认,也不自行触发 GC 或单独 push。正常业务提交仍须展示并确认一次 Push 计划;用户 `commit-only` 和 auto-loop 内部 `commit-only` 不因本规则扩大授权。
|
|
113
|
+
|
|
101
114
|
业务 Git 文件分为两组;普通模式的当前任务记录 exact files 按下方独立提交规则处理:
|
|
102
115
|
|
|
103
116
|
- `planned`:本轮明确归属且准备提交的 exact files。
|
|
@@ -142,7 +155,7 @@ auto-loop 内部 `commit-only` 不渲染交互式计划或结果,也不再次
|
|
|
142
155
|
|
|
143
156
|
## Step 4:精确提交与推送
|
|
144
157
|
|
|
145
|
-
每个仓库按计划顺序执行。执行前重新检查 planned files、当前分支、HEAD、upstream、冲突状态、staged、全部 dirty paths 和 retained
|
|
158
|
+
每个仓库按计划顺序执行。执行前重新检查 planned files、当前分支、HEAD、upstream、冲突状态、staged、全部 dirty paths 和 retained 摘要;普通推送还须逐个重验计划中已验证 GC 的完整哈希与当前 `@{u}..HEAD`。任一关键条件变化都停止当前执行并重新规划。普通模式仅 `retained` 内容变化时可更新说明;auto-loop 内部模式的 retained 内容必须保持不变。
|
|
146
159
|
|
|
147
160
|
计划包含本地生成命令时,前置仓成功后按计划执行命令,再复用本节现有预检。命令成功、后续仓全部 dirty paths(auto-loop 内部 `commit-only` 扣除已登记且验证未变化的 retained paths)都在预计 exact files 内且 retained 摘要未漂移时直接继续;否则停止并重新生成计划。预计文件最终 clean 时不强行提交。
|
|
148
161
|
|
|
@@ -220,7 +233,7 @@ auto-loop 内部链失败时向调用方返回全部已完成仓库提交和失
|
|
|
220
233
|
|
|
221
234
|
- 当前任务 exact files 与首次确认的路径集合一致,没有新增当前任务路径或无法归属的 dirty 内容。
|
|
222
235
|
- 父仓分支、upstream 和冲突状态安全。
|
|
223
|
-
-
|
|
236
|
+
- 对当前 `@{u}..HEAD` 重做 Step 2 归属检查;已验证的自动 GC 可随任务记录推送,其它无法归属的历史 ahead commits 仍停止。
|
|
224
237
|
|
|
225
238
|
全部业务 commit/push 成功时,通过 helper 用同一份最终 progress 原子写入 `progress`、`status=completed`、`completedAt` 与确定性 `closeout`:
|
|
226
239
|
|
|
@@ -251,7 +264,7 @@ git push origin <current-branch>
|
|
|
251
264
|
- 任务记录 commit 成功但 push 失败:任务目录应为 clean,并保留可归属的 ahead commit;后续只重试该 commit 的 push,不重复业务提交、helper 写入或任务记录 commit。
|
|
252
265
|
- 任务记录 push 成功:本任务产生的当前任务目录变更必须 clean;不得再次写入完成态或 Close。
|
|
253
266
|
|
|
254
|
-
任何恢复都必须验证当前分支、upstream、HEAD、`@{u}..HEAD`、任务记录 commit message 与 exact file set,以及 `task.json`
|
|
267
|
+
任何恢复都必须验证当前分支、upstream、HEAD、`@{u}..HEAD`、任务记录 commit message 与 exact file set,以及 `task.json` 的最终完成态。已验证自动 GC 可按 Step 2 默认许可归属;其它未知 ahead 或 dirty 不能当作可恢复任务记录。
|
|
255
268
|
|
|
256
269
|
## Step 6:结果
|
|
257
270
|
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
固定当前任务路径与 `task.json`,读取文件级任务状态、`closeout`、当前分支、upstream、`HEAD`、`@{u}..HEAD` 的提交消息与文件集合,以及 `python3 ./.trellis/scripts/auto_loop.py status --verbose`。所有恢复都必须验证 exact task、最终 progress、`status=completed`、Close 状态、分支和提交归属;progress 文本不能替代 runtime 或 Git 证据。
|
|
8
8
|
|
|
9
|
+
普通发布恢复逐个审计 ahead 提交。除本任务可归属的业务或任务记录提交外,只有按同目录 `SKILL.md` Step 2 及 `scripts/verify_gc_commit.py` 验证的纯自动 GC 提交可使用默认许可;仅凭 GC 消息或路径名不构成证据。
|
|
10
|
+
|
|
9
11
|
## Outcomes
|
|
10
12
|
|
|
11
13
|
按以下优先级只返回一个结果:
|
|
12
14
|
|
|
13
15
|
1. **Auto-loop 本地完成,无需 Push**:健康的终态或 recent auto-loop run 精确包含当前任务,记录的本地提交仍可验证,且任务 dirty 仅为 runner 在提交后写入的 `<task-dir>/task.json` progress/lifecycle bookkeeping。停止 Push;`closeout.status=closed` 时无需其它生命周期动作。
|
|
14
16
|
2. **任务记录 commit + push 恢复计划**:当前任务 exact files 仍 dirty,`task.json` 已包含合法最终 progress 与 `status=completed`,且文件集合可由首次确认或重新确认闭合。不得重复业务提交或 helper 写入;即使 Close 已成功,也只恢复已确认的任务记录发布。
|
|
15
|
-
3. **任务记录 push-only 恢复计划**:当前任务目录 clean;upstream 存在;`@{u}..HEAD` 中存在消息、exact file set 和完成态均可归属的任务记录 commit
|
|
17
|
+
3. **任务记录 push-only 恢复计划**:当前任务目录 clean;upstream 存在;`@{u}..HEAD` 中存在消息、exact file set 和完成态均可归属的任务记录 commit;其余 ahead 仅含已验证自动 GC 或其它可归属本任务的提交。只推送这些已存在提交,不创建新 commit。
|
|
16
18
|
4. **Close 待解阻**:任务记录已经同步,`closeout.status=blocked|pending`,且 blockers 证据一致。报告结构化 blockers;条件解决后只重试 `task.py close <task>`,持久化语义 blocker 由所属 owner 完成后追加 `--resolve-blocker <code>`,不重复业务 Git 动作或 progress 写入。
|
|
17
19
|
5. **无需动作**:任务记录已经同步且 `closeout.status=closed`。任务已退出活跃视图,物理 GC 由后续 SessionStart 在三天阈值后处理。
|
|
18
|
-
6. **阻断**:runtime 与 Git 矛盾、auto-loop marker 无健康完成证据、缺少普通路径 upstream、任务 dirty 超出 exact files
|
|
20
|
+
6. **阻断**:runtime 与 Git 矛盾、auto-loop marker 无健康完成证据、缺少普通路径 upstream、任务 dirty 超出 exact files、存在未通过 GC 审计且无法归属的 ahead,或提交消息/文件集合/分支无法闭合。报告具体证据缺口,不 push、不 Close,也不猜测完成来源。
|
|
19
21
|
|
|
20
22
|
恢复计划仍使用 `trellis-push` 的既有一次确认、执行前漂移检查和结果模板。这里只决定完成态恢复范围,不新增状态、持久化字段或自动确认。
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- **变更**:<N> 个文件 · `+<adds> -<deletes>`
|
|
24
24
|
- **父提交**:`<pre-merge-head>` + `<merge-head>`(仅已有 merge 时显示)
|
|
25
25
|
- **Push**:<执行 / 跳过(commit-only)>
|
|
26
|
+
- **已验证自动 GC**:<随本次推送的历史 GC short hashes>(仅存在时显示)
|
|
26
27
|
|
|
27
28
|
计划提交:
|
|
28
29
|
- <exact files 或分组摘要>
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
- 计划中单仓 `planned` 不超过 8 个文件时完整列出。
|
|
55
56
|
- 超过 8 个时按目录归组,最多 12 行;用户要求展开时展示同一 exact set。
|
|
56
57
|
- 顶部仓库/commit/file 总数包含独立任务记录提交所在 Git root、该提交及其 exact files;任务记录文件使用相同的 8 文件展示阈值和展开规则。
|
|
58
|
+
- 已验证自动 GC 是已有的历史 ahead,不计入顶部本轮新建 commit/file 数、业务 planned 或任务记录 exact files;仅在实际将随普通推送发布时展示其仓库和 short hashes,不作为风险或第二次确认项。
|
|
57
59
|
- 计划中的保留变更按仓库计数:不超过 8 项时逐项标注 `[untracked]`、`[unstaged]`、`[staged]`;超过 8 项时将非 staged 项按目录与 Git 状态汇总数量,每仓最多 12 行,必要时合并到上级目录。同一路径计数一次,兼有 staged/unstaged 时同时标注。
|
|
58
60
|
- 计划中的计划外 staged 项始终逐项单列,不计入分组摘要;计划和结果中的真正风险均在独立“风险”区逐项展示,不受行数限制。分组、展开均只改变展示,不改变 exact set 或确认范围。
|
|
59
61
|
- 用户要求“展开保留变更”时在对话中列出同一 exact set 与 Git 状态,不生成清单附件;“展开文件”仍指 planned files。
|
|
@@ -73,6 +75,7 @@
|
|
|
73
75
|
[推送成功 / 仅本地提交成功 / 部分完成 / 失败]
|
|
74
76
|
|
|
75
77
|
- **<repository-name>**:`<branch> → <upstream>` · `<short-hash[, short-hash...]>` · <已推送 / 仅本地 / 失败 / 未执行>
|
|
78
|
+
- **已验证自动 GC**:<repository-name> · <short hashes> · <已推送 / 未推送待恢复>(仅存在时显示)
|
|
76
79
|
- **任务记录**:`<task-record-hash>` · <completed + Close 已同步 / completed 但 Close blocked / partial 已同步,仍为 in_progress / commit 待恢复 / push 待恢复 / 同步失败>
|
|
77
80
|
- **保留未提交的变更(dirty)**:<每仓数量与实际核对结论>
|
|
78
81
|
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
- 仅普通模式且存在活动任务时显示“任务记录”行;没有实际 commit hash 时省略 hash,不填占位值。用户 `commit-only` 的仓库行只写当前分支与本地提交,不显示推送箭头或暗示已同步远端。
|
|
93
96
|
- untracked 结果用“无任务状态”行替代“任务记录”,展示 work id 与 `<已清理/保留待恢复>`;不生成或暗示 task progress commit。没有保留变更、失败或风险时省略对应行或章节。
|
|
94
97
|
- 部分完成时必须明确列出已成功仓库、失败仓库/步骤、当前分支和下一恢复动作。业务结果与 progress sync 状态不得合并成一个模糊结论。
|
|
98
|
+
- 结果中的已验证自动 GC 只报告实际随对应仓库推送的历史提交;push 失败时标明待恢复,不把本地 commit 误报为已推送,也不重复为 GC 生成提交。
|
|
95
99
|
- 普通成功结果必须确认本任务产生的当前任务目录变更 clean。其它 retained dirty(含计划外 staged)仍逐项核验,已核对保持原状时每仓只报告数量与结论,不重复清单。异常或未核验项列出路径、实际状态和处理情况,不得笼统声称全部保持原状;用户要求详情时再展示实际文件、message、命令或进度,展开文件仍沿用共用规则。
|
|
96
100
|
- Git 成功不消除现有风险;成功结果省略未变化且接受仍有效的问题,不重复接受数量或原影响说明。结果的“风险”区保留新增、变化、接受失效或无法验证的事项,以及仍适用的其它完成链风险与 `[上线后验证]`;用户要求详情时再展开原问题与处置。
|
|
97
101
|
- helper 成功但任务记录 commit 失败时,结果写“任务记录 commit 待恢复”,说明本地 `completed`、Close 结果与 exact task dirty 已保留;任务记录 commit 成功但 push 失败时写“任务记录 push 待恢复”,说明 clean ahead commit 已保留。两种情况都不得暗示需要重复业务提交、helper 写入或 Close。
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""只读核验 Trellis 物理 GC 提交是否为纯归档移动。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
GC_MESSAGE = b"chore(task): gc closed tasks\n"
|
|
17
|
+
TASK_ROOT = b".trellis/tasks/"
|
|
18
|
+
ARCHIVE_ROOT = TASK_ROOT + b"archive/"
|
|
19
|
+
COMMIT_RE = re.compile(r"[0-9a-fA-F]{40}(?:[0-9a-fA-F]{24})?\Z")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GcCommitAuditError(Exception):
|
|
23
|
+
"""表示提交缺少默认放行所需的确定性证据。"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _git(repo_root: Path, *args: str) -> bytes:
|
|
27
|
+
"""以字节形式读取 Git,保留路径中的制表符及非 ASCII 字节。"""
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["git", "-C", str(repo_root), *args],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
check=False,
|
|
32
|
+
)
|
|
33
|
+
if result.returncode != 0:
|
|
34
|
+
detail = result.stderr.decode("utf-8", errors="replace").strip()
|
|
35
|
+
raise GcCommitAuditError(f"Git 查询失败:{detail or '未知错误'}")
|
|
36
|
+
return result.stdout
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tree(repo_root: Path, commit: str) -> dict[bytes, tuple[bytes, bytes]]:
|
|
40
|
+
"""读取任务目录的路径、文件模式和 blob ID。"""
|
|
41
|
+
raw = _git(repo_root, "ls-tree", "-r", "-z", commit, "--", ".trellis/tasks")
|
|
42
|
+
entries: dict[bytes, tuple[bytes, bytes]] = {}
|
|
43
|
+
for record in raw.split(b"\0"):
|
|
44
|
+
if not record:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
header, path = record.split(b"\t", 1)
|
|
48
|
+
mode, kind, oid = header.split(b" ")
|
|
49
|
+
except ValueError as error:
|
|
50
|
+
raise GcCommitAuditError("Git 树条目格式损坏") from error
|
|
51
|
+
if kind != b"blob" or path in entries:
|
|
52
|
+
raise GcCommitAuditError("任务树包含非文件或重复路径")
|
|
53
|
+
entries[path] = (mode, oid)
|
|
54
|
+
return entries
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _changes(repo_root: Path, parent: str, commit: str) -> tuple[set[bytes], set[bytes]]:
|
|
58
|
+
"""读取无重命名推断的新增和删除路径。"""
|
|
59
|
+
raw = _git(
|
|
60
|
+
repo_root,
|
|
61
|
+
"diff-tree",
|
|
62
|
+
"--no-commit-id",
|
|
63
|
+
"--no-renames",
|
|
64
|
+
"--ignore-submodules=none",
|
|
65
|
+
"--name-status",
|
|
66
|
+
"-r",
|
|
67
|
+
"-z",
|
|
68
|
+
parent,
|
|
69
|
+
commit,
|
|
70
|
+
)
|
|
71
|
+
records = raw.split(b"\0")
|
|
72
|
+
if not records or records[-1] != b"" or len(records[:-1]) % 2:
|
|
73
|
+
raise GcCommitAuditError("Git 差异记录格式损坏")
|
|
74
|
+
deleted: set[bytes] = set()
|
|
75
|
+
added: set[bytes] = set()
|
|
76
|
+
for index in range(0, len(records) - 1, 2):
|
|
77
|
+
status, path = records[index:index + 2]
|
|
78
|
+
if not path or path in deleted or path in added:
|
|
79
|
+
raise GcCommitAuditError("Git 差异包含空路径或重复路径")
|
|
80
|
+
if status == b"D":
|
|
81
|
+
deleted.add(path)
|
|
82
|
+
elif status == b"A":
|
|
83
|
+
added.add(path)
|
|
84
|
+
else:
|
|
85
|
+
raise GcCommitAuditError("提交包含非归档移动的文件修改")
|
|
86
|
+
if not deleted:
|
|
87
|
+
raise GcCommitAuditError("提交没有顶层任务文件删除")
|
|
88
|
+
return deleted, added
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _source_name(path: bytes) -> bytes:
|
|
92
|
+
"""从被删除文件中提取顶层任务名。"""
|
|
93
|
+
if not path.startswith(TASK_ROOT):
|
|
94
|
+
raise GcCommitAuditError("提交修改了任务目录以外的路径")
|
|
95
|
+
remainder = path[len(TASK_ROOT):]
|
|
96
|
+
name, separator, relative = remainder.partition(b"/")
|
|
97
|
+
if not separator or not name or name == b"archive" or not relative:
|
|
98
|
+
raise GcCommitAuditError("被删除文件不属于顶层任务")
|
|
99
|
+
return name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _bucket(repo_root: Path, task_json: tuple[bytes, bytes]) -> bytes:
|
|
103
|
+
"""校验关闭态,并返回生成器使用的 UTC 归档月份。"""
|
|
104
|
+
mode, oid = task_json
|
|
105
|
+
if mode not in {b"100644", b"100755"}:
|
|
106
|
+
raise GcCommitAuditError("task.json 不是普通文件")
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(_git(repo_root, "cat-file", "blob", oid.decode("ascii")).decode("utf-8"))
|
|
109
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
110
|
+
raise GcCommitAuditError("task.json 不是有效的 UTF-8 JSON") from error
|
|
111
|
+
if not isinstance(data, dict) or data.get("status") != "completed":
|
|
112
|
+
raise GcCommitAuditError("归档任务不是 completed")
|
|
113
|
+
closeout = data.get("closeout")
|
|
114
|
+
if not isinstance(closeout, dict) or closeout.get("status") != "closed" or closeout.get("blockers") != []:
|
|
115
|
+
raise GcCommitAuditError("归档任务没有合法的 closed 状态")
|
|
116
|
+
closed_at = closeout.get("closedAt")
|
|
117
|
+
if not isinstance(closed_at, str):
|
|
118
|
+
raise GcCommitAuditError("归档任务缺少关闭时间")
|
|
119
|
+
try:
|
|
120
|
+
parsed = datetime.fromisoformat(closed_at.strip().replace("Z", "+00:00"))
|
|
121
|
+
except ValueError as error:
|
|
122
|
+
raise GcCommitAuditError("归档任务的关闭时间无效") from error
|
|
123
|
+
if parsed.tzinfo is None:
|
|
124
|
+
raise GcCommitAuditError("归档任务的关闭时间缺少时区")
|
|
125
|
+
return parsed.astimezone(timezone.utc).strftime("%Y-%m").encode("ascii")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def verify_gc_commit(repo_root: Path, commit: str) -> dict[str, Any]:
|
|
129
|
+
"""验证单个提交仅将已关闭任务原字节移入归档。
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
repo_root: 待检查的 Git 仓库根目录。
|
|
133
|
+
commit: 完整的 40 或 64 位提交哈希。
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
已验证提交、任务名与移动文件数的结构化摘要。
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
GcCommitAuditError: 提交不满足纯 GC 归档契约或 Git 证据不可读。
|
|
140
|
+
"""
|
|
141
|
+
if not COMMIT_RE.fullmatch(commit):
|
|
142
|
+
raise GcCommitAuditError("必须提供完整提交哈希")
|
|
143
|
+
resolved = _git(repo_root, "rev-parse", "--verify", f"{commit}^{{commit}}").strip().decode("ascii")
|
|
144
|
+
header, separator, message = _git(repo_root, "cat-file", "-p", resolved).partition(b"\n\n")
|
|
145
|
+
parents = [line[7:].decode("ascii") for line in header.splitlines() if line.startswith(b"parent ")]
|
|
146
|
+
if not separator or len(parents) != 1 or message != GC_MESSAGE:
|
|
147
|
+
raise GcCommitAuditError("提交父节点或消息不符合自动 GC 契约")
|
|
148
|
+
parent = parents[0]
|
|
149
|
+
deleted, added = _changes(repo_root, parent, resolved)
|
|
150
|
+
before = _tree(repo_root, parent)
|
|
151
|
+
after = _tree(repo_root, resolved)
|
|
152
|
+
names = {_source_name(path) for path in deleted}
|
|
153
|
+
expected_added: set[bytes] = set()
|
|
154
|
+
reported_names: list[str] = []
|
|
155
|
+
for name in sorted(names):
|
|
156
|
+
source_prefix = TASK_ROOT + name + b"/"
|
|
157
|
+
source_files = {path for path in before if path.startswith(source_prefix)}
|
|
158
|
+
if not source_files or deleted.intersection(source_files) != source_files:
|
|
159
|
+
raise GcCommitAuditError("顶层任务未完整移走")
|
|
160
|
+
task_json = before.get(source_prefix + b"task.json")
|
|
161
|
+
if task_json is None:
|
|
162
|
+
raise GcCommitAuditError("顶层任务缺少 task.json")
|
|
163
|
+
destination_prefix = ARCHIVE_ROOT + _bucket(repo_root, task_json) + b"/" + name + b"/"
|
|
164
|
+
if any(path.startswith(source_prefix) for path in after):
|
|
165
|
+
raise GcCommitAuditError("提交后仍残留顶层任务文件")
|
|
166
|
+
for source in source_files:
|
|
167
|
+
destination = destination_prefix + source[len(source_prefix):]
|
|
168
|
+
if after.get(destination) != before[source]:
|
|
169
|
+
raise GcCommitAuditError("归档文件与原文件内容或模式不同")
|
|
170
|
+
if destination not in before:
|
|
171
|
+
expected_added.add(destination)
|
|
172
|
+
try:
|
|
173
|
+
reported_names.append(name.decode("utf-8"))
|
|
174
|
+
except UnicodeDecodeError as error:
|
|
175
|
+
raise GcCommitAuditError("任务名不是 UTF-8") from error
|
|
176
|
+
if added != expected_added:
|
|
177
|
+
raise GcCommitAuditError("提交包含未归属的新增路径")
|
|
178
|
+
return {"status": "verified", "commit": resolved, "tasks": reported_names, "files": len(deleted)}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(argv: list[str] | None = None) -> int:
|
|
182
|
+
"""解析只读校验参数并输出机器可读的结果。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
argv: 测试可传入的命令行参数;缺省使用进程参数。
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
验证通过返回 0,证据不足或 Git 查询失败返回 1。
|
|
189
|
+
"""
|
|
190
|
+
parser = argparse.ArgumentParser(description="核验 Trellis 纯归档 GC 提交")
|
|
191
|
+
parser.add_argument("--repo", type=Path, required=True)
|
|
192
|
+
parser.add_argument("--commit", required=True)
|
|
193
|
+
args = parser.parse_args(argv)
|
|
194
|
+
try:
|
|
195
|
+
result = verify_gc_commit(args.repo, args.commit)
|
|
196
|
+
except GcCommitAuditError as error:
|
|
197
|
+
result = {"status": "rejected", "commit": args.commit, "reason": str(error)}
|
|
198
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
199
|
+
return 0 if result["status"] == "verified" else 1
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-09-
|
|
2
|
+
"syncedAt": "2026-09-26T04:08:42.758Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "eb848932da1890aaf66804b69c483072b1685b05",
|
|
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.10-beta.
|
|
3
|
+
"version": "0.6.10-beta.7",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"commit-and-tag-version": "^12.7.3"
|
|
71
71
|
},
|
|
72
72
|
"flowerReleaseNotes": {
|
|
73
|
-
"version": "0.6.10-beta.
|
|
73
|
+
"version": "0.6.10-beta.7",
|
|
74
74
|
"source": "CHANGELOG.md",
|
|
75
|
-
"body": "### ✨ 新功能 Features\n\n* **
|
|
75
|
+
"body": "### ✨ 新功能 Features\n\n* **flower:** 支持自动 GC 提交默认放行 ([79ef652](https://github.com/SilentFlower/flower-trellis/commit/79ef652abf793c4b84e8b5150b4808be02ad3eec))\n* **flower:** 默认只保留一份升级备份并支持 Sol 提示 ([7704dc9](https://github.com/SilentFlower/flower-trellis/commit/7704dc9e190abc5d6e57de517e2dfc0ec31cbbae))",
|
|
76
76
|
"truncated": false
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
@@ -19,10 +19,10 @@ HOOKS = (".codex/hooks/session-start.py", ".claude/hooks/session-start.py")
|
|
|
19
19
|
MAX_PART_CHARS = 8000
|
|
20
20
|
WORKFLOW_BLOCK = re.compile(r"<trellis-workflow>\n(.*?)\n</trellis-workflow>\n*", re.DOTALL)
|
|
21
21
|
ASTRA_MODEL = "gpt-6-astra"
|
|
22
|
-
|
|
22
|
+
SOL_MODEL = "gpt-6-sol"
|
|
23
|
+
MODEL_HINT_MAX_BYTES = 2048
|
|
23
24
|
WORKFLOW_STATE_REFRESH_ARG = "--trellis-session-start-refresh"
|
|
24
|
-
|
|
25
|
-
Applies only while the active model is gpt-6-astra; it does not apply after switching models. Perform checks internally, without a routine checklist report. Keep ordinary answers brief.
|
|
25
|
+
MODEL_WORKFLOW_HINT_BODY = """Applies only while the active model is {model}; it does not apply after switching models. Perform checks internally, without a routine checklist report. Keep ordinary answers brief.
|
|
26
26
|
When executing the current task:
|
|
27
27
|
- Act autonomously within applicable SKILL and WORKFLOW steps. Do not waive required steps, references, review gates or templates because work seems simple, a check seems redundant, or fewer interruptions are preferred.
|
|
28
28
|
- Before a step, review its rules and required references. Reuse material already read in full and unchanged; search matches are not full reads.
|
|
@@ -30,14 +30,22 @@ When executing the current task:
|
|
|
30
30
|
- Resolve conflicts by instruction hierarchy. Follow the owning workflow's phase-transition and review requirements. Permission to begin planning does not approve the final plan.
|
|
31
31
|
- Where the workflow requires review of a displayed plan, a generic request such as "commit and push" starts that workflow; it does not itself confirm a plan produced afterward. Reuse approval of the same plan or an explicit waiver within its scope, including valid auto-loop preauthorization. Otherwise, display the plan and wait for the user's confirmation before acting. Verify the actual user reply; displaying a plan or saying "executing as authorized" is not confirmation.
|
|
32
32
|
- Before claiming "read", "checked", or "complete", verify actual tool records and artifacts. Successful reading and compliant execution are separate facts.
|
|
33
|
-
- When corrected, review the applicable rules, execution records, and actual result before repairing it. If evidence is missing, state uncertainty. Do not invent causes such as "not read", "forgot", or "file missing", or consult unrelated rules in place of the relevant ones.
|
|
34
|
-
|
|
33
|
+
- When corrected, review the applicable rules, execution records, and actual result before repairing it. If evidence is missing, state uncertainty. Do not invent causes such as "not read", "forgot", or "file missing", or consult unrelated rules in place of the relevant ones."""
|
|
34
|
+
ASTRA_WORKFLOW_HINT = (f'<trellis-astra-workflow-hint model="{ASTRA_MODEL}" version="1">\n'
|
|
35
|
+
f'{MODEL_WORKFLOW_HINT_BODY.format(model=ASTRA_MODEL)}\n'
|
|
36
|
+
'</trellis-astra-workflow-hint>')
|
|
37
|
+
SOL_WORKFLOW_HINT = (f'<trellis-sol-workflow-hint model="{SOL_MODEL}" version="1">\n'
|
|
38
|
+
f'{MODEL_WORKFLOW_HINT_BODY.format(model=SOL_MODEL)}\n'
|
|
39
|
+
'</trellis-sol-workflow-hint>')
|
|
35
40
|
|
|
36
41
|
|
|
37
|
-
def
|
|
38
|
-
"""
|
|
42
|
+
def _model_workflow_hint(root: Path, config_key: str, hint: str, label: str) -> str:
|
|
43
|
+
"""读取指定模型的项目开关,返回该模型提示或空串。
|
|
39
44
|
|
|
40
45
|
@param root: 当前部署项目根目录。
|
|
46
|
+
@param config_key: codex 配置下的模型开关键。
|
|
47
|
+
@param hint: 对应模型的完整提示块。
|
|
48
|
+
@param label: 用于诊断的模型名称。
|
|
41
49
|
@return: 完整提示块;显式关闭时为空串,非法配置或超预算时抛出异常。
|
|
42
50
|
"""
|
|
43
51
|
scripts_dir = str(root / ".trellis" / "scripts")
|
|
@@ -49,17 +57,35 @@ def _astra_workflow_hint(root: Path) -> str:
|
|
|
49
57
|
codex = config.get("codex", {})
|
|
50
58
|
if not isinstance(codex, dict):
|
|
51
59
|
raise ValueError("codex 配置必须为映射")
|
|
52
|
-
enabled = codex.get(
|
|
60
|
+
enabled = codex.get(config_key, True)
|
|
53
61
|
# 上游无依赖 YAML 读取器返回字符串,不能把字符串 false 当作真值。
|
|
54
62
|
if isinstance(enabled, str) and enabled.lower() in ("true", "false"):
|
|
55
63
|
enabled = enabled.lower() == "true"
|
|
56
64
|
if not isinstance(enabled, bool):
|
|
57
|
-
raise ValueError("codex.
|
|
65
|
+
raise ValueError(f"codex.{config_key} 必须为 true 或 false")
|
|
58
66
|
if not enabled:
|
|
59
67
|
return ""
|
|
60
|
-
if len(
|
|
61
|
-
raise ValueError("
|
|
62
|
-
return
|
|
68
|
+
if len(hint.encode("utf-8")) > MODEL_HINT_MAX_BYTES:
|
|
69
|
+
raise ValueError(f"{label} 工作流提示超过 2048 字节预算")
|
|
70
|
+
return hint
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _astra_workflow_hint(root: Path) -> str:
|
|
74
|
+
"""读取 Astra 独立开关并返回提示。
|
|
75
|
+
|
|
76
|
+
@param root: 当前部署项目根目录。
|
|
77
|
+
@return: Astra 提示块;关闭时为空串。
|
|
78
|
+
"""
|
|
79
|
+
return _model_workflow_hint(root, "astra_workflow_hint", ASTRA_WORKFLOW_HINT, "Astra")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _sol_workflow_hint(root: Path) -> str:
|
|
83
|
+
"""读取 Sol 独立开关并返回提示。
|
|
84
|
+
|
|
85
|
+
@param root: 当前部署项目根目录。
|
|
86
|
+
@return: Sol 提示块;关闭时为空串。
|
|
87
|
+
"""
|
|
88
|
+
return _model_workflow_hint(root, "sol_workflow_hint", SOL_WORKFLOW_HINT, "Sol")
|
|
63
89
|
|
|
64
90
|
|
|
65
91
|
def split_workflow(summary: str) -> dict[str, str]:
|
|
@@ -285,17 +311,24 @@ def render_part(root: Path, hook: str, part: str, hook_input: dict) -> dict | No
|
|
|
285
311
|
message = f"workflow-state 基线未刷新:{error}"
|
|
286
312
|
result["systemMessage"] = "\n".join(filter(None, [result.get("systemMessage"), message]))
|
|
287
313
|
print(message, file=sys.stderr)
|
|
288
|
-
if
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
if hook == HOOKS[0] and source in ("startup", "clear", "compact"):
|
|
315
|
+
model = hook_input.get("model")
|
|
316
|
+
hint_builder = None
|
|
317
|
+
label = ""
|
|
318
|
+
if model == ASTRA_MODEL:
|
|
319
|
+
hint_builder, label = _astra_workflow_hint, "Astra"
|
|
320
|
+
elif model == SOL_MODEL:
|
|
321
|
+
hint_builder, label = _sol_workflow_hint, "Sol"
|
|
322
|
+
if hint_builder is not None:
|
|
323
|
+
try:
|
|
324
|
+
hint = hint_builder(root)
|
|
325
|
+
if hint:
|
|
326
|
+
context = f"{context}\n{hint}"
|
|
327
|
+
except Exception as error:
|
|
328
|
+
# 提示是可选增强;失败不能吞掉已成功生成的原生启动上下文。
|
|
329
|
+
message = f"{label} 工作流提示未注入:{error}"
|
|
330
|
+
result["systemMessage"] = "\n".join(filter(None, [result.get("systemMessage"), message]))
|
|
331
|
+
print(message, file=sys.stderr)
|
|
299
332
|
else:
|
|
300
333
|
module = _load_hook(root, hook)
|
|
301
334
|
if module.should_skip_injection():
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ProjectStore } from "./plugin/state/project-store.js";
|
|
|
8
8
|
import { runTrellis } from "./lib/trellis-runner.js";
|
|
9
9
|
import { parseCliArgs } from "./lib/cli-args.js";
|
|
10
10
|
import { installWindowsTerminalInputRecovery, scheduleWindowsTerminalExit } from "./lib/terminal-state.js";
|
|
11
|
+
import { DEFAULT_UPDATE_BACKUP_RETENTION } from "./constants.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* flower-trellis CLI 主入口。
|
|
@@ -98,7 +99,7 @@ flower 自有 flag:
|
|
|
98
99
|
--variant <old|0.5|0.6> 强制强化包变体(默认按 .trellis/.version 自动选)
|
|
99
100
|
--target <dir> 目标目录(默认当前目录)
|
|
100
101
|
--no-update-check 本次跳过 flower-trellis 新版本检测(等价 FLOWER_NO_UPDATE_CHECK=1)
|
|
101
|
-
--backup-retention <n> update 成功后保留最近 n 份升级备份(默认
|
|
102
|
+
--backup-retention <n> update 成功后保留最近 n 份升级备份(默认 ${DEFAULT_UPDATE_BACKUP_RETENTION},0=不清理)
|
|
102
103
|
|
|
103
104
|
启动更新检查:
|
|
104
105
|
self-check --json [--manual] 稳定输出检查 JSON;manual 只绕过提示节流
|
package/src/constants.js
CHANGED