dsh-tower 2.0.4 → 2.1.0-beta.1

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 (171) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/COMPATIBILITY.md +38 -0
  3. package/CONTRIBUTING.md +150 -2
  4. package/README.en.md +40 -3
  5. package/README.md +38 -3
  6. package/SECURITY.md +27 -0
  7. package/adapters/default.js +10 -0
  8. package/adapters/store-fs.js +6 -0
  9. package/adapters/vcs-git.js +4 -0
  10. package/app/batch.js +79 -0
  11. package/app/board.js +25 -0
  12. package/app/checkCompletion.js +76 -0
  13. package/app/enrichedMissions.js +26 -0
  14. package/app/findings.js +78 -0
  15. package/app/gate.js +108 -0
  16. package/app/init.js +249 -0
  17. package/app/merge.js +567 -0
  18. package/app/messages.js +66 -0
  19. package/app/mission.js +35 -0
  20. package/app/missionContext.js +14 -0
  21. package/app/plan.js +42 -0
  22. package/app/provision.js +193 -0
  23. package/app/read.js +57 -0
  24. package/app/rebase.js +302 -0
  25. package/app/recording.js +72 -0
  26. package/app/review.js +145 -0
  27. package/app/roster.js +123 -0
  28. package/app/spawn.js +125 -0
  29. package/app/teardown.js +184 -0
  30. package/app/tower.js +524 -0
  31. package/cordis.patch.yml +61 -53
  32. package/docs/README.md +3 -1
  33. package/docs/architecture/overview.md +111 -12
  34. package/docs/board.md +72 -0
  35. package/docs/guide/hot-reload.md +1 -1
  36. package/docs/guide/install-upgrade-migration.md +8 -1
  37. package/docs/guide/release.md +125 -0
  38. package/docs/operations/security.md +47 -2
  39. package/docs/reference/config.generated.md +66 -0
  40. package/docs/reference/config.md +4 -1
  41. package/docs/reference/errors.generated.md +39 -0
  42. package/docs/reference/errors.md +4 -2
  43. package/docs/reference/tools.generated.md +33 -0
  44. package/docs/reference/tools.md +1 -1
  45. package/docs/zh/README.md +3 -2
  46. package/docs/zh/guide/hot-reload.md +1 -1
  47. package/docs/zh/guide/install-upgrade-migration.md +6 -1
  48. package/docs/zh/operations/security.md +30 -1
  49. package/docs/zh/reference/config.md +4 -1
  50. package/docs/zh/reference/errors.md +3 -1
  51. package/docs/zh/reference/tools.md +1 -1
  52. package/domain/collab/batch.js +112 -0
  53. package/domain/collab/messages.js +127 -0
  54. package/domain/collab/roster.js +158 -0
  55. package/domain/collab/spawn.js +188 -0
  56. package/domain/config/schema.js +404 -0
  57. package/domain/contracts/outputs.js +279 -0
  58. package/domain/contracts/tool-inputs/collaboration.js +58 -0
  59. package/domain/contracts/tool-inputs/findings.js +29 -0
  60. package/domain/contracts/tool-inputs/index.js +94 -0
  61. package/domain/contracts/tool-inputs/integration.js +37 -0
  62. package/domain/contracts/tool-inputs/missions.js +94 -0
  63. package/domain/contracts/tool-inputs/ops.js +75 -0
  64. package/domain/contracts/tool-inputs/shared.js +25 -0
  65. package/domain/errors.js +96 -0
  66. package/domain/gate.js +125 -0
  67. package/domain/integration/gate.js +176 -0
  68. package/domain/integration/init.js +225 -0
  69. package/domain/integration/liveness.js +31 -0
  70. package/domain/integration/merge.js +377 -0
  71. package/domain/integration/provision.js +124 -0
  72. package/domain/integration/rebase.js +236 -0
  73. package/domain/integration/review.js +52 -0
  74. package/domain/integration/teardown.js +118 -0
  75. package/domain/mission.js +68 -0
  76. package/domain/missions/completion.js +114 -0
  77. package/domain/missions/findings.js +201 -0
  78. package/domain/missions/lifecycle.js +185 -0
  79. package/domain/missions/plan.js +149 -0
  80. package/domain/missions/review.js +179 -0
  81. package/domain/planning.js +118 -0
  82. package/domain/read/enrichedMissions.js +58 -0
  83. package/domain/read/missionContext.js +53 -0
  84. package/domain/read/views.js +128 -0
  85. package/domain/report.js +59 -0
  86. package/domain/roster.js +40 -0
  87. package/domain/scope.js +105 -0
  88. package/domain/state/schema.js +233 -0
  89. package/domain/state/semantics.js +117 -0
  90. package/domain/state.js +33 -0
  91. package/domain/summary.js +114 -0
  92. package/domain/vocabulary.js +179 -0
  93. package/index.js +63 -20
  94. package/lib/briefing.js +176 -0
  95. package/lib/compatibility.js +108 -0
  96. package/lib/config.js +33 -375
  97. package/lib/doctor.js +97 -4
  98. package/lib/git.js +60 -16
  99. package/lib/hmr.js +4 -3
  100. package/lib/leases.js +3 -2
  101. package/lib/legacy.js +20 -9
  102. package/lib/modeState.js +39 -0
  103. package/lib/modeStore.js +11 -43
  104. package/lib/persistence.js +349 -0
  105. package/lib/presets.js +35 -0
  106. package/lib/spawn-prechecks.js +168 -0
  107. package/lib/spawn-support.js +79 -0
  108. package/lib/store.js +54 -137
  109. package/lib/tests.js +11 -10
  110. package/lib/tool-registry.js +544 -0
  111. package/lib/workspaceLock.js +2 -2
  112. package/lib/worktreeOwnership.js +5 -4
  113. package/locale/en.json +23 -1
  114. package/locale/zh.json +22 -0
  115. package/package.json +32 -4
  116. package/ports/clock.js +6 -0
  117. package/ports/ids.js +7 -0
  118. package/ports/index.js +2 -0
  119. package/ports/store.js +11 -0
  120. package/ports/vcs.js +11 -0
  121. package/scripts/architecture.mjs +94 -0
  122. package/scripts/check-docs.mjs +9 -3
  123. package/scripts/check-pack.mjs +49 -0
  124. package/scripts/domain-metrics.mjs +48 -0
  125. package/scripts/generate-config-reference.mjs +46 -0
  126. package/scripts/generate-error-reference.mjs +29 -0
  127. package/scripts/generate-tools-reference.mjs +56 -0
  128. package/scripts/issue-audit.mjs +259 -0
  129. package/scripts/lib/cli.mjs +76 -0
  130. package/scripts/lib/tmp.mjs +20 -0
  131. package/scripts/mount-smoke/fixtures.mjs +162 -0
  132. package/scripts/mount-smoke/host.json +4 -0
  133. package/scripts/mount-smoke/probe.mjs +125 -0
  134. package/scripts/mount-smoke/surface.mjs +141 -0
  135. package/scripts/mount-smoke.mjs +270 -0
  136. package/scripts/pack-manifest.txt +203 -0
  137. package/scripts/release-check.mjs +303 -0
  138. package/tools/adopt.js +72 -0
  139. package/tools/batch.js +78 -0
  140. package/tools/complete.js +53 -0
  141. package/tools/diff.js +39 -0
  142. package/tools/doctor.js +50 -0
  143. package/tools/finding.js +40 -0
  144. package/tools/findings.js +39 -0
  145. package/tools/import-legacy.js +42 -0
  146. package/tools/inbox.js +39 -0
  147. package/tools/index.js +39 -0
  148. package/tools/init.js +63 -0
  149. package/tools/interrupt.js +42 -0
  150. package/tools/legacy-map.js +44 -0
  151. package/tools/merge.js +39 -0
  152. package/tools/mission.js +41 -0
  153. package/tools/plan.js +39 -0
  154. package/tools/preflight.js +44 -0
  155. package/tools/rebase.js +39 -0
  156. package/tools/recover.js +45 -0
  157. package/tools/reload.js +52 -0
  158. package/tools/resource.js +54 -0
  159. package/tools/review.js +45 -0
  160. package/tools/roster.js +60 -0
  161. package/tools/send.js +147 -0
  162. package/tools/spawn.js +196 -0
  163. package/tools/status.js +94 -0
  164. package/tools/teardown.js +42 -0
  165. package/tools/test.js +61 -0
  166. package/types/cordis.d.ts +6 -0
  167. package/types/index.d.ts +339 -0
  168. package/lib/errors.js +0 -123
  169. package/lib/tools.js +0 -2460
  170. package/lib/tower.js +0 -2912
  171. /package/{lib → domain}/batch.js +0 -0
@@ -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.
@@ -66,6 +103,68 @@ After the command, regardless of exit status, it inspects `HEAD`:
66
103
  |---|---|
67
104
  | `<git-common-dir>/dsh-tower/state.json` | missions, roster, reviews, findings, messages, reviews sequence |
68
105
  | `<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) |
106
+ | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (dead-PID reclaimed, live lock reported) |
107
+ | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
108
+ | `<git-common-dir>/dsh-tower/lock-holds.jsonl` | state/git lock-hold telemetry |
70
109
  | `<checkout parent>/.dsh-tower-worktrees/<hash>/<missionId>` | mission worktree |
71
110
  | `$DSH_HOME/tower-mode/modes.json` | Tower mode per agent/session |
111
+ | `$DSH_HOME/tower-mode/modes.lock` | mode-store single-writer lock |
112
+
113
+ `state.json` and `modes.json` both use an atomic write (temp + rename), token-checked lock release,
114
+ dead-PID reclamation, and bounded lock waits. `state.json` is the mission persistence contract;
115
+ `modes.json` is the per-agent mode contract; neither is written without its own lock.
116
+
117
+ ## Layering rules
118
+
119
+ `npm run architecture:check` (also in CI) enforces the rules as **errors**:
120
+
121
+ - `domain/**` must not import I/O or adapters (`domain-pure`).
122
+ - Only `adapters/vcs-git.js` may import `node:child_process`, and only
123
+ `adapters/store-fs.js` may import `node:fs` (`child-process-surface`, `fs-surface`).
124
+ The rule is enforced over the source roots — `lib/`, `app/`, `domain/`, `tools/`,
125
+ `adapters/`, `ports/` — and does not cover `scripts/**`, which holds maintenance
126
+ entry points (release check, mount smoke, docs generators) that may spawn git or npm.
127
+ - `tools/**` must stay thin and free of status/type literals (`tools-thin`, `tools-vocabulary`);
128
+ `tools/index.js` is the thin public wiring module and stays exempt from the thinness rule,
129
+ while registration/shared machinery lives in `lib/tool-registry.js`.
130
+ - Aggregate files have size ceilings; top-level mutable state is limited to explicit composition
131
+ roots (`aggregate-size`, `ambient-state`). The per-tool ceiling is 200 lines; only the
132
+ `tools/index.js` wiring module is exempt, and it is now far below the ceiling.
133
+
134
+ A fixture test seeds one violation per rule, so a rule that cannot fail is itself a test failure.
135
+
136
+ ## Mount smoke and host internals
137
+
138
+ `npm run smoke:mount` (#95) boots a real profile through the pinned host instead of mocking the loader.
139
+ `scripts/mount-smoke/probe.mjs` is the one file that imports the host's internals — `runProfile` from
140
+ `dsh/lib/profile-boot.js` and the layered env from `dsh-app-boot` — because those are the entry points
141
+ `dsh` itself calls to compose a profile; the probe uses them to boot without a model, browser, or
142
+ session. `scripts/mount-smoke/host.json` is the single host/pnpm pin (a unit test keeps its `dsh` field
143
+ equal to the newest `COMPATIBILITY.md` row), so a host bump can move those entry points and may require
144
+ probe adjustments in the same change.
145
+
146
+ ## #56 definition of done
147
+
148
+ All epic DoD items now have an executable gate:
149
+
150
+ - Pure domain share: **37.9%** (5,993 / 15,820 source lines), enforced by
151
+ `npm run domain:check` (threshold 35%).
152
+ - `domain/**` imports no fs, child_process, adapters, app, tools, or lib infrastructure;
153
+ `domain-pure` is a blocking architecture rule.
154
+ - Inside the source roots, only `adapters/vcs-git.js` imports `node:child_process`; only
155
+ `adapters/store-fs.js` imports `node:fs`; both are blocking rules with fixtures.
156
+ - `lib`, `app`, and `domain` files stay <= 800 lines; every `tools/**` file stays
157
+ 40-200 lines (one handler module per tool; `tools/index.js` is 40-line wiring).
158
+ - No `withLock` scope starts a process; the two-phase tests instrument every git call.
159
+ - `tools/**` has no status/type/enum literal; vocabulary is owned by
160
+ `domain/vocabulary.js`; the `tools-vocabulary` rule is blocking.
161
+ - `validateState` has a four-line body over `domain/state/schema.js` and
162
+ `domain/state/semantics.js`.
163
+ - `state.json` and `modes.json` share `lib/persistence.js` atomic write, token-owned
164
+ lock, dead-PID quarantine, and bounded-wait primitives.
165
+ - The in-memory lifecycle and adapter contracts run a full mission lifecycle with no
166
+ disk and no git.
167
+ - The seven fitness rules are errors, each with a seeded failing fixture.
168
+
169
+
170
+
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,13 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
29
29
 
30
30
  ## Migration notes
31
31
 
32
+ - **2.0.4 → 2.1.0-beta.1**: no state/config migration. This is a **pre-release** published on the
33
+ `next` dist-tag, so `latest` still resolves to 2.0.4 — install it with
34
+ `dsh plugin --profile web add dsh-tower@next` (or the exact version). The runtime behavior is
35
+ unchanged; the release carries the delivery gate (mount smoke in CI, `npm run release:check`, the
36
+ evidence conventions and the tracker policy) rather than tool changes. These docs merge before the
37
+ human publish step: `next` only resolves to 2.1.0-beta.1 after the [release checklist](release.md)
38
+ runs `npm publish --tag next`, and `latest` stays on 2.0.4 until a stable release.
32
39
  - **2.0.3 → 2.0.4**: no state/config migration. This is a security-hardening and integrity release
33
40
  (authorization, merge SHA binding, dirty-worktree completion, stale-lock recovery, limited test/git
34
41
  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
 
@@ -0,0 +1,66 @@
1
+ # Config reference (generated)
2
+
3
+ > Generated by `npm run docs:generate:config` from `domain/config/schema.js`. Values are defaults before repo config.
4
+
5
+ | Key | Default | Description |
6
+ |---|---|---|
7
+ | `section` | `""` | Legacy alias for mode.promptSection: replaces the whole Tower-mode system prompt text. |
8
+ | `allowAgentControl` | `` | Legacy alias for mode.allowAgentControl: register tower_control so the model may toggle the mode. |
9
+ | `init.autoGitInit` | `false` | tower_init may run git init and create an empty initial commit when the directory is not a repository. |
10
+ | `init.snapshotDirty` | `false` | tower_init may snapshot uncommitted tracked changes into a dangling commit used as the mission baseline. |
11
+ | `init.autoExclude` | `true` | Add in-repo Tower artifacts (MISSIONS.md, in-repo worktree roots) to .git/info/exclude so they do not look like user dirt. |
12
+ | `mode.promptSection` | `` | Replacement Tower-mode system prompt text. Empty keeps the built-in reminder. |
13
+ | `mode.sectionAppend` | `` | Extra text appended to the Tower-mode system prompt section (team conventions). |
14
+ | `mode.allowAgentControl` | `true` | Register tower_control so the model may enter/leave the mode with a reason. |
15
+ | `mode.autoPauseGoal` | `true` | Pause an active goal when entering Tower mode. |
16
+ | `mode.requireResumeAuthorization` | `false` | Require an authorization reference when resuming a mission paused with pauseCause=user. |
17
+ | `mode.requireHumanAckOnFirstEntry` | `false` | Require a human /tower ack before the model may enter Tower mode for the first time in a session. |
18
+ | `mode.workspaceLock` | `false` | Allow only one Tower session per repository (advisory <git-common-dir>/dsh-tower/tower.lock). |
19
+ | `mode.workspaceLockScope` | `"all"` | all = every repo-scoped tool acquires the workspace lock; mutations = only tools that write state/worktrees do. |
20
+ | `mode.planModePolicy` | `"clear"` | On Tower entry: clear an active plan mode, or refuse to enter until the plan is exited. |
21
+ | `mode.workspaceStaleMs` | `300000` | A workspace lock older than this is stale and can be taken over. |
22
+ | `mode.heartbeatMs` | `10000` | Workspace-lock heartbeat interval while Tower mode is on. |
23
+ | `mode.denyTools` | `["create_goal","todo_write","team_task_create","team_task_update","team_task_reopen","team_task_list","spawn_teammate","wait_agent","workflow","ralph","subagent","subagent_fork"]` | Tools refused while Tower mode is active. |
24
+ | `mode.storePath` | `` | Directory for modes.json; defaults to $DSH_HOME/tower-mode. |
25
+ | `mode.maxEntries` | `200` | Maximum persisted mode entries. |
26
+ | `mode.lockTimeoutMs` | `5000` | How long saveModes waits for the cross-process modes.lock before giving up. |
27
+ | `review.requireReview` | `false` | Require an independent clean review before merge. Default false: review is optional; set true to enforce the review gate globally. |
28
+ | `review.maxRoundsPerReviewer` | `5` | Review-round cap per reviewer per mission branch. |
29
+ | `merge.timeoutMs` | `900000` | Default tower_merge timeout, including pre-merge hooks. |
30
+ | `merge.maxBufferBytes` | `67108864` | Captured merge output buffer. |
31
+ | `git.timeoutMs` | `15000` | Default timeout for Git commands other than merge; merge uses merge.timeoutMs. |
32
+ | `worktrees.root` | `` | Default parent directory for mission worktrees; empty derives <main-checkout-parent>/.dsh-tower-worktrees/<hash>. |
33
+ | `spawn.workerPreset` | `"tower-worker"` | Agent preset for worker missions. |
34
+ | `spawn.reviewerPreset` | `"tower-reviewer"` | Agent preset for reviewers. |
35
+ | `spawn.maxDepth` | `1` | Subagent depth requested when spawning tower agents. |
36
+ | `spawn.provider` | `` | Subagent provider id; empty picks spawn, then the first registered provider. |
37
+ | `spawn.workerModel` | `{}` | Model route for workers; omitted inherits the parent session. |
38
+ | `spawn.reviewerModel` | `{}` | Model route for reviewers; omitted inherits the parent session (recommended). |
39
+ | `spawn.maxLiveWorkers` | `4` | Maximum live worker entries allowed per repository at once. |
40
+ | `spawn.staleMs` | `15000` | Roster entries with no catalog activity after this age are marked stale and block merge (0 disables). |
41
+ | `spawn.maxWaitMs` | `90000` | Upper bound for tower_spawn waitMs (default 90s, Kimi-compatible). |
42
+ | `spawn.requireLease` | `` | Optional lease name tower_spawn must hold before starting a worker. |
43
+ | `send.maxPerMinute` | `120` | Per-repository tower_send rate limit per minute (0 disables). |
44
+ | `send.interruptDefault` | `false` | Default value of tower_send.interrupt when the caller omits it. |
45
+ | `tests.enabled` | `true` | Run declared test discovery when a mission configures it. |
46
+ | `tests.admission` | `"off"` | Run discovery before spawning a worker, when completing, or only on explicit tower_test (default off: repository code runs only on an explicit tower_test). |
47
+ | `tests.timeoutMs` | `60000` | Per discovery command timeout. |
48
+ | `tests.maxBytes` | `16777216` | Maximum captured stdout bytes per command. |
49
+ | `tests.autoChecks` | `true` | Attach a tests check to the completion report when discovery runs. |
50
+ | `tests.allowPackageDiscovery` | `true` | Allow package.json tower.testDiscovery commands from the mission worktree (executes repository code; see security docs). |
51
+ | `gates.requireChecks` | `false` | Require at least one completion check (tower_complete.checks) before a mission can merge. |
52
+ | `gates.requireTests` | `false` | Require a passing test-discovery receipt for the reviewed worktree tip before merge. |
53
+ | `gates.allowReviewExemption` | `false` | Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers. |
54
+ | `docs.board` | `true` | Write a human-readable MISSIONS.md after mission changes. |
55
+ | `docs.boardLocation` | `"common"` | common = <git-common-dir>/dsh-tower/MISSIONS.md; repo = <repoRoot>/MISSIONS.md. |
56
+ | `tools.exposeOperational` | `true` | Master switch for batch/resource/import/legacy/recover/test/doctor tools. |
57
+ | `tools.exposeBatch` | `true` | Register tower_batch. |
58
+ | `tools.exposeImport` | `true` | Register tower_import_legacy/tower_legacy_map. |
59
+ | `tools.exposeLeases` | `true` | Register tower_resource advisory leases. |
60
+ | `tools.exposeTests` | `true` | Register tower_test; gates.requireTests requires it. |
61
+ | `tools.exposeRecovery` | `true` | Register tower_recover (stale state-lock escape). |
62
+ | `tools.exposeDoctor` | `true` | Register tower_doctor (read-only health contract). |
63
+ | `leases.ttlMs` | `900000` | Default advisory lease lifetime; expiry never auto-releases. |
64
+ | `dev.hotReload` | `true` | Register tower_reload and /tower reload when the DSH HMR service is available. |
65
+ | `dev.reloadDelayMs` | `50` | Delay before a scheduled reload, so the tool result is delivered first. |
66
+ | `dev.reloadRequiredIds` | `["tower-bundle","tower","tower-skills","preset-tower-worker","preset-tower-reviewer"]` | Loader entry ids passed to reconcileProfile as required. |
@@ -41,11 +41,13 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
41
41
  | `allowAgentControl` | `true` (alias of `mode.allowAgentControl`) | Legacy alias: register `tower_control` so the model may toggle the mode. |
42
42
  | `init.autoGitInit` | `false` (bundled patch: `true`) | `tower_init` may run `git init` + an empty initial commit when `repoRoot` is an absolute, existing directory that is not a repository. |
43
43
  | `init.snapshotDirty` | `false` | `tower_init` may snapshot uncommitted **tracked** changes into a dangling commit used as the mission baseline (untracked files are not included; the commit is kept alive under `refs/tower/snapshot/*`). |
44
+ | `init.autoExclude` | `true` | Append in-repo artifacts (`MISSIONS.md`, in-repo worktree roots) to `.git/info/exclude`; never touches `.gitignore`. |
44
45
  | `mode.promptSection` | `''` | Replacement prompt text (takes precedence over `section` when both set). |
45
46
  | `mode.sectionAppend` | `''` | Extra text appended after the reminder (team conventions). |
46
47
  | `mode.allowAgentControl` | `true` | Register `tower_control`; the model may enter/leave with a reason. Set `false` for human-only mode control. |
47
48
  | `mode.autoPauseGoal` | `true` | Pause an active goal on entry; the goal is never auto-resumed on exit. |
48
49
  | `mode.requireResumeAuthorization` | `false` | Resuming a mission paused with `pauseCause:user` requires an `authorization` reference. |
50
+ | `mode.requireHumanAckOnFirstEntry` | `false` | Require a human `/tower ack` before the model may make the first Tower-mode entry in a session. |
49
51
  | `mode.workspaceLock` | `false` (bundled patch: `true`) | Allow only one Tower session per repository (`tower.lock`); others get `workspace-busy`. |
50
52
  | `mode.workspaceLockScope` | `all` | `all` locks every repo tool; `mutations` skips the workspace lock for read-only tools. |
51
53
  | `mode.planModePolicy` | `clear` | Tower entry clears an active plan mode (or `refuse` until the plan is exited); a pre-step also clears a plan activated later. |
@@ -83,10 +85,11 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
83
85
  | `gates.allowReviewExemption` | `false` | With `review.requireReview:true`, allow `reviewPolicy:"none"` + a non-empty reason to skip review blockers. Off deliberately; irrelevant while the global review gate is off. |
84
86
  | `docs.board` | `true` | Write `MISSIONS.md` after mutations. |
85
87
  | `docs.boardLocation` | `common` | `common` = `<git-common-dir>/dsh-tower/MISSIONS.md`; `repo` = `<repoRoot>/MISSIONS.md`. |
88
+ | `tools.exposeOperational` | `true` | Register batch/resource/import/legacy/recover/test/doctor tools; `false` shrinks the fixed tool-schema context. |
86
89
  | `leases.ttlMs` | `900000` | Default advisory lease lifetime; expiry never auto-releases. |
87
90
  | `dev.hotReload` | `true` | Register `tower_reload`; `/tower reload` is disabled when off. It returns `reload-unavailable` when the profile has no HMR service. |
88
91
  | `dev.reloadDelayMs` | `50` | Delay before a scheduled reload so the tool result is delivered first. |
89
- | `dev.reloadRequiredIds` | four Tower loader ids | Entry ids passed to `reconcileProfile` as required. |
92
+ | `dev.reloadRequiredIds` | five Tower loader ids | Entry ids passed to `reconcileProfile` as required. |
90
93
 
91
94
  ## Example
92
95
 
@@ -0,0 +1,39 @@
1
+ # Error code catalog (generated)
2
+
3
+ > Generated by `npm run docs:generate:errors` from `domain/errors.js`. Do not edit by hand.
4
+
5
+ | Code | Description |
6
+ |---|---|
7
+ | `invalid-argument` | A tool argument is missing, malformed, or not allowed. |
8
+ | `repo-invalid` | repoRoot is missing, relative, or not inside a Git repository. |
9
+ | `not-initialized` | Tower is not initialized for this repository; run tower_init. |
10
+ | `mission-not-found` | No mission with that id exists. |
11
+ | `finding-not-found` | No finding with that id exists. |
12
+ | `invalid-transition` | The requested state transition is not allowed. |
13
+ | `stale-base` | The repository state changed while the operation was being prepared; retry. |
14
+ | `state-locked` | Another Tower writer holds the state lock. |
15
+ | `state-corrupt` | The Tower state file is unreadable or invalid. |
16
+ | `config-invalid` | Tower configuration is invalid. |
17
+ | `config-unknown-key` | Tower configuration has an unknown top-level key. |
18
+ | `git-failed` | A git command failed. |
19
+ | `git-timeout` | A git command timed out or exceeded its output buffer. |
20
+ | `worktree-dirty` | The mission worktree has uncommitted changes and cannot complete. |
21
+ | `spawn-unavailable` | The subagents service is unavailable. |
22
+ | `provider-missing` | No subagent provider is registered. |
23
+ | `provider-cooldown` | The provider is throttled; spawn is in a short cooldown. |
24
+ | `preset-missing` | The configured agent preset is not registered. |
25
+ | `reviewer-prerequisite` | The reviewer spawn prerequisite is not met. |
26
+ | `not-authorized` | The calling agent role may not perform this action. |
27
+ | `workspace-busy` | Another Tower session owns this repository workspace lock. |
28
+ | `worker-limit` | The repository reached spawn.maxLiveWorkers live workers. |
29
+ | `rate-limited` | The tower_send per-minute rate limit was reached. |
30
+ | `takeover-refused` | The mission already has a live worker that was not stopped first. |
31
+ | `reload-unavailable` | The DSH HMR service is not available in this profile. |
32
+ | `reload-failed` | The HMR reload failed; check the DSH log and restart if needed. |
33
+ | `tests-failed` | Declared test discovery failed or did not meet its admission requirements. |
34
+ | `tests-timeout` | Test discovery exceeded its time or output budget. |
35
+ | `lease-busy` | Another holder owns the requested advisory lease. |
36
+ | `lease-token` | The lease release token does not match the current holder. |
37
+ | `batch-invalid` | The batch request or its close evidence is invalid. |
38
+ | `legacy-invalid` | The legacy snapshot or mapping is invalid. |
39
+ | `tool-internal` | An unexpected failure; read the message and the activity log. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tool failures are thrown errors, not `{ok:false}` results (gate outcomes are the exception: see
4
4
  [blockers.md](blockers.md)). Every thrown error carries a stable code as a message prefix. This
5
- release defines 31 tool error codes.
5
+ release defines 33 tool error codes.
6
6
 
7
7
  Every `TowerToolError` also carries `retryable` (boolean), `nextAction` (string or `null`) and `details`
8
8
  (structured context or `null`) for programmatic callers; the message prefix remains the stable contract
@@ -13,7 +13,7 @@ for text-only clients.
13
13
  ```
14
14
 
15
15
  Callers can match the prefix instead of the prose. Codes are reserved for the conditions below; a
16
- message without a more specific match is reported as `tool-internal`.
16
+ plain `Error` without a structured code is reported as `tool-internal`.
17
17
 
18
18
  | Code | Meaning | Typical trigger |
19
19
  |---|---|---|
@@ -23,6 +23,7 @@ message without a more specific match is reported as `tool-internal`.
23
23
  | `mission-not-found` | No mission with that id exists. | a stale or mistyped `M9` |
24
24
  | `finding-not-found` | No finding with that id exists. | `tower_findings({id:"F99"})` |
25
25
  | `invalid-transition` | The requested state transition is not allowed. | a blocked→merged attempt; a finding transition not in the graph; a reviewer mutating findings; completing a mission with open tasks, blockers, no commits, or unread inbox messages. |
26
+ | `stale-base` | The repository state changed while the operation was being prepared; retry. | two-phase mutation revalidation |
26
27
  | `worktree-dirty` | The mission worktree has uncommitted changes and cannot complete. | staged, modified, or untracked files left behind before `tower_complete` |
27
28
  | `state-locked` | Another Tower writer holds the state lock. | concurrent mutation after the lock timeout |
28
29
  | `state-corrupt` | The Tower state file is unreadable or invalid. | hand-edited `state.json` |
@@ -32,6 +33,7 @@ message without a more specific match is reported as `tool-internal`.
32
33
  | `git-timeout` | A git command timed out or exceeded its output buffer. | slow pre-merge hook, huge diff |
33
34
  | `spawn-unavailable` | The subagents service is unavailable. | a Host without `subagents` |
34
35
  | `provider-missing` | No subagent provider is registered. | an empty provider list |
36
+ | `provider-cooldown` | spawn is in a short cooldown after repeated retryable provider failures. | wait for `blockedUntil`, then retry |
35
37
  | `preset-missing` | The configured agent preset is not registered. | missing `tower-worker`/`tower-reviewer` preset rows, DSH not restarted |
36
38
  | `reviewer-prerequisite` | The reviewer spawn prerequisite is not met. | reviewer spawned before the mission is `completed`, or before a worktree exists |
37
39
  | `not-authorized` | The calling agent role may not perform this action. | a worker calling a tower-only tool or changing mission status/scope; a reviewer mutating findings |