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.
Files changed (98) hide show
  1. package/enhancements/0.6/.agents/skills/trellis-auto-loop/SKILL.md +19 -1
  2. package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +10 -0
  3. package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +4 -2
  4. package/enhancements/0.6/.agents/skills/trellis-route/scripts/route_state.py +112 -26
  5. package/enhancements/0.6/.claude/skills/trellis-auto-loop/SKILL.md +19 -1
  6. package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +10 -0
  7. package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +4 -2
  8. package/enhancements/0.6/.claude/skills/trellis-route/scripts/route_state.py +112 -26
  9. package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +10 -0
  10. package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -0
  11. package/enhancements/0.6/overrides/bundles/task-progress-recovery.json +7 -0
  12. package/enhancements/0.6/overrides/conflicts.json +353 -4
  13. package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/claude-content.py +17 -0
  14. package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/claude-selector.py +3 -0
  15. package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/codex-content.py +17 -0
  16. package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/codex-selector.py +3 -0
  17. package/enhancements/0.6/overrides/patches/hooks/session-start/pre-check-hold/patch.json +35 -0
  18. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-fallback-content.py +20 -0
  19. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-fallback-selector.py +23 -0
  20. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-resolution-content.py +22 -0
  21. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-resolution-selector.py +27 -0
  22. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-runtime-io-content.py +24 -0
  23. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-runtime-io-selector.py +18 -0
  24. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-set-content.py +26 -0
  25. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/active-task-set-selector.py +27 -0
  26. package/enhancements/0.6/overrides/patches/scripts/runtime-state-integrity/patch.json +55 -0
  27. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/degraded-content.py +19 -0
  28. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/degraded-selector.py +25 -0
  29. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/helper-content.py +39 -0
  30. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/patch.json +14 -0
  31. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/session-content.py +23 -0
  32. package/enhancements/0.6/overrides/patches/scripts/task-start-brief-gate/session-selector.py +20 -0
  33. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/add-subtask-content.py +23 -0
  34. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/add-subtask-selector.py +5 -0
  35. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-guard-content.py +4 -0
  36. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-guard-selector.py +3 -0
  37. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-write-content.py +5 -0
  38. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/archive-write-selector.py +3 -0
  39. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-active-content.py +17 -0
  40. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-active-selector.py +10 -0
  41. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-duplicate-content.py +4 -0
  42. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-duplicate-selector.py +4 -0
  43. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-parent-content.py +38 -0
  44. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-parent-selector.py +21 -0
  45. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-write-content.py +8 -0
  46. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/create-write-selector.py +3 -0
  47. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/helpers-content.py +36 -0
  48. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/helpers-selector.py +6 -0
  49. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/patch.json +93 -0
  50. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/remove-subtask-content.py +23 -0
  51. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/remove-subtask-selector.py +5 -0
  52. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-base-branch-content.py +4 -0
  53. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-base-branch-selector.py +2 -0
  54. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-branch-content.py +4 -0
  55. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-branch-selector.py +2 -0
  56. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-scope-content.py +4 -0
  57. package/enhancements/0.6/overrides/patches/scripts/task-store-write-integrity/set-scope-selector.py +2 -0
  58. package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/content.md +5 -0
  59. package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/patch.json +31 -0
  60. package/enhancements/0.6/overrides/patches/skills/trellis-before-dev/project-knowledge-discovery/selector.md +1 -0
  61. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-authorization/content.md +2 -0
  62. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -0
  63. package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/content.md +21 -0
  64. package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/patch.json +32 -0
  65. package/enhancements/0.6/overrides/patches/skills/trellis-continue/task-progress-recovery/selector.md +1 -0
  66. package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/gemini-content.md +3 -0
  67. package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/gemini-selector.txt +1 -0
  68. package/enhancements/0.6/overrides/patches/skills/trellis-finish-work/exact-bookkeeping/patch.json +34 -0
  69. package/enhancements/0.6/overrides/patches/skills/trellis-start/no-task-routing/content.md +1 -1
  70. package/enhancements/0.6/overrides/patches/skills/trellis-update-spec/autonomous-evaluation/patch.json +22 -0
  71. package/enhancements/0.6/overrides/patches/workflow/hub/content.md +30 -162
  72. package/enhancements/0.6/overrides/patches/workflow/intent-routing/request-triage/content.md +11 -5
  73. package/enhancements/0.6/overrides/patches/workflow/phase-ownership/phase-2-check-content.md +2 -0
  74. package/enhancements/0.6/overrides/patches/workflow/phase-ownership/phase-2-implement-content.md +11 -0
  75. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-clear-fallback-content.py +14 -6
  76. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-read-result-content.py +15 -0
  77. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-result-content.py +25 -0
  78. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/active-task-result-selector.py +1 -0
  79. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/patch.json +50 -0
  80. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/paths-clear-current-content.py +26 -0
  81. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/paths-clear-current-selector.py +24 -0
  82. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/task-finish-content.py +20 -0
  83. package/enhancements/0.6/overrides/patches/workflow/state-missing-task/task-finish-selector.py +19 -0
  84. package/enhancements/0.6/overrides/patches/workflow/state-no-task/content.md +2 -1
  85. package/enhancements/0.6/overrides/patches/workflow/states-in-progress/common-content.md +2 -1
  86. package/enhancements/0.6/overrides/patches/workflow/states-planning/common-content.md +1 -0
  87. package/enhancements/0.6/scripts/auto_loop.py +539 -48
  88. package/enhancements/0.6/scripts/pre_check_state.py +353 -0
  89. package/enhancements/0.6/scripts/task_intent.py +8 -1
  90. package/enhancements/0.6/scripts/task_progress.py +63 -11
  91. package/enhancements/MANIFEST.json +63 -2
  92. package/package.json +3 -3
  93. package/src/commands/uninstall.js +10 -8
  94. package/src/constants.js +25 -0
  95. package/src/lib/apply-enhancements.js +9 -7
  96. package/src/lib/copy-scripts.js +14 -0
  97. package/src/lib/copy-skills.js +27 -36
  98. 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
- | `refresh_brief` | 使用 `trellis-task-brief` 生成并展示 brief | `record --action refresh_brief --result ok` |
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 _read_json(path: Path) -> dict[str, Any]:
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 (FileNotFoundError, json.JSONDecodeError, OSError):
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
- """原子性要求不高的 runtime 状态写入。"""
78
+ """使用同目录临时文件原子写入 runtime 状态。"""
64
79
  path.parent.mkdir(parents=True, exist_ok=True)
65
- path.write_text(
66
- json.dumps(data, indent=2, ensure_ascii=False) + "\n",
67
- encoding="utf-8",
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 _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[str | None, Path | None, str | None]:
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
- session = _read_json(session_path)
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
- for run_id in (session.get("current_auto_run"), _read_json(_auto_loop_pointer(repo_root)).get("run_id")):
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
- state = _read_json(path)
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
- context = _read_json(path)
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
- context = _read_json(path)
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
- context = _read_json(path)
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(repo_root, context_key, args.target)
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
- path, decision = _write_runtime_decision(
539
- repo_root,
540
- context_key,
541
- current_task,
542
- args.target,
543
- mode,
544
- args.source,
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
- return args.func(args)
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
- | `refresh_brief` | 使用 `trellis-task-brief` 生成并展示 brief | `record --action refresh_brief --result ok` |
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 _read_json(path: Path) -> dict[str, Any]:
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 (FileNotFoundError, json.JSONDecodeError, OSError):
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
- """原子性要求不高的 runtime 状态写入。"""
78
+ """使用同目录临时文件原子写入 runtime 状态。"""
64
79
  path.parent.mkdir(parents=True, exist_ok=True)
65
- path.write_text(
66
- json.dumps(data, indent=2, ensure_ascii=False) + "\n",
67
- encoding="utf-8",
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 _auto_route_mode(repo_root: Path, context_key: str, target: str) -> tuple[str | None, Path | None, str | None]:
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
- session = _read_json(session_path)
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
- for run_id in (session.get("current_auto_run"), _read_json(_auto_loop_pointer(repo_root)).get("run_id")):
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
- state = _read_json(path)
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
- context = _read_json(path)
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
- context = _read_json(path)
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
- context = _read_json(path)
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(repo_root, context_key, args.target)
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
- path, decision = _write_runtime_decision(
539
- repo_root,
540
- context_key,
541
- current_task,
542
- args.target,
543
- mode,
544
- args.source,
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
- return args.func(args)
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__":
@@ -0,0 +1,10 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "control-plane-integrity",
4
+ "aliases": [],
5
+ "installMode": "full-only",
6
+ "patches": [
7
+ "scripts/task-store-write-integrity",
8
+ "scripts/runtime-state-integrity"
9
+ ]
10
+ }