flower-trellis 0.5.0-beta.6 → 0.5.1-beta.0
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 +19 -1
- package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +10 -0
- package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +4 -2
- package/enhancements/0.6/.agents/skills/trellis-route/scripts/route_state.py +112 -26
- package/enhancements/0.6/.claude/skills/trellis-auto-loop/SKILL.md +19 -1
- package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +10 -0
- package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +4 -2
- package/enhancements/0.6/.claude/skills/trellis-route/scripts/route_state.py +112 -26
- package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +10 -0
- package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -0
- package/enhancements/0.6/overrides/bundles/task-progress-recovery.json +7 -0
- package/enhancements/0.6/overrides/conflicts.json +353 -4
- package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/claude-content.py +17 -0
- package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/claude-selector.py +3 -0
- package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/codex-content.py +17 -0
- package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/codex-selector.py +3 -0
- package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/patch.json +35 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-fallback-content.py +20 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-fallback-selector.py +23 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-resolution-content.py +22 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-resolution-selector.py +27 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-runtime-io-content.py +24 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-runtime-io-selector.py +18 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-set-content.py +26 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-set-selector.py +27 -0
- package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/patch.json +55 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/degraded-content.py +19 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/degraded-selector.py +25 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/helper-content.py +39 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/patch.json +14 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/session-content.py +23 -0
- package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/session-selector.py +20 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/add-subtask-content.py +23 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/add-subtask-selector.py +5 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-guard-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-guard-selector.py +3 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-write-content.py +5 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-write-selector.py +3 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-active-content.py +17 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-active-selector.py +10 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-duplicate-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-duplicate-selector.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-parent-content.py +38 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-parent-selector.py +21 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-write-content.py +8 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-write-selector.py +3 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/helpers-content.py +36 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/helpers-selector.py +6 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/patch.json +93 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/remove-subtask-content.py +23 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/remove-subtask-selector.py +5 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-base-branch-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-base-branch-selector.py +2 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-branch-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-branch-selector.py +2 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-scope-content.py +4 -0
- package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-scope-selector.py +2 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/content.md +5 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/patch.json +31 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/selector.md +1 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-authorization/content.md +2 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/content.md +21 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/patch.json +32 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/selector.md +1 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/gemini-content.md +3 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/gemini-selector.txt +1 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/patch.json +34 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-start/no-task-routing/content.md +1 -1
- package/enhancements/0.6/overrides/patches/skills/trellis-update-spec/autonomous-evaluation/patch.json +22 -0
- package/enhancements/0.6/overrides/patches/workflow/hub/content.md +30 -162
- package/enhancements/0.6/overrides/patches/workflow/intent-routing/request-triage/content.md +11 -5
- package/enhancements/0.6/overrides/patches/workflow/phase-ownership/phase-2-check-content.md +2 -0
- package/enhancements/0.6/overrides/patches/workflow/phase-ownership/phase-2-implement-content.md +11 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-clear-fallback-content.py +14 -6
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-read-result-content.py +15 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-result-content.py +25 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-result-selector.py +1 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/patch.json +50 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/paths-clear-current-content.py +26 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/paths-clear-current-selector.py +24 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/task-finish-content.py +20 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/task-finish-selector.py +19 -0
- package/enhancements/0.6/overrides/patches/workflow/state-no-task/content.md +2 -1
- package/enhancements/0.6/overrides/patches/workflow/states-in-progress/common-content.md +2 -1
- package/enhancements/0.6/overrides/patches/workflow/states-planning/common-content.md +1 -0
- package/enhancements/0.6/scripts/auto_loop.py +539 -48
- package/enhancements/0.6/scripts/pre_check_state.py +353 -0
- package/enhancements/0.6/scripts/task_intent.py +8 -1
- package/enhancements/0.6/scripts/task_progress.py +63 -11
- package/enhancements/MANIFEST.json +63 -2
- package/package.json +3 -3
- package/src/commands/uninstall.js +10 -8
- package/src/constants.js +25 -0
- package/src/lib/apply-enhancements.js +9 -7
- package/src/lib/copy-scripts.js +14 -0
- package/src/lib/copy-skills.js +27 -36
- package/src/lib/enhancement-catalog.js +3 -3
|
@@ -22,11 +22,19 @@ description: "启动、恢复和推进 Trellis 自动任务循环。用于用户
|
|
|
22
22
|
- auto-loop 启动前若 implement 与 check 都缺 route,优先展示 auto-loop 专用的合并选择,不要把 `trellis-route` 的两套完整 fallback 原样贴给用户。仍允许用户回复高级格式 `implement 1, check 1`。
|
|
23
23
|
- 检查深度由 run 级 `--check-depth auto|light|full` 控制,默认 `auto`,与 `--route-check` 独立;历史 run 缺少该字段时按 `full` 兼容。
|
|
24
24
|
- 代码提交必须复用 `trellis-push` 的内部 commit-only 执行能力;auto-loop 自己负责预授权校验和 runner 回写,不要裸 `git commit` / `git push`。
|
|
25
|
+
- `Open Questions` 的确定性状态由 runner 处理:`- [ ]` 阻塞,`- [x]` 不阻塞,无章节或空章节放行。历史无 checkbox 列表只能按 `review_open_questions` action 做语义复核,不能自行跳过 runner 回写。
|
|
26
|
+
- 启动或恢复 validated auto-loop 前先运行 `python3 ./.trellis/scripts/pre_check_state.py clear`,静默清除当前任务的交互式 Check-All 前暂缓偏好。清理返回 miss 或 task mismatch 视为无状态;runtime 损坏只记录诊断,不得阻断 runner,也不得让 runner 读取该偏好。
|
|
25
27
|
|
|
26
28
|
## 启动
|
|
27
29
|
|
|
28
30
|
用户给了任务列表时,按原顺序传入;用户只说当前任务时,用 `task.py current --source` 的当前任务。
|
|
29
31
|
|
|
32
|
+
启动前先静默清理交互式暂缓偏好:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3 ./.trellis/scripts/pre_check_state.py clear
|
|
36
|
+
```
|
|
37
|
+
|
|
30
38
|
启动前对当前任务执行 route 准备度检查:
|
|
31
39
|
|
|
32
40
|
```bash
|
|
@@ -81,6 +89,7 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
81
89
|
压缩、重开会话、用户说“继续自动跑 / continue auto loop”时:
|
|
82
90
|
|
|
83
91
|
```bash
|
|
92
|
+
python3 ./.trellis/scripts/pre_check_state.py clear
|
|
84
93
|
python3 ./.trellis/scripts/auto_loop.py resume
|
|
85
94
|
python3 ./.trellis/scripts/auto_loop.py next
|
|
86
95
|
```
|
|
@@ -107,7 +116,10 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
107
116
|
|
|
108
117
|
| runner action | 主 agent 动作 | 成功 record |
|
|
109
118
|
| --- | --- | --- |
|
|
110
|
-
| `
|
|
119
|
+
| `review_open_questions` | 结合 PRD 语义判断历史裸列表是已解决、仍阻塞或无法确定 | 已解决:`record --action review_open_questions --result ok --review-verdict resolved --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --review-verdict blocking|ambiguous` |
|
|
120
|
+
| `review_planning_readiness` | 按 `trellis-brainstorm` Quality Bar 复核验收标准可测试、范围明确、关键决策收敛、仓库问题已研究 | ready:`record --action review_planning_readiness --result ok --readiness-verdict ready --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --readiness-verdict blocking|ambiguous` |
|
|
121
|
+
| `refresh_brief` | 使用 `trellis-task-brief` 刷新并展示 brief;这一步不代表用户确认 | `record --action refresh_brief --result ok` |
|
|
122
|
+
| `confirm_brief` | 再次展示 action 对应的完整 brief,并等待用户显式确认 | 收到确认后:`record --action confirm_brief --result ok --summary "<确认摘要>"`;未确认时停止,不得 record |
|
|
111
123
|
| `start_task` | 执行返回的 `task.py start ...` 命令 | `record --action start_task --result ok` |
|
|
112
124
|
| `run_implement` | 进入 Phase 2.1,先用 `trellis-route(target=implement)` 决定 inline/subagent,再实现 | `record --action run_implement --result ok --route-mode <mode> --route-source <source>` |
|
|
113
125
|
| `run_check_all` | 进入 Phase 2.2,先用 `trellis-route(target=check)`,按 action 的 requested depth 执行 Check-All | `record --action run_check_all --result ok --route-mode <mode> --route-source <source> --effective-check-depth <light|full> --check-depth-reason "<摘要>"` |
|
|
@@ -143,6 +155,12 @@ python3 ./.trellis/scripts/auto_loop.py record \
|
|
|
143
155
|
|
|
144
156
|
runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
|
|
145
157
|
|
|
158
|
+
`review_open_questions` 必须只依据 action 返回的 `questions` 和当前 PRD 判断。runner 会把结果绑定到 action 发出时的 PRD SHA-256;PRD 在复核期间发生变化时,record 会返回 `stale-open-questions-review`,此时重新运行 `next`,不得复用旧判断。`ambiguous` 必须保守阻塞,交由用户或 planning 收敛。
|
|
159
|
+
|
|
160
|
+
`review_planning_readiness` 必须读取 action 指向的当前 planning artifacts,并按 Brainstorm Gate 的语义质量线复核,不能把文件存在、章节存在或 route context 已 curated 当作 ready。runner 会把结论绑定到 `prd.md`、存在时的 `design.md`/`implement.md` 内容;任一文件变化后旧结论失效。
|
|
161
|
+
|
|
162
|
+
`refresh_brief` 成功后必须立即 `next`。runner 随后返回 `confirm_brief`,主 agent 要展示当前完整 brief 并停止等待用户;启动 auto-loop、选择 route 或生成 brief 都不构成确认。`confirm_brief` 只在收到明确确认后回写,且确认绑定 planning artifacts 与 brief 内容;任一文件变化后必须重新展示并确认。
|
|
163
|
+
|
|
146
164
|
`record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
|
|
147
165
|
|
|
148
166
|
route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
|
|
@@ -28,6 +28,16 @@ description: "按确认的精确文件范围提交普通变更或完成已就绪
|
|
|
28
28
|
|
|
29
29
|
内部 `commit-only` 不接受临时扩大文件范围、远端推送或其他附加动作。安全条件不满足时返回失败,由调用方决定后续状态。
|
|
30
30
|
|
|
31
|
+
## Step 0:交互式完成链门禁
|
|
32
|
+
|
|
33
|
+
除 auto-loop 内部 `commit-only` 外,任何普通 push 或用户 `commit-only` 在读取 Git 提交计划前,都必须确认当前工作内容已经通过 Check-All,且该结论之后存在当前有效的 `spec_update_result`:
|
|
34
|
+
|
|
35
|
+
- `status=no-op|written`:继续本 skill 的 Git 预检与计划。
|
|
36
|
+
- 缺失,或实际 diff、Check-All 结论、用户 spec 意图已变化:先加载 `trellis-update-spec`,只有新的 `no-op|written` 才能回到本 skill。
|
|
37
|
+
- `status=needs-review`:停止,不生成提交计划。
|
|
38
|
+
|
|
39
|
+
用户直接说 push/提交不构成跳过 Phase 3.3 的授权。auto-loop 内部 `commit-only` 已由 runner 的 `run_spec_update -> commit_only` 状态机和预授权保证顺序,因此不得重复进入本门禁。
|
|
40
|
+
|
|
31
41
|
## Step 1:发现仓库与任务
|
|
32
42
|
|
|
33
43
|
候选仓库包括:
|
|
@@ -177,11 +177,13 @@ helper 写入规则:保留另一个 target 的 runtime 决策和偏好;覆
|
|
|
177
177
|
|
|
178
178
|
| 路由决定 | 主 agent 应执行 |
|
|
179
179
|
|---------|----------------|
|
|
180
|
-
| `inline implement` | `Skill({skill: "trellis-before-dev"})` 加载 spec → 读任务文档 → 主线程实施 → 跑必要验证 |
|
|
181
|
-
| `subagent implement` | `Agent({subagent_type: "trellis-implement"})`;若 `subagent_skip_compile=true`,dispatch prompt 附加“跳过 mvn install / npm run build / tsc 等耗时编译类检查(已由主 agent
|
|
180
|
+
| `inline implement` | `Skill({skill: "trellis-before-dev"})` 加载 spec → 读任务文档 → 主线程实施 → 跑必要验证 → 回到 Phase 2.1 completion contract 解析 Pre-Check,不得在局部验证后直接结束 |
|
|
181
|
+
| `subagent implement` | `Agent({subagent_type: "trellis-implement"})`;若 `subagent_skip_compile=true`,dispatch prompt 附加“跳过 mvn install / npm run build / tsc 等耗时编译类检查(已由主 agent 验证或最终统一执行)”;主 agent 收到结果后回到 Phase 2.1 completion contract 解析 Pre-Check |
|
|
182
182
|
| `inline check-all` | `Skill({skill: "trellis-check-all"})` |
|
|
183
183
|
| `subagent check-all` | 优先使用明确 audit-only 的 `trellis-check-all` agent;不存在时使用平台通用 subagent,并用下方 dispatch 契约执行本地 `trellis-check-all`。禁止 fallback 到会直接修改工作区的 `trellis-check` agent;无兼容 subagent 时停止并请用户改选 inline |
|
|
184
184
|
|
|
185
|
+
implement 路由只决定执行位置,不拥有实现后的停止策略。无论 inline 或 subagent,focused validation 完成后都必须返回 workflow Phase 2.1 的 completion contract;由该 owner 处理 auto-loop、用户显式继续/暂缓、已有 hold 和默认立即 Check-All 的优先级。
|
|
186
|
+
|
|
185
187
|
### Subagent Check-All Dispatch 契约
|
|
186
188
|
|
|
187
189
|
使用专用或通用 subagent 时,dispatch prompt 第一行必须是当前任务路径,并包含以下完整边界:
|
|
@@ -5,8 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
|
+
import os
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
11
|
+
import tempfile
|
|
10
12
|
from datetime import datetime, timezone
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
@@ -50,22 +52,45 @@ def _utc_now() -> str:
|
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
def
|
|
54
|
-
"""读取 JSON
|
|
55
|
+
def _read_json_result(path: Path) -> dict[str, Any]:
|
|
56
|
+
"""读取 runtime JSON,并区分缺失、损坏和 I/O 错误。"""
|
|
55
57
|
try:
|
|
56
58
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
-
except
|
|
58
|
-
return {}
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
return {"status": "missing", "data": None, "error": None}
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
return {"status": "corrupt", "data": None, "error": str(exc)}
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
return {"status": "io_error", "data": None, "error": str(exc)}
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
return {"status": "corrupt", "data": None, "error": "JSON 根节点不是对象"}
|
|
67
|
+
return {"status": "ok", "data": data, "error": None}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
71
|
+
"""兼容非关键扫描;仅返回成功解析的 JSON 对象。"""
|
|
72
|
+
result = _read_json_result(path)
|
|
73
|
+
data = result.get("data")
|
|
59
74
|
return data if isinstance(data, dict) else {}
|
|
60
75
|
|
|
61
76
|
|
|
62
77
|
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
63
|
-
"""
|
|
78
|
+
"""使用同目录临时文件原子写入 runtime 状态。"""
|
|
64
79
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
-
path.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
81
|
+
temp_path = Path(temp_name)
|
|
82
|
+
try:
|
|
83
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
84
|
+
handle.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
85
|
+
handle.flush()
|
|
86
|
+
os.fsync(handle.fileno())
|
|
87
|
+
os.replace(temp_path, path)
|
|
88
|
+
except Exception:
|
|
89
|
+
try:
|
|
90
|
+
temp_path.unlink()
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
pass
|
|
93
|
+
raise
|
|
69
94
|
|
|
70
95
|
|
|
71
96
|
def _print(data: dict[str, Any]) -> int:
|
|
@@ -187,7 +212,27 @@ def _auto_state_path(repo_root: Path, run_id: Any) -> Path | None:
|
|
|
187
212
|
return _auto_loop_dir(repo_root) / f"{run_id.strip()}.json"
|
|
188
213
|
|
|
189
214
|
|
|
190
|
-
def
|
|
215
|
+
def _run_contains_task(state: dict[str, Any], current_task: str) -> bool:
|
|
216
|
+
"""判断当前任务是否属于 run 的未完成队列。"""
|
|
217
|
+
queue = state.get("queue")
|
|
218
|
+
if not isinstance(queue, list):
|
|
219
|
+
return False
|
|
220
|
+
normalized = current_task.replace("\\", "/")
|
|
221
|
+
for item in queue:
|
|
222
|
+
if not isinstance(item, dict) or item.get("status") not in {"pending", "running"}:
|
|
223
|
+
continue
|
|
224
|
+
task = item.get("task")
|
|
225
|
+
if isinstance(task, str) and task.replace("\\", "/") == normalized:
|
|
226
|
+
return True
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _auto_route_mode(
|
|
231
|
+
repo_root: Path,
|
|
232
|
+
context_key: str,
|
|
233
|
+
current_task: str,
|
|
234
|
+
target: str,
|
|
235
|
+
) -> tuple[str | None, Path | None, str | None]:
|
|
191
236
|
"""读取当前 running auto-loop run 的临时 route 授权。
|
|
192
237
|
|
|
193
238
|
auto 授权低于个人 `.route-prefs.tmp`,只在当前 session runtime 绑定了
|
|
@@ -195,15 +240,26 @@ def _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[st
|
|
|
195
240
|
如果这些指针 stale,则忽略 stale pointer,并 fallback 到唯一 running run。
|
|
196
241
|
"""
|
|
197
242
|
session_path = _session_path(repo_root, context_key)
|
|
198
|
-
|
|
243
|
+
session_result = _read_json_result(session_path)
|
|
244
|
+
if session_result["status"] in {"corrupt", "io_error"}:
|
|
245
|
+
return None, session_path, f"session-runtime-{session_result['status']}"
|
|
246
|
+
session = session_result["data"] if isinstance(session_result.get("data"), dict) else {}
|
|
199
247
|
candidate_paths: list[Path] = []
|
|
200
248
|
stale_paths: list[Path] = []
|
|
201
249
|
|
|
202
|
-
|
|
250
|
+
pointer_result = _read_json_result(_auto_loop_pointer(repo_root))
|
|
251
|
+
pointer = pointer_result["data"] if isinstance(pointer_result.get("data"), dict) else {}
|
|
252
|
+
for source, run_id in (
|
|
253
|
+
("session", session.get("current_auto_run")),
|
|
254
|
+
("pointer", pointer.get("run_id")),
|
|
255
|
+
):
|
|
203
256
|
path = _auto_state_path(repo_root, run_id)
|
|
204
257
|
if path is None:
|
|
205
258
|
continue
|
|
206
|
-
|
|
259
|
+
result = _read_json_result(path)
|
|
260
|
+
if source == "session" and result["status"] in {"corrupt", "io_error"}:
|
|
261
|
+
return None, path, f"session-auto-run-{result['status']}"
|
|
262
|
+
state = result["data"] if isinstance(result.get("data"), dict) else {}
|
|
207
263
|
if state.get("status") == "running":
|
|
208
264
|
candidate_paths.append(path)
|
|
209
265
|
else:
|
|
@@ -227,6 +283,8 @@ def _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[st
|
|
|
227
283
|
|
|
228
284
|
path = unique_paths[0]
|
|
229
285
|
state = _read_json(path)
|
|
286
|
+
if not _run_contains_task(state, current_task):
|
|
287
|
+
return None, path, "auto-run-task-mismatch"
|
|
230
288
|
|
|
231
289
|
auth = state.get("route_authorization")
|
|
232
290
|
if not isinstance(auth, dict):
|
|
@@ -302,7 +360,10 @@ def _write_runtime_decision(
|
|
|
302
360
|
) -> tuple[Path, dict[str, str]]:
|
|
303
361
|
"""写入 session runtime 文件中的 route_decisions 字段。"""
|
|
304
362
|
path = _session_path(repo_root, context_key)
|
|
305
|
-
|
|
363
|
+
context_result = _read_json_result(path)
|
|
364
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
365
|
+
raise ValueError(f"session-runtime-{context_result['status']}")
|
|
366
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
306
367
|
now = _utc_now()
|
|
307
368
|
context.setdefault("platform", context_key.split("_", 1)[0] if "_" in context_key else "session")
|
|
308
369
|
context["last_seen_at"] = now
|
|
@@ -349,7 +410,14 @@ def read_runtime(args: argparse.Namespace) -> int:
|
|
|
349
410
|
assert current_task is not None and context_key is not None
|
|
350
411
|
|
|
351
412
|
path = _session_path(repo_root, context_key)
|
|
352
|
-
|
|
413
|
+
context_result = _read_json_result(path)
|
|
414
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
415
|
+
return _output(
|
|
416
|
+
args,
|
|
417
|
+
{"status": "miss", "reason": f"session-runtime-{context_result['status']}"},
|
|
418
|
+
{"path": _rel_path(repo_root, path), "error": context_result.get("error")},
|
|
419
|
+
)
|
|
420
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
353
421
|
decision = context.get("route_decisions", {}).get(args.target)
|
|
354
422
|
normalized = _normalized_decision(decision, args.target, current_task)
|
|
355
423
|
if normalized is not None:
|
|
@@ -393,7 +461,14 @@ def resolve_route(args: argparse.Namespace) -> int:
|
|
|
393
461
|
assert current_task is not None and context_key is not None
|
|
394
462
|
|
|
395
463
|
path = _session_path(repo_root, context_key)
|
|
396
|
-
|
|
464
|
+
context_result = _read_json_result(path)
|
|
465
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
466
|
+
return _output(
|
|
467
|
+
args,
|
|
468
|
+
{"status": "miss", "reason": f"session-runtime-{context_result['status']}"},
|
|
469
|
+
{"path": _rel_path(repo_root, path), "error": context_result.get("error")},
|
|
470
|
+
)
|
|
471
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
397
472
|
decision = context.get("route_decisions", {}).get(args.target)
|
|
398
473
|
normalized = _normalized_decision(decision, args.target, current_task)
|
|
399
474
|
if normalized is not None:
|
|
@@ -451,7 +526,12 @@ def resolve_route(args: argparse.Namespace) -> int:
|
|
|
451
526
|
}
|
|
452
527
|
)
|
|
453
528
|
|
|
454
|
-
auto_mode, auto_path, auto_reason = _auto_route_mode(
|
|
529
|
+
auto_mode, auto_path, auto_reason = _auto_route_mode(
|
|
530
|
+
repo_root,
|
|
531
|
+
context_key,
|
|
532
|
+
current_task,
|
|
533
|
+
args.target,
|
|
534
|
+
)
|
|
455
535
|
if auto_mode in PREF_MODES[args.target]:
|
|
456
536
|
written_path, auto_decision = _write_runtime_decision(
|
|
457
537
|
repo_root,
|
|
@@ -535,14 +615,17 @@ def write_route(args: argparse.Namespace) -> int:
|
|
|
535
615
|
if not context_key:
|
|
536
616
|
return _print({"status": "skipped", "reason": "no-session-context", "source": source})
|
|
537
617
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
618
|
+
try:
|
|
619
|
+
path, decision = _write_runtime_decision(
|
|
620
|
+
repo_root,
|
|
621
|
+
context_key,
|
|
622
|
+
current_task,
|
|
623
|
+
args.target,
|
|
624
|
+
mode,
|
|
625
|
+
args.source,
|
|
626
|
+
)
|
|
627
|
+
except (OSError, ValueError) as exc:
|
|
628
|
+
return _print({"status": "error", "reason": "runtime-write-failed", "message": str(exc)})
|
|
546
629
|
return _output(
|
|
547
630
|
args,
|
|
548
631
|
{
|
|
@@ -617,7 +700,10 @@ def main() -> int:
|
|
|
617
700
|
"""脚本入口。"""
|
|
618
701
|
parser = build_parser()
|
|
619
702
|
args = parser.parse_args()
|
|
620
|
-
|
|
703
|
+
try:
|
|
704
|
+
return args.func(args)
|
|
705
|
+
except (OSError, ValueError) as exc:
|
|
706
|
+
return _print({"status": "error", "reason": "runtime-io-error", "message": str(exc)})
|
|
621
707
|
|
|
622
708
|
|
|
623
709
|
if __name__ == "__main__":
|
|
@@ -22,11 +22,19 @@ description: "启动、恢复和推进 Trellis 自动任务循环。用于用户
|
|
|
22
22
|
- auto-loop 启动前若 implement 与 check 都缺 route,优先展示 auto-loop 专用的合并选择,不要把 `trellis-route` 的两套完整 fallback 原样贴给用户。仍允许用户回复高级格式 `implement 1, check 1`。
|
|
23
23
|
- 检查深度由 run 级 `--check-depth auto|light|full` 控制,默认 `auto`,与 `--route-check` 独立;历史 run 缺少该字段时按 `full` 兼容。
|
|
24
24
|
- 代码提交必须复用 `trellis-push` 的内部 commit-only 执行能力;auto-loop 自己负责预授权校验和 runner 回写,不要裸 `git commit` / `git push`。
|
|
25
|
+
- `Open Questions` 的确定性状态由 runner 处理:`- [ ]` 阻塞,`- [x]` 不阻塞,无章节或空章节放行。历史无 checkbox 列表只能按 `review_open_questions` action 做语义复核,不能自行跳过 runner 回写。
|
|
26
|
+
- 启动或恢复 validated auto-loop 前先运行 `python3 ./.trellis/scripts/pre_check_state.py clear`,静默清除当前任务的交互式 Check-All 前暂缓偏好。清理返回 miss 或 task mismatch 视为无状态;runtime 损坏只记录诊断,不得阻断 runner,也不得让 runner 读取该偏好。
|
|
25
27
|
|
|
26
28
|
## 启动
|
|
27
29
|
|
|
28
30
|
用户给了任务列表时,按原顺序传入;用户只说当前任务时,用 `task.py current --source` 的当前任务。
|
|
29
31
|
|
|
32
|
+
启动前先静默清理交互式暂缓偏好:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3 ./.trellis/scripts/pre_check_state.py clear
|
|
36
|
+
```
|
|
37
|
+
|
|
30
38
|
启动前对当前任务执行 route 准备度检查:
|
|
31
39
|
|
|
32
40
|
```bash
|
|
@@ -81,6 +89,7 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
81
89
|
压缩、重开会话、用户说“继续自动跑 / continue auto loop”时:
|
|
82
90
|
|
|
83
91
|
```bash
|
|
92
|
+
python3 ./.trellis/scripts/pre_check_state.py clear
|
|
84
93
|
python3 ./.trellis/scripts/auto_loop.py resume
|
|
85
94
|
python3 ./.trellis/scripts/auto_loop.py next
|
|
86
95
|
```
|
|
@@ -107,7 +116,10 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
107
116
|
|
|
108
117
|
| runner action | 主 agent 动作 | 成功 record |
|
|
109
118
|
| --- | --- | --- |
|
|
110
|
-
| `
|
|
119
|
+
| `review_open_questions` | 结合 PRD 语义判断历史裸列表是已解决、仍阻塞或无法确定 | 已解决:`record --action review_open_questions --result ok --review-verdict resolved --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --review-verdict blocking|ambiguous` |
|
|
120
|
+
| `review_planning_readiness` | 按 `trellis-brainstorm` Quality Bar 复核验收标准可测试、范围明确、关键决策收敛、仓库问题已研究 | ready:`record --action review_planning_readiness --result ok --readiness-verdict ready --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --readiness-verdict blocking|ambiguous` |
|
|
121
|
+
| `refresh_brief` | 使用 `trellis-task-brief` 刷新并展示 brief;这一步不代表用户确认 | `record --action refresh_brief --result ok` |
|
|
122
|
+
| `confirm_brief` | 再次展示 action 对应的完整 brief,并等待用户显式确认 | 收到确认后:`record --action confirm_brief --result ok --summary "<确认摘要>"`;未确认时停止,不得 record |
|
|
111
123
|
| `start_task` | 执行返回的 `task.py start ...` 命令 | `record --action start_task --result ok` |
|
|
112
124
|
| `run_implement` | 进入 Phase 2.1,先用 `trellis-route(target=implement)` 决定 inline/subagent,再实现 | `record --action run_implement --result ok --route-mode <mode> --route-source <source>` |
|
|
113
125
|
| `run_check_all` | 进入 Phase 2.2,先用 `trellis-route(target=check)`,按 action 的 requested depth 执行 Check-All | `record --action run_check_all --result ok --route-mode <mode> --route-source <source> --effective-check-depth <light|full> --check-depth-reason "<摘要>"` |
|
|
@@ -143,6 +155,12 @@ python3 ./.trellis/scripts/auto_loop.py record \
|
|
|
143
155
|
|
|
144
156
|
runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
|
|
145
157
|
|
|
158
|
+
`review_open_questions` 必须只依据 action 返回的 `questions` 和当前 PRD 判断。runner 会把结果绑定到 action 发出时的 PRD SHA-256;PRD 在复核期间发生变化时,record 会返回 `stale-open-questions-review`,此时重新运行 `next`,不得复用旧判断。`ambiguous` 必须保守阻塞,交由用户或 planning 收敛。
|
|
159
|
+
|
|
160
|
+
`review_planning_readiness` 必须读取 action 指向的当前 planning artifacts,并按 Brainstorm Gate 的语义质量线复核,不能把文件存在、章节存在或 route context 已 curated 当作 ready。runner 会把结论绑定到 `prd.md`、存在时的 `design.md`/`implement.md` 内容;任一文件变化后旧结论失效。
|
|
161
|
+
|
|
162
|
+
`refresh_brief` 成功后必须立即 `next`。runner 随后返回 `confirm_brief`,主 agent 要展示当前完整 brief 并停止等待用户;启动 auto-loop、选择 route 或生成 brief 都不构成确认。`confirm_brief` 只在收到明确确认后回写,且确认绑定 planning artifacts 与 brief 内容;任一文件变化后必须重新展示并确认。
|
|
163
|
+
|
|
146
164
|
`record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
|
|
147
165
|
|
|
148
166
|
route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
|
|
@@ -28,6 +28,16 @@ description: "按确认的精确文件范围提交普通变更或完成已就绪
|
|
|
28
28
|
|
|
29
29
|
内部 `commit-only` 不接受临时扩大文件范围、远端推送或其他附加动作。安全条件不满足时返回失败,由调用方决定后续状态。
|
|
30
30
|
|
|
31
|
+
## Step 0:交互式完成链门禁
|
|
32
|
+
|
|
33
|
+
除 auto-loop 内部 `commit-only` 外,任何普通 push 或用户 `commit-only` 在读取 Git 提交计划前,都必须确认当前工作内容已经通过 Check-All,且该结论之后存在当前有效的 `spec_update_result`:
|
|
34
|
+
|
|
35
|
+
- `status=no-op|written`:继续本 skill 的 Git 预检与计划。
|
|
36
|
+
- 缺失,或实际 diff、Check-All 结论、用户 spec 意图已变化:先加载 `trellis-update-spec`,只有新的 `no-op|written` 才能回到本 skill。
|
|
37
|
+
- `status=needs-review`:停止,不生成提交计划。
|
|
38
|
+
|
|
39
|
+
用户直接说 push/提交不构成跳过 Phase 3.3 的授权。auto-loop 内部 `commit-only` 已由 runner 的 `run_spec_update -> commit_only` 状态机和预授权保证顺序,因此不得重复进入本门禁。
|
|
40
|
+
|
|
31
41
|
## Step 1:发现仓库与任务
|
|
32
42
|
|
|
33
43
|
候选仓库包括:
|
|
@@ -177,11 +177,13 @@ helper 写入规则:保留另一个 target 的 runtime 决策和偏好;覆
|
|
|
177
177
|
|
|
178
178
|
| 路由决定 | 主 agent 应执行 |
|
|
179
179
|
|---------|----------------|
|
|
180
|
-
| `inline implement` | `Skill({skill: "trellis-before-dev"})` 加载 spec → 读任务文档 → 主线程实施 → 跑必要验证 |
|
|
181
|
-
| `subagent implement` | `Agent({subagent_type: "trellis-implement"})`;若 `subagent_skip_compile=true`,dispatch prompt 附加“跳过 mvn install / npm run build / tsc 等耗时编译类检查(已由主 agent
|
|
180
|
+
| `inline implement` | `Skill({skill: "trellis-before-dev"})` 加载 spec → 读任务文档 → 主线程实施 → 跑必要验证 → 回到 Phase 2.1 completion contract 解析 Pre-Check,不得在局部验证后直接结束 |
|
|
181
|
+
| `subagent implement` | `Agent({subagent_type: "trellis-implement"})`;若 `subagent_skip_compile=true`,dispatch prompt 附加“跳过 mvn install / npm run build / tsc 等耗时编译类检查(已由主 agent 验证或最终统一执行)”;主 agent 收到结果后回到 Phase 2.1 completion contract 解析 Pre-Check |
|
|
182
182
|
| `inline check-all` | `Skill({skill: "trellis-check-all"})` |
|
|
183
183
|
| `subagent check-all` | 优先使用明确 audit-only 的 `trellis-check-all` agent;不存在时使用平台通用 subagent,并用下方 dispatch 契约执行本地 `trellis-check-all`。禁止 fallback 到会直接修改工作区的 `trellis-check` agent;无兼容 subagent 时停止并请用户改选 inline |
|
|
184
184
|
|
|
185
|
+
implement 路由只决定执行位置,不拥有实现后的停止策略。无论 inline 或 subagent,focused validation 完成后都必须返回 workflow Phase 2.1 的 completion contract;由该 owner 处理 auto-loop、用户显式继续/暂缓、已有 hold 和默认立即 Check-All 的优先级。
|
|
186
|
+
|
|
185
187
|
### Subagent Check-All Dispatch 契约
|
|
186
188
|
|
|
187
189
|
使用专用或通用 subagent 时,dispatch prompt 第一行必须是当前任务路径,并包含以下完整边界:
|
|
@@ -5,8 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
|
+
import os
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
11
|
+
import tempfile
|
|
10
12
|
from datetime import datetime, timezone
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Any
|
|
@@ -50,22 +52,45 @@ def _utc_now() -> str:
|
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
def
|
|
54
|
-
"""读取 JSON
|
|
55
|
+
def _read_json_result(path: Path) -> dict[str, Any]:
|
|
56
|
+
"""读取 runtime JSON,并区分缺失、损坏和 I/O 错误。"""
|
|
55
57
|
try:
|
|
56
58
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
-
except
|
|
58
|
-
return {}
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
return {"status": "missing", "data": None, "error": None}
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
return {"status": "corrupt", "data": None, "error": str(exc)}
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
return {"status": "io_error", "data": None, "error": str(exc)}
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
return {"status": "corrupt", "data": None, "error": "JSON 根节点不是对象"}
|
|
67
|
+
return {"status": "ok", "data": data, "error": None}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
71
|
+
"""兼容非关键扫描;仅返回成功解析的 JSON 对象。"""
|
|
72
|
+
result = _read_json_result(path)
|
|
73
|
+
data = result.get("data")
|
|
59
74
|
return data if isinstance(data, dict) else {}
|
|
60
75
|
|
|
61
76
|
|
|
62
77
|
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
63
|
-
"""
|
|
78
|
+
"""使用同目录临时文件原子写入 runtime 状态。"""
|
|
64
79
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
-
path.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
81
|
+
temp_path = Path(temp_name)
|
|
82
|
+
try:
|
|
83
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
84
|
+
handle.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
85
|
+
handle.flush()
|
|
86
|
+
os.fsync(handle.fileno())
|
|
87
|
+
os.replace(temp_path, path)
|
|
88
|
+
except Exception:
|
|
89
|
+
try:
|
|
90
|
+
temp_path.unlink()
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
pass
|
|
93
|
+
raise
|
|
69
94
|
|
|
70
95
|
|
|
71
96
|
def _print(data: dict[str, Any]) -> int:
|
|
@@ -187,7 +212,27 @@ def _auto_state_path(repo_root: Path, run_id: Any) -> Path | None:
|
|
|
187
212
|
return _auto_loop_dir(repo_root) / f"{run_id.strip()}.json"
|
|
188
213
|
|
|
189
214
|
|
|
190
|
-
def
|
|
215
|
+
def _run_contains_task(state: dict[str, Any], current_task: str) -> bool:
|
|
216
|
+
"""判断当前任务是否属于 run 的未完成队列。"""
|
|
217
|
+
queue = state.get("queue")
|
|
218
|
+
if not isinstance(queue, list):
|
|
219
|
+
return False
|
|
220
|
+
normalized = current_task.replace("\\", "/")
|
|
221
|
+
for item in queue:
|
|
222
|
+
if not isinstance(item, dict) or item.get("status") not in {"pending", "running"}:
|
|
223
|
+
continue
|
|
224
|
+
task = item.get("task")
|
|
225
|
+
if isinstance(task, str) and task.replace("\\", "/") == normalized:
|
|
226
|
+
return True
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _auto_route_mode(
|
|
231
|
+
repo_root: Path,
|
|
232
|
+
context_key: str,
|
|
233
|
+
current_task: str,
|
|
234
|
+
target: str,
|
|
235
|
+
) -> tuple[str | None, Path | None, str | None]:
|
|
191
236
|
"""读取当前 running auto-loop run 的临时 route 授权。
|
|
192
237
|
|
|
193
238
|
auto 授权低于个人 `.route-prefs.tmp`,只在当前 session runtime 绑定了
|
|
@@ -195,15 +240,26 @@ def _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[st
|
|
|
195
240
|
如果这些指针 stale,则忽略 stale pointer,并 fallback 到唯一 running run。
|
|
196
241
|
"""
|
|
197
242
|
session_path = _session_path(repo_root, context_key)
|
|
198
|
-
|
|
243
|
+
session_result = _read_json_result(session_path)
|
|
244
|
+
if session_result["status"] in {"corrupt", "io_error"}:
|
|
245
|
+
return None, session_path, f"session-runtime-{session_result['status']}"
|
|
246
|
+
session = session_result["data"] if isinstance(session_result.get("data"), dict) else {}
|
|
199
247
|
candidate_paths: list[Path] = []
|
|
200
248
|
stale_paths: list[Path] = []
|
|
201
249
|
|
|
202
|
-
|
|
250
|
+
pointer_result = _read_json_result(_auto_loop_pointer(repo_root))
|
|
251
|
+
pointer = pointer_result["data"] if isinstance(pointer_result.get("data"), dict) else {}
|
|
252
|
+
for source, run_id in (
|
|
253
|
+
("session", session.get("current_auto_run")),
|
|
254
|
+
("pointer", pointer.get("run_id")),
|
|
255
|
+
):
|
|
203
256
|
path = _auto_state_path(repo_root, run_id)
|
|
204
257
|
if path is None:
|
|
205
258
|
continue
|
|
206
|
-
|
|
259
|
+
result = _read_json_result(path)
|
|
260
|
+
if source == "session" and result["status"] in {"corrupt", "io_error"}:
|
|
261
|
+
return None, path, f"session-auto-run-{result['status']}"
|
|
262
|
+
state = result["data"] if isinstance(result.get("data"), dict) else {}
|
|
207
263
|
if state.get("status") == "running":
|
|
208
264
|
candidate_paths.append(path)
|
|
209
265
|
else:
|
|
@@ -227,6 +283,8 @@ def _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[st
|
|
|
227
283
|
|
|
228
284
|
path = unique_paths[0]
|
|
229
285
|
state = _read_json(path)
|
|
286
|
+
if not _run_contains_task(state, current_task):
|
|
287
|
+
return None, path, "auto-run-task-mismatch"
|
|
230
288
|
|
|
231
289
|
auth = state.get("route_authorization")
|
|
232
290
|
if not isinstance(auth, dict):
|
|
@@ -302,7 +360,10 @@ def _write_runtime_decision(
|
|
|
302
360
|
) -> tuple[Path, dict[str, str]]:
|
|
303
361
|
"""写入 session runtime 文件中的 route_decisions 字段。"""
|
|
304
362
|
path = _session_path(repo_root, context_key)
|
|
305
|
-
|
|
363
|
+
context_result = _read_json_result(path)
|
|
364
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
365
|
+
raise ValueError(f"session-runtime-{context_result['status']}")
|
|
366
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
306
367
|
now = _utc_now()
|
|
307
368
|
context.setdefault("platform", context_key.split("_", 1)[0] if "_" in context_key else "session")
|
|
308
369
|
context["last_seen_at"] = now
|
|
@@ -349,7 +410,14 @@ def read_runtime(args: argparse.Namespace) -> int:
|
|
|
349
410
|
assert current_task is not None and context_key is not None
|
|
350
411
|
|
|
351
412
|
path = _session_path(repo_root, context_key)
|
|
352
|
-
|
|
413
|
+
context_result = _read_json_result(path)
|
|
414
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
415
|
+
return _output(
|
|
416
|
+
args,
|
|
417
|
+
{"status": "miss", "reason": f"session-runtime-{context_result['status']}"},
|
|
418
|
+
{"path": _rel_path(repo_root, path), "error": context_result.get("error")},
|
|
419
|
+
)
|
|
420
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
353
421
|
decision = context.get("route_decisions", {}).get(args.target)
|
|
354
422
|
normalized = _normalized_decision(decision, args.target, current_task)
|
|
355
423
|
if normalized is not None:
|
|
@@ -393,7 +461,14 @@ def resolve_route(args: argparse.Namespace) -> int:
|
|
|
393
461
|
assert current_task is not None and context_key is not None
|
|
394
462
|
|
|
395
463
|
path = _session_path(repo_root, context_key)
|
|
396
|
-
|
|
464
|
+
context_result = _read_json_result(path)
|
|
465
|
+
if context_result["status"] in {"corrupt", "io_error"}:
|
|
466
|
+
return _output(
|
|
467
|
+
args,
|
|
468
|
+
{"status": "miss", "reason": f"session-runtime-{context_result['status']}"},
|
|
469
|
+
{"path": _rel_path(repo_root, path), "error": context_result.get("error")},
|
|
470
|
+
)
|
|
471
|
+
context = context_result["data"] if isinstance(context_result.get("data"), dict) else {}
|
|
397
472
|
decision = context.get("route_decisions", {}).get(args.target)
|
|
398
473
|
normalized = _normalized_decision(decision, args.target, current_task)
|
|
399
474
|
if normalized is not None:
|
|
@@ -451,7 +526,12 @@ def resolve_route(args: argparse.Namespace) -> int:
|
|
|
451
526
|
}
|
|
452
527
|
)
|
|
453
528
|
|
|
454
|
-
auto_mode, auto_path, auto_reason = _auto_route_mode(
|
|
529
|
+
auto_mode, auto_path, auto_reason = _auto_route_mode(
|
|
530
|
+
repo_root,
|
|
531
|
+
context_key,
|
|
532
|
+
current_task,
|
|
533
|
+
args.target,
|
|
534
|
+
)
|
|
455
535
|
if auto_mode in PREF_MODES[args.target]:
|
|
456
536
|
written_path, auto_decision = _write_runtime_decision(
|
|
457
537
|
repo_root,
|
|
@@ -535,14 +615,17 @@ def write_route(args: argparse.Namespace) -> int:
|
|
|
535
615
|
if not context_key:
|
|
536
616
|
return _print({"status": "skipped", "reason": "no-session-context", "source": source})
|
|
537
617
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
618
|
+
try:
|
|
619
|
+
path, decision = _write_runtime_decision(
|
|
620
|
+
repo_root,
|
|
621
|
+
context_key,
|
|
622
|
+
current_task,
|
|
623
|
+
args.target,
|
|
624
|
+
mode,
|
|
625
|
+
args.source,
|
|
626
|
+
)
|
|
627
|
+
except (OSError, ValueError) as exc:
|
|
628
|
+
return _print({"status": "error", "reason": "runtime-write-failed", "message": str(exc)})
|
|
546
629
|
return _output(
|
|
547
630
|
args,
|
|
548
631
|
{
|
|
@@ -617,7 +700,10 @@ def main() -> int:
|
|
|
617
700
|
"""脚本入口。"""
|
|
618
701
|
parser = build_parser()
|
|
619
702
|
args = parser.parse_args()
|
|
620
|
-
|
|
703
|
+
try:
|
|
704
|
+
return args.func(args)
|
|
705
|
+
except (OSError, ValueError) as exc:
|
|
706
|
+
return _print({"status": "error", "reason": "runtime-io-error", "message": str(exc)})
|
|
621
707
|
|
|
622
708
|
|
|
623
709
|
if __name__ == "__main__":
|