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,130 @@
1
+ # 0005 — The component surface: mandatory host, switchable optional rows
2
+
3
+ - Date: 2026-10-23
4
+ - Status: Accepted
5
+ - Deciders: maintainer
6
+ - Supersedes: none
7
+
8
+ ## Context
9
+
10
+ The plugin had no durable statement of what may and may not be a switch. The
11
+ policy that #65 was resolved against lived only in the issue body, so when the
12
+ issue closed the policy evaporated: the 2.0.4 surface (four top-level rows:
13
+ `tower`, `tower-skills`, `preset-tower-worker`, `preset-tower-reviewer`) became
14
+ one all-or-nothing `tower-bundle` group row plus seven `tools.expose*` config
15
+ keys. The user-controllable component count went 4 → 1 while nothing recorded
16
+ that decision or refused the next one.
17
+
18
+ Two failure modes are already visible in the 2.3.0 tree:
19
+
20
+ 1. Optional capabilities have no component. Skills, the reviewer preset, the
21
+ batch/import/leases/test tools and hot reload can only be changed by editing
22
+ YAML under `tools:`, which is the discoverability problem components exist to
23
+ solve. A config key is still a switch, just a worse-labelled one.
24
+ 2. The safety nets are switchable. `tools.exposeOperational:false` removes all
25
+ seven operational tools including `tower_recover` and `tower_doctor`, so the
26
+ equipment that diagnoses and repairs a stale `state.lock` can itself be
27
+ turned off, while the plugin stays mounted. That off state is worse than
28
+ uninstalling the plugin, because uninstalling would remove the lock problem
29
+ too.
30
+
31
+ `PRINCIPLES.md` already says an enforcement pointer that does not resolve is a
32
+ slogan; by that standard the component policy was a slogan.
33
+
34
+ ## Decision
35
+
36
+ Adopt this shape and record it as principle 11:
37
+
38
+ - **The host is mandatory.** `tower` (and its worker preset dependency) is never
39
+ a switch, not as a row and not as a boolean that removes it.
40
+ - **Each optional capability is its own component row**, individually toggleable
41
+ from the plugin page, with `title`, `summary` and `disables` in both locales and
42
+ a documented fallback stating what still works while it is off. A capability
43
+ qualifies only when its off state leaves the rest of Tower intact and is not
44
+ worse than uninstalling.
45
+ - **The safety nets are never switches.** The health contract, the recovery
46
+ escape, the review contract and the merge gates are configuration or code; no
47
+ group, row or boolean key may gate them. Enforcement covers both routes: a
48
+ `cordis.patch.yml` row that defaults any key classified `forbidden-to-disable`
49
+ to `false` is refused, and every `tools.expose*` route in
50
+ `host/tool-registry.js` must match the `gateableTools` allowlist in the source
51
+ of truth, so a new route to a review, merge or host tool fails even when that
52
+ capability lists no tools of its own.
53
+ - **Config keys stay on the component axis.** Every config key is classified
54
+ `core`, `optional-component`, `optional-config` or `forbidden-to-disable` in
55
+ `docs/reference/component-surface.md`, generated from the single source of
56
+ truth `scripts/component-surface.json`, so "should this be a component?" is a
57
+ table lookup rather than a judgement call.
58
+ - **The component count is a baselined surface.** `scripts/surface-baseline.json`
59
+ gains `components` (seed 4, the 2.0.4 row count; value 1, today's group row) and
60
+ the 4 → 1 contraction is itself a dated decision. Unlike the other surfaces, the
61
+ component count is two-way: a contraction needs a decision record just like
62
+ growth does. `npm run surface:check` and `npm run docs:check:components` refuse a
63
+ count that moved without one. That is what makes the next contraction — or the
64
+ coming growth — visible in a diff.
65
+
66
+ This ADR records the policy. The individual instance fixes follow it: #121 turns
67
+ the qualifying capabilities into optional rows, and #122 deletes
68
+ `tools.exposeDoctor` / `tools.exposeRecovery`, removes the `exposeOperational`
69
+ master switch and the whole `tools` config group. While that migration was in
70
+ flight the two `tools.expose*` keys that gated the health and recovery contracts
71
+ were recorded as dated debt in `scripts/component-surface.json`, so a *new*
72
+ switch failed the gate immediately even before #122 landed.
73
+
74
+ ## Migration (2.4.0): the #65 policy is restored from the carrier
75
+
76
+ - Date: 2026-10-23
77
+ - Status: implemented by #121 and #122
78
+
79
+ The one `tower-bundle` group is gone. `cordis.patch.yml` now ships the mandatory
80
+ `tower` host anchor (the Host plugin and the worker preset are its internal
81
+ sections) plus the individually switchable top-level rows `tower-skills`,
82
+ `tower-batch`, `tower-import`, `tower-leases`, `tower-tests`, `tower-hmr`
83
+ (shipped off) and `preset-tower-reviewer` (conditional on
84
+ `review.requireReview`). The visible component count moved 1 → 8 and is recorded
85
+ as a dated decision in `scripts/surface-baseline.json`.
86
+
87
+ The capability switches became rows: the seven `tools.expose*` keys and
88
+ `dev.hotReload` were deleted from the schema and are now rejected as unknown
89
+ keys. `tower_doctor` and `tower_recover` are registered for every configuration
90
+ and have no switch; the `gateableTools` allowlist is empty. This is the
91
+ carrier's promised instance fix, not a new policy: #120 wrote the rule down, and
92
+ this wave makes the shipped surface obey it instead of carrying debt.
93
+
94
+ ## Consequences
95
+
96
+ - The user can control every capability that legitimately has an off state, from
97
+ the place components are controlled, instead of editing YAML.
98
+ - The host, the diagnostics and the lock escape are always registered; a failure
99
+ message may safely point at `tower_recover`.
100
+ - A component count change now needs a written decision, so the 2.3.0 4 → 1
101
+ contraction can never recur silently; the 1 → 8 restoration is recorded the
102
+ same way.
103
+ - The 2.3.0 debt is paid: `tools.exposeDoctor`, `tools.exposeRecovery` and the
104
+ `exposeOperational` master switch are gone, so no configuration can remove the
105
+ health contract or the lock escape. The `gateableTools` allowlist stays in the
106
+ source of truth, empty, so the next `tools.expose*` route is refused rather
107
+ than legalised by default.
108
+ - Adding a component costs a locale row in two languages and a fallback sentence
109
+ in the source of truth; that is the intended marginal cost.
110
+
111
+ ## Rejected alternatives
112
+
113
+ - **One all-or-nothing group row (the shipped 2.3.0 shape).** Simple, and it
114
+ removes the orphan-mount hazard, but it removes every user control as well:
115
+ the qualifying capabilities become unreachable from the plugin page. It also
116
+ makes the "off state" question vacuous, because the only off state is
117
+ "uninstalled".
118
+ - **Config-key switches instead of components.** `tools.expose*` and
119
+ `dev.hotReload` carry the same capabilities with none of the discoverability,
120
+ and they make safety nets disableable in a place where no label warns about it.
121
+ A config key is a switch with worse labelling, not an absence of a switch.
122
+ - **Per-tool components.** 29 rows is a worse surface than 29 tools, and it turns
123
+ every implementation detail of the tool registry into a user decision.
124
+ - **A denylist-only rule with no component count.** It would catch a forbidden
125
+ switch but not the 4 → 1 contraction, which is the failure that actually
126
+ happened; the count baseline is the part that keeps the policy alive across the
127
+ next refactor.
128
+ - **A GitHub issue label or a docs-only policy.** The brake must run offline in
129
+ `npm test` and in CI without a token, and the policy must survive the closure
130
+ of the issue that introduced it.
@@ -0,0 +1,52 @@
1
+ # Architecture decision records
2
+
3
+ An ADR is a dated record of a decision that is already load-bearing. It is not a
4
+ proposal: the status says whether the decision is accepted, and the rejected
5
+ alternatives exist so a future reader can see what was actually weighed.
6
+
7
+ The engineering principles that these records serve are in
8
+ [../PRINCIPLES.md](../PRINCIPLES.md). `npm run docs:check:philosophy` requires at
9
+ least three numbered ADRs, each with Context, Decision, Consequences, Rejected
10
+ alternatives, Date and Status.
11
+
12
+ | ADR | Decision | Date |
13
+ |---|---|---|
14
+ | [0001](0001-ports-adapters-as-data.md) | Ports and adapters are data, not a dependency-injection container | 2026-10-23 |
15
+ | [0002](0002-single-state-json.md) | One `state.json` is the mission persistence contract | 2026-10-23 |
16
+ | [0003](0003-allow-agent-control-default-true.md) | `allowAgentControl` defaults to `true` | 2026-10-23 |
17
+ | [0004](0004-surface-budget.md) | The concept surface is frozen by a committed baseline | 2026-10-23 |
18
+ | [0005](0005-component-surface.md) | Mandatory host plus individually toggleable optional component rows | 2026-10-23 |
19
+
20
+ ## Template
21
+
22
+ ```markdown
23
+ # NNNN — Decision title
24
+
25
+ - Date: YYYY-MM-DD
26
+ - Status: Proposed | Accepted | Superseded by NNNN
27
+ - Deciders: <who>
28
+
29
+ ## Context
30
+
31
+ What forced the decision.
32
+
33
+ ## Decision
34
+
35
+ What was decided, in one paragraph.
36
+
37
+ ## Consequences
38
+
39
+ What becomes easier, what becomes harder, and what is now refused.
40
+
41
+ ## Rejected alternatives
42
+
43
+ - **Alternative** — why it was not chosen.
44
+ ```
45
+
46
+ ## Non-goals
47
+
48
+ These records do not form an RFC queue, an architecture review board, or a
49
+ process for approving ordinary changes.
50
+
51
+ Unowned. This is a documentation convention, not a gate; nothing in CI blocks a
52
+ new ADR or rewards one.
@@ -0,0 +1,51 @@
1
+ # Execution workflow: Matt Pocock skills × Tower
2
+
3
+ This repository uses two workflows together. They are not alternatives: the **Matt Pocock skills** own thinking, planning, review discipline and retros; **Tower** owns delivery — isolated worktrees, scope fences, review gates, merges and durable mission state. **GitHub Issues** is the single source of truth for what is planned, in progress, blocked and shipped.
4
+
5
+ ## Who owns what
6
+
7
+ | Stage | Owner | Artifact |
8
+ |---|---|---|
9
+ | Intake | Matt Pocock `/triage` for raw external reports; milestones + `P*`/`area/*` labels for self-authored findings | issue with owner-facing state |
10
+ | Plan | `/to-spec` → `/to-tickets` (or an existing issue whose acceptance criteria are already ticket-shaped) | spec issue, tickets with native `blocked_by` edges |
11
+ | Execute | `/implement-spec` subagents for one-session waves; **Tower missions** for parallel or long-lived work | integration branch + PRs, or mission worktrees + merge gate |
12
+ | Build each unit | `/tdd` inside the ticket/mission | failing test first, then green |
13
+ | Verify | gates + required CI checks + `/code-review` (Standards + Spec) | green PR with two-axis review |
14
+ | Land | protected `main`, four-line PR body, `verified-by:`/`commit:` closure | merged PR, closed issues |
15
+ | Release | `release:check`, CHANGELOG, COMPATIBILITY, tag; publish only when planned | tag-only or published release |
16
+ | Learn | `/retro`; mechanical findings become checks, judgement becomes standards | CONTRIBUTING/AGENTS/scanner updates |
17
+
18
+ ## The flow
19
+
20
+ 1. **Intake.** Raw external bug reports and feature requests go through `/triage` (they are not ours). Findings we file ourselves (audits, retros) skip triage and get a priority, an area and a milestone; they are already acceptance-criteria-shaped.
21
+ 2. **Plan.** Multi-session work goes `/to-spec` → `/to-tickets`. Tickets are tracer bullets with native blocking edges; the frontier is whatever has no open blockers. Small work may skip the spec when the issue already states acceptance criteria (e.g. a patch ticket).
22
+ 3. **Choose the orchestrator.**
23
+ - **One-session wave (≤3 tickets, mostly independent):** `/implement-spec`-style: an integration branch, one implementer subagent per ticket in its own worktree, merger merges to the integration branch, one PR to `main`.
24
+ - **Parallel or long-lived wave (architecture epics, >3 tickets, review needed):** **Tower**. `tower_init` → `tower_plan` with one mission per ticket, `scope` = the ticket's write fence, `deps` = the ticket's blocking edges → `tower_spawn` a worker per mission → optional reviewer (`tower-reviewer`) → `tower_merge` (gate checks scope, branch freshness, review, tasks) → `tower_teardown`. Tower's persistent state makes the wave resumable after a restart.
25
+ - Never run both orchestrators over the same files at once.
26
+ 4. **Build.** Each unit starts with `/tdd`: one failing test, then the implementation, then the next slice. Commit messages carry `Refs #n`; the completing commit uses `Fixes #n`. Workers do not merge; they hand off a branch.
27
+ 5. **Verify before landing.** Run `npm test`, `npm run docs:check`, all five `docs:check:generated*` (including the generated type vocabulary), `npm run pack:check`, `npm run architecture:check`, `npm run types:coverage`, `npm run types:strict:check`, `npm run types:check` (both tsconfigs), `npm run domain:check`. Bundle changes also run `npm run smoke:mount`. CI must be green on the required checks (`test (22)`, `test (24)`, `test (22, windows)`, `mount`). Then `/code-review` runs both axes on the diff (Standards against CONTRIBUTING/architecture docs, Spec against the ticket); every finding is fixed in the same PR or explicitly deferred with a reason.
28
+ 6. **Land.** PR body follows the four-line contract (failure mode → change → test that fails without it → blast radius). `main` is protected: PR + required checks, admins included, no force-push. A closure comment carries `verified-by: <test file>` and `commit: <sha>`; `npm run audit:issues` flags closures whose evidence is missing.
29
+ 7. **Release.** `release:check` compares the version, the previous tag and the CHANGELOG; a tag push fails when the version did not move or the tag does not match. Releases are either **tag-only** (version + changelog + compatibility row + annotated tag; no npm publish, no GitHub Release) or **published** (adds the human `npm publish --tag latest` with 2FA, `dist-tags` verification, then the GitHub Release). The mode is recorded in the milestone/plan.
30
+ 8. **Learn.** After a release (or a wave that went sideways) run `/retro`: mechanical mistakes become deterministic checks (scripts, CI jobs, preflight), judgement calls become written standards. Periodically run `/improve-codebase-architecture` for deepening opportunities and `/domain-modeling` to keep the vocabulary and ADRs (2.3.0) honest.
31
+
32
+ ## Decision table
33
+
34
+ | Situation | Flow |
35
+ |---|---|
36
+ | Raw external issue | `/triage` → then plan |
37
+ | Design question you cannot settle on paper | `/grill-with-docs`, detour through `/prototype` if it needs a runnable answer |
38
+ | Multi-session feature | `/to-spec` → `/to-tickets` → `/implement-spec` |
39
+ | Small fix with acceptance criteria | ticket → `/implement` (+ `/tdd`) |
40
+ | Architecture epic / parallel missions | Tower plan → missions → review gate → merge |
41
+ | Something broken, cause unknown | `/diagnosing-bugs` (tight red loop first) |
42
+ | Repeated mistakes | `/retro` → checks/standards |
43
+ | Release | release plan: tag-only or publish |
44
+
45
+ ## Invariants
46
+
47
+ - One ticket per PR series; a branch never touches two tickets.
48
+ - Tracked blocking edges, not prose: work only the frontier.
49
+ - Tower missions: `scope` is the write fence, a mission that outgrows it is re-scoped, not silently widened; the workspace lock keeps one Tower session per repository.
50
+ - Human-only gates: npm publish/2FA, branch-protection changes, anything behind a third-party credential. Stop and hand over rather than inventing a credential path.
51
+ - Context hygiene: keep planning in one window until tickets exist; start each implementation unit from the ticket, not from the previous unit's context.
@@ -2,36 +2,71 @@
2
2
 
3
3
  ## Components
4
4
 
5
- The bundle is all-or-nothing: `cordis.patch.yml` inserts one `tower-bundle` `cordis:group` row whose
6
- internal sections are the Host plugin, the skills provider, and the worker/reviewer presets. The
7
- group is the only user-facing component; disabling it disables every section together, so no preset
8
- or skill can remain mounted while the `tower_*` tools are gone.
5
+ Since #121 the bundle ships a mandatory host anchor plus individually switchable
6
+ optional rows: `cordis.patch.yml` declares top-level `tower` (a `cordis:group`
7
+ that mounts the Host plugin and the mandatory worker preset), then
8
+ `tower-skills`, `tower-batch`, `tower-import`, `tower-leases`, `tower-tests`,
9
+ `tower-hmr` (shipped off) and `preset-tower-reviewer`. Turning one optional row
10
+ off removes exactly that capability; the host, the diagnostics and the recovery
11
+ escape keep working.
12
+
13
+ [Principle 11](../../PRINCIPLES.md) and
14
+ [ADR 0005](../adr/0005-component-surface.md) say a component may exist only for
15
+ a capability whose off state leaves the rest intact, and that the host, health,
16
+ recovery, review and merge-gate capabilities are never switches. Which keys are
17
+ allowed to be switches is a table lookup in
18
+ [component-surface.md](../reference/component-surface.md), generated from
19
+ `scripts/component-surface.json`; the component count is baselined and checked by
20
+ `npm run docs:check:components`. That gate also refuses a `tools.expose*` route
21
+ to a tool that is not on the recorded allowlist (empty since #122) and any
22
+ `cordis.patch.yml` row that defaults a forbidden key off.
9
23
 
10
24
  ```
11
25
  Host plugin (index.js)
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
14
- │ (+ 27 registered repository tools)
15
- │ (+ tower_reload while dev.hotReload is on; fails reload-unavailable without HMR)
26
+ ├─ /tower command + Tower mode (guard, prompt section, tower_mode/tower_control)
27
+ ├─ host/tool-registry.js -> registration aggregator, actor guards, shared helpers
28
+ │ (+ 21 define()-registered host tools)
29
+ │ (+ tower_mode/tower_control registered by the Host entry, 29 total
30
+ │ once every optional capability row is on)
31
+ │ (tower_doctor/tower_recover are host tools, never switches)
32
+ ├─ host/capability-*.js -> optional capability entry points (batch/import/leases/tests/hmr)
16
33
  ├─ tools/index.js -> thin public wiring (re-exports the registry + briefings)
17
34
  └─ app/tower.js -> orchestration surface; delegates to use-case modules
18
35
  ├─ app/init.js, provision.js, merge.js, rebase.js, teardown.js
19
36
  ├─ app/review.js, findings.js, batch.js, messages.js, roster.js
20
37
  ├─ 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
38
+ ├─ app/spawn-prechecks.js, spawn-support.js, briefing.js
39
+ └─ app/archive.js, gate.js, read.js, board.js, missionContext.js, enrichedMissions.js
40
+ host (composition root + process/session glue; the only layer that may hold ambient state):
41
+ host/tool-registry.js registration aggregator + shared guards/helpers
42
+ host/config.js profile/repo config layers over the pure schema
43
+ host/doctor.js read-only environment contract and health checks
44
+ host/capability-registry.js mounted-component report for status/doctor
45
+ host/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
46
+ host/modeStore.js per-agent Tower mode persistence
47
+ host/modeState.js owned in-memory mode state
48
+ host/presets.js cross-field preset validation before work starts
49
+ host/compatibility.js COMPATIBILITY.md provenance
50
+ host/tower.js default-deps Tower binding for tests/embedders
51
+ ports (explicit runtime contracts, no implementation):
52
+ ports/contract.js definePort/assertPort runtime contract primitives
53
+ ports/{store,vcs,clock,ids,config,state,ownership,caseSensitivity,archive}.js
54
+ ports/index.js PORTS table + assertPorts(deps) injection check
55
+ adapters (the only I/O; they implement the ports):
56
+ adapters/store-fs.js the only module importing node:fs; createStorePort()
57
+ adapters/vcs-git.js the only module importing node:child_process; createVcsPort()
58
+ adapters/git.js bounded git commands + timeout classification
59
+ adapters/store.js repo-scoped lock + atomic JSON + activity.jsonl
60
+ adapters/persistence.js shared atomic write / lock / heartbeat primitives
61
+ adapters/archive.js terminal-mission archive files
62
+ adapters/leases.js repo-scoped advisory lease ledger
63
+ adapters/workspaceLock.js optional one-Tower-session-per-repository lock
64
+ adapters/worktreeOwnership.js worktree identity verification
65
+ adapters/stateFile.js state-file load/migrate/persist
66
+ adapters/caseSensitivity.js filesystem case-folding probe
67
+ adapters/legacy.js read-only legacy snapshot validation + storage
68
+ adapters/tests.js test discovery execution + TAP/JSON parsing
69
+ adapters/default.js createDefaultDeps(): the one default port implementation set
35
70
  pure domain modules (no fs/git):
36
71
  domain/errors.js stable, structural tool error catalog
37
72
  domain/batch.js batch admission create/close/summary validation
@@ -48,7 +83,6 @@ Host plugin (index.js)
48
83
  domain/report.js board rendering
49
84
  domain/roster.js roster predicates
50
85
  tools layer:
51
- lib/tool-registry.js registration aggregator + shared guards/helpers
52
86
  tools/index.js thin public wiring (re-exports only)
53
87
  tools/*.js one module per tool, handler + imported input schema
54
88
  Subagents
@@ -61,6 +95,54 @@ The main checkout is the control-tower repo; each mission has a branch `tower/<i
61
95
  worktree in `.dsh-tower-worktrees/<hash>/<missionId>`. The plugin never operates on the main checkout's
62
96
  working tree except merge and the read-only diff.
63
97
 
98
+ ## Ports and the composition root (#69)
99
+
100
+ The seam is real, not documented-only. `ports/` exports **runtime contracts**: each module defines a
101
+ frozen `{ name, methods }` descriptor plus an `assert*Port` helper, and `ports/index.js` exposes the
102
+ `PORTS` table and `assertPorts(deps)`.
103
+
104
+ `app/**` imports only app siblings, `domain/**`, `ports/**` and `node:path`. Every effectful call goes
105
+ through the `deps` object:
106
+
107
+ | deps member | contract | default implementation |
108
+ |---|---|---|
109
+ | `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()` |
110
+ | `vcs` | VcsPort: `git/tryGit/revParse/isClean/isAncestor/resolveRepo/currentBranch/countCommits/listWorktrees` | `adapters/vcs-git.js` `createVcsPort()` |
111
+ | `clock`, `ids` | ClockPort / IdsPort | `adapters/default.js` |
112
+ | `config` | ConfigPort: `runtimeConfig/resolveRepoConfig` | `host/config.js` |
113
+ | `state` | StatePort: `loadStateFile` | `adapters/stateFile.js` |
114
+ | `archive` | ArchivePort: `readIndex/readEntry/writeEntries` | `adapters/archive.js` |
115
+ | `ownership` | OwnershipPort: `assertOwnership/canonicalPath` | `adapters/worktreeOwnership.js` |
116
+ | `caseSensitivity` | CaseSensitivityPort: `detect` | `adapters/caseSensitivity.js` |
117
+
118
+ `index.js` is the composition root: it calls `createDefaultDeps()` once and passes the object to
119
+ `registerTowerTools`. The registry adopts and validates that object (`assertPorts`) instead of building
120
+ its own adapter set, hands every tool an `openTower(repoRoot)` factory bound to those ports, and
121
+ `Tower.open(repoRoot, deps)` / `new Tower(repo, config, deps)` validates the object again before use.
122
+ `app/tower.js` itself imports no adapter and no `host/` module. The one piece of lock policy the app and
123
+ the store share (the git-mutex budget, #93) is pure and lives in `domain/lockBudget.js`;
124
+ `adapters/store.js` re-exports it for the infrastructure callers.
125
+
126
+ `store.rename` and `store.renameWithRetry` are both declared because they are not interchangeable: the
127
+ retrying helper has temp-file semantics and removes its source when the rename finally fails, which is
128
+ right for a disposable temp file but would destroy the lock bytes `tower_recover` must keep auditable.
129
+ The audit rename therefore uses the plain `rename`. The contract test scans `app/**` **and** `tools/**`
130
+ for every `deps.<port>.<method>` call and fails if the named port does not declare it, so a tool that
131
+ reaches a new port method cannot slip past `assertPorts`.
132
+
133
+ `lib/` no longer exists (#70): every module was `git mv`-ed into the layer that owns it. The I/O
134
+ implementations now sit in `adapters/` (`store`, `persistence`, `git`, `archive`, `stateFile`,
135
+ `leases`, `workspaceLock`, `worktreeOwnership`, `caseSensitivity`, `legacy`, `tests`), the
136
+ composition root and process/session glue in `host/`, and spawn policy in `app/`. `host/**` is the
137
+ only source root, besides `adapters/**` itself, allowed to import an adapter; `app/**` and `domain/**`
138
+ may not import `lib/**` (rule `no-lib-from-core`) and may not hold ambient module state
139
+ (`host-scope`). Tools receive effectful helpers from the registry instead of importing an adapter.
140
+
141
+ A full mission lifecycle (init → plan → provision → spawn → complete → review → gate → merge →
142
+ teardown) runs through the real app use cases on an in-memory store and a scripted vcs:
143
+ `tests/ports-lifecycle.test.mjs`. The default adapter set and the in-memory doubles are both asserted
144
+ against the same port contracts in `tests/memory-adapters.contract.test.mjs`.
145
+
64
146
  ## Mission state machine
65
147
 
66
148
  ```
@@ -82,7 +164,7 @@ set by `tower_merge`/reconciliation.
82
164
  against the merge base.
83
165
  3. Survey missions must not change files; build missions must stay inside `scope`.
84
166
  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.
167
+ 5. Dependency missions must be merged/abandoned, except a completed survey dependency which is its terminal success state (#107).
86
168
  6. If `review.requireReview:true` (default false), a review must exist, belong to this mission, be
87
169
  clean, approve merge, and be bound to the tip (unless a clean rebase waived it). No worker may be
88
170
  running.
@@ -105,7 +187,7 @@ After the command, regardless of exit status, it inspects `HEAD`:
105
187
  | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
106
188
  | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
107
189
  | `<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) |
190
+ | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (heartbeat reclaim; a recycled PID cannot pin it) |
109
191
  | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
110
192
  | `<git-common-dir>/dsh-tower/lock-holds.jsonl` | state/git lock-hold telemetry |
111
193
  | `<checkout parent>/.dsh-tower-worktrees/<hash>/<missionId>` | mission worktree |
@@ -113,8 +195,10 @@ After the command, regardless of exit status, it inspects `HEAD`:
113
195
  | `$DSH_HOME/tower-mode/modes.lock` | mode-store single-writer lock |
114
196
 
115
197
  `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.
198
+ heartbeat-based reclamation (a live PID with a silent heartbeat is reclaimed after byte verification;
199
+ a live holder that keeps heartbeating is never stolen), and bounded lock waits. `state.json` is the
200
+ mission persistence contract; `modes.json` is the per-agent mode contract; neither is written without
201
+ its own lock.
118
202
 
119
203
  ## Layering rules
120
204
 
@@ -123,17 +207,66 @@ dead-PID reclamation, and bounded lock waits. `state.json` is the mission persis
123
207
  - `domain/**` must not import I/O or adapters (`domain-pure`).
124
208
  - Only `adapters/vcs-git.js` may import `node:child_process`, and only
125
209
  `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/`,
210
+ The rule is enforced over the source roots — `host/`, `app/`, `domain/`, `tools/`,
127
211
  `adapters/`, `ports/` — and does not cover `scripts/**`, which holds maintenance
128
212
  entry points (release check, mount smoke, docs generators) that may spawn git or npm.
213
+ The optional Web client (`ui-bundle/`) is its own tier with its own rules.
129
214
  - `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.
215
+ `tools/index.js` is the thin public wiring module (it imports `host/tool-registry.js`, not a
216
+ `node:` module), while registration/shared machinery lives in `host/tool-registry.js`.
217
+ Tools receive effectful helpers (repository resolution, leases, recovery, legacy import) from
218
+ the registry instead of importing an adapter.
219
+ - Aggregate files have size ceilings; top-level mutable state is limited to `host/**`
220
+ (`aggregate-size`, `host-scope`). The per-tool ceiling is 200 lines; `tools/index.js`
221
+ currently sits inside the 40-200 range, so the tools allow-list is empty rather than silently
222
+ exempting the wiring module.
223
+ - `app/**` may import only app siblings, `domain/**`, `ports/**` and `node:path` (`app-ports-only`);
224
+ an app module that reaches an adapter, a `host/` module or `lib/` fails the gate.
225
+ - `app/**` and `domain/**` may not import the retired `lib/` layer (`no-lib-from-core`); with the
226
+ layer gone this keeps it from creeping back.
227
+ - `host/**` is the only source root that may hold ambient module state (`host-scope`);
228
+ `app/**` and `domain/**` must stay stateless at module scope. Only `host/**` and `adapters/**`
229
+ may import an adapter (`composition-root`); `app/`, `tools/`, `domain/` and `ports/` never may.
230
+ - Every `ports/**` module must export a runtime contract (`const`/`function`/`class`) and must not
231
+ import an implementation (`port-shape`); a typedef plus `export {}` is no longer accepted.
232
+ - `ui-bundle/**` is a browser tier, not host code (#71): no `node:*` import
233
+ (`client-no-node` — DOM globals are fine; host capabilities must come through the
234
+ plugin's registration surface), every client file stays at or below 800 lines
235
+ (`client-size`), and module-level mutable state is banned exactly as it is for
236
+ `app/**` (`client-ambient`); the split surfaces keep their state inside the
237
+ `create*` closure the entry composes. `ui-bundle/` is also excluded from
238
+ `aggregate-size`, which it would otherwise duplicate.
239
+
240
+ ### Exemptions are justified data
241
+
242
+ The gate's only exemptions live in `scripts/architecture-waivers.json`; `scripts/architecture.mjs`
243
+ keeps no allow-lists of its own. Since #70 the file has exactly two entries, both I/O adapter
244
+ boundaries (`adapters/vcs-git.js` for `node:child_process` and `adapters/store-fs.js` for
245
+ `node:fs`). Each entry names the `rule` and `path` and must carry a non-empty `reason` plus an
246
+ `owner` or `ref`. An entry missing any of those is itself a blocking `waiver-contract` violation
247
+ that names the rule and the path, so a bare path cannot turn the gate green. The waiver file is a
248
+ two-key path in `CONTRIBUTING.md`; there is no `reviewBy`/expiry field in this release, so the
249
+ reason and the owner are the reviewed decision.
250
+
251
+ `host-scope` replaces the old `ambient-state` waivers (#70). It scans `app/**` and `domain/**` and
252
+ records every module-level `let`; `host/**` owns the process/session state and needs no waiver. The
253
+ scan is statement-aware rather than line-based: it walks the module's token stream and records every
254
+ binding of every top-level `let` declaration. A second declarator (`let a = 1, b = 2`), a second
255
+ statement on one line (`let a = 1; let b = 2`), and any new top-level `let` in `app/` or `domain/`
256
+ are each their own violation. Strings, comments, template literals and regex literals are skipped,
257
+ and `let` inside a function, block, class or `for (…)` header is not module-level state.
135
258
 
136
- A fixture test seeds one violation per rule, so a rule that cannot fail is itself a test failure.
259
+ `domain-pure`, `host-scope`, `no-lib-from-core`, and anything under `domain/**` are load-bearing: no
260
+ waiver may target them, even a justified one. A `waiver-contract` violation rejects that entry and
261
+ the underlying rule still fails.
262
+
263
+ A green run prints `architecture: ok (waivers: N)`; N is the number of entries that actually
264
+ suppressed a violation, so a stale waiver is visible in the output and in the tests. The fixture
265
+ suite seeds an unjustified waiver (it fails), a justified one (it passes), a `host-scope` fixture
266
+ that puts ambient state in `host/` (allowed) and in `app/`/`domain/` (rejected), a `no-lib-from-core`
267
+ import from `app/` (rejected), a same-line second binding (rejected), and a new violation inside
268
+ each rule. A separate fixture seeds one violation per rule, so a rule that cannot fail is itself a
269
+ test failure.
137
270
 
138
271
  ## Mount smoke and host internals
139
272
 
@@ -149,24 +282,40 @@ probe adjustments in the same change.
149
282
 
150
283
  All epic DoD items now have an executable gate:
151
284
 
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;
285
+ - Pure domain share: **39.5%** (7,475 / 18,936 host source lines), enforced by
286
+ `npm run domain:check` (threshold 35%). `npm run domain:metrics` also prints a
287
+ separate `ui-bundle` line, so Web-client growth is visible in CI output while the
288
+ share itself still measures the Host architecture.
289
+ - `domain/**` imports no fs, child_process, adapters, app, tools, or host infrastructure;
155
290
  `domain-pure` is a blocking architecture rule.
156
291
  - 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
292
+ `adapters/store-fs.js` imports `node:fs`; both are blocking rules whose files are the only
293
+ `child-process-surface`/`fs-surface` entries in `scripts/architecture-waivers.json`.
294
+ - `host`, `app`, and `domain` files stay <= 800 lines; the same ceiling applies to
295
+ `ui-bundle/**` (`client-size`); every `tools/**` file stays
159
296
  40-200 lines (one handler module per tool; `tools/index.js` is 40-line wiring).
160
297
  - No `withLock` scope starts a process; the two-phase tests instrument every git call.
161
298
  - `tools/**` has no status/type/enum literal; vocabulary is owned by
162
299
  `domain/vocabulary.js`; the `tools-vocabulary` rule is blocking.
163
300
  - `validateState` has a four-line body over `domain/state/schema.js` and
164
301
  `domain/state/semantics.js`.
165
- - `state.json` and `modes.json` share `lib/persistence.js` atomic write, token-owned
302
+ - `state.json` and `modes.json` share `adapters/persistence.js` atomic write, token-owned
166
303
  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.
304
+ - The in-memory lifecycle runs the real app use cases (init → plan → provision → spawn → complete →
305
+ review → gate → merge → teardown) on an in-memory store and scripted vcs supplied through the
306
+ composition root, with no disk, no git and no `defaultDeps` (#69).
307
+ - The eleven host fitness rules are errors, each with a seeded failing fixture; the
308
+ three `client-*` rules form the Web-client tier with one fixture each, and the
309
+ `waiver-contract` rule makes an unjustified exemption blocking as well.
310
+
311
+ ## Non-goals
312
+
313
+ This layer map is not a dependency-injection container, a service locator, or a
314
+ place to register classes by reflection. Ports stay runtime contracts and the
315
+ composition root stays the only place that assembles implementations.
316
+
317
+ Enforced by: `scripts/architecture.mjs` and `ports/index.js`; tests in
318
+ `tests/architecture.test.mjs`; gate: `npm run architecture:check`.
170
319
 
171
320
 
172
321
 
@@ -26,8 +26,8 @@ This page covers the development loop. It is not required to use Tower.
26
26
  `plugin_manager`); it compares already-loaded modules with disk and replaces the changed ones even
27
27
  with `root: []`. Watching only makes that happen automatically after a save.
28
28
  - HMR is only mounted for profile boots (`disabled` when there is no `profileContext`). A process
29
- booted without a profile reports reload as unavailable (`tower_reload` stays registered while
30
- `dev.hotReload` is on, so the tool returns `reload-unavailable` instead of disappearing).
29
+ booted without a profile reports reload as unavailable (`tower_reload` stays registered while the
30
+ `tower-hmr` component row is on, so the tool returns `reload-unavailable` instead of disappearing).
31
31
  - The profile patch layer is loaded when DSH boots. Editing it — in practice the hmr `root` list —
32
32
  takes effect after **one DSH restart**. Nothing in the running process re-reads the HMR row's own
33
33
  configuration.
@@ -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.
@@ -99,9 +99,9 @@ The bridge behind the tool follows the `dev` config group:
99
99
 
100
100
  | Config key | Meaning |
101
101
  |---|---|
102
- | `dev.hotReload` | Register `tower_reload`; `/tower reload` is disabled when off (default `true`). Without the HMR service the tool is still registered so it can return `reload-unavailable`. |
102
+ | `tower-hmr` (component row) | The switch: on registers `tower_reload` and `/tower reload`, off reports "disabled by component tower-hmr". The row ships off; enable it from the plugin page for a linked dev profile. `dev.hotReload` was removed in 2.4.0 and is now an unknown key. |
103
103
  | `dev.reloadDelayMs` | Delay before the scheduled reconciliation runs, in milliseconds (default `50`), so the tool/command result reaches the model before the plugin is replaced. |
104
- | `dev.reloadRequiredIds` | Loader entry ids the profile must keep active (default `tower-bundle`, `tower`, `tower-skills`, `preset-tower-worker`, `preset-tower-reviewer`). An inactive required entry is reported as a reload failure even if it was already inactive. |
104
+ | `dev.reloadRequiredIds` | Loader entry ids the profile must keep active (default `tower`, `tower-host`, `preset-tower-worker`). An inactive required entry is reported as a reload failure even if it was already inactive. Optional capability rows are deliberately not required, so a row switched off is not a reload failure. |
105
105
 
106
106
  A reload is **scheduled, not awaited**: `/tower reload` and `tower_reload` return immediately with
107
107
  `scheduled`, `delayMs`, `requiredIds` (and the current `lastError` when one is pending), then HMR
@@ -45,6 +45,19 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
45
45
  overlapping and case-only mismatches are no longer refused by the out-of-scope gate. Stored scope
46
46
  strings are never rewritten: the board keeps showing what the operator typed.
47
47
 
48
+ - **2.3.0 → 2.4.0 (unreleased)**: the single `tower-bundle` group row is gone. `cordis.patch.yml`
49
+ now ships the mandatory `tower` host anchor (the Host plugin and the worker preset are its internal
50
+ sections) plus individually switchable top-level rows: `tower-skills`, `tower-batch`, `tower-import`,
51
+ `tower-leases`, `tower-tests`, `tower-hmr` (shipped off) and `preset-tower-reviewer` (conditional on
52
+ `review.requireReview`). The `tools` config group and `dev.hotReload` were removed: a profile that
53
+ still carries them is now rejected as an unknown key (`config-unknown-key`) and the host stops
54
+ mounting until they are deleted, so no silent no-op switch remains. Move each capability to its row
55
+ in the plugin page instead; `tower-hmr` ships off, so `tower_reload` and `/tower reload` are reported
56
+ as "disabled by component tower-hmr" until you enable it for a linked dev profile. `tower_doctor` and
57
+ `tower_recover` are registered for every configuration and need no key. The reviewer row may be off
58
+ only while `review.requireReview` is false; with the gate on, planning and mode entry refuse the
59
+ combination with an explanatory error. No state migration is needed.
60
+
48
61
  - **2.0.4 → 2.1.0-beta.1**: no config migration; the state file migrates automatically (v1/v2 → v3) as
49
62
  described above. This is a **pre-release** published on the
50
63
  `next` dist-tag, so `latest` still resolves to 2.0.4 — install it with
@@ -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).