dsh-tower 2.5.0 → 2.6.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/CHANGELOG.md +32 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +1 -1
- package/GLOSSARY.md +6 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +16 -15
- package/README.md +12 -10
- package/app/review.js +7 -0
- package/app/spawn-prechecks.js +12 -2
- package/app/tower.js +5 -5
- package/client.js +321 -0
- package/cordis.patch.yml +5 -3
- package/docs/adr/0005-component-surface.md +58 -13
- package/docs/architecture/overview.md +12 -6
- package/docs/examples/first-mission.md +3 -2
- package/docs/faq.md +9 -7
- package/docs/guide/install-upgrade-migration.md +32 -7
- package/docs/guide/quickstart.md +20 -10
- package/docs/guide/scenarios-and-philosophy.md +35 -28
- package/docs/operations/security.md +11 -10
- package/docs/reference/blockers.md +12 -7
- package/docs/reference/component-surface.md +14 -7
- package/docs/reference/config.generated.md +3 -2
- package/docs/reference/config.md +29 -3
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/surface-budget.md +20 -3
- package/docs/reference/testing.md +1 -1
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +3 -3
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +25 -6
- package/docs/zh/guide/quickstart.md +17 -10
- package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
- package/docs/zh/operations/security.md +9 -8
- package/docs/zh/reference/blockers.md +11 -7
- package/docs/zh/reference/config.md +21 -3
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +2 -2
- package/docs/zh/reference/tools.md +3 -3
- package/domain/components.js +12 -4
- package/domain/config/schema.js +40 -5
- package/domain/contracts/outputs.js +2 -0
- package/domain/contracts/tool-inputs/missions.js +1 -1
- package/domain/errors.js +6 -0
- package/domain/gate.js +6 -3
- package/domain/integration/review.js +203 -9
- package/host/config.js +40 -3
- package/host/doctor.js +18 -7
- package/host/presets.js +9 -8
- package/host/tool-registry.js +9 -1
- package/package.json +7 -1
- package/scripts/check-components.mjs +120 -6
- package/scripts/check-pack.mjs +3 -0
- package/scripts/component-surface.json +16 -10
- package/scripts/pack-manifest.txt +1 -0
- package/scripts/surface-baseline.json +8 -1
- package/scripts/types-coverage.mjs +7 -0
- package/tools/preflight.js +1 -1
- package/tools/review.js +1 -1
- package/tools/spawn.js +2 -2
- package/tools/status.js +8 -1
- package/types/index.d.ts +10 -0
|
@@ -31,6 +31,23 @@ Host 插件在 DSH 启动时加载:**安装或更新后需要重启 DSH**。
|
|
|
31
31
|
|
|
32
32
|
## 迁移说明
|
|
33
33
|
|
|
34
|
+
- **2.5.0 → 2.6.0(#M39/#M40)—— 审查模式取代布尔开关**:审查策略是插件页 `dsh-tower` 配置区里的
|
|
35
|
+
一个三态控件,配置键为 `review.mode`:`off`、`optional`(默认)或 `required`。
|
|
36
|
+
- `off` 是受支持且有文档的模式:不注册 `tower_review`,拒绝 reviewer spawn,合并门禁忽略审查。
|
|
37
|
+
- `optional` 保留 `tower_review`,但从不阻塞合并。
|
|
38
|
+
- `required` 保留合并门禁:已完成 mission 必须有绑定分支 tip 的 clean 审查(原有的单 mission
|
|
39
|
+
豁免仍适用)。
|
|
40
|
+
`review.requireReview` 是已弃用、保留一个版本的别名:`true` 解析为 `required`,`false` 解析为
|
|
41
|
+
`optional`,不设置则为 `optional`。`true` 与非 `required` 的显式 mode 同时出现会以 `config-invalid`
|
|
42
|
+
拒绝;`false` 不会覆盖显式 mode。从插件页保存时总是写入 `review.mode`,并把别名归一化为 `false`。
|
|
43
|
+
该策略不是组件行,也不存在互斥的审查行:`preset-tower-reviewer` 仍是策略可以不使用的可选能力。
|
|
44
|
+
`review.mode` 为 `off` 或 `optional` 时该行可以关闭;当 `review.mode` 为 `required` 且该 reviewer
|
|
45
|
+
行关闭时,规划与模式进入会以带解释的 `preset-missing` 错误拒绝该组合。
|
|
46
|
+
以 `off` 启动的 profile 从未注册 `tower_review`;切换到 `optional` 或 `required` 会立即改变合并门禁,
|
|
47
|
+
但工具本身要等 DSH 重启(或 profile 重载)后才出现,插件页在保存后会说明这一点。
|
|
48
|
+
升级方式:把 `review.requireReview: true|false` 替换为 `review.mode: required|optional`;仅当同一份
|
|
49
|
+
配置还要供旧 2.5 安装使用时保留别名,并在本版本后移除。不需要迁移 state。
|
|
50
|
+
|
|
34
51
|
- **state schema v1 → v2(未发布)**:`state.json` 会自动迁移。升级后的第一次**写入**会保留
|
|
35
52
|
`state.json.v1.bak` 备份、`revision` 只 +1,并向 `activity.jsonl` 追加 `state.migrated {from,to}`;
|
|
36
53
|
只读工具仍可读取 v1 文件且不改写它。`tower_doctor` 报告磁盘上的版本与待执行的迁移。由更新插件
|
|
@@ -45,13 +62,13 @@ Host 插件在 DSH 启动时加载:**安装或更新后需要重启 DSH**。
|
|
|
45
62
|
- **2.3.0 → 2.4.0(未发布)**:所有能力合一的 `tower-bundle` 组行已移除。`cordis.patch.yml`
|
|
46
63
|
现在随包提供强制的 `tower` host 锚点(Host 插件与 worker preset 是它的内部 section),以及可单独
|
|
47
64
|
开关的顶层行:`tower-skills`、`tower-batch`、`tower-import`、`tower-leases`、`tower-tests`、
|
|
48
|
-
`tower-hmr`(随包关闭)与 `preset-tower-reviewer
|
|
65
|
+
`tower-hmr`(随包关闭)与 `preset-tower-reviewer`(受审查策略 `review.mode` 条件约束)。
|
|
49
66
|
`tools` 配置组与 `dev.hotReload` 已删除:仍带这些键的 profile 现在会作为未知键被拒绝
|
|
50
67
|
(`config-unknown-key`),在删除它们之前 host 不会挂载,因此不再存在静默无效的开关。请改为在插件页
|
|
51
68
|
使用对应的组件行;`tower-hmr` 随包关闭,因此在为联动开发 profile 开启它之前,`tower_reload` 与
|
|
52
69
|
`/tower reload` 会报告为 “disabled by component tower-hmr”。`tower_doctor` 与 `tower_recover`
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
在任何配置下都会注册,无需配置键。策略为 `off` 或 `optional` 时 reviewer 行可以关闭;策略为
|
|
71
|
+
`review.mode:required` 时,规划与模式进入会以带解释的错误拒绝该组合。不需要迁移 state。
|
|
55
72
|
- **2.0.4 → 2.1.0-beta.1**:不需要迁移 config;state 会按上述说明自动迁移(v1/v2 → v3)。
|
|
56
73
|
这是发布到 `next` dist-tag 的**预发布**,
|
|
57
74
|
`latest` 仍指向 2.0.4——请用 `dsh plugin --profile web add dsh-tower@next`(或精确版本号)安装。
|
|
@@ -60,9 +77,11 @@ Host 插件在 DSH 启动时加载:**安装或更新后需要重启 DSH**。
|
|
|
60
77
|
`npm publish --tag next` 之后,`next` 才会解析到 2.1.0-beta.1;`latest` 在稳定版发布前保持 2.0.4。
|
|
61
78
|
- **2.0.3 → 2.0.4**:不需要迁移 state/config。本版本是安全加固与完整性修复(授权、merge SHA 绑定、
|
|
62
79
|
dirty worktree 完成拒绝、stale lock 恢复、限制 test/git 进程环境、`tower_recover`、UI 0.2.2)。
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
升级后请按威胁模型重新检查审查策略(`review.mode`,或已弃用的 `review.requireReview` 别名)、
|
|
81
|
+
`tests.admission` 与 worker 工具权限。
|
|
82
|
+
- **2.0.1 → 2.0.3**:不需要迁移状态。2.0.3 起独立审查**默认可选**(即今天的
|
|
83
|
+
`review.mode:optional`);若要保留旧的审查门禁行为,请显式设为 `review.mode:required`。
|
|
84
|
+
2.0.1 为 `tower_batch` 增加 `abandon`、允许 merged
|
|
66
85
|
成员 close、让 doctor 跳过已 teardown 的 worktree,并修复终态 mission spawn/preflight 边界。
|
|
67
86
|
可选 `ui-bundle` 0.2.1 增加常驻 Tower 模式 chip 与模式感知的 disabled 工具行。
|
|
68
87
|
- **1.x → 2.0(已发布)**:不需要迁移状态(`schemaVersion` 保持 1;新字段都是可选的)。需要留意的
|
|
@@ -34,15 +34,22 @@ tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
|
|
|
34
34
|
|
|
35
35
|
## 4. 可选:审查
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
`review.
|
|
37
|
+
审查策略是插件页 `dsh-tower` 配置区里的一个三态控件:`off`、`optional`(默认)或 `required`;
|
|
38
|
+
也可以在 profile 或仓库配置中写 `review.mode`。它是一个控件,不是组件行,也不是三个行。需要严格
|
|
39
|
+
流程时设为 `required`,然后:
|
|
39
40
|
|
|
40
41
|
```
|
|
41
42
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
reviewer 使用只读的 `tower-reviewer` preset,查看 `tower_diff` 并提交 `tower_review
|
|
45
|
-
`review.
|
|
45
|
+
reviewer 使用只读的 `tower-reviewer` preset,查看 `tower_diff` 并提交 `tower_review`。设为
|
|
46
|
+
`review.mode:required` 后,只有 `clean` + `merge` 的结论才允许合并。`optional` 会记录审查但从不
|
|
47
|
+
阻塞合并;`off` 是受支持的模式,不会注册 `tower_review`。
|
|
48
|
+
|
|
49
|
+
旧布尔审查开关是保留一个版本的别名(`true` → `required`,`false` → `optional`;不设置 →
|
|
50
|
+
`optional`)。请优先使用 `review.mode`。以 `off` 启动的 profile 需要
|
|
51
|
+
重启或重载后才会注册 `tower_review`,但合并门禁会立即改变。完整迁移说明见
|
|
52
|
+
[安装、升级与迁移](install-upgrade-migration.md)。
|
|
46
53
|
|
|
47
54
|
## 5. 门禁、合并与清理
|
|
48
55
|
|
|
@@ -52,8 +59,8 @@ tower_merge repoRoot=D:\work\repo missionId=M1
|
|
|
52
59
|
tower_teardown repoRoot=D:\work\repo
|
|
53
60
|
```
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
策略为 `review.mode:required` 时,审查前运行 `tower_preflight` 会报告 `no-review`;干净审查之后应为
|
|
63
|
+
`ok:true`。默认 `optional` 时没有审查 blocker。存在其他 blocker 时 `tower_merge` 拒绝合并。
|
|
57
64
|
`tower_teardown` 移除已 merged/abandoned 的 worktree 与分支。
|
|
58
65
|
|
|
59
66
|
```
|
|
@@ -63,9 +70,9 @@ tower_teardown repoRoot=D:\work\repo
|
|
|
63
70
|
## 如果失败
|
|
64
71
|
|
|
65
72
|
阅读 blocker code 并查阅 [blocker 参考](../reference/blockers.md)。最常见的情况:`stale-base`
|
|
66
|
-
(执行 `tower_rebase`)、`no-review`(仅在 `review.
|
|
67
|
-
`merge-hook-failed`(重试,或调大 `timeoutMs`)、`tip-moved
|
|
68
|
-
|
|
73
|
+
(执行 `tower_rebase`)、`no-review`(仅在 `review.mode:required` 时;spawn reviewer)、
|
|
74
|
+
`merge-hook-failed`(重试,或调大 `timeoutMs`)、`tip-moved`(仅在策略为 `required` 时;审查后
|
|
75
|
+
分支已变化,需要重新审查)。
|
|
69
76
|
|
|
70
77
|
工具抛出的错误带 `[tower:<code>]` 前缀;见[错误码](../reference/errors.md)(例如
|
|
71
78
|
`preset-missing` 表示安装 bundle 后需要重启 DSH,`not-initialized` 表示先运行 `tower_init`)。
|
|
@@ -77,7 +84,7 @@ tower_teardown repoRoot=D:\work\repo
|
|
|
77
84
|
|
|
78
85
|
```json
|
|
79
86
|
{
|
|
80
|
-
"review": { "
|
|
87
|
+
"review": { "mode": "required", "maxRoundsPerReviewer": 3 },
|
|
81
88
|
"merge": { "timeoutMs": 1800000 },
|
|
82
89
|
"worktrees": { "root": "D:/tower-worktrees" }
|
|
83
90
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Tower 不是所有多代理模式的替代品。它是**面向 Git 仓库变更的交付协议**:少量边界清晰的 mission、隔离的写入者、可选独立审查、证据与合并门禁。本页解释这些设计取舍、适用场景,以及什么时候 Agent Teams、Ralph 循环、Workflow 编排或普通并行子代理更合适。
|
|
6
6
|
|
|
7
|
-
如果只记一句话:**输出是需要经过受控合并落地的仓库变更时用 Tower
|
|
7
|
+
如果只记一句话:**输出是需要经过受控合并落地的仓库变更时用 Tower;输出是探索、重复尝试或程序化扇出时用其他模式。**如果还需要独立审查,把审查策略设为 `required`;默认为 `optional`。
|
|
8
8
|
|
|
9
9
|
## 1. 设计哲学
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ mission 不是聊天线程或任务卡,而是绑定到分支与 worktree 的
|
|
|
26
26
|
|
|
27
27
|
worker 永远不合并。控制塔运行 preflight;只有之后 `tower_merge` 才执行 `--no-ff` 合并。门禁重新检查 scope、base 新鲜度、任务、worktree 归属、worker 状态、测试与证据。开启审查时,reviewer 还会审查特定分支 tip,门禁检查审查绑定;职责分离(写代码者不批准、批准者不合并)在审查开启时生效。
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
审查策略是一个三态控件:`off`、`optional`(默认)或 `required`。设为 `required` 才会强制独立审查(此时已完成 mission 必须带有绑定 tip 的 clean/merge 审查)。对敏感仓库建议设为 `required`;策略为 `required` 时仍可用 `reviewPolicy:"none"` + 理由 + `gates.allowReviewExemption:true` 做单个 mission 豁免。`optional` 会记录审查但从不阻塞合并;`off` 是受支持的模式,会移除审查工具。该策略是配置,不是组件行:插件页上只有一个控件,不是三个互斥行。
|
|
30
30
|
|
|
31
31
|
### 1.5 证据优先于断言
|
|
32
32
|
|
|
@@ -127,7 +127,7 @@ Tower 是 Host 插件,只使用本地 Git、本地文件和 DSH subagent 服
|
|
|
127
127
|
| 隔离 | 每 mission 独立 worktree/分支;单写入者 | 共享工作区;靠任务归属协调 | 每轮新 child;无内置分支/合并契约 | 每步 child;无 Git 隔离 | 子会话;可能共享工作区 |
|
|
128
128
|
| 持久状态 | Git 元数据 + JSON state + 活动日志 + 看板 | Team 任务板 + 会话 | 很少;每轮从零开始 | 运行记录/结果;不是合并账本 | 会话/子代理记录 |
|
|
129
129
|
| 集成权限 | 仅控制塔,经 preflight/merge 门禁(可选再加审查) | 人类/teammate 手动合并或编辑 | 通常由父 agent/worker 应用结果 | 脚本应用结果 | 父 agent 应用结果 |
|
|
130
|
-
| 审查/门禁 |
|
|
130
|
+
| 审查/门禁 | 可选独立审查(一个 `review.mode` 控件:off/optional/required)+ preflight/merge 门禁 | 审查是协作约定,不是硬门禁 | 无内置审查门禁 | 无内置审查门禁 | 无内置审查门禁 |
|
|
131
131
|
| 并行度 | 多 mission、互斥 scope、合并串行 | 多 teammate、共享任务板 | 顺序轮次(或外部扇出) | 高扇出,由脚本控制 | 高扇出 |
|
|
132
132
|
| 人的角色 | 拥有模式;可检查证据与发布 | 协作者/协调者 | 重复目标的作者 | 编排脚本作者 | 委派者 |
|
|
133
133
|
| 最适合 | 特性交付、迁移、发布绑定变更 | 设计、研究、横切协作 | 迭代搜索/修复,需要新鲜上下文 | 大规模审计、批量分析、确定性流水线 | 快速探索或独立子任务 |
|
|
@@ -137,7 +137,7 @@ Tower 是 Host 插件,只使用本地 Git、本地文件和 DSH subagent 服
|
|
|
137
137
|
|
|
138
138
|
普通 `subagent`/`subagent_fork` 便宜灵活,适合"并行读五个文件"或"试三种方案"。它们不提供写范围、专用 worktree、持久 mission、独立审查或合并门禁。Tower 用这些契约包装同一个子代理原语,因此每个 child 更贵。
|
|
139
139
|
|
|
140
|
-
只读、短生命周期、或你准备自己集成结果时,用普通子代理;结果必须落到分支并经受控合并时,用 Tower
|
|
140
|
+
只读、短生命周期、或你准备自己集成结果时,用普通子代理;结果必须落到分支并经受控合并时,用 Tower;需要 reviewer 角色把关合并时,再把审查策略设为 `required`。
|
|
141
141
|
|
|
142
142
|
### 4.2 Tower vs Agent Teams
|
|
143
143
|
|
|
@@ -161,13 +161,16 @@ Tower 是**仓库交付协议**,mission 数量少,不是高扇出数据平
|
|
|
161
161
|
|
|
162
162
|
Plan mode 决定做什么;Tower 把已决定的计划执行到仓库。进入 Tower 会清除活跃 plan(`mode.planModePolicy:"refuse"` 时拒绝进入),且 Tower 会话不能使用 goal/todo/team/workflow 工具。先规划,再执行;不要指望 Tower 替你发现计划。
|
|
163
163
|
|
|
164
|
-
### 4.6
|
|
164
|
+
### 4.6 审查策略是一个三态控件
|
|
165
165
|
|
|
166
|
-
`
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
`
|
|
170
|
-
`
|
|
166
|
+
审查策略是一个控件:`off`、`optional`(默认)或 `required`,位于插件页 `dsh-tower` 配置区,
|
|
167
|
+
配置中对应 `review.mode`。它不是组件行,也不是三个互斥行。`optional` 让低风险仓库、临时工作或
|
|
168
|
+
人类逐轮监督的会话可以在没有 reviewer 时合并已完成 mission。`required` 才启用强制独立审查门禁;
|
|
169
|
+
此时检查 `no-review`、`not-clean`、`review-hold`、`review-not-approved`、`tip-moved`、
|
|
170
|
+
`review-mission-mismatch`。`off` 是受支持的模式,不会注册 `tower_review`;`optional` 记录结论但
|
|
171
|
+
从不阻塞合并。只豁免单个 mission 时,在策略为 `required` 的前提下用 `reviewPolicy:"none"` + 理由
|
|
172
|
+
+ `gates.allowReviewExemption:true`。已弃用、保留一个版本的布尔别名把 `true` 映射为 `required`,
|
|
173
|
+
`false` 映射为 `optional`;见[安装、升级与迁移](install-upgrade-migration.md)。
|
|
171
174
|
|
|
172
175
|
### 4.7 各工作流的具体例子
|
|
173
176
|
|
|
@@ -223,7 +226,7 @@ reviewer 时合并已完成 mission。设为 `true` 才启用强制独立审查
|
|
|
223
226
|
1. 规划四个 build mission,scope 分别 `src/tokens/**`、`src/email/**`、`src/api/invites/**`、`src/ui/invites/**`;让 API 依赖 token 与 email。
|
|
224
227
|
2. 对独立 mission 启动 worker;API 等依赖完成。
|
|
225
228
|
3. 每个 worker 在自己分支提交,带 checks 完成。
|
|
226
|
-
4.
|
|
229
|
+
4. 若策略为 `review.mode:required`,启动 reviewer 并要求 clean + merge;否则审查可选或关闭。
|
|
227
230
|
5. 逐 mission `tower_preflight` + `tower_merge`,处理 scope/base/review blocker。
|
|
228
231
|
6. `tower_teardown`;`MISSIONS.md` 是人类可读记录。
|
|
229
232
|
|
|
@@ -248,14 +251,17 @@ reviewer 时合并已完成 mission。设为 `true` 才启用强制独立审查
|
|
|
248
251
|
|
|
249
252
|
### 场景 E:敏感发布
|
|
250
253
|
|
|
251
|
-
有外部用户的库发布。为变更、迁移指南、release notes 规划 mission
|
|
254
|
+
有外部用户的库发布。为变更、迁移指南、release notes 规划 mission;如发布需要独立审查,把策略设为
|
|
255
|
+
`review.mode:required`;要求测试,并用 `tower_batch` 只有在所有必需检查齐全时才收口发布证据;
|
|
256
|
+
按你的发布流程在 Tower 之外合并/打 tag。
|
|
252
257
|
|
|
253
258
|
## 6. 实践中的选择路径
|
|
254
259
|
|
|
255
260
|
1. **输出是 Git 变更吗?** 否 → 普通 agent、Agent Teams、Workflow 或 Ralph。
|
|
256
261
|
2. **你会为它开 PR 吗?** 否 → Tower 太重,用普通 agent。
|
|
257
262
|
3. **切片有稳定 scope 和已知完成定义吗?** 否 → 先规划/探索,再回到 Tower。
|
|
258
|
-
4. **需要合并门禁吗?** 有集成风险的仓库变更 → Tower
|
|
263
|
+
4. **需要合并门禁吗?** 有集成风险的仓库变更 → Tower;如果还需要独立审查,把审查策略设为
|
|
264
|
+
`required`。两者都不需要时,普通子代理可能足够。
|
|
259
265
|
5. **需要大量并行分析或重复新鲜尝试吗?** 那个阶段用 Workflow/Ralph;把结果编码成 Tower mission。
|
|
260
266
|
6. **需要横切的人类协作吗?** 用 Agent Teams 决定;用 Tower 执行决定的仓库工作。
|
|
261
267
|
|
|
@@ -263,7 +269,9 @@ reviewer 时合并已完成 mission。设为 `true` 才启用强制独立审查
|
|
|
263
269
|
|
|
264
270
|
- **每个 mission 有真实开销**:worktree、spawn、完成检查、可选审查轮次、门禁、合并。mission 应有意义(大致 PR 大小),不要一个函数一个 mission。
|
|
265
271
|
- **合并会串行**:重叠 scope 自动加依赖;独立 mission 仍然逐个合入 base。合并密集的工作不要期待线性加速。
|
|
266
|
-
-
|
|
272
|
+
- **审查不是免费的(策略为 required 时)**:每个 mission 需要 reviewer 与 clean 结论;
|
|
273
|
+
`review.mode:optional`(默认)让审查不阻塞,`off` 则移除审查工具;策略为 `required` 时,豁免是
|
|
274
|
+
显式且需要理由。
|
|
267
275
|
- **过细的 mission** 增加协调与合并成本;**过粗的 mission** 失去并行审查收益,scope 冲突概率高。
|
|
268
276
|
- **Tower 不是沙箱**:它是协调协议;Host 权限、仓库 hooks、测试命令仍以 DSH 用户的 OS 权限执行。
|
|
269
277
|
- **不要同时跑两套编排协议**:Tower 模式会拒绝 Team/Workflow/Ralph/subagent 工具并清除 plan mode;请顺序分阶段。
|
|
@@ -274,7 +282,7 @@ reviewer 时合并已完成 mission。设为 `true` 才启用强制独立审查
|
|
|
274
282
|
- [ ] 工作会产生值得 PR 式审查的仓库变更。
|
|
275
283
|
- [ ] 我能列出 2–8 个 scope 不重叠、依赖顺序大致的 mission。
|
|
276
284
|
- [ ] 每个 mission 有可测试的完成定义(任务、checks/测试、可选审查)。
|
|
277
|
-
- [ ]
|
|
285
|
+
- [ ] 如果这次变更需要独立审查,审查策略为 `required`(默认为 `optional`)。
|
|
278
286
|
- [ ] 我知道哪些 mission 可并行、哪些必须等待。
|
|
279
287
|
- [ ] 我接受合并门禁开销(以及开启审查时的审查开销),并会检查 `MISSIONS.md`/分支。
|
|
280
288
|
- [ ] 当前没有在同一会话依赖 Team 任务板、Workflow 脚本、Ralph 循环或 plan mode。
|
|
@@ -6,12 +6,13 @@ Tower 提供的是**协作层面的保证**,不是沙箱。需要理解哪些
|
|
|
6
6
|
|
|
7
7
|
## 由插件强制执行的约束
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
tip 变化会被门禁拒绝(`tip-moved`),除非一次干净且无冲突的 rebase
|
|
11
|
-
之后修改分支会被拒绝(`reviewer must not modify the mission branch`)。
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
- **审查绑定(策略为 required 时)。** `tower_review` 会记录它检查的分支 tip;策略为
|
|
10
|
+
`review.mode:required` 时,tip 变化会被门禁拒绝(`tip-moved`),除非一次干净且无冲突的 rebase
|
|
11
|
+
给予了豁免。reviewer 在 spawn 之后修改分支会被拒绝(`reviewer must not modify the mission branch`)。
|
|
12
|
+
- **审查默认可选,可由三态策略强制。** `review.mode` 取 `off`、`optional`(默认)或 `required`。
|
|
13
|
+
设为 `required` 才启用独立审查门禁。策略为 required 时,mission 级 `reviewPolicy:"none"` + 理由是
|
|
14
|
+
更窄的豁免。策略为 `off` 时审查被有意关闭;为 `optional` 时插件不强制职责分离,只应用于可信仓库或
|
|
15
|
+
有人类监督的会话。该策略是插件页上的一个控件,不是组件行。
|
|
15
16
|
- **角色强制。** 调用方会根据 roster 解析。worker 的 mission 更新仅限于 task/blocker/note;
|
|
16
17
|
`completed`、`abandoned`、扩大 scope 与清除 blocker 仅限控制塔,完成(连同未读消息/提交门禁)必须
|
|
17
18
|
走 `tower_complete`。仅限控制塔的工具会以 `[tower:not-authorized]` 拒绝 roster 中的 worker。
|
|
@@ -52,8 +53,8 @@ Tower 模式并修复它”。Tower **不认证**这句话背后的人类意图
|
|
|
52
53
|
|
|
53
54
|
它**不**防御:完全通过所有门禁但被恶意指令操纵的模型;仓库自己声明的 hooks/测试命令;或无视
|
|
54
55
|
指令、通过 shell 访问 worktree 之外文件的 worker。更高保证的团队应设
|
|
55
|
-
`mode.allowAgentControl:false
|
|
56
|
-
OS 级隔离运行 host。
|
|
56
|
+
`mode.allowAgentControl:false`、保持审查策略为 `review.mode:required`、在运行 Tower 前审查仓库,
|
|
57
|
+
并用 OS 级隔离运行 host。
|
|
57
58
|
|
|
58
59
|
## 不由插件强制执行的约束(部署关注点)
|
|
59
60
|
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
`tower_preflight` 返回 `{ok:false, blockers:[{code, detail}]}`。`tower_merge` 与 `tower_rebase`
|
|
6
6
|
在操作失败时返回相同结构。code 稳定;detail 面向人。下表共定义 32 个稳定 blocker/failure code。
|
|
7
7
|
|
|
8
|
+
下列审查 blocker(`no-review`、`review-mission-mismatch`、`not-clean`、`review-hold`、
|
|
9
|
+
`review-not-approved`、`tip-moved`、`review-exemption-invalid`)只在 `review.mode` 为 `required`
|
|
10
|
+
时产生。`off` 或 `optional` 不会产生审查 blocker;`off` 还会移除 `tower_review` 并拒绝 reviewer spawn。
|
|
11
|
+
|
|
8
12
|
## 门禁 blocker(26 个)
|
|
9
13
|
|
|
10
14
|
| Code | 含义 / 下一步 |
|
|
@@ -20,12 +24,12 @@
|
|
|
20
24
|
| `out-of-scope` | 改动文件超出 mission scope;有意为之就扩大 scope,否则回退。在大小写敏感的检出上,仅大小写不同的路径仍会被拒绝,detail 会指出折叠大小写后本可匹配的 scope 条目。 |
|
|
21
25
|
| `base-dirty` | 主检出存在未提交改动,合并会覆盖它们。 |
|
|
22
26
|
| `deps-unmerged` | 依赖 mission 尚未满足:build 依赖须 `merged`/`abandoned`;survey 依赖在 `completed` 即视为满足(含归档记录)。 |
|
|
23
|
-
| `no-review` | `review.
|
|
24
|
-
| `review-mission-mismatch` | `review.
|
|
25
|
-
| `not-clean` | `review.
|
|
26
|
-
| `review-hold` | `review.
|
|
27
|
-
| `review-not-approved` | `review.
|
|
28
|
-
| `tip-moved` | `review.
|
|
27
|
+
| `no-review` | `review.mode:required` 且分支上没有审查:spawn reviewer(或关闭审查门禁)。 |
|
|
28
|
+
| `review-mission-mismatch` | `review.mode:required` 且最新审查属于另一个 mission。 |
|
|
29
|
+
| `not-clean` | `review.mode:required` 且最新审查是 `p1-Nitems`/`p2-Nitems`;需要再来一轮(同一轮的各 route 结论会追加记录,不会让状态反复翻转)。 |
|
|
30
|
+
| `review-hold` | `review.mode:required` 且最新审查建议 hold;必须由后续的干净审查取代。 |
|
|
31
|
+
| `review-not-approved` | `review.mode:required` 且最新干净审查给出的是 `fix-then-merge` 或 `hold`;只有 `merge` 才算批准。 |
|
|
32
|
+
| `tip-moved` | `review.mode:required` 且审查之后分支发生变化;重新审查(或使用干净 rebase 的免审豁免)。 |
|
|
29
33
|
| `worker-busy` | 该 mission 的 roster agent 正在运行;等待或让其退役。 |
|
|
30
34
|
| `worker-status-unknown` | 父目录不可用,无法证明 live roster agent 已停止;等目录恢复后重试,或先 adopt。 |
|
|
31
35
|
| `base-wrong-branch` | 主检出不在记录的基线分支上。 |
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
| `worker-stale` | 该 mission 的 roster worker 超过 `spawn.staleMs` 没有目录活动;合并前 adopt(只有确实还在运行时才 interrupt)。 |
|
|
34
38
|
| `tests-missing` | 启用了 `gates.requireTests` 且没有针对当前分支 tip 的通过测试回执;运行 `tower_test` 或 `tower_complete`。 |
|
|
35
39
|
| `tests-failed` | 最新测试发现回执失败,或不满足 minCount/required 检查。修复测试并重跑。 |
|
|
36
|
-
| `review-exemption-invalid` | `review.
|
|
40
|
+
| `review-exemption-invalid` | `review.mode:required` 且 mission 请求 `reviewPolicy:"none"`,但 `gates.allowReviewExemption` 关闭(或缺少理由)。 |
|
|
37
41
|
| `merge-intent-pending` | 崩溃/失败前记录了合并意图;先运行 `tower_merge` 对账,再进行其他门禁。 |
|
|
38
42
|
|
|
39
43
|
## 合并失败(4 个)
|
|
@@ -27,13 +27,30 @@ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON
|
|
|
27
27
|
|
|
28
28
|
```json
|
|
29
29
|
{
|
|
30
|
-
"review": { "
|
|
30
|
+
"review": { "mode": "required", "maxRoundsPerReviewer": 3 },
|
|
31
31
|
"merge": { "timeoutMs": 1800000 },
|
|
32
32
|
"worktrees": { "root": "D:/tower-worktrees" },
|
|
33
33
|
"spawn": { "reviewerPreset": "tower-reviewer", "maxDepth": 2 }
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
`review.mode` 就是审查策略:`off`、`optional` 或 `required`。`review.requireReview` 是保留一个版本的
|
|
38
|
+
兼容别名(`true` → `required`,`false` → `optional`),请优先使用 `review.mode`。同一层同时设置两者时,
|
|
39
|
+
`requireReview:true` 与任何非 `required` 的 mode 冲突,会以 `config-invalid` 拒绝并同时点名两个键。
|
|
40
|
+
`requireReview:false` 与任何显式 mode 都不冲突:`off`、`optional`、`required` 都接受,且以 mode 为准
|
|
41
|
+
(`required` 仍然强制门禁;只有该 mode 才会把解析后的别名镜像回 `true`)。仓库层会独立解析:它可以放宽
|
|
42
|
+
审查,但若 profile 已是 `review.mode:off`,`tower_review` 根本没有注册,仓库层不能自行提高 mode。
|
|
43
|
+
|
|
44
|
+
插件页控件显示的正是运行时解析出的策略:`review.mode` 存在时以它为准;只设置了 `review.requireReview`
|
|
45
|
+
的配置会显示为「必需」(`true`)或「可选」(`false`)。保存任何选择都会在同一份 profile patch 中写入
|
|
46
|
+
`review.mode`,并把 `review.requireReview` 置为 `false`,因此该别名不可能与所选 mode 矛盾。
|
|
47
|
+
|
|
48
|
+
同样的三个选项也出现在插件页的 `dsh-tower` 配置区:**关闭**、**可选**(默认)和 **必需**,每个选项都有一行效果说明。
|
|
49
|
+
该控件通过宿主设置表单写入 profile 配置,因此无需手动编辑文件,页面就会接受并显示所选值。
|
|
50
|
+
|
|
51
|
+
但已挂载的工具面无法实时重建:启动时审查为关闭的 profile 没有注册 `tower_review`。通过页面切换到「可选」
|
|
52
|
+
或「必需」会立即改变合并门禁,但审查工具本身需要重启(或重新加载配置)后才会出现;出现这种情况后控件会给出提示。
|
|
53
|
+
|
|
37
54
|
`mode.*` 分组是会话级的,不是仓库级的:为兼容 schema,它可以在仓库文件中出现,但只有 profile
|
|
38
55
|
配置能控制 Tower 模式提示与模型切换开关。文件格式错误、值不是对象、未知顶层键,或违反声明 schema
|
|
39
56
|
的值(例如 `merge.timeoutMs` 小于 1000)会以稳定的 `config-invalid` / `config-unknown-key`
|
|
@@ -60,7 +77,8 @@ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON
|
|
|
60
77
|
| `mode.denyTools` | goal/todo/team/workflow/subagent 工具(默认 12 个) | 模式激活期间被拒绝的工具。 |
|
|
61
78
|
| `mode.storePath` | `$DSH_HOME/tower-mode` | `modes.json` 所在目录。 |
|
|
62
79
|
| `mode.maxEntries` | `200` | 最多持久化的模式条目数(1–2000)。 |
|
|
63
|
-
| `review.
|
|
80
|
+
| `review.mode` | `optional` | 审查策略。`off` = 审查被有意关闭(记录审查会被拒绝、`tower_spawn` 拒绝 reviewer 角色、合并门禁忽略审查;profile 级的 `off` 也不会注册 `tower_review`,仓库级的 `off` 则拒绝提交);`optional` = 审查可用但从不阻塞合并(默认);`required` = 绑定分支 tip 的 clean 审查是合并门禁(`no-review`/`not-clean`/`review-hold`/`review-not-approved`/`tip-moved`/`review-mission-mismatch`)。 |
|
|
81
|
+
| `review.requireReview` | `false` | `review.mode` 的兼容别名(已弃用):`true` → `required`,`false` → `optional`。请优先使用 `review.mode`。`true` 与非 `required` 的 `review.mode` 同时设置会以 `config-invalid` 拒绝并点名两个键;`false` 可与任何显式 `review.mode`(`off`/`optional`/`required`)共存,且不会覆盖它。插件页控件在该别名是唯一键时按它显示,并在保存时写入 `review.mode` 且把 `review.requireReview` 归一化为 `false`。 |
|
|
64
82
|
| `review.maxRoundsPerReviewer` | `5` | 每个 reviewer 对每条分支的审查轮数上限。 |
|
|
65
83
|
| `merge.timeoutMs` | `900000`(15 分钟) | `tower_merge` 默认超时,包含钩子;单次调用的 `timeoutMs` 优先。 |
|
|
66
84
|
| `merge.maxBufferBytes` | `67108864`(64 MB) | 合并输出缓冲区。 |
|
|
@@ -85,7 +103,7 @@ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON
|
|
|
85
103
|
| `tests.allowPackageDiscovery` | `true` | 允许仓库控制的 `tower.testDiscovery` 命令(会执行 worktree 代码;见安全页)。 |
|
|
86
104
|
| `gates.requireChecks` | `false` | 合并前要求至少一条 `tower_complete.checks` 证据(`no-check-evidence`)。 |
|
|
87
105
|
| `gates.requireTests` | `false` | 要求针对当前 mission 分支 tip 的通过测试回执(`tests-missing`);与审查门禁独立。`tower-tests` 组件行关闭时,规划/进入模式会带解释拒绝该组合。 |
|
|
88
|
-
| `gates.allowReviewExemption` | `false` | 在 `review.
|
|
106
|
+
| `gates.allowReviewExemption` | `false` | 在 `review.mode:required` 时,允许用 `reviewPolicy:"none"` + 非空理由跳过审查 blocker;默认故意关闭;`review.mode` 为 `off` 或 `optional` 时无影响(不会产生审查 blocker)。 |
|
|
89
107
|
| `gates.maxReportBytes` | `32768`(32 KiB) | `tower_complete.report` 的 UTF-8 字节硬上限。超出时以 `invalid-argument` 拒绝并给出可执行的 `nextAction`(把全文写进 mission worktree 的文件并引用路径),绝不截断。已存在的超限报告仍可读取。 |
|
|
90
108
|
| `docs.boardLocation` | `common` | `common` = `<git-common-dir>/dsh-tower/MISSIONS.md`;`repo` = `<repoRoot>/MISSIONS.md`。是否写出看板由 `tower-board` 组件行决定(#133);`docs.board` 已移除,会作为未知键被拒绝。 |
|
|
91
109
|
| `archive.afterDays` | `30` | 年龄触发:终态任务超过这么多天未更新,会在下次变更时归档(`0` 表示立即归档)。归档本身由 `tower-archive` 组件行决定(#135);`archive.enabled` 已移除,会作为未知键被拒绝。 |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[English](../../reference/errors.md) | **简体中文**
|
|
4
4
|
|
|
5
5
|
工具失败是抛出的错误,不是 `{ok:false}` 结果(门禁结果是例外:见 [blockers.md](blockers.md))。
|
|
6
|
-
每个抛出的错误都以稳定 code 作为消息前缀。本版本定义
|
|
6
|
+
每个抛出的错误都以稳定 code 作为消息前缀。本版本定义 36 个工具错误码。
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
[tower:mission-not-found] unknown mission "M9"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
| `provider-cooldown` | 连续可重试的 provider 失败后,spawn 进入短暂冷却。 | 等待 `blockedUntil` 后重试 |
|
|
36
36
|
| `preset-missing` | 配置的 agent preset 未注册。 | 缺少 `tower-worker`/`tower-reviewer` preset 行、DSH 未重启 |
|
|
37
37
|
| `reviewer-prerequisite` | reviewer spawn 的前置条件未满足。 | mission 尚未 `completed` 或还没有 worktree 就 spawn reviewer |
|
|
38
|
+
| `review-disabled` | 审查被 `review.mode:off` 关闭;该配置下没有 reviewer、审查结论或审查 blocker。 | `review.mode` 为 `off` 时以 `role:"reviewer"` 调用 `tower_spawn`,或提交审查 |
|
|
38
39
|
| `not-authorized` | 调用方 agent 角色无权执行该操作。 | worker 调用仅限控制塔的工具,或修改 mission 状态/scope;reviewer 修改 finding |
|
|
39
40
|
| `workspace-busy` | 另一个 Tower 会话持有该仓库的 workspace 锁。 | `mode.workspaceLock:true` 时第二个会话调用仓库级工具 |
|
|
40
41
|
| `worker-limit` | 仓库达到 `spawn.maxLiveWorkers` 个活跃 worker。 | 默认上限下 spawn 第五个 worker |
|
|
@@ -50,8 +50,8 @@ stdout/stderr 文件、SHA-256 哈希、退出码、计数、required 名称、w
|
|
|
50
50
|
- `tower_test({repoRoot,missionId})` 按需运行发现并记录回执。
|
|
51
51
|
- `tests.autoChecks` 开启时,`tower_complete` 会添加一条 `tests` 检查。
|
|
52
52
|
- `gates.requireChecks` 要求任意完成检查;`gates.requireTests` 要求针对当前 mission 分支 tip 的
|
|
53
|
-
通过回执(`tests-missing
|
|
54
|
-
|
|
53
|
+
通过回执(`tests-missing`)。测试门禁与审查独立:即使策略为 `review.mode:optional`(默认)或
|
|
54
|
+
`off`,测试门禁仍检查分支 tip。
|
|
55
55
|
|
|
56
56
|
## 信任边界
|
|
57
57
|
|
|
@@ -37,7 +37,7 @@ roster 角色由 `exec.agent` 与 roster 共同决定。mission 级工具(`tow
|
|
|
37
37
|
| `tower_plan` | `missions[]`(`title`——可打印 Unicode,NFC 规范化,≤120 字符、`scope[]`、`tasks[]?`、`deps[]?`、`kind?`、`context?`、`references[]?` ≤20、`reviewPolicy?`、`reviewExemptionReason?`) | 创建 mission `M1..`;重叠的 build scope 会自动加依赖;拒绝未知依赖与依赖循环。mission id 保持 ASCII(`M1`、`M2`);不可见字符与超长标题会以 `invalid-argument` 拒绝。`reviewPolicy:"none"` 需要非空理由;审查门禁开启时,preflight 还要求 `gates.allowReviewExemption`。 |
|
|
38
38
|
| `tower_mission` | `id`(别名 `missionId`)、`status?`、`task_done?`、`task_drop?`+`task_drop_reason?`、`blocker?`、`clear_blockers?`、`note?`、`scope?`、`pause_cause?`、`authorization?` | 更新单个 mission。worker 可安全使用的字段是 task/blocker/note;不能在这里设置 `merged`。暂停的 mission 记录 `pause_cause`(user/failure/resource/dependency);恢复用户暂停的 mission 会遵循 `mode.requireResumeAuthorization`。 |
|
|
39
39
|
| `tower_status` | `missionId?`、`offset?`、`limit?`、`includeGate?`、`format?` | 默认返回面板。分页 mission 只带 `reportBytes`;`missionId` 返回完整详情(任务/备注/审查/finding/完成报告正文)。`offset`/`limit` 对 mission 分页(默认 20,最大 100),`includeGate` 附带门禁摘要,`format:"markdown"` 返回看板。host 提供 mode hook 时还会包含 `mode.lastChange` 与最近 `history`。还返回仓库级 git 互斥锁 `gitLock`(`{held, pid, phase, heldMs}`,空闲时 `{held:false}`)。 |
|
|
40
|
-
| `tower_spawn` | `missionId`(别名 `id`)、`role`(`worker`/`reviewer`)、`name?`、`instructions?`、`worktreeRoot?`、`wait?`、`waitMs?`、`takeover?`、`includeDirtyBase?` | 准备或复用 worktree 并启动子代理。worker 使用 `tower-worker`;reviewer 要求 mission 为 `completed`,并使用只读的 `tower-reviewer
|
|
40
|
+
| `tower_spawn` | `missionId`(别名 `id`)、`role`(`worker`/`reviewer`)、`name?`、`instructions?`、`worktreeRoot?`、`wait?`、`waitMs?`、`takeover?`、`includeDirtyBase?` | 准备或复用 worktree 并启动子代理。worker 使用 `tower-worker`;reviewer 要求 mission 为 `completed`,并使用只读的 `tower-reviewer`;`review.mode:off` 时 reviewer 角色会以 `[tower:review-disabled]` 被拒绝。`takeover:true` 替换已不再运行的 worker 条目(ownerGeneration 递增)。每个 mission 只能有一个活跃 worker;`spawn.maxLiveWorkers` 限制整个仓库。 |
|
|
41
41
|
| `tower_roster` | — | roster 及来自子代理目录的实时活动(`running`/`inactive`/`missing`/`unknown`)与 `stale` 标记。只读。 |
|
|
42
42
|
| `tower_adopt` | `retireMissing?` | 把 roster 条目与持久子代理目录对账。可证明已消失的 agent 会被标记为 retired(默认),或标记为 `missing`(`retireMissing:false`)以便检查/接管。 |
|
|
43
43
|
| `tower_diff` | `missionId`(别名 `id`)、`maxBytes?` | 只读 merge-base diff:文件、提交、有上限的 patch。供 reviewer 安全使用。 |
|
|
@@ -47,8 +47,8 @@ roster 角色由 `exec.agent` 与 roster 共同决定。mission 级工具(`tow
|
|
|
47
47
|
| `tower_interrupt` | `to?`(roster 名称)、`agentId?`、`reason?` | 通过 `subagents.interrupt` 中断一个运行中的 roster agent,并记录 `state:"interrupted"`。幂等(未运行 → `interrupted:false`)。 |
|
|
48
48
|
| `tower_finding` | `type`、`title`、`summary`、`details`、`suggested_fix`、`severity?`、`missionId?`、`location?` | 记录 scope 之外的问题;reviewer 用它代替直接改文件。新 finding 初始为 `open`。 |
|
|
49
49
|
| `tower_findings` | `id?`、`status?`、`note?`、过滤(`type?`、`severity?`、`missionId?`、`open?`、`limit?`) | 生命周期工具:列出/读取/流转。状态为 `open → triaged → fixed`/`wontfix`,允许重新打开。只有控制塔或 worker 能修改;reviewer 只能列出/读取。 |
|
|
50
|
-
| `tower_review` | `missionId`(别名 `id`)、`status`(`clean`/`p1-Nitems`/`p2-Nitems`)、`merge`(`merge`/`fix-then-merge`/`hold`)、`findings`、`decision`、`checks?`、`breakGlassReason?` | 绑定当前 tip 的 reviewer 结论。reviewer 必须是获派的 reviewer(或由控制塔使用 break-glass
|
|
51
|
-
| `tower_preflight` | `missionId`(别名 `id`) | 只读门禁:返回 `{ok, blockers[], branchTip, baseTip, changedFiles, review}
|
|
50
|
+
| `tower_review` | `missionId`(别名 `id`)、`status`(`clean`/`p1-Nitems`/`p2-Nitems`)、`merge`(`merge`/`fix-then-merge`/`hold`)、`findings`、`decision`、`checks?`、`breakGlassReason?` | 绑定当前 tip 的 reviewer 结论。reviewer 必须是获派的 reviewer(或由控制塔使用 break-glass)。`review.mode:off` 时不注册;仓库层为 `off` 时提交会以 `[tower:review-disabled]` 被拒绝。非 clean 结论使 mission 回到 active 后,其他 route 仍可为同一 reviewed tip 提交结论:追加到同一轮(`appended:true`),不再改变状态;tip 已移动时会被明确拒绝。 |
|
|
51
|
+
| `tower_preflight` | `missionId`(别名 `id`) | 只读门禁:返回 `{ok, blockers[], branchTip, baseTip, changedFiles, review}`。审查 blocker 只在 `review.mode:required` 时出现;`off`/`optional` 会跳过。 |
|
|
52
52
|
| `tower_merge` | `missionId`(别名 `id`)、`timeoutMs?`(别名 `timeout_ms`) | 门禁全绿后合并。若合并提交已落地则记录(`reconciled`);若命令失败但提交已存在则恢复(`recoveredFromFailure`);区分 `merge-hook-failed` 与 `merge-conflict`。 |
|
|
53
53
|
| `tower_rebase` | `missionId`(别名 `id`) | 在 worktree 内把分支 rebase 到基线。worker 运行中或 worktree 脏时拒绝。分支已包含在基线中时返回 `alreadyMerged` 且保持分支不动。 |
|
|
54
54
|
| `tower_teardown` | `force?`、`exclude?`、`dry_run?`(别名 `dryRun`) | 移除符合条件的 merged/abandoned worktree 与分支;运行中的 agent 始终保留。 |
|
package/domain/components.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// Principle 11 says optional capabilities are switches and the core is not. This
|
|
4
4
|
// module is that policy as data plus the pure checks over it: the component rows
|
|
5
5
|
// in `cordis.patch.yml`, the four-way classification of every config key, and
|
|
6
|
-
// the forbidden-to-disable capabilities (host, health, recovery,
|
|
7
|
-
//
|
|
6
|
+
// the forbidden-to-disable capabilities (host, health, recovery, merge gates)
|
|
7
|
+
// that no group, row or boolean key may gate. The review policy is not one of
|
|
8
|
+
// those never-switches: `review.mode` is the one tri-state control, and only the
|
|
9
|
+
// gate it turns on in `required` mode (evidence, tip binding, exemption path) is
|
|
10
|
+
// non-disableable.
|
|
8
11
|
//
|
|
9
12
|
// The single source of truth is `scripts/component-surface.json`. The generated
|
|
10
13
|
// reference `docs/reference/component-surface.md` is a rendering of it, so
|
|
@@ -700,8 +703,13 @@ ${componentRows}
|
|
|
700
703
|
|
|
701
704
|
## Forbidden to disable
|
|
702
705
|
|
|
703
|
-
The host, the health contract, the recovery escape
|
|
704
|
-
|
|
706
|
+
The host, the health contract, the recovery escape and the merge gates are
|
|
707
|
+
configuration or code, never switches. The review gate in \`required\` mode is
|
|
708
|
+
also non-disableable: a completed mission still needs the review evidence bound
|
|
709
|
+
to the branch tip, and the exemption path stays explicit. The policy itself is
|
|
710
|
+
not a never-switch — \`review.mode\` is the one tri-state control on the
|
|
711
|
+
plugin-page config form (\`off\`, \`optional\` or \`required\`) — and only the gate it
|
|
712
|
+
turns on in \`required\` mode is non-disableable. \`npm run
|
|
705
713
|
docs:check:components\` derives the live disablers from \`host/tool-registry.js\`
|
|
706
714
|
and \`cordis.patch.yml\`; a disabler that is not recorded here fails the gate.
|
|
707
715
|
|
package/domain/config/schema.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import Schema from '@deepseek-ai/schemastery'
|
|
17
17
|
import { TowerToolError } from '../errors.js'
|
|
18
18
|
import { DEFAULT_MAX_REPORT_BYTES } from '../missions/completion.js'
|
|
19
|
+
import { materializeVolatile, resolveReviewPolicy } from '../integration/review.js'
|
|
19
20
|
|
|
20
21
|
export const DEFAULT_DENY_TOOLS = [
|
|
21
22
|
'create_goal',
|
|
@@ -57,7 +58,7 @@ const SECTION_KEYS = {
|
|
|
57
58
|
'maxEntries',
|
|
58
59
|
'lockTimeoutMs',
|
|
59
60
|
],
|
|
60
|
-
review: ['requireReview', 'maxRoundsPerReviewer'],
|
|
61
|
+
review: ['mode', 'requireReview', 'maxRoundsPerReviewer'],
|
|
61
62
|
merge: ['timeoutMs', 'maxBufferBytes'],
|
|
62
63
|
git: ['timeoutMs'],
|
|
63
64
|
worktrees: ['root'],
|
|
@@ -169,13 +170,35 @@ export const Config = Schema.object({
|
|
|
169
170
|
.default({})
|
|
170
171
|
.description('Tower-mode behavior and persistence.'),
|
|
171
172
|
review: Schema.object({
|
|
173
|
+
// #M39: deliberately *no* schema default here. Cordis resolves the schema
|
|
174
|
+
// before `apply()` on some paths, and a materialized `mode:'optional'` would
|
|
175
|
+
// sit next to a legacy `review.requireReview:true` and look like a user
|
|
176
|
+
// contradiction. The resolver (`resolveTowerConfig`) is the single place
|
|
177
|
+
// that applies the `optional` default, after alias resolution.
|
|
178
|
+
// #M40: `.volatile()` is what makes a field editable from the DSH
|
|
179
|
+
// Plugins-page settings form. The form projection keeps only fields under a
|
|
180
|
+
// volatile ancestor, and Cordis then hands the runtime a live reference
|
|
181
|
+
// that is updated in place on a settings write (see host/config.js).
|
|
182
|
+
//
|
|
183
|
+
// Both review keys are volatile on purpose: the page must show the
|
|
184
|
+
// *resolved* policy, and `resolveReviewPolicy` lets the deprecated alias
|
|
185
|
+
// decide when `mode` is absent. A non-volatile alias would be dropped by the
|
|
186
|
+
// projection, so a profile that sets only `review.requireReview:true` would
|
|
187
|
+
// render as Optional (and a save could persist a contradiction). The group
|
|
188
|
+
// itself stays non-volatile so the tuning keys remain ordinary config.
|
|
189
|
+
mode: Schema.union(['off', 'optional', 'required'])
|
|
190
|
+
.volatile()
|
|
191
|
+
.description(
|
|
192
|
+
'Review policy. off = review is intentionally unavailable (recording a review is refused, reviewer spawn is refused, and the merge gate ignores review; a profile-level off also does not register tower_review); optional = review is available but never blocks a merge (default); required = a clean review bound to the branch tip is a merge blocker.',
|
|
193
|
+
),
|
|
172
194
|
requireReview: Schema.boolean()
|
|
173
195
|
.default(false)
|
|
174
|
-
.
|
|
196
|
+
.volatile()
|
|
197
|
+
.description('Deprecated alias for review.mode (true -> required, false -> optional); prefer review.mode. true next to a non-required mode is rejected; false never overrides an explicit mode (off, optional or required). The Plugins-page control shows the alias when it is the only key and always writes review.mode with review.requireReview:false.'),
|
|
175
198
|
maxRoundsPerReviewer: Schema.natural().min(1).default(5).description('Review-round cap per reviewer per mission branch.'),
|
|
176
199
|
})
|
|
177
200
|
.default({})
|
|
178
|
-
.description('Review policy.'),
|
|
201
|
+
.description('Review policy: off, optional (default) or required.'),
|
|
179
202
|
merge: Schema.object({
|
|
180
203
|
timeoutMs: Schema.natural().min(1000).default(900000).description('Default tower_merge timeout, including pre-merge hooks.'),
|
|
181
204
|
maxBufferBytes: Schema.natural().min(65536).default(67108864).description('Captured merge output buffer.'),
|
|
@@ -432,7 +455,10 @@ export function assertSafeKeys(value, label) {
|
|
|
432
455
|
|
|
433
456
|
/** @param {any} [rawConfig] */
|
|
434
457
|
export function resolveTowerConfig(rawConfig) {
|
|
435
|
-
|
|
458
|
+
// #M40: the profile config Cordis hands `apply()` carries `review.mode` as a
|
|
459
|
+
// live volatile reference. Materialize before validation so the value checked
|
|
460
|
+
// here is the current one; the reference itself stays live in host/config.js.
|
|
461
|
+
const raw = materializeVolatile(rawConfig ?? {})
|
|
436
462
|
if (raw !== null && typeof raw !== 'object') throw new TowerToolError('config-invalid', 'Tower config must be an object')
|
|
437
463
|
assertKnownKeys(raw, 'Tower config')
|
|
438
464
|
let parsed
|
|
@@ -464,11 +490,20 @@ export function resolveTowerConfig(rawConfig) {
|
|
|
464
490
|
// `resolve(raw)` for every valid config without changing any resolved value.
|
|
465
491
|
if (mode.promptSection === undefined) mode.promptSection = ''
|
|
466
492
|
if (!Array.isArray(mode.denyTools) || mode.denyTools.length === 0) mode.denyTools = [...DEFAULT_DENY_TOOLS]
|
|
493
|
+
// #M39: resolve the review policy here so every consumer reads one shape.
|
|
494
|
+
// `review.mode` is canonical; the deprecated `review.requireReview` boolean is
|
|
495
|
+
// materialized from it (true only for `required`), which keeps older readers
|
|
496
|
+
// working and makes `resolve(resolve(raw))` a fixed point — the mirrored alias
|
|
497
|
+
// is compatible with the mode it came from (domain/integration/review.js).
|
|
498
|
+
const reviewPolicy = resolveReviewPolicy(raw.review)
|
|
499
|
+
const review = clone(parsed.review ?? {})
|
|
500
|
+
review.mode = reviewPolicy.mode
|
|
501
|
+
review.requireReview = reviewPolicy.mode === 'required'
|
|
467
502
|
return {
|
|
468
503
|
section: mode.promptSection,
|
|
469
504
|
init: clone(parsed.init ?? {}),
|
|
470
505
|
mode,
|
|
471
|
-
review
|
|
506
|
+
review,
|
|
472
507
|
merge: clone(parsed.merge ?? {}),
|
|
473
508
|
git: clone(parsed.git ?? {}),
|
|
474
509
|
worktrees: clone(parsed.worktrees ?? {}),
|
|
@@ -87,6 +87,7 @@ const SCHEMAS = {
|
|
|
87
87
|
gitLock: S.obj({ held: S.bool, pid: S.any, phase: S.any, heldMs: S.any }),
|
|
88
88
|
mode: S.any,
|
|
89
89
|
spawn: S.any,
|
|
90
|
+
review: S.any,
|
|
90
91
|
towerAgentId: S.any,
|
|
91
92
|
ownerMissing: S.bool,
|
|
92
93
|
},
|
|
@@ -259,6 +260,7 @@ const SCHEMAS = {
|
|
|
259
260
|
}),
|
|
260
261
|
reports: S.any,
|
|
261
262
|
surface: S.any,
|
|
263
|
+
review: S.any,
|
|
262
264
|
archive: S.obj({
|
|
263
265
|
missions: S.any,
|
|
264
266
|
archivedMissions: S.int,
|
|
@@ -21,7 +21,7 @@ export const PlanInput = Type.Object({
|
|
|
21
21
|
kind: Type.Optional(enumOf(MISSION_KINDS, { description: 'survey = read-only investigation that reserves no scope.' })),
|
|
22
22
|
context: Type.Optional(Type.String({ description: "The user's own key sentences, verbatim (max 4096 chars)." })),
|
|
23
23
|
references: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: 'Read-only pointers/files the worker should read (max 20); copied into the briefing.' })),
|
|
24
|
-
reviewPolicy: Type.Optional(enumOf(REVIEW_POLICIES, { description: 'required-clean (mission default) means a clean review is required when review.
|
|
24
|
+
reviewPolicy: Type.Optional(enumOf(REVIEW_POLICIES, { description: 'required-clean (mission default) means a clean review is required when review.mode is required; none needs reviewExemptionReason, and with the review gate on it also needs gates.allowReviewExemption.' })),
|
|
25
25
|
reviewExemptionReason: Type.Optional(Type.String({ description: 'Non-empty justification when reviewPolicy is "none".' })),
|
|
26
26
|
}, { additionalProperties: false }),
|
|
27
27
|
{ description: 'Missions to create, in intended order.' },
|
package/domain/errors.js
CHANGED
|
@@ -181,6 +181,12 @@ export const ERROR_CODES = {
|
|
|
181
181
|
retryable: false,
|
|
182
182
|
nextAction: 'complete the mission first, then spawn the reviewer',
|
|
183
183
|
},
|
|
184
|
+
'review-disabled': {
|
|
185
|
+
description: 'Review is disabled by `review.mode:off`; no reviewer, verdict, or review blocker exists in this configuration.',
|
|
186
|
+
trigger: '`tower_spawn` with `role:"reviewer"`, or a review submission, while `review.mode` is `off`',
|
|
187
|
+
retryable: false,
|
|
188
|
+
nextAction: 'set review.mode to optional or required in the Tower config, then retry',
|
|
189
|
+
},
|
|
184
190
|
'not-authorized': {
|
|
185
191
|
description: 'The calling agent role may not perform this action.',
|
|
186
192
|
trigger: 'a worker calling a tower-only tool or changing mission status/scope; a reviewer mutating findings',
|
package/domain/gate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure completion gate checks (#49). No repository I/O.
|
|
2
2
|
// @ts-check
|
|
3
3
|
import { isTerminal } from './mission.js'
|
|
4
|
-
import { isReviewExempt, reviewExemptionInvalid } from './integration/review.js'
|
|
4
|
+
import { isReviewExempt, reviewExemptionInvalid, reviewRequired } from './integration/review.js'
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../types/index.js').Blocker} Blocker */
|
|
7
7
|
/** @typedef {import('../types/index.js').MergeGateOptions} MergeGateOptions */
|
|
@@ -74,13 +74,16 @@ export function mergeGateBlockers({
|
|
|
74
74
|
blockers.push({ code, detail })
|
|
75
75
|
}
|
|
76
76
|
const inactive = new Set(inactiveStates)
|
|
77
|
-
|
|
77
|
+
// #M39: only `review.mode:required` produces review blockers. `optional`
|
|
78
|
+
// records reviews without gating, and `off` disables review entirely (no
|
|
79
|
+
// tower_review, no reviewer spawn), so both skip this whole block.
|
|
80
|
+
const requireReview = reviewRequired(config)
|
|
78
81
|
const exempt = isReviewExempt(mission, config)
|
|
79
82
|
if (reviewExemptionInvalid(mission, config)) {
|
|
80
83
|
push('review-exemption-invalid', `mission ${mission.id} requests no review but gates.allowReviewExemption is disabled`)
|
|
81
84
|
}
|
|
82
85
|
if (requireReview && !exempt && !review) {
|
|
83
|
-
push('no-review', `branch ${branch ?? '(none)'} has no review; assign a reviewer first (or set review.
|
|
86
|
+
push('no-review', `branch ${branch ?? '(none)'} has no review; assign a reviewer first (or set review.mode:optional)`)
|
|
84
87
|
} else if (requireReview && !exempt) {
|
|
85
88
|
const latest = /** @type {Review} */ (review)
|
|
86
89
|
if (latest.missionId !== mission.id) push('review-mission-mismatch', `latest review belongs to mission ${latest.missionId}`)
|