dsh-tower 2.0.4 → 2.1.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 (184) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/COMPATIBILITY.md +39 -0
  3. package/CONTRIBUTING.md +157 -2
  4. package/README.en.md +46 -3
  5. package/README.md +44 -3
  6. package/SECURITY.md +27 -0
  7. package/adapters/default.js +12 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/archive.js +190 -0
  11. package/app/batch.js +79 -0
  12. package/app/board.js +27 -0
  13. package/app/checkCompletion.js +89 -0
  14. package/app/enrichedMissions.js +26 -0
  15. package/app/findings.js +78 -0
  16. package/app/gate.js +126 -0
  17. package/app/init.js +291 -0
  18. package/app/merge.js +573 -0
  19. package/app/messages.js +66 -0
  20. package/app/mission.js +37 -0
  21. package/app/missionContext.js +14 -0
  22. package/app/plan.js +50 -0
  23. package/app/provision.js +193 -0
  24. package/app/read.js +113 -0
  25. package/app/rebase.js +302 -0
  26. package/app/recording.js +72 -0
  27. package/app/review.js +147 -0
  28. package/app/roster.js +123 -0
  29. package/app/spawn.js +125 -0
  30. package/app/teardown.js +205 -0
  31. package/app/tower.js +552 -0
  32. package/cordis.patch.yml +61 -53
  33. package/docs/README.md +3 -1
  34. package/docs/agents/domain.md +38 -0
  35. package/docs/agents/issue-tracker.md +46 -0
  36. package/docs/agents/triage-labels.md +15 -0
  37. package/docs/architecture/overview.md +114 -13
  38. package/docs/board.md +72 -0
  39. package/docs/guide/hot-reload.md +1 -1
  40. package/docs/guide/install-upgrade-migration.md +25 -1
  41. package/docs/guide/release.md +125 -0
  42. package/docs/operations/security.md +47 -2
  43. package/docs/reference/blockers.md +1 -1
  44. package/docs/reference/config.generated.md +70 -0
  45. package/docs/reference/config.md +8 -1
  46. package/docs/reference/doctor.md +58 -8
  47. package/docs/reference/errors.generated.md +41 -0
  48. package/docs/reference/errors.md +7 -3
  49. package/docs/reference/tools.generated.md +33 -0
  50. package/docs/reference/tools.md +7 -6
  51. package/docs/zh/README.md +3 -2
  52. package/docs/zh/guide/hot-reload.md +1 -1
  53. package/docs/zh/guide/install-upgrade-migration.md +18 -1
  54. package/docs/zh/operations/security.md +30 -1
  55. package/docs/zh/reference/blockers.md +1 -1
  56. package/docs/zh/reference/config.md +8 -1
  57. package/docs/zh/reference/doctor.md +35 -2
  58. package/docs/zh/reference/errors.md +6 -2
  59. package/docs/zh/reference/tools.md +3 -3
  60. package/domain/collab/batch.js +112 -0
  61. package/domain/collab/messages.js +127 -0
  62. package/domain/collab/roster.js +158 -0
  63. package/domain/collab/spawn.js +188 -0
  64. package/domain/config/schema.js +427 -0
  65. package/domain/contracts/outputs.js +305 -0
  66. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  67. package/domain/contracts/tool-inputs/findings.js +29 -0
  68. package/domain/contracts/tool-inputs/index.js +94 -0
  69. package/domain/contracts/tool-inputs/integration.js +37 -0
  70. package/domain/contracts/tool-inputs/missions.js +95 -0
  71. package/domain/contracts/tool-inputs/ops.js +75 -0
  72. package/domain/contracts/tool-inputs/shared.js +25 -0
  73. package/domain/errors.js +98 -0
  74. package/domain/gate.js +125 -0
  75. package/domain/integration/gate.js +190 -0
  76. package/domain/integration/init.js +261 -0
  77. package/domain/integration/liveness.js +31 -0
  78. package/domain/integration/merge.js +377 -0
  79. package/domain/integration/provision.js +124 -0
  80. package/domain/integration/rebase.js +236 -0
  81. package/domain/integration/review.js +52 -0
  82. package/domain/integration/teardown.js +118 -0
  83. package/domain/mission.js +68 -0
  84. package/domain/missions/completion.js +167 -0
  85. package/domain/missions/findings.js +201 -0
  86. package/domain/missions/lifecycle.js +186 -0
  87. package/domain/missions/plan.js +149 -0
  88. package/domain/missions/review.js +179 -0
  89. package/domain/planning.js +121 -0
  90. package/domain/read/enrichedMissions.js +58 -0
  91. package/domain/read/missionContext.js +53 -0
  92. package/domain/read/views.js +141 -0
  93. package/domain/report.js +67 -0
  94. package/domain/roster.js +40 -0
  95. package/domain/scope.js +177 -0
  96. package/domain/state/archive.js +257 -0
  97. package/domain/state/migrations.js +145 -0
  98. package/domain/state/schema.js +239 -0
  99. package/domain/state/semantics.js +117 -0
  100. package/domain/state.js +183 -0
  101. package/domain/summary.js +116 -0
  102. package/domain/vocabulary.js +179 -0
  103. package/index.js +63 -20
  104. package/lib/archive.js +279 -0
  105. package/lib/briefing.js +176 -0
  106. package/lib/caseSensitivity.js +66 -0
  107. package/lib/compatibility.js +108 -0
  108. package/lib/config.js +33 -375
  109. package/lib/doctor.js +275 -4
  110. package/lib/git.js +60 -16
  111. package/lib/hmr.js +4 -3
  112. package/lib/leases.js +17 -7
  113. package/lib/legacy.js +39 -14
  114. package/lib/modeState.js +39 -0
  115. package/lib/modeStore.js +11 -43
  116. package/lib/persistence.js +356 -0
  117. package/lib/presets.js +35 -0
  118. package/lib/spawn-prechecks.js +168 -0
  119. package/lib/spawn-support.js +79 -0
  120. package/lib/stateFile.js +99 -0
  121. package/lib/store.js +67 -135
  122. package/lib/tests.js +17 -12
  123. package/lib/tool-registry.js +544 -0
  124. package/lib/workspaceLock.js +2 -2
  125. package/lib/worktreeOwnership.js +22 -10
  126. package/locale/en.json +23 -1
  127. package/locale/zh.json +22 -0
  128. package/package.json +32 -4
  129. package/ports/clock.js +6 -0
  130. package/ports/ids.js +7 -0
  131. package/ports/index.js +2 -0
  132. package/ports/store.js +11 -0
  133. package/ports/vcs.js +11 -0
  134. package/scripts/architecture.mjs +94 -0
  135. package/scripts/check-docs.mjs +9 -3
  136. package/scripts/check-pack.mjs +49 -0
  137. package/scripts/domain-metrics.mjs +48 -0
  138. package/scripts/generate-config-reference.mjs +46 -0
  139. package/scripts/generate-error-reference.mjs +29 -0
  140. package/scripts/generate-tools-reference.mjs +56 -0
  141. package/scripts/issue-audit.mjs +259 -0
  142. package/scripts/lib/cli.mjs +76 -0
  143. package/scripts/lib/tmp.mjs +20 -0
  144. package/scripts/mount-smoke/fixtures.mjs +162 -0
  145. package/scripts/mount-smoke/host.json +4 -0
  146. package/scripts/mount-smoke/probe.mjs +125 -0
  147. package/scripts/mount-smoke/surface.mjs +141 -0
  148. package/scripts/mount-smoke.mjs +270 -0
  149. package/scripts/pack-manifest.txt +212 -0
  150. package/scripts/release-check.mjs +303 -0
  151. package/tools/adopt.js +72 -0
  152. package/tools/batch.js +78 -0
  153. package/tools/complete.js +53 -0
  154. package/tools/diff.js +39 -0
  155. package/tools/doctor.js +50 -0
  156. package/tools/finding.js +40 -0
  157. package/tools/findings.js +39 -0
  158. package/tools/import-legacy.js +42 -0
  159. package/tools/inbox.js +39 -0
  160. package/tools/index.js +39 -0
  161. package/tools/init.js +63 -0
  162. package/tools/interrupt.js +42 -0
  163. package/tools/legacy-map.js +44 -0
  164. package/tools/merge.js +39 -0
  165. package/tools/mission.js +41 -0
  166. package/tools/plan.js +39 -0
  167. package/tools/preflight.js +45 -0
  168. package/tools/rebase.js +39 -0
  169. package/tools/recover.js +45 -0
  170. package/tools/reload.js +52 -0
  171. package/tools/resource.js +54 -0
  172. package/tools/review.js +45 -0
  173. package/tools/roster.js +60 -0
  174. package/tools/send.js +147 -0
  175. package/tools/spawn.js +196 -0
  176. package/tools/status.js +102 -0
  177. package/tools/teardown.js +42 -0
  178. package/tools/test.js +61 -0
  179. package/types/cordis.d.ts +6 -0
  180. package/types/index.d.ts +373 -0
  181. package/lib/errors.js +0 -123
  182. package/lib/tools.js +0 -2460
  183. package/lib/tower.js +0 -2912
  184. /package/{lib → domain}/batch.js +0 -0
@@ -0,0 +1,38 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`GLOSSARY.md`** at the repo root, or
8
+ - **`GLOSSARY-MAP.md`** at the repo root if it exists: it points at one `GLOSSARY.md` per context. Read each one relevant to the topic.
9
+ - **`docs/adr/`**: read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
12
+
13
+ ## File structure
14
+
15
+ This repo is **single-context**: one `GLOSSARY.md` and one `docs/adr/` at the repo root.
16
+
17
+ ```
18
+ /
19
+ ├── GLOSSARY.md
20
+ ├── docs/adr/
21
+ │ ├── 0001-event-sourced-orders.md
22
+ │ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ (If the repo ever grows per-package contexts, switch to the multi-context layout: a root `GLOSSARY-MAP.md` pointing at per-context `GLOSSARY.md` files, with system-wide decisions in `docs/adr/` and context decisions in `src/<context>/docs/adr/`.)
27
+
28
+ ## Use the glossary's vocabulary
29
+
30
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `GLOSSARY.md`. Don't drift to synonyms the glossary explicitly avoids.
31
+
32
+ If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
33
+
34
+ ## Flag ADR conflicts
35
+
36
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
37
+
38
+ > _Contradicts ADR-0007 (event-sourced orders), but worth reopening because…_
@@ -0,0 +1,46 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Make an issue a sub-issue of a parent**: `gh issue create --parent <parent> ...`, or `gh issue edit <parent> --add-sub-issue <child>` afterwards (`gh` 2.94+). Older `gh`: `gh api --method POST repos/<owner>/<repo>/issues/<parent>/sub_issues -F sub_issue_id=<child-db-id>` (database id, as in **Blocking** below). Without sub-issues, put `Part of #<parent>` at the top of the child body.
11
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
12
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
13
+ - **Close**: `gh issue close <number> --comment "..."`
14
+
15
+ Infer the repo from `git remote -v`; `gh` does this automatically when run inside a clone. For this repo the remote is `louifinn/dsh-tower`.
16
+
17
+ ## Pull requests as a triage surface
18
+
19
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
20
+
21
+ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
22
+
23
+ - **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
24
+ - **List external PRs for triage**: `gh api --paginate 'repos/{owner}/{repo}/pulls?state=open' --jq '.[] | select(.author_association | IN("OWNER","MEMBER","COLLABORATOR") | not) | {number, title, author: .user.login, author_association, labels: [.labels[].name]}'`.
25
+ - **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
26
+
27
+ GitHub shares one number space across issues and PRs, so a bare `#42` may be either: resolve with `gh pr view 42` and fall back to `gh issue view 42`.
28
+
29
+ ## When a skill says "publish to the issue tracker"
30
+
31
+ Create a GitHub issue.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run `gh issue view <number> --comments`.
36
+
37
+ ## Wayfinding operations
38
+
39
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
40
+
41
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
42
+ - **Child ticket**: an issue linked to the map as a GitHub sub-issue (see **Make an issue a sub-issue of a parent**). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
43
+ - **Blocking**: GitHub's **native issue dependencies**, the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only, the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
44
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
45
+ - **Claim**: `gh issue edit <n> --add-assignee @me`, the session's first write.
46
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use. The right column currently equals the role name, so these are the defaults — `wontfix` already exists on the GitHub repo, while `needs-triage`, `needs-info`, `ready-for-agent`, and `ready-for-human` will be created by `/triage` on first use (or add them upfront with `gh label create`).
@@ -2,24 +2,61 @@
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.
9
+
5
10
  ```
6
11
  Host plugin (index.js)
7
12
  ├─ /tower command + Tower mode (guard, prompt section, tower_mode/tower_control, /tower reload)
8
- ├─ lib/tools.js -> 19 repository tools incl. tower_diff, tower_findings and tower_interrupt
9
- │ (+ tower_reload while dev.hotReload is on; fails reload-unavailable without HMR)
10
- └─ lib/tower.js -> Tower domain: missions, gates, worktrees, roster, review, findings, merge
11
- ├─ lib/git.js minimal git port (execFile, bounded output, timeout classification)
12
- ├─ lib/store.js repo-scoped lock + atomic JSON + activity.jsonl
13
- ├─ lib/config.js Config schema + profile/repo layers + runtime snapshot
14
- ├─ lib/errors.js stable tool error codes and classification
15
- ├─ lib/workspaceLock.js optional one-Tower-session-per-repository advisory lock
16
- ├─ lib/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
17
- └─ lib/modeStore.js per-agent Tower mode persistence
18
- Subagents
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)
16
+ ├─ tools/index.js -> thin public wiring (re-exports the registry + briefings)
17
+ └─ app/tower.js -> orchestration surface; delegates to use-case modules
18
+ ├─ app/init.js, provision.js, merge.js, rebase.js, teardown.js
19
+ ├─ app/review.js, findings.js, batch.js, messages.js, roster.js
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
35
+ pure domain modules (no fs/git):
36
+ domain/errors.js stable, structural tool error catalog
37
+ domain/batch.js batch admission create/close/summary validation
38
+ domain/config/schema.js Schemastery config schema, defaults, validation
39
+ domain/contracts/outputs.js tool output-schema catalog
40
+ domain/contracts/tool-inputs/ per-area TypeBox input schemas + ToolInputs map
41
+ domain/vocabulary.js canonical status/type/action enums + predicates
42
+ domain/mission.js statuses, transitions, pause causes
43
+ domain/gate.js completion and merge-gate snapshot decisions
44
+ domain/scope.js glob/scope validation and overlap
45
+ domain/summary.js mission/finding summaries
46
+ domain/planning.js mission numbering, dependency DAG, auto-deps
47
+ domain/state.js load-time state validation
48
+ domain/report.js board rendering
49
+ domain/roster.js roster predicates
50
+ tools layer:
51
+ lib/tool-registry.js registration aggregator + shared guards/helpers
52
+ tools/index.js thin public wiring (re-exports only)
53
+ tools/*.js one module per tool, handler + imported input schema
54
+ Subagents
19
55
  ├─ tower-worker (dsh-tower/worker) read/write/edit/pwsh + mission tools + findings
20
56
  └─ tower-reviewer (dsh-tower/reviewer) read/glob/grep + tower_diff + review/finding (read-only)
21
57
  ```
22
58
 
59
+
23
60
  The main checkout is the control-tower repo; each mission has a branch `tower/<id>-<slug>` and a
24
61
  worktree in `.dsh-tower-worktrees/<hash>/<missionId>`. The plugin never operates on the main checkout's
25
62
  working tree except merge and the read-only diff.
@@ -64,8 +101,72 @@ After the command, regardless of exit status, it inspects `HEAD`:
64
101
 
65
102
  | Path | Contents |
66
103
  |---|---|
67
- | `<git-common-dir>/dsh-tower/state.json` | missions, roster, reviews, findings, messages, reviews sequence |
104
+ | `<git-common-dir>/dsh-tower/state.json` | active missions, roster, reviews, findings, messages, reviews sequence |
105
+ | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
106
+ | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
68
107
  | `<git-common-dir>/dsh-tower/activity.jsonl` | append-only activity |
69
- | `<git-common-dir>/dsh-tower/state.lock` | single-writer lock (stale locks are reported, never stolen) |
108
+ | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (dead-PID reclaimed, live lock reported) |
109
+ | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
110
+ | `<git-common-dir>/dsh-tower/lock-holds.jsonl` | state/git lock-hold telemetry |
70
111
  | `<checkout parent>/.dsh-tower-worktrees/<hash>/<missionId>` | mission worktree |
71
112
  | `$DSH_HOME/tower-mode/modes.json` | Tower mode per agent/session |
113
+ | `$DSH_HOME/tower-mode/modes.lock` | mode-store single-writer lock |
114
+
115
+ `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.
118
+
119
+ ## Layering rules
120
+
121
+ `npm run architecture:check` (also in CI) enforces the rules as **errors**:
122
+
123
+ - `domain/**` must not import I/O or adapters (`domain-pure`).
124
+ - Only `adapters/vcs-git.js` may import `node:child_process`, and only
125
+ `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/`,
127
+ `adapters/`, `ports/` — and does not cover `scripts/**`, which holds maintenance
128
+ entry points (release check, mount smoke, docs generators) that may spawn git or npm.
129
+ - `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.
135
+
136
+ A fixture test seeds one violation per rule, so a rule that cannot fail is itself a test failure.
137
+
138
+ ## Mount smoke and host internals
139
+
140
+ `npm run smoke:mount` (#95) boots a real profile through the pinned host instead of mocking the loader.
141
+ `scripts/mount-smoke/probe.mjs` is the one file that imports the host's internals — `runProfile` from
142
+ `dsh/lib/profile-boot.js` and the layered env from `dsh-app-boot` — because those are the entry points
143
+ `dsh` itself calls to compose a profile; the probe uses them to boot without a model, browser, or
144
+ session. `scripts/mount-smoke/host.json` is the single host/pnpm pin (a unit test keeps its `dsh` field
145
+ equal to the newest `COMPATIBILITY.md` row), so a host bump can move those entry points and may require
146
+ probe adjustments in the same change.
147
+
148
+ ## #56 definition of done
149
+
150
+ All epic DoD items now have an executable gate:
151
+
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;
155
+ `domain-pure` is a blocking architecture rule.
156
+ - 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
159
+ 40-200 lines (one handler module per tool; `tools/index.js` is 40-line wiring).
160
+ - No `withLock` scope starts a process; the two-phase tests instrument every git call.
161
+ - `tools/**` has no status/type/enum literal; vocabulary is owned by
162
+ `domain/vocabulary.js`; the `tools-vocabulary` rule is blocking.
163
+ - `validateState` has a four-line body over `domain/state/schema.js` and
164
+ `domain/state/semantics.js`.
165
+ - `state.json` and `modes.json` share `lib/persistence.js` atomic write, token-owned
166
+ 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.
170
+
171
+
172
+
package/docs/board.md ADDED
@@ -0,0 +1,72 @@
1
+ # Tracker board
2
+
3
+ The three questions an owner asks weekly, as commands that actually work. Run them from the
4
+ repository root (or add `--repo louifinn/dsh-tower`). Verified against the live tracker on
5
+ 2026-10-22; the counts below are evidence for the commands, not a promise they stay fixed.
6
+
7
+ Priority lives in the `P0`–`P3` labels and area in `area/*`; issue titles carry no `[P*]`
8
+ prefix any more. Every open issue has exactly one `P*` label and at least one `area/*` label.
9
+
10
+ ## Open P1
11
+
12
+ ```bash
13
+ gh issue list --state open --label P1
14
+ ```
15
+
16
+ Verified: **10** open on 2026-10-22 (#56, #69, #76, #77, #82, #94, #95, #96, #97, #98).
17
+
18
+ `--label` uses REST filters, so it does not wait for the search index after an edit. Do **not**
19
+ write `--label P0,P1`: `gh` treats that as one literal label name and returns nothing (verified).
20
+ For "P0 or P1" use `--search 'label:P0,P1'` — inside `--search`, the comma means OR, while
21
+ `label:P0 label:P1` (a space) means AND and returns only issues carrying both.
22
+
23
+ ## Unmilestoned P0/P1
24
+
25
+ ```bash
26
+ gh issue list --state open --search 'no:milestone label:P0,P1'
27
+ ```
28
+
29
+ Verified: **0** on 2026-10-22 — every open P0/P1 is in a milestone. A non-zero result means a
30
+ release-scoped issue has no home; put it in the next release milestone or demote it to P2/P3.
31
+
32
+ ## Closed this week by area
33
+
34
+ ```bash
35
+ # week starts Monday; compute it rather than pasting yesterday's date
36
+ # GNU date (Linux/CI); on macOS use `gdate` or work the date out by hand
37
+ MONDAY=$(date -d 'last monday' +%F)
38
+ gh issue list --state closed --search "closed:>=$MONDAY label:area/ci"
39
+ ```
40
+
41
+ ```powershell
42
+ $MONDAY = (Get-Date).Date.AddDays(-(([int](Get-Date).DayOfWeek+6)%7)).ToString('yyyy-MM-dd')
43
+ gh issue list --state closed --search "closed:>=$MONDAY label:area/ci"
44
+ ```
45
+
46
+ Swap `area/ci` for `area/state`, `area/tools`, `area/docs`, `area/release`, `area/process`,
47
+ `area/architecture`, or `area/scope`. Verified for `area/ci` with `closed:>=2026-10-19`:
48
+ **0**, because all **54** issues closed that week carry no `area/*` label at all. The query is
49
+ right; the history is unlabelled. Label a closure before you close it (or backfill) and the
50
+ by-area view starts working.
51
+
52
+ ## Release rule query
53
+
54
+ A release may not be tagged while its milestone holds an open P0/P1 unless the release notes
55
+ say why. Check the milestone that is about to ship (see the [release checklist](guide/release.md)):
56
+
57
+ ```bash
58
+ gh issue list --state open --search 'milestone:2.1.0-beta.1 label:P0,P1' --json number,title
59
+ ```
60
+
61
+ Verified: **8** on 2026-10-22 (#76, #77, #82, #94, #95, #96, #97, #98) — the delivery gate's own
62
+ tickets block the release until they land. That is the rule doing its job, not a false positive.
63
+
64
+ ## Milestones
65
+
66
+ ```bash
67
+ gh api 'repos/louifinn/dsh-tower/milestones?state=all&per_page=100' \
68
+ --jq '.[] | "\(.number)\t\(.title)\topen=\(.open_issues)\tclosed=\(.closed_issues)"'
69
+ ```
70
+
71
+ Only issues that actually block a release belong in a milestone; everything else stays
72
+ unmilestoned and is queried by priority.
@@ -101,7 +101,7 @@ The bridge behind the tool follows the `dev` config group:
101
101
  |---|---|
102
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`. |
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`, `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-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. |
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Requirements
4
4
 
5
- - DSH `0.2.1-rc.2` or newer
5
+ - DSH `0.2.1-rc.2`; see [COMPATIBILITY.md](../../COMPATIBILITY.md) for the builds actually verified per release
6
6
  - Node.js 22+
7
7
  - Git (local worktrees; no network access is required)
8
8
 
@@ -29,6 +29,30 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
29
29
 
30
30
  ## Migration notes
31
31
 
32
+ - **state schema v1 → v2 (unreleased)**: `state.json` migrates automatically. The first **write** after
33
+ upgrading keeps a `state.json.v1.bak` backup, advances `revision` once, and appends
34
+ `state.migrated {from,to}` to `activity.jsonl`; read-only tools keep reading a v1 file without
35
+ rewriting it. `tower_doctor` reports the on-disk version and the pending migration. A file written by
36
+ a newer plugin is refused with `[tower:state-version-unsupported]` and is never rewritten or
37
+ downgraded; a migration that throws reports `[tower:state-migration-failed]` and leaves the original
38
+ file byte-identical.
39
+
40
+ - **state schema v2 → v3 (unreleased, #90)**: scope matching follows the checkout's filesystem. The
41
+ migration backfills `caseInsensitive: null` ("not detected"); a migration never reads the filesystem,
42
+ so the first `tower_init` after upgrading probes the checkout once and records `true`/`false`. Until
43
+ then the safe `case-sensitive` comparison applies, and `tower_doctor` reports the unknown mode as a
44
+ `warn` (`case-mode`). On a case-insensitive checkout, `src/Api/**` and `src/api/**` then count as
45
+ overlapping and case-only mismatches are no longer refused by the out-of-scope gate. Stored scope
46
+ strings are never rewritten: the board keeps showing what the operator typed.
47
+
48
+ - **2.0.4 → 2.1.0-beta.1**: no config migration; the state file migrates automatically (v1/v2 → v3) as
49
+ described above. This is a **pre-release** published on the
50
+ `next` dist-tag, so `latest` still resolves to 2.0.4 — install it with
51
+ `dsh plugin --profile web add dsh-tower@next` (or the exact version). The runtime behavior is
52
+ unchanged; the release carries the delivery gate (mount smoke in CI, `npm run release:check`, the
53
+ evidence conventions and the tracker policy) rather than tool changes. These docs merge before the
54
+ human publish step: `next` only resolves to 2.1.0-beta.1 after the [release checklist](release.md)
55
+ runs `npm publish --tag next`, and `latest` stays on 2.0.4 until a stable release.
32
56
  - **2.0.3 → 2.0.4**: no state/config migration. This is a security-hardening and integrity release
33
57
  (authorization, merge SHA binding, dirty-worktree completion, stale-lock recovery, limited test/git
34
58
  process environment, `tower_recover`, UI 0.2.2). Review `review.requireReview`, `tests.admission` and
@@ -0,0 +1,125 @@
1
+ # Release checklist
2
+
3
+ One page, six steps, in order. The only **human-only** step is `npm publish` (npm
4
+ credentials + 2FA); an agent can do everything else in this repository. A release is a claim —
5
+ `npm run release:check` is the mechanical half of it.
6
+
7
+ ## Milestone rule (before step 1)
8
+
9
+ A release may not be tagged while its milestone holds an open P0/P1, unless the release
10
+ notes say why it was accepted:
11
+
12
+ ```bash
13
+ gh issue list --state open --search 'milestone:2.1.0-beta.1 label:P0,P1' --json number,title
14
+ ```
15
+
16
+ Empty output → proceed. Non-empty → close the issues (with the two-line closure contract in
17
+ [CONTRIBUTING.md](../../CONTRIBUTING.md#the-two-line-closure-contract)), or name each accepted
18
+ issue and the reason in the release notes. The same query is saved in [the board](../board.md);
19
+ comma means OR inside `--search`, while `--label P0,P1` matches one literal label name and
20
+ returns nothing.
21
+
22
+ `npm run release:check` prints `git describe`, the previous tag, the commit count since that
23
+ tag and whether Unreleased is non-empty on every run. On a PR it warns; on a tag push it fails
24
+ when `package.json` did not move past the last tag, when the tag is not `v<version>`, or when
25
+ the CHANGELOG has no release content. CI runs it on every push and PR.
26
+
27
+ ## 1. Bump the version
28
+
29
+ Edit `package.json` (`"version": "2.1.0-beta.1"`). The previous tag is found from the pushed
30
+ tag's parent (`<tag>^`), so a tag push that repeats the old version fails.
31
+
32
+ ```bash
33
+ npm run release:check -- --mode tag --tag v2.1.0-beta.1
34
+ ```
35
+
36
+ ## 2. Write the changelog
37
+
38
+ Move the Unreleased entries under a version heading, so the GitHub Release body can be
39
+ extracted mechanically. Keep the `## 未发布(Unreleased)` heading (it may be empty).
40
+
41
+ ```markdown
42
+ ## 未发布(Unreleased)
43
+
44
+ ## 2.1.0-beta.1(2026-10-22)
45
+
46
+ - feat: ...
47
+ ```
48
+
49
+ The gate accepts a non-empty Unreleased section (bump first, rename later) **or** a non-empty
50
+ section whose heading names the version. Write the version section as user-facing notes.
51
+
52
+ ## 3. Add the COMPATIBILITY row
53
+
54
+ `COMPATIBILITY.md` keeps the columns `| dsh version | tag / commit | verified on | what was run |`.
55
+ Put the **host build in cell 0** and the **tag in cell 1**:
56
+
57
+ ```markdown
58
+ | 0.2.1-rc.2 | v2.1.0-beta.1 | 2026-10-22 | `npm test`, `npm run docs:check`, mount smoke |
59
+ ```
60
+
61
+ `lib/compatibility.js#parseCompatibility` reads cells 0–3 only: never insert a column before
62
+ the tag and never add one before "what was run", or `tower_doctor` stops seeing the row. The
63
+ host pin in `scripts/mount-smoke/host.json` must equal cell 0 of the newest row (a unit test
64
+ enforces it).
65
+
66
+ Commit steps 1–3 as the release PR against protected `main`, merge it, then continue from a
67
+ **clean clone/worktree at the merge commit** — not the integration worktree.
68
+
69
+ ## 4. Publish — human-only (2FA)
70
+
71
+ ```bash
72
+ git status --porcelain # must be empty: files:["docs/"] packs untracked docs too
73
+ npm ci
74
+ npm test && npm run docs:check && npm run pack:check
75
+ npm pack --dry-run # inspect the published file list
76
+ npm publish --tag next --otp <code>
77
+ npm view dsh-tower dist-tags --json
78
+ npm view dsh-tower@next version
79
+ ```
80
+
81
+ - Publish pre-releases with `--tag next`, **never** `latest`; `latest` moves only for a stable minor.
82
+ - Publish **before tagging**. If publishing fails, do not tag. If you already tagged, delete it
83
+ (`git tag -d <tag>` and `git push --delete origin <tag>`) instead of leaving a tag on an
84
+ unpublished commit.
85
+ - Both checks must name the version just published, and `next` must differ from `latest`.
86
+
87
+ ## 5. Annotated tag on the published commit
88
+
89
+ ```bash
90
+ npm run release:check -- --mode tag --tag v2.1.0-beta.1
91
+ git tag -a "v2.1.0-beta.1" -m "dsh-tower 2.1.0-beta.1"
92
+ git push origin "v2.1.0-beta.1"
93
+ git for-each-ref refs/tags/v2.1.0-beta.1 --format='%(objecttype)|%(subject)'
94
+ # tag|dsh-tower 2.1.0-beta.1 (a lightweight tag prints commit, and is not allowed)
95
+ ```
96
+
97
+ The pushed tag must be exactly `v<version>`. The tag push re-runs `release:check` against
98
+ `<tag>^`; this is why tagging a commit whose `package.json` did not move fails.
99
+
100
+ ## 6. GitHub Release from the CHANGELOG section
101
+
102
+ ```bash
103
+ VERSION=2.1.0-beta.1
104
+ awk -v v="$VERSION" '$0 ~ "^##[[:space:]]+.*" v {s=1; next} s && /^##[[:space:]]/ {exit} s {print}' \
105
+ CHANGELOG.md > release-notes.md
106
+ test -s release-notes.md || { echo "empty section for $VERSION" >&2; exit 1; }
107
+ gh release create "v$VERSION" --title "dsh-tower $VERSION" --notes-file release-notes.md \
108
+ --prerelease --verify-tag --latest=false
109
+ gh release view "v$VERSION" --json tagName,isPrerelease,body
110
+ ```
111
+
112
+ PowerShell extraction of the same section:
113
+
114
+ ```powershell
115
+ $version = '2.1.0-beta.1'; $lines = Get-Content CHANGELOG.md
116
+ $start = ($lines | Select-String -Pattern "^##\s+.*$([regex]::Escape($version))" | Select-Object -First 1).LineNumber
117
+ if (-not $start) { throw "CHANGELOG has no section for $version" }
118
+ $next = ($lines | Select-String -Pattern '^##\s+' | Where-Object LineNumber -gt $start | Select-Object -First 1).LineNumber
119
+ $end = if ($next) { $next - 2 } else { $lines.Count - 1 }
120
+ ($lines[$start..$end] -join "`n").Trim() | Set-Content release-notes.md -Encoding utf8
121
+ ```
122
+
123
+ `--verify-tag` refuses to create the tag at the default branch when the pushed tag is missing;
124
+ `--prerelease --latest=false` keeps the GitHub "latest" marker on the stable release. Releases
125
+ are what consumers and the plugin directory can see — a tag alone is not a release.
@@ -44,6 +44,49 @@ Tower's guarantees are **coordination guarantees**, not a sandbox. Understand wh
44
44
  - **Legacy import is read-only.** Imported snapshots live under the canonical state directory and are never
45
45
  activated, adopted, or resumed; an operator must explicitly re-plan/migrate work.
46
46
 
47
+ ## Repository protection
48
+
49
+ This section is about **this repository**, not about a repository Tower runs on. Server-side branch
50
+ protection is not a plugin feature — Tower writes to the local base branch and cannot configure a
51
+ remote — but `dsh-tower` enables it for its own `main`, because the code that is the gate is also the
52
+ code an attacker would most want to change:
53
+
54
+ - `main` requires a pull request for every change, **enforced for admins**, so a direct push is
55
+ rejected even for the maintainer.
56
+ - Required checks are `test (22)`, `test (24)` and `mount`. The mount job packs the tarball and boots
57
+ it in a throwaway profile, so "the tests pass" cannot land a bundle that fails to appear after
58
+ `dsh plugin add`.
59
+ - Force-push and branch deletion are blocked, so history cannot be rewritten under a published tag.
60
+ - There are no mandatory approvals: with one maintainer, a self-approval requirement would be a rubber
61
+ stamp. The evidence instead is the four-line PR body (failure mode, change, test, blast radius) for
62
+ the two-key paths, written in [CONTRIBUTING.md](../../CONTRIBUTING.md#landing-changes).
63
+
64
+ What this buys: no untested change reaches `main`, a red or missing required check cannot be bypassed
65
+ by the owner, and history is append-only. What it does **not** buy: a second reader, protection against
66
+ a compromised maintainer token, or any protection for the repositories Tower itself operates on — those
67
+ remain deployment concerns. The settings, their rationale and a verification command are recorded in
68
+ [SECURITY.md](../../SECURITY.md#repository-protection-on-main).
69
+
70
+ ## Model-initiated mode entry (threat model)
71
+
72
+ `mode.allowAgentControl:true` means a prompt injection in a repository file, issue text, or fetched
73
+ page can ask the model to “enter tower mode and fix this”. Tower does **not** authenticate the human
74
+ intent behind that entry. What it does provide:
75
+
76
+ - the entry is recorded with actor, action, reason, and timestamp; `tower_status.mode.history`
77
+ surfaces the recent transitions without grepping the activity log;
78
+ - the model cannot merge without the preflight gate, review policy, scope checks, tests, and
79
+ clean-base checks; writing the plan does not bypass them;
80
+ - workers are tool-restricted and instructed to stay in their worktree, but the main-checkout
81
+ protection is enforced by scope/merge checks and the preset, **not** by an OS sandbox;
82
+ - `mode.workspaceLock` prevents a second Tower session from driving the same repository.
83
+
84
+ It does **not** defend against: a model that legitimately passes every gate while acting on malicious
85
+ instructions; repository hooks or test commands the repository itself declares; or filesystem/shell
86
+ access outside the worktree by a worker that ignores its instructions. For a higher-assurance team,
87
+ set `mode.allowAgentControl:false`, keep `review.requireReview:true`, review the repository before
88
+ Tower work, and run the host under OS-level isolation.
89
+
47
90
  ## Not enforced by the plugin (deployment concerns)
48
91
 
49
92
  - **Worktree isolation is a protocol convention, not an OS sandbox.** Workers are assigned a dedicated
@@ -65,8 +108,10 @@ Tower's guarantees are **coordination guarantees**, not a sandbox. Understand wh
65
108
  from disk. Only enable HMR watch roots for checkouts you trust; a reload executes the current files.
66
109
  - **Prompt injection.** The reviewer reads untrusted branch content (diff/comments). The read-only preset
67
110
  limits impact, but no model-side protection is absolute.
68
- - **Base branch protection.** Tower writes to the local base branch; server-side branch protection/push
69
- rules are not part of the plugin.
111
+ - **Base branch protection.** Tower writes to the local base branch; server-side branch protection and
112
+ push rules are not part of the plugin. This repository's own `main` protection is described in
113
+ [Repository protection](#repository-protection); enabling equivalent protection on the repository you
114
+ run Tower on remains a deployment concern.
70
115
 
71
116
  ## Reporting
72
117
 
@@ -16,7 +16,7 @@ tables below define 32 stable blocker/failure codes.
16
16
  | `branch-missing` | No worktree/branch yet: spawn a worker first. |
17
17
  | `stale-base` | The branch does not contain the base tip: run `tower_rebase`. |
18
18
  | `read-only-survey` | Survey mission changed files; survey missions must be read-only. |
19
- | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. |
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
21
  | `deps-unmerged` | A dependency mission is not merged/abandoned. |
22
22
  | `no-review` | `review.requireReview:true` and no review exists on the branch: spawn a reviewer (or disable the review gate). |