dsh-tower 2.1.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/COMPATIBILITY.md +6 -1
  3. package/CONTRIBUTING.md +78 -5
  4. package/PRINCIPLES.md +163 -0
  5. package/README.en.md +35 -19
  6. package/README.md +28 -18
  7. package/SECURITY.md +6 -0
  8. package/{lib → adapters}/archive.js +1 -1
  9. package/{lib → adapters}/caseSensitivity.js +1 -1
  10. package/adapters/default.js +51 -10
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +1 -1
  13. package/{lib → adapters}/legacy.js +1 -1
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/store-fs.js +28 -1
  16. package/adapters/store.js +204 -0
  17. package/{lib → adapters}/tests.js +9 -5
  18. package/adapters/vcs-git.js +15 -1
  19. package/{lib → adapters}/workspaceLock.js +1 -1
  20. package/{lib → adapters}/worktreeOwnership.js +1 -1
  21. package/app/archive.js +4 -6
  22. package/app/batch.js +9 -10
  23. package/app/board.js +2 -4
  24. package/app/checkCompletion.js +9 -10
  25. package/app/enrichedMissions.js +2 -4
  26. package/app/findings.js +6 -7
  27. package/app/gate.js +1 -2
  28. package/app/init.js +24 -29
  29. package/app/merge.js +58 -51
  30. package/app/messages.js +7 -8
  31. package/app/mission.js +5 -5
  32. package/app/plan.js +8 -5
  33. package/app/provision.js +12 -14
  34. package/app/read.js +6 -3
  35. package/app/rebase.js +14 -16
  36. package/app/recording.js +7 -8
  37. package/app/review.js +15 -8
  38. package/app/roster.js +14 -15
  39. package/{lib → app}/spawn-prechecks.js +4 -3
  40. package/app/spawn.js +12 -13
  41. package/app/teardown.js +8 -9
  42. package/app/tower.js +89 -35
  43. package/cordis.patch.yml +56 -28
  44. package/docs/README.md +7 -2
  45. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  46. package/docs/adr/0002-single-state-json.md +50 -0
  47. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  48. package/docs/adr/0004-surface-budget.md +70 -0
  49. package/docs/adr/0005-component-surface.md +130 -0
  50. package/docs/adr/README.md +52 -0
  51. package/docs/agents/workflow.md +51 -0
  52. package/docs/architecture/overview.md +192 -43
  53. package/docs/guide/hot-reload.md +5 -5
  54. package/docs/guide/install-upgrade-migration.md +13 -0
  55. package/docs/guide/release.md +1 -1
  56. package/docs/operations/incident.md +82 -0
  57. package/docs/operations/runbook.md +12 -3
  58. package/docs/operations/security.md +1 -1
  59. package/docs/reference/blockers.md +2 -2
  60. package/docs/reference/component-surface.md +135 -0
  61. package/docs/reference/config.generated.md +2 -9
  62. package/docs/reference/config.md +11 -9
  63. package/docs/reference/doctor.md +53 -2
  64. package/docs/reference/errors.generated.md +41 -37
  65. package/docs/reference/errors.md +53 -41
  66. package/docs/reference/events.md +38 -6
  67. package/docs/reference/surface-budget.md +157 -0
  68. package/docs/reference/tools.generated.md +8 -5
  69. package/docs/reference/tools.md +10 -7
  70. package/docs/zh/README.md +1 -1
  71. package/docs/zh/guide/hot-reload.md +4 -4
  72. package/docs/zh/guide/install-upgrade-migration.md +10 -0
  73. package/docs/zh/operations/security.md +1 -1
  74. package/docs/zh/reference/blockers.md +2 -2
  75. package/docs/zh/reference/config.md +9 -9
  76. package/docs/zh/reference/doctor.md +29 -1
  77. package/docs/zh/reference/tools.md +10 -8
  78. package/domain/batch.js +2 -2
  79. package/domain/components.js +641 -0
  80. package/domain/config/schema.js +62 -28
  81. package/domain/contracts/outputs.js +16 -2
  82. package/domain/contracts/tool-inputs/ops.js +1 -0
  83. package/domain/errors.js +308 -43
  84. package/domain/events.js +207 -0
  85. package/domain/integration/gate.js +17 -2
  86. package/domain/integration/merge.js +35 -0
  87. package/domain/lockBudget.js +30 -0
  88. package/domain/mission.js +12 -1
  89. package/domain/missions/completion.js +14 -0
  90. package/domain/missions/lifecycle.js +9 -3
  91. package/domain/missions/plan.js +14 -0
  92. package/domain/missions/review.js +90 -7
  93. package/domain/read/views.js +3 -0
  94. package/domain/state/archive.js +1 -1
  95. package/domain/state.js +1 -1
  96. package/domain/surface.js +347 -0
  97. package/host/capability-batch.js +15 -0
  98. package/host/capability-hmr.js +30 -0
  99. package/host/capability-import.js +11 -0
  100. package/host/capability-leases.js +11 -0
  101. package/host/capability-registry.js +149 -0
  102. package/host/capability-tests.js +16 -0
  103. package/{lib → host}/doctor.js +157 -18
  104. package/{lib → host}/hmr.js +2 -4
  105. package/{lib → host}/modeStore.js +1 -1
  106. package/host/presets.js +77 -0
  107. package/{lib → host}/tool-registry.js +235 -44
  108. package/host/tower.js +32 -0
  109. package/index.js +40 -25
  110. package/locale/en.json +31 -11
  111. package/locale/zh.json +30 -10
  112. package/package.json +24 -4
  113. package/ports/archive.js +18 -0
  114. package/ports/caseSensitivity.js +15 -0
  115. package/ports/clock.js +10 -2
  116. package/ports/config.js +19 -0
  117. package/ports/contract.js +38 -0
  118. package/ports/events.js +24 -0
  119. package/ports/ids.js +10 -2
  120. package/ports/index.js +54 -2
  121. package/ports/ownership.js +17 -0
  122. package/ports/state.js +16 -0
  123. package/ports/store.js +52 -3
  124. package/ports/vcs.js +25 -4
  125. package/scripts/architecture-waivers.json +19 -0
  126. package/scripts/architecture.mjs +634 -23
  127. package/scripts/check-components.mjs +235 -0
  128. package/scripts/check-docs.mjs +8 -35
  129. package/scripts/check-pack.mjs +68 -39
  130. package/scripts/check-philosophy.mjs +266 -0
  131. package/scripts/component-surface.json +395 -0
  132. package/scripts/domain-metrics.mjs +13 -4
  133. package/scripts/generate-component-surface.mjs +59 -0
  134. package/scripts/generate-config-reference.mjs +14 -1
  135. package/scripts/generate-error-reference.mjs +68 -14
  136. package/scripts/generate-events-reference.mjs +83 -0
  137. package/scripts/generate-tools-reference.mjs +18 -25
  138. package/scripts/generate-types.mjs +127 -0
  139. package/scripts/issue-audit.mjs +2 -2
  140. package/scripts/lib/surface.mjs +87 -0
  141. package/scripts/lib/tool-surface.mjs +85 -0
  142. package/scripts/mount-smoke/fixtures.mjs +3 -3
  143. package/scripts/mount-smoke/surface.mjs +96 -27
  144. package/scripts/mount-smoke.mjs +3 -1
  145. package/scripts/pack-manifest.txt +75 -22
  146. package/scripts/regression-audit.mjs +164 -0
  147. package/scripts/release-check.mjs +1 -1
  148. package/scripts/surface-baseline.json +52 -0
  149. package/scripts/surface-baseline.mjs +118 -0
  150. package/scripts/types-coverage.mjs +184 -0
  151. package/scripts/types-strict-manifest.txt +10 -0
  152. package/scripts/types-strict.mjs +141 -0
  153. package/skills.js +8 -1
  154. package/tools/adopt.js +2 -3
  155. package/tools/batch.js +2 -3
  156. package/tools/complete.js +2 -3
  157. package/tools/diff.js +3 -3
  158. package/tools/doctor.js +7 -10
  159. package/tools/finding.js +2 -3
  160. package/tools/findings.js +3 -3
  161. package/tools/import-legacy.js +3 -5
  162. package/tools/inbox.js +3 -3
  163. package/tools/index.js +9 -8
  164. package/tools/init.js +3 -6
  165. package/tools/interrupt.js +2 -3
  166. package/tools/legacy-map.js +3 -5
  167. package/tools/merge.js +3 -3
  168. package/tools/mission.js +2 -3
  169. package/tools/plan.js +6 -5
  170. package/tools/preflight.js +2 -3
  171. package/tools/rebase.js +3 -3
  172. package/tools/recover.js +38 -13
  173. package/tools/reload.js +36 -40
  174. package/tools/resource.js +3 -5
  175. package/tools/review.js +3 -4
  176. package/tools/roster.js +2 -3
  177. package/tools/send.js +2 -2
  178. package/tools/spawn.js +8 -5
  179. package/tools/status.js +11 -4
  180. package/tools/teardown.js +2 -3
  181. package/tools/test.js +2 -2
  182. package/types/index.d.ts +29 -13
  183. package/ui-bundle/README.md +37 -3
  184. package/ui-bundle/client.cards.js +356 -0
  185. package/ui-bundle/client.denied.js +142 -0
  186. package/ui-bundle/client.dock.js +333 -0
  187. package/ui-bundle/client.helpers.js +271 -0
  188. package/ui-bundle/client.js +79 -1747
  189. package/ui-bundle/client.locales.js +247 -0
  190. package/ui-bundle/client.mode.js +206 -0
  191. package/ui-bundle/client.overlay.js +64 -0
  192. package/ui-bundle/client.styles.js +151 -0
  193. package/ui-bundle/client.toolview.js +82 -0
  194. package/ui-bundle/package.json +1 -0
  195. package/lib/persistence.js +0 -356
  196. package/lib/presets.js +0 -35
  197. package/lib/store.js +0 -133
  198. /package/{lib → adapters}/stateFile.js +0 -0
  199. /package/{lib → app}/briefing.js +0 -0
  200. /package/{lib → app}/spawn-support.js +0 -0
  201. /package/{lib → host}/compatibility.js +0 -0
  202. /package/{lib → host}/config.js +0 -0
  203. /package/{lib → host}/modeState.js +0 -0
@@ -0,0 +1,82 @@
1
+ # Incidents and regressions
2
+
3
+ A **regression** is something that *worked in a released version, then stopped*.
4
+ It is not a feature request and not an ordinary bug: a user could rely on the old
5
+ behavior, so the record must say which release broke it, who was affected, and
6
+ which test now prevents it from happening again.
7
+
8
+ Only shipped regressions get this record. Ordinary bugs, missing features and
9
+ questions use the normal issue flow.
10
+
11
+ ## The six-field record
12
+
13
+ Copy this into the issue body (or the closing comment) when a regression is
14
+ confirmed. All six lines are part of the closure; the `Regression test:` line is
15
+ the one a machine can check.
16
+
17
+ ```text
18
+ Symptom: what the user sees, with the exact error text
19
+ Introduced in: version / commit range
20
+ Blast radius: who is affected (all sessions? restored ones? one platform?)
21
+ Root cause: one paragraph, mechanism not blame; include the fix
22
+ Regression test: path to the test that fails without the fix
23
+ Released in: version that carries the fix, and how it was verified
24
+ ```
25
+
26
+ The fields answer, in order: what broke, which release introduced it, the user
27
+ impact, the cause and the fix, the proof, and the release that carries the fix.
28
+ `Introduced in` and `Released in` are different answers; do not fill both with the
29
+ current version.
30
+
31
+ ## Label
32
+
33
+ The `regression` label means exactly **"worked in a released version, then
34
+ stopped"**. It is mutually exclusive with `bug` for a shipped break: the regression
35
+ label is the narrower claim. The issue title still describes the symptom.
36
+
37
+ ## Closing rule
38
+
39
+ A `regression` issue may not be closed while any of these is true:
40
+
41
+ - no `Regression test:` line names a path;
42
+ - the path does not exist in the repository;
43
+ - the path is not a test file (a script or a doc is not evidence).
44
+
45
+ The offline audit reads a local tracker export, so it needs no network and no
46
+ token:
47
+
48
+ ```powershell
49
+ # 1) Export (network, once) — same export as the P0/P1 audit
50
+ New-Item -ItemType Directory -Force tracker | Out-Null
51
+ $all = foreach ($n in (gh issue list --repo louifinn/dsh-tower --state all --limit 500 --json number --jq '.[].number')) {
52
+ gh issue view $n --repo louifinn/dsh-tower --json number,title,state,labels,closedAt,body,comments | ConvertFrom-Json
53
+ }
54
+ @{ issues = @($all) } | ConvertTo-Json -Depth 8 | Set-Content tracker/issues.json -Encoding utf8
55
+
56
+ # 2) Audit (offline)
57
+ npm run audit:regressions
58
+ npm run audit:regressions -- --tracker path/to/export.json --json
59
+ ```
60
+
61
+ Exit codes: `0` — every closed regression names an existing test; `1` — at least
62
+ one is missing or points at a nonexistent/non-test path; `2` — the export is
63
+ missing or malformed.
64
+
65
+ The audit is a script because a checklist is only as reliable as the reader.
66
+ `tests/regression-audit.test.mjs` proves the check fails for a seeded missing path
67
+ and passes for a real one, so the rule cannot silently become prose.
68
+
69
+ ## Where the practice starts
70
+
71
+ The first real entry is #68 (all tower write tools failed with `unknown` after a
72
+ session restore). The tower retro-fills that issue with these six fields; this
73
+ document is the template it used.
74
+
75
+ ## Non-goals
76
+
77
+ No blameless-postmortem ceremony for ordinary bugs, no SLA commitments, no
78
+ five-whys, and no incident commander. Only shipped regressions get the six fields.
79
+
80
+ Unowned. This is a writing habit and a closing convention; no runtime code
81
+ enforces it. The one checkable part — the `Regression test:` path — is enforced by
82
+ `scripts/regression-audit.mjs`.
@@ -48,9 +48,18 @@ fails, the previous plugin code no longer runs: restart DSH and check the HMR lo
48
48
 
49
49
  ## State lock
50
50
 
51
- `tower state is locked by another writer` means an operation holds `state.lock`. Wait and retry. Only if
52
- the owning process is gone, remove `<git-common-dir>/dsh-tower/state.lock` deliberately; never delete it
53
- while an agent may still be running.
51
+ `tower state is locked by another writer` means an operation holds `state.lock`. Wait and retry: a
52
+ live holder rewrites the record's `at` heartbeat every second, so it is never stolen while it runs.
53
+ A crashed holder stops heartbeating; once its last heartbeat is older than three intervals (3 s), the
54
+ next mutation reclaims the record after byte verification. A reclaimed live-PID record is kept as
55
+ `state.lock.reclaimed-<ts>-<id>` for audit; a provably dead PID (ESRCH) is reclaimed immediately.
56
+
57
+ If a lock never clears, `tower_recover({action:"status"})` reports the owner's `pid`, `startedAt`,
58
+ `ageMs` and `heartbeatAgeMs`, and `tower_doctor` reports the same. `tower_recover
59
+ clear-stale-lock` with `confirm:"CLEAR_STALE_LOCK"` moves a provably dead lock to
60
+ `state.lock.recovered-<ts>`; add `force: true` to clear a live-but-silent PID (the recycled-PID case)
61
+ after confirming no writer is active. The repair always renames, never deletes, so the bytes stay
62
+ auditable. Never delete `state.lock` by hand while an agent may still be running.
54
63
 
55
64
  ## Worktrees and teardown
56
65
 
@@ -104,7 +104,7 @@ Tower work, and run the host under OS-level isolation.
104
104
  cannot make those safe; review hooks before running Tower on an untrusted repository.
105
105
  - **Package test commands.** Declared discovery commands have the same trust as repository hooks; a
106
106
  malicious `package.json` can run code. Disable `tests.allowPackageDiscovery` and review the file.
107
- - **HMR reload.** With `dev.hotReload`, `/tower reload`/`tower_reload` asks DSH to re-import the plugin
107
+ - **HMR reload.** With the `tower-hmr` component row on, `/tower reload`/`tower_reload` asks DSH to re-import the plugin
108
108
  from disk. Only enable HMR watch roots for checkouts you trust; a reload executes the current files.
109
109
  - **Prompt injection.** The reviewer reads untrusted branch content (diff/comments). The read-only preset
110
110
  limits impact, but no model-side protection is absolute.
@@ -18,10 +18,10 @@ tables below define 32 stable blocker/failure codes.
18
18
  | `read-only-survey` | Survey mission changed files; survey missions must be read-only. |
19
19
  | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. On a case-sensitive checkout a case-only mismatch is still rejected and the detail names the scope entry it would match if case were folded. |
20
20
  | `base-dirty` | The main checkout has uncommitted changes the merge would overwrite. |
21
- | `deps-unmerged` | A dependency mission is not merged/abandoned. |
21
+ | `deps-unmerged` | A dependency mission is not satisfied: a build dependency must be `merged`/`abandoned`, while a survey dependency is satisfied at `completed` (archived entries included). |
22
22
  | `no-review` | `review.requireReview:true` and no review exists on the branch: spawn a reviewer (or disable the review gate). |
23
23
  | `review-mission-mismatch` | `review.requireReview:true` and the latest review belongs to another mission. |
24
- | `not-clean` | `review.requireReview:true` and latest review is `p1-Nitems`/`p2-Nitems`; another round is required. |
24
+ | `not-clean` | `review.requireReview:true` and latest review is `p1-Nitems`/`p2-Nitems`; another round is required. Verdicts from later routes of the same round are appended to it instead of flipping the mission again. |
25
25
  | `review-hold` | `review.requireReview:true` and latest review recommends hold; a later clean review must supersede it. |
26
26
  | `review-not-approved` | `review.requireReview:true` and latest clean review says `fix-then-merge` or `hold`; only `merge` approves. |
27
27
  | `tip-moved` | `review.requireReview:true` and the branch changed after review; review again (or use a clean rebase waiver). |
@@ -0,0 +1,135 @@
1
+ # Component surface (generated)
2
+
3
+ > Generated by `npm run docs:generate:components` from `scripts/component-surface.json` and
4
+ > `scripts/surface-baseline.json`. Do not edit by hand.
5
+
6
+ Decision: [ADR 0005](../adr/0005-component-surface.md) · decision date: 2026-10-23 · baseline 2026-10-23 · visible components: 8 (`tower`, `tower-skills`, `tower-batch`, `tower-import`, `tower-leases`, `tower-tests`, `tower-hmr`, `preset-tower-reviewer`)
7
+
8
+ The component axis answers one question for every config key: may a user turn it
9
+ off, and if so, must that switch be a component? Four classes cover every key, so
10
+ "should this be a component?" is a table lookup rather than a judgement call.
11
+
12
+ | Class | Meaning |
13
+ |---|---|
14
+ | `core` | Removing it breaks the Tower contract. It is never a switch. |
15
+ | `optional-component` | A capability whose off state leaves the rest of Tower intact. It must be a user-visible switch with a documented fallback. |
16
+ | `optional-config` | A tuning knob, not a capability. It stays in the config form and never hides a component. |
17
+ | `forbidden-to-disable` | The host, the health contract, the recovery escape, the review contract and the merge gates. No group, row or boolean key may turn it off. |
18
+
19
+ ## Components
20
+
21
+ The shipped `cordis.patch.yml` rows, with the fallback contract each off state
22
+ must satisfy. A visible row is a component the plugin page may switch; an internal
23
+ row is a section of its parent and is not independently switchable.
24
+
25
+ | Component | Scope | Kind | Off state and fallback |
26
+ |---|---|---|---|
27
+ | `tower` | visible | group | Off equals uninstalling Tower: no tools, presets or skills remain. That is the accepted all-off boundary of the mandatory host anchor, not an optional capability. |
28
+ | `tower-host` | internal | row | The Host plugin itself, nested under the mandatory tower anchor. A mandatory dependency, never its own switch. |
29
+ | `tower-skills` | visible | row | The 3 skills leave the catalog; the worker/reviewer PROTOCOL sections still ship through systemPrompt.section, so only the tower-recovery guidance is lost. |
30
+ | `tower-batch` | visible | row | tower_batch is not registered; missions, gates and merges are unaffected because no state depends on a batch. |
31
+ | `tower-import` | visible | row | tower_import_legacy and tower_legacy_map are not registered; new repository state is unaffected. |
32
+ | `tower-leases` | visible | row | tower_resource is not registered; advisory leases are coordination only, so nothing else depends on them. |
33
+ | `tower-tests` | visible | row | tower_test is not registered and no test receipt can be recorded. With gates.requireTests:true planning/mode entry is rejected with an explanation, and the merge gate still reports tests-missing. |
34
+ | `tower-hmr` | visible | row | tower_reload and /tower reload are unavailable; a profile without HMR behaves exactly as before. Shipped disabled: enable the row for a linked dev profile. |
35
+ | `preset-tower-worker` | internal | row | tower_spawn fails without the worker preset. A mandatory dependency, never a switch. |
36
+ | `preset-tower-reviewer` | visible | row | The reviewer cannot be dispatched. Supported while review.requireReview is false (merge is unaffected); with the gate on planning/mode entry is locked with an explanatory error. |
37
+
38
+ ## Forbidden to disable
39
+
40
+ The host, the health contract, the recovery escape, the review contract and the
41
+ merge gates are configuration or code, never switches. `npm run
42
+ docs:check:components` derives the live disablers from `host/tool-registry.js`
43
+ and `cordis.patch.yml`; a disabler that is not recorded here fails the gate.
44
+
45
+ | Capability | Why it may not be disabled | Recorded disablers | Decision |
46
+ |---|---|---|---|
47
+ | the Tower host | Without the host the plugin registers nothing; there is no off state that leaves the rest working, and no config boolean may remove it. The visible `tower` anchor is the all-off boundary, and the host section nested under it may never become its own switch. | none | none may exist |
48
+ | tower_doctor (read-only health contract) | Diagnosis must always exist so a failure message can point at it. #122 removed exposeDoctor and the exposeOperational master switch; the tool is registered for every configuration. | none | none may exist |
49
+ | tower_recover (stale state-lock escape) | The escape hatch must exist whenever the lock it repairs can be left behind. #122 removed exposeRecovery and the exposeOperational master switch; the tool is registered for every configuration. | none | none may exist |
50
+ | the review contract | review.requireReview is a merge blocker, not a component; a row may not switch it off. The reviewer row is conditional, not a way to weaken the gate. | none | none may exist |
51
+ | the merge gates | gates.* decides delivery; a component may not bypass it. | none | none may exist |
52
+
53
+ ## Gateable tools
54
+
55
+ The allowlist of boolean routes in `host/tool-registry.js`. Every shipped
56
+ `tools.expose*` route must match this table. A route to a tool that is not listed
57
+ here is refused, so no boolean key can quietly remove a review, merge or host
58
+ tool. Since #122 the shipped table is empty: `tower_doctor` and `tower_recover`
59
+ are registered for every configuration and no other capability may become a
60
+ boolean switch.
61
+
62
+ | Tool | Switch key |
63
+ |---|---|
64
+ | _(none)_ | — |
65
+
66
+ ## Config keys
67
+
68
+ All keys from `domain/config/schema.js`, classified on the component axis.
69
+
70
+ | Key | Component class | Decision note |
71
+ |---|---|---|
72
+ | `section` | `core` | Legacy alias for mode.promptSection: the Tower-mode prompt contract, not a capability. |
73
+ | `allowAgentControl` | `core` | Legacy alias for mode.allowAgentControl: the human-owned mode rule (ADR 0003), not a component. |
74
+ | `init.autoGitInit` | `optional-config` | Bootstrap convenience; off only means tower_init will not run git init. |
75
+ | `init.snapshotDirty` | `optional-config` | Dirty-work baseline convenience; no capability is removed. |
76
+ | `init.autoExclude` | `optional-config` | Keeps Tower artifacts out of .git/info/exclude; safe either way. |
77
+ | `mode.promptSection` | `core` | Replaces the whole Tower-mode prompt text; contract text, not a capability. |
78
+ | `mode.sectionAppend` | `optional-config` | Appends team conventions to the prompt. |
79
+ | `mode.allowAgentControl` | `core` | Decides whether the model may toggle the mode (ADR 0003). A governance rule, never a component. |
80
+ | `mode.autoPauseGoal` | `core` | Entering the mode pauses an active goal; exit never resumes it. |
81
+ | `mode.requireResumeAuthorization` | `core` | Guards resuming a user-paused mission. |
82
+ | `mode.requireHumanAckOnFirstEntry` | `core` | First entry can require an explicit human ack. |
83
+ | `mode.workspaceLock` | `core` | One Tower session per repository is a safety contract; a row may not turn it off. |
84
+ | `mode.workspaceLockScope` | `optional-config` | Chooses whether read-only tools take the workspace lock. |
85
+ | `mode.planModePolicy` | `core` | Plan mode and Tower mode stay mutually exclusive. |
86
+ | `mode.workspaceStaleMs` | `optional-config` | Stale window for the advisory workspace lock. |
87
+ | `mode.heartbeatMs` | `optional-config` | Heartbeat interval while the mode is on. |
88
+ | `mode.denyTools` | `core` | The denied-tool list is part of the mode guard. |
89
+ | `mode.storePath` | `optional-config` | Custom mode-store location; no capability is removed. |
90
+ | `mode.maxEntries` | `optional-config` | Bounds the persisted mode store. |
91
+ | `mode.lockTimeoutMs` | `optional-config` | Bounded wait for the cross-process mode lock. |
92
+ | `review.requireReview` | `forbidden-to-disable` | Turns independent review into a merge blocker; no component may switch it off. With it on, the preset-tower-reviewer row may not be off (planning/mode entry refuse the combination). |
93
+ | `review.maxRoundsPerReviewer` | `forbidden-to-disable` | Caps review rounds; part of the review contract. |
94
+ | `merge.timeoutMs` | `optional-config` | Default merge timeout; a per-call value wins. |
95
+ | `merge.maxBufferBytes` | `optional-config` | Captured merge output buffer. |
96
+ | `git.timeoutMs` | `optional-config` | Default Git command timeout outside merge. |
97
+ | `worktrees.root` | `optional-config` | Default worktree placement. |
98
+ | `spawn.workerPreset` | `core` | Chooses the worker tool surface; a dependency, never a switch. |
99
+ | `spawn.reviewerPreset` | `core` | Chooses the read-only reviewer surface; a dependency, never a switch. |
100
+ | `spawn.maxDepth` | `optional-config` | Subagent depth requested at spawn. |
101
+ | `spawn.provider` | `optional-config` | Provider route for spawned agents. |
102
+ | `spawn.workerModel` | `optional-config` | Optional model route for workers. |
103
+ | `spawn.reviewerModel` | `optional-config` | Optional independent model route for reviewers. |
104
+ | `spawn.maxLiveWorkers` | `core` | Resource-safety cap on live workers. |
105
+ | `spawn.staleMs` | `optional-config` | Roster staleness window before merge is blocked. |
106
+ | `spawn.maxWaitMs` | `optional-config` | Upper bound for a blocking spawn wait. |
107
+ | `spawn.requireLease` | `optional-config` | Advisory-lease gate before a worker starts. |
108
+ | `send.maxPerMinute` | `optional-config` | Message rate limit; zero disables it. |
109
+ | `send.interruptDefault` | `optional-config` | Default for the interrupt-before-deliver flag. |
110
+ | `tests.enabled` | `optional-component` | Tuning inside the optional tower-tests component: it decides whether discovery runs when the row is on. Off with gates.requireTests:true is rejected at config resolution. |
111
+ | `tests.admission` | `optional-config` | Chooses when discovery runs inside the optional tests capability. |
112
+ | `tests.timeoutMs` | `optional-config` | Per-command discovery timeout. |
113
+ | `tests.maxBytes` | `optional-config` | Captured discovery output cap. |
114
+ | `tests.autoChecks` | `optional-config` | Attaches a tests check to completion when discovery runs. |
115
+ | `tests.allowPackageDiscovery` | `core` | Allows repository code to execute; a security contract, not a component. |
116
+ | `gates.requireChecks` | `forbidden-to-disable` | Requires completion evidence before merge; a merge gate. |
117
+ | `gates.requireTests` | `forbidden-to-disable` | Requires a test receipt before merge; a merge gate. With the tower-tests row off, planning/mode entry refuse the combination. |
118
+ | `gates.allowReviewExemption` | `forbidden-to-disable` | The review-exemption path is part of the review and merge contract. |
119
+ | `gates.maxReportBytes` | `forbidden-to-disable` | The report cap is part of the completion contract; the refusal itself is never configurable away. |
120
+ | `docs.board` | `optional-config` | Writes the human-readable board; no capability is removed. |
121
+ | `docs.boardLocation` | `optional-config` | Chooses the board location. |
122
+ | `archive.enabled` | `optional-config` | Archival is maintenance, not a tool capability; off only leaves terminal missions hot. |
123
+ | `archive.afterDays` | `optional-config` | Archive age threshold. |
124
+ | `archive.maxMissions` | `optional-config` | Hot terminal-mission cap. |
125
+ | `leases.ttlMs` | `optional-config` | Tuning inside the optional tower-leases component. |
126
+ | `dev.reloadDelayMs` | `optional-config` | Delay so a tool result is delivered before a scheduled reload inside the optional tower-hmr component. |
127
+ | `dev.reloadRequiredIds` | `optional-config` | Loader ids passed to reconcileProfile as required by the tower-hmr component. |
128
+
129
+ ## Non-goals
130
+
131
+ This page does not decide whether a key is useful, and it is not an approval
132
+ process. It records the class so the gate can refuse an unclassified or
133
+ forbidden surface.
134
+
135
+ Enforced by: `scripts/check-components.mjs`; tests in `tests/components.test.mjs`; gate: `npm run docs:check:components`.
@@ -1,6 +1,7 @@
1
1
  # Config reference (generated)
2
2
 
3
3
  > Generated by `npm run docs:generate:config` from `domain/config/schema.js`. Values are defaults before repo config.
4
+ > Surface: config keys: 56 · sections: 14 · tools: 29 · baseline 2026-10-23. Generated from `scripts/surface-baseline.json`; do not edit by hand.
4
5
 
5
6
  | Key | Default | Description |
6
7
  |---|---|---|
@@ -57,14 +58,6 @@
57
58
  | `archive.enabled` | `true` | Move terminal (merged/abandoned) missions whose worktree is gone into <git-common-dir>/dsh-tower/archive/ at the next mutation, so state.json stops carrying their full history. |
58
59
  | `archive.afterDays` | `30` | Terminal missions untouched for at least this many days are archived on the next mutation (0 archives them immediately). |
59
60
  | `archive.maxMissions` | `100` | Maximum terminal missions kept hot; once exceeded, the oldest eligible terminal missions are archived until the cap holds (0 archives every eligible terminal mission). |
60
- | `tools.exposeOperational` | `true` | Master switch for batch/resource/import/legacy/recover/test/doctor tools. |
61
- | `tools.exposeBatch` | `true` | Register tower_batch. |
62
- | `tools.exposeImport` | `true` | Register tower_import_legacy/tower_legacy_map. |
63
- | `tools.exposeLeases` | `true` | Register tower_resource advisory leases. |
64
- | `tools.exposeTests` | `true` | Register tower_test; gates.requireTests requires it. |
65
- | `tools.exposeRecovery` | `true` | Register tower_recover (stale state-lock escape). |
66
- | `tools.exposeDoctor` | `true` | Register tower_doctor (read-only health contract). |
67
61
  | `leases.ttlMs` | `900000` | Default advisory lease lifetime; expiry never auto-releases. |
68
- | `dev.hotReload` | `true` | Register tower_reload and /tower reload when the DSH HMR service is available. |
69
62
  | `dev.reloadDelayMs` | `50` | Delay before a scheduled reload, so the tool result is delivered first. |
70
- | `dev.reloadRequiredIds` | `["tower-bundle","tower","tower-skills","preset-tower-worker","preset-tower-reviewer"]` | Loader entry ids passed to reconcileProfile as required. |
63
+ | `dev.reloadRequiredIds` | `["tower","tower-host","preset-tower-worker"]` | Loader entry ids passed to reconcileProfile as required. Optional component rows are deliberately not required, so a row switched off is not a reload failure. |
@@ -74,14 +74,14 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
74
74
  | `spawn.requireLease` | `''` | Optional lease name that must be held before a worker can spawn. |
75
75
  | `send.maxPerMinute` | `120` | Per-repository `tower_send` rate limit (0 disables); exceeding it returns `rate-limited`. |
76
76
  | `send.interruptDefault` | `false` (bundled patch: `true`) | Default for `tower_send.interrupt` (interrupt the running turn before delivering). |
77
- | `tests.enabled` | `true` | Run declared test discovery when the worktree `package.json` configures it. |
77
+ | `tests.enabled` | `true` | Run declared test discovery when the worktree `package.json` configures it. It only tunes the optional `tower-tests` component: with that row off there is no `tower_test` tool and no receipt, and `gates.requireTests:true` is refused rather than silently unmet. |
78
78
  | `tests.admission` | `off` | `spawn` runs discovery before starting a worker, `complete` at `tower_complete`, `off` only on explicit `tower_test` (default; repository code is not executed automatically). |
79
79
  | `tests.timeoutMs` | `60000` | Per discovery command timeout. |
80
80
  | `tests.maxBytes` | `16777216` (16 MiB) | Captured stdout cap per command. |
81
81
  | `tests.autoChecks` | `true` | Attach an automatic `tests` check to the completion report. |
82
82
  | `tests.allowPackageDiscovery` | `true` | Allow repo-controlled `tower.testDiscovery` commands (executes worktree code; see security). |
83
83
  | `gates.requireChecks` | `false` | Require at least one `tower_complete.checks` entry before merge (`no-check-evidence`). |
84
- | `gates.requireTests` | `false` | Require a passing discovery receipt for the current mission branch tip (`tests-missing`); independent of the review gate. |
84
+ | `gates.requireTests` | `false` | Require a passing discovery receipt for the current mission branch tip (`tests-missing`); independent of the review gate. With the `tower-tests` component row off, planning/mode entry refuse this combination with an explanation. |
85
85
  | `gates.allowReviewExemption` | `false` | With `review.requireReview:true`, allow `reviewPolicy:"none"` + a non-empty reason to skip review blockers. Off deliberately; irrelevant while the global review gate is off. |
86
86
  | `gates.maxReportBytes` | `32768` (32 KiB) | Hard cap in UTF-8 bytes for `tower_complete.report`. A larger report is rejected with `invalid-argument` and an actionable `nextAction` (file the full text in the mission worktree and reference its path); it is never truncated. Existing larger reports still load. |
87
87
  | `docs.board` | `true` | Write `MISSIONS.md` after mutations. |
@@ -89,16 +89,14 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
89
89
  | `archive.enabled` | `true` | Move terminal (`merged`/`abandoned`) missions whose worktree is gone into `<git-common-dir>/dsh-tower/archive/` at a mutation boundary. Archived missions keep tasks, reviews, findings and the completion report; they are moved, never deleted. |
90
90
  | `archive.afterDays` | `30` | Age trigger: a terminal mission untouched for at least this many days is archived on the next mutation (`0` archives immediately). |
91
91
  | `archive.maxMissions` | `100` | Cap trigger: once more than this many terminal missions are hot, the oldest eligible ones are archived until the cap holds (`0` archives every eligible terminal mission). A mission with a live worktree is never archived. |
92
- | `tools.exposeOperational` | `true` | Register batch/resource/import/legacy/recover/test/doctor tools; `false` shrinks the fixed tool-schema context. |
93
92
  | `leases.ttlMs` | `900000` | Default advisory lease lifetime; expiry never auto-releases. |
94
- | `dev.hotReload` | `true` | Register `tower_reload`; `/tower reload` is disabled when off. It returns `reload-unavailable` when the profile has no HMR service. |
95
- | `dev.reloadDelayMs` | `50` | Delay before a scheduled reload so the tool result is delivered first. |
96
- | `dev.reloadRequiredIds` | five Tower loader ids | Entry ids passed to `reconcileProfile` as required. |
93
+ | `dev.reloadDelayMs` | `50` | Delay before a scheduled reload so the tool result is delivered first. Tuning inside the optional `tower-hmr` component. |
94
+ | `dev.reloadRequiredIds` | three Tower loader ids | Entry ids passed to `reconcileProfile` as required by the `tower-hmr` component. Optional component rows are deliberately not required. |
97
95
 
98
96
  ## Example
99
97
 
100
98
  ```yaml
101
- - id: tower
99
+ - id: tower-host
102
100
  name: 'dsh-tower'
103
101
  config:
104
102
  mode:
@@ -113,10 +111,14 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
113
111
  reviewerModel:
114
112
  provider: deepseek-account
115
113
  model: deepseek-flash
116
- dev:
117
- hotReload: true
118
114
  ```
119
115
 
116
+ The optional capabilities (`tower-skills`, `tower-batch`, `tower-import`,
117
+ `tower-leases`, `tower-tests`, `tower-hmr`, `preset-tower-reviewer`) are switched
118
+ from the plugin page, not from a config key. The keys that used to switch them
119
+ (`tools.expose*`, `dev.hotReload`) were removed in 2.4.0 and are rejected as
120
+ unknown keys.
121
+
120
122
  ## Looking up the effective schema
121
123
 
122
124
  `cordis_inspect_query` with `Config.listConfigs({ name: "dsh-tower" })` returns the projected JSON
@@ -8,10 +8,35 @@ It returns:
8
8
  "ok": true,
9
9
  "checks": [
10
10
  { "id": "git", "status": "pass", "detail": "git version 2.55.0" },
11
+ { "id": "surface", "status": "pass", "detail": "config keys: 56 · sections: 14 · tools: 29 (baseline 2026-10-23)" },
11
12
  { "id": "state", "status": "pass", "detail": ".../state.json revision 7" },
12
13
  { "id": "state-schema", "status": "pass", "detail": "schemaVersion 3 is current" },
13
14
  { "id": "case-mode", "status": "pass", "detail": "caseInsensitive: true; scope matching uses case-insensitive comparison" }
14
15
  ],
16
+ "surface": {
17
+ "configKeys": 56,
18
+ "sections": 14,
19
+ "tools": 29,
20
+ "baselineDate": "2026-10-23",
21
+ "line": "config keys: 56 · sections: 14 · tools: 29"
22
+ },
23
+ "stateLock": {
24
+ "held": true,
25
+ "pid": 4242,
26
+ "startedAt": 1761280000000,
27
+ "at": 1761280120000,
28
+ "alive": true,
29
+ "ageMs": 121000,
30
+ "heartbeatAgeMs": 1000,
31
+ "staleAfterMs": 3000,
32
+ "stale": false
33
+ },
34
+ "gitLock": {
35
+ "held": true,
36
+ "pid": 4242,
37
+ "phase": "merge",
38
+ "heldMs": 92000
39
+ },
15
40
  "stateSchema": {
16
41
  "currentVersion": 3,
17
42
  "supportedVersion": 3,
@@ -40,6 +65,19 @@ It returns:
40
65
  }
41
66
  ```
42
67
 
68
+ `surface` (and the `surface` check) reports the concept budget in the same format
69
+ as the generated references: `config keys: N · sections: N · tools: N`, plus the
70
+ committed baseline date from `scripts/surface-baseline.json`. The counts are live:
71
+ the config schema and every tool the build ships — `define()`-registered tools,
72
+ the `tower_mode`/`tower_control` pair the Host entry registers directly (#118),
73
+ and the capability tools whose optional component row may be off (#121) —
74
+ independent of the operational switches. The `tools` count is therefore the full
75
+ build surface (29 in 2.4.0), even though the shipped profile mounts 28 because
76
+ `tower-hmr` ships off; the `components` check names the mounted subset and every
77
+ disabled row. `npm run surface:check` is what fails when a count grew without an
78
+ ADR, a matching deletion or a recorded measurement correction. See
79
+ [surface-budget.md](surface-budget.md) for the classification table.
80
+
43
81
  `stateSchema` describes the file **on disk**, not the in-memory document: `currentVersion` is the
44
82
  `schemaVersion` doctor read, `supportedVersion` is the build's `SCHEMA_VERSION`, and `migrations` is
45
83
  the ordered v→v path a write would apply (`[]` when current). `status` is one of `current`,
@@ -49,6 +87,19 @@ and keeps a `state.json.v<N>.bak` backup). A file written by a newer plugin repo
49
87
  `fail`: this build refuses to downgrade it. A state-load failure is reported as a failed check rather
50
88
  than thrown so doctor remains useful on a broken repository.
51
89
 
90
+ `stateLock` (and the `state-lock` check) reports the held lock's age (`ageMs`, from the record's
91
+ `since`) and heartbeat freshness (`heartbeatAgeMs`, from its last `at`) against the stale window
92
+ (`staleAfterMs = 3 × 1000 ms`). A live PID is not proof of a live writer: a holder that stopped
93
+ heartbeating is reported as `stale: true` and is reclaimable by the next mutation (or by
94
+ `tower_recover clear-stale-lock force:true`), while a fresh heartbeat is reported as held and is never
95
+ stolen. This is what makes a recycled PID diagnosable without reading `state.lock` by hand.
96
+
97
+ `gitLock` (and the `git-lock` check) reports the repo-scoped git mutex: `{held, pid, phase, heldMs}`
98
+ while a merge/rebase/init/teardown/provision holds it, and `{held: false}` when free. A merge may hold
99
+ it for the configured merge timeout (15 minutes by default), so a held lock is a `warn` rather than a
100
+ failure; the holder's phase and age make a stalled tower diagnosable. `tower_status` returns the same
101
+ `gitLock` object.
102
+
52
103
  `archive` reports the terminal-mission archive: active `missions`, `archivedMissions` and `stateBytes`,
53
104
  the archive directory's `archiveBytes`/`years`/`files`, every `unreadable` diagnostic, and the configured
54
105
  `policy` (`enabled`, `afterDays`, `maxMissions`). A missing archive directory is a healthy empty archive
@@ -68,8 +119,8 @@ never probes — a probe creates a temporary file, and a read-only call must not
68
119
  applies.
69
120
 
70
121
  Checks cover: git availability, repository identity, state schema/load, the detected case mode, repo
71
- config presence, worker/reviewer preset names, subagents and HMR services, workspace lock
72
- holder/staleness, live roster, worktree existence/ownership/dirtiness, test-discovery policy, legacy
122
+ config presence, worker/reviewer preset names, subagents and HMR services, state lock age/heartbeat,
123
+ workspace lock holder/staleness, live roster, worktree existence/ownership/dirtiness, test-discovery policy, legacy
73
124
  snapshots, batches and leases. Missions intentionally cleaned up by `tower_teardown` are skipped (the
74
125
  check notes the removed count) instead of being reported as missing.
75
126
  `warn` is advisory; `fail` means the repository needs attention before spawning/merging.
@@ -2,40 +2,44 @@
2
2
 
3
3
  > Generated by `npm run docs:generate:errors` from `domain/errors.js`. Do not edit by hand.
4
4
 
5
- | Code | Description |
6
- |---|---|
7
- | `invalid-argument` | A tool argument is missing, malformed, or not allowed. |
8
- | `repo-invalid` | repoRoot is missing, relative, or not inside a Git repository. |
9
- | `not-initialized` | Tower is not initialized for this repository; run tower_init. |
10
- | `mission-not-found` | No mission with that id exists. |
11
- | `finding-not-found` | No finding with that id exists. |
12
- | `invalid-transition` | The requested state transition is not allowed. |
13
- | `stale-base` | The repository state changed while the operation was being prepared; retry. |
14
- | `state-locked` | Another Tower writer holds the state lock. |
15
- | `state-corrupt` | The Tower state file is unreadable or invalid. |
16
- | `state-version-unsupported` | The Tower state file uses a schema version this build cannot read or downgrade. |
17
- | `state-migration-failed` | A Tower state migration failed; the original state file was not modified. |
18
- | `config-invalid` | Tower configuration is invalid. |
19
- | `config-unknown-key` | Tower configuration has an unknown top-level key. |
20
- | `git-failed` | A git command failed. |
21
- | `git-timeout` | A git command timed out or exceeded its output buffer. |
22
- | `worktree-dirty` | The mission worktree has uncommitted changes and cannot complete. |
23
- | `spawn-unavailable` | The subagents service is unavailable. |
24
- | `provider-missing` | No subagent provider is registered. |
25
- | `provider-cooldown` | The provider is throttled; spawn is in a short cooldown. |
26
- | `preset-missing` | The configured agent preset is not registered. |
27
- | `reviewer-prerequisite` | The reviewer spawn prerequisite is not met. |
28
- | `not-authorized` | The calling agent role may not perform this action. |
29
- | `workspace-busy` | Another Tower session owns this repository workspace lock. |
30
- | `worker-limit` | The repository reached spawn.maxLiveWorkers live workers. |
31
- | `rate-limited` | The tower_send per-minute rate limit was reached. |
32
- | `takeover-refused` | The mission already has a live worker that was not stopped first. |
33
- | `reload-unavailable` | The DSH HMR service is not available in this profile. |
34
- | `reload-failed` | The HMR reload failed; check the DSH log and restart if needed. |
35
- | `tests-failed` | Declared test discovery failed or did not meet its admission requirements. |
36
- | `tests-timeout` | Test discovery exceeded its time or output budget. |
37
- | `lease-busy` | Another holder owns the requested advisory lease. |
38
- | `lease-token` | The lease release token does not match the current holder. |
39
- | `batch-invalid` | The batch request or its close evidence is invalid. |
40
- | `legacy-invalid` | The legacy snapshot or mapping is invalid. |
41
- | `tool-internal` | An unexpected failure; read the message and the activity log. |
5
+ The catalog is the source of truth for the stable codes and the recovery metadata the tool boundary
6
+ renders: `retryable` marks a failure that can succeed later unchanged, and `nextAction` is the
7
+ single recovery step appended to the message as ` — next: …` (with ` (retryable)` when retryable).
8
+
9
+ | Code | Meaning | Retryable | Next action |
10
+ |---|---|---|---|
11
+ | `invalid-argument` | A tool argument is missing, malformed, or not allowed. | no | — |
12
+ | `repo-invalid` | repoRoot is missing, relative, or not inside a Git repository. | no | pass an absolute path to a directory inside a Git repository |
13
+ | `not-initialized` | Tower is not initialized for this repository; run tower_init. | no | run tower_init for this repository |
14
+ | `mission-not-found` | No mission with that id exists. | no | list missions with tower_status |
15
+ | `finding-not-found` | No finding with that id exists. | no | list findings with tower_findings |
16
+ | `invalid-transition` | The requested state transition is not allowed. | no | — |
17
+ | `stale-base` | The repository state changed while the operation was being prepared; retry. | yes | retry the tool; if it keeps failing, run tower_preflight and inspect tower_status |
18
+ | `state-locked` | Another Tower writer holds the state lock. | yes | wait for the other Tower writer to finish, then retry |
19
+ | `state-corrupt` | The Tower state file is unreadable or invalid. | no | restore state.json from a backup, or re-initialize after filing the corruption |
20
+ | `state-version-unsupported` | The Tower state file uses a schema version this build cannot read or downgrade. | no | upgrade dsh-tower to a release that supports this state schemaVersion |
21
+ | `state-migration-failed` | A Tower state migration failed; the original state file was not modified. | no | restore the pre-migration backup state.json.v<N>.bak and report the migration bug |
22
+ | `config-invalid` | Tower configuration is invalid. | no | fix the repository config file, then retry |
23
+ | `config-unknown-key` | Tower configuration has an unknown top-level key. | no | remove the unknown key from the repository config file, then retry |
24
+ | `git-failed` | A git command failed. | no | — |
25
+ | `git-timeout` | A git command timed out or exceeded its output buffer. | yes | retry; if it persists, raise git.timeoutMs or merge.timeoutMs |
26
+ | `worktree-dirty` | The mission worktree has uncommitted changes and cannot complete. | no | commit or remove the worktree changes, then retry tower_complete |
27
+ | `spawn-unavailable` | The subagents service is unavailable. | no | enable the DSH subagents service, then retry |
28
+ | `provider-missing` | No subagent provider is registered. | no | register a subagent provider in the DSH profile, then retry |
29
+ | `provider-cooldown` | The provider is throttled; spawn is in a short cooldown. | yes | wait for the cooldown to expire, then retry |
30
+ | `preset-missing` | The configured agent preset is not registered. | no | install the tower-worker and tower-reviewer agent presets and restart DSH |
31
+ | `reviewer-prerequisite` | The reviewer spawn prerequisite is not met. | no | complete the mission first, then spawn the reviewer |
32
+ | `not-authorized` | The calling agent role may not perform this action. | no | call the tool as the tower, or as the worker/reviewer it is assigned to |
33
+ | `workspace-busy` | Another Tower session owns this repository workspace lock. | yes | run /tower off in the other session or wait for its lock to become stale |
34
+ | `worker-limit` | The repository reached spawn.maxLiveWorkers live workers. | no | wait for a live worker to finish or raise spawn.maxLiveWorkers |
35
+ | `rate-limited` | The tower_send per-minute rate limit was reached. | yes | wait for the next per-minute window or raise send.maxPerMinute |
36
+ | `takeover-refused` | The mission already has a live worker that was not stopped first. | no | run tower_interrupt for the live worker, then retry the takeover |
37
+ | `reload-unavailable` | The DSH HMR service is not available in this profile. | no | restart DSH instead of reloading |
38
+ | `reload-failed` | The HMR reload failed; check the DSH log and restart if needed. | no | check the DSH log and restart DSH |
39
+ | `tests-failed` | Declared test discovery failed or did not meet its admission requirements. | no | fix the failing tests, then retry tower_complete |
40
+ | `tests-timeout` | Test discovery exceeded its time or output budget. | no | raise tests.timeoutMs or tests.maxBytes (or speed up the suite), then retry |
41
+ | `lease-busy` | Another holder owns the requested advisory lease. | no | release the lease or wait for it to expire, then retry |
42
+ | `lease-token` | The lease release token does not match the current holder. | no | use the token returned when the lease was acquired |
43
+ | `batch-invalid` | The batch request or its close evidence is invalid. | no | fix the batch request or the receipt hashes, then retry |
44
+ | `legacy-invalid` | The legacy snapshot or mapping is invalid. | no | check the legacy snapshot path, schema and mapping, then retry |
45
+ | `tool-internal` | An unexpected failure; read the message and the activity log. | no | — |