flower-trellis 0.4.0-beta.4 → 0.4.0-beta.5

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.
@@ -101,12 +101,13 @@ merge 目标仍从对应 package 的 `merge_target` 读取;父仓没有 packag
101
101
  ```bash
102
102
  # 在仓库根目录(含 .trellis/ 的项目根,不是 package 子目录)
103
103
  python3 ./.trellis/scripts/task.py current
104
+ python3 ./.trellis/scripts/push_snapshot.py status --json
104
105
  ```
105
106
 
106
107
  - **无活动任务**(命令退出码非 0 或输出为空):在计划中标记 `snapshot: 跳过(无活动任务)`。
107
108
  - **有活动任务**:解析输出拿到 task 路径(形如 `.trellis/tasks/MM-DD-name/`),并读取下列内容:
108
109
  - `<task_dir>/implement.md`(如有)— 步骤清单,AI 推断进度的主依据
109
- - `<task_dir>/task.json` 的 `last_push_snapshot` 字段(如有)— 上次推送时的进度基线
110
+ - `push_snapshot.py status --json` 的 `snapshot` / `summary`(如有)— 上次推送时的进度基线;若返回 `no-snapshot` / `no-current-task`,按无上次 snapshot 继续
110
111
  - `<task_dir>/task.json` 的 `base_branch` 字段 — 用于 `git log <base_branch>..HEAD` 圈定本任务 commit 范围
111
112
 
112
113
  ---
@@ -192,6 +193,7 @@ commit message 生成规则:
192
193
  - branch: 执行成功后按实际分支补齐
193
194
  - pushed_commits: 执行成功后按实际 commit hash 补齐
194
195
  - snapshot_at: 执行成功后写入当前 ISO 8601 时间
196
+ - push_mode: <push / commit-only / snapshot-only / 指定仓库 / reconfigure / 临时目标>
195
197
  - bookkeeping: 只提交 `<task_dir>/task.json`
196
198
  ```
197
199
 
@@ -205,6 +207,7 @@ commit message 生成规则:
205
207
  "frontend": "abc1234",
206
208
  "backend": "def5678"
207
209
  },
210
+ "push_mode": "push",
208
211
  "completed_steps": ["Step 1", "Step 2"],
209
212
  "partial_step": "Step 3(可选)",
210
213
  "next_step": "Step 4(可选)",
@@ -214,7 +217,9 @@ commit message 生成规则:
214
217
 
215
218
  > `pushed_commits`、`snapshot_at`、实际 branch / commit hash 是运行后字段。用户确认的是 snapshot 的语义内容和写入动作;执行成功后由 AI 按实际结果补齐。
216
219
 
217
- commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻辑;值记录本次生成的本地 commit hash,并在 `notes` 中注明“commit-only:本地已提交,未推送”。
220
+ `push_mode` 记录本轮确认的模式。默认实际 push 模式写 `"push"`;commit-only `"commit-only"`;snapshot-only `"snapshot-only"`;指定仓库、reconfigure 或临时目标可写对应模式名。
221
+
222
+ commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻辑;值记录本次生成的本地 commit hash,`push_mode` 写 `"commit-only"`,并在 `notes` 中注明“commit-only:本地已提交,未推送”。
218
223
 
219
224
  ### 2.3 展示确认模板
220
225
 
@@ -356,6 +361,7 @@ git checkout <current_branch>
356
361
  - `snapshot_at`:当前 ISO 8601 时间戳
357
362
  - `branch`:实际分支;多仓不同分支时使用字典
358
363
  - `pushed_commits`:各 package 的短 hash
364
+ - `push_mode`:统一执行计划确认的模式;默认 push 写 `"push"`,commit-only 写 `"commit-only"`,snapshot-only 写 `"snapshot-only"`
359
365
  - `notes`:保留已确认 notes;commit-only 模式追加“本地已提交,未推送”
360
366
 
361
367
  写入前先复核目标文件:
@@ -369,13 +375,16 @@ git status --porcelain -- <task_json_path> [".trellis/config.yaml"]
369
375
 
370
376
  写入方式:
371
377
 
372
- 1. `<task_dir>/task.json`
373
- 2. 解析 JSON
374
- 3. 只设置 / 更新 `last_push_snapshot` 字段
375
- 4. 保留其它字段原样
376
- 5. 写回并保持原有缩进(通常 2 空格)
378
+ 1. 将已确认语义内容与运行后字段合并成完整 snapshot JSON。
379
+ 2. 调用 helper 写入:
380
+
381
+ ```bash
382
+ python3 ./.trellis/scripts/push_snapshot.py write --task <task_dir> --snapshot-json '<完整 snapshot JSON>'
383
+ ```
384
+
385
+ 3. 确认 helper 成功返回;失败时停止并展示错误,不继续父仓 bookkeeping commit。
377
386
 
378
- 不要覆盖整个 `task.json`。
387
+ helper 只允许更新 `last_push_snapshot` 字段;不要绕过 helper 手工覆盖整个 `task.json`。
379
388
 
380
389
  ### 5.2 父仓 status 检查
381
390
 
@@ -25,7 +25,7 @@ description: |
25
25
 
26
26
  个人 route 配置只决定“已获准执行后的模式”,不是开工授权。调用 helper 前,必须确认当前 workflow 已允许进入对应 target:implement 需要任务已完成规划确认并处于 `in_progress`;check 用于 Phase 2.2 检查执行,或用户明确要求最终复查 / 轻量检查。最终复查只有在 Phase 2.2 结果缺失、风险较高或用户明确要求复查时才回到 Phase 2.2;回到 Phase 2.2 后优先复用当前任务的合法 check route,除非用户明确要求重选/临时改/清除默认。如果仍在 planning、等待用户确认,或用户表达“等一下 / 我再想想”,停止,不读取 runtime/prefs。
27
27
 
28
- 合法 route 决策必须能追溯到 `trellis-route`、同编号 fallback 选项、由本 skill 读取到的有效 `.trellis/.route-prefs.tmp` 配置,或由 route helper 校验过的 auto-loop 临时 route 授权,并且 `task` 字段必须等于当前 `task.py current --source` 返回的任务路径。runtime state 只能保存和恢复这些原始合法来源,不能把 `.runtime` 自身当成新的 `route_decision.source`。用户自然语言说过“inline/subagent”、compact summary、SessionStart 摘要、`codex-mode`、空 `.route-prefs.tmp`、旧单值偏好,都不能单独作为有效 route 决策。
28
+ 合法 route 决策必须能追溯到 `trellis-route`、同编号 fallback 选项、由本 skill 读取到的有效 `.trellis/.route-prefs.tmp` 配置,或由 route helper 校验过的 auto-loop 临时 route 授权,并且 `task` 字段必须等于当前 `task.py current --source` 返回的任务路径。runtime state 只能保存和恢复这些原始合法来源,不能把 `.runtime` 自身当成新的 `route_decision.source`。用户自然语言说过“inline/subagent”、compact summary、ordinary summary、SessionStart 摘要、replacement history、`codex-mode`、空 `.route-prefs.tmp`、旧单值偏好、历史用户裸数字,都不能单独作为有效 route 决策。
29
29
 
30
30
  当前上下文内已有 target 匹配、task 等于当前任务路径、且来源合法的 route 决策时,后续实现、check 发现问题、用户指出刚检查过的实现有问题、修复后重检、提交前复查均默认复用最近 implement/check 路由;除非用户明确要求重选/临时改/清除默认,不再调用本 skill。当前上下文没有 route 决策但 runtime state 命中时,本 skill 恢复该决策并输出同样的结构化 `route_decision`。如果上下文里只有上一个任务的 `route_decision`,必须忽略并重新解析当前任务。
31
31
 
@@ -72,7 +72,9 @@ helper 默认输出为精简 JSON,只包含 route 执行必需的 `status`、`
72
72
 
73
73
  优先调用 `AskUserQuestion`。选项 label 前缀编号,方便用户直接打数字快速选。
74
74
 
75
- 如果当前平台或模式没有 `AskUserQuestion` / `request_user_input`,不要自行选择 inline subagent 继续。改用普通聊天消息原样呈现同一组编号选项,并停止等待用户回复;用户回复数字后再进入 Step 2.5 / 2.6 / 3。
75
+ 裸数字回复(如 `1` / `2` / `3` / `4`)只有在当前可见的上一条 assistant 消息刚刚展示同一个 target route 选项、并明确停下等待用户回答时,才可解释为本次 numbered fallback 选择。compact summary、ordinary summary、SessionStart 摘要、replacement history、历史消息、旧 target 的裸数字、非紧邻回复里的裸数字都不能触发 `write --source numbered-fallback`;遇到这些情况必须重新展示当前 target 的选项并等待新的紧邻回复。
76
+
77
+ 如果当前平台或模式没有 `AskUserQuestion` / `request_user_input`,不要自行选择 inline 或 subagent 继续。改用普通聊天消息原样呈现同一组编号选项,并停止等待用户回复;只有用户在这条选项消息之后立即回复裸数字,才可进入 Step 2.5 / 2.6 / 3。
76
78
 
77
79
  ### target = implement,且 resolve 未命中
78
80
 
@@ -211,6 +213,7 @@ helper 写入规则:保留另一个 target 的 runtime 决策和偏好;覆
211
213
  [来自 session runtime route state:`.trellis/.runtime/sessions/<context-key>.json` 的 `route_decisions`。]
212
214
  [已写入 session runtime route state:`.trellis/.runtime/sessions/<context-key>.json` 的 `route_decisions`。]
213
215
  [说明:用户明确请求轻量检查,使用隐藏逃生口。]
216
+ [仅当本消息刚展示 route 选项并等待用户回答时:用户下一条紧邻裸数字回复才可按本 target 解释;摘要、历史消息或旧 target 的裸数字无效。]
214
217
 
215
218
  route_decision:
216
219
  target: <implement | check>
@@ -227,7 +230,7 @@ route_decision:
227
230
  - <要避免的工具调用>
228
231
  ```
229
232
 
230
- 中括号内行为条件性出现:仅命中个人配置时显示配置行;仅命中 runtime state 时显示“来自”行;写入 runtime state 成功时显示“已写入”行;仅轻量 check 时显示隐藏逃生口说明;仅 implement subagent + skip_compile=true 时附加“跳过编译”段。`route_decision` 必须保留在回复中,并至少保留 target/mode/source/scope/task;需要 path/decided_at 等诊断字段时重新调用 helper 并加 `--verbose`。compact summary 若只有自然语言描述,后续 agent 仍应优先读取 runtime state,而不是把 summary 当证据。
233
+ 中括号内行为条件性出现:仅命中个人配置时显示配置行;仅命中 runtime state 时显示“来自”行;写入 runtime state 成功时显示“已写入”行;仅轻量 check 时显示隐藏逃生口说明;仅展示选项并等待用户时显示裸数字有效性提醒;仅 implement subagent + skip_compile=true 时附加“跳过编译”段。`route_decision` 必须保留在回复中,并至少保留 target/mode/source/scope/task;需要 path/decided_at 等诊断字段时重新调用 helper 并加 `--verbose`。compact summary 若只有自然语言描述,后续 agent 仍应优先读取 runtime state,而不是把 summary 当证据。
231
234
 
232
235
  ---
233
236
 
@@ -260,6 +263,8 @@ route_decision:
260
263
  - `AskUserQuestion` / `request_user_input` 不可用时,记录为 inline 或 subagent 路径并继续。
261
264
  - 没有有效 check 配置、用户选择或最近本轮 check 路由决定时,自动执行 inline check。
262
265
  - 没有 `source` 合法的 `route_decision`,就把“用户说过 inline/subagent”或 compact summary 当成已路由。
266
+ - 把 compact summary、ordinary summary、SessionStart 摘要、replacement history 或历史消息里的裸数字 `1` 当成当前 target 的 numbered fallback 选择。
267
+ - 用户裸数字回复不是紧邻当前 route 选项消息时,仍调用 `write --source numbered-fallback`。
263
268
  - check 发现问题后,把当前任务内已有合法 route 的修复/重检当成新的 route 边界再次询问模式。
264
269
  - 给 check 任何模式附加“跳过编译”指令。
265
270
  - 询问后忽视用户答案默认 subagent。
@@ -101,12 +101,13 @@ merge 目标仍从对应 package 的 `merge_target` 读取;父仓没有 packag
101
101
  ```bash
102
102
  # 在仓库根目录(含 .trellis/ 的项目根,不是 package 子目录)
103
103
  python3 ./.trellis/scripts/task.py current
104
+ python3 ./.trellis/scripts/push_snapshot.py status --json
104
105
  ```
105
106
 
106
107
  - **无活动任务**(命令退出码非 0 或输出为空):在计划中标记 `snapshot: 跳过(无活动任务)`。
107
108
  - **有活动任务**:解析输出拿到 task 路径(形如 `.trellis/tasks/MM-DD-name/`),并读取下列内容:
108
109
  - `<task_dir>/implement.md`(如有)— 步骤清单,AI 推断进度的主依据
109
- - `<task_dir>/task.json` 的 `last_push_snapshot` 字段(如有)— 上次推送时的进度基线
110
+ - `push_snapshot.py status --json` 的 `snapshot` / `summary`(如有)— 上次推送时的进度基线;若返回 `no-snapshot` / `no-current-task`,按无上次 snapshot 继续
110
111
  - `<task_dir>/task.json` 的 `base_branch` 字段 — 用于 `git log <base_branch>..HEAD` 圈定本任务 commit 范围
111
112
 
112
113
  ---
@@ -192,6 +193,7 @@ commit message 生成规则:
192
193
  - branch: 执行成功后按实际分支补齐
193
194
  - pushed_commits: 执行成功后按实际 commit hash 补齐
194
195
  - snapshot_at: 执行成功后写入当前 ISO 8601 时间
196
+ - push_mode: <push / commit-only / snapshot-only / 指定仓库 / reconfigure / 临时目标>
195
197
  - bookkeeping: 只提交 `<task_dir>/task.json`
196
198
  ```
197
199
 
@@ -205,6 +207,7 @@ commit message 生成规则:
205
207
  "frontend": "abc1234",
206
208
  "backend": "def5678"
207
209
  },
210
+ "push_mode": "push",
208
211
  "completed_steps": ["Step 1", "Step 2"],
209
212
  "partial_step": "Step 3(可选)",
210
213
  "next_step": "Step 4(可选)",
@@ -214,7 +217,9 @@ commit message 生成规则:
214
217
 
215
218
  > `pushed_commits`、`snapshot_at`、实际 branch / commit hash 是运行后字段。用户确认的是 snapshot 的语义内容和写入动作;执行成功后由 AI 按实际结果补齐。
216
219
 
217
- commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻辑;值记录本次生成的本地 commit hash,并在 `notes` 中注明“commit-only:本地已提交,未推送”。
220
+ `push_mode` 记录本轮确认的模式。默认实际 push 模式写 `"push"`;commit-only `"commit-only"`;snapshot-only `"snapshot-only"`;指定仓库、reconfigure 或临时目标可写对应模式名。
221
+
222
+ commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻辑;值记录本次生成的本地 commit hash,`push_mode` 写 `"commit-only"`,并在 `notes` 中注明“commit-only:本地已提交,未推送”。
218
223
 
219
224
  ### 2.3 展示确认模板
220
225
 
@@ -356,6 +361,7 @@ git checkout <current_branch>
356
361
  - `snapshot_at`:当前 ISO 8601 时间戳
357
362
  - `branch`:实际分支;多仓不同分支时使用字典
358
363
  - `pushed_commits`:各 package 的短 hash
364
+ - `push_mode`:统一执行计划确认的模式;默认 push 写 `"push"`,commit-only 写 `"commit-only"`,snapshot-only 写 `"snapshot-only"`
359
365
  - `notes`:保留已确认 notes;commit-only 模式追加“本地已提交,未推送”
360
366
 
361
367
  写入前先复核目标文件:
@@ -369,13 +375,16 @@ git status --porcelain -- <task_json_path> [".trellis/config.yaml"]
369
375
 
370
376
  写入方式:
371
377
 
372
- 1. `<task_dir>/task.json`
373
- 2. 解析 JSON
374
- 3. 只设置 / 更新 `last_push_snapshot` 字段
375
- 4. 保留其它字段原样
376
- 5. 写回并保持原有缩进(通常 2 空格)
378
+ 1. 将已确认语义内容与运行后字段合并成完整 snapshot JSON。
379
+ 2. 调用 helper 写入:
380
+
381
+ ```bash
382
+ python3 ./.trellis/scripts/push_snapshot.py write --task <task_dir> --snapshot-json '<完整 snapshot JSON>'
383
+ ```
384
+
385
+ 3. 确认 helper 成功返回;失败时停止并展示错误,不继续父仓 bookkeeping commit。
377
386
 
378
- 不要覆盖整个 `task.json`。
387
+ helper 只允许更新 `last_push_snapshot` 字段;不要绕过 helper 手工覆盖整个 `task.json`。
379
388
 
380
389
  ### 5.2 父仓 status 检查
381
390
 
@@ -25,7 +25,7 @@ description: |
25
25
 
26
26
  个人 route 配置只决定“已获准执行后的模式”,不是开工授权。调用 helper 前,必须确认当前 workflow 已允许进入对应 target:implement 需要任务已完成规划确认并处于 `in_progress`;check 用于 Phase 2.2 检查执行,或用户明确要求最终复查 / 轻量检查。最终复查只有在 Phase 2.2 结果缺失、风险较高或用户明确要求复查时才回到 Phase 2.2;回到 Phase 2.2 后优先复用当前任务的合法 check route,除非用户明确要求重选/临时改/清除默认。如果仍在 planning、等待用户确认,或用户表达“等一下 / 我再想想”,停止,不读取 runtime/prefs。
27
27
 
28
- 合法 route 决策必须能追溯到 `trellis-route`、同编号 fallback 选项、由本 skill 读取到的有效 `.trellis/.route-prefs.tmp` 配置,或由 route helper 校验过的 auto-loop 临时 route 授权,并且 `task` 字段必须等于当前 `task.py current --source` 返回的任务路径。runtime state 只能保存和恢复这些原始合法来源,不能把 `.runtime` 自身当成新的 `route_decision.source`。用户自然语言说过“inline/subagent”、compact summary、SessionStart 摘要、`codex-mode`、空 `.route-prefs.tmp`、旧单值偏好,都不能单独作为有效 route 决策。
28
+ 合法 route 决策必须能追溯到 `trellis-route`、同编号 fallback 选项、由本 skill 读取到的有效 `.trellis/.route-prefs.tmp` 配置,或由 route helper 校验过的 auto-loop 临时 route 授权,并且 `task` 字段必须等于当前 `task.py current --source` 返回的任务路径。runtime state 只能保存和恢复这些原始合法来源,不能把 `.runtime` 自身当成新的 `route_decision.source`。用户自然语言说过“inline/subagent”、compact summary、ordinary summary、SessionStart 摘要、replacement history、`codex-mode`、空 `.route-prefs.tmp`、旧单值偏好、历史用户裸数字,都不能单独作为有效 route 决策。
29
29
 
30
30
  当前上下文内已有 target 匹配、task 等于当前任务路径、且来源合法的 route 决策时,后续实现、check 发现问题、用户指出刚检查过的实现有问题、修复后重检、提交前复查均默认复用最近 implement/check 路由;除非用户明确要求重选/临时改/清除默认,不再调用本 skill。当前上下文没有 route 决策但 runtime state 命中时,本 skill 恢复该决策并输出同样的结构化 `route_decision`。如果上下文里只有上一个任务的 `route_decision`,必须忽略并重新解析当前任务。
31
31
 
@@ -72,7 +72,9 @@ helper 默认输出为精简 JSON,只包含 route 执行必需的 `status`、`
72
72
 
73
73
  优先调用 `AskUserQuestion`。选项 label 前缀编号,方便用户直接打数字快速选。
74
74
 
75
- 如果当前平台或模式没有 `AskUserQuestion` / `request_user_input`,不要自行选择 inline subagent 继续。改用普通聊天消息原样呈现同一组编号选项,并停止等待用户回复;用户回复数字后再进入 Step 2.5 / 2.6 / 3。
75
+ 裸数字回复(如 `1` / `2` / `3` / `4`)只有在当前可见的上一条 assistant 消息刚刚展示同一个 target route 选项、并明确停下等待用户回答时,才可解释为本次 numbered fallback 选择。compact summary、ordinary summary、SessionStart 摘要、replacement history、历史消息、旧 target 的裸数字、非紧邻回复里的裸数字都不能触发 `write --source numbered-fallback`;遇到这些情况必须重新展示当前 target 的选项并等待新的紧邻回复。
76
+
77
+ 如果当前平台或模式没有 `AskUserQuestion` / `request_user_input`,不要自行选择 inline 或 subagent 继续。改用普通聊天消息原样呈现同一组编号选项,并停止等待用户回复;只有用户在这条选项消息之后立即回复裸数字,才可进入 Step 2.5 / 2.6 / 3。
76
78
 
77
79
  ### target = implement,且 resolve 未命中
78
80
 
@@ -211,6 +213,7 @@ helper 写入规则:保留另一个 target 的 runtime 决策和偏好;覆
211
213
  [来自 session runtime route state:`.trellis/.runtime/sessions/<context-key>.json` 的 `route_decisions`。]
212
214
  [已写入 session runtime route state:`.trellis/.runtime/sessions/<context-key>.json` 的 `route_decisions`。]
213
215
  [说明:用户明确请求轻量检查,使用隐藏逃生口。]
216
+ [仅当本消息刚展示 route 选项并等待用户回答时:用户下一条紧邻裸数字回复才可按本 target 解释;摘要、历史消息或旧 target 的裸数字无效。]
214
217
 
215
218
  route_decision:
216
219
  target: <implement | check>
@@ -227,7 +230,7 @@ route_decision:
227
230
  - <要避免的工具调用>
228
231
  ```
229
232
 
230
- 中括号内行为条件性出现:仅命中个人配置时显示配置行;仅命中 runtime state 时显示“来自”行;写入 runtime state 成功时显示“已写入”行;仅轻量 check 时显示隐藏逃生口说明;仅 implement subagent + skip_compile=true 时附加“跳过编译”段。`route_decision` 必须保留在回复中,并至少保留 target/mode/source/scope/task;需要 path/decided_at 等诊断字段时重新调用 helper 并加 `--verbose`。compact summary 若只有自然语言描述,后续 agent 仍应优先读取 runtime state,而不是把 summary 当证据。
233
+ 中括号内行为条件性出现:仅命中个人配置时显示配置行;仅命中 runtime state 时显示“来自”行;写入 runtime state 成功时显示“已写入”行;仅轻量 check 时显示隐藏逃生口说明;仅展示选项并等待用户时显示裸数字有效性提醒;仅 implement subagent + skip_compile=true 时附加“跳过编译”段。`route_decision` 必须保留在回复中,并至少保留 target/mode/source/scope/task;需要 path/decided_at 等诊断字段时重新调用 helper 并加 `--verbose`。compact summary 若只有自然语言描述,后续 agent 仍应优先读取 runtime state,而不是把 summary 当证据。
231
234
 
232
235
  ---
233
236
 
@@ -260,6 +263,8 @@ route_decision:
260
263
  - `AskUserQuestion` / `request_user_input` 不可用时,记录为 inline 或 subagent 路径并继续。
261
264
  - 没有有效 check 配置、用户选择或最近本轮 check 路由决定时,自动执行 inline check。
262
265
  - 没有 `source` 合法的 `route_decision`,就把“用户说过 inline/subagent”或 compact summary 当成已路由。
266
+ - 把 compact summary、ordinary summary、SessionStart 摘要、replacement history 或历史消息里的裸数字 `1` 当成当前 target 的 numbered fallback 选择。
267
+ - 用户裸数字回复不是紧邻当前 route 选项消息时,仍调用 `write --source numbered-fallback`。
263
268
  - check 发现问题后,把当前任务内已有合法 route 的修复/重检当成新的 route 边界再次询问模式。
264
269
  - 给 check 任何模式附加“跳过编译”指令。
265
270
  - 询问后忽视用户答案默认 subagent。
@@ -70,4 +70,18 @@ Do not write `None`, `No release operations identified`, or `Rollback code only`
70
70
 
71
71
  If multiple tasks will be archived in the same finish-work run, process the active task at minimum. Process extra archived tasks only when Step 1 provides enough local context to infer safely; do not add per-task confirmation prompts.
72
72
 
73
+ #### Finish Bookkeeping Auto-push Step
74
+
75
+ Run this step after finish-work Step 4 records the session journal and produces the `chore: record journal` commit, before the final finish-work report.
76
+
77
+ Read the active task's `last_push_snapshot.push_mode` with `python3 ./.trellis/scripts/push_snapshot.py status --json` before archive moves the task. If `push_mode` is `"commit-only"` or missing, do not auto-push finish-work's archive/journal commits; report that local bookkeeping commits remain ahead.
78
+
79
+ If `push_mode` is not `"commit-only"`, Step 4 left `git status --porcelain` clean, and the current branch has an upstream, push the current branch:
80
+
81
+ ```bash
82
+ git push origin <current_branch>
83
+ ```
84
+
85
+ Never force push. If push fails, stop and report the failure.
86
+
73
87
  <!-- END skill-garden skill override trellis-finish-work v0.6 -->
@@ -2,7 +2,7 @@
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (in_progress-inline):
3
3
  This state block is a breadcrumb; the top-level skill-garden hub is the source of truth for route details.
4
4
  Before the first implement route, read `<task>/brief.md` if present and restate the task brief in chat. If it is missing, read the task artifacts and suggest backfilling brief; do not silently rely on memory.
5
- Before procedural or high-impact actions, run `.trellis/scripts/spec_router.py` with a short query describing the intended action and read the strongest 1-2 matched SOP/spec files before acting.
5
+ At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py` with a short query describing the intended action; read high-confidence matches before acting and skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
6
6
  Inline workflow-state is not an inline route decision. At Phase 2.1/2.2, reuse only an explicit target-matched `route_decision`; otherwise MUST load/read/use `trellis-route(implement|check)` (or its local `SKILL.md`) to resolve session runtime state/prefs, write the resolved decision, or show numbered fallback and wait.
7
7
  Plain preferences, ordinary summaries, `codex-mode`, raw `.runtime` files, and empty/old prefs are not route evidence by themselves; only `trellis-route` may validate runtime route state.
8
8
  User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and prefs.
@@ -10,5 +10,5 @@ Ignore lower Active Task Routing shortcuts that start editing/checking directly.
10
10
  After `trellis-check` / `trellis-check-all`, stop and report; point the user to Phase 3.4 `trellis-push` (or commit-only when needed). Do not run `/trellis:finish-work` unless the user explicitly asks after Phase 3.4 is complete.
11
11
  This guard overrides any lower `Flow: ... -> /trellis:finish-work` line in this state block.
12
12
  At Phase 3.4, code commit/push still goes through `trellis-push` (commit-only for commit-without-push); never bare `git commit`/`git push` on code (hub: Code Commit Confirmation Gate).
13
- If active task.json has `last_push_snapshot`, relay `partial_step` + `next_step` once before starting new work.
13
+ Push snapshot recovery: follow the hub; use `push_snapshot.py status --json` only when needed.
14
14
  <!-- END skill-garden workflow-state in_progress_inline v0.6 -->
@@ -2,7 +2,7 @@
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (in_progress):
3
3
  This state block is a breadcrumb; the top-level skill-garden hub is the source of truth for route details.
4
4
  Before the first implement route, read `<task>/brief.md` if present and restate the task brief in chat. If it is missing, read the task artifacts and suggest backfilling brief; do not silently rely on memory.
5
- Before procedural or high-impact actions, run `.trellis/scripts/spec_router.py` with a short query describing the intended action and read the strongest 1-2 matched SOP/spec files before acting.
5
+ At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py` with a short query describing the intended action; read high-confidence matches before acting and skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
6
6
  At Phase 2.1/2.2, reuse only an explicit target-matched `route_decision`; otherwise MUST load/read/use `trellis-route(implement|check)` (or its local `SKILL.md`) to resolve session runtime state/prefs, write the resolved decision, or show numbered fallback and wait.
7
7
  Plain preferences, ordinary summaries, `codex-mode`, raw `.runtime` files, and empty/old prefs are not route evidence by themselves; only `trellis-route` may validate runtime route state.
8
8
  User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and prefs.
@@ -10,5 +10,5 @@ Ignore lower Active Task Routing shortcuts that dispatch implement/check directl
10
10
  After `trellis-check` / `trellis-check-all`, stop and report; point the user to Phase 3.4 `trellis-push` (or commit-only when needed). Do not run `/trellis:finish-work` unless the user explicitly asks after Phase 3.4 is complete.
11
11
  This guard overrides any lower `Flow: ... -> /trellis:finish-work` line in this state block.
12
12
  At Phase 3.4, code commit/push goes through `trellis-push` (commit-only mode for commit-without-push); never bare `git commit`/`git push` on code (hub: Code Commit Confirmation Gate).
13
- If active task.json has `last_push_snapshot`, relay `partial_step` + `next_step` once before starting new work.
13
+ Push snapshot recovery: follow the hub; use `push_snapshot.py status --json` only when needed.
14
14
  <!-- END skill-garden workflow-state in_progress v0.6 -->
@@ -2,8 +2,8 @@
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (no_task):
3
3
  Creating or resuming a task is not implementation permission.
4
4
  After PRD is ready and the task is started, the next implementation action is Phase 2.1 `trellis-route(implement)` unless a valid current-task implement route decision already exists.
5
- If no active task exists, scan `.trellis/tasks/*/task.json` once per session for in-progress tasks with `last_push_snapshot`; surface `completed_steps` + `next_step` and suggest rebinding the active task before resuming.
6
- Before procedural or high-impact actions, run `.trellis/scripts/spec_router.py` with a short query describing the intended action and read the strongest 1-2 matched SOP/spec files before acting.
5
+ If no active task exists, use `push_snapshot.py status --json` once per session; if it returns candidates, relay them and suggest rebinding before resuming.
6
+ At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py` with a short query describing the intended action; read high-confidence matches before acting and skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
7
7
  Do NOT call the harness built-in plan mode (`EnterPlanMode` / `ExitPlanMode`) for Trellis planning. It is not a substitute for Trellis task-creation consent, Trellis planning, or the route gate. For complex work, classify the turn, ask for task-creation consent, then use `trellis-brainstorm`.
8
8
  If the turn is a meta edit to Trellis itself and Trellis tracking would be overkill, say so and ask to skip Trellis; never silently swap built-in plan mode in for the consent gate.
9
9
  <!-- END skill-garden workflow-state no_task v0.6 -->
@@ -4,6 +4,6 @@ Planning is not implementation permission.
4
4
  Complete prd.md + required context first.
5
5
  If the active workflow later routes to sub-agent execution, required context includes real curated entries in both `implement.jsonl` and `check.jsonl`; the seed `_example` row alone is not ready.
6
6
  Before `task.py start`, use `trellis-task-brief` to refresh `brief.md` from the latest task artifacts and display it in chat for review.
7
- Before procedural or high-impact actions, run `.trellis/scripts/spec_router.py` with a short query describing the intended action and read the strongest 1-2 matched SOP/spec files before acting.
7
+ At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py` with a short query describing the intended action; read high-confidence matches before acting and skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
8
8
  After status becomes in_progress, next action = `trellis-route(implement)`, not direct edits.
9
9
  <!-- END skill-garden workflow-state planning_inline v0.6 -->
@@ -4,6 +4,6 @@ Planning is not implementation permission.
4
4
  Complete prd.md + required context first.
5
5
  For sub-agent-dispatch platforms, required context includes real curated entries in both `implement.jsonl` and `check.jsonl`; the seed `_example` row alone is not ready.
6
6
  Before `task.py start`, use `trellis-task-brief` to refresh `brief.md` from the latest task artifacts and display it in chat for review.
7
- Before procedural or high-impact actions, run `.trellis/scripts/spec_router.py` with a short query describing the intended action and read the strongest 1-2 matched SOP/spec files before acting.
7
+ At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py` with a short query describing the intended action; read high-confidence matches before acting and skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
8
8
  After status becomes in_progress, next action = `trellis-route(implement)`, not direct edits.
9
9
  <!-- END skill-garden workflow-state planning v0.6 -->
@@ -6,13 +6,17 @@
6
6
 
7
7
  **Priority**: This hub overrides any conflicting Trellis workflow, skill, or command text for the scoped behaviors below.
8
8
 
9
- **Scope**: project knowledge discovery for procedural/high-impact actions, Phase 1.4 task brief handoff, Phase 2.1 implement routing, Phase 2.2 check/check-all routing, current-task route reuse, post-check stop, auto-loop commit-only preauthorization, Phase 3.4 trellis-push, explicit Phase 3.5 finish-work bookkeeping, and push-progress recovery. State blocks should keep one short skill-garden sentinel; long-form rules live here.
9
+ **Scope**: project knowledge discovery at project-local knowledge decision boundaries, Phase 1.4 task brief handoff, Phase 2.1 implement routing, Phase 2.2 check/check-all routing, current-task route reuse, post-check stop, auto-loop commit-only preauthorization, Phase 3.4 trellis-push, explicit Phase 3.5 finish-work bookkeeping, and push-progress recovery. State blocks should keep one short skill-garden sentinel; long-form rules live here.
10
10
 
11
11
  **Mechanical rule**: use this hub as the source of truth. Do not add separate top-level skill-garden override sections or multiple skill-garden sentinels inside the same `workflow-state:*` block.
12
12
 
13
13
  #### Project Knowledge Discovery
14
14
 
15
- Before procedural or high-impact actions, run project knowledge discovery:
15
+ Before choosing an approach for non-trivial project work, run project knowledge
16
+ discovery when project-local SOPs, package conventions, workflow rules,
17
+ config/state contracts, release/publish/deploy steps, git history actions,
18
+ data changes, cross-layer design, generated artifacts, install/sync pipelines,
19
+ or destructive operations may affect the correct approach:
16
20
 
17
21
  ```bash
18
22
  python3 ./.trellis/scripts/spec_router.py "<short query describing the intended action>"
@@ -21,11 +25,15 @@ python3 ./.trellis/scripts/spec_router.py "<short query describing the intended
21
25
  Build the query from the current user request plus relevant immediate context:
22
26
  the intended action, commands about to run, files or systems involved, package/layer,
23
27
  and domain words such as release, publish, deploy, migration, config, CI, workflow,
24
- hooks, rollback, data fix, or destructive command.
28
+ hooks, rollback, data fix, generated artifact, install/sync pipeline, or destructive command.
25
29
 
26
- Read the strongest 1-2 matched SOP/spec files before acting. Continue to lower-ranked
27
- matches only when their path, heading, or trigger reason is clearly relevant; if
28
- nothing matches, continue normally.
30
+ Read high-confidence matches before acting. For medium-confidence matches, read only
31
+ when the path, heading, index description, or reason clearly fits the intended change.
32
+ If nothing matches, continue normally.
33
+
34
+ Do not run discovery for pure Q&A, simple read-only inspection, opening local tools,
35
+ or trivial edits unless the request mentions project conventions or local SOPs may
36
+ change the approach.
29
37
 
30
38
  #### Task Brief Handoff
31
39
 
@@ -46,7 +54,7 @@ At each route boundary:
46
54
  3. If the platform cannot invoke skills directly, open the local `trellis-route/SKILL.md` copy first, then follow its numbered fallback choices in normal chat and wait.
47
55
  4. If the route helper cannot ask through `AskUserQuestion` / `request_user_input`, ask the same numbered choices from `trellis-route` in normal chat and wait.
48
56
 
49
- Plain user preference, ordinary compact/SessionStart summary, `codex-mode`, empty/old prefs, and raw `.trellis/.runtime/sessions/*` `route_decisions` content that has not been validated by `trellis-route` are not route evidence by themselves.
57
+ Plain user preference, ordinary compact/SessionStart summary, ordinary summary, replacement history, historical bare numeric replies, `codex-mode`, empty/old prefs, and raw `.trellis/.runtime/sessions/*` `route_decisions` content that has not been validated by `trellis-route` are not route evidence by themselves; numbered fallback validity is governed by `trellis-route`.
50
58
 
51
59
  User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and personal prefs.
52
60
 
@@ -80,10 +88,10 @@ This exception does not apply to ordinary `trellis-push`. If the plan contains u
80
88
 
81
89
  #### Push Progress Recovery / Snapshot
82
90
 
83
- `trellis-push` may write `last_push_snapshot` into an active task's `task.json` with this schema: `snapshot_at`, `branch`, `pushed_commits`, `completed_steps`, `partial_step`, `next_step`, and `notes`.
91
+ Use `python3 ./.trellis/scripts/push_snapshot.py status --json` for recovery reads and `write --task ... --snapshot-json ...` for `trellis-push` writes; do not hand-scan or hand-edit `task.json`.
84
92
 
85
- When there is no active task, scan `.trellis/tasks/*/task.json` for `status="in_progress"` entries that carry `last_push_snapshot`. If any exist and this session has not already relayed recovery, surface the paused state to the user and suggest rebinding the active-task pointer before resuming.
93
+ `trellis-push` still owns snapshot semantics, user confirmation, git operations, and post-run fields; the helper only touches `task.json.last_push_snapshot`.
86
94
 
87
- When an active in-progress task carries `last_push_snapshot`, briefly relay `partial_step` and `next_step` before starting new work. Skip the reminder if it was already relayed in this session or the field is absent.
95
+ On recovery, relay the helper's `summary` / `candidates` once and suggest rebinding if there is no active task. Never auto-rebind, infer workflow phase, or hook this into SessionStart / workflow-state injection / `trellis-continue`.
88
96
 
89
97
  <!-- END skill-garden overrides v0.6 -->
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """读取和写入 Trellis 任务的 last_push_snapshot。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ DIR_WORKFLOW = ".trellis"
15
+ DIR_TASKS = "tasks"
16
+ FILE_TASK_JSON = "task.json"
17
+ REQUIRED_FIELDS = {
18
+ "snapshot_at",
19
+ "branch",
20
+ "pushed_commits",
21
+ "completed_steps",
22
+ "next_step",
23
+ }
24
+ OPTIONAL_STRING_FIELDS = {"partial_step", "notes"}
25
+
26
+
27
+ def _find_repo_root(start: Path) -> Path | None:
28
+ """从当前位置向上查找 Trellis 项目根目录。"""
29
+ current = start.resolve()
30
+ if current.is_file():
31
+ current = current.parent
32
+ while True:
33
+ if (current / DIR_WORKFLOW).is_dir():
34
+ return current
35
+ if current == current.parent:
36
+ return None
37
+ current = current.parent
38
+
39
+
40
+ def _scripts_dir(repo_root: Path) -> Path:
41
+ """返回当前项目的 Trellis scripts 目录。"""
42
+ return repo_root / DIR_WORKFLOW / "scripts"
43
+
44
+
45
+ def _load_common_modules(repo_root: Path) -> None:
46
+ """把 `.trellis/scripts` 加入 import path 以复用 Trellis 公共模块。"""
47
+ scripts_dir = _scripts_dir(repo_root)
48
+ if str(scripts_dir) not in sys.path:
49
+ sys.path.insert(0, str(scripts_dir))
50
+
51
+
52
+ def _read_json(path: Path) -> dict[str, Any] | None:
53
+ """读取 JSON 对象,失败或非对象时返回 None。"""
54
+ try:
55
+ data = json.loads(path.read_text(encoding="utf-8"))
56
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
57
+ return None
58
+ return data if isinstance(data, dict) else None
59
+
60
+
61
+ def _write_json(path: Path, data: dict[str, Any]) -> None:
62
+ """用 Trellis 任务文件常见格式写回 JSON。"""
63
+ path.write_text(
64
+ json.dumps(data, indent=2, ensure_ascii=False) + "\n",
65
+ encoding="utf-8",
66
+ )
67
+
68
+
69
+ def _rel_path(repo_root: Path, path: Path) -> str:
70
+ """尽量输出 repo-root 相对路径。"""
71
+ try:
72
+ return path.relative_to(repo_root).as_posix()
73
+ except ValueError:
74
+ return str(path)
75
+
76
+
77
+ def _resolve_task_dir(repo_root: Path, task_ref: str) -> Path:
78
+ """解析任务目录引用。"""
79
+ _load_common_modules(repo_root)
80
+ from common.task_utils import resolve_task_dir # type: ignore[import-not-found]
81
+
82
+ return resolve_task_dir(task_ref, repo_root)
83
+
84
+
85
+ def _current_task_dir(repo_root: Path) -> Path | None:
86
+ """读取当前 session 的 active task 目录。"""
87
+ _load_common_modules(repo_root)
88
+ from common.active_task import resolve_active_task # type: ignore[import-not-found]
89
+
90
+ active = resolve_active_task(repo_root)
91
+ if not active.task_path:
92
+ return None
93
+ task_dir = Path(active.task_path)
94
+ if task_dir.is_absolute():
95
+ return task_dir
96
+ return repo_root / task_dir
97
+
98
+
99
+ def _task_json_path(task_dir: Path) -> Path:
100
+ """返回任务 task.json 路径。"""
101
+ return task_dir / FILE_TASK_JSON
102
+
103
+
104
+ def _snapshot_summary(snapshot: dict[str, Any]) -> dict[str, Any]:
105
+ """提取恢复提示需要的精简 snapshot 字段。"""
106
+ return {
107
+ "completed_steps": snapshot.get("completed_steps", []),
108
+ "partial_step": snapshot.get("partial_step"),
109
+ "next_step": snapshot.get("next_step"),
110
+ }
111
+
112
+
113
+ def _load_task_snapshot(repo_root: Path, task_dir: Path) -> dict[str, Any]:
114
+ """读取单个任务的 snapshot 状态。"""
115
+ task_json = _task_json_path(task_dir)
116
+ data = _read_json(task_json)
117
+ task_rel = _rel_path(repo_root, task_dir)
118
+ if data is None:
119
+ return {
120
+ "status": "error",
121
+ "reason": "invalid-task-json",
122
+ "task": task_rel,
123
+ "path": _rel_path(repo_root, task_json),
124
+ }
125
+ snapshot = data.get("last_push_snapshot")
126
+ if not isinstance(snapshot, dict):
127
+ return {"status": "no-snapshot", "task": task_rel}
128
+ return {
129
+ "status": "ok",
130
+ "task": task_rel,
131
+ "snapshot": snapshot,
132
+ "summary": _snapshot_summary(snapshot),
133
+ }
134
+
135
+
136
+ def _iter_active_task_dirs(repo_root: Path) -> list[Path]:
137
+ """列出 active task tree 下的一层任务目录。"""
138
+ tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS
139
+ try:
140
+ return [
141
+ path for path in sorted(tasks_dir.iterdir())
142
+ if path.is_dir() and path.name != "archive"
143
+ ]
144
+ except OSError:
145
+ return []
146
+
147
+
148
+ def _snapshot_candidates(repo_root: Path) -> list[dict[str, Any]]:
149
+ """扫描 in_progress 且带 last_push_snapshot 的任务候选。"""
150
+ candidates: list[dict[str, Any]] = []
151
+ for task_dir in _iter_active_task_dirs(repo_root):
152
+ task_json = _task_json_path(task_dir)
153
+ data = _read_json(task_json)
154
+ if data is None:
155
+ continue
156
+ if data.get("status") != "in_progress":
157
+ continue
158
+ snapshot = data.get("last_push_snapshot")
159
+ if not isinstance(snapshot, dict):
160
+ continue
161
+ candidates.append({
162
+ "task": _rel_path(repo_root, task_dir),
163
+ **_snapshot_summary(snapshot),
164
+ })
165
+ return candidates
166
+
167
+
168
+ def _validate_string(value: Any, field: str, errors: list[str]) -> None:
169
+ """校验字符串字段。"""
170
+ if not isinstance(value, str) or not value.strip():
171
+ errors.append(f"{field} 必须是非空字符串")
172
+
173
+
174
+ def _validate_string_or_object(value: Any, field: str, errors: list[str]) -> None:
175
+ """校验字符串或对象字段。"""
176
+ if isinstance(value, str) and value.strip():
177
+ return
178
+ if isinstance(value, dict):
179
+ return
180
+ errors.append(f"{field} 必须是非空字符串或对象")
181
+
182
+
183
+ def _validate_snapshot(snapshot: Any) -> tuple[dict[str, Any] | None, list[str]]:
184
+ """校验 last_push_snapshot schema。"""
185
+ if not isinstance(snapshot, dict):
186
+ return None, ["snapshot 必须是 JSON 对象"]
187
+
188
+ errors: list[str] = []
189
+ for field in sorted(REQUIRED_FIELDS):
190
+ if field not in snapshot:
191
+ errors.append(f"缺少必填字段 {field}")
192
+
193
+ if "snapshot_at" in snapshot:
194
+ _validate_string(snapshot.get("snapshot_at"), "snapshot_at", errors)
195
+ if "branch" in snapshot:
196
+ _validate_string_or_object(snapshot.get("branch"), "branch", errors)
197
+ if "pushed_commits" in snapshot:
198
+ _validate_string_or_object(
199
+ snapshot.get("pushed_commits"),
200
+ "pushed_commits",
201
+ errors,
202
+ )
203
+ completed_steps = snapshot.get("completed_steps")
204
+ if "completed_steps" in snapshot and (
205
+ not isinstance(completed_steps, list)
206
+ or any(not isinstance(item, str) for item in completed_steps)
207
+ ):
208
+ errors.append("completed_steps 必须是字符串数组")
209
+ if "next_step" in snapshot:
210
+ _validate_string(snapshot.get("next_step"), "next_step", errors)
211
+
212
+ for field in sorted(OPTIONAL_STRING_FIELDS):
213
+ if field in snapshot and snapshot.get(field) is not None and not isinstance(snapshot.get(field), str):
214
+ errors.append(f"{field} 存在时必须是字符串")
215
+
216
+ if errors:
217
+ return None, errors
218
+ return snapshot, []
219
+
220
+
221
+ def _print_json(data: dict[str, Any], exit_code: int = 0) -> int:
222
+ """输出紧凑 JSON。"""
223
+ print(json.dumps(data, ensure_ascii=False, sort_keys=True))
224
+ return exit_code
225
+
226
+
227
+ def _print_status_text(data: dict[str, Any]) -> int:
228
+ """输出给人和 AI 快速阅读的状态文本。"""
229
+ status = data.get("status")
230
+ if status == "ok":
231
+ summary = data.get("summary") if isinstance(data.get("summary"), dict) else {}
232
+ print(f"任务:{data.get('task')}")
233
+ print("snapshot:存在")
234
+ print(f"completed_steps: {summary.get('completed_steps', [])}")
235
+ print(f"partial_step: {summary.get('partial_step') or '(none)'}")
236
+ print(f"next_step: {summary.get('next_step') or '(none)'}")
237
+ return 0
238
+ if status == "candidates":
239
+ candidates = data.get("candidates")
240
+ print("无活动任务。snapshot 候选:")
241
+ if isinstance(candidates, list) and candidates:
242
+ for item in candidates:
243
+ print(f"- {item.get('task')}: next_step={item.get('next_step') or '(none)'}")
244
+ else:
245
+ print("(none)")
246
+ return 0
247
+ if status == "no-snapshot":
248
+ print(f"任务没有 last_push_snapshot:{data.get('task')}")
249
+ return 0
250
+ if status == "no-current-task":
251
+ print("没有活动任务,也没有可用的 snapshot 候选。")
252
+ return 0
253
+ print(f"错误:{data.get('reason') or status}", file=sys.stderr)
254
+ return 1
255
+
256
+
257
+ def cmd_status(args: argparse.Namespace, repo_root: Path) -> int:
258
+ """执行 status 子命令。"""
259
+ if args.task:
260
+ task_dir = _resolve_task_dir(repo_root, args.task)
261
+ result = _load_task_snapshot(repo_root, task_dir)
262
+ else:
263
+ task_dir = _current_task_dir(repo_root)
264
+ if task_dir is None:
265
+ candidates = _snapshot_candidates(repo_root)
266
+ result = {
267
+ "status": "candidates" if candidates else "no-current-task",
268
+ "candidates": candidates,
269
+ }
270
+ else:
271
+ result = _load_task_snapshot(repo_root, task_dir)
272
+
273
+ exit_code = 1 if result.get("status") == "error" else 0
274
+ if args.json:
275
+ return _print_json(result, exit_code)
276
+ return _print_status_text(result)
277
+
278
+
279
+ def cmd_write(args: argparse.Namespace, repo_root: Path) -> int:
280
+ """执行 write 子命令。"""
281
+ task_dir = _resolve_task_dir(repo_root, args.task)
282
+ task_json = _task_json_path(task_dir)
283
+ data = _read_json(task_json)
284
+ if data is None:
285
+ result = {
286
+ "status": "error",
287
+ "reason": "invalid-task-json",
288
+ "task": _rel_path(repo_root, task_dir),
289
+ }
290
+ if args.json:
291
+ return _print_json(result, 1)
292
+ print(f"错误:{result['reason']}", file=sys.stderr)
293
+ return 1
294
+
295
+ try:
296
+ raw_snapshot = json.loads(args.snapshot_json)
297
+ except json.JSONDecodeError as exc:
298
+ result = {
299
+ "status": "error",
300
+ "reason": "invalid-snapshot-json",
301
+ "message": str(exc),
302
+ }
303
+ if args.json:
304
+ return _print_json(result, 1)
305
+ print(f"错误:{result['reason']}:{result['message']}", file=sys.stderr)
306
+ return 1
307
+
308
+ snapshot, errors = _validate_snapshot(raw_snapshot)
309
+ if snapshot is None:
310
+ result = {
311
+ "status": "error",
312
+ "reason": "invalid-snapshot-schema",
313
+ "errors": errors,
314
+ }
315
+ if args.json:
316
+ return _print_json(result, 1)
317
+ print("错误:invalid-snapshot-schema", file=sys.stderr)
318
+ for error in errors:
319
+ print(f"- {error}", file=sys.stderr)
320
+ return 1
321
+
322
+ data["last_push_snapshot"] = snapshot
323
+ try:
324
+ _write_json(task_json, data)
325
+ except OSError as exc:
326
+ result = {
327
+ "status": "error",
328
+ "reason": "write-failed",
329
+ "message": str(exc),
330
+ "task": _rel_path(repo_root, task_dir),
331
+ }
332
+ if args.json:
333
+ return _print_json(result, 1)
334
+ print(f"错误:write-failed:{exc}", file=sys.stderr)
335
+ return 1
336
+
337
+ result = {
338
+ "status": "written",
339
+ "task": _rel_path(repo_root, task_dir),
340
+ "path": _rel_path(repo_root, task_json),
341
+ "summary": _snapshot_summary(snapshot),
342
+ }
343
+ if args.json:
344
+ return _print_json(result)
345
+ print(f"✓ 已更新 last_push_snapshot:{result['path']}")
346
+ print(f"next_step: {result['summary'].get('next_step') or '(none)'}")
347
+ return 0
348
+
349
+
350
+ def build_parser() -> argparse.ArgumentParser:
351
+ """创建命令行解析器。"""
352
+ parser = argparse.ArgumentParser(
353
+ description="读取或写入 Trellis 任务的 last_push_snapshot。",
354
+ )
355
+ subparsers = parser.add_subparsers(dest="command", required=True)
356
+
357
+ status = subparsers.add_parser("status", help="读取 last_push_snapshot")
358
+ status.add_argument("--task", help="任务目录、路径或任务名")
359
+ status.add_argument("--json", action="store_true", help="输出 JSON")
360
+
361
+ write = subparsers.add_parser("write", help="写入 last_push_snapshot")
362
+ write.add_argument("--task", required=True, help="任务目录、路径或任务名")
363
+ write.add_argument("--snapshot-json", required=True, help="snapshot JSON 对象")
364
+ write.add_argument("--json", action="store_true", help="输出 JSON")
365
+
366
+ return parser
367
+
368
+
369
+ def main() -> int:
370
+ """命令入口。"""
371
+ repo_root = _find_repo_root(Path.cwd())
372
+ if repo_root is None:
373
+ print("错误:不是 Trellis 项目(缺少 .trellis/)", file=sys.stderr)
374
+ return 1
375
+
376
+ parser = build_parser()
377
+ args = parser.parse_args()
378
+ if args.command == "status":
379
+ return cmd_status(args, repo_root)
380
+ if args.command == "write":
381
+ return cmd_write(args, repo_root)
382
+ parser.print_help()
383
+ return 1
384
+
385
+
386
+ if __name__ == "__main__":
387
+ sys.exit(main())
@@ -2,8 +2,8 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """从 `.trellis/spec/` 发现相关项目 SOP/spec 文件。
4
4
 
5
- 这个 helper 刻意保持轻量:只返回候选路径和命中原因,让 AI 在执行流程性或
6
- 高影响动作前读取匹配文件;它不把完整文档注入上下文。
5
+ 这个 helper 刻意保持轻量:只返回候选路径、置信度和命中原因,让 AI 在项目
6
+ 局部知识可能影响做法的决策边界前读取匹配文件;它不把完整文档注入上下文。
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -21,40 +21,72 @@ MAX_BODY_CHARS = 8000
21
21
  DEFAULT_LIMIT = 3
22
22
  MIN_SCORE = 3
23
23
  MIN_BODY_ONLY_HITS = 5
24
- MIN_HEADING_BODY_HITS = 3
25
- BODY_WEAK_TOKENS = {
24
+ MIN_ANCHORED_BODY_HITS = 2
25
+ WEAK_TOKENS = {
26
26
  "action",
27
27
  "actions",
28
28
  "after",
29
+ "and",
29
30
  "before",
31
+ "change",
32
+ "changes",
33
+ "cli",
30
34
  "command",
31
35
  "commands",
36
+ "commit",
32
37
  "context",
33
38
  "current",
39
+ "data",
40
+ "documentation",
41
+ "edit",
34
42
  "file",
35
43
  "files",
44
+ "flow",
45
+ "flower",
46
+ "for",
47
+ "from",
48
+ "guide",
49
+ "guides",
50
+ "in",
51
+ "index",
36
52
  "match",
37
53
  "matched",
38
54
  "matches",
39
55
  "matching",
56
+ "md",
40
57
  "normal",
58
+ "of",
59
+ "or",
41
60
  "path",
42
61
  "paths",
43
62
  "project",
63
+ "py",
44
64
  "read",
65
+ "readme",
45
66
  "reason",
46
67
  "reasons",
47
68
  "relevant",
69
+ "run",
70
+ "simple",
71
+ "small",
48
72
  "sop",
49
73
  "spec",
50
74
  "status",
51
75
  "task",
52
76
  "tasks",
77
+ "the",
78
+ "to",
79
+ "trellis",
80
+ "typo",
81
+ "update",
82
+ "with",
53
83
  "workflow",
54
84
  }
55
- TOKEN_RE = re.compile(r"[A-Za-z0-9_.@/-]+|[\u4e00-\u9fff]{2,}")
85
+ TOKEN_RE = re.compile(r"[A-Za-z0-9@]+|[\u4e00-\u9fff]+")
86
+ CJK_RE = re.compile(r"^[\u4e00-\u9fff]+$")
56
87
  HEADER_RE = re.compile(r"^\s{0,3}#{1,3}\s+(.+?)\s*$", re.MULTILINE)
57
88
  FRONTMATTER_BOUNDARY_RE = re.compile(r"^---\s*$")
89
+ MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+\.md(?:#[^)]+)?)\)")
58
90
 
59
91
 
60
92
  @dataclass
@@ -66,7 +98,9 @@ class Candidate:
66
98
  kind: str
67
99
  load: str
68
100
  priority: str
101
+ confidence: str
69
102
  reasons: list[str]
103
+ action: str
70
104
 
71
105
 
72
106
  def find_trellis_root(start: Path) -> Path | None:
@@ -183,8 +217,45 @@ def as_list(value: Any) -> list[str]:
183
217
  return [text] if text else []
184
218
 
185
219
 
220
+ def add_token(tokens: list[str], seen: set[str], token: str) -> None:
221
+ """按原始顺序追加去重 token。
222
+
223
+ Args:
224
+ tokens: 正在构造的 token 列表。
225
+ seen: 已追加 token 集合。
226
+ token: 待追加 token。
227
+
228
+ Returns:
229
+ None。
230
+ """
231
+ if len(token) < 2 or token in seen:
232
+ return
233
+ seen.add(token)
234
+ tokens.append(token)
235
+
236
+
237
+ def add_cjk_tokens(tokens: list[str], seen: set[str], text: str) -> None:
238
+ """为连续中文文本追加有限 n-gram token。
239
+
240
+ 中文没有空格分词。这里生成 2 到 6 字的 n-gram,用 token 集合匹配替代旧
241
+ 版任意子串匹配,同时保留 `发版` 命中 `发版流程` 这类常见能力。
242
+
243
+ Args:
244
+ tokens: 正在构造的 token 列表。
245
+ seen: 已追加 token 集合。
246
+ text: 连续中文文本。
247
+
248
+ Returns:
249
+ None。
250
+ """
251
+ max_size = min(6, len(text))
252
+ for size in range(2, max_size + 1):
253
+ for start in range(0, len(text) - size + 1):
254
+ add_token(tokens, seen, text[start : start + size])
255
+
256
+
186
257
  def normalize_tokens(text: str) -> list[str]:
187
- """提取查询 token,用于确定性的轻量匹配。
258
+ """提取查询或文档 token,用于确定性的轻量匹配。
188
259
 
189
260
  Args:
190
261
  text: 查询或可搜索文本。
@@ -195,14 +266,129 @@ def normalize_tokens(text: str) -> list[str]:
195
266
  seen: set[str] = set()
196
267
  tokens: list[str] = []
197
268
  for match in TOKEN_RE.finditer(text.lower()):
198
- token = match.group(0).strip("._-/")
199
- if len(token) < 2 or token in seen:
200
- continue
201
- seen.add(token)
202
- tokens.append(token)
269
+ token = match.group(0)
270
+ if CJK_RE.match(token):
271
+ add_cjk_tokens(tokens, seen, token)
272
+ else:
273
+ add_token(tokens, seen, token)
203
274
  return tokens
204
275
 
205
276
 
277
+ def significant_hits(query_tokens: list[str], target_tokens: list[str]) -> list[str]:
278
+ """计算非弱词 token 命中,保留查询 token 顺序。
279
+
280
+ Args:
281
+ query_tokens: 查询 token。
282
+ target_tokens: 待匹配文本 token。
283
+
284
+ Returns:
285
+ 非弱词命中列表。
286
+ """
287
+ target_set = set(target_tokens)
288
+ return [
289
+ token
290
+ for token in query_tokens
291
+ if token not in WEAK_TOKENS and token in target_set
292
+ ]
293
+
294
+
295
+ def collect_index_descriptions(spec_dir: Path) -> dict[str, list[str]]:
296
+ """从 `index.md` 链接行收集目标文档的路由描述。
297
+
298
+ Args:
299
+ spec_dir: `.trellis/spec` 目录。
300
+
301
+ Returns:
302
+ 以 spec 相对路径为 key 的描述文本列表。
303
+ """
304
+ descriptions: dict[str, list[str]] = {}
305
+ spec_root = spec_dir.resolve()
306
+ for index_path in iter_spec_files(spec_dir):
307
+ if index_path.name != "index.md":
308
+ continue
309
+
310
+ text = read_markdown(index_path)
311
+ if text is None:
312
+ continue
313
+ _, body = parse_frontmatter(text)
314
+
315
+ for line in body.splitlines():
316
+ matches = list(MARKDOWN_LINK_RE.finditer(line))
317
+ if not matches:
318
+ continue
319
+
320
+ clean_line = MARKDOWN_LINK_RE.sub(lambda item: item.group(1), line).strip()
321
+ for match in matches:
322
+ link_target = match.group(2).split("#", 1)[0].strip()
323
+ if "://" in link_target or link_target.startswith("#"):
324
+ continue
325
+
326
+ target_path = (index_path.parent / link_target).resolve()
327
+ try:
328
+ target_rel_path = target_path.relative_to(spec_root).as_posix()
329
+ except ValueError:
330
+ continue
331
+
332
+ if not target_path.is_file() or target_path == index_path.resolve():
333
+ continue
334
+
335
+ description = f"{match.group(1)} {clean_line}".strip()
336
+ if description:
337
+ descriptions.setdefault(target_rel_path, []).append(description)
338
+ return descriptions
339
+
340
+
341
+ def classify_confidence(
342
+ matched_triggers: list[str],
343
+ path_hits: list[str],
344
+ header_hits: list[str],
345
+ index_hits: list[str],
346
+ body_hits: list[str],
347
+ ) -> str | None:
348
+ """根据强锚点和弱证据判断候选置信度。
349
+
350
+ Args:
351
+ matched_triggers: frontmatter trigger 命中。
352
+ path_hits: 路径命中。
353
+ header_hits: 标题命中。
354
+ index_hits: index 描述命中。
355
+ body_hits: 正文样本命中。
356
+
357
+ Returns:
358
+ `high` / `medium`;证据不足时返回 None。
359
+ """
360
+ if matched_triggers:
361
+ return "high"
362
+
363
+ anchor_groups = [path_hits, header_hits, index_hits]
364
+ anchor_count = sum(1 for group in anchor_groups if group)
365
+ if len(path_hits) >= 2 or len(header_hits) >= 2 or len(index_hits) >= 2:
366
+ return "high"
367
+ if anchor_count >= 2:
368
+ return "high"
369
+ if anchor_count == 1 and len(body_hits) >= MIN_ANCHORED_BODY_HITS:
370
+ return "high"
371
+ if anchor_count == 1:
372
+ return "medium"
373
+ if len(body_hits) >= MIN_BODY_ONLY_HITS:
374
+ return "medium"
375
+ return None
376
+
377
+
378
+ def action_for_confidence(confidence: str) -> str:
379
+ """按置信度返回读取建议。
380
+
381
+ Args:
382
+ confidence: 候选置信度。
383
+
384
+ Returns:
385
+ 面向 AI 的行动建议。
386
+ """
387
+ if confidence == "high":
388
+ return "read before acting"
389
+ return "read if clearly relevant"
390
+
391
+
206
392
  def read_markdown(path: Path) -> str | None:
207
393
  """以容错 UTF-8 方式读取 Markdown 文件。
208
394
 
@@ -239,7 +425,13 @@ def iter_spec_files(spec_dir: Path) -> list[Path]:
239
425
  return sorted(result)
240
426
 
241
427
 
242
- def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> Candidate | None:
428
+ def score_file(
429
+ root: Path,
430
+ path: Path,
431
+ query: str,
432
+ query_tokens: list[str],
433
+ index_descriptions: dict[str, list[str]],
434
+ ) -> Candidate | None:
243
435
  """按查询为一个 Markdown spec 文件打分。
244
436
 
245
437
  Args:
@@ -247,6 +439,7 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
247
439
  path: Markdown 文件路径。
248
440
  query: 原始查询文本。
249
441
  query_tokens: 标准化后的查询 token。
442
+ index_descriptions: 从 index.md 收集的目标文档描述。
250
443
 
251
444
  Returns:
252
445
  分数达到阈值时返回候选项,否则返回 None。
@@ -258,11 +451,13 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
258
451
  metadata, body = parse_frontmatter(text)
259
452
  rel_path = path.relative_to(root).as_posix()
260
453
  spec_rel_path = path.relative_to(root / ".trellis" / "spec").as_posix()
261
- spec_rel_lower = spec_rel_path.lower()
454
+ spec_rel_tokens = normalize_tokens(spec_rel_path)
262
455
  body_sample = body[:MAX_BODY_CHARS]
263
- body_lower = body_sample.lower()
456
+ body_tokens = normalize_tokens(body_sample)
264
457
  headers = HEADER_RE.findall(body)
265
- header_text = " ".join(headers).lower()
458
+ header_tokens = normalize_tokens(" ".join(headers))
459
+ index_text = " ".join(index_descriptions.get(spec_rel_path, []))
460
+ index_tokens = normalize_tokens(index_text)
266
461
 
267
462
  kind = str(metadata.get("kind") or "").strip()
268
463
  load = str(metadata.get("load") or "").strip()
@@ -282,31 +477,35 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
282
477
  score += 8 * len(matched_triggers)
283
478
  reasons.append(f"matched triggers: {', '.join(matched_triggers[:5])}")
284
479
 
285
- path_hits = [token for token in query_tokens if token in spec_rel_lower]
480
+ path_hits = significant_hits(query_tokens, spec_rel_tokens)
286
481
  if path_hits:
287
- score += 4 * len(path_hits)
482
+ score += 5 * len(path_hits)
288
483
  reasons.append(f"matched path tokens: {', '.join(path_hits[:5])}")
289
484
 
290
- header_hits = [token for token in query_tokens if token in header_text]
485
+ header_hits = significant_hits(query_tokens, header_tokens)
291
486
  if header_hits:
292
487
  score += 3 * len(header_hits)
293
488
  reasons.append(f"matched headings: {', '.join(header_hits[:5])}")
294
489
 
295
- raw_body_hits = [token for token in query_tokens if token in body_lower]
296
- body_hits = [token for token in raw_body_hits if token not in BODY_WEAK_TOKENS]
490
+ index_hits = significant_hits(query_tokens, index_tokens)
491
+ if index_hits:
492
+ score += 4 * len(index_hits)
493
+ reasons.append(f"matched index descriptions: {', '.join(index_hits[:5])}")
494
+
495
+ body_hits = significant_hits(query_tokens, body_tokens)
297
496
  if body_hits:
298
497
  score += len(body_hits)
299
498
  reasons.append(f"matched body tokens: {', '.join(body_hits[:5])}")
300
499
 
301
- # 避免 `json` / `output` / `spec` 这类泛词把只有正文弱命中的文件全部拉进上下文。
302
- strong_match = (
303
- bool(matched_triggers)
304
- or bool(path_hits)
305
- or len(header_hits) >= 2
306
- or (bool(header_hits) and len(body_hits) >= MIN_HEADING_BODY_HITS)
307
- or len(body_hits) >= MIN_BODY_ONLY_HITS
500
+ # 避免 `to` / `flow` / `commit` 这类泛词或少量正文词把无关文件拉进上下文。
501
+ confidence = classify_confidence(
502
+ matched_triggers,
503
+ path_hits,
504
+ header_hits,
505
+ index_hits,
506
+ body_hits,
308
507
  )
309
- if not strong_match:
508
+ if confidence is None:
310
509
  return None
311
510
 
312
511
  if kind.lower() in {"sop", "procedure", "guide", "thinking-guide"} and score > 0:
@@ -325,7 +524,9 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
325
524
  kind=kind or ("thinking-guide" if "/guides/" in f"/{rel_path}" else "spec"),
326
525
  load=load,
327
526
  priority=priority,
527
+ confidence=confidence,
328
528
  reasons=reasons,
529
+ action=action_for_confidence(confidence),
329
530
  )
330
531
 
331
532
 
@@ -345,12 +546,20 @@ def find_candidates(root: Path, query: str, limit: int) -> list[Candidate]:
345
546
  return []
346
547
 
347
548
  candidates: list[Candidate] = []
348
- for path in iter_spec_files(root / ".trellis" / "spec"):
349
- candidate = score_file(root, path, query, query_tokens)
549
+ spec_dir = root / ".trellis" / "spec"
550
+ index_descriptions = collect_index_descriptions(spec_dir)
551
+ for path in iter_spec_files(spec_dir):
552
+ candidate = score_file(root, path, query, query_tokens, index_descriptions)
350
553
  if candidate:
351
554
  candidates.append(candidate)
352
555
 
353
- candidates.sort(key=lambda item: (-item.score, item.path))
556
+ candidates.sort(
557
+ key=lambda item: (
558
+ 0 if item.confidence == "high" else 1,
559
+ -item.score,
560
+ item.path,
561
+ )
562
+ )
354
563
  return candidates[:limit]
355
564
 
356
565
 
@@ -372,13 +581,14 @@ def format_markdown(candidates: list[Candidate]) -> str:
372
581
  lines.append(f"- {candidate.path}")
373
582
  lines.append(f" kind: {candidate.kind}")
374
583
  lines.append(f" score: {candidate.score}")
584
+ lines.append(f" confidence: {candidate.confidence}")
375
585
  if candidate.load:
376
586
  lines.append(f" load: {candidate.load}")
377
587
  if candidate.priority:
378
588
  lines.append(f" priority: {candidate.priority}")
379
589
  if candidate.reasons:
380
590
  lines.append(f" reason: {'; '.join(candidate.reasons)}")
381
- lines.append(" action: read before acting")
591
+ lines.append(f" action: {candidate.action}")
382
592
  return "\n".join(lines)
383
593
 
384
594
 
@@ -396,10 +606,11 @@ def format_json(candidates: list[Candidate]) -> str:
396
606
  "file": candidate.path,
397
607
  "kind": candidate.kind,
398
608
  "score": candidate.score,
609
+ "confidence": candidate.confidence,
399
610
  "load": candidate.load,
400
611
  "priority": candidate.priority,
401
612
  "reason": candidate.reasons,
402
- "action": "read before acting",
613
+ "action": candidate.action,
403
614
  }
404
615
  for candidate in candidates
405
616
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-07-01T04:27:03.524Z",
2
+ "syncedAt": "2026-07-02T01:39:38.800Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "7a9b882db76500b49b38cf423be682fb06effe1a",
4
+ "sourceCommit": "39a09335cc3e2878c1e1121137dd55524882b13f",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "craft-rpa",
@@ -140,6 +140,7 @@
140
140
  ],
141
141
  "scripts": [
142
142
  "auto_loop.py",
143
+ "push_snapshot.py",
143
144
  "spec_router.py"
144
145
  ]
145
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.0-beta.4",
3
+ "version": "0.4.0-beta.5",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,8 @@ export function copyScriptAssets(target, variantDir, skills = []) {
24
24
  let aliases = [];
25
25
  if (name === "auto_loop") {
26
26
  aliases = ["auto-loop", "auto-loop-runner", "trellis-auto-loop"];
27
+ } else if (name === "push_snapshot") {
28
+ aliases = ["push-snapshot", "trellis-push", "push", "snapshot"];
27
29
  } else if (name === "spec_router") {
28
30
  aliases = [
29
31
  "spec-router",