dsh-tower 2.4.2 → 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 +72 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +5 -3
- package/GLOSSARY.md +73 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +16 -15
- package/README.md +18 -10
- package/app/archive.js +16 -1
- package/app/board.js +16 -1
- package/app/gate.js +14 -0
- package/app/review.js +7 -0
- package/app/spawn-prechecks.js +30 -5
- package/app/tower.js +5 -5
- package/client.js +321 -0
- package/cordis.patch.yml +26 -9
- package/docs/adr/0004-surface-budget.md +10 -4
- package/docs/adr/0005-component-surface.md +222 -11
- 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 +53 -7
- package/docs/guide/quickstart.md +91 -10
- package/docs/guide/release.md +60 -19
- 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 +40 -21
- package/docs/reference/config.generated.md +6 -8
- package/docs/reference/config.md +36 -8
- package/docs/reference/doctor.md +11 -5
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/surface-budget.md +47 -12
- package/docs/reference/testing.md +1 -1
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +5 -5
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +26 -7
- 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 +28 -8
- package/docs/zh/reference/doctor.md +7 -2
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +2 -2
- package/docs/zh/reference/tools.md +6 -5
- package/domain/component-rules.js +216 -0
- package/domain/components.js +181 -53
- package/domain/config/schema.js +86 -28
- 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 +23 -5
- package/domain/integration/review.js +203 -9
- package/domain/scope.js +176 -5
- package/domain/surface.js +21 -7
- package/host/capability-archive.js +18 -0
- package/host/capability-board.js +18 -0
- package/host/capability-registry.js +4 -0
- package/host/config.js +40 -3
- package/host/doctor.js +68 -12
- package/host/presets.js +26 -8
- package/host/tool-registry.js +30 -5
- package/index.js +17 -6
- package/locale/archive/en.json +6 -0
- package/locale/archive/zh.json +6 -0
- package/locale/batch/en.json +1 -1
- package/locale/batch/zh.json +1 -1
- package/locale/board/en.json +6 -0
- package/locale/board/zh.json +6 -0
- package/locale/en.json +37 -22
- package/locale/hmr/en.json +2 -2
- package/locale/hmr/zh.json +1 -1
- package/locale/import/en.json +1 -1
- package/locale/import/zh.json +1 -1
- package/locale/leases/en.json +2 -2
- package/locale/leases/zh.json +2 -2
- package/locale/preset-reviewer/en.json +1 -1
- package/locale/preset-reviewer/zh.json +1 -1
- package/locale/preset-worker/en.json +1 -1
- package/locale/preset-worker/zh.json +1 -1
- package/locale/skills/en.json +1 -1
- package/locale/skills/zh.json +1 -1
- package/locale/tests/en.json +2 -2
- package/locale/tests/zh.json +2 -2
- package/locale/zh.json +36 -21
- package/package.json +17 -2
- package/scripts/check-components.mjs +698 -44
- package/scripts/check-pack.mjs +3 -0
- package/scripts/component-surface.json +101 -37
- package/scripts/generate-capability-locale.mjs +3 -0
- package/scripts/mount-smoke/surface.mjs +2 -0
- package/scripts/pack-manifest.txt +10 -0
- package/scripts/release-notes.mjs +256 -0
- package/scripts/surface-baseline.json +39 -3
- package/scripts/surface-baseline.mjs +15 -5
- package/scripts/types-coverage.mjs +7 -0
- package/skills.js +4 -1
- package/tools/complete.js +3 -1
- package/tools/preflight.js +1 -1
- package/tools/review.js +1 -1
- package/tools/spawn.js +7 -7
- package/tools/status.js +8 -1
- package/tools/test.js +7 -7
- package/types/index.d.ts +10 -0
- package/ui-bundle/locale/en.json +1 -1
- package/ui-bundle/locale/zh.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,78 @@
|
|
|
4
4
|
|
|
5
5
|
## 未发布(Unreleased)
|
|
6
6
|
|
|
7
|
+
## 2.6.0(2026-11-01)
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- **#M39/#M40 审查策略成为单一三态控件**:`review.mode` 提供 `off` / `optional`(默认)/
|
|
12
|
+
`required`;插件页 `dsh-tower` 配置区只有一个三态单选组,不是组件行,也不是三个互斥行。
|
|
13
|
+
`off` 是受支持的模式:不注册 `tower_review`、拒绝 reviewer spawn、合并门禁忽略审查;
|
|
14
|
+
`optional` 记录审查但从不阻塞合并;`required` 恢复"干净且绑定分支 tip 的审查"合并门禁。
|
|
15
|
+
- **#M42 双语迁移说明**:安装、升级与迁移文档新增 2.5.0 → 2.6.0 一节,说明三种模式、控件位置、
|
|
16
|
+
旧布尔别名的映射与单版本兼容窗口、从 `off` 启动需要重启的限制,以及 `required` 对
|
|
17
|
+
`preset-tower-reviewer` 行的依赖。
|
|
18
|
+
|
|
19
|
+
### 变更
|
|
20
|
+
|
|
21
|
+
- **#M42 审查策略的组件表面重新分类(2026-11-01 决策)**:`review.mode` 是一个三态配置控件;
|
|
22
|
+
`review.requireReview` 从 `forbidden-to-disable` 改为 `core`(已弃用的单版本别名)。`required`
|
|
23
|
+
模式下的合并门禁、审查证据与豁免路径仍不可被行/布尔关闭;`preset-tower-reviewer` 仍是可关闭的
|
|
24
|
+
能力行。PRINCIPLES 原则 11 与 ADR 0005 增加 wave-5 说明;组件门禁新增"审查策略不是行、没有互斥行"
|
|
25
|
+
的检查。数量不变:54 个配置键、11 个可见组件。
|
|
26
|
+
- **#M42 文档全面改用 `review.mode`**:README、quickstart、FAQ、场景、架构、安全、测试参考与
|
|
27
|
+
reviewer 行注释不再把旧布尔写成当前用法;历史迁移记录保留。
|
|
28
|
+
- `GLOSSARY.md` 增补 review policy / 审查策略 术语与"一个三态控件,不是三个行"的文案规则。
|
|
29
|
+
|
|
30
|
+
### 修复
|
|
31
|
+
|
|
32
|
+
- **F25 reviewer 行注释过时**:`cordis.patch.yml` 不再声称"仅当旧布尔为 false 时才能关闭",
|
|
33
|
+
改为按 `review.mode` 的三态语义描述(`off`/`optional` 可关闭,`required` 时拒绝该组合)。
|
|
34
|
+
- **F26/F28 文档不再把审查策略称为"永不可关闭"**:生成的组件参考与架构总览现在只把宿主、
|
|
35
|
+
健康契约、恢复出口与合并门禁列为永不可关闭;`required` 模式下的审查门禁(证据、tip 绑定、
|
|
36
|
+
豁免路径)仍不可关闭,而策略本身是一个三态控件。
|
|
37
|
+
- **F27 测试计数同步**:CONTRIBUTING 与 COMPATIBILITY 的 2.6.0 行更新为 729 个离线测试。
|
|
38
|
+
|
|
39
|
+
## 2.5.0(2026-11-01)
|
|
40
|
+
|
|
41
|
+
### 新增
|
|
42
|
+
|
|
43
|
+
- **#133 追踪看板成为可选能力**:新增 `tower-board` 组件行,关闭后不再写 `MISSIONS.md`,
|
|
44
|
+
`tower_status`/`tower_doctor` 说明关闭原因;`docs.boardLocation` 仍是位置调节项。
|
|
45
|
+
旧的 `docs.board` 配置键被删除,写入旧键会收到带迁移说明的 `config-unknown-key`。
|
|
46
|
+
- **#135 归档成为可选能力**:新增 `tower-archive` 组件行,关闭后终态任务保持热存储且不自动归档,
|
|
47
|
+
doctor 报告关闭后果;`archive.afterDays`/`archive.maxMissions` 仍是节奏调节项。
|
|
48
|
+
旧的 `archive.enabled` 配置键被删除。
|
|
49
|
+
- **#134 `tower-ui` 进入组件源**:`ui-bundle/cordis.patch.yml` 的 UI 行现在被组件源、locale 覆盖与
|
|
50
|
+
基线同时看到;门禁解析所有随包发布的 `cordis.patch.yml`。
|
|
51
|
+
- **#137 面向普通用户的双语标签与术语表**:新增 `GLOSSARY.md`(12 组中英术语 + 文案规则),
|
|
52
|
+
重写全部可见组件行的标题、用途与关闭后果;不再出现原始配置键、工具 id、斜杠命令或模块说明符。
|
|
53
|
+
|
|
54
|
+
### 变更
|
|
55
|
+
|
|
56
|
+
- **#138 组件门禁改为失败关闭**:键分类、行为开关、行覆盖、随包补丁与双向计数面全部机器校验;
|
|
57
|
+
双向面的每次计数决策必须带日期;`OPERATIONAL_TOOLS` 直接导入真实注册表,不再靠正则猜测。
|
|
58
|
+
- **#136 删除 `tests.enabled` 第二开关**:`tower-tests` 组件行是唯一开关;`tower_test` 在行关闭时
|
|
59
|
+
报告 `disabled by component tower-tests` 并给出修复方式,不再误报“未配置”。
|
|
60
|
+
- **#132 组行关闭后果可见**:`tower` 组行的“关闭等于卸载 Tower”现在显示在 Plugins 页面的描述里,
|
|
61
|
+
不再只是不可达的 locale 文本。
|
|
62
|
+
- **#141 HMR 明确定位为开发工具**:`tower-hmr` 保持默认关闭,文案改为“开发热重载(开发工具)”。
|
|
63
|
+
- 配置面 55 → 53 键;可见组件 11 个;新增 `tower-board`/`tower-archive` 行 locale 与打包清单条目。
|
|
64
|
+
|
|
65
|
+
### 修复
|
|
66
|
+
|
|
67
|
+
- **#131 租约组合提前拒绝**:`tower-leases` 关闭且 `spawn.requireLease` 非空时,配置/预检阶段直接
|
|
68
|
+
拒绝并说明行与两项修复方式;`lease-busy` 只指向真实注册的工具,`tower_doctor` 交叉检查该组合。
|
|
69
|
+
- **#139 能力卸载不留残影**:skills provider 注册与 Tower 模式提示段都通过 `ctx.effect` 释放,
|
|
70
|
+
热卸载后 `componentReport` 与真实挂载状态一致。
|
|
71
|
+
- **#140 禁用行原因一致**:`tests-missing` 合并阻塞项点名 `tower-tests` 行;机器输出的英文契约写入文档。
|
|
72
|
+
- **#142 `/tower` 预检不再误伤转发输入**:运行前置条件只在进入模式/切换时重新校验,
|
|
73
|
+
转发指令与 `/tower off` 在无关行关闭时继续工作。
|
|
74
|
+
- **F16 作用域重叠判定补全**:`scopesOverlap` 在保留文档化近似的同时加入精确 token-NFA 交集,
|
|
75
|
+
修掉 `a/b*` 与 `a/bc*` 被误判为不重叠的问题。
|
|
76
|
+
- **F17 配置解析一遍收敛**:`resolveTowerConfig` 现在对空输入也满足严格一遍幂等。
|
|
77
|
+
- **F23 术语表随包发布**:`GLOSSARY.md` 进入 `package.json` `files` 与打包清单(293 个文件)。
|
|
78
|
+
|
|
7
79
|
## 2.4.2(2026-10-24)
|
|
8
80
|
|
|
9
81
|
### 修复
|
package/COMPATIBILITY.md
CHANGED
|
@@ -5,6 +5,8 @@ is shipped in the npm package; doctor reads it locally and never contacts a regi
|
|
|
5
5
|
|
|
6
6
|
| dsh version | tag / commit | verified on | what was run |
|
|
7
7
|
|---|---|---|---|
|
|
8
|
+
| 0.2.1-rc.2 | v2.6.0 | 2026-11-01 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (729), `npm run test:property` (111), `npm run check` (docs + generated + component + surface + pack 294 + types + architecture + domain), `npm run smoke:mount` |
|
|
9
|
+
| 0.2.1-rc.2 | v2.5.0 | 2026-11-01 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (696), `npm run test:property` (111), `npm run check` (docs + generated + component + surface + pack 293 + types + architecture + domain), `npm run release:check -- --mode tag --tag v2.5.0` |
|
|
8
10
|
| 0.2.1-rc.2 | v2.4.2 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets with distinct labels, 1 bundle row), `npm test` (631), docs + seven generated checks, types coverage 148/148, components + philosophy + surface gates |
|
|
9
11
|
| 0.2.1-rc.2 | v2.4.1 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets, 1 bundle row), `npm test` (620), `types:coverage` 146/146, docs + seven generated checks incl capability-row locales, components + philosophy + surface gates |
|
|
10
12
|
| 0.2.1-rc.2 | v2.4.0 | 2026-10-24 | mount smoke (28/28 default tools, 3 skills, 2 presets, 1 bundle row), `npm test` (612), `types:coverage` 146/146, strict ratchet 10, docs + six generated checks, components + philosophy + surface gates |
|
package/CONTRIBUTING.md
CHANGED
|
@@ -102,7 +102,9 @@ cd dsh-tower
|
|
|
102
102
|
npm test # no dependencies to install; Node's built-in test runner only
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Requirements: Node.js 22+, Git. No package installation is needed for the offline suite.
|
|
105
|
+
Requirements: Node.js 22+, Git. No package installation is needed for the offline example suite.
|
|
106
|
+
|
|
107
|
+
The property-based PR tier (`npm run test:property`) does need the dev dependencies: run `npm ci` once, then `npm run test:property`. It is part of `npm run check` and runs on every CI matrix leg; the nightly long tier is `npm run test:property:long`. See [tests/README.md](tests/README.md#property-based-tests-fast-check-130).
|
|
106
108
|
|
|
107
109
|
The opt-in live suite is the exception: install it once with
|
|
108
110
|
`npm ci --prefix tests/live && npx --prefix tests/live playwright install chromium`, then run it with `npm run test:live`
|
|
@@ -118,13 +120,13 @@ the push gates; CI schedules it nightly.
|
|
|
118
120
|
- `host/` — the composition root and session/process glue (`tool-registry.js`, `config.js`, `hmr.js`, `modeStore.js`, `doctor.js`); `adapters/` — the only I/O implementations (store, git, archive, leases, locks); `app/` — use cases; `domain/` — the pure core.
|
|
119
121
|
- `worker.js` — the `tower-worker` preset policy (tool surface + protocol text).
|
|
120
122
|
- `host/preset-worker.js` / `host/preset-reviewer.js` — the two Tower-owned preset declaration rows (`dsh-tower/preset-*`). They keep the row config in `cordis.patch.yml`, forward it to `ctx.agentPresets.register()`, and give each preset row its own Plugins-page label; do not mount the shared `@deepseek-ai/dsh-agent-preset` for these rows.
|
|
121
|
-
- `tests/` — add a regression test for every behavior change. The offline suite is `tests/*.test.mjs`;
|
|
123
|
+
- `tests/` — add a regression test for every behavior change. The offline suite is `tests/*.test.mjs`; an invariant over a pure domain function belongs in `tests/property/*.prop.mjs` (fast-check) rather than a hand-rolled randomized loop, and `tests/no-random.test.mjs` enforces that. Live-runtime scripts are documented in `tests/README.md`.
|
|
122
124
|
- `scripts/architecture.mjs` — the fitness rules; `scripts/architecture-waivers.json` — the only exemptions, each with a `reason` and an `owner`/`ref`. An entry without a reason fails the blocking `waiver-contract` rule, The three layering rules that keep the core pure — `domain-pure`, `host-scope`, and `no-lib-from-core` — cannot be waived at all. The `let` scan behind `host-scope` is statement-aware: every binding of every top-level declaration is recorded, and only `host/**` may hold ambient state.
|
|
123
125
|
- `types/` — the published declaration surface (`package.json` `types` is `types/index.d.ts`). The vocabulary block in that file is generated from the runtime constants by `npm run types:generate` and gated by `npm run docs:check:generated:types`; never edit it by hand. `tsconfig.strict.json` turns `noImplicitAny` on for an explicit file list that may grow but never silently shrink: add a file, then record it with `npm run types:strict:write` (a removal also needs `--allow-shrink`). `npm run types:coverage` fails when a shipped runtime `.js` file is outside the `tsc` program.
|
|
124
126
|
|
|
125
127
|
## Pull request checklist
|
|
126
128
|
|
|
127
|
-
1. `npm test` passes locally (
|
|
129
|
+
1. `npm test` passes locally (729 tests today, including protocol end-to-end against a real temporary Git repository and mock spawn coverage), `npm run test:property` passes (111 generated property cases; also part of `npm run check`), and `npm run docs:check` passes.
|
|
128
130
|
2. New behavior has a test. Bug fixes should add a test that fails before the fix.
|
|
129
131
|
3. No secrets, tokens, personal paths, session logs, or generated artifacts are added. `.gitignore` covers local acceptance output; do not force-add it.
|
|
130
132
|
4. Keep the public API and state-file formats compatible; if a change is breaking, call it out in `CHANGELOG.md` and bump the version.
|
package/GLOSSARY.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# GLOSSARY
|
|
2
|
+
|
|
3
|
+
The canonical bilingual vocabulary for Tower's user-facing copy. Plain language
|
|
4
|
+
first: a component label says what a person can do, then what changes when the
|
|
5
|
+
component is off. The reader should not need to know a config key, a tool id or
|
|
6
|
+
an internal module name.
|
|
7
|
+
|
|
8
|
+
This file is enforced. `npm run docs:check:components` fails when a label field
|
|
9
|
+
in `locale/`, the generated `locale/<specifier>/` row labels or
|
|
10
|
+
`ui-bundle/locale/` uses a different Chinese term, leaves the English word
|
|
11
|
+
untranslated, or drifts from the table below. The gate that enforces it is
|
|
12
|
+
described in [ADR 0005](docs/adr/0005-component-surface.md) (wave-4 addendum,
|
|
13
|
+
#137).
|
|
14
|
+
|
|
15
|
+
## Canonical vocabulary
|
|
16
|
+
|
|
17
|
+
One term per concept, in both languages. Use these in every label, summary and
|
|
18
|
+
off-state sentence.
|
|
19
|
+
|
|
20
|
+
| Concept | English | 中文 | Notes |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| mission | mission | 任务线 | The unit of work a Tower session plans, executes and merges. Reserve 任务 for a single task inside a mission. |
|
|
23
|
+
| task | task | 任务 | One checklist item inside a mission. Not a substitute for 任务线. |
|
|
24
|
+
| batch | batch | 批次 | A finite evidence batch. Do not mix 批量 into label copy; that word describes bulk actions, not the component. |
|
|
25
|
+
| doctor | doctor | 体检 | The read-only health check. Do not use 诊断 as a noun for the health tool. |
|
|
26
|
+
| board | board | 看板 | The human-readable mission board (`MISSIONS.md`). |
|
|
27
|
+
| archive | archive | 归档 | Moving finished missions out of the live state file. |
|
|
28
|
+
| preset | preset | 预设 | A shipped role definition. Never leave the English word in Chinese copy. |
|
|
29
|
+
| worktree | worktree | 工作树 | The isolated checkout a worker runs in. |
|
|
30
|
+
| review | review | 审查 | The review contract and the act of reviewing. Do not use 评审 in plugin-page copy. |
|
|
31
|
+
| review policy | review policy | 审查策略 | The one tri-state control on the plugin-page configuration form: off, optional (default) or required. It is not a component row and never three mutually exclusive rows. |
|
|
32
|
+
| lease | lease | 租约 | Advisory resource coordination. Never 咨询式租约: the lease coordinates, it does not consult. |
|
|
33
|
+
| worker | worker | 执行代理 | The role that executes a mission. "Tower Worker" is a product name and stays as-is. |
|
|
34
|
+
| reviewer | reviewer | 审查代理 | The role that independently reviews a mission. "Tower Reviewer" is a product name and stays as-is. |
|
|
35
|
+
|
|
36
|
+
## Product names and other surfaces
|
|
37
|
+
|
|
38
|
+
Brand and product names are not translated: Tower, DSH, Tower Worker, Tower
|
|
39
|
+
Reviewer, Tower UI, MISSIONS.md. On first use in a sentence, gloss an unfamiliar
|
|
40
|
+
one parenthetically — for example "Tower Codex (Tower's earlier state format)".
|
|
41
|
+
|
|
42
|
+
The Web client has its own string catalogue (`ui-bundle/client.locales.js`).
|
|
43
|
+
When its wording disagrees with this table, this table wins and the client copy
|
|
44
|
+
is migrated separately under the same glossary.
|
|
45
|
+
|
|
46
|
+
## Copy rules
|
|
47
|
+
|
|
48
|
+
1. One sentence for the capability, one sentence for the off consequence.
|
|
49
|
+
2. No config-key syntax (`gates.requireTests:true`), no bare tool ids
|
|
50
|
+
(`tower_batch`), no slash commands (`/tower reload`) and no module specifiers
|
|
51
|
+
in a label field. Describe the capability and the effect instead.
|
|
52
|
+
3. Say "Tower mode", not just "模式", and "refused with an explanation", not
|
|
53
|
+
"locked": the user needs to know they will get the reason.
|
|
54
|
+
4. The off sentence must be truthful. A lease is coordination only, but a worker
|
|
55
|
+
configured to require one cannot start while the lease row is off — planning
|
|
56
|
+
and Tower-mode entry are refused with an explanation until the setting is
|
|
57
|
+
cleared or the row is re-enabled. Say that, and say that everything else keeps
|
|
58
|
+
working.
|
|
59
|
+
5. English and Chinese must agree field by field. Chinese copy reads as Chinese:
|
|
60
|
+
no untranslated English common nouns, no machine-translated calques — say
|
|
61
|
+
关闭该批次, not 收口; 当前状态文件, not 在线状态文件; 某一具体工作, not
|
|
62
|
+
一项命名工作; and 卡住的锁如何恢复, not 锁状态的恢复出口.
|
|
63
|
+
6. When turning a row off makes an operation impossible under the current
|
|
64
|
+
settings, name that operation (planning, entering Tower mode, merging) and how
|
|
65
|
+
the user clears the requirement. Never claim a blocked operation keeps
|
|
66
|
+
working: a reviewer preset needed for merging is a precondition, not an
|
|
67
|
+
optional extra, and test evidence that cannot be recorded keeps merges blocked
|
|
68
|
+
until the requirement is cleared.
|
|
69
|
+
7. Describe the review policy as one tri-state control (off / optional /
|
|
70
|
+
required), never as three rows and never as a capability that is permanently
|
|
71
|
+
on. In required mode it is a merge precondition; in off mode review is
|
|
72
|
+
intentionally unavailable. The deprecated boolean alias maps true → required,
|
|
73
|
+
false → optional, and unset → optional.
|
package/PRINCIPLES.md
CHANGED
|
@@ -140,18 +140,26 @@ Enforced by: `docs/operations/incident.md`, `scripts/regression-audit.mjs`, and
|
|
|
140
140
|
A component may exist only for a capability whose off state leaves the rest of the
|
|
141
141
|
product intact. Every capability that qualifies gets a component, so the user can
|
|
142
142
|
control it. Every capability that does not — the host, the health contract, the
|
|
143
|
-
recovery escape, the
|
|
144
|
-
|
|
143
|
+
recovery escape, the merge gates — is configuration or code, never a switch: not
|
|
144
|
+
as a row, and not as a boolean that removes it.
|
|
145
|
+
|
|
146
|
+
The review policy is the deliberate middle case: it is one tri-state configuration
|
|
147
|
+
control (`off`, `optional`, `required`) on the plugin page, not a component row and
|
|
148
|
+
never one row per mode. Its off mode is documented and supported, while the review
|
|
149
|
+
gate in required mode — the clean, tip-bound review evidence and its exemption
|
|
150
|
+
path — remains non-disableable. A row may not own the policy, and the reviewer row
|
|
151
|
+
is not mutually exclusive with it.
|
|
145
152
|
|
|
146
153
|
Refused because of this: a single all-or-nothing switch offered as "simpler"; a
|
|
147
154
|
disableable `tower_doctor`/`tower_recover`; a core feature exposed as a toggle; a
|
|
148
|
-
component whose off state is worse than uninstalling the plugin
|
|
155
|
+
component whose off state is worse than uninstalling the plugin; a review policy
|
|
156
|
+
exposed as a row or as three mutually exclusive rows.
|
|
149
157
|
|
|
150
158
|
Enforced by: `scripts/check-components.mjs` (locale coverage, the
|
|
151
|
-
forbidden-to-disable denylist and the generated
|
|
152
|
-
`scripts/surface-baseline.mjs` (the component count is a
|
|
153
|
-
`docs/adr/0005-component-surface.md`, and
|
|
154
|
-
`npm run docs:check:components`.
|
|
159
|
+
forbidden-to-disable denylist, the review-policy control and the generated
|
|
160
|
+
component reference), `scripts/surface-baseline.mjs` (the component count is a
|
|
161
|
+
baselined surface), `docs/adr/0005-component-surface.md`, and
|
|
162
|
+
`tests/components.test.mjs`; gate: `npm run docs:check:components`.
|
|
155
163
|
|
|
156
164
|
## Non-goals
|
|
157
165
|
|
package/README.en.md
CHANGED
|
@@ -16,7 +16,7 @@ The repository root **is** the DSH bundle: `package.json` declares `dsh.bundle.p
|
|
|
16
16
|
|
|
17
17
|
- **Configurable Tower mode.** `/tower` toggles a session-level stance that narrows the tool surface and injects the Tower protocol. `/tower on|off <instruction>` also forwards your instruction to the model in the same step. By default the model may also enter/leave with `tower_control` and a concrete reason; set `mode.allowAgentControl:false` for human-only control.
|
|
18
18
|
- **Missions with real isolation.** Every mission gets its own branch and Git worktree; workers never touch your checkout.
|
|
19
|
-
- **Review-gated merges.**
|
|
19
|
+
- **Review-gated merges.** The review policy is one tri-state control on the plugin page: `off`, `optional` (the default) or `required` (`review.mode` in config). With `required`, a reviewer bound to the current branch tip must submit a `clean` / `merge` verdict before merge; the merge gate re-checks scope, staleness, task completion, review history, and worktree ownership before a `--no-ff` merge. Independent review is optional by default; use `reviewPolicy:"none"` + reason plus `gates.allowReviewExemption:true` for a single mission when the policy is required. Multi-route review is recorded per round: while a non-clean round keeps the mission `active`, a later reviewer may still submit its verdict for the same reviewed tip and it appends to that round. A completed `survey` dependency satisfies the dependency gate (a build dependency still must be merged). Upgrade note: the old boolean review toggle is a one-release alias; the mapping is in [install, upgrade and migration](docs/guide/install-upgrade-migration.md).
|
|
20
20
|
- **Durable coordination.** Roster, inbox, findings, reviews, and mission state are persisted under `.git/dsh-tower/`, so a restart does not lose the plan.
|
|
21
21
|
- **Bounded state growth.** Terminal missions whose worktree is gone move to `.git/dsh-tower/archive/` once `archive.afterDays`/`archive.maxMissions` says so, keeping their tasks, reviews, findings and completion report; mutations then rewrite only active work. `tower_status includeArchived` lists compact archived summaries, and `tower_doctor` reports archive size, count and policy.
|
|
22
22
|
- **Safe by default.** While Tower mode is active, `create_goal`, `update_goal(resume)`, `todo_write`, the Team task board, and `spawn_teammate` are refused; a paused goal is never resumed automatically.
|
|
@@ -100,10 +100,10 @@ tower_init repoRoot=D:\work\repo # records the base (current bra
|
|
|
100
100
|
tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement","commit"],"kind":"build"}]
|
|
101
101
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
|
|
102
102
|
# the worker implements and commits in its worktree, calls tower_complete; its final report returns here
|
|
103
|
-
# Optional:
|
|
103
|
+
# Optional: set review.mode:required first if you need mandatory independent review
|
|
104
104
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
|
|
105
105
|
# the reviewer inspects read-only and calls tower_review (clean+merge, or p1-Nitems/p2-Nitems)
|
|
106
|
-
tower_preflight repoRoot=D:\work\repo missionId=M1 # review optional by default; with review.
|
|
106
|
+
tower_preflight repoRoot=D:\work\repo missionId=M1 # review optional by default (review.mode:optional); with review.mode:required expect no-review before review and ok:true after
|
|
107
107
|
tower_merge repoRoot=D:\work\repo missionId=M1
|
|
108
108
|
tower_teardown repoRoot=D:\work\repo
|
|
109
109
|
|
|
@@ -138,18 +138,19 @@ planning tools, `tower_status`, and the remaining tools keep working.
|
|
|
138
138
|
|
|
139
139
|
Mode entry and `tower_plan` validate the required presets before work starts. The
|
|
140
140
|
worker preset is mandatory; the reviewer preset is required only when
|
|
141
|
-
`review.
|
|
142
|
-
optional and is locked with an explanatory `preset-missing` error when
|
|
143
|
-
is
|
|
144
|
-
`tower_doctor` and `tower_recover` are not components and
|
|
145
|
-
are registered for every configuration (#122).
|
|
141
|
+
`review.mode` is `required`, so the reviewer row may be off while the policy is
|
|
142
|
+
`off` or `optional` and is locked with an explanatory `preset-missing` error when
|
|
143
|
+
the policy is `required`. `tower-tests` off with `gates.requireTests:true` is
|
|
144
|
+
refused the same way. `tower_doctor` and `tower_recover` are not components and
|
|
145
|
+
have no config key: they are registered for every configuration (#122).
|
|
146
146
|
|
|
147
147
|
[Principle 11](PRINCIPLES.md) and
|
|
148
148
|
[ADR 0005](docs/adr/0005-component-surface.md) say a component may exist only for
|
|
149
|
-
a capability whose off state leaves the rest intact; the host, health, recovery
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
a capability whose off state leaves the rest intact; the host, health, recovery
|
|
150
|
+
and merge-gate capabilities are never switches. The review policy is the one
|
|
151
|
+
tri-state configuration control (off/optional/required), not a row. The per-key
|
|
152
|
+
answer is the generated [component surface](docs/reference/component-surface.md),
|
|
153
|
+
enforced by `npm run docs:check:components`.
|
|
153
154
|
|
|
154
155
|
## Configuration
|
|
155
156
|
|
|
@@ -164,7 +165,7 @@ generated [component surface](docs/reference/component-surface.md), enforced by
|
|
|
164
165
|
autoPauseGoal: true
|
|
165
166
|
denyTools: [create_goal, todo_write, team_task_create, team_task_update, team_task_reopen, spawn_teammate]
|
|
166
167
|
review:
|
|
167
|
-
|
|
168
|
+
mode: optional # one tri-state control: off, optional (default) or required
|
|
168
169
|
maxRoundsPerReviewer: 5
|
|
169
170
|
merge:
|
|
170
171
|
timeoutMs: 900000
|
|
@@ -179,7 +180,7 @@ generated [component surface](docs/reference/component-surface.md), enforced by
|
|
|
179
180
|
- `mode.allowAgentControl` defaults to `true` in code; the bundled `cordis.patch.yml` also sets it explicitly to `true` so the model may enter/leave when the user intent or task state warrants it, always stating a reason. Set it to `false` for human-only control. The bundled patch also enables `mode.workspaceLock` (single-active tower), `send.interruptDefault`, `init.autoGitInit`, and sets `spawn.staleMs` to 15s.
|
|
180
181
|
- `/tower base <branch>` records a session-level init base preference; `/tower on` clears an active plan mode (`mode.planModePolicy:"refuse"` instead refuses entry), keeping plan and Tower mutually exclusive.
|
|
181
182
|
- Repository-level config: put the same JSON shape in `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/config.json`) to override `review`/`merge`/`worktrees`/`spawn`. Precedence is code defaults < profile config < repository config < tool arguments. `mode.*` is session-scoped and only the profile layer controls it.
|
|
182
|
-
- Every key and default is in [docs/reference/config.md](docs/reference/config.md); unknown keys are rejected (top-level and inside a group) so typos cannot silently disable security-relevant settings. Tool error codes are in [docs/reference/errors.md](docs/reference/errors.md).
|
|
183
|
+
- Every key and default is in [docs/reference/config.md](docs/reference/config.md); unknown keys are rejected (top-level and inside a group) so typos cannot silently disable security-relevant settings. The three review modes, where the plugin-page control lives, and the one-release alias migration are in [install, upgrade and migration](docs/guide/install-upgrade-migration.md). Tool error codes are in [docs/reference/errors.md](docs/reference/errors.md).
|
|
183
184
|
|
|
184
185
|
## Tools (29: 22 core + 7 optional)
|
|
185
186
|
|
|
@@ -190,7 +191,7 @@ The list above is the 22 core tools plus 5 of the component-owned optional tools
|
|
|
190
191
|
## Safety and guardrails
|
|
191
192
|
|
|
192
193
|
- **No merge without a green gate.** `tower_preflight` is read-only and reports stable blocker codes; `tower_merge` refuses when any blocker remains.
|
|
193
|
-
- **Review is bound to a commit when
|
|
194
|
+
- **Review is bound to a commit when required.** With `review.mode:required`, a moved tip reports `tip-moved` and requires a fresh review; reviewers use a read-only preset, and a branch change during review is rejected. `off` is a documented mode that removes `tower_review`; `optional` records reviews but never blocks a merge.
|
|
194
195
|
- **Scope enforcement.** A mission may only change files matched by its declared scope globs; `out-of-scope` blocks the merge.
|
|
195
196
|
- **Worktree ownership.** Dirty worktrees, a running worker, or a branch owned by a closed mission block teardown/merge.
|
|
196
197
|
- **Goal isolation.** Entering Tower mode pauses the active goal and records it; leaving does not resume it. Use `/tower off` and resume the goal yourself.
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Tower 模式与 mission/审查工作流的灵感来自 Moonshot AI 的 **Kimi Co
|
|
|
16
16
|
|
|
17
17
|
- **可配置的 Tower 模式。** `/tower` 切换会话级模式:收窄工具面并注入 Tower 协议;`/tower on|off <指令>` 还能把指令作为下一步用户消息转发给模型。默认也允许 Agent 通过 `tower_control` 带理由进出;设 `mode.allowAgentControl:false` 可改为仅人类控制。
|
|
18
18
|
- **任务级隔离。** 每个 mission 使用独立分支和 Git worktree,worker 不会碰到你的主检出。
|
|
19
|
-
- **审查门禁合并。**
|
|
19
|
+
- **审查门禁合并。** 审查策略是插件页上的一个三态控件:`off`、`optional`(默认)或 `required`(配置键 `review.mode`)。设为 `required` 时,审查者针对当前分支 tip 给出 `clean` / `merge` 结论;合并门禁重新校验 scope、分支新鲜度、任务完成度、审查记录与 worktree 归属,通过后才执行 `--no-ff` 合并。独立审查默认可选;策略为 `required` 时,单个 mission 可用 `reviewPolicy:"none"` + 理由并开启 `gates.allowReviewExemption:true` 做更窄豁免。升级提示:旧的布尔审查开关是保留一个版本的别名;映射关系见[安装、升级与迁移](docs/zh/guide/install-upgrade-migration.md)。
|
|
20
20
|
- **可恢复的协作状态。** roster、inbox、findings、reviews 与 mission 状态持久化在 `.git/dsh-tower/` 下,重启不会丢计划。
|
|
21
21
|
- **受控的状态增长。** worktree 已移除的终态任务在达到 `archive.afterDays`/`archive.maxMissions` 后移入 `.git/dsh-tower/archive/`,保留 tasks、reviews、findings 与完成报告;此后 mutation 只重写活跃工作。`tower_status includeArchived` 列出压缩后的归档摘要,`tower_doctor` 报告归档大小、数量与策略。
|
|
22
22
|
- **默认安全。** Tower 模式开启期间拒绝 `create_goal`、`update_goal(resume)`、`todo_write`、Team 任务板与 `spawn_teammate`;被暂停的 goal 不会被自动恢复。
|
|
@@ -99,10 +99,10 @@ tower_init repoRoot=D:\work\repo # 首次初始化,记录 base
|
|
|
99
99
|
tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement","commit"],"kind":"build"}]
|
|
100
100
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
|
|
101
101
|
# worker 在 worktree 内实现并提交,调用 tower_complete;最终报告会返回本会话
|
|
102
|
-
#
|
|
102
|
+
# 可选:需要强制独立审查时,先设 review.mode:required
|
|
103
103
|
tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
|
|
104
104
|
# reviewer 只读审查并调用 tower_review(clean+merge 或 p1-Nitems/p2-Nitems)
|
|
105
|
-
tower_preflight repoRoot=D:\work\repo missionId=M1 #
|
|
105
|
+
tower_preflight repoRoot=D:\work\repo missionId=M1 # 默认审查可选(review.mode:optional);设为 review.mode:required 时审查前预期 no-review,审查后 ok:true
|
|
106
106
|
tower_merge repoRoot=D:\work\repo missionId=M1
|
|
107
107
|
tower_teardown repoRoot=D:\work\repo
|
|
108
108
|
|
|
@@ -125,6 +125,12 @@ tower_teardown repoRoot=D:\work\repo
|
|
|
125
125
|
|
|
126
126
|
图片与文件附件会随指令一起作为一条完整的 `UserMessage` 转发。
|
|
127
127
|
|
|
128
|
+
运行时前置条件(preset 行、组件行与配置的冲突)只在**进入**模式的路径上重新校验:
|
|
129
|
+
裸 `/tower`、`/tower on`、`/tower on <指令>`,以及模式关闭时的 `/tower <指令>`。已经开启
|
|
130
|
+
模式后的 `/tower <指令>`、`/tower off`、`/tower off <指令>` 与 `/tower ack` 不再重复校验,
|
|
131
|
+
因此会话中途关掉某一行之后,人仍然退得出去、指令也仍然转发得出去;`tower_spawn`、规划与
|
|
132
|
+
合并/预检各自保留自己的护栏。详见 [组件行与消息语言](docs/guide/quickstart.md#component-rows-runtime-preconditions-and-message-language)。
|
|
133
|
+
|
|
128
134
|
## 组件模型
|
|
129
135
|
|
|
130
136
|
本 bundle 采用 **强制 host 锚点 + 可单独启停的可选行**(#121):`cordis.patch.yml` 插入顶层
|
|
@@ -134,13 +140,15 @@ tower_teardown repoRoot=D:\work\repo
|
|
|
134
140
|
与其余工具继续工作。
|
|
135
141
|
|
|
136
142
|
进入 Tower 模式与 `tower_plan` 前会校验必需的 preset。worker preset 是强制依赖;reviewer
|
|
137
|
-
preset 只在 `review.
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
preset 只在 `review.mode` 为 `required` 时必需,因此策略为 `off` 或 `optional` 时允许关闭
|
|
144
|
+
reviewer 行,策略为 `required` 时会以带解释的 `preset-missing` 锁定。`tower-tests` 关闭且
|
|
145
|
+
`gates.requireTests:true` 时同样拒绝。`tower_doctor` 与 `tower_recover` 不是组件、也没有配置键:
|
|
146
|
+
任何配置都会注册它们(#122)。
|
|
140
147
|
|
|
141
148
|
原则 11([PRINCIPLES.md](PRINCIPLES.md))与
|
|
142
149
|
[ADR 0005](docs/adr/0005-component-surface.md) 要求:只有关闭后仍不破坏其余能力的 capability
|
|
143
|
-
才能成为组件;host
|
|
150
|
+
才能成为组件;host、健康检查、恢复通道与合并门禁永远不是开关。审查策略是唯一的
|
|
151
|
+
三态配置控件(off/optional/required),不是组件行。逐键答案见生成的
|
|
144
152
|
[组件表面](docs/reference/component-surface.md),由 `npm run docs:check:components` 强制。
|
|
145
153
|
|
|
146
154
|
## 配置
|
|
@@ -156,7 +164,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
|
|
|
156
164
|
autoPauseGoal: true
|
|
157
165
|
denyTools: [create_goal, todo_write, team_task_create, team_task_update, team_task_reopen, spawn_teammate]
|
|
158
166
|
review:
|
|
159
|
-
|
|
167
|
+
mode: optional # 一个三态控件:off、optional(默认)或 required
|
|
160
168
|
maxRoundsPerReviewer: 5
|
|
161
169
|
merge:
|
|
162
170
|
timeoutMs: 900000
|
|
@@ -171,7 +179,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
|
|
|
171
179
|
- 代码中 `mode.allowAgentControl` 默认 `true`;随包 `cordis.patch.yml` 也显式设为 `true`:模型可在用户意图/任务状态明确时自行进出,但必须给出理由;设为 `false` 时只有人类能切换。随包 patch 还默认开启 `mode.workspaceLock`(单活塔)、`send.interruptDefault`、`init.autoGitInit`,并把 `spawn.staleMs` 设为 15s。
|
|
172
180
|
- `/tower base <branch>` 可设置会话级 init base 偏好;`/tower on` 会清除活跃 plan mode(`mode.planModePolicy:"refuse"` 则拒绝进入),保证 plan/tower 互斥。
|
|
173
181
|
- 仓库级配置:在 `<git-common-dir>/dsh-tower/config.json`(通常是 `.git/dsh-tower/config.json`)放入同样的 JSON 结构,可覆盖 `review`/`merge`/`worktrees`/`spawn`;优先级为代码默认 < profile < 仓库配置 < 工具参数。`mode.*` 属于会话级,只在 profile 生效。
|
|
174
|
-
- 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md)
|
|
182
|
+
- 所有键与默认值见 [docs/reference/config.md](docs/reference/config.md);未知键(顶层与分组内)都会被拒绝,防止拼写错误静默失效。三种审查模式、插件页控件位置与单版本别名迁移见[安装、升级与迁移](docs/zh/guide/install-upgrade-migration.md)。工具错误码见 [docs/reference/errors.md](docs/reference/errors.md)。
|
|
175
183
|
|
|
176
184
|
## 工具(29 个:22 个核心 + 7 个可选)
|
|
177
185
|
|
|
@@ -182,7 +190,7 @@ preset 只在 `review.requireReview` 为 true 时必需,因此审查可选时
|
|
|
182
190
|
## 安全与约束
|
|
183
191
|
|
|
184
192
|
- **门禁不全绿不合并。** `tower_preflight` 是只读检查,返回稳定的 blocker code;存在 blocker 时 `tower_merge` 拒绝合并。
|
|
185
|
-
- **审查绑定提交。**
|
|
193
|
+
- **审查绑定提交。** 策略为 `review.mode:required` 时,审查后分支 tip 变化会报 `tip-moved`,必须重新审查;reviewer 使用只读 preset,若在审查期间改动分支,提交审查会被拒绝。`off` 是受支持的模式,会移除 `tower_review`;`optional` 记录审查但从不阻塞合并。非 clean 的一轮审查会让 mission 回到 active,此后其他 route 仍可为同一 reviewed tip 提交结论并追加到同一轮;依赖为 `completed` 的 survey 视为已满足(build 依赖仍需 merged)。
|
|
186
194
|
- **scope 强制。** mission 只能修改其 scope glob 匹配的文件,越界会以 `out-of-scope` 阻止合并。
|
|
187
195
|
- **worktree 归属。** 脏工作树、运行中的 worker、已关闭 mission 的分支都会阻止合并或 teardown。
|
|
188
196
|
- **goal 隔离。** 进入 Tower 模式会暂停活跃 goal 并记录;退出不会自动恢复,需要你自行恢复。
|
package/app/archive.js
CHANGED
|
@@ -94,6 +94,21 @@ export function archiveView(index) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* #135: archival is the `tower-archive` component row, not a config switch. The
|
|
99
|
+
* probe is supplied by the composition root. A Tower built without a component
|
|
100
|
+
* probe (unit fixtures, direct adapter use) keeps archiving: an absent probe means
|
|
101
|
+
* "not composed", not "row disabled".
|
|
102
|
+
*
|
|
103
|
+
* @param {any} tower
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function archiveEnabled(tower) {
|
|
107
|
+
const components = tower.deps?.components
|
|
108
|
+
if (!components || typeof components.enabled !== 'function') return true
|
|
109
|
+
return components.enabled('tower-archive') === true
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
/**
|
|
98
113
|
* Move the eligible terminal missions out of `state` and into the archive.
|
|
99
114
|
* Mutates `state` in memory (missions, linked reviews and closed findings) and
|
|
@@ -105,8 +120,8 @@ export function archiveView(index) {
|
|
|
105
120
|
* @returns {Promise<{ archived: string[], written: boolean, files?: string[], error: string | null, year?: number }>}
|
|
106
121
|
*/
|
|
107
122
|
export async function sweepArchive(tower, state, options = {}) {
|
|
123
|
+
if (!archiveEnabled(tower)) return { archived: [], written: false, error: null }
|
|
108
124
|
const policy = tower.config.archive ?? {}
|
|
109
|
-
if (policy.enabled === false) return { archived: [], written: false, error: null }
|
|
110
125
|
const now = options.now ?? Date.now()
|
|
111
126
|
const activity = options.activity ?? {}
|
|
112
127
|
|
package/app/board.js
CHANGED
|
@@ -8,8 +8,23 @@ export function boardFile(tower) {
|
|
|
8
8
|
: path.join(tower.paths.dir, 'MISSIONS.md')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* #133: the board is the `tower-board` component row, not a config switch. The
|
|
13
|
+
* probe is supplied by the composition root. A Tower built without a component
|
|
14
|
+
* probe (unit fixtures, direct adapter use) keeps writing the board: an absent
|
|
15
|
+
* probe means "not composed", not "row disabled".
|
|
16
|
+
*
|
|
17
|
+
* @param {any} tower
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function boardEnabled(tower) {
|
|
21
|
+
const components = tower.deps?.components
|
|
22
|
+
if (!components || typeof components.enabled !== 'function') return true
|
|
23
|
+
return components.enabled('tower-board') === true
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
export async function writeBoard(tower) {
|
|
12
|
-
if (tower
|
|
27
|
+
if (!boardEnabled(tower)) return null
|
|
13
28
|
try {
|
|
14
29
|
const state = await tower.load()
|
|
15
30
|
const index = await tower.archiveIndex()
|
package/app/gate.js
CHANGED
|
@@ -91,6 +91,19 @@ export async function evaluateGate(tower, input) {
|
|
|
91
91
|
|
|
92
92
|
const branch = worktree ? worktree.branch : null
|
|
93
93
|
const latest = latestReview(reviewsForBranch(reviews, branch))
|
|
94
|
+
// #140: `mergeGateBlockers` is pure and may not read the component registry, so
|
|
95
|
+
// the composition root publishes `deps.components` and this layer snapshots the
|
|
96
|
+
// one fact the gate needs. Absent (unit tests, custom port bags) means unknown,
|
|
97
|
+
// and the blocker keeps its generic wording.
|
|
98
|
+
const components = /** @type {{ enabled?: (id: string) => boolean, reason?: (id: string) => string } | undefined} */ (
|
|
99
|
+
tower.deps?.components
|
|
100
|
+
)
|
|
101
|
+
const testsComponent = components
|
|
102
|
+
? {
|
|
103
|
+
enabled: typeof components.enabled === 'function' ? components.enabled('tower-tests') === true : false,
|
|
104
|
+
reason: typeof components.reason === 'function' ? components.reason('tower-tests') : 'disabled by component tower-tests',
|
|
105
|
+
}
|
|
106
|
+
: null
|
|
94
107
|
for (const blocker of mergeGateBlockers({
|
|
95
108
|
mission,
|
|
96
109
|
roster: state.roster ?? [],
|
|
@@ -102,6 +115,7 @@ export async function evaluateGate(tower, input) {
|
|
|
102
115
|
parentStatus: input.parentStatus,
|
|
103
116
|
inactiveStates: ROSTER_INACTIVE_STATES,
|
|
104
117
|
rebaseWaived: latest ? tower.rebaseWaived(mission, latest) : false,
|
|
118
|
+
testsComponent,
|
|
105
119
|
})) push(blocker.code, blocker.detail)
|
|
106
120
|
|
|
107
121
|
const checkoutBranch = await tower.deps.vcs.currentBranch(tower.root)
|
package/app/review.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { sweepArchive } from './archive.js'
|
|
3
3
|
import { validateState } from '../domain/state.js'
|
|
4
4
|
import { TowerToolError } from '../domain/errors.js'
|
|
5
|
+
import { reviewUnavailable } from '../domain/integration/review.js'
|
|
5
6
|
import {
|
|
6
7
|
assertReviewableMission,
|
|
7
8
|
resolveReviewActor,
|
|
@@ -78,6 +79,12 @@ async function prepareReview(tower, input) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export async function submitReview(tower, input) {
|
|
82
|
+
// #M39: `review.mode:off` means review is intentionally unavailable. The tool
|
|
83
|
+
// is not registered in that profile, but the repository layer can also turn it
|
|
84
|
+
// off, so the app layer refuses before any state is read.
|
|
85
|
+
if (reviewUnavailable(tower.config)) {
|
|
86
|
+
throw new TowerToolError('review-disabled', 'review.mode is off, so tower_review cannot record a verdict')
|
|
87
|
+
}
|
|
81
88
|
// Two-phase (#51): the lock is held only for the state reload, revision
|
|
82
89
|
// revalidation, and write; every git call happens in prepareReview.
|
|
83
90
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
package/app/spawn-prechecks.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// service/provider/lease validation. Split out of tools/spawn.js; no I/O.
|
|
3
3
|
import { TowerToolError } from '../domain/errors.js'
|
|
4
4
|
import { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
|
|
5
|
+
import { reviewUnavailable } from '../domain/integration/review.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Pick a unique roster name before any activation.
|
|
@@ -27,10 +28,19 @@ export function resolveRosterName({ role, missionId, requested, roster }) {
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Role/mission preconditions checked before any provisioning.
|
|
30
|
-
*
|
|
31
|
+
* `config` is the repository-resolved Tower config (#M39): with
|
|
32
|
+
* `review.mode:off` there is no review to dispatch, so the reviewer role is
|
|
33
|
+
* refused before a worktree, preset or activation is touched.
|
|
34
|
+
* @param {{ role: string, context: any, config?: any }} input
|
|
31
35
|
*/
|
|
32
|
-
export function assertSpawnableMission({ role, context }) {
|
|
36
|
+
export function assertSpawnableMission({ role, context, config }) {
|
|
33
37
|
if (role === 'reviewer') {
|
|
38
|
+
if (reviewUnavailable(config)) {
|
|
39
|
+
throw new TowerToolError(
|
|
40
|
+
'review-disabled',
|
|
41
|
+
'review.mode is off, so review is unavailable and a reviewer cannot be spawned',
|
|
42
|
+
)
|
|
43
|
+
}
|
|
34
44
|
if (!context.mission.worktree) {
|
|
35
45
|
throw new TowerToolError('reviewer-prerequisite', 'reviewer needs an existing mission worktree; spawn a worker first')
|
|
36
46
|
}
|
|
@@ -132,10 +142,12 @@ export async function precheckWorkerOwnership({
|
|
|
132
142
|
/**
|
|
133
143
|
* Service/provider/preset/lease checks, run before provisioning.
|
|
134
144
|
* `inspectLeases` is injected by the composition root so the app layer never
|
|
135
|
-
* imports the lease adapter (#69).
|
|
136
|
-
*
|
|
145
|
+
* imports the lease adapter (#69). `leaseComponent` is the matching probe for the
|
|
146
|
+
* `tower-leases` row (#131): when the row is off there is no `tower_resource`
|
|
147
|
+
* tool, so a missing lease cannot be fixed by following the old advice.
|
|
148
|
+
* @param {{ tower: any, ctx: any, config: any, preset: string, actor: any, exec: any, inspectLeases: any, leaseComponent?: () => { enabled: boolean, reason: string } }} input
|
|
137
149
|
*/
|
|
138
|
-
export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases }) {
|
|
150
|
+
export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases, leaseComponent }) {
|
|
139
151
|
const subagents = ctx.get('subagents')
|
|
140
152
|
if (!subagents) throw new TowerToolError('spawn-unavailable', 'subagents service is unavailable; cannot spawn')
|
|
141
153
|
const providers = subagents.list()
|
|
@@ -150,6 +162,19 @@ export async function precheckSpawnServices({ tower, ctx, config, preset, actor,
|
|
|
150
162
|
}
|
|
151
163
|
if (typeof config.spawn.requireLease === 'string' && config.spawn.requireLease.trim()) {
|
|
152
164
|
const requiredLease = config.spawn.requireLease.trim()
|
|
165
|
+
// #131: registration-aware. Telling a caller to "acquire it with
|
|
166
|
+
// tower_resource" is useless when the tower-leases row is off and the tool is
|
|
167
|
+
// not registered; name the component and both remedies instead.
|
|
168
|
+
const leasesRow = typeof leaseComponent === 'function' ? leaseComponent() : null
|
|
169
|
+
if (leasesRow && leasesRow.enabled !== true) {
|
|
170
|
+
// Refuse before reading the ledger: a lease left over from when the row was
|
|
171
|
+
// on must not let a spawn through, and the ledger cannot be fixed without
|
|
172
|
+
// the unregistered tool (issue #131, evidence note 4).
|
|
173
|
+
throw new TowerToolError(
|
|
174
|
+
'lease-busy',
|
|
175
|
+
`spawn.requireLease="${requiredLease}" cannot be satisfied: ${leasesRow.reason ?? 'the tower-leases component is disabled'}, so tower_resource is not registered; re-enable the tower-leases row or clear spawn.requireLease`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
153
178
|
const info = await inspectLeases(tower.commonDir)
|
|
154
179
|
const lease = info.leases.find((entry) => entry.name === requiredLease && entry.expired !== true)
|
|
155
180
|
if (!lease) {
|
package/app/tower.js
CHANGED
|
@@ -8,6 +8,7 @@ import { assertPorts } from '../ports/index.js'
|
|
|
8
8
|
import { validateCreate as validateBatchCreate, validateClose as validateBatchClose, summarize as summarizeBatch } from '../domain/batch.js'
|
|
9
9
|
|
|
10
10
|
import { rebaseWaived } from '../domain/gate.js'
|
|
11
|
+
import { isReviewExempt } from '../domain/integration/review.js'
|
|
11
12
|
import { DEFAULT_MERGE_TIMEOUT_MS, gitLockTimeoutMs } from '../domain/lockBudget.js'
|
|
12
13
|
export { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
|
|
13
14
|
|
|
@@ -508,11 +509,10 @@ export class Tower {
|
|
|
508
509
|
if (!mission.worktree) return null
|
|
509
510
|
const branch = mission.worktree.branch
|
|
510
511
|
const reviews = (state.reviews ?? []).filter((review) => review.branch === branch)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
String(mission.reviewExemptionReason ?? '').trim().length > 0)
|
|
512
|
+
// #M39: reconciliation follows the same policy as the gate — only
|
|
513
|
+
// `review.mode:required` demands review evidence; `off`/`optional` reconcile
|
|
514
|
+
// a landed branch without one.
|
|
515
|
+
const exempt = isReviewExempt(mission, this.config)
|
|
516
516
|
if (reviews.length === 0 && !exempt) return null
|
|
517
517
|
const latest = reviews.length > 0 ? reviews.reduce((a, b) => (a.round >= b.round ? a : b)) : null
|
|
518
518
|
if (latest && !exempt && (latest.status !== 'clean' || latest.mergeVerdict !== 'merge')) return null
|