flower-trellis 0.4.0-beta.1 → 0.4.0-beta.2

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.
@@ -12,10 +12,13 @@ description: "启动、恢复和推进 Trellis 自动任务循环。用于用户
12
12
  - 只有用户明确要求自动跑、auto loop、goal-like、继续自动 run、批量任务队列时才启动或恢复;不要把普通实现请求自动升级为 auto-loop。
13
13
  - 每次开始、恢复、压缩后继续时,先运行 runner 的 `resume` 或 `next`,不要凭聊天摘要推断下一步。
14
14
  - 每完成一个 action,必须用 `record --action <next 返回的 action>` 精确写回结果;runner 会拒绝缺失或不匹配的 action。写回后立即再调用 `next`,直到 `done`、`blocked` 或需要用户决策。
15
+ - run 进入 `blocked` 后不要用 `start --force` 新建 run 来纠正参数;先补齐缺失 route/context,然后用 `retry-blocked` 在同一个 run 内恢复。
16
+ - runner 默认输出是紧凑 JSON,只包含当前 action、队列计数、简短 blocked/pending/completed 列表和最近少量决策摘要;排障时才给 `status` / `resume` / `next` / `record` / `retry-blocked` 加 `--verbose` 读取完整 item、blocked detail 和 decision data。
15
17
  - 默认 profile 是 `commit-only`:自动推进到本地 commit,不 push、不发布、不归档。
16
18
  - 多任务只按用户显式给出的任务顺序执行;同一 worktree 不并发。
17
19
  - 启动 runner 前先完成 route 准备度判断:已有当前任务 runtime route 决策或个人 `.trellis/.route-prefs.tmp` 时可启动;没有时先进入 `trellis-route` 正常询问 / fallback,写入真实决策后再启动。
18
- - auto-loop 不默认写 `route_authorization`;如果未来显式传 route 参数,只能表示用户本次明确给出的临时策略,不能当成模型真实执行结果。
20
+ - auto-loop 不默认写 `route_authorization`;只有用户本次明确给出的临时 route 策略,才能通过 `--route-implement` / `--route-check` 传给 runner,且不能当成模型真实执行结果。
21
+ - auto-loop 启动前若 implement 与 check 都缺 route,优先展示 auto-loop 专用的合并选择,不要把 `trellis-route` 的两套完整 fallback 原样贴给用户。仍允许用户回复高级格式 `implement 1, check 1`。
19
22
  - 代码提交必须走 `trellis-push` 的 commit-only 语义;不要裸 `git commit` / `git push`。
20
23
 
21
24
  ## 启动
@@ -29,12 +32,35 @@ python3 .agents/skills/trellis-route/scripts/route_state.py resolve --target imp
29
32
  python3 .agents/skills/trellis-route/scripts/route_state.py resolve --target check
30
33
  ```
31
34
 
32
- 如果任一 target 返回 `status=miss`,先按 `trellis-route` 的正常 numbered fallback 询问用户并写入 runtime;不要替用户默认 inline 或 subagent。route 准备完成后启动 runner:
35
+ 如果两个 target 都返回 `status=miss`,优先用 auto-loop 专用合并选择询问用户,避免把两套 route fallback 列表完整贴出:
36
+
37
+ ```text
38
+ auto-loop 需要你先选执行路线,才能启动。
39
+
40
+ 推荐:
41
+ 1. 本次全 Inline:implement inline + check-all inline(只影响本次 run)
42
+ 2. 本次全 Subagent:implement subagent + check-all subagent(只影响本次 run)
43
+ 3. 保存默认全 Inline:写入个人默认,后续自动复用
44
+ 4. 保存默认全 Subagent:写入个人默认,后续自动复用
45
+
46
+ 高级:也可以回复 `implement 1, check 2` 分别选择。
47
+ ```
48
+
49
+ 映射规则:
50
+
51
+ - `1` → `--route-implement inline --route-check check-all-inline`
52
+ - `2` → `--route-implement subagent --route-check check-all-subagent`
53
+ - `3` → 先用 `trellis-route` helper 分别写入 `implement=inline`、`check=check-all-inline` 且 `--save-pref`,再启动 runner
54
+ - `4` → 先用 `trellis-route` helper 分别写入 `implement=subagent`、`check=check-all-subagent` 且 `--save-pref`,再启动 runner
55
+
56
+ 如果只有一个 target 返回 `status=miss`,再按 `trellis-route` 的对应 target 正常 numbered fallback 询问。不要替用户默认 inline 或 subagent。若用户选择的是本次临时策略,把选择映射为 runner route 参数一起传入,例如 `implement 1, check 1` 对应 `--route-implement inline --route-check check-all-inline`。若用户选择保存默认,则由 `trellis-route` 写入偏好后再启动 runner。
33
57
 
34
58
  ```bash
35
59
  python3 ./.trellis/scripts/auto_loop.py start \
36
60
  --tasks <task> [<task> ...] \
37
- --profile commit-only
61
+ --profile commit-only \
62
+ [--route-implement inline|subagent] \
63
+ [--route-check check-all-inline|check-all-subagent]
38
64
  ```
39
65
 
40
66
  多任务队列中,当前任务切换到下一个任务且缺少该任务 route 决策时,回到 `trellis-route` 获取该任务真实选择,再继续 `next` / `record`。个人 `.trellis/.route-prefs.tmp` 会由 `trellis-route` 统一复用并写回 runtime。
@@ -54,7 +80,22 @@ python3 ./.trellis/scripts/auto_loop.py resume
54
80
  python3 ./.trellis/scripts/auto_loop.py next
55
81
  ```
56
82
 
57
- `resume_capsule` 只用于展示;下一步以 `next` 返回的 JSON 为准。
83
+ `resume` 默认只输出紧凑状态;`resume_capsule` 不再持久写入 runtime JSON,仅在 `resume --verbose` / `status --verbose` 等诊断输出中动态生成。下一步以 `next` 返回的 JSON 为准。
84
+
85
+ ## Blocked 后重试
86
+
87
+ 如果 `next` 或 `status` 显示 run 内有 blocked 队列项,先根据 blocked reason 补齐条件,然后复用同一个 run:
88
+
89
+ ```bash
90
+ python3 ./.trellis/scripts/auto_loop.py retry-blocked \
91
+ [--run-id <run-id>] \
92
+ [--task <task>] \
93
+ [--route-implement inline|subagent] \
94
+ [--route-check check-all-inline|check-all-subagent]
95
+ python3 ./.trellis/scripts/auto_loop.py next
96
+ ```
97
+
98
+ 常见场景:启动时漏传临时 route,导致 `missing-implement-context` / `missing-check-context`。此时不要 `start --force`,直接用 `retry-blocked --route-implement ... --route-check ...` 重置 blocked 项。
58
99
 
59
100
  ## Action 映射
60
101
 
@@ -92,6 +133,8 @@ python3 ./.trellis/scripts/auto_loop.py record \
92
133
 
93
134
  runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
94
135
 
136
+ `record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
137
+
95
138
  route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
96
139
  `--route-mode inline --route-source route-prefs` 或
97
140
  `--route-mode check-all-subagent --route-source trellis-route`;不要写 auto-loop 默认值。
@@ -12,10 +12,13 @@ description: "启动、恢复和推进 Trellis 自动任务循环。用于用户
12
12
  - 只有用户明确要求自动跑、auto loop、goal-like、继续自动 run、批量任务队列时才启动或恢复;不要把普通实现请求自动升级为 auto-loop。
13
13
  - 每次开始、恢复、压缩后继续时,先运行 runner 的 `resume` 或 `next`,不要凭聊天摘要推断下一步。
14
14
  - 每完成一个 action,必须用 `record --action <next 返回的 action>` 精确写回结果;runner 会拒绝缺失或不匹配的 action。写回后立即再调用 `next`,直到 `done`、`blocked` 或需要用户决策。
15
+ - run 进入 `blocked` 后不要用 `start --force` 新建 run 来纠正参数;先补齐缺失 route/context,然后用 `retry-blocked` 在同一个 run 内恢复。
16
+ - runner 默认输出是紧凑 JSON,只包含当前 action、队列计数、简短 blocked/pending/completed 列表和最近少量决策摘要;排障时才给 `status` / `resume` / `next` / `record` / `retry-blocked` 加 `--verbose` 读取完整 item、blocked detail 和 decision data。
15
17
  - 默认 profile 是 `commit-only`:自动推进到本地 commit,不 push、不发布、不归档。
16
18
  - 多任务只按用户显式给出的任务顺序执行;同一 worktree 不并发。
17
19
  - 启动 runner 前先完成 route 准备度判断:已有当前任务 runtime route 决策或个人 `.trellis/.route-prefs.tmp` 时可启动;没有时先进入 `trellis-route` 正常询问 / fallback,写入真实决策后再启动。
18
- - auto-loop 不默认写 `route_authorization`;如果未来显式传 route 参数,只能表示用户本次明确给出的临时策略,不能当成模型真实执行结果。
20
+ - auto-loop 不默认写 `route_authorization`;只有用户本次明确给出的临时 route 策略,才能通过 `--route-implement` / `--route-check` 传给 runner,且不能当成模型真实执行结果。
21
+ - auto-loop 启动前若 implement 与 check 都缺 route,优先展示 auto-loop 专用的合并选择,不要把 `trellis-route` 的两套完整 fallback 原样贴给用户。仍允许用户回复高级格式 `implement 1, check 1`。
19
22
  - 代码提交必须走 `trellis-push` 的 commit-only 语义;不要裸 `git commit` / `git push`。
20
23
 
21
24
  ## 启动
@@ -29,12 +32,35 @@ python3 .agents/skills/trellis-route/scripts/route_state.py resolve --target imp
29
32
  python3 .agents/skills/trellis-route/scripts/route_state.py resolve --target check
30
33
  ```
31
34
 
32
- 如果任一 target 返回 `status=miss`,先按 `trellis-route` 的正常 numbered fallback 询问用户并写入 runtime;不要替用户默认 inline 或 subagent。route 准备完成后启动 runner:
35
+ 如果两个 target 都返回 `status=miss`,优先用 auto-loop 专用合并选择询问用户,避免把两套 route fallback 列表完整贴出:
36
+
37
+ ```text
38
+ auto-loop 需要你先选执行路线,才能启动。
39
+
40
+ 推荐:
41
+ 1. 本次全 Inline:implement inline + check-all inline(只影响本次 run)
42
+ 2. 本次全 Subagent:implement subagent + check-all subagent(只影响本次 run)
43
+ 3. 保存默认全 Inline:写入个人默认,后续自动复用
44
+ 4. 保存默认全 Subagent:写入个人默认,后续自动复用
45
+
46
+ 高级:也可以回复 `implement 1, check 2` 分别选择。
47
+ ```
48
+
49
+ 映射规则:
50
+
51
+ - `1` → `--route-implement inline --route-check check-all-inline`
52
+ - `2` → `--route-implement subagent --route-check check-all-subagent`
53
+ - `3` → 先用 `trellis-route` helper 分别写入 `implement=inline`、`check=check-all-inline` 且 `--save-pref`,再启动 runner
54
+ - `4` → 先用 `trellis-route` helper 分别写入 `implement=subagent`、`check=check-all-subagent` 且 `--save-pref`,再启动 runner
55
+
56
+ 如果只有一个 target 返回 `status=miss`,再按 `trellis-route` 的对应 target 正常 numbered fallback 询问。不要替用户默认 inline 或 subagent。若用户选择的是本次临时策略,把选择映射为 runner route 参数一起传入,例如 `implement 1, check 1` 对应 `--route-implement inline --route-check check-all-inline`。若用户选择保存默认,则由 `trellis-route` 写入偏好后再启动 runner。
33
57
 
34
58
  ```bash
35
59
  python3 ./.trellis/scripts/auto_loop.py start \
36
60
  --tasks <task> [<task> ...] \
37
- --profile commit-only
61
+ --profile commit-only \
62
+ [--route-implement inline|subagent] \
63
+ [--route-check check-all-inline|check-all-subagent]
38
64
  ```
39
65
 
40
66
  多任务队列中,当前任务切换到下一个任务且缺少该任务 route 决策时,回到 `trellis-route` 获取该任务真实选择,再继续 `next` / `record`。个人 `.trellis/.route-prefs.tmp` 会由 `trellis-route` 统一复用并写回 runtime。
@@ -54,7 +80,22 @@ python3 ./.trellis/scripts/auto_loop.py resume
54
80
  python3 ./.trellis/scripts/auto_loop.py next
55
81
  ```
56
82
 
57
- `resume_capsule` 只用于展示;下一步以 `next` 返回的 JSON 为准。
83
+ `resume` 默认只输出紧凑状态;`resume_capsule` 不再持久写入 runtime JSON,仅在 `resume --verbose` / `status --verbose` 等诊断输出中动态生成。下一步以 `next` 返回的 JSON 为准。
84
+
85
+ ## Blocked 后重试
86
+
87
+ 如果 `next` 或 `status` 显示 run 内有 blocked 队列项,先根据 blocked reason 补齐条件,然后复用同一个 run:
88
+
89
+ ```bash
90
+ python3 ./.trellis/scripts/auto_loop.py retry-blocked \
91
+ [--run-id <run-id>] \
92
+ [--task <task>] \
93
+ [--route-implement inline|subagent] \
94
+ [--route-check check-all-inline|check-all-subagent]
95
+ python3 ./.trellis/scripts/auto_loop.py next
96
+ ```
97
+
98
+ 常见场景:启动时漏传临时 route,导致 `missing-implement-context` / `missing-check-context`。此时不要 `start --force`,直接用 `retry-blocked --route-implement ... --route-check ...` 重置 blocked 项。
58
99
 
59
100
  ## Action 映射
60
101
 
@@ -92,6 +133,8 @@ python3 ./.trellis/scripts/auto_loop.py record \
92
133
 
93
134
  runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
94
135
 
136
+ `record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
137
+
95
138
  route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
96
139
  `--route-mode inline --route-source route-prefs` 或
97
140
  `--route-mode check-all-subagent --route-source trellis-route`;不要写 auto-loop 默认值。
@@ -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 any matched SOP/spec before acting.
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.
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.
@@ -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 any matched SOP/spec before acting.
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.
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.
@@ -3,7 +3,7 @@ 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
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 any matched SOP/spec before acting.
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.
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 any matched SOP/spec before acting.
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.
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 any matched SOP/spec before acting.
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.
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 -->
@@ -23,7 +23,9 @@ the intended action, commands about to run, files or systems involved, package/l
23
23
  and domain words such as release, publish, deploy, migration, config, CI, workflow,
24
24
  hooks, rollback, data fix, or destructive command.
25
25
 
26
- Read any matched SOP/spec files before acting; if nothing matches, continue normally.
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.
27
29
 
28
30
  #### Task Brief Handoff
29
31
 
@@ -16,8 +16,17 @@ from typing import Any
16
16
  SCHEMA_VERSION = 1
17
17
  DEFAULT_PROFILE = "commit-only"
18
18
  MAX_FIX_RECHECK = 3
19
+ DECISION_LOG_LIMIT = 20
19
20
  VALID_IMPLEMENT_ROUTES = {"inline", "subagent"}
20
21
  VALID_CHECK_ROUTES = {"check-all-inline", "check-all-subagent"}
22
+ RECOVERABLE_BLOCK_REASONS = {
23
+ "missing-prd",
24
+ "open-questions",
25
+ "incomplete-complex-artifacts",
26
+ "missing-implement-context",
27
+ "missing-check-context",
28
+ "unknown-step",
29
+ }
21
30
  STEP_ACTIONS = {
22
31
  "refresh_brief": "refresh_brief",
23
32
  "start_task": "start_task",
@@ -214,10 +223,10 @@ def _append_item_decision(
214
223
  "summary": summary,
215
224
  "data": data or {},
216
225
  })
217
- item["decision_log"] = log[-50:]
226
+ item["decision_log"] = log[-DECISION_LOG_LIMIT:]
218
227
 
219
228
 
220
- def _decision_tail(state: dict[str, Any], limit: int = 8) -> list[dict[str, Any]]:
229
+ def _decision_tail(state: dict[str, Any], limit: int = 8, include_data: bool = True) -> list[dict[str, Any]]:
221
230
  """返回 run 内最近的关键决策摘要。"""
222
231
  queue = state.get("queue") if isinstance(state.get("queue"), list) else []
223
232
  events: list[dict[str, Any]] = []
@@ -227,7 +236,17 @@ def _decision_tail(state: dict[str, Any], limit: int = 8) -> list[dict[str, Any]
227
236
  log = item.get("decision_log")
228
237
  if isinstance(log, list):
229
238
  events.extend(event for event in log if isinstance(event, dict))
230
- return events[-limit:]
239
+ tail = events[-limit:]
240
+ if include_data:
241
+ return tail
242
+ compact: list[dict[str, Any]] = []
243
+ for event in tail:
244
+ compact.append({
245
+ key: event.get(key)
246
+ for key in ("at", "type", "task", "summary")
247
+ if event.get(key) is not None
248
+ })
249
+ return compact
231
250
 
232
251
 
233
252
  def _run_path(repo_root: Path, run_id: str) -> Path:
@@ -235,6 +254,11 @@ def _run_path(repo_root: Path, run_id: str) -> Path:
235
254
  return _auto_dir(repo_root) / f"{run_id}.json"
236
255
 
237
256
 
257
+ def _run_paths(repo_root: Path) -> list[Path]:
258
+ """返回按 run id 从新到旧排序的 auto run 文件。"""
259
+ return sorted(_auto_dir(repo_root).glob("auto-*.json"), reverse=True)
260
+
261
+
238
262
  def _new_run_id() -> str:
239
263
  """生成短小稳定的 run id。"""
240
264
  return "auto-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
@@ -417,7 +441,7 @@ def _load_current_state(repo_root: Path, run_id: str | None = None) -> tuple[Pat
417
441
  _clear_pointer_if_current(repo_root, current)
418
442
 
419
443
  running: list[tuple[Path, dict[str, Any]]] = []
420
- for path in sorted(_auto_dir(repo_root).glob("auto-*.json")):
444
+ for path in _run_paths(repo_root):
421
445
  state = _read_json(path)
422
446
  if state.get("status") == "running":
423
447
  running.append((path, state))
@@ -431,10 +455,62 @@ def _load_current_state(repo_root: Path, run_id: str | None = None) -> tuple[Pat
431
455
  raise ValueError("没有可恢复的唯一 auto run")
432
456
 
433
457
 
458
+ def _load_blocked_state(repo_root: Path, run_id: str | None = None) -> tuple[Path, dict[str, Any]]:
459
+ """加载可重试的 blocked auto run。"""
460
+ if run_id:
461
+ path, state = _load_current_state(repo_root, run_id)
462
+ if _blocked_items(state):
463
+ return path, state
464
+ raise ValueError(f"auto run 没有 blocked 队列项:{run_id}")
465
+
466
+ try:
467
+ path, state = _load_current_state(repo_root)
468
+ if _blocked_items(state):
469
+ return path, state
470
+ except ValueError:
471
+ pass
472
+
473
+ blocked_runs: list[tuple[Path, dict[str, Any]]] = []
474
+ for path in _run_paths(repo_root):
475
+ state = _read_json(path)
476
+ if state and _blocked_items(state):
477
+ blocked_runs.append((path, state))
478
+ if len(blocked_runs) == 1:
479
+ return blocked_runs[0]
480
+ if not blocked_runs:
481
+ raise ValueError("没有可重试的 blocked auto run")
482
+ run_ids = ", ".join(str(state.get("run_id") or path.stem) for path, state in blocked_runs[:8])
483
+ raise ValueError(f"存在多个 blocked auto run,请指定 --run-id。候选:{run_ids}")
484
+
485
+
486
+ def _recent_run_summaries(repo_root: Path, limit: int = 8) -> list[dict[str, Any]]:
487
+ """返回最近 auto run 的轻量状态列表。"""
488
+ runs: list[dict[str, Any]] = []
489
+ for path in _run_paths(repo_root)[:limit]:
490
+ state = _read_json(path)
491
+ if not state:
492
+ continue
493
+ counts = _queue_counts(state)
494
+ current = _current_queue_item(state)
495
+ runs.append({
496
+ "run_id": state.get("run_id") or path.stem,
497
+ "path": _rel_path(repo_root, path),
498
+ "run_status": state.get("status"),
499
+ "profile": state.get("profile"),
500
+ "updated_at": state.get("updated_at"),
501
+ "completed": counts["completed"],
502
+ "blocked": counts["blocked"],
503
+ "remaining": counts["remaining"],
504
+ "current_task": current.get("task") if current else None,
505
+ "next_step": current.get("current_step") if current else "done",
506
+ })
507
+ return runs
508
+
509
+
434
510
  def _write_state(path: Path, state: dict[str, Any]) -> None:
435
511
  """刷新 auto run 状态和更新时间。"""
436
512
  state["updated_at"] = _utc_now()
437
- state["resume_capsule"] = _resume_capsule(state)
513
+ state.pop("resume_capsule", None)
438
514
  _write_json(path, state)
439
515
 
440
516
 
@@ -459,72 +535,167 @@ def _clear_pointer_if_current(repo_root: Path, run_id: str | None) -> None:
459
535
 
460
536
  def _resume_capsule(state: dict[str, Any]) -> dict[str, Any]:
461
537
  """生成短小的人类可读恢复摘要。"""
462
- queue = state.get("queue") if isinstance(state.get("queue"), list) else []
463
- current = None
464
- auto_completed = []
465
- unarchived_tasks = []
466
- blocked = []
467
- for item in queue:
468
- if not isinstance(item, dict):
469
- continue
470
- if current is None and item.get("status") in {"pending", "running"}:
471
- current = item
472
- if item.get("status") == "completed":
473
- auto_completed.append(item.get("task"))
474
- unarchived_tasks.append(item.get("task"))
475
- if item.get("status") == "blocked":
476
- blocked.append(item.get("task"))
538
+ queue = _queue_items(state)
539
+ current = _current_queue_item(state)
540
+ auto_completed = [item.get("task") for item in queue if item.get("status") == "completed"]
541
+ blocked = [item.get("task") for item in queue if item.get("status") == "blocked"]
542
+ counts = _queue_counts(state)
477
543
  return {
478
544
  "run_id": state.get("run_id"),
479
545
  "status": state.get("status"),
480
546
  "current_task": current.get("task") if current else None,
481
547
  "next_step": current.get("current_step") if current else "done",
482
- "completed": sum(1 for item in queue if isinstance(item, dict) and item.get("status") == "completed"),
483
- "blocked": sum(1 for item in queue if isinstance(item, dict) and item.get("status") == "blocked"),
484
- "remaining": sum(1 for item in queue if isinstance(item, dict) and item.get("status") in {"pending", "running"}),
548
+ "completed": counts["completed"],
549
+ "blocked": counts["blocked"],
550
+ "remaining": counts["remaining"],
485
551
  "auto_completed_tasks": auto_completed,
486
- "unarchived_tasks": unarchived_tasks,
487
552
  "task_lifecycle_note": "auto-loop completed means local commit completed; run finish-work/archive explicitly when ready" if auto_completed else None,
488
553
  "blocked_tasks": blocked,
489
- "recent_decisions": _decision_tail(state, 5),
490
554
  }
491
555
 
492
556
 
493
- def _summary(state: dict[str, Any]) -> dict[str, Any]:
494
- """返回队列状态摘要。"""
557
+ def _terminal_status(queue: list[Any]) -> str:
558
+ """根据队列终态区分全完成和带阻塞结束。"""
559
+ has_blocked = any(isinstance(item, dict) and item.get("status") == "blocked" for item in queue)
560
+ return "blocked" if has_blocked else "completed"
561
+
562
+
563
+ def _queue_items(state: dict[str, Any]) -> list[dict[str, Any]]:
564
+ """返回合法队列项。"""
495
565
  queue = state.get("queue") if isinstance(state.get("queue"), list) else []
496
- outstanding_action = None
497
- for item in queue:
498
- if not isinstance(item, dict) or item.get("status") != "running":
566
+ return [item for item in queue if isinstance(item, dict)]
567
+
568
+
569
+ def _queue_counts(state: dict[str, Any]) -> dict[str, int]:
570
+ """返回队列状态计数。"""
571
+ queue = _queue_items(state)
572
+ return {
573
+ "total": len(queue),
574
+ "completed": sum(1 for item in queue if item.get("status") == "completed"),
575
+ "blocked": sum(1 for item in queue if item.get("status") == "blocked"),
576
+ "remaining": sum(1 for item in queue if item.get("status") in {"pending", "running"}),
577
+ }
578
+
579
+
580
+ def _current_queue_item(state: dict[str, Any]) -> dict[str, Any] | None:
581
+ """返回当前待处理队列项。"""
582
+ for item in _queue_items(state):
583
+ if item.get("status") in {"pending", "running"}:
584
+ return item
585
+ return None
586
+
587
+
588
+ def _blocked_items(state: dict[str, Any]) -> list[dict[str, Any]]:
589
+ """返回 blocked 队列项。"""
590
+ return [item for item in _queue_items(state) if item.get("status") == "blocked"]
591
+
592
+
593
+ def _outstanding_action(state: dict[str, Any]) -> dict[str, Any] | None:
594
+ """返回当前等待 record 回写的 action。"""
595
+ for item in _queue_items(state):
596
+ if item.get("status") != "running":
499
597
  continue
500
598
  last_action = item.get("last_action")
501
599
  if isinstance(last_action, dict):
502
- outstanding_action = {
600
+ return {
503
601
  "task": item.get("task"),
504
602
  "action": last_action.get("action"),
505
603
  "current_step": last_action.get("current_step"),
506
604
  "issued_at": last_action.get("issued_at"),
507
605
  }
508
- break
509
- return {
606
+ return None
607
+ return None
608
+
609
+
610
+ def _completed_task_summaries(state: dict[str, Any]) -> list[dict[str, Any]]:
611
+ """返回已完成任务的紧凑摘要。"""
612
+ completed: list[dict[str, Any]] = []
613
+ for item in _queue_items(state):
614
+ if item.get("status") != "completed":
615
+ continue
616
+ task = {"task": item.get("task")}
617
+ if item.get("commit"):
618
+ task["commit"] = item.get("commit")
619
+ completed.append(task)
620
+ return completed
621
+
622
+
623
+ def _blocked_task_summaries(state: dict[str, Any], include_detail: bool = False) -> list[dict[str, Any]]:
624
+ """返回 blocked 任务摘要,默认不带 detail。"""
625
+ blocked_tasks: list[dict[str, Any]] = []
626
+ for item in _blocked_items(state):
627
+ blocked = item.get("blocked")
628
+ blocked_data = blocked if isinstance(blocked, dict) else {}
629
+ task = {
630
+ "task": item.get("task"),
631
+ "reason": blocked_data.get("reason"),
632
+ "summary": blocked_data.get("summary"),
633
+ "blocked_at": blocked_data.get("blocked_at"),
634
+ }
635
+ if include_detail:
636
+ task["detail"] = blocked_data.get("detail") or {}
637
+ blocked_tasks.append(task)
638
+ return blocked_tasks
639
+
640
+
641
+ def _pending_task_summaries(state: dict[str, Any], include_status: bool = False) -> list[dict[str, Any]]:
642
+ """返回未完成任务摘要。"""
643
+ pending: list[dict[str, Any]] = []
644
+ for item in _queue_items(state):
645
+ if item.get("status") not in {"pending", "running"}:
646
+ continue
647
+ task = {
648
+ "task": item.get("task"),
649
+ "status": item.get("status"),
650
+ "current_step": item.get("current_step"),
651
+ }
652
+ if include_status:
653
+ task["last_failure"] = item.get("last_failure")
654
+ task["attempts"] = item.get("attempts")
655
+ pending.append(task)
656
+ return pending
657
+
658
+
659
+ def _compact_summary(state: dict[str, Any]) -> dict[str, Any]:
660
+ """返回默认给 agent 消费的紧凑状态摘要。"""
661
+ current = _current_queue_item(state)
662
+ completed_tasks = _completed_task_summaries(state)
663
+ summary = {
510
664
  "run_id": state.get("run_id"),
511
665
  "run_status": state.get("status"),
512
666
  "profile": state.get("profile"),
513
667
  "current_index": state.get("current_index"),
514
- "outstanding_action": outstanding_action,
515
- "auto_completed": [i.get("task") for i in queue if isinstance(i, dict) and i.get("status") == "completed"],
516
- "completed": [i.get("task") for i in queue if isinstance(i, dict) and i.get("status") == "completed"],
517
- "unarchived_tasks": [i.get("task") for i in queue if isinstance(i, dict) and i.get("status") == "completed"],
518
- "task_lifecycle_note": "completed 仅表示 auto-loop item 已本地提交;任务归档仍需显式 finish-work/archive",
519
- "blocked": [
520
- {"task": i.get("task"), "blocked": i.get("blocked")}
521
- for i in queue
522
- if isinstance(i, dict) and i.get("status") == "blocked"
523
- ],
524
- "pending": [i.get("task") for i in queue if isinstance(i, dict) and i.get("status") in {"pending", "running"}],
525
- "recent_decisions": _decision_tail(state),
526
- "resume_capsule": state.get("resume_capsule"),
668
+ "current_task": current.get("task") if current else None,
669
+ "next_step": current.get("current_step") if current else "done",
670
+ "outstanding_action": _outstanding_action(state),
671
+ "queue_counts": _queue_counts(state),
672
+ "completed_tasks": completed_tasks,
673
+ "blocked_tasks": _blocked_task_summaries(state),
674
+ "pending_tasks": _pending_task_summaries(state),
675
+ "recent_decisions": _decision_tail(state, 3, include_data=False),
527
676
  }
677
+ if completed_tasks:
678
+ summary["task_lifecycle_note"] = "completed 仅表示 auto-loop item 已本地提交;任务归档仍需显式 finish-work/archive"
679
+ return summary
680
+
681
+
682
+ def _summary(state: dict[str, Any]) -> dict[str, Any]:
683
+ """返回 verbose 诊断状态摘要。"""
684
+ summary = _compact_summary(state)
685
+ summary.update({
686
+ "blocked_tasks": _blocked_task_summaries(state, include_detail=True),
687
+ "pending_tasks": _pending_task_summaries(state, include_status=True),
688
+ "recent_decisions": _decision_tail(state, DECISION_LOG_LIMIT, include_data=True),
689
+ "resume_capsule": _resume_capsule(state),
690
+ })
691
+ return summary
692
+
693
+
694
+ def _format_summary(state: dict[str, Any], args: argparse.Namespace | None = None) -> dict[str, Any]:
695
+ """根据 --verbose 返回紧凑或详细摘要。"""
696
+ if args is not None and getattr(args, "verbose", False):
697
+ return _summary(state)
698
+ return _compact_summary(state)
528
699
 
529
700
 
530
701
  def _make_item(repo_root: Path, task_ref: str) -> dict[str, Any]:
@@ -662,12 +833,16 @@ def _next_item(repo_root: Path, state: dict[str, Any]) -> tuple[dict[str, Any] |
662
833
 
663
834
  _block_item(item, "unknown-step", f"未知 current_step:{step}")
664
835
 
665
- state["status"] = "completed"
836
+ state["status"] = _terminal_status(queue)
666
837
  return None, {
667
- "status": "done",
838
+ "status": "blocked" if state["status"] == "blocked" else "done",
668
839
  "finish_work_required_for_archive": True,
669
- "instruction": "auto-loop 队列已结束;如需归档任务,请用户显式运行 trellis-finish-work。",
670
- "summary": _summary(state),
840
+ "instruction": (
841
+ "auto-loop 队列存在 blocked 项;补齐条件后运行 retry-blocked 继续同一个 run。"
842
+ if state["status"] == "blocked"
843
+ else "auto-loop 队列已结束;如需归档任务,请用户显式运行 trellis-finish-work。"
844
+ ),
845
+ "summary": _compact_summary(state),
671
846
  }
672
847
 
673
848
 
@@ -686,6 +861,15 @@ def cmd_start(args: argparse.Namespace) -> int:
686
861
  "run_id": current.get("run_id"),
687
862
  "path": _rel_path(repo_root, current_path),
688
863
  })
864
+ if current.get("status") == "blocked" and not args.force:
865
+ return _print({
866
+ "status": "error",
867
+ "reason": "auto-run-blocked-retry-available",
868
+ "run_id": current.get("run_id"),
869
+ "path": _rel_path(repo_root, current_path),
870
+ "suggested_command": f"python3 ./.trellis/scripts/auto_loop.py retry-blocked --run-id {current.get('run_id')}",
871
+ "summary": _format_summary(current, args),
872
+ })
689
873
  except ValueError:
690
874
  pass
691
875
 
@@ -709,13 +893,12 @@ def cmd_start(args: argparse.Namespace) -> int:
709
893
  "current_index": 0,
710
894
  "route_authorization": route_authorization,
711
895
  "queue": queue,
712
- "resume_capsule": {},
713
896
  }
714
897
  path = _run_path(repo_root, run_id)
715
898
  _write_state(path, state)
716
899
  _write_pointer(repo_root, run_id)
717
900
  _link_session_run(repo_root, run_id)
718
- return _print({"status": "started", "path": _rel_path(repo_root, path), **_summary(state)})
901
+ return _print({"status": "started", "path": _rel_path(repo_root, path), **_format_summary(state, args)})
719
902
 
720
903
 
721
904
  def cmd_resume(args: argparse.Namespace) -> int:
@@ -728,7 +911,10 @@ def cmd_resume(args: argparse.Namespace) -> int:
728
911
  except ValueError as exc:
729
912
  return _print({"status": "error", "reason": "resume-failed", "message": str(exc)})
730
913
  _link_session_run(repo_root, str(state.get("run_id")))
731
- return _print({"status": "resumed", "path": _rel_path(repo_root, path), **_summary(state)})
914
+ output = {"status": "resumed", "path": _rel_path(repo_root, path), **_format_summary(state, args)}
915
+ if getattr(args, "verbose", False):
916
+ output["resume_capsule"] = _resume_capsule(state)
917
+ return _print(output)
732
918
 
733
919
 
734
920
  def cmd_next(args: argparse.Namespace) -> int:
@@ -743,16 +929,106 @@ def cmd_next(args: argparse.Namespace) -> int:
743
929
  _link_session_run(repo_root, str(state.get("run_id")))
744
930
  if state.get("status") != "running":
745
931
  output_status = "done" if state.get("status") == "completed" else state.get("status") or "unknown"
746
- return _print({"run_id": state.get("run_id"), "status": output_status, "summary": _summary(state)})
932
+ return _print({"run_id": state.get("run_id"), "status": output_status, "summary": _format_summary(state, args)})
747
933
  _, action = _next_item(repo_root, state)
748
934
  _write_state(path, state)
749
935
  if state.get("status") == "completed":
750
936
  _clear_pointer_if_current(repo_root, str(state.get("run_id") or ""))
751
- if action.get("status") == "done":
752
- action["summary"] = _summary(state)
937
+ if action.get("status") in {"done", "blocked"}:
938
+ action["summary"] = _format_summary(state, args)
753
939
  return _print({"run_id": state.get("run_id"), **action})
754
940
 
755
941
 
942
+ def _apply_route_authorization_args(state: dict[str, Any], args: argparse.Namespace) -> dict[str, str]:
943
+ """把命令行 route 参数合并进 run 的临时授权。"""
944
+ route_authorization = state.get("route_authorization")
945
+ if not isinstance(route_authorization, dict):
946
+ route_authorization = {}
947
+ if getattr(args, "route_implement", None):
948
+ route_authorization["implement"] = args.route_implement
949
+ if getattr(args, "route_check", None):
950
+ route_authorization["check"] = args.route_check
951
+ state["route_authorization"] = route_authorization
952
+ return {str(key): str(value) for key, value in route_authorization.items()}
953
+
954
+
955
+ def _blocked_reason(item: dict[str, Any]) -> str:
956
+ """读取队列项 blocked reason。"""
957
+ blocked = item.get("blocked")
958
+ if isinstance(blocked, dict):
959
+ reason = blocked.get("reason")
960
+ if isinstance(reason, str):
961
+ return reason
962
+ return ""
963
+
964
+
965
+ def cmd_retry_blocked(args: argparse.Namespace) -> int:
966
+ """把可恢复的 blocked 队列项重置为 pending,复用同一个 auto run。"""
967
+ repo_root = _repo_root()
968
+ if repo_root is None:
969
+ return _print({"status": "error", "reason": "not-trellis-project"})
970
+ try:
971
+ path, state = _load_blocked_state(repo_root, args.run_id)
972
+ except ValueError as exc:
973
+ return _print({
974
+ "status": "error",
975
+ "reason": "retry-blocked-failed",
976
+ "message": str(exc),
977
+ "runs": _recent_run_summaries(repo_root),
978
+ })
979
+
980
+ task = _normalize_task_ref(repo_root, args.task) if args.task else None
981
+ route_authorization = _apply_route_authorization_args(state, args)
982
+ queue = state.get("queue") if isinstance(state.get("queue"), list) else []
983
+ reset: list[str] = []
984
+ skipped: list[dict[str, str]] = []
985
+
986
+ for index, item in enumerate(queue):
987
+ if not isinstance(item, dict) or item.get("status") != "blocked":
988
+ continue
989
+ if task and item.get("task") != task:
990
+ continue
991
+ reason = _blocked_reason(item)
992
+ if not args.all and not task and reason not in RECOVERABLE_BLOCK_REASONS:
993
+ skipped.append({"task": str(item.get("task")), "reason": reason or "unknown"})
994
+ continue
995
+ item["status"] = "pending"
996
+ item["blocked"] = None
997
+ item["last_action"] = None
998
+ item["task_status"] = _task_status(repo_root, str(item.get("task")))
999
+ item["updated_at"] = _utc_now()
1000
+ state["current_index"] = min(int(state.get("current_index") or index), index)
1001
+ reset.append(str(item.get("task")))
1002
+ _append_item_decision(
1003
+ item,
1004
+ "retry_unblocked",
1005
+ "blocked 队列项已重置,将在同一个 auto run 内重试",
1006
+ {"previous_reason": reason, "route_authorization": route_authorization},
1007
+ )
1008
+
1009
+ if not reset:
1010
+ return _print({
1011
+ "status": "error",
1012
+ "reason": "no-retryable-blocked-items",
1013
+ "message": "没有可自动重试的 blocked 队列项;如确认要重试,可加 --all 或指定 --task。",
1014
+ "skipped": skipped,
1015
+ "summary": _format_summary(state, args),
1016
+ })
1017
+
1018
+ state["status"] = "running"
1019
+ _write_state(path, state)
1020
+ _write_pointer(repo_root, str(state.get("run_id")))
1021
+ _link_session_run(repo_root, str(state.get("run_id")))
1022
+ return _print({
1023
+ "status": "retry-ready",
1024
+ "run_id": state.get("run_id"),
1025
+ "path": _rel_path(repo_root, path),
1026
+ "reset": reset,
1027
+ "skipped": skipped,
1028
+ "summary": _format_summary(state, args),
1029
+ })
1030
+
1031
+
756
1032
  def _find_record_item(state: dict[str, Any], task_ref: str | None) -> dict[str, Any] | None:
757
1033
  """找到 record 要更新的队列项。"""
758
1034
  queue = state.get("queue") if isinstance(state.get("queue"), list) else []
@@ -880,7 +1156,7 @@ def cmd_record(args: argparse.Namespace) -> int:
880
1156
  "status": "error",
881
1157
  "reason": "auto-run-not-running",
882
1158
  "run_status": state.get("status"),
883
- "summary": _summary(state),
1159
+ "summary": _format_summary(state, args),
884
1160
  })
885
1161
 
886
1162
  task = _normalize_task_ref(repo_root, args.task) if args.task else None
@@ -926,7 +1202,18 @@ def cmd_record(args: argparse.Namespace) -> int:
926
1202
  )
927
1203
 
928
1204
  _write_state(path, state)
929
- return _print({"status": "recorded", "run_id": state.get("run_id"), "task": item.get("task"), "item": item, "summary": _summary(state)})
1205
+ output = {
1206
+ "status": "recorded",
1207
+ "run_id": state.get("run_id"),
1208
+ "task": item.get("task"),
1209
+ "item_status": item.get("status"),
1210
+ "current_step": item.get("current_step"),
1211
+ "commit": item.get("commit"),
1212
+ "summary": _format_summary(state, args),
1213
+ }
1214
+ if getattr(args, "verbose", False):
1215
+ output["item"] = item
1216
+ return _print(output)
930
1217
 
931
1218
 
932
1219
  def cmd_status(args: argparse.Namespace) -> int:
@@ -937,8 +1224,14 @@ def cmd_status(args: argparse.Namespace) -> int:
937
1224
  try:
938
1225
  path, state = _load_current_state(repo_root, args.run_id)
939
1226
  except ValueError as exc:
940
- return _print({"status": "error", "reason": "status-failed", "message": str(exc)})
941
- return _print({"status": "ok", "path": _rel_path(repo_root, path), **_summary(state)})
1227
+ return _print({
1228
+ "status": "ok",
1229
+ "run_status": "no-current-run",
1230
+ "reason": "status-list",
1231
+ "message": str(exc),
1232
+ "runs": _recent_run_summaries(repo_root),
1233
+ })
1234
+ return _print({"status": "ok", "path": _rel_path(repo_root, path), **_format_summary(state, args)})
942
1235
 
943
1236
 
944
1237
  def cmd_stop(args: argparse.Namespace) -> int:
@@ -969,11 +1262,13 @@ def build_parser() -> argparse.ArgumentParser:
969
1262
  start.add_argument("--route-implement", choices=sorted(VALID_IMPLEMENT_ROUTES))
970
1263
  start.add_argument("--route-check", choices=sorted(VALID_CHECK_ROUTES))
971
1264
  start.add_argument("--force", action="store_true")
1265
+ start.add_argument("--verbose", action="store_true")
972
1266
  start.set_defaults(func=cmd_start)
973
1267
 
974
1268
  for name, func in (("resume", cmd_resume), ("next", cmd_next), ("status", cmd_status)):
975
1269
  sub = subparsers.add_parser(name)
976
1270
  sub.add_argument("--run-id")
1271
+ sub.add_argument("--verbose", action="store_true")
977
1272
  sub.set_defaults(func=func)
978
1273
 
979
1274
  record = subparsers.add_parser("record")
@@ -990,8 +1285,18 @@ def build_parser() -> argparse.ArgumentParser:
990
1285
  record.add_argument("--snapshot-commit")
991
1286
  record.add_argument("--route-mode")
992
1287
  record.add_argument("--route-source")
1288
+ record.add_argument("--verbose", action="store_true")
993
1289
  record.set_defaults(func=cmd_record)
994
1290
 
1291
+ retry = subparsers.add_parser("retry-blocked")
1292
+ retry.add_argument("--run-id")
1293
+ retry.add_argument("--task")
1294
+ retry.add_argument("--route-implement", choices=sorted(VALID_IMPLEMENT_ROUTES))
1295
+ retry.add_argument("--route-check", choices=sorted(VALID_CHECK_ROUTES))
1296
+ retry.add_argument("--all", action="store_true")
1297
+ retry.add_argument("--verbose", action="store_true")
1298
+ retry.set_defaults(func=cmd_retry_blocked)
1299
+
995
1300
  stop = subparsers.add_parser("stop")
996
1301
  stop.add_argument("--run-id")
997
1302
  stop.add_argument("--reason")
@@ -18,8 +18,40 @@ from typing import Any
18
18
 
19
19
 
20
20
  MAX_BODY_CHARS = 8000
21
- DEFAULT_LIMIT = 5
21
+ DEFAULT_LIMIT = 3
22
22
  MIN_SCORE = 3
23
+ MIN_BODY_ONLY_HITS = 5
24
+ MIN_HEADING_BODY_HITS = 3
25
+ BODY_WEAK_TOKENS = {
26
+ "action",
27
+ "actions",
28
+ "after",
29
+ "before",
30
+ "command",
31
+ "commands",
32
+ "context",
33
+ "current",
34
+ "file",
35
+ "files",
36
+ "match",
37
+ "matched",
38
+ "matches",
39
+ "matching",
40
+ "normal",
41
+ "path",
42
+ "paths",
43
+ "project",
44
+ "read",
45
+ "reason",
46
+ "reasons",
47
+ "relevant",
48
+ "sop",
49
+ "spec",
50
+ "status",
51
+ "task",
52
+ "tasks",
53
+ "workflow",
54
+ }
23
55
  TOKEN_RE = re.compile(r"[A-Za-z0-9_.@/-]+|[\u4e00-\u9fff]{2,}")
24
56
  HEADER_RE = re.compile(r"^\s{0,3}#{1,3}\s+(.+?)\s*$", re.MULTILINE)
25
57
  FRONTMATTER_BOUNDARY_RE = re.compile(r"^---\s*$")
@@ -225,10 +257,11 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
225
257
 
226
258
  metadata, body = parse_frontmatter(text)
227
259
  rel_path = path.relative_to(root).as_posix()
228
- rel_lower = rel_path.lower()
260
+ spec_rel_path = path.relative_to(root / ".trellis" / "spec").as_posix()
261
+ spec_rel_lower = spec_rel_path.lower()
229
262
  body_sample = body[:MAX_BODY_CHARS]
230
263
  body_lower = body_sample.lower()
231
- headers = HEADER_RE.findall(body_sample)
264
+ headers = HEADER_RE.findall(body)
232
265
  header_text = " ".join(headers).lower()
233
266
 
234
267
  kind = str(metadata.get("kind") or "").strip()
@@ -249,7 +282,7 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
249
282
  score += 8 * len(matched_triggers)
250
283
  reasons.append(f"matched triggers: {', '.join(matched_triggers[:5])}")
251
284
 
252
- path_hits = [token for token in query_tokens if token in rel_lower]
285
+ path_hits = [token for token in query_tokens if token in spec_rel_lower]
253
286
  if path_hits:
254
287
  score += 4 * len(path_hits)
255
288
  reasons.append(f"matched path tokens: {', '.join(path_hits[:5])}")
@@ -259,11 +292,23 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
259
292
  score += 3 * len(header_hits)
260
293
  reasons.append(f"matched headings: {', '.join(header_hits[:5])}")
261
294
 
262
- body_hits = [token for token in query_tokens if token in body_lower]
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]
263
297
  if body_hits:
264
298
  score += len(body_hits)
265
299
  reasons.append(f"matched body tokens: {', '.join(body_hits[:5])}")
266
300
 
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
308
+ )
309
+ if not strong_match:
310
+ return None
311
+
267
312
  if kind.lower() in {"sop", "procedure", "guide", "thinking-guide"} and score > 0:
268
313
  score += 2
269
314
  if load.lower() in {"before_action", "before-acting", "before_acting"} and score > 0:
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-06-29T10:34:58.491Z",
2
+ "syncedAt": "2026-06-30T00:30:48.425Z",
3
3
  "syncedFrom": "vendor/skill-garden/.trellis",
4
- "sourceCommit": "43e16e445596067cb5d7278be9c9f3c8e97d085b",
4
+ "sourceCommit": "44b5b31a0c0346f4d615ad6f3f58af860cc3983d",
5
5
  "variants": {
6
6
  "old": {
7
7
  "claudeSkills": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.0-beta.1",
3
+ "version": "0.4.0-beta.2",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {