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.
- package/enhancements/0.6/.agents/skills/trellis-auto-loop/SKILL.md +47 -4
- package/enhancements/0.6/.claude/skills/trellis-auto-loop/SKILL.md +47 -4
- package/enhancements/0.6/overrides/workflow-states/in_progress-inline.md +1 -1
- package/enhancements/0.6/overrides/workflow-states/in_progress.md +1 -1
- package/enhancements/0.6/overrides/workflow-states/no_task.md +1 -1
- 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 +3 -1
- package/enhancements/0.6/scripts/auto_loop.py +365 -60
- package/enhancements/0.6/scripts/spec_router.py +50 -5
- package/enhancements/MANIFEST.json +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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[-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
463
|
-
current =
|
|
464
|
-
auto_completed = []
|
|
465
|
-
|
|
466
|
-
|
|
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":
|
|
483
|
-
"blocked":
|
|
484
|
-
"remaining":
|
|
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
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
515
|
-
"
|
|
516
|
-
"
|
|
517
|
-
"
|
|
518
|
-
"
|
|
519
|
-
"
|
|
520
|
-
|
|
521
|
-
|
|
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"] =
|
|
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":
|
|
670
|
-
|
|
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), **
|
|
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
|
-
|
|
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":
|
|
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")
|
|
752
|
-
action["summary"] =
|
|
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":
|
|
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
|
-
|
|
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({
|
|
941
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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-
|
|
2
|
+
"syncedAt": "2026-06-30T00:30:48.425Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden/.trellis",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "44b5b31a0c0346f4d615ad6f3f58af860cc3983d",
|
|
5
5
|
"variants": {
|
|
6
6
|
"old": {
|
|
7
7
|
"claudeSkills": [],
|