dsh-tower 2.1.0 → 2.3.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 (170) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/COMPATIBILITY.md +5 -1
  3. package/CONTRIBUTING.md +64 -5
  4. package/PRINCIPLES.md +145 -0
  5. package/README.en.md +7 -4
  6. package/README.md +7 -4
  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/docs/README.md +6 -2
  44. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  45. package/docs/adr/0002-single-state-json.md +50 -0
  46. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  47. package/docs/adr/0004-surface-budget.md +50 -0
  48. package/docs/adr/README.md +51 -0
  49. package/docs/agents/workflow.md +51 -0
  50. package/docs/architecture/overview.md +167 -36
  51. package/docs/guide/hot-reload.md +1 -1
  52. package/docs/guide/release.md +1 -1
  53. package/docs/operations/incident.md +82 -0
  54. package/docs/operations/runbook.md +12 -3
  55. package/docs/reference/blockers.md +2 -2
  56. package/docs/reference/config.generated.md +1 -0
  57. package/docs/reference/doctor.md +48 -2
  58. package/docs/reference/errors.generated.md +41 -37
  59. package/docs/reference/errors.md +53 -41
  60. package/docs/reference/events.md +38 -6
  61. package/docs/reference/surface-budget.md +128 -0
  62. package/docs/reference/tools.generated.md +5 -4
  63. package/docs/reference/tools.md +4 -4
  64. package/docs/zh/guide/hot-reload.md +1 -1
  65. package/docs/zh/reference/blockers.md +2 -2
  66. package/docs/zh/reference/doctor.md +29 -1
  67. package/docs/zh/reference/tools.md +4 -4
  68. package/domain/batch.js +2 -2
  69. package/domain/config/schema.js +25 -1
  70. package/domain/contracts/outputs.js +6 -2
  71. package/domain/contracts/tool-inputs/ops.js +1 -0
  72. package/domain/errors.js +297 -42
  73. package/domain/events.js +207 -0
  74. package/domain/integration/gate.js +17 -2
  75. package/domain/integration/merge.js +35 -0
  76. package/domain/lockBudget.js +30 -0
  77. package/domain/missions/completion.js +14 -0
  78. package/domain/missions/lifecycle.js +9 -3
  79. package/domain/missions/plan.js +14 -0
  80. package/domain/missions/review.js +90 -7
  81. package/domain/read/views.js +3 -0
  82. package/domain/state/archive.js +1 -1
  83. package/domain/state.js +1 -1
  84. package/domain/surface.js +248 -0
  85. package/{lib → host}/doctor.js +116 -9
  86. package/{lib → host}/modeStore.js +1 -1
  87. package/{lib → host}/tool-registry.js +106 -31
  88. package/host/tower.js +32 -0
  89. package/index.js +12 -9
  90. package/package.json +10 -3
  91. package/ports/archive.js +18 -0
  92. package/ports/caseSensitivity.js +15 -0
  93. package/ports/clock.js +10 -2
  94. package/ports/config.js +19 -0
  95. package/ports/contract.js +38 -0
  96. package/ports/events.js +24 -0
  97. package/ports/ids.js +10 -2
  98. package/ports/index.js +54 -2
  99. package/ports/ownership.js +17 -0
  100. package/ports/state.js +16 -0
  101. package/ports/store.js +52 -3
  102. package/ports/vcs.js +25 -4
  103. package/scripts/architecture-waivers.json +19 -0
  104. package/scripts/architecture.mjs +634 -23
  105. package/scripts/check-docs.mjs +8 -35
  106. package/scripts/check-pack.mjs +68 -39
  107. package/scripts/check-philosophy.mjs +266 -0
  108. package/scripts/domain-metrics.mjs +13 -4
  109. package/scripts/generate-config-reference.mjs +14 -1
  110. package/scripts/generate-error-reference.mjs +68 -14
  111. package/scripts/generate-events-reference.mjs +83 -0
  112. package/scripts/generate-tools-reference.mjs +11 -0
  113. package/scripts/issue-audit.mjs +2 -2
  114. package/scripts/lib/surface.mjs +87 -0
  115. package/scripts/pack-manifest.txt +58 -22
  116. package/scripts/regression-audit.mjs +164 -0
  117. package/scripts/release-check.mjs +1 -1
  118. package/scripts/surface-baseline.json +21 -0
  119. package/scripts/surface-baseline.mjs +120 -0
  120. package/tools/adopt.js +2 -3
  121. package/tools/batch.js +2 -3
  122. package/tools/complete.js +2 -3
  123. package/tools/diff.js +3 -3
  124. package/tools/doctor.js +7 -10
  125. package/tools/finding.js +2 -3
  126. package/tools/findings.js +3 -3
  127. package/tools/import-legacy.js +3 -5
  128. package/tools/inbox.js +3 -3
  129. package/tools/index.js +7 -7
  130. package/tools/init.js +3 -6
  131. package/tools/interrupt.js +2 -3
  132. package/tools/legacy-map.js +3 -5
  133. package/tools/merge.js +3 -3
  134. package/tools/mission.js +2 -3
  135. package/tools/plan.js +6 -5
  136. package/tools/preflight.js +2 -3
  137. package/tools/rebase.js +3 -3
  138. package/tools/recover.js +38 -13
  139. package/tools/reload.js +3 -6
  140. package/tools/resource.js +3 -5
  141. package/tools/review.js +3 -4
  142. package/tools/roster.js +2 -3
  143. package/tools/send.js +2 -2
  144. package/tools/spawn.js +8 -5
  145. package/tools/status.js +3 -3
  146. package/tools/teardown.js +2 -3
  147. package/tools/test.js +2 -2
  148. package/types/index.d.ts +1 -1
  149. package/ui-bundle/README.md +37 -3
  150. package/ui-bundle/client.cards.js +356 -0
  151. package/ui-bundle/client.denied.js +142 -0
  152. package/ui-bundle/client.dock.js +333 -0
  153. package/ui-bundle/client.helpers.js +271 -0
  154. package/ui-bundle/client.js +79 -1747
  155. package/ui-bundle/client.locales.js +247 -0
  156. package/ui-bundle/client.mode.js +206 -0
  157. package/ui-bundle/client.overlay.js +64 -0
  158. package/ui-bundle/client.styles.js +151 -0
  159. package/ui-bundle/client.toolview.js +82 -0
  160. package/ui-bundle/package.json +1 -0
  161. package/lib/persistence.js +0 -356
  162. package/lib/store.js +0 -133
  163. /package/{lib → adapters}/stateFile.js +0 -0
  164. /package/{lib → app}/briefing.js +0 -0
  165. /package/{lib → app}/spawn-support.js +0 -0
  166. /package/{lib → host}/compatibility.js +0 -0
  167. /package/{lib → host}/config.js +0 -0
  168. /package/{lib → host}/hmr.js +0 -0
  169. /package/{lib → host}/modeState.js +0 -0
  170. /package/{lib → host}/presets.js +0 -0
@@ -10,7 +10,7 @@ or skill can remain mounted while the `tower_*` tools are gone.
10
10
  ```
11
11
  Host plugin (index.js)
12
12
  ├─ /tower command + Tower mode (guard, prompt section, tower_mode/tower_control, /tower reload)
13
- ├─ lib/tool-registry.js -> registration aggregator, actor guards, shared helpers
13
+ ├─ host/tool-registry.js -> registration aggregator, actor guards, shared helpers
14
14
  │ (+ 27 registered repository tools)
15
15
  │ (+ tower_reload while dev.hotReload is on; fails reload-unavailable without HMR)
16
16
  ├─ tools/index.js -> thin public wiring (re-exports the registry + briefings)
@@ -18,20 +18,37 @@ Host plugin (index.js)
18
18
  ├─ app/init.js, provision.js, merge.js, rebase.js, teardown.js
19
19
  ├─ app/review.js, findings.js, batch.js, messages.js, roster.js
20
20
  ├─ app/plan.js, mission.js, checkCompletion.js, spawn.js, recording.js
21
- └─ app/gate.js, read.js, board.js, missionContext.js, enrichedMissions.js
22
- ports (explicit contracts):
23
- ports/store.js vcs.js clock.js ids.js
24
- adapters (the only I/O):
25
- adapters/store-fs.js the only module importing node:fs
26
- adapters/vcs-git.js the only module importing node:child_process
27
- adapters/default.js composition root: store, vcs, clock, ids
28
- infrastructure (lib/):
29
- lib/store.js repo-scoped lock + atomic JSON + activity.jsonl
30
- lib/git.js bounded git commands + timeout classification
31
- lib/config.js profile/repo config layers over the pure schema
32
- lib/workspaceLock.js optional one-Tower-session-per-repository advisory lock
33
- lib/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
34
- lib/modeStore.js per-agent Tower mode persistence
21
+ ├─ app/spawn-prechecks.js, spawn-support.js, briefing.js
22
+ └─ app/archive.js, gate.js, read.js, board.js, missionContext.js, enrichedMissions.js
23
+ host (composition root + process/session glue; the only layer that may hold ambient state):
24
+ host/tool-registry.js registration aggregator + shared guards/helpers
25
+ host/config.js profile/repo config layers over the pure schema
26
+ host/doctor.js read-only environment contract and health checks
27
+ host/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
28
+ host/modeStore.js per-agent Tower mode persistence
29
+ host/modeState.js owned in-memory mode state
30
+ host/presets.js cross-field preset validation before work starts
31
+ host/compatibility.js COMPATIBILITY.md provenance
32
+ host/tower.js default-deps Tower binding for tests/embedders
33
+ ports (explicit runtime contracts, no implementation):
34
+ ports/contract.js definePort/assertPort runtime contract primitives
35
+ ports/{store,vcs,clock,ids,config,state,ownership,caseSensitivity,archive}.js
36
+ ports/index.js PORTS table + assertPorts(deps) injection check
37
+ adapters (the only I/O; they implement the ports):
38
+ adapters/store-fs.js the only module importing node:fs; createStorePort()
39
+ adapters/vcs-git.js the only module importing node:child_process; createVcsPort()
40
+ adapters/git.js bounded git commands + timeout classification
41
+ adapters/store.js repo-scoped lock + atomic JSON + activity.jsonl
42
+ adapters/persistence.js shared atomic write / lock / heartbeat primitives
43
+ adapters/archive.js terminal-mission archive files
44
+ adapters/leases.js repo-scoped advisory lease ledger
45
+ adapters/workspaceLock.js optional one-Tower-session-per-repository lock
46
+ adapters/worktreeOwnership.js worktree identity verification
47
+ adapters/stateFile.js state-file load/migrate/persist
48
+ adapters/caseSensitivity.js filesystem case-folding probe
49
+ adapters/legacy.js read-only legacy snapshot validation + storage
50
+ adapters/tests.js test discovery execution + TAP/JSON parsing
51
+ adapters/default.js createDefaultDeps(): the one default port implementation set
35
52
  pure domain modules (no fs/git):
36
53
  domain/errors.js stable, structural tool error catalog
37
54
  domain/batch.js batch admission create/close/summary validation
@@ -48,7 +65,6 @@ Host plugin (index.js)
48
65
  domain/report.js board rendering
49
66
  domain/roster.js roster predicates
50
67
  tools layer:
51
- lib/tool-registry.js registration aggregator + shared guards/helpers
52
68
  tools/index.js thin public wiring (re-exports only)
53
69
  tools/*.js one module per tool, handler + imported input schema
54
70
  Subagents
@@ -61,6 +77,54 @@ The main checkout is the control-tower repo; each mission has a branch `tower/<i
61
77
  worktree in `.dsh-tower-worktrees/<hash>/<missionId>`. The plugin never operates on the main checkout's
62
78
  working tree except merge and the read-only diff.
63
79
 
80
+ ## Ports and the composition root (#69)
81
+
82
+ The seam is real, not documented-only. `ports/` exports **runtime contracts**: each module defines a
83
+ frozen `{ name, methods }` descriptor plus an `assert*Port` helper, and `ports/index.js` exposes the
84
+ `PORTS` table and `assertPorts(deps)`.
85
+
86
+ `app/**` imports only app siblings, `domain/**`, `ports/**` and `node:path`. Every effectful call goes
87
+ through the `deps` object:
88
+
89
+ | deps member | contract | default implementation |
90
+ |---|---|---|
91
+ | `store` | StorePort: `pathsFor/exists/ensureDir/readJson/writeJsonAtomic/appendJsonl/withLock/withGitLock/readActivityTail` plus the raw-file and lock primitives `readText/writeText/appendText/rename/renameWithRetry/stat/unlink/readGitLock/gitLockView` | `adapters/store-fs.js` `createStorePort()` |
92
+ | `vcs` | VcsPort: `git/tryGit/revParse/isClean/isAncestor/resolveRepo/currentBranch/countCommits/listWorktrees` | `adapters/vcs-git.js` `createVcsPort()` |
93
+ | `clock`, `ids` | ClockPort / IdsPort | `adapters/default.js` |
94
+ | `config` | ConfigPort: `runtimeConfig/resolveRepoConfig` | `host/config.js` |
95
+ | `state` | StatePort: `loadStateFile` | `adapters/stateFile.js` |
96
+ | `archive` | ArchivePort: `readIndex/readEntry/writeEntries` | `adapters/archive.js` |
97
+ | `ownership` | OwnershipPort: `assertOwnership/canonicalPath` | `adapters/worktreeOwnership.js` |
98
+ | `caseSensitivity` | CaseSensitivityPort: `detect` | `adapters/caseSensitivity.js` |
99
+
100
+ `index.js` is the composition root: it calls `createDefaultDeps()` once and passes the object to
101
+ `registerTowerTools`. The registry adopts and validates that object (`assertPorts`) instead of building
102
+ its own adapter set, hands every tool an `openTower(repoRoot)` factory bound to those ports, and
103
+ `Tower.open(repoRoot, deps)` / `new Tower(repo, config, deps)` validates the object again before use.
104
+ `app/tower.js` itself imports no adapter and no `host/` module. The one piece of lock policy the app and
105
+ the store share (the git-mutex budget, #93) is pure and lives in `domain/lockBudget.js`;
106
+ `adapters/store.js` re-exports it for the infrastructure callers.
107
+
108
+ `store.rename` and `store.renameWithRetry` are both declared because they are not interchangeable: the
109
+ retrying helper has temp-file semantics and removes its source when the rename finally fails, which is
110
+ right for a disposable temp file but would destroy the lock bytes `tower_recover` must keep auditable.
111
+ The audit rename therefore uses the plain `rename`. The contract test scans `app/**` **and** `tools/**`
112
+ for every `deps.<port>.<method>` call and fails if the named port does not declare it, so a tool that
113
+ reaches a new port method cannot slip past `assertPorts`.
114
+
115
+ `lib/` no longer exists (#70): every module was `git mv`-ed into the layer that owns it. The I/O
116
+ implementations now sit in `adapters/` (`store`, `persistence`, `git`, `archive`, `stateFile`,
117
+ `leases`, `workspaceLock`, `worktreeOwnership`, `caseSensitivity`, `legacy`, `tests`), the
118
+ composition root and process/session glue in `host/`, and spawn policy in `app/`. `host/**` is the
119
+ only source root, besides `adapters/**` itself, allowed to import an adapter; `app/**` and `domain/**`
120
+ may not import `lib/**` (rule `no-lib-from-core`) and may not hold ambient module state
121
+ (`host-scope`). Tools receive effectful helpers from the registry instead of importing an adapter.
122
+
123
+ A full mission lifecycle (init → plan → provision → spawn → complete → review → gate → merge →
124
+ teardown) runs through the real app use cases on an in-memory store and a scripted vcs:
125
+ `tests/ports-lifecycle.test.mjs`. The default adapter set and the in-memory doubles are both asserted
126
+ against the same port contracts in `tests/memory-adapters.contract.test.mjs`.
127
+
64
128
  ## Mission state machine
65
129
 
66
130
  ```
@@ -82,7 +146,7 @@ set by `tower_merge`/reconciliation.
82
146
  against the merge base.
83
147
  3. Survey missions must not change files; build missions must stay inside `scope`.
84
148
  4. The main checkout must be clean for the files this merge would overwrite and on the base branch.
85
- 5. Dependency missions must be merged/abandoned.
149
+ 5. Dependency missions must be merged/abandoned, except a completed survey dependency which is its terminal success state (#107).
86
150
  6. If `review.requireReview:true` (default false), a review must exist, belong to this mission, be
87
151
  clean, approve merge, and be bound to the tip (unless a clean rebase waived it). No worker may be
88
152
  running.
@@ -105,7 +169,7 @@ After the command, regardless of exit status, it inspects `HEAD`:
105
169
  | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
106
170
  | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
107
171
  | `<git-common-dir>/dsh-tower/activity.jsonl` | append-only activity |
108
- | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (dead-PID reclaimed, live lock reported) |
172
+ | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (heartbeat reclaim; a recycled PID cannot pin it) |
109
173
  | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
110
174
  | `<git-common-dir>/dsh-tower/lock-holds.jsonl` | state/git lock-hold telemetry |
111
175
  | `<checkout parent>/.dsh-tower-worktrees/<hash>/<missionId>` | mission worktree |
@@ -113,8 +177,10 @@ After the command, regardless of exit status, it inspects `HEAD`:
113
177
  | `$DSH_HOME/tower-mode/modes.lock` | mode-store single-writer lock |
114
178
 
115
179
  `state.json` and `modes.json` both use an atomic write (temp + rename), token-checked lock release,
116
- dead-PID reclamation, and bounded lock waits. `state.json` is the mission persistence contract;
117
- `modes.json` is the per-agent mode contract; neither is written without its own lock.
180
+ heartbeat-based reclamation (a live PID with a silent heartbeat is reclaimed after byte verification;
181
+ a live holder that keeps heartbeating is never stolen), and bounded lock waits. `state.json` is the
182
+ mission persistence contract; `modes.json` is the per-agent mode contract; neither is written without
183
+ its own lock.
118
184
 
119
185
  ## Layering rules
120
186
 
@@ -123,17 +189,66 @@ dead-PID reclamation, and bounded lock waits. `state.json` is the mission persis
123
189
  - `domain/**` must not import I/O or adapters (`domain-pure`).
124
190
  - Only `adapters/vcs-git.js` may import `node:child_process`, and only
125
191
  `adapters/store-fs.js` may import `node:fs` (`child-process-surface`, `fs-surface`).
126
- The rule is enforced over the source roots — `lib/`, `app/`, `domain/`, `tools/`,
192
+ The rule is enforced over the source roots — `host/`, `app/`, `domain/`, `tools/`,
127
193
  `adapters/`, `ports/` — and does not cover `scripts/**`, which holds maintenance
128
194
  entry points (release check, mount smoke, docs generators) that may spawn git or npm.
195
+ The optional Web client (`ui-bundle/`) is its own tier with its own rules.
129
196
  - `tools/**` must stay thin and free of status/type literals (`tools-thin`, `tools-vocabulary`);
130
- `tools/index.js` is the thin public wiring module and stays exempt from the thinness rule,
131
- while registration/shared machinery lives in `lib/tool-registry.js`.
132
- - Aggregate files have size ceilings; top-level mutable state is limited to explicit composition
133
- roots (`aggregate-size`, `ambient-state`). The per-tool ceiling is 200 lines; only the
134
- `tools/index.js` wiring module is exempt, and it is now far below the ceiling.
197
+ `tools/index.js` is the thin public wiring module (it imports `host/tool-registry.js`, not a
198
+ `node:` module), while registration/shared machinery lives in `host/tool-registry.js`.
199
+ Tools receive effectful helpers (repository resolution, leases, recovery, legacy import) from
200
+ the registry instead of importing an adapter.
201
+ - Aggregate files have size ceilings; top-level mutable state is limited to `host/**`
202
+ (`aggregate-size`, `host-scope`). The per-tool ceiling is 200 lines; `tools/index.js`
203
+ currently sits inside the 40-200 range, so the tools allow-list is empty rather than silently
204
+ exempting the wiring module.
205
+ - `app/**` may import only app siblings, `domain/**`, `ports/**` and `node:path` (`app-ports-only`);
206
+ an app module that reaches an adapter, a `host/` module or `lib/` fails the gate.
207
+ - `app/**` and `domain/**` may not import the retired `lib/` layer (`no-lib-from-core`); with the
208
+ layer gone this keeps it from creeping back.
209
+ - `host/**` is the only source root that may hold ambient module state (`host-scope`);
210
+ `app/**` and `domain/**` must stay stateless at module scope. Only `host/**` and `adapters/**`
211
+ may import an adapter (`composition-root`); `app/`, `tools/`, `domain/` and `ports/` never may.
212
+ - Every `ports/**` module must export a runtime contract (`const`/`function`/`class`) and must not
213
+ import an implementation (`port-shape`); a typedef plus `export {}` is no longer accepted.
214
+ - `ui-bundle/**` is a browser tier, not host code (#71): no `node:*` import
215
+ (`client-no-node` — DOM globals are fine; host capabilities must come through the
216
+ plugin's registration surface), every client file stays at or below 800 lines
217
+ (`client-size`), and module-level mutable state is banned exactly as it is for
218
+ `app/**` (`client-ambient`); the split surfaces keep their state inside the
219
+ `create*` closure the entry composes. `ui-bundle/` is also excluded from
220
+ `aggregate-size`, which it would otherwise duplicate.
135
221
 
136
- A fixture test seeds one violation per rule, so a rule that cannot fail is itself a test failure.
222
+ ### Exemptions are justified data
223
+
224
+ The gate's only exemptions live in `scripts/architecture-waivers.json`; `scripts/architecture.mjs`
225
+ keeps no allow-lists of its own. Since #70 the file has exactly two entries, both I/O adapter
226
+ boundaries (`adapters/vcs-git.js` for `node:child_process` and `adapters/store-fs.js` for
227
+ `node:fs`). Each entry names the `rule` and `path` and must carry a non-empty `reason` plus an
228
+ `owner` or `ref`. An entry missing any of those is itself a blocking `waiver-contract` violation
229
+ that names the rule and the path, so a bare path cannot turn the gate green. The waiver file is a
230
+ two-key path in `CONTRIBUTING.md`; there is no `reviewBy`/expiry field in this release, so the
231
+ reason and the owner are the reviewed decision.
232
+
233
+ `host-scope` replaces the old `ambient-state` waivers (#70). It scans `app/**` and `domain/**` and
234
+ records every module-level `let`; `host/**` owns the process/session state and needs no waiver. The
235
+ scan is statement-aware rather than line-based: it walks the module's token stream and records every
236
+ binding of every top-level `let` declaration. A second declarator (`let a = 1, b = 2`), a second
237
+ statement on one line (`let a = 1; let b = 2`), and any new top-level `let` in `app/` or `domain/`
238
+ are each their own violation. Strings, comments, template literals and regex literals are skipped,
239
+ and `let` inside a function, block, class or `for (…)` header is not module-level state.
240
+
241
+ `domain-pure`, `host-scope`, `no-lib-from-core`, and anything under `domain/**` are load-bearing: no
242
+ waiver may target them, even a justified one. A `waiver-contract` violation rejects that entry and
243
+ the underlying rule still fails.
244
+
245
+ A green run prints `architecture: ok (waivers: N)`; N is the number of entries that actually
246
+ suppressed a violation, so a stale waiver is visible in the output and in the tests. The fixture
247
+ suite seeds an unjustified waiver (it fails), a justified one (it passes), a `host-scope` fixture
248
+ that puts ambient state in `host/` (allowed) and in `app/`/`domain/` (rejected), a `no-lib-from-core`
249
+ import from `app/` (rejected), a same-line second binding (rejected), and a new violation inside
250
+ each rule. A separate fixture seeds one violation per rule, so a rule that cannot fail is itself a
251
+ test failure.
137
252
 
138
253
  ## Mount smoke and host internals
139
254
 
@@ -149,24 +264,40 @@ probe adjustments in the same change.
149
264
 
150
265
  All epic DoD items now have an executable gate:
151
266
 
152
- - Pure domain share: **37.9%** (5,993 / 15,820 source lines), enforced by
153
- `npm run domain:check` (threshold 35%).
154
- - `domain/**` imports no fs, child_process, adapters, app, tools, or lib infrastructure;
267
+ - Pure domain share: **39.5%** (7,475 / 18,936 host source lines), enforced by
268
+ `npm run domain:check` (threshold 35%). `npm run domain:metrics` also prints a
269
+ separate `ui-bundle` line, so Web-client growth is visible in CI output while the
270
+ share itself still measures the Host architecture.
271
+ - `domain/**` imports no fs, child_process, adapters, app, tools, or host infrastructure;
155
272
  `domain-pure` is a blocking architecture rule.
156
273
  - Inside the source roots, only `adapters/vcs-git.js` imports `node:child_process`; only
157
- `adapters/store-fs.js` imports `node:fs`; both are blocking rules with fixtures.
158
- - `lib`, `app`, and `domain` files stay <= 800 lines; every `tools/**` file stays
274
+ `adapters/store-fs.js` imports `node:fs`; both are blocking rules whose files are the only
275
+ `child-process-surface`/`fs-surface` entries in `scripts/architecture-waivers.json`.
276
+ - `host`, `app`, and `domain` files stay <= 800 lines; the same ceiling applies to
277
+ `ui-bundle/**` (`client-size`); every `tools/**` file stays
159
278
  40-200 lines (one handler module per tool; `tools/index.js` is 40-line wiring).
160
279
  - No `withLock` scope starts a process; the two-phase tests instrument every git call.
161
280
  - `tools/**` has no status/type/enum literal; vocabulary is owned by
162
281
  `domain/vocabulary.js`; the `tools-vocabulary` rule is blocking.
163
282
  - `validateState` has a four-line body over `domain/state/schema.js` and
164
283
  `domain/state/semantics.js`.
165
- - `state.json` and `modes.json` share `lib/persistence.js` atomic write, token-owned
284
+ - `state.json` and `modes.json` share `adapters/persistence.js` atomic write, token-owned
166
285
  lock, dead-PID quarantine, and bounded-wait primitives.
167
- - The in-memory lifecycle and adapter contracts run a full mission lifecycle with no
168
- disk and no git.
169
- - The seven fitness rules are errors, each with a seeded failing fixture.
286
+ - The in-memory lifecycle runs the real app use cases (init → plan → provision → spawn → complete →
287
+ review → gate → merge → teardown) on an in-memory store and scripted vcs supplied through the
288
+ composition root, with no disk, no git and no `defaultDeps` (#69).
289
+ - The eleven host fitness rules are errors, each with a seeded failing fixture; the
290
+ three `client-*` rules form the Web-client tier with one fixture each, and the
291
+ `waiver-contract` rule makes an unjustified exemption blocking as well.
292
+
293
+ ## Non-goals
294
+
295
+ This layer map is not a dependency-injection container, a service locator, or a
296
+ place to register classes by reflection. Ports stay runtime contracts and the
297
+ composition root stays the only place that assembles implementations.
298
+
299
+ Enforced by: `scripts/architecture.mjs` and `ports/index.js`; tests in
300
+ `tests/architecture.test.mjs`; gate: `npm run architecture:check`.
170
301
 
171
302
 
172
303
 
@@ -86,7 +86,7 @@ To stop watching, run `npm run dev:hmr -- --remove` and restart DSH once.
86
86
 
87
87
  ## The edit loop
88
88
 
89
- 1. Edit `lib/*.js`, `index.js`, `worker.js`, `reviewer.js` or `skills.js`.
89
+ 1. Edit `host/*.js`, `adapters/*.js`, `app/*.js`, `domain/**/*.js`, `index.js`, `worker.js`, `reviewer.js` or `skills.js`.
90
90
  2. Save the file. The watcher debounces (DSH default 100 ms) and reconciles the profile; changed
91
91
  modules are replaced and the affected Loader entries restart.
92
92
  3. Watch the DSH log for `reload plugin at <path>` / `restart required: …` messages.
@@ -58,7 +58,7 @@ Put the **host build in cell 0** and the **tag in cell 1**:
58
58
  | 0.2.1-rc.2 | v2.1.0-beta.1 | 2026-10-22 | `npm test`, `npm run docs:check`, mount smoke |
59
59
  ```
60
60
 
61
- `lib/compatibility.js#parseCompatibility` reads cells 0–3 only: never insert a column before
61
+ `host/compatibility.js#parseCompatibility` reads cells 0–3 only: never insert a column before
62
62
  the tag and never add one before "what was run", or `tower_doctor` stops seeing the row. The
63
63
  host pin in `scripts/mount-smoke/host.json` must equal cell 0 of the newest row (a unit test
64
64
  enforces it).
@@ -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
 
@@ -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). |
@@ -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: 64 · sections: 15 · tools: 27 · baseline 2026-10-23. Generated from `scripts/surface-baseline.json`; do not edit by hand.
4
5
 
5
6
  | Key | Default | Description |
6
7
  |---|---|---|
@@ -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: 64 · sections: 15 · tools: 27 (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": 64,
18
+ "sections": 15,
19
+ "tools": 27,
20
+ "baselineDate": "2026-10-23",
21
+ "line": "config keys: 64 · sections: 15 · tools: 27"
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,14 @@ 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 the defined tool surface, independent of the operational
72
+ switches); `npm run surface:check` is what fails when a count grew without an ADR
73
+ or a matching deletion. See
74
+ [surface-budget.md](surface-budget.md) for the classification table.
75
+
43
76
  `stateSchema` describes the file **on disk**, not the in-memory document: `currentVersion` is the
44
77
  `schemaVersion` doctor read, `supportedVersion` is the build's `SCHEMA_VERSION`, and `migrations` is
45
78
  the ordered v→v path a write would apply (`[]` when current). `status` is one of `current`,
@@ -49,6 +82,19 @@ and keeps a `state.json.v<N>.bak` backup). A file written by a newer plugin repo
49
82
  `fail`: this build refuses to downgrade it. A state-load failure is reported as a failed check rather
50
83
  than thrown so doctor remains useful on a broken repository.
51
84
 
85
+ `stateLock` (and the `state-lock` check) reports the held lock's age (`ageMs`, from the record's
86
+ `since`) and heartbeat freshness (`heartbeatAgeMs`, from its last `at`) against the stale window
87
+ (`staleAfterMs = 3 × 1000 ms`). A live PID is not proof of a live writer: a holder that stopped
88
+ heartbeating is reported as `stale: true` and is reclaimable by the next mutation (or by
89
+ `tower_recover clear-stale-lock force:true`), while a fresh heartbeat is reported as held and is never
90
+ stolen. This is what makes a recycled PID diagnosable without reading `state.lock` by hand.
91
+
92
+ `gitLock` (and the `git-lock` check) reports the repo-scoped git mutex: `{held, pid, phase, heldMs}`
93
+ while a merge/rebase/init/teardown/provision holds it, and `{held: false}` when free. A merge may hold
94
+ it for the configured merge timeout (15 minutes by default), so a held lock is a `warn` rather than a
95
+ failure; the holder's phase and age make a stalled tower diagnosable. `tower_status` returns the same
96
+ `gitLock` object.
97
+
52
98
  `archive` reports the terminal-mission archive: active `missions`, `archivedMissions` and `stateBytes`,
53
99
  the archive directory's `archiveBytes`/`years`/`files`, every `unreadable` diagnostic, and the configured
54
100
  `policy` (`enabled`, `afterDays`, `maxMissions`). A missing archive directory is a healthy empty archive
@@ -68,8 +114,8 @@ never probes — a probe creates a temporary file, and a read-only call must not
68
114
  applies.
69
115
 
70
116
  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
117
+ config presence, worker/reviewer preset names, subagents and HMR services, state lock age/heartbeat,
118
+ workspace lock holder/staleness, live roster, worktree existence/ownership/dirtiness, test-discovery policy, legacy
73
119
  snapshots, batches and leases. Missions intentionally cleaned up by `tower_teardown` are skipped (the
74
120
  check notes the removed count) instead of being reported as missing.
75
121
  `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 | — |