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.
- package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +17 -8
- package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +8 -3
- package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +17 -8
- package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +8 -3
- package/enhancements/0.6/overrides/skills/trellis-finish-work.md +14 -0
- package/enhancements/0.6/overrides/workflow-states/in_progress-inline.md +2 -2
- package/enhancements/0.6/overrides/workflow-states/in_progress.md +2 -2
- package/enhancements/0.6/overrides/workflow-states/no_task.md +2 -2
- package/enhancements/0.6/overrides/workflow-states/planning-inline.md +1 -1
- package/enhancements/0.6/overrides/workflow-states/planning.md +1 -1
- package/enhancements/0.6/overrides/workflow.md +18 -10
- package/enhancements/0.6/scripts/push_snapshot.py +387 -0
- package/enhancements/0.6/scripts/spec_router.py +244 -33
- package/enhancements/MANIFEST.json +3 -2
- package/package.json +1 -1
- package/src/lib/copy-scripts.js +2 -0
|
@@ -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
|
-
-
|
|
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
|
|
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.
|
|
373
|
-
2.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
373
|
-
2.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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-
|
|
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
|
-
"""
|
|
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)
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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
|
-
|
|
454
|
+
spec_rel_tokens = normalize_tokens(spec_rel_path)
|
|
262
455
|
body_sample = body[:MAX_BODY_CHARS]
|
|
263
|
-
|
|
456
|
+
body_tokens = normalize_tokens(body_sample)
|
|
264
457
|
headers = HEADER_RE.findall(body)
|
|
265
|
-
|
|
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 =
|
|
480
|
+
path_hits = significant_hits(query_tokens, spec_rel_tokens)
|
|
286
481
|
if path_hits:
|
|
287
|
-
score +=
|
|
482
|
+
score += 5 * len(path_hits)
|
|
288
483
|
reasons.append(f"matched path tokens: {', '.join(path_hits[:5])}")
|
|
289
484
|
|
|
290
|
-
header_hits =
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
# 避免 `
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
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(
|
|
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:
|
|
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":
|
|
613
|
+
"action": candidate.action,
|
|
403
614
|
}
|
|
404
615
|
for candidate in candidates
|
|
405
616
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-07-
|
|
2
|
+
"syncedAt": "2026-07-02T01:39:38.800Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
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
package/src/lib/copy-scripts.js
CHANGED
|
@@ -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",
|