dsh-tower 1.3.0 → 2.0.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 (57) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.en.md +22 -8
  4. package/README.md +20 -8
  5. package/cordis.patch.yml +10 -0
  6. package/docs/README.md +8 -4
  7. package/docs/architecture/overview.md +13 -9
  8. package/docs/faq.md +33 -2
  9. package/docs/guide/hot-reload.md +189 -0
  10. package/docs/guide/install-upgrade-migration.md +11 -0
  11. package/docs/guide/quickstart.md +33 -1
  12. package/docs/operations/runbook.md +23 -0
  13. package/docs/operations/security.md +23 -0
  14. package/docs/reference/blockers.md +10 -2
  15. package/docs/reference/config.md +65 -2
  16. package/docs/reference/doctor.md +31 -0
  17. package/docs/reference/errors.md +50 -0
  18. package/docs/reference/events.md +45 -0
  19. package/docs/reference/operations.md +52 -0
  20. package/docs/reference/testing.md +61 -0
  21. package/docs/reference/tools.md +65 -23
  22. package/docs/zh/README.md +58 -0
  23. package/docs/zh/faq.md +72 -0
  24. package/docs/zh/guide/hot-reload.md +178 -0
  25. package/docs/zh/guide/install-upgrade-migration.md +57 -0
  26. package/docs/zh/guide/quickstart.md +96 -0
  27. package/docs/zh/operations/security.md +42 -0
  28. package/docs/zh/reference/blockers.md +62 -0
  29. package/docs/zh/reference/config.md +113 -0
  30. package/docs/zh/reference/doctor.md +30 -0
  31. package/docs/zh/reference/errors.md +52 -0
  32. package/docs/zh/reference/events.md +46 -0
  33. package/docs/zh/reference/operations.md +48 -0
  34. package/docs/zh/reference/testing.md +60 -0
  35. package/docs/zh/reference/tools.md +75 -0
  36. package/index.js +261 -13
  37. package/lib/batch.js +578 -0
  38. package/lib/config.js +270 -8
  39. package/lib/doctor.js +165 -0
  40. package/lib/errors.js +111 -0
  41. package/lib/hmr.js +196 -0
  42. package/lib/leases.js +317 -0
  43. package/lib/legacy.js +413 -0
  44. package/lib/modeStore.js +28 -1
  45. package/lib/store.js +24 -1
  46. package/lib/tests.js +628 -0
  47. package/lib/tools.js +1554 -182
  48. package/lib/tower.js +957 -102
  49. package/lib/workspaceLock.js +117 -0
  50. package/lib/worktreeOwnership.js +166 -0
  51. package/package.json +4 -1
  52. package/reviewer.js +1 -0
  53. package/scripts/check-docs.mjs +135 -0
  54. package/scripts/enable-dev-hmr.mjs +347 -0
  55. package/skills/tower-review.md +4 -2
  56. package/skills/tower-worker-protocol.md +3 -1
  57. package/worker.js +4 -2
@@ -0,0 +1,113 @@
1
+ # 配置参考
2
+
3
+ [English](../../reference/config.md) | **简体中文**
4
+
5
+ 每个键都是可选的;默认值即随包行为。未知顶层键会被拒绝。
6
+ `section` 与 `allowAgentControl` 是 `mode.promptSection` 与 `mode.allowAgentControl` 的兼容别名。
7
+ 兼容别名只用于补缺:同时设置两种写法时,规范的 `mode.*` 键优先。
8
+
9
+ ## 层级与优先级
10
+
11
+ 从低到高:
12
+
13
+ 1. 代码默认值(下表中的默认值),
14
+ 2. profile 配置(挂载 `dsh-tower` 的 Cordis patch 传给 `apply()` 的配置),
15
+ 3. 仓库配置 `<git-common-dir>/dsh-tower/config.json`(通常是 `.git/dsh-tower/config.json`),
16
+ 4. 单次工具参数,例如 `worktreeRoot` 与 `timeoutMs`。
17
+
18
+ 每次 Tower 工具打开仓库时都会读取仓库配置层,因此仓库可以在不修改 profile 的情况下调整 review、
19
+ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON 形状(相同的分组与兼容别名),
20
+ 并按分组合并:仓库设置的键优先,仓库未设置的 profile 键保留。`tower_status` 会报告
21
+ `config.path` 与 `config.repoConfigPresent`。
22
+
23
+ ```json
24
+ {
25
+ "review": { "maxRoundsPerReviewer": 3 },
26
+ "merge": { "timeoutMs": 1800000 },
27
+ "worktrees": { "root": "D:/tower-worktrees" },
28
+ "spawn": { "reviewerPreset": "tower-reviewer", "maxDepth": 2 }
29
+ }
30
+ ```
31
+
32
+ `mode.*` 分组是会话级的,不是仓库级的:为兼容 schema,它可以在仓库文件中出现,但只有 profile
33
+ 配置能控制 Tower 模式提示与模型切换开关。文件格式错误、值不是对象、未知顶层键,或违反声明 schema
34
+ 的值(例如 `merge.timeoutMs` 小于 1000)会以稳定的 `config-invalid` / `config-unknown-key`
35
+ 工具错误码失败。
36
+
37
+ | 键 | 默认值 | 含义 |
38
+ |---|---|---|
39
+ | `section` | `''` | 兼容别名:替换整段 Tower 模式提示文案。空值保留内置提醒。 |
40
+ | `allowAgentControl` | `false`(随包 patch:`true`) | 兼容别名:注册 `tower_control`。 |
41
+ | `init.autoGitInit` | `false`(随包 patch:`true`) | 当 `repoRoot` 是绝对、已存在但不是仓库的目录时,`tower_init` 可以执行 `git init` 并创建一个空的初始提交。 |
42
+ | `init.snapshotDirty` | `false` | `tower_init` 可以把未提交的**已跟踪**改动快照为游离提交作为 mission 基线(不含未跟踪文件;该提交通过 `refs/tower/snapshot/*` 保持存活)。 |
43
+ | `mode.promptSection` | `''` | 替换用的提示文案(与 `section` 同时设置时优先)。 |
44
+ | `mode.sectionAppend` | `''` | 追加在提醒之后的额外文本(团队约定)。 |
45
+ | `mode.allowAgentControl` | `false` | 注册 `tower_control`;模型可带理由进入/退出。 |
46
+ | `mode.autoPauseGoal` | `true` | 进入模式时暂停活跃 goal;退出时不会自动恢复。 |
47
+ | `mode.requireResumeAuthorization` | `false` | 恢复因 `pauseCause:user` 暂停的 mission 时,必须提供 `authorization` 引用。 |
48
+ | `mode.workspaceLock` | `false`(随包 patch:`true`) | 每个仓库只允许一个 Tower 会话(`tower.lock`);其他会话得到 `workspace-busy`。 |
49
+ | `mode.workspaceLockScope` | `all` | `all` 锁定所有仓库工具;`mutations` 对只读工具跳过 workspace 锁。 |
50
+ | `mode.planModePolicy` | `clear` | 进入 Tower 模式会清除活跃的 plan mode(或 `refuse` 直到退出计划);前置步骤也会清除稍后激活的计划。 |
51
+ | `mode.workspaceStaleMs` | `300000` | 超过该时长的 workspace 锁可以被接管。 |
52
+ | `mode.heartbeatMs` | `10000` | workspace 锁的心跳间隔。 |
53
+ | `mode.denyTools` | goal/todo/team/workflow/subagent 工具(默认 12 个) | 模式激活期间被拒绝的工具。 |
54
+ | `mode.storePath` | `$DSH_HOME/tower-mode` | `modes.json` 所在目录。 |
55
+ | `mode.maxEntries` | `200` | 最多持久化的模式条目数(1–2000)。 |
56
+ | `review.maxRoundsPerReviewer` | `5` | 每个 reviewer 对每条分支的审查轮数上限。 |
57
+ | `merge.timeoutMs` | `900000`(15 分钟) | `tower_merge` 默认超时,包含钩子;单次调用的 `timeoutMs` 优先。 |
58
+ | `merge.maxBufferBytes` | `67108864`(64 MB) | 合并输出缓冲区。 |
59
+ | `worktrees.root` | `''` | 默认 worktree 父目录;空值推导为 `<主检出父目录>/.dsh-tower-worktrees/<hash>`。 |
60
+ | `spawn.workerPreset` | `tower-worker` | `role=worker` 使用的 preset。 |
61
+ | `spawn.reviewerPreset` | `tower-reviewer` | `role=reviewer` 使用的 preset。 |
62
+ | `spawn.maxDepth` | `1` | spawn 时请求的子代理深度。 |
63
+ | `spawn.provider` | `''` | 子代理 provider id;空值优先 `spawn`,否则取第一个 provider。 |
64
+ | `spawn.workerModel` | `{}` | worker 可选模型路由 `{provider, model, reasoningEffort, maxTokens}`。 |
65
+ | `spawn.reviewerModel` | `{}` | reviewer 可选模型路由;使用不同模型可获得更强的独立审查。 |
66
+ | `spawn.maxLiveWorkers` | `4` | 每个仓库同时存活的 worker 上限(空闲 `inactive`、failed/interrupted/missing/stale/retired 不计入);超过返回 `worker-limit`。 |
67
+ | `spawn.staleMs` | `15000` | 超过该时长没有目录活动的 roster 条目会被标记为 `stale` 并阻止合并(`worker-stale`);0 关闭。 |
68
+ | `spawn.maxWaitMs` | `90000` | `tower_spawn.waitMs` 的上限(与 Kimi 兼容的 90s 默认值)。 |
69
+ | `spawn.requireLease` | `''` | worker spawn 前必须持有的可选租约名。 |
70
+ | `send.maxPerMinute` | `120` | 每个仓库的 `tower_send` 每分钟速率限制(0 关闭);超出返回 `rate-limited`。 |
71
+ | `send.interruptDefault` | `false`(随包 patch:`true`) | `tower_send.interrupt` 的默认值(投递前先中断运行中的回合)。 |
72
+ | `tests.enabled` | `true` | worktree 的 `package.json` 配置了测试发现时运行它。 |
73
+ | `tests.admission` | `complete` | `spawn` 在启动 worker 前运行发现(fail-closed,不启动子代理);`complete` 在 `tower_complete` 时运行;`off` 只在显式 `tower_test` 时运行。 |
74
+ | `tests.timeoutMs` | `60000` | 每条发现命令的超时。 |
75
+ | `tests.maxBytes` | `16777216`(16 MiB) | 每条命令捕获的 stdout 上限。 |
76
+ | `tests.autoChecks` | `true` | 向完成报告附加自动 `tests` 检查。 |
77
+ | `tests.allowPackageDiscovery` | `true` | 允许仓库控制的 `tower.testDiscovery` 命令(会执行 worktree 代码;见安全页)。 |
78
+ | `gates.requireChecks` | `false` | 合并前要求至少一条 `tower_complete.checks` 证据(`no-check-evidence`)。 |
79
+ | `gates.requireTests` | `false` | 要求针对被审查 tip 的通过测试回执(`tests-missing`)。 |
80
+ | `gates.allowReviewExemption` | `false` | 允许用 `reviewPolicy:"none"` + 理由跳过审查 blocker;默认故意关闭。 |
81
+ | `docs.board` | `true` | 变更后写入 `MISSIONS.md`。 |
82
+ | `docs.boardLocation` | `common` | `common` = `<git-common-dir>/dsh-tower/MISSIONS.md`;`repo` = `<repoRoot>/MISSIONS.md`。 |
83
+ | `leases.ttlMs` | `900000` | 默认咨询式租约寿命;过期不会自动释放。 |
84
+ | `dev.hotReload` | `true` | 注册 `tower_reload`;关闭时 `/tower reload` 也被禁用。无 HMR 服务时调用返回 `reload-unavailable`。 |
85
+ | `dev.reloadDelayMs` | `50` | 计划中的 reload 延迟,先让工具结果送达。 |
86
+ | `dev.reloadRequiredIds` | 四个 Tower loader id | 作为 required 传给 `reconcileProfile` 的条目 id。 |
87
+
88
+ ## 示例
89
+
90
+ ```yaml
91
+ - id: tower
92
+ name: 'dsh-tower'
93
+ config:
94
+ mode:
95
+ allowAgentControl: true
96
+ sectionAppend: 'Team conventions: tests are mandatory.'
97
+ workspaceLock: true
98
+ review:
99
+ maxRoundsPerReviewer: 3
100
+ merge:
101
+ timeoutMs: 1800000
102
+ spawn:
103
+ reviewerModel:
104
+ provider: deepseek-account
105
+ model: deepseek-flash
106
+ dev:
107
+ hotReload: true
108
+ ```
109
+
110
+ ## 查看实际生效的 schema
111
+
112
+ 插件声明 `Config` 后(本版本已声明),用 `cordis_inspect_query` 调用
113
+ `Config.listConfigs({ name: "dsh-tower" })` 可返回投影后的 JSON Schema。
@@ -0,0 +1,30 @@
1
+ # Doctor
2
+
3
+ [English](../../reference/doctor.md) | **简体中文**
4
+
5
+ `tower_doctor({repoRoot})` 是只读的。它从不获取 state 锁、写状态或改动 Git。它返回:
6
+
7
+ ```json
8
+ {
9
+ "ok": true,
10
+ "checks": [
11
+ { "id": "git", "status": "pass", "detail": "git version 2.55.0" },
12
+ { "id": "state", "status": "pass", "detail": ".../state.json revision 7" }
13
+ ],
14
+ "environmentContract": {
15
+ "platform": "win32",
16
+ "gitVersion": "git version 2.55.0",
17
+ "repoRoot": "D:/repo",
18
+ "commonDir": "D:/repo/.git",
19
+ "stateDir": "D:/repo/.git/dsh-tower"
20
+ }
21
+ }
22
+ ```
23
+
24
+ 检查覆盖:git 可用性、仓库身份、state schema/加载、仓库配置是否存在、worker/reviewer preset 名称、
25
+ subagents 与 HMR 服务、workspace 锁持有者/是否过期、实时 roster、worktree 是否存在/归属/是否脏、
26
+ test-discovery 策略、旧版快照、batch 与租约。`warn` 是提示;`fail` 表示仓库在 spawn/merge 前需要
27
+ 处理。state 加载失败会作为失败的检查报告而不是抛出,因此 doctor 在损坏的仓库上仍然有用。
28
+
29
+ 环境契约仅供参考;它不是身份或授权证明。`tower_init` 仍要求控制塔角色,`tower_spawn` 仍会检查
30
+ 实时子代理目录。
@@ -0,0 +1,52 @@
1
+ # 工具错误码
2
+
3
+ [English](../../reference/errors.md) | **简体中文**
4
+
5
+ 工具失败是抛出的错误,不是 `{ok:false}` 结果(门禁结果是例外:见 [blockers.md](blockers.md))。
6
+ 每个抛出的错误都以稳定 code 作为消息前缀:
7
+
8
+ ```
9
+ [tower:mission-not-found] unknown mission "M9"
10
+ ```
11
+
12
+ 调用方可以匹配前缀,而不必匹配文案。下面的 code 保留给对应条件;没有更具体匹配的消息会报告为
13
+ `tool-internal`。
14
+
15
+ | Code | 含义 | 典型触发 |
16
+ |---|---|---|
17
+ | `invalid-argument` | 工具参数缺失、格式错误或不被允许。 | 缺少 `missionId`/`id`;未知状态;空 report;非法 scope glob;`timeoutMs < 1000`;更新时传入 `status: "all"`。 |
18
+ | `repo-invalid` | `repoRoot` 缺失、是相对路径,或不在 Git 仓库内。 | 传入的路径所在目录不是仓库 |
19
+ | `not-initialized` | 该仓库尚未初始化 Tower。 | 在 `tower_init` 之前调用读取类工具 |
20
+ | `mission-not-found` | 不存在该 id 的 mission。 | 过期或拼错的 `M9` |
21
+ | `finding-not-found` | 不存在该 id 的 finding。 | `tower_findings({id:"F99"})` |
22
+ | `invalid-transition` | 请求的状态转换不被允许。 | 尝试 blocked→merged;finding 状态图之外的流转;reviewer 修改 finding;在还有未完成任务、blocker、无提交或未读消息时完成 mission。 |
23
+ | `state-locked` | 另一个 Tower 写入者持有 state 锁。 | 锁超时后的并发写 |
24
+ | `state-corrupt` | Tower state 文件不可读或无效。 | 手工编辑过的 `state.json` |
25
+ | `config-invalid` | Tower 配置无效。 | 格式错误的仓库 `config.json`、违反 schema 的值 |
26
+ | `config-unknown-key` | Tower 配置存在未知顶层键。 | 例如把键拼成 `allowAgentToggle` |
27
+ | `git-failed` | git 命令因未归入其他分类的原因失败。 | 被拒绝的合并命令、非法 ref |
28
+ | `git-timeout` | git 命令超时或输出超出缓冲区。 | 慢 pre-merge 钩子、超大 diff |
29
+ | `spawn-unavailable` | subagents 服务不可用。 | 没有 `subagents` 的 Host |
30
+ | `provider-missing` | 没有注册的 subagent provider。 | provider 列表为空 |
31
+ | `preset-missing` | 配置的 agent preset 未注册。 | 缺少 `tower-worker`/`tower-reviewer` preset 行、DSH 未重启 |
32
+ | `reviewer-prerequisite` | reviewer spawn 的前置条件未满足。 | mission 尚未 `completed` 或还没有 worktree 就 spawn reviewer |
33
+ | `not-authorized` | 调用方 agent 角色无权执行该操作。 | worker 调用仅限控制塔的工具,或修改 mission 状态/scope;reviewer 修改 finding |
34
+ | `workspace-busy` | 另一个 Tower 会话持有该仓库的 workspace 锁。 | `mode.workspaceLock:true` 时第二个会话调用仓库级工具 |
35
+ | `worker-limit` | 仓库达到 `spawn.maxLiveWorkers` 个活跃 worker。 | 默认上限下 spawn 第五个 worker |
36
+ | `rate-limited` | 达到 `tower_send` 的每分钟速率限制。 | 超出 `send.maxPerMinute`;稍后重试 |
37
+ | `takeover-refused` | mission 仍有运行中的 worker。 | 在 `tower_interrupt` 之前调用 `tower_spawn({takeover:true})` |
38
+ | `reload-unavailable` | 当前 profile 中没有可用的 DSH HMR 服务。 | 在没有 HMR 的 profile 上调用 `tower_reload` / `/tower reload` |
39
+ | `reload-failed` | HMR reload 失败。 | `reconcileProfile` 抛错;检查 DSH 日志并重启 |
40
+ | `tests-failed` | 声明的测试发现失败,或不满足准入要求。 | 命令非零退出、输出格式错误、minCount/required 不足 |
41
+ | `tests-timeout` | 测试发现超出时间或输出预算。 | `tests.timeoutMs`,或 stdout 超过 `tests.maxBytes` |
42
+ | `lease-busy` | 另一个持有者拥有请求的咨询式租约。 | `heavy-ci` 被持有时调用 `tower_resource acquire` |
43
+ | `lease-token` | 租约释放 token 与持有者不匹配。 | 用另一个会话的 token 释放 |
44
+ | `batch-invalid` | batch 请求或其 close 证据无效。 | 未知成员、重复 id、不安全的回执路径、缺少回执哈希 |
45
+ | `legacy-invalid` | 旧版快照或映射无效。 | 未知 schema、源路径位于 Tower state 目录内、映射重写 |
46
+ | `tool-internal` | 意外失败。 | bug;阅读消息与 activity 日志 |
47
+
48
+ ## 为什么抛出?
49
+
50
+ 返回 `{ok:false}` 的形状保留给控制塔能决定的预期策略结果(`tower_merge`/`tower_rebase` 的门禁
51
+ blocker 以及 `tower_preflight`)。任何阻止工具给出决策的问题都会抛出,以免模型把它误当成正常的
52
+ 否定结果。
@@ -0,0 +1,46 @@
1
+ # Tower 事件
2
+
3
+ [English](../../reference/events.md) | **简体中文**
4
+
5
+ Tower 在工具成功调用(变更与只读)后发出进程内 Cordis 事件,其他插件因此可以观察仓库,而不必轮询。
6
+ 事件**不会**通过网络发送,也不包含遥测。
7
+
8
+ ```js
9
+ ctx.on('tower/merge', (payload) => {
10
+ // payload.tool, payload.repoRoot, payload.value (the tool's JSON result)
11
+ })
12
+ ```
13
+
14
+ | 事件 | 触发时机 |
15
+ |---|---|
16
+ | `tower/init` | `tower_init` 创建或重新锚定 state 之后 |
17
+ | `tower/plan` | `tower_plan` 创建 mission 之后 |
18
+ | `tower/mission` | `tower_mission` 修改单个 mission 之后 |
19
+ | `tower/spawn` | `tower_spawn` 记录 worker/reviewer 之后 |
20
+ | `tower/complete` | `tower_complete` 记录完成报告之后 |
21
+ | `tower/review` | `tower_review` 记录审查结论之后 |
22
+ | `tower/merge` | `tower_merge` 返回之后(包括门禁失败;检查 `value.ok`) |
23
+ | `tower/rebase` | `tower_rebase` 返回之后 |
24
+ | `tower/teardown` | `tower_teardown` 移除 worktree 之后 |
25
+ | `tower/finding` | `tower_finding` 创建单条 finding 之后 |
26
+ | `tower/findings` | `tower_findings` 读取/列出/更新 finding 之后 |
27
+ | `tower/send` | `tower_send` 持久化消息之后 |
28
+ | `tower/inbox` | `tower_inbox` 确认消息之后 |
29
+ | `tower/interrupt` | `tower_interrupt` 停止运行中的 agent 之后 |
30
+ | `tower/diff` | `tower_diff` 读取 mission diff 之后 |
31
+ | `tower/adopt` | `tower_adopt` 对账 roster 之后 |
32
+ | `tower/roster` | `tower_roster` 读取 roster 之后 |
33
+ | `tower/status` | `tower_status` 读取面板之后 |
34
+ | `tower/preflight` | `tower_preflight` 评估门禁之后 |
35
+ | `tower/doctor` | `tower_doctor` 完成只读检查之后 |
36
+ | `tower/test` | `tower_test` 运行测试发现之后 |
37
+ | `tower/batch` | `tower_batch` 创建、查看或关闭 batch 之后 |
38
+ | `tower/resource` | `tower_resource` 获取/释放/查看租约之后 |
39
+ | `tower/import_legacy` | `tower_import_legacy` 保存旧版快照之后 |
40
+ | `tower/legacy_map` | `tower_legacy_map` 映射或列出旧 id 之后 |
41
+
42
+ `tower_mode`、`tower_control` 与 `tower_reload` 不发出 Tower 事件。payload 是 JSON:
43
+ `{tool, repoRoot, value}`。监听器抛错不会影响工具结果。
44
+
45
+ 需要可持久审计的调用链时,请使用 `<git-common-dir>/dsh-tower/activity.jsonl` 与 `MISSIONS.md`;
46
+ 事件是进程内的,不会重放。
@@ -0,0 +1,48 @@
1
+ # 批次、租约与旧版导入
2
+
3
+ [English](../../reference/operations.md) | **简体中文**
4
+
5
+ ## 证据批次
6
+
7
+ `tower_batch` 把有限的 mission 完成证据分组。
8
+
9
+ - `create` 冻结 `missionIds`、`exclusions`、`requiredChecks`(默认 `["check"]`)与精确的相对
10
+ `receiptPaths`。成员必须是已知 mission,且只有暂停的旧版 mission 可以被排除;创建后成员关系不可变。
11
+ - `status` 汇总成员与待处理/失败的检查。
12
+ - `close` 校验已完成成员的每条必需检查(来自 `completionReport.checks`、`testRuns` 或 close 时的
13
+ `checks`),在提供 `treeTip` 时校验它,并要求每个冻结的回执路径都在
14
+ `receiptHashes`/`receipts` 中提供(或通过 `receiptHash` 绑定到通过的检查)。缺少证据会返回
15
+ `{ok:false, blockers}`;不会删除任何内容。
16
+
17
+ close 失败不是部分关闭:batch 保持 `open`,修复证据后可以重试。
18
+
19
+ ## 咨询式租约
20
+
21
+ `tower_resource` 管理每个仓库的租约,位于 `<git-common-dir>/dsh-tower/leases.json`:
22
+
23
+ ```text
24
+ tower_resource({action:"acquire", name:"heavy-ci", ttlMs:900000})
25
+ tower_resource({action:"status"})
26
+ tower_resource({action:"release", name:"heavy-ci", token:"<token from acquire>"})
27
+ ```
28
+
29
+ - acquire 仅限控制塔;调用方会得到 token、pid、holder、创建/过期时间戳;
30
+ - 同一个 token 可以续租;另一个持有者会得到 `[tower:lease-busy]`;
31
+ - release 需要 token;**已过期**的租约仍需要 token 与显式 `force:true`(不会静默接管);
32
+ - `spawn.requireLease` 要求持有该租约,否则 worker spawn 失败。
33
+
34
+ 租约用于协调本地任务与 CI 窗口。它们**不是** OS CPU/内存硬上限:DSH 没有公开的 Job Object 配额
35
+ API,Tower 无法强制每进程资源限制;这类限制请使用 live-worker 上限以及你的 OS/CI 调度器。
36
+
37
+ ## 旧版导入与映射
38
+
39
+ `tower_import_legacy({repoRoot,sourcePath})` 接受 Tower Codex `schemaVersion:1` state 文件或 DSH
40
+ 旧版 state。它会:
41
+
42
+ - 要求绝对源路径,且位于 `<git-common-dir>/dsh-tower` 之外;
43
+ - 校验 schema(上限 16 MiB),把原始 payload 逐字节保存到
44
+ `<git-common-dir>/dsh-tower/legacy/<sha256>.json`,并在 `state.legacySnapshots` 中只记录摘要;
45
+ - 绝不激活、接管或恢复暂停的 mission。
46
+
47
+ `tower_legacy_map` 记录仅审计用途的 `sourceHash + oldId → newMissionId?` 映射。映射只追加;把已有
48
+ 映射改写到另一个 mission 会被拒绝。映射不会改变 mission 状态。
@@ -0,0 +1,60 @@
1
+ # 测试、发现与证据
2
+
3
+ [English](../../reference/testing.md) | **简体中文**
4
+
5
+ Tower 2.1 可以执行仓库声明的测试发现,并把结果变成持久的完成证据。发现是**按配置选择性启用**的:
6
+ worktree 没有 `package.json.tower.testDiscovery` 块时,行为与之前完全一致。
7
+
8
+ ## 配置
9
+
10
+ `package.json`(与 Tower Codex 兼容):
11
+
12
+ ```json
13
+ {
14
+ "tower": {
15
+ "testDiscovery": {
16
+ "version": 1,
17
+ "commands": [
18
+ {
19
+ "id": "unit",
20
+ "command": "node --test dist/tests/*.test.js",
21
+ "format": "node-tap",
22
+ "minCount": 1,
23
+ "required": ["smoke"]
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ | 字段 | 含义 |
32
+ |---|---|
33
+ | `id` | 唯一,`[A-Za-z0-9][A-Za-z0-9._-]{0,63}`;也用于诊断文件名。 |
34
+ | `command` | 以 worktree 为 cwd 执行的 shell 命令(Windows 用 `pwsh`,POSIX 用 `bash`)。 |
35
+ | `format` | `node-tap`(TAP 的 `ok N - name` / `not ok`)或 `json-array`(`{name, ok?}` 的 JSON 数组)。 |
36
+ | `minCount` | 该命令至少应发现的测试数(默认 1)。 |
37
+ | `required` | 必须出现且通过的测试名。 |
38
+
39
+ Tower 配置中的 `tests.*` 控制准入、超时、输出上限以及是否允许 package discovery。
40
+ `tests.admission` 取 `spawn`(worker 启动前运行;fail-closed,不启动子代理)、`complete`
41
+ (在 `tower_complete` 时运行;默认)或 `off`(只在显式 `tower_test` 时运行)。
42
+
43
+ ## 回执与门禁
44
+
45
+ 每次运行都会在 `<git-common-dir>/dsh-tower/test-runs/<mission>-<stamp>/` 下保存回执,包含
46
+ stdout/stderr 文件、SHA-256 哈希、退出码、计数、required 名称、worktree tip 以及总体 `ok`。
47
+ `state.missions[].testRuns` 保留最近的 50 条回执。失败时工具返回带诊断的 `ok:false`;自动发现失败时
48
+ `tower_complete` 会抛出 `[tower:tests-failed]`。
49
+
50
+ - `tower_test({repoRoot,missionId})` 按需运行发现并记录回执。
51
+ - `tests.autoChecks` 开启时,`tower_complete` 会添加一条 `tests` 检查。
52
+ - `gates.requireChecks` 要求任意完成检查;`gates.requireTests` 要求针对被审查 tip 的通过回执
53
+ (`tests-missing`)。
54
+
55
+ ## 信任边界
56
+
57
+ 发现命令是仓库控制的代码。`tests.allowPackageDiscovery:false` 会禁用 package 块;CI 或策略 profile
58
+ 在克隆不受信任仓库时应设置它。命令以与 DSH 相同的 OS 权限、在 mission worktree 中运行,Tower 不会
59
+ 为它增加网络访问。输出上限为 `tests.maxBytes`(默认 16 MiB),每条命令受 `tests.timeoutMs`
60
+ (默认 60 秒)限制;超出预算的运行 fail closed。
@@ -0,0 +1,75 @@
1
+ # 工具参考
2
+
3
+ [English](../../reference/tools.md) | **简体中文**
4
+
5
+ 所有工具都接受 `repoRoot`(目标 Git 仓库内的绝对路径)并返回 JSON。
6
+ 本构建最多 28 个工具:下列 19 个核心仓库工具(含 `tower_interrupt`)、`tower_mode`、
7
+ `mode.allowAgentControl` 开启时注册的 `tower_control`、`dev.hotReload` 开启时注册的
8
+ `tower_reload`,以及 2.1 的验证/运维工具 `tower_doctor`、`tower_test`、`tower_batch`、
9
+ `tower_resource`、`tower_import_legacy`、`tower_legacy_map`。
10
+
11
+ `tower_mission` 使用 `id`(别名 `missionId`);其他所有 mission 级工具使用 `missionId`
12
+ (别名 `id`)。其他输入别名:`task_done`/`taskDone`、`task_drop`/`taskDrop`、
13
+ `task_drop_reason`/`taskDropReason`、`clear_blockers`/`clearBlockers`、
14
+ `include_read`/`includeRead`、`dry_run`/`dryRun`、`timeoutMs`/`timeout_ms`、
15
+ `waitMs`/`wait_ms`、finding 上的 `mission_id`/`missionId`、interrupt 上的 `agent_id`/`agentId`、
16
+ status 上的 `include_gate`/`includeGate`、adopt 上的 `retire_missing`/`retireMissing`。
17
+ 输出始终使用规范的 camelCase 字段名。
18
+
19
+ ## 角色
20
+
21
+ roster 角色由 `exec.agent` 与 roster 共同决定。mission 级工具(`tower_interrupt`、
22
+ `tower_init`、`tower_plan`、`tower_spawn`、`tower_merge`、`tower_rebase`、`tower_teardown`、
23
+ `tower_adopt`、`tower_reload`)**仅限控制塔**。`tower_batch` 的 create/close、`tower_resource`
24
+ 的 acquire/release、`tower_import_legacy`、`tower_legacy_map` 的 map 以及 review-exemption 规划
25
+ 仅限控制塔;它们的只读 status 动作对 roster 角色开放。worker 调用 `tower_mission` 时只能使用
26
+ `task_done`/`task_drop`/`blocker`/`note`;状态变更、扩大 scope、`clear_blockers` 与 `completed`
27
+ 仅限控制塔(完成必须走 `tower_complete`)。roster 中的 reviewer 可以读取 finding,但不能修改。
28
+ 违规会抛出 `[tower:not-authorized]` 或 `[tower:invalid-transition]`。
29
+
30
+ ## 工具
31
+
32
+ | 工具 | 参数 | 用途 / 说明 |
33
+ |---|---|---|
34
+ | `tower_init` | `base?`、`autoGitInit?`、`snapshotDirty?`、`reanchor?` | 创建 `<git-common-dir>/dsh-tower/state.json`。`autoGitInit` 执行 `git init` 并创建空的初始提交;`snapshotDirty` 记录一个游离快照提交作为 mission 基线;`reanchor` 仅在没有非终态 mission 时修改基线。返回 `dirty`、`snapshotCommit`、`reanchored`。 |
35
+ | `tower_plan` | `missions[]`(`title`、`scope[]`、`tasks[]?`、`deps[]?`、`kind?`、`context?`、`references[]?` ≤20、`reviewPolicy?`、`reviewExemptionReason?`) | 创建 mission `M1..`;重叠的 build scope 会自动加依赖;拒绝未知依赖与依赖循环。`reviewPolicy:"none"` 需要理由,并且要求 `gates.allowReviewExemption`。 |
36
+ | `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`。 |
37
+ | `tower_status` | `missionId?`、`offset?`、`limit?`、`includeGate?`、`format?` | 默认返回面板。`missionId` 返回完整详情(任务/备注/审查/finding/完成报告)。`offset`/`limit` 对 mission 分页(默认 20,最大 100),`includeGate` 附带门禁摘要,`format:"markdown"` 返回看板。 |
38
+ | `tower_spawn` | `missionId`(别名 `id`)、`role`(`worker`/`reviewer`)、`name?`、`instructions?`、`worktreeRoot?`、`wait?`、`waitMs?`、`takeover?`、`includeDirtyBase?` | 准备或复用 worktree 并启动子代理。worker 使用 `tower-worker`;reviewer 要求 mission 为 `completed`,并使用只读的 `tower-reviewer`。`takeover:true` 替换已不再运行的 worker 条目(ownerGeneration 递增)。每个 mission 只能有一个活跃 worker;`spawn.maxLiveWorkers` 限制整个仓库。 |
39
+ | `tower_roster` | — | roster 及来自子代理目录的实时活动(`running`/`inactive`/`missing`/`unknown`)与 `stale` 标记。只读。 |
40
+ | `tower_adopt` | `retireMissing?` | 把 roster 条目与持久子代理目录对账。可证明已消失的 agent 会被标记为 retired(默认),或标记为 `missing`(`retireMissing:false`)以便检查/接管。 |
41
+ | `tower_diff` | `missionId`(别名 `id`)、`maxBytes?` | 只读 merge-base diff:文件、提交、有上限的 patch。供 reviewer 安全使用。 |
42
+ | `tower_send` | `to`、`subject`、`body`、`scope?`、`interrupt?`、`consentRef?` | 向 roster 名称、`tower` 或 `all` 发送持久消息;对运行中的 agent 会即时投递。`interrupt:true` 先中断运行中的回合(Kimi 风格)再投递;`consentRef` 记录用于审计。受 `send.maxPerMinute` 限速。 |
43
+ | `tower_inbox` | `limit?`、`include_read?` | 读取并确认发给调用者的消息;未读消息会阻止 `tower_complete`。 |
44
+ | `tower_complete` | `missionId`(别名 `id`)、`report`、`checks?` | worker 报告完成(mission 变为 `completed`,等待审查)。要求当前 owner、无未完成任务/blocker、≥1 个提交、无未读消息。`checks` 记录 `{name, command?, exitCode?, evidence?}` 证据;`gates.requireChecks` 可以强制要求。配置了测试发现时还会自动运行并添加 `tests` 检查;失败会抛出 `[tower:tests-failed]`。 |
45
+ | `tower_interrupt` | `to?`(roster 名称)、`agentId?`、`reason?` | 通过 `subagents.interrupt` 中断一个运行中的 roster agent,并记录 `state:"interrupted"`。幂等(未运行 → `interrupted:false`)。 |
46
+ | `tower_finding` | `type`、`title`、`summary`、`details`、`suggested_fix`、`severity?`、`missionId?`、`location?` | 记录 scope 之外的问题;reviewer 用它代替直接改文件。新 finding 初始为 `open`。 |
47
+ | `tower_findings` | `id?`、`status?`、`note?`、过滤(`type?`、`severity?`、`missionId?`、`open?`、`limit?`) | 生命周期工具:列出/读取/流转。状态为 `open → triaged → fixed`/`wontfix`,允许重新打开。只有控制塔或 worker 能修改;reviewer 只能列出/读取。 |
48
+ | `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)。 |
49
+ | `tower_preflight` | `missionId`(别名 `id`) | 只读门禁:返回 `{ok, blockers[], branchTip, baseTip, changedFiles, review}`。 |
50
+ | `tower_merge` | `missionId`(别名 `id`)、`timeoutMs?`(别名 `timeout_ms`) | 门禁全绿后合并。若合并提交已落地则记录(`reconciled`);若命令失败但提交已存在则恢复(`recoveredFromFailure`);区分 `merge-hook-failed` 与 `merge-conflict`。 |
51
+ | `tower_rebase` | `missionId`(别名 `id`) | 在 worktree 内把分支 rebase 到基线。worker 运行中或 worktree 脏时拒绝。分支已包含在基线中时返回 `alreadyMerged` 且保持分支不动。 |
52
+ | `tower_teardown` | `force?`、`exclude?`、`dry_run?`(别名 `dryRun`) | 移除符合条件的 merged/abandoned worktree 与分支;运行中的 agent 始终保留。 |
53
+ | `tower_mode` | — | 报告模式、最近修改、被暂停的 goal、被拒绝的工具、store 路径、插件版本、HMR 可用性、workspace 锁持有者。 |
54
+ | `tower_control` | `action`(`enter`/`exit`)、`reason` | 模型主动切换模式;仅当 `mode.allowAgentControl` 为 `true` 时可用;`reason` 必填。 |
55
+ | `tower_reload` | `repoRoot?` | 仅开发用:请求 DSH HMR 对账 profile 并从磁盘重载 Host 插件。`dev.hotReload` 开启时注册;在工具结果返回后调度(默认 50 ms);没有 HMR 时以 `reload-unavailable` 失败。传入 `repoRoot` 时调用者必须是控制塔。 |
56
+ | `tower_doctor` | —(需要 `repoRoot`) | 只读健康契约:git、state schema、config、preset/subagents/HMR、workspace 锁、roster、worktree realpath/归属、测试配置、leases。返回 `{ok, checks[{id,status,detail}], environmentContract}`。从不写状态。 |
57
+ | `tower_test` | `missionId`(别名 `id`) | 运行 worktree 的 `package.json` 中 `tower.testDiscovery` 配置的发现命令并记录回执;失败时 `ok:false`。自动准入检查请用 `tower_complete`。 |
58
+ | `tower_batch` | `action`(`create`/`status`/`close`)、`missionIds?`、`exclusions?`、`requiredChecks?`、`receiptPaths?`、`batchId?`、`checks?`、`treeTip?`、`receiptHashes?` | 有限证据批次。create 冻结成员/检查/回执;close 校验每条必需检查与回执哈希。成员关系不可变;close 从不删除 mission。 |
59
+ | `tower_resource` | `action`(`acquire`/`release`/`status`)、`name?`、`holder?`、`token?`、`ttlMs?`、`force?` | 每个仓库的咨询式租约,位于 `<common>/dsh-tower/leases.json`。没有 OS CPU/内存硬上限;过期的租约需要显式 `force` 与正确 token。 |
60
+ | `tower_import_legacy` | `sourcePath` | 只读导入 Tower Codex `schemaVersion:1` 或 DSH 旧版 state。完整 payload 存到 `<common>/dsh-tower/legacy/<sha256>.json`;state 中只保存摘要。绝不激活/接管/恢复暂停的 mission。 |
61
+ | `tower_legacy_map` | `action`(`map`/`status`)、`sourceHash?`、`oldId?`、`newMissionId?` | 仅审计用途的旧 id 映射;映射只追加,绝不容纳 mission。 |
62
+
63
+ ## workspace 锁
64
+
65
+ 当 `mode.workspaceLock:true` 时(随包 patch 开启),Tower 模式会话的第一次仓库级 Tower 工具调用会
66
+ 获取 `<git-common-dir>/dsh-tower/tower.lock`;另一个会话会得到 `[tower:workspace-busy]`,直到持有
67
+ 者执行 `/tower off` 或锁超过 `mode.workspaceStaleMs`。`mode.workspaceLockScope:"mutations"` 会跳过
68
+ 只读工具的锁。代码默认关闭;随包 patch 开启,以提供与 Kimi 兼容的“单活跃控制塔”行为。
69
+
70
+ ## 失败结果
71
+
72
+ 门禁类工具返回 `{ok:false, blockers:[{code, detail}]}`;code 列表见 [blockers.md](blockers.md)。
73
+ 抛出的工具错误带稳定的 `[tower:<code>]` 前缀(见 [errors.md](errors.md))。成功的变更会发出进程内
74
+ 事件(见 [events.md](events.md)),并且在 `docs.board:true` 时刷新
75
+ `<git-common-dir>/dsh-tower/MISSIONS.md`。