dsh-tower 2.4.2 → 2.5.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 +40 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +5 -3
- package/GLOSSARY.md +67 -0
- package/README.md +6 -0
- package/app/archive.js +16 -1
- package/app/board.js +16 -1
- package/app/gate.js +14 -0
- package/app/spawn-prechecks.js +18 -3
- package/cordis.patch.yml +21 -6
- package/docs/adr/0004-surface-budget.md +10 -4
- package/docs/adr/0005-component-surface.md +166 -0
- package/docs/guide/install-upgrade-migration.md +21 -0
- package/docs/guide/quickstart.md +71 -0
- package/docs/guide/release.md +60 -19
- package/docs/reference/component-surface.md +27 -15
- package/docs/reference/config.generated.md +4 -7
- package/docs/reference/config.md +7 -5
- package/docs/reference/doctor.md +11 -5
- package/docs/reference/surface-budget.md +29 -11
- package/docs/reference/tools.generated.md +1 -1
- package/docs/reference/tools.md +2 -2
- package/docs/zh/guide/install-upgrade-migration.md +1 -1
- package/docs/zh/reference/config.md +7 -5
- package/docs/zh/reference/doctor.md +7 -2
- package/docs/zh/reference/tools.md +3 -2
- package/domain/component-rules.js +216 -0
- package/domain/components.js +169 -49
- package/domain/config/schema.js +46 -23
- package/domain/gate.js +17 -2
- 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/doctor.js +50 -5
- package/host/presets.js +17 -0
- package/host/tool-registry.js +21 -4
- 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 +11 -2
- package/scripts/check-components.mjs +582 -42
- package/scripts/component-surface.json +85 -27
- package/scripts/generate-capability-locale.mjs +3 -0
- package/scripts/mount-smoke/surface.mjs +2 -0
- package/scripts/pack-manifest.txt +9 -0
- package/scripts/release-notes.mjs +256 -0
- package/scripts/surface-baseline.json +32 -3
- package/scripts/surface-baseline.mjs +15 -5
- package/skills.js +4 -1
- package/tools/complete.js +3 -1
- package/tools/spawn.js +5 -5
- package/tools/test.js +7 -7
- package/ui-bundle/locale/en.json +1 -1
- package/ui-bundle/locale/zh.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@
|
|
|
4
4
|
|
|
5
5
|
## 未发布(Unreleased)
|
|
6
6
|
|
|
7
|
+
## 2.5.0(2026-11-01)
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- **#133 追踪看板成为可选能力**:新增 `tower-board` 组件行,关闭后不再写 `MISSIONS.md`,
|
|
12
|
+
`tower_status`/`tower_doctor` 说明关闭原因;`docs.boardLocation` 仍是位置调节项。
|
|
13
|
+
旧的 `docs.board` 配置键被删除,写入旧键会收到带迁移说明的 `config-unknown-key`。
|
|
14
|
+
- **#135 归档成为可选能力**:新增 `tower-archive` 组件行,关闭后终态任务保持热存储且不自动归档,
|
|
15
|
+
doctor 报告关闭后果;`archive.afterDays`/`archive.maxMissions` 仍是节奏调节项。
|
|
16
|
+
旧的 `archive.enabled` 配置键被删除。
|
|
17
|
+
- **#134 `tower-ui` 进入组件源**:`ui-bundle/cordis.patch.yml` 的 UI 行现在被组件源、locale 覆盖与
|
|
18
|
+
基线同时看到;门禁解析所有随包发布的 `cordis.patch.yml`。
|
|
19
|
+
- **#137 面向普通用户的双语标签与术语表**:新增 `GLOSSARY.md`(12 组中英术语 + 文案规则),
|
|
20
|
+
重写全部可见组件行的标题、用途与关闭后果;不再出现原始配置键、工具 id、斜杠命令或模块说明符。
|
|
21
|
+
|
|
22
|
+
### 变更
|
|
23
|
+
|
|
24
|
+
- **#138 组件门禁改为失败关闭**:键分类、行为开关、行覆盖、随包补丁与双向计数面全部机器校验;
|
|
25
|
+
双向面的每次计数决策必须带日期;`OPERATIONAL_TOOLS` 直接导入真实注册表,不再靠正则猜测。
|
|
26
|
+
- **#136 删除 `tests.enabled` 第二开关**:`tower-tests` 组件行是唯一开关;`tower_test` 在行关闭时
|
|
27
|
+
报告 `disabled by component tower-tests` 并给出修复方式,不再误报“未配置”。
|
|
28
|
+
- **#132 组行关闭后果可见**:`tower` 组行的“关闭等于卸载 Tower”现在显示在 Plugins 页面的描述里,
|
|
29
|
+
不再只是不可达的 locale 文本。
|
|
30
|
+
- **#141 HMR 明确定位为开发工具**:`tower-hmr` 保持默认关闭,文案改为“开发热重载(开发工具)”。
|
|
31
|
+
- 配置面 55 → 53 键;可见组件 11 个;新增 `tower-board`/`tower-archive` 行 locale 与打包清单条目。
|
|
32
|
+
|
|
33
|
+
### 修复
|
|
34
|
+
|
|
35
|
+
- **#131 租约组合提前拒绝**:`tower-leases` 关闭且 `spawn.requireLease` 非空时,配置/预检阶段直接
|
|
36
|
+
拒绝并说明行与两项修复方式;`lease-busy` 只指向真实注册的工具,`tower_doctor` 交叉检查该组合。
|
|
37
|
+
- **#139 能力卸载不留残影**:skills provider 注册与 Tower 模式提示段都通过 `ctx.effect` 释放,
|
|
38
|
+
热卸载后 `componentReport` 与真实挂载状态一致。
|
|
39
|
+
- **#140 禁用行原因一致**:`tests-missing` 合并阻塞项点名 `tower-tests` 行;机器输出的英文契约写入文档。
|
|
40
|
+
- **#142 `/tower` 预检不再误伤转发输入**:运行前置条件只在进入模式/切换时重新校验,
|
|
41
|
+
转发指令与 `/tower off` 在无关行关闭时继续工作。
|
|
42
|
+
- **F16 作用域重叠判定补全**:`scopesOverlap` 在保留文档化近似的同时加入精确 token-NFA 交集,
|
|
43
|
+
修掉 `a/b*` 与 `a/bc*` 被误判为不重叠的问题。
|
|
44
|
+
- **F17 配置解析一遍收敛**:`resolveTowerConfig` 现在对空输入也满足严格一遍幂等。
|
|
45
|
+
- **F23 术语表随包发布**:`GLOSSARY.md` 进入 `package.json` `files` 与打包清单(293 个文件)。
|
|
46
|
+
|
|
7
47
|
## 2.4.2(2026-10-24)
|
|
8
48
|
|
|
9
49
|
### 修复
|
package/COMPATIBILITY.md
CHANGED
|
@@ -5,6 +5,7 @@ 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.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
9
|
| 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
10
|
| 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
11
|
| 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 (696 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,67 @@
|
|
|
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
|
+
| lease | lease | 租约 | Advisory resource coordination. Never 咨询式租约: the lease coordinates, it does not consult. |
|
|
32
|
+
| worker | worker | 执行代理 | The role that executes a mission. "Tower Worker" is a product name and stays as-is. |
|
|
33
|
+
| reviewer | reviewer | 审查代理 | The role that independently reviews a mission. "Tower Reviewer" is a product name and stays as-is. |
|
|
34
|
+
|
|
35
|
+
## Product names and other surfaces
|
|
36
|
+
|
|
37
|
+
Brand and product names are not translated: Tower, DSH, Tower Worker, Tower
|
|
38
|
+
Reviewer, Tower UI, MISSIONS.md. On first use in a sentence, gloss an unfamiliar
|
|
39
|
+
one parenthetically — for example "Tower Codex (Tower's earlier state format)".
|
|
40
|
+
|
|
41
|
+
The Web client has its own string catalogue (`ui-bundle/client.locales.js`).
|
|
42
|
+
When its wording disagrees with this table, this table wins and the client copy
|
|
43
|
+
is migrated separately under the same glossary.
|
|
44
|
+
|
|
45
|
+
## Copy rules
|
|
46
|
+
|
|
47
|
+
1. One sentence for the capability, one sentence for the off consequence.
|
|
48
|
+
2. No config-key syntax (`gates.requireTests:true`), no bare tool ids
|
|
49
|
+
(`tower_batch`), no slash commands (`/tower reload`) and no module specifiers
|
|
50
|
+
in a label field. Describe the capability and the effect instead.
|
|
51
|
+
3. Say "Tower mode", not just "模式", and "refused with an explanation", not
|
|
52
|
+
"locked": the user needs to know they will get the reason.
|
|
53
|
+
4. The off sentence must be truthful. A lease is coordination only, but a worker
|
|
54
|
+
configured to require one cannot start while the lease row is off — planning
|
|
55
|
+
and Tower-mode entry are refused with an explanation until the setting is
|
|
56
|
+
cleared or the row is re-enabled. Say that, and say that everything else keeps
|
|
57
|
+
working.
|
|
58
|
+
5. English and Chinese must agree field by field. Chinese copy reads as Chinese:
|
|
59
|
+
no untranslated English common nouns, no machine-translated calques — say
|
|
60
|
+
关闭该批次, not 收口; 当前状态文件, not 在线状态文件; 某一具体工作, not
|
|
61
|
+
一项命名工作; and 卡住的锁如何恢复, not 锁状态的恢复出口.
|
|
62
|
+
6. When turning a row off makes an operation impossible under the current
|
|
63
|
+
settings, name that operation (planning, entering Tower mode, merging) and how
|
|
64
|
+
the user clears the requirement. Never claim a blocked operation keeps
|
|
65
|
+
working: a reviewer preset needed for merging is a precondition, not an
|
|
66
|
+
optional extra, and test evidence that cannot be recorded keeps merges blocked
|
|
67
|
+
until the requirement is cleared.
|
package/README.md
CHANGED
|
@@ -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` 插入顶层
|
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/spawn-prechecks.js
CHANGED
|
@@ -132,10 +132,12 @@ export async function precheckWorkerOwnership({
|
|
|
132
132
|
/**
|
|
133
133
|
* Service/provider/preset/lease checks, run before provisioning.
|
|
134
134
|
* `inspectLeases` is injected by the composition root so the app layer never
|
|
135
|
-
* imports the lease adapter (#69).
|
|
136
|
-
*
|
|
135
|
+
* imports the lease adapter (#69). `leaseComponent` is the matching probe for the
|
|
136
|
+
* `tower-leases` row (#131): when the row is off there is no `tower_resource`
|
|
137
|
+
* tool, so a missing lease cannot be fixed by following the old advice.
|
|
138
|
+
* @param {{ tower: any, ctx: any, config: any, preset: string, actor: any, exec: any, inspectLeases: any, leaseComponent?: () => { enabled: boolean, reason: string } }} input
|
|
137
139
|
*/
|
|
138
|
-
export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases }) {
|
|
140
|
+
export async function precheckSpawnServices({ tower, ctx, config, preset, actor, exec, inspectLeases, leaseComponent }) {
|
|
139
141
|
const subagents = ctx.get('subagents')
|
|
140
142
|
if (!subagents) throw new TowerToolError('spawn-unavailable', 'subagents service is unavailable; cannot spawn')
|
|
141
143
|
const providers = subagents.list()
|
|
@@ -150,6 +152,19 @@ export async function precheckSpawnServices({ tower, ctx, config, preset, actor,
|
|
|
150
152
|
}
|
|
151
153
|
if (typeof config.spawn.requireLease === 'string' && config.spawn.requireLease.trim()) {
|
|
152
154
|
const requiredLease = config.spawn.requireLease.trim()
|
|
155
|
+
// #131: registration-aware. Telling a caller to "acquire it with
|
|
156
|
+
// tower_resource" is useless when the tower-leases row is off and the tool is
|
|
157
|
+
// not registered; name the component and both remedies instead.
|
|
158
|
+
const leasesRow = typeof leaseComponent === 'function' ? leaseComponent() : null
|
|
159
|
+
if (leasesRow && leasesRow.enabled !== true) {
|
|
160
|
+
// Refuse before reading the ledger: a lease left over from when the row was
|
|
161
|
+
// on must not let a spawn through, and the ledger cannot be fixed without
|
|
162
|
+
// the unregistered tool (issue #131, evidence note 4).
|
|
163
|
+
throw new TowerToolError(
|
|
164
|
+
'lease-busy',
|
|
165
|
+
`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`,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
153
168
|
const info = await inspectLeases(tower.commonDir)
|
|
154
169
|
const lease = info.leases.find((entry) => entry.name === requiredLease && entry.expired !== true)
|
|
155
170
|
if (!lease) {
|
package/cordis.patch.yml
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Policy: principle 11 in PRINCIPLES.md + docs/adr/0005-component-surface.md.
|
|
10
10
|
# The per-key answer is docs/reference/component-surface.md, generated from
|
|
11
|
-
# scripts/component-surface.json. `npm run docs:check:components`
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# `
|
|
11
|
+
# scripts/component-surface.json. `npm run docs:check:components` reads every
|
|
12
|
+
# shipped patch (this file and ui-bundle/cordis.patch.yml, which ships the
|
|
13
|
+
# tower-ui row) and refuses a row with no classification, a missing locale label,
|
|
14
|
+
# or a moved component count without a written decision. `tower_doctor` and
|
|
15
|
+
# `tower_recover` are part of the host and are never switches (#122); a capability
|
|
16
|
+
# row owns exactly one optional capability, and `tower_status` / `tower_doctor`
|
|
17
|
+
# name any row that is off.
|
|
16
18
|
- insert:
|
|
17
19
|
# Mandatory host anchor. Not an optional capability: off == uninstall.
|
|
18
20
|
- id: tower
|
|
@@ -66,7 +68,20 @@
|
|
|
66
68
|
name: 'dsh-tower/leases'
|
|
67
69
|
- id: tower-tests
|
|
68
70
|
name: 'dsh-tower/tests'
|
|
69
|
-
#
|
|
71
|
+
# #133: the MISSIONS.md board is a capability, not the old `docs.board` key.
|
|
72
|
+
# Off writes no board and leaves an existing one to go stale; every tool and
|
|
73
|
+
# gate keeps working. `docs.boardLocation` stays genuine tuning.
|
|
74
|
+
- id: tower-board
|
|
75
|
+
name: 'dsh-tower/board'
|
|
76
|
+
# #135: archival is a capability, not the old `archive.enabled` key. Off keeps
|
|
77
|
+
# terminal missions hot in state.json, so the hot state keeps growing; merges,
|
|
78
|
+
# gates and every other capability keep working and nothing is deleted.
|
|
79
|
+
# `archive.afterDays` / `archive.maxMissions` stay genuine tuning.
|
|
80
|
+
- id: tower-archive
|
|
81
|
+
name: 'dsh-tower/archive'
|
|
82
|
+
# #141: developer tooling, not an end-user capability switch. It stays shipped
|
|
83
|
+
# off; the locale copy states that this row exists for linked-profile
|
|
84
|
+
# development only, so the plugin page does not read as a missing feature.
|
|
70
85
|
- id: tower-hmr
|
|
71
86
|
name: 'dsh-tower/hmr'
|
|
72
87
|
disabled: true
|
|
@@ -30,7 +30,9 @@ note for each niche key.
|
|
|
30
30
|
|
|
31
31
|
- Growth is visible in a diff instead of arriving one reasonable-looking key at a
|
|
32
32
|
time.
|
|
33
|
-
- A reduction is cheap
|
|
33
|
+
- A reduction is cheap for `sections` and `tools`: the baseline can be ratcheted
|
|
34
|
+
down with no ADR. Since #138 the config-key count is two-way like the component
|
|
35
|
+
count (see [ADR 0005](0005-component-surface.md)).
|
|
34
36
|
- The classification table is auditable, not decorative: the check fails when a
|
|
35
37
|
live key or tool is missing from it.
|
|
36
38
|
- The numbers are three lines in `tower_doctor` output, not a new dashboard.
|
|
@@ -53,8 +55,10 @@ extends the budget with a fourth — the number of user-visible component rows i
|
|
|
53
55
|
`cordis.patch.yml` — and records it as its own `components` baseline entry with a
|
|
54
56
|
two-way decision rule (a contraction needs a decision just like growth). The
|
|
55
57
|
three original counts are still measured by `npm run surface:check`; the
|
|
56
|
-
component count is validated by `npm run docs:check:components`.
|
|
57
|
-
|
|
58
|
+
component count is validated by `npm run docs:check:components`. ADR 0005's wave
|
|
59
|
+
3 (#138) also makes `configKeys` two-way, so the cheap-shrink consequence above
|
|
60
|
+
now applies only to `sections` and `tools`. The historical decision above is
|
|
61
|
+
unchanged: "three surfaces" describes the 2.3.0 rule.
|
|
58
62
|
|
|
59
63
|
## Rejected alternatives
|
|
60
64
|
|
|
@@ -65,6 +69,8 @@ decision above is unchanged: "three surfaces" describes the 2.3.0 rule.
|
|
|
65
69
|
- **A per-key count in each generated reference** — two references can disagree;
|
|
66
70
|
one shared baseline plus one check keeps the counts equal.
|
|
67
71
|
- **Only an ADR requirement** — a pure deletion would need paperwork even though it
|
|
68
|
-
shrinks the surface; the matching-deletion path keeps
|
|
72
|
+
shrinks the surface; the matching-deletion path keeps `sections`/`tools`
|
|
73
|
+
reductions cheap. The config-key surface is the exception since #138: a removed
|
|
74
|
+
switch is itself a capability decision, so it needs a dated record (ADR 0005).
|
|
69
75
|
- **A GitHub issue label instead of a repo file** — the brake must run offline in
|
|
70
76
|
`npm test` and in CI without a token.
|
|
@@ -128,3 +128,169 @@ this wave makes the shipped surface obey it instead of carrying debt.
|
|
|
128
128
|
- **A GitHub issue label or a docs-only policy.** The brake must run offline in
|
|
129
129
|
`npm test` and in CI without a token, and the policy must survive the closure
|
|
130
130
|
of the issue that introduced it.
|
|
131
|
+
|
|
132
|
+
## Addendum (wave 2): board, archive, UI and explicit development tooling
|
|
133
|
+
|
|
134
|
+
- Date: 2026-11-02
|
|
135
|
+
- Decisions: #132, #133, #134, #135, #141
|
|
136
|
+
|
|
137
|
+
Four components were component-shaped but sat outside the policy: `docs.board` and
|
|
138
|
+
`archive.enabled` were booleans in the config form, `tower-ui` shipped as a real
|
|
139
|
+
plugin-page row that no source-of-truth entry described, and the group rows never
|
|
140
|
+
stated their off consequence where a reader sees it. Wave 2 closes those gaps
|
|
141
|
+
without widening the mechanism.
|
|
142
|
+
|
|
143
|
+
- **`tower-board` (#133).** `docs.board` is deleted; the board is the optional
|
|
144
|
+
`tower-board` row with its own capability module. Its fallback: no `MISSIONS.md`
|
|
145
|
+
is written or refreshed, an existing board goes stale, and every state tool and
|
|
146
|
+
gate keeps working. `docs.boardLocation` stays a tuning key because it only
|
|
147
|
+
chooses a path. With the row off, the composition behaves like an all-Tower-off
|
|
148
|
+
profile for the board (nothing is written) while the rest of Tower keeps working.
|
|
149
|
+
- **`tower-archive` (#135).** `archive.enabled` is deleted; archival is the
|
|
150
|
+
optional `tower-archive` row. Its fallback is deliberately honest: terminal
|
|
151
|
+
missions stay hot in `state.json`, the hot state keeps growing, and `status` /
|
|
152
|
+
`doctor` keep loading their full history; merges and gates are unaffected and
|
|
153
|
+
nothing is deleted. `archive.afterDays` / `archive.maxMissions` stay tuning.
|
|
154
|
+
- **Removed keys fail loudly.** A config that still sets `docs.board` or
|
|
155
|
+
`archive.enabled` is rejected with `config-unknown-key` and an upgrade note that
|
|
156
|
+
names the replacing row and points here. There is no silent alias: a switch that
|
|
157
|
+
moved to the plugin page must not keep working from a config file.
|
|
158
|
+
- **`tower-ui` is classified (#134).** The UI plugin row is now a visible
|
|
159
|
+
component with a documented fallback (the web UI plugin does not load: no tool
|
|
160
|
+
cards, composer-dock summary or status strip; host tools, missions and the board
|
|
161
|
+
are unaffected). Because it ships in `ui-bundle/`, the gate reads **every**
|
|
162
|
+
shipped `cordis.patch.yml` (root and `ui-bundle/`), classifies every visible row
|
|
163
|
+
and enforces a label source for each: either a generated `<specifier>/locale`
|
|
164
|
+
row, or the row package's own `meta` label when the labels live there. A row
|
|
165
|
+
added to any shipped patch without a source-of-truth entry fails
|
|
166
|
+
`npm run docs:check:components`.
|
|
167
|
+
- **A group row states its consequence where it is read (#132).** The plugin page
|
|
168
|
+
renders the group row from the package `meta` block, so the off-equals-uninstall
|
|
169
|
+
consequence lives in the root `meta.description` in both languages, and the gate
|
|
170
|
+
pins it there instead of trusting only the `components` block. This is a label
|
|
171
|
+
fix, not a new mechanism.
|
|
172
|
+
- **`tower-hmr` stays shipped off (#141).** Moving it to a separate install path
|
|
173
|
+
would hide the switch and re-open the orphan-mount hazard; the row is kept as
|
|
174
|
+
explicit development tooling instead: its locale copy and its source-of-truth
|
|
175
|
+
fallback say that it exists for a linked dev profile, that a normal profile
|
|
176
|
+
behaves exactly as before, and that it is off on purpose.
|
|
177
|
+
- **Count decision.** The visible component count moved 8 → 9 → 10 → 11 across
|
|
178
|
+
#133, #135 and #134; the three dated steps are recorded in
|
|
179
|
+
`scripts/surface-baseline.json`. The config surface contracted 55 → 53, which the
|
|
180
|
+
other surfaces allow without a decision record.
|
|
181
|
+
|
|
182
|
+
## Addendum (wave 3): close the gate's blind spots
|
|
183
|
+
|
|
184
|
+
- Date: 2026-11-03
|
|
185
|
+
- Decisions: #138
|
|
186
|
+
|
|
187
|
+
Wave 2 made the gate read every shipped row, but four ways to evade it remained:
|
|
188
|
+
a class was free text, the operational route table was scraped with a regex, only
|
|
189
|
+
the patches named in `COMPONENT_PATCH_FILES` were read, and only booleans were
|
|
190
|
+
reviewed. Wave 3 closes them without changing the shipped decisions: 53 config
|
|
191
|
+
keys, 11 visible components.
|
|
192
|
+
|
|
193
|
+
- **The class is checked against the key's shape (#138.1).** A boolean that
|
|
194
|
+
removes a capability cannot file as `optional-config` by assertion. Every live
|
|
195
|
+
boolean must be `core`, `optional-component` or `forbidden-to-disable`, or an
|
|
196
|
+
`optional-config` key carrying a dated `exemption` whose reason names its
|
|
197
|
+
tuning-only nature. The same rule covers the non-boolean behaviour knobs below.
|
|
198
|
+
An exemption is only legal on a reviewed boolean/knob, and its `ref` must exist.
|
|
199
|
+
- **The config-key count is two-way (#138.1).** Wave 2's closing note ("the config
|
|
200
|
+
surface contracted 55 → 53, which the other surfaces allow without a decision
|
|
201
|
+
record") is retracted. Removing a switch is a capability decision, so
|
|
202
|
+
`configKeys` now joins `components` in `SURFACE_TWO_WAY_KEYS`: any later increase
|
|
203
|
+
or decrease needs a dated decision in `scripts/surface-baseline.json`. The gate
|
|
204
|
+
requires the `date` field itself (`YYYY-MM-DD`) on every decision of a two-way
|
|
205
|
+
surface, so a bare from/to record fails even when its chain reaches the value
|
|
206
|
+
(review of #138). The 64 → 53 history is recorded as one dated decision there;
|
|
207
|
+
sections and tools keep the ADR 0004 cheap-shrink rule.
|
|
208
|
+
- **The route table is imported, not scraped (#138.2).** `host/tool-registry.js`
|
|
209
|
+
exports `OPERATIONAL_TOOLS`, and the gate imports that module instead of
|
|
210
|
+
regex-matching its source. A missing file, a failed import, or a missing/non-Map
|
|
211
|
+
export is `registry-unreadable` and fails the gate. An empty Map is still the
|
|
212
|
+
correct determined state (#122): "nothing is gated" is distinct from "could not
|
|
213
|
+
read".
|
|
214
|
+
- **Every shipped patch is discovered (#138.3).** The gate walks the package for
|
|
215
|
+
`cordis.patch.yml` and compares the result with `COMPONENT_PATCH_FILES` in both
|
|
216
|
+
directions. An unlisted patch fails `patch-unclassified`; a listed patch that is
|
|
217
|
+
missing fails `patch-missing`. The row checks stay bidirectional: every shipped
|
|
218
|
+
row needs a source-of-truth entry, and every source-of-truth row needs a shipped
|
|
219
|
+
row.
|
|
220
|
+
- **Behaviour knobs and row overrides are reviewed (#138.4).** `spawn.requireLease`
|
|
221
|
+
and `tests.admission` are recorded in `behaviorKnobs` and carry tuning-only
|
|
222
|
+
exemptions: the optional `tower-leases` / `tower-tests` row owns the capability,
|
|
223
|
+
while these keys choose the safety level or timing inside it. A row's scalar
|
|
224
|
+
`config:` overrides are now parsed and must name a live config key and be listed
|
|
225
|
+
in that component's `allowedOverrides`. A row that sets `docs.board`,
|
|
226
|
+
`archive.enabled` or `tests.enabled` therefore fails as `row-override-unknown`
|
|
227
|
+
(those capabilities are rows), a row that enables or disables a key it does not
|
|
228
|
+
own fails as `row-override-foreign`, and `allowedOverrides` may never name a
|
|
229
|
+
forbidden-to-disable key. Fixtures reproduce the #133/#135/#136 shapes and a
|
|
230
|
+
foreign override, proving they fail before the fix passes.
|
|
231
|
+
|
|
232
|
+
## Addendum (wave 4): broad-audience labels and a copy gate
|
|
233
|
+
|
|
234
|
+
- Date: 2026-11-04
|
|
235
|
+
- Decisions: #137
|
|
236
|
+
|
|
237
|
+
Wave 3 made the gate fail closed, but every label it admitted still passed. The
|
|
238
|
+
plugin page leaked config keys (`gates.requireTests:true`, `review.requireReview`)
|
|
239
|
+
and bare tool ids (`tower_batch`, `tower_resource`) into the off-state copy, the
|
|
240
|
+
Chinese copy mixed 咨询式租约 / 重型工作 / untranslated `mission` and `preset`, and
|
|
241
|
+
the Web UI used 任务线 while the plugin page left `mission` in English. The counts
|
|
242
|
+
do not move: 53 config keys, 13 components, 11 visible.
|
|
243
|
+
|
|
244
|
+
- **The labels are rewritten for a reader, not for an operator (#137.1).** Every
|
|
245
|
+
visible row, the group row and the UI row now say what the capability is for
|
|
246
|
+
and what stops when it is off, in plain language. Config keys, tool ids, slash
|
|
247
|
+
commands and module specifiers are gone from `title`, `summary`, `disables` and
|
|
248
|
+
the two `meta` descriptions. The off copy says "refused with an explanation",
|
|
249
|
+
not "locked", and names Tower mode. The `tower-leases` fix also removes the F19
|
|
250
|
+
claim that "nothing else depends on a lease": a lease is coordination only, but
|
|
251
|
+
a worker configured to require one cannot start while the row is off; planning
|
|
252
|
+
and Tower-mode entry are refused with an explanation until the setting is
|
|
253
|
+
cleared or the row is re-enabled, and everything else keeps working.
|
|
254
|
+
- **One bilingual vocabulary, recorded (#137.2).** `GLOSSARY.md` is the single
|
|
255
|
+
table for mission/任务线, batch/批次, doctor/体检, board/看板, archive/归档,
|
|
256
|
+
preset/预设, worktree/工作树, review/审查, lease/租约, worker/执行代理 and
|
|
257
|
+
reviewer/审查代理. It also records the copy rules (two sentences, no
|
|
258
|
+
identifiers, truthful off effect, natural Chinese) and is a gate input: a label
|
|
259
|
+
that uses a term the table does not pair with its other-language field fails.
|
|
260
|
+
The Web client's own string catalogue shares this table but is migrated
|
|
261
|
+
separately; the runtime error copy in `host/presets.js` is likewise a follow-up.
|
|
262
|
+
- **The copy is checked mechanically (#137.3).** `checkLocaleCopy` runs over the
|
|
263
|
+
root locale pair, the ui-bundle meta pair and every generated row-locale pair.
|
|
264
|
+
It fails on config-key syntax, `tower_*` ids, `/tower` commands, `dsh-tower/*`
|
|
265
|
+
specifiers, empty fields, zh/en path drift and glossary mismatches in both
|
|
266
|
+
directions; the Chinese side may not leave glossary words untranslated.
|
|
267
|
+
`checkMetaRowCopy` pins the hand-written ui-bundle `meta.title` / `description`
|
|
268
|
+
to the root `components['tower-ui']` entry and the same composition the
|
|
269
|
+
generator renders, so the one row that ships its own label cannot drift.
|
|
270
|
+
`checkGlossaryFile` keeps `GLOSSARY.md` present and complete. Negative
|
|
271
|
+
fixtures in `tests/locale-copy.test.mjs` prove a raw key, a bare tool id and a
|
|
272
|
+
term mismatch fail in both languages before the fixed copy passes.
|
|
273
|
+
- **Review of #137 corrected three claims and hardened the guard.** The
|
|
274
|
+
`tower-tests` off copy promised that "merges … keep working", but
|
|
275
|
+
`domain/gate.js` emits the `tests-missing` merge blocker while the test
|
|
276
|
+
requirement is on and `host/presets.js` refuses planning and Tower-mode entry,
|
|
277
|
+
so the copy now says planning and entry are refused and merges stay blocked
|
|
278
|
+
until the requirement is cleared. The reviewer preset summary said "only used
|
|
279
|
+
when review before merging is required"; the row is usable while review is
|
|
280
|
+
optional and becomes a precondition for merging only when the requirement is
|
|
281
|
+
on, so the summary says that instead. The Chinese calques 锁状态的恢复出口,
|
|
282
|
+
收口, 一项命名工作, 在线状态文件 and 关闭该行 were replaced with 卡住的锁如何
|
|
283
|
+
恢复, 关闭该批次, 某一具体工作, 当前状态文件 and 关闭这一项. The guard now
|
|
284
|
+
normalizes full-width punctuation and matches case-insensitively, so
|
|
285
|
+
`Gates.requireTests`, `Tower_batch` and `gates。requireTests` fail; the worker
|
|
286
|
+
and reviewer role nouns accept an initial capital but exclude the product names
|
|
287
|
+
"Tower Worker" and "Tower Reviewer", in both the en→zh and zh→en directions.
|
|
288
|
+
`GLOSSARY.md` ships in the package (`package.json` `files`, pinned by
|
|
289
|
+
`scripts/pack-manifest.txt`) because it is a gate input, not repository-only
|
|
290
|
+
prose.
|
|
291
|
+
- **Rejected: a prose style guide with no gate.** It is what let wave 1–3 pass;
|
|
292
|
+
a reviewer's attention does not survive the next rename.
|
|
293
|
+
- **Rejected: translating the developer reference table.** The fallback text in
|
|
294
|
+
`scripts/component-surface.json` and the generated
|
|
295
|
+
`docs/reference/component-surface.md` is an operator reference for auditors, not
|
|
296
|
+
a plugin-page label. It may name keys and tools; the locale fields may not.
|
|
@@ -29,6 +29,27 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
|
|
|
29
29
|
|
|
30
30
|
## Migration notes
|
|
31
31
|
|
|
32
|
+
- **2.4.2 → 2.5.0 (#131/#133/#135/#136/#142)**: three component-shaped config
|
|
33
|
+
keys are gone — `tests.enabled`, `docs.board`, and `archive.enabled`. The
|
|
34
|
+
`tower-tests`, `tower-board`, and `tower-archive` component rows are the only
|
|
35
|
+
switches. A profile or repository config that still writes any of these keys is
|
|
36
|
+
rejected as `config-unknown-key` with an upgrade note and the host stops
|
|
37
|
+
mounting, so delete the key and enable or disable the row in the plugin page
|
|
38
|
+
instead. `docs.boardLocation` and `archive.afterDays`/`archive.maxMissions`
|
|
39
|
+
remain tuning keys. With `tower-tests` off there is no `tower_test` tool and no
|
|
40
|
+
receipt, which also means `gates.requireTests` cannot be satisfied: planning and
|
|
41
|
+
mode entry refuse that combination, and the merge gate's `tests-missing` blocker
|
|
42
|
+
names the disabled row and the fix. `tower_test` no longer reports "not
|
|
43
|
+
configured" for a component cause; it reports `disabled by component
|
|
44
|
+
tower-tests` with the remedy.
|
|
45
|
+
Two more runtime rules became explicit: a config that sets `spawn.requireLease`
|
|
46
|
+
while the `tower-leases` row is off is refused up front with `config-invalid`
|
|
47
|
+
naming the row and both remedies (previously every spawn failed at `lease-busy`
|
|
48
|
+
advising an unregistered `tower_resource`), and `/tower` re-validates the runtime
|
|
49
|
+
preconditions only when it enters the mode, so `/tower off` and a forwarded
|
|
50
|
+
`/tower <text>` keep working while an unrelated row is disabled. No state
|
|
51
|
+
migration; see [component rows and messages](quickstart.md#component-rows-runtime-preconditions-and-message-language).
|
|
52
|
+
|
|
32
53
|
- **state schema v1 → v2 (unreleased)**: `state.json` migrates automatically. The first **write** after
|
|
33
54
|
upgrading keeps a `state.json.v1.bak` backup, advances `revision` once, and appends
|
|
34
55
|
`state.migrated {from,to}` to `activity.jsonl`; read-only tools keep reading a v1 file without
|