flower-trellis 0.5.0-beta.6 → 0.5.0-beta.7
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 +12 -0
- 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 +12 -0
- 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 +1 -0
- package/enhancements/0.6/overrides/conflicts.json +125 -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-brainstorm/planning-handoff/content.md +2 -0
- package/enhancements/0.6/overrides/patches/workflow/hub/content.md +1 -0
- 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/scripts/auto_loop.py +287 -45
- 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 +43 -7
- package/enhancements/MANIFEST.json +54 -2
- package/package.json +3 -3
- package/src/lib/copy-scripts.js +11 -0
|
@@ -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,6 +116,7 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
107
116
|
|
|
108
117
|
| runner action | 主 agent 动作 | 成功 record |
|
|
109
118
|
| --- | --- | --- |
|
|
119
|
+
| `review_open_questions` | 结合 PRD 语义判断历史裸列表是已解决、仍阻塞或无法确定 | 已解决:`record --action review_open_questions --result ok --review-verdict resolved --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --review-verdict blocking|ambiguous` |
|
|
110
120
|
| `refresh_brief` | 使用 `trellis-task-brief` 生成并展示 brief | `record --action refresh_brief --result ok` |
|
|
111
121
|
| `start_task` | 执行返回的 `task.py start ...` 命令 | `record --action start_task --result ok` |
|
|
112
122
|
| `run_implement` | 进入 Phase 2.1,先用 `trellis-route(target=implement)` 决定 inline/subagent,再实现 | `record --action run_implement --result ok --route-mode <mode> --route-source <source>` |
|
|
@@ -143,6 +153,8 @@ python3 ./.trellis/scripts/auto_loop.py record \
|
|
|
143
153
|
|
|
144
154
|
runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
|
|
145
155
|
|
|
156
|
+
`review_open_questions` 必须只依据 action 返回的 `questions` 和当前 PRD 判断。runner 会把结果绑定到 action 发出时的 PRD SHA-256;PRD 在复核期间发生变化时,record 会返回 `stale-open-questions-review`,此时重新运行 `next`,不得复用旧判断。`ambiguous` 必须保守阻塞,交由用户或 planning 收敛。
|
|
157
|
+
|
|
146
158
|
`record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
|
|
147
159
|
|
|
148
160
|
route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
|
|
@@ -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,6 +116,7 @@ python3 ./.trellis/scripts/auto_loop.py next
|
|
|
107
116
|
|
|
108
117
|
| runner action | 主 agent 动作 | 成功 record |
|
|
109
118
|
| --- | --- | --- |
|
|
119
|
+
| `review_open_questions` | 结合 PRD 语义判断历史裸列表是已解决、仍阻塞或无法确定 | 已解决:`record --action review_open_questions --result ok --review-verdict resolved --summary "<摘要>"`;仍阻塞/无法确定:使用 `--result blocked --review-verdict blocking|ambiguous` |
|
|
110
120
|
| `refresh_brief` | 使用 `trellis-task-brief` 生成并展示 brief | `record --action refresh_brief --result ok` |
|
|
111
121
|
| `start_task` | 执行返回的 `task.py start ...` 命令 | `record --action start_task --result ok` |
|
|
112
122
|
| `run_implement` | 进入 Phase 2.1,先用 `trellis-route(target=implement)` 决定 inline/subagent,再实现 | `record --action run_implement --result ok --route-mode <mode> --route-source <source>` |
|
|
@@ -143,6 +153,8 @@ python3 ./.trellis/scripts/auto_loop.py record \
|
|
|
143
153
|
|
|
144
154
|
runner 会按 3 轮 fix/recheck 预算决定继续、跳过当前任务或结束队列。
|
|
145
155
|
|
|
156
|
+
`review_open_questions` 必须只依据 action 返回的 `questions` 和当前 PRD 判断。runner 会把结果绑定到 action 发出时的 PRD SHA-256;PRD 在复核期间发生变化时,record 会返回 `stale-open-questions-review`,此时重新运行 `next`,不得复用旧判断。`ambiguous` 必须保守阻塞,交由用户或 planning 收敛。
|
|
157
|
+
|
|
146
158
|
`record` 默认只返回当前 item 的 `task`、`item_status`、`current_step`、`commit` 和紧凑 `summary`;只有排查状态漂移时才加 `--verbose` 查看完整 `item`。
|
|
147
159
|
|
|
148
160
|
route action 成功回写时必须带上 `trellis-route` 输出里的真实 `mode` / `source`,例如
|
|
@@ -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__":
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"hooks/codex-session-start/missing-task-routing",
|
|
25
25
|
"hooks/claude-session-start/no-task-routing",
|
|
26
26
|
"hooks/claude-session-start/missing-task-routing",
|
|
27
|
+
"hooks/session-start/pre-check-hold",
|
|
27
28
|
"skills/trellis-start/no-task-routing",
|
|
28
29
|
"skills/trellis-brainstorm/planning-authorization",
|
|
29
30
|
"skills/trellis-brainstorm/auto-task-create",
|