dsh-tower 2.5.0 → 2.7.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 (142) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/COMPATIBILITY.md +2 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/GLOSSARY.md +6 -0
  5. package/PRINCIPLES.md +15 -7
  6. package/README.en.md +18 -16
  7. package/README.md +18 -11
  8. package/adapters/leases.js +20 -6
  9. package/adapters/legacy.js +149 -37
  10. package/adapters/stateFile.js +11 -1
  11. package/app/archive.js +30 -5
  12. package/app/briefing.js +95 -12
  13. package/app/checkCompletion.js +107 -18
  14. package/app/enrichedMissions.js +1 -1
  15. package/app/freshness.js +77 -0
  16. package/app/gate.js +26 -12
  17. package/app/merge.js +7 -4
  18. package/app/mission.js +24 -1
  19. package/app/missionContext.js +2 -1
  20. package/app/plan.js +64 -1
  21. package/app/provision.js +133 -22
  22. package/app/read.js +38 -5
  23. package/app/rebase.js +10 -8
  24. package/app/review.js +104 -61
  25. package/app/reviewCheckout.js +133 -0
  26. package/app/sandbox.js +121 -0
  27. package/app/sharedCheckout.js +90 -0
  28. package/app/spawn-prechecks.js +31 -20
  29. package/app/spawn-support.js +59 -8
  30. package/app/spawn.js +3 -0
  31. package/app/teardown.js +119 -2
  32. package/app/tower.js +34 -6
  33. package/client.js +321 -0
  34. package/cordis.patch.yml +55 -3
  35. package/docs/adr/0005-component-surface.md +58 -13
  36. package/docs/adr/0006-mission-shapes.md +306 -0
  37. package/docs/architecture/overview.md +18 -10
  38. package/docs/examples/first-mission.md +3 -2
  39. package/docs/faq.md +9 -7
  40. package/docs/guide/install-upgrade-migration.md +58 -7
  41. package/docs/guide/quickstart.md +20 -10
  42. package/docs/guide/scenarios-and-philosophy.md +35 -28
  43. package/docs/operations/runbook.md +1 -1
  44. package/docs/operations/security.md +11 -10
  45. package/docs/reference/blockers.md +20 -11
  46. package/docs/reference/component-surface.md +20 -8
  47. package/docs/reference/config.generated.md +8 -5
  48. package/docs/reference/config.md +30 -5
  49. package/docs/reference/doctor.md +7 -7
  50. package/docs/reference/errors.generated.md +1 -0
  51. package/docs/reference/errors.md +2 -1
  52. package/docs/reference/operations.md +1 -1
  53. package/docs/reference/shapes.md +123 -0
  54. package/docs/reference/surface-budget.md +53 -4
  55. package/docs/reference/testing.md +5 -3
  56. package/docs/reference/tools.generated.md +7 -7
  57. package/docs/reference/tools.md +5 -5
  58. package/docs/zh/faq.md +7 -5
  59. package/docs/zh/guide/install-upgrade-migration.md +25 -6
  60. package/docs/zh/guide/quickstart.md +17 -10
  61. package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
  62. package/docs/zh/operations/security.md +9 -8
  63. package/docs/zh/reference/blockers.md +13 -9
  64. package/docs/zh/reference/config.md +22 -5
  65. package/docs/zh/reference/errors.md +2 -1
  66. package/docs/zh/reference/testing.md +5 -4
  67. package/docs/zh/reference/tools.md +3 -3
  68. package/domain/batch.js +75 -105
  69. package/domain/collab/batch.js +30 -10
  70. package/domain/collab/spawn.js +53 -16
  71. package/domain/components.js +12 -4
  72. package/domain/config/schema.js +87 -22
  73. package/domain/contracts/outputs.js +2 -1
  74. package/domain/contracts/tool-inputs/missions.js +10 -2
  75. package/domain/errors.js +6 -0
  76. package/domain/evidence.js +283 -0
  77. package/domain/gate.js +17 -34
  78. package/domain/integration/gate.js +17 -43
  79. package/domain/integration/init.js +3 -2
  80. package/domain/integration/provision.js +51 -0
  81. package/domain/integration/rebase.js +16 -1
  82. package/domain/integration/review.js +203 -9
  83. package/domain/mission.js +16 -33
  84. package/domain/missions/completion.js +46 -8
  85. package/domain/missions/findings.js +2 -1
  86. package/domain/missions/lifecycle.js +131 -35
  87. package/domain/missions/plan.js +30 -6
  88. package/domain/missions/review.js +2 -18
  89. package/domain/planning.js +5 -5
  90. package/domain/read/missionContext.js +17 -3
  91. package/domain/read/views.js +5 -1
  92. package/domain/roles.js +222 -0
  93. package/domain/roster.js +9 -3
  94. package/domain/scope.js +51 -0
  95. package/domain/shapes/build.js +254 -0
  96. package/domain/shapes/index.js +171 -0
  97. package/domain/shapes/survey.js +445 -0
  98. package/domain/state/archive.js +28 -4
  99. package/domain/state/migrations.js +111 -4
  100. package/domain/state/schema.js +69 -7
  101. package/domain/state/semantics.js +93 -13
  102. package/domain/state.js +5 -1
  103. package/domain/summary.js +5 -11
  104. package/domain/vocabulary.js +15 -33
  105. package/host/capability-registry.js +25 -9
  106. package/host/config.js +40 -3
  107. package/host/doctor.js +20 -8
  108. package/host/preset-survey-sandbox.js +19 -0
  109. package/host/preset-survey.js +18 -0
  110. package/host/presets.js +17 -11
  111. package/host/tool-registry.js +23 -13
  112. package/locale/en.json +16 -1
  113. package/locale/preset-survey/en.json +6 -0
  114. package/locale/preset-survey/zh.json +6 -0
  115. package/locale/preset-survey-sandbox/en.json +6 -0
  116. package/locale/preset-survey-sandbox/zh.json +6 -0
  117. package/locale/zh.json +16 -1
  118. package/package.json +19 -2
  119. package/ports/state.js +8 -1
  120. package/scripts/architecture-waivers.json +3 -3
  121. package/scripts/check-components.mjs +120 -6
  122. package/scripts/check-docs.mjs +1 -1
  123. package/scripts/check-pack.mjs +5 -0
  124. package/scripts/component-surface.json +83 -29
  125. package/scripts/generate-capability-locale.mjs +5 -0
  126. package/scripts/generate-shapes-reference.mjs +171 -0
  127. package/scripts/mount-smoke/surface.mjs +3 -0
  128. package/scripts/pack-manifest.txt +22 -0
  129. package/scripts/shape-audit.mjs +361 -0
  130. package/scripts/surface-baseline.json +75 -4
  131. package/scripts/types-coverage.mjs +7 -0
  132. package/survey-sandbox.js +48 -0
  133. package/survey.js +40 -0
  134. package/tools/complete.js +5 -5
  135. package/tools/diff.js +5 -3
  136. package/tools/plan.js +1 -1
  137. package/tools/preflight.js +3 -2
  138. package/tools/review.js +1 -1
  139. package/tools/spawn.js +48 -50
  140. package/tools/status.js +8 -1
  141. package/tools/test.js +2 -1
  142. package/types/index.d.ts +76 -5
@@ -0,0 +1,306 @@
1
+ # ADR 0006: Mission shapes
2
+
3
+ - Status: accepted
4
+ - Date: 2026-11-10
5
+ - Ref: #148, #149
6
+
7
+ ## Context
8
+
9
+ `kind` was a column on one mission record, but it carried three contracts: a build
10
+ owns a branch and a worktree, is reviewed at its tip, and merges; a survey only
11
+ reads one commit. The state document could describe both shapes at once, so the
12
+ generic layers kept asking `kind === 'survey'` and the answer leaked into
13
+ completion (`commit >= 1`), the merge gate (`read-only-survey`), archive and
14
+ reanchor terminality, planning's open-work count, and provisioning. A survey
15
+ that completed could not be archived, blocked reanchor forever, counted as open
16
+ work, and paid for a full checkout while consuming the live-worker budget.
17
+
18
+ ## Decision
19
+
20
+ `domain/shapes/` is the one registry. Each shape module owns its statuses,
21
+ transitions, terminality, merge/rebase permission, provisioning, plan fields,
22
+ completion invariants, dependency satisfaction, briefing and summary projection.
23
+ `domain/shapes/index.js` is the only way a generic layer resolves a shape:
24
+ `shapeFor(mission.kind)`, `isTerminal(kind, status)`, `canTransition(kind, from,
25
+ to)`, and `assertActionSupported(mission, action)` for merge/rebase/preflight/diff.
26
+ The global `TERMINAL_STATUSES`/`TRANSITIONS` vocabulary and the one-argument
27
+ `isTerminal`/`canTransition` entry points are deleted.
28
+
29
+ Schema version 4 is a TypeBox union: a build mission keeps its worktree/branch and
30
+ merge fields; a survey carries `readCommit` and `readDirty` and may not carry
31
+ `worktree`, `branch`, `baseCommit`, `mergedCommit` or `mergedAt`. `merged` is not
32
+ a survey status. Migration v3→v4 is pure, one-way and idempotent: it stamps every
33
+ mission with a shape, reads a legacy survey's commit from
34
+ `worktree.baseCommit ?? baseCommit`, drops its branch fields, and maps a legacy
35
+ `merged` survey to `completed`.
36
+
37
+ Read-only is structural, not a gate: `tower_plan({kind:'survey'})` refuses while
38
+ the `tower-survey` component row is off; a survey spawn uses the shared checkout
39
+ and the `tower-survey` preset allow-list; `tower_complete` checks that HEAD still
40
+ equals `readCommit`, that the tracked-status set matches the set captured at plan
41
+ time, and that zero commits were made.
42
+
43
+ `scripts/shape-audit.mjs` enforces the decision: no kind or role literal
44
+ comparison outside `domain/shapes/` and `domain/roles.js`, no global status
45
+ vocabulary, no one-argument entry points. Waivers are justified data in
46
+ `scripts/architecture-waivers.json` under `shapeWaivers`; an unjustified or stale
47
+ waiver fails. `docs/reference/shapes.md` is generated from the registry.
48
+
49
+ ## Surface budget
50
+
51
+ The shape redesign grows the config surface by exactly one key, recorded in
52
+ `scripts/surface-baseline.json`:
53
+
54
+ - `spawn.surveyModel` (tuning): an optional independent model route, mirroring
55
+ `spawn.workerModel`/`spawn.reviewerModel`.
56
+
57
+ An earlier cut of this work also added `spawn.surveyPreset` (core) and
58
+ `survey.sandbox` (tuning) in a new `survey` section. Review contracted the
59
+ surface back before release (config 57 → 55, sections 15 → 14): a shape owns its
60
+ preset (`SHAPES.survey.spawn.defaultPreset`), so a `spawn.surveyPreset` key could
61
+ only disagree with the registry; and `survey.sandbox` advertised a detached
62
+ read-only checkout that W1 does not implement. Neither is a tuning knob a user
63
+ should see; the completion invariants (HEAD, tracked-status fingerprint, zero
64
+ commits) are the read-only guarantee, not a placement option.
65
+
66
+ The `tower-survey` row is a visible optional component (components 11 → 12): off
67
+ means surveys cannot be planned (an actionable code) and no survey agent can be
68
+ dispatched; build missions are unaffected. It is a declarative preset row: the
69
+ host resolves its availability through the async `agentPresets` probe, not the
70
+ process-local code registry.
71
+
72
+ ## Rejected alternatives
73
+
74
+ - Keep `kind` on one record and guard each leak with another `kind === 'survey'`
75
+ check. Rejected: that is exactly the census in #149; the state document would
76
+ still be able to describe both shapes and every new layer would re-learn the
77
+ rule.
78
+ - Add a `shape: 'build'|'survey'` flag selecting an old and a new code path during
79
+ a transition period. Rejected: a dual-shape path lets a document carry an
80
+ impossible combination (a survey with a worktree, a merged survey) and makes the
81
+ migration untestable.
82
+ - Keep `commit >= 1` and make a survey commit an empty commit. Rejected: a survey
83
+ is a read, so the empty commit records nothing and hides that HEAD moved.
84
+ - Enforce read-only at merge time (`read-only-survey` in the gate). Rejected: the
85
+ write already happened by then; read-only must be structural (preset allow-list)
86
+ and checked as a completion invariant.
87
+ - Give a survey its own branch so the existing lifecycle "just works". Rejected:
88
+ it reintroduces provisioning cost, branch ownership, stale-base and merge
89
+ vocabulary for a kind that never merges.
90
+
91
+ ## Consequences
92
+
93
+ - `kind` is now a closed vocabulary with one registry; adding a kind means adding
94
+ a shape module and regenerating `docs/reference/shapes.md`, not editing generic
95
+ layers.
96
+ - Older v3 documents migrate on load; a legacy survey's `worktree` is removed by
97
+ a bounded-retry teardown and its completion with commits is accepted as history.
98
+ - A completed survey is terminal, archivable, no longer open work, and no longer
99
+ blocks reanchor or the live-worker budget.
100
+ - Surveys can be planned only while the `tower-survey` row is on, and they cannot
101
+ be merged, rebased, preflighted or diffed: each refuses with `invalid-transition`
102
+ and an actionable next step.
103
+ - The shape audit is part of `npm run check`; a new kind or role comparison in a
104
+ generic layer fails CI unless a justified waiver is recorded.
105
+
106
+ ## Addendum: W2 detached sandbox and dependency semantics (#148)
107
+
108
+ W1 implemented the shared-checkout survey only. W2 adds the placement it left
109
+ out, without changing the default or the shared path:
110
+
111
+ - `survey.sandbox` (`none`|`detached`, default `none`) is tuning, recorded in
112
+ `scripts/surface-baseline.json` (config keys 55 → 56, sections 14 → 15). The
113
+ shape resolves the placement (`resolveProvisioning`) and the preset
114
+ (`resolvePreset`); no generic layer reads the key or compares a kind.
115
+ `spawn.surveyPreset` stays removed because the preset follows the registry and
116
+ the resolved placement.
117
+ - `detached` provisions `git worktree add --detach <worktreeRoot>/<id>-sandbox
118
+ <readCommit>` with no branch and records `mission.sandbox`. It never records
119
+ `baseCommit`, never participates in stale-base, review or merge, and the
120
+ completion invariant is sandbox HEAD === `readCommit` plus zero commits.
121
+ Expected write/shell side effects in the disposable checkout are ignored;
122
+ the shared placement keeps the tracked-status fingerprint invariant.
123
+ - The sandbox preset (`tower-survey-sandbox`) allows read/write/edit/pwsh plus
124
+ the mission tools and still denies spawn/merge/rebase/review/diff. Both survey
125
+ presets are internal children of the one visible `tower-survey` group row, so
126
+ the component count does not move (12) and turning the capability off removes
127
+ both.
128
+ - Cleanup is advisory: completion, abandon and `tower_teardown` remove the
129
+ sandbox with a bounded retry. A failure keeps `mission.sandbox`, records
130
+ `sandboxCleanupError` and names the `git worktree remove --force <path>`
131
+ remedy in the activity log; the transition itself is not rolled back. Archival
132
+ defers a mission whose sandbox is still present instead of moving the only
133
+ record of the path out of hot state.
134
+ - `gates.requireFreshSurvey` (default `false`) controls dependency freshness. A
135
+ build depending on a survey whose `readCommit` is not ancestor-or-equal of the
136
+ build base gets a `survey-stale` warning in plan/preflight with
137
+ `{code,missionId,depId,readCommit,baseCommit,message}`; only when the gate is
138
+ `true` does the same code block the merge. The ancestor check is the existing
139
+ `isAncestor` (`git merge-base --is-ancestor`, which is true for equal commits).
140
+ - A build briefing carries each survey dependency's recorded `result`
141
+ (`paths`/`symbols`/`plan`/`notes`) with bounded formatting, so the dependent
142
+ worker sees the evidence rather than only the terminal status. `result`
143
+ remains survey-only at completion.
144
+ - Placement is decided at provisioning time, not at completion time: a recorded
145
+ `mission.sandbox` wins over current `survey.sandbox` config for provisioning,
146
+ preset and completion invariants. Flipping the key after a sandbox was created
147
+ therefore cannot skip the sandbox HEAD/zero-commit checks or strand the
148
+ disposable checkout, and a survey that never got a sandbox keeps the shared
149
+ invariants.
150
+ - The archive index keeps a survey's `readCommit` and bounded `result`, so an
151
+ archived survey dependency still carries its evidence into a build briefing
152
+ and `gates.requireFreshSurvey` cannot fail open after archiving (#73).
153
+
154
+ ## Addendum: W3 review, batch, leases and paused-vs-blocked (#149)
155
+
156
+ W3 audited four state dimensions that are not mission kinds. None became a
157
+ shape: each has one owning module, and each verdict names a fitness rule that
158
+ `npm run shape:audit` enforces (generated into `docs/reference/shapes.md`).
159
+
160
+ - **Review — commit-stamped evidence (`app/review.js`, `app/reviewCheckout.js`,
161
+ `domain/missions/review.js`).** A verdict is evidence about a commit, not about
162
+ a mutable worktree. `tower_spawn` provisions a disposable detached checkout of
163
+ the mission branch tip for the reviewer and records `reviewCommit` /
164
+ `reviewCheckout` on the roster row. `tower_review` stamps `reviewedCommit` with
165
+ that recorded commit, refuses if the checkout HEAD moved, and the merge gate
166
+ compares the stamp to the current branch tip with the existing `tip-moved`
167
+ blocker. `tower_diff` shows a reviewer the same recorded commit. The old
168
+ after-the-fact "reviewer modified the branch" check (`assertReviewerSpawnTip`)
169
+ is deleted: a stale worker write can no longer affect what a review decided.
170
+ The checkout is removed when the verdict is recorded and retried by
171
+ `tower_teardown`. A tower break-glass verdict stamps the current branch tip
172
+ because there is no checkout. Fitness rule: `review-tip-read`.
173
+ - **Batch — correctly generic second lifecycle (`domain/batch.js`,
174
+ `domain/collab/batch.js`, `app/batch.js`).** A batch is finite evidence over
175
+ missions, not a mission kind, so it stays generic — but its second state
176
+ machine is now owned by one module: `BATCH_STATUS`, `BATCH_TRANSITIONS`
177
+ (`open → closed | abandoned`), `canTransitionBatch`, `isBatchTerminal` and
178
+ `normalizeBatchStatus` (legacy `created`/`active`/`pending` read as `open`).
179
+ `createBatch`/`closeBatch`/`abandonBatch` share one guarded transition and
180
+ state semantics validates the stored status, so an unknown or invented batch
181
+ status fails as state corruption. Receipt verification is unchanged. Fitness
182
+ rule: `batch-status-literal`.
183
+ - **Leases — advisory coordination, never authority (`adapters/leases.js`,
184
+ `app/spawn-prechecks.js`).** Lease acquisition is not OS-enforced exclusivity,
185
+ and the docs say so. There is exactly one enforcement point: the fail-closed
186
+ spawn admission when `spawn.requireLease` names a lease, which refuses a
187
+ missing lease and a lease held by another actor with an actionable remedy. The
188
+ prior contract deadlocked: an expired lease held by a crashed process could
189
+ never be released because a token mismatch was refused even with `force`. Now
190
+ `force: true` releases an **expired** lease (explicit crash recovery) while a
191
+ live lease still refuses a foreign token, and `acquire` never takes over
192
+ automatically. Fitness rule: `lease-ledger`.
193
+ - **Paused vs blocked — two distinct stopped semantics
194
+ (`domain/missions/lifecycle.js`, `domain/state/semantics.js`).** `blocked` is
195
+ derived machine state: it means "open blockers exist". `paused` is a deliberate
196
+ stop with a `pauseCause` (`user`|`failure`|`resource`|`dependency`). They never
197
+ coexist: a recorded blocker clears a pause and moves the mission to `blocked`;
198
+ `clear_blockers` clears them and resumes the mission (to `active`, or to
199
+ `completed` in the same patch); pausing or completing is refused while blockers
200
+ are open; `status: "blocked"` is refused and machine writers go through
201
+ `markBlocked(mission, text)` (merge/rebase conflicts use it). The state
202
+ invariant `status === "blocked" ⇔ open blockers exist` (for non-terminal
203
+ missions) is checked on every load/validation, and migration v4→v5 normalizes
204
+ recorded states: a blocked mission without a blocker gets an explicit legacy
205
+ blocker, blockers under another non-terminal status become `blocked`, and a
206
+ superseded `pauseCause` is removed. It is not a per-kind rule: both shapes
207
+ declare `blocked`/`paused` in their own transition tables. Fitness rule:
208
+ `blocked-status-write`.
209
+ - Because the stopped-status invariant is new, the persisted schema version is
210
+ now **5** (`SCHEMA_VERSION = 5` in `domain/mission.js`); the v4→v5 migration is
211
+ pure and idempotent like its predecessors.
212
+ - The audit grew four rules (`review-tip-read`, `batch-status-literal`,
213
+ `lease-ledger`, `blocked-status-write`) with mutation tests proving each rule
214
+ fails on a seeded violation. `scripts/shape-audit.mjs` exports `RULES`, and
215
+ `scripts/generate-shapes-reference.mjs` refuses to generate if a verdict names
216
+ a rule the audit does not run.
217
+
218
+ ## Addendum: W4 evidence, legacy, scope and role-vs-kind (#149)
219
+
220
+ W4 audited the four remaining dimensions. Each had no single owner; each got a
221
+ written verdict, one owning module, and a fitness rule that `npm run shape:audit`
222
+ enforces (generated into `docs/reference/shapes.md`). No dimension became a
223
+ per-kind conditional and no second shape table was added.
224
+
225
+ - **Test receipts / evidence — redesigned to one reader (`domain/evidence.js`).**
226
+ The same question ("did tests run?") previously had four answers:
227
+ `mission.completionReport.checks`, a synthetic `tests` check that
228
+ `tower_complete` copied from discovery via `tests.autoChecks`,
229
+ `mission.testRuns`, and a separate `gates.requireTests` filter that re-read
230
+ receipts. `domain/evidence.js` now owns the normalized record
231
+ `{names, name, source, ok, command, exitCode, evidence, treeTip, entry}` with
232
+ sources `completionReport` | `testRuns` | `input`, aliases `tests`/`tower_test`
233
+ carried in `names`, and one ordering (completion checks first, then newest
234
+ receipts, then caller checks scoped to the mission) applied by `findEvidence` —
235
+ precedence and ordering are reader decisions, not fields on the record.
236
+ `evidenceEntries`, `findEvidence`, `passingDiscovery`,
237
+ `hasCheckEvidence` and `hasTestReceipts` are the only readers; the shapes
238
+ declare the contract (`build.evidence = {completionChecks: true, testReceipts:
239
+ "tip-bound"}`, `survey.evidence = {completionChecks: true, testReceipts:
240
+ false}`) because only a shape with a branch has a tip to bind a receipt to.
241
+ `gates.requireTests` uses `passingDiscovery`; completion blockers use
242
+ `hasCheckEvidence`; `tower_batch` validation/summary use `findEvidence`.
243
+ `tower_complete` no longer copies a `tests` check into the report and
244
+ `tower_test` returns the receipt only, so the config key `tests.autoChecks`
245
+ disappears (config surface 57 → 56, recorded in `scripts/surface-baseline.json`
246
+ and `docs/reference/surface-budget.md`). Fitness rule:
247
+ `evidence-single-read`, owned by `domain/evidence.js`; mutation tests seed a
248
+ completion-check read, a `mission.testRuns` read and prove the owner module is
249
+ exempt.
250
+ - **Legacy import — redesigned to a strict adapter contract
251
+ (`adapters/legacy.js`).** Import was approximate in four ways: the format was
252
+ inferred from the first mission id (mixed namespaces were only caught
253
+ incidentally), an absent `kind` was silently stamped `build`, a Tower Codex
254
+ `retired` status leaked into the mission summary as if it were a DSH status,
255
+ and batch exclusion read several hand-written snapshot shapes
256
+ (`missions`/`snapshot.missions`/`payload.missions`/`pausedIds`). The adapter now
257
+ exports `LEGACY_FORMATS`: one declared entry per accepted format (id pattern,
258
+ id hint, input statuses, `statusRemap`, `baseBranchField`,
259
+ `requiresBaseBranch`). Detection requires exactly one id namespace (a declared
260
+ `payload.format` is authoritative and validated against the ids); a Codex
261
+ `retired` status is remapped to `abandoned` and reported in `remapped`; an
262
+ absent kind is reported in `unresolved` instead of silently invented; mission
263
+ fields the summary does not carry are reported in `dropped` with the reason
264
+ that the exact bytes remain in the stored snapshot. The persisted snapshot
265
+ record gains `unresolved`, `droppedFields` and `remapped`, so the honesty
266
+ survives a restart. `domain/batch.js` reads only the canonical
267
+ `pausedMissionIds`. The import remains read-only, byte-exact,
268
+ hash-addressed and idempotent: it never writes `state.json`, never activates,
269
+ adopts or resumes a mission, and a paused mission stays paused. Fitness rule:
270
+ `legacy-vocabulary`, owned by `adapters/legacy.js`; mutation tests prove a
271
+ second `tower-codex`/`pausedIds` reader fails outside the adapter.
272
+ - **Scope ownership — redesigned to `shape.reach` (`domain/scope.js` +
273
+ shapes).** The shape flag `ownsScope` lived beside three enforcers (the
274
+ integration gate, the planner's auto-dependencies, and lifecycle scope
275
+ widening), each of which read `mission.scope` itself. The shape now declares
276
+ `reach = { ownsPaths, fence: "scope" | "read-only" }`: a build owns paths and
277
+ fences changed files against its scope; a survey reaches nothing and is fenced
278
+ read-only. `domain/scope.js` exports one `scopeFenceBlocker({ mission, reach,
279
+ changedFiles, caseInsensitive })`; `domain/integration/gate.js` derives the
280
+ reach from `shapeFor(mission.kind)` and re-exports the existing
281
+ `outOfScopeBlocker` shape; `domain/missions/plan.js`,
282
+ `domain/planning.js` and `domain/missions/lifecycle.js` derive
283
+ `reach.ownsPaths`. `ownsScope` is deleted. Fitness rule: `scope-reach`, owned
284
+ by `domain/scope.js`; mutation tests seed an `ownsScope` reader and a direct
285
+ `fileInScope` call.
286
+ - **Role vs kind — redesigned to an orthogonal role contract
287
+ (`domain/roles.js`).** Spawn resolved a reviewer by combining
288
+ `isReviewerRole(role)` with `shape.fields.readCommit`/`shape.canMerge`, and
289
+ presets, prechecks, placement, briefings and the roster each re-derived role
290
+ facts independently. `domain/roles.js` now owns `ROLE_CONTRACT`, one entry per
291
+ actor role with `preset`, `model`, `placement`, `briefing`, `tracksMission`,
292
+ `countsTowardBudget`, `spawnDiscovery` and `requires` (review, canMerge,
293
+ worktree, status set). `resolveRoleSpawn` returns the preset/model route,
294
+ `roleMissionBlockers` returns ordered prerequisites, and
295
+ `roleCountsTowardBudget`, `roleTracksMission`, `rolePlacement`,
296
+ `roleBriefingKind` and `rolePresetConfig` replace the scattered checks. The
297
+ shape supplies only facts (`canMerge`, `fields.worktree`, `spawn.model`);
298
+ `tools/spawn.js`, `app/spawn-prechecks.js`, `app/spawn-support.js`,
299
+ `domain/roster.js` and `host/presets.js` call the role API and never combine a
300
+ role predicate with a shape decision. Fitness rule: `role-shape-conflation`,
301
+ owned by `domain/roles.js`; mutation tests seed a role∧shape conditional and
302
+ prove the owner module is exempt.
303
+ - The audit now runs 12 rules. The generated reference's `Dimension inventory
304
+ (#149)` lists every audited dimension with its verdict, owner module and
305
+ fitness rule; `scripts/generate-shapes-reference.mjs` still refuses to emit a
306
+ rule id the audit does not run.
@@ -12,9 +12,14 @@ escape keep working.
12
12
 
13
13
  [Principle 11](../../PRINCIPLES.md) and
14
14
  [ADR 0005](../adr/0005-component-surface.md) say a component may exist only for
15
- a capability whose off state leaves the rest intact, and that the host, health,
16
- recovery, review and merge-gate capabilities are never switches. Which keys are
17
- allowed to be switches is a table lookup in
15
+ a capability whose off state leaves the rest intact, and that the host, the
16
+ health contract, the recovery escape and the merge gates are never switches.
17
+ The review policy is not one of them: `review.mode` is a single tri-state
18
+ control (`off`, `optional` or `required`) on the plugin-page configuration
19
+ form, not a component row and never one row per mode, and only the review gate
20
+ it turns on in `required` mode — the clean, tip-bound evidence and the
21
+ exemption path — is non-disableable. Which keys are allowed to be switches is a
22
+ table lookup in
18
23
  [component-surface.md](../reference/component-surface.md), generated from
19
24
  `scripts/component-surface.json`; the component count is baselined and checked by
20
25
  `npm run docs:check:components`. That gate also refuses a `tools.expose*` route
@@ -221,12 +226,14 @@ planned ──spawn worker──▶ active ──tower_complete──▶ complet
221
226
  │ │ ▲ │
222
227
  │ │ └──── review round not clean ────┐
223
228
  └── abandon ──▶ abandoned│ │
224
- ├── blocker ──▶ blocked ──active/completed──▶ ...
229
+ ├── blocker ──▶ blocked ──clear_blockers──▶ active
225
230
  └── pause ─────▶ paused ──active──▶ ...
226
231
  ```
227
232
 
228
- `merged` and `abandoned` are terminal. `tower_mission` may set `blocked → completed`; `merged` is only
229
- set by `tower_merge`/reconciliation.
233
+ `merged` and `abandoned` are terminal. `blocked` is derived: it holds exactly while the mission has an open
234
+ blocker, `clear_blockers` resumes it to `active`, and `tower_mission` refuses to set `status: "blocked"` directly.
235
+ `paused` is a deliberate stop (`pause_cause`); a blocker supersedes a pause. `merged` is only set by
236
+ `tower_merge`/reconciliation.
230
237
 
231
238
  ## Gate pipeline (`tower_preflight`)
232
239
 
@@ -236,9 +243,10 @@ set by `tower_merge`/reconciliation.
236
243
  3. Survey missions must not change files; build missions must stay inside `scope`.
237
244
  4. The main checkout must be clean for the files this merge would overwrite and on the base branch.
238
245
  5. Dependency missions must be merged/abandoned, except a completed survey dependency which is its terminal success state (#107).
239
- 6. If `review.requireReview:true` (default false), a review must exist, belong to this mission, be
240
- clean, approve merge, and be bound to the tip (unless a clean rebase waived it). No worker may be
241
- running.
246
+ 6. If the review policy is `required` (`review.mode:required`; the default is `optional`), a review
247
+ must exist, belong to this mission, be clean, approve merge, and be bound to the tip (unless a clean
248
+ rebase waived it). With `off`, review is intentionally unavailable and the gate ignores it. No
249
+ worker may be running.
242
250
 
243
251
  ## Merge
244
252
 
@@ -255,7 +263,7 @@ After the command, regardless of exit status, it inspects `HEAD`:
255
263
  | Path | Contents |
256
264
  |---|---|
257
265
  | `<git-common-dir>/dsh-tower/state.json` | active missions, roster, reviews, findings, messages, reviews sequence |
258
- | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
266
+ | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps, survey `readCommit` and bounded `result`) |
259
267
  | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
260
268
  | `<git-common-dir>/dsh-tower/activity.jsonl` | append-only activity |
261
269
  | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (heartbeat reclaim; a recycled PID cannot pin it) |
@@ -23,8 +23,9 @@ Wait for the worker's final report (delivered to this session) or check `tower_r
23
23
  creates `src/greet.txt`, commits on `tower/m1-add-greeting`, marks both tasks done, and calls
24
24
  `tower_complete`.
25
25
 
26
- This example follows the strict workflow: enable `review.requireReview:true` in the profile or
27
- repository config before the review step (default is optional review). Spawn the reviewer:
26
+ This example follows the strict workflow: set the review policy to `required` (`review.mode:required`
27
+ in the profile or repository config, or the one tri-state control on the Plugins page) before the
28
+ review step; the default is `optional`. Spawn the reviewer:
28
29
 
29
30
  ```json
30
31
  {"repoRoot":"D:\\work\\demo","missionId":"M1","role":"reviewer"}
package/docs/faq.md CHANGED
@@ -14,9 +14,9 @@ phase instead—use Workflow/Team to analyze and decide, then start Tower to exe
14
14
  review) the repository change. See the [full guide](guide/scenarios-and-philosophy.md#4-how-tower-differs-from-other-dsh-workflow-patterns).
15
15
 
16
16
  **Why did `tower_preflight` fail with `no-review` right after the worker finished?**
17
- That happens only when `review.requireReview:true` is enabled: a completed mission awaits an
18
- independent review. Spawn a reviewer; after a clean review the gate turns green. With the default
19
- `review.requireReview:false`, review is optional and no `no-review` blocker is produced.
17
+ That happens only when the review policy is `required` (`review.mode:required`): a completed mission
18
+ awaits an independent review. Spawn a reviewer; after a clean review the gate turns green. With the
19
+ default `review.mode:optional`, review is optional and no `no-review` blocker is produced.
20
20
 
21
21
  **Do I need to poll the worker?**
22
22
  No. The worker's final report is delivered to this session; `tower_roster` shows live activity. Polling
@@ -40,10 +40,12 @@ Correct: the branch is already contained in the base. Call `tower_merge` to reco
40
40
  instead of rebasing.
41
41
 
42
42
  **Can I require independent review?**
43
- Yes. Set `review.requireReview:true` in the profile or repository config to enable the global review
44
- gate (default is `false`, review optional). When enabled, mission-level `reviewPolicy:"none"` + a
45
- reason + `gates.allowReviewExemption:true` is the narrower exemption. `tower_review` works and records
46
- verdicts either way.
43
+ Yes. Set the review policy to `required` — the one tri-state control on the Plugins page, or
44
+ `review.mode:required` in the profile or repository config (the default is `optional`). When required,
45
+ mission-level `reviewPolicy:"none"` + a reason + `gates.allowReviewExemption:true` is the narrower
46
+ exemption. `tower_review` works and records verdicts in `optional` mode too. The old boolean toggle is
47
+ a deprecated one-release alias; the mapping is in
48
+ [install, upgrade and migration](guide/install-upgrade-migration.md).
47
49
 
48
50
  **Can a reviewer edit files?**
49
51
  No. Reviewers use the read-only `tower-reviewer` preset (no write/edit/pwsh); a branch changed after the
@@ -29,6 +29,55 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
29
29
 
30
30
  ## Migration notes
31
31
 
32
+ - **2.6.0 → 2.7.0 (#148/#149) — mission shapes, survey tasks, and state schema v5**: a completed
33
+ survey is a read of a commit, not a write of a branch. `tower_plan({kind:"survey"})` records
34
+ `readCommit` and `readDirty`, creates no branch or worktree, and completes only while HEAD and the
35
+ tracked-file status still match that read. The optional `survey.sandbox: "detached"` mode gives a
36
+ writable inspection a disposable worktree pinned to the same commit. `tower-survey` is an optional
37
+ component row; survey presets are read-only by default, and surveys do not count against
38
+ `spawn.maxLiveWorkers`.
39
+ - **State migration is automatic and one-way.** The first write after upgrading migrates
40
+ `state.json` from v3 to v5 in one step, writes `state.json.v3.bak` first, advances `revision`
41
+ once, and logs `state.migrated` with steps v3→v4 and v4→v5. Legacy survey rows gain
42
+ `readCommit`/`readDirty` (anchored at `state.base` when the old row never started), and build
43
+ rows are stamped `kind:"build"`. Read-only loads migrate in memory without rewriting the file.
44
+ A newer document is still refused with `state-version-unsupported` and never downgraded.
45
+ - **Config key count 57 → 56.** `tests.autoChecks` is gone; test evidence is now the
46
+ `mission.testRuns` receipt read by `domain/evidence.js`. A config that still writes the key is
47
+ rejected as `config-unknown-key` with this note; there is no replacement switch.
48
+ - **`blocked` is derived.** Write `status:"blocked"` through `tower_mission` is refused; use
49
+ `blocker` to report and `clear_blockers` to resolve. Batch statuses are canonical
50
+ `open → closed|abandoned`; an expired foreign lease requires explicit `force` to recover.
51
+ - **Survey actions are explicit.** `tower_merge`, `tower_rebase`, `tower_preflight`, and
52
+ `tower_diff` refuse surveys with `invalid-transition`; a completed survey is terminal for
53
+ planning, archive, dependency satisfaction, and reanchor. `tower_complete` accepts an optional
54
+ structured `result` (`paths`, `symbols`, `notes`, `plan`) for surveys only.
55
+ - Reviewers are now spawned into a detached checkout bound to the recorded commit; a verdict is
56
+ tied to that commit, and teardown cleans the checkout. Existing build workflows are unchanged.
57
+ - No npm dist-tag or review-policy change is required. Restart DSH after upgrading.
58
+ - **2.5.0 → 2.6.0 (#M39/#M40) — review modes replace the boolean**: the review policy is one
59
+ tri-state control on the Plugins page in the `dsh-tower` configuration section, and the config key
60
+ is `review.mode`: `off`, `optional` (the default) or `required`.
61
+ - `off` is supported and documented: `tower_review` is not registered, reviewer spawn is refused,
62
+ and the merge gate ignores review.
63
+ - `optional` keeps `tower_review` but never blocks a merge.
64
+ - `required` keeps the merge gate: a completed mission needs a clean review bound to the branch tip
65
+ (the existing per-mission exemption still applies).
66
+ `review.requireReview` is a deprecated one-release alias: `true` resolves to `required`, `false`
67
+ to `optional`, and leaving it unset means `optional`. A `true` next to an explicit non-`required`
68
+ mode is rejected with `config-invalid`; `false` never overrides an explicit mode. Saving from the
69
+ plugin page always writes `review.mode` and normalizes the alias to `false`. The policy is not a
70
+ component row and there are no mutually exclusive review rows: `preset-tower-reviewer` remains an
71
+ optional capability the policy may leave unused. The row may be off while `review.mode` is `off`
72
+ or `optional`; when `review.mode` is `required` and the reviewer row is off, planning and mode
73
+ entry refuse the combination with an explanatory `preset-missing` error.
74
+ A profile that starts with the policy `off` never registered `tower_review`; switching to
75
+ `optional` or `required` changes the merge gate immediately, but the tool appears only after a DSH
76
+ restart (or a profile reload), and the plugin page states that after such a save.
77
+ Upgrade: replace `review.requireReview: true|false` with `review.mode: required|optional`; keep the
78
+ alias only if the same config is shared with older 2.5 installations, and remove it after this
79
+ release. No state migration.
80
+
32
81
  - **2.4.2 → 2.5.0 (#131/#133/#135/#136/#142)**: three component-shaped config
33
82
  keys are gone — `tests.enabled`, `docs.board`, and `archive.enabled`. The
34
83
  `tower-tests`, `tower-board`, and `tower-archive` component rows are the only
@@ -70,14 +119,14 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
70
119
  now ships the mandatory `tower` host anchor (the Host plugin and the worker preset are its internal
71
120
  sections) plus individually switchable top-level rows: `tower-skills`, `tower-batch`, `tower-import`,
72
121
  `tower-leases`, `tower-tests`, `tower-hmr` (shipped off) and `preset-tower-reviewer` (conditional on
73
- `review.requireReview`). The `tools` config group and `dev.hotReload` were removed: a profile that
74
- still carries them is now rejected as an unknown key (`config-unknown-key`) and the host stops
122
+ the review policy, today `review.mode`). The `tools` config group and `dev.hotReload` were removed: a
123
+ profile that still carries them is now rejected as an unknown key (`config-unknown-key`) and the host stops
75
124
  mounting until they are deleted, so no silent no-op switch remains. Move each capability to its row
76
125
  in the plugin page instead; `tower-hmr` ships off, so `tower_reload` and `/tower reload` are reported
77
126
  as "disabled by component tower-hmr" until you enable it for a linked dev profile. `tower_doctor` and
78
127
  `tower_recover` are registered for every configuration and need no key. The reviewer row may be off
79
- only while `review.requireReview` is false; with the gate on, planning and mode entry refuse the
80
- combination with an explanatory error. No state migration is needed.
128
+ while the policy is `off` or `optional`; with `review.mode:required`, planning and mode entry refuse
129
+ the combination with an explanatory error. No state migration is needed.
81
130
 
82
131
  - **2.0.4 → 2.1.0-beta.1**: no config migration; the state file migrates automatically (v1/v2 → v3) as
83
132
  described above. This is a **pre-release** published on the
@@ -89,10 +138,12 @@ The Host plugin loads at DSH startup: **restart DSH after install or update**.
89
138
  runs `npm publish --tag next`, and `latest` stays on 2.0.4 until a stable release.
90
139
  - **2.0.3 → 2.0.4**: no state/config migration. This is a security-hardening and integrity release
91
140
  (authorization, merge SHA binding, dirty-worktree completion, stale-lock recovery, limited test/git
92
- process environment, `tower_recover`, UI 0.2.2). Review `review.requireReview`, `tests.admission` and
93
- worker tool permissions against your threat model after upgrading.
141
+ process environment, `tower_recover`, UI 0.2.2). Review the review policy (`review.mode`, or the
142
+ deprecated `review.requireReview` alias), `tests.admission` and worker tool permissions against your
143
+ threat model after upgrading.
94
144
  - **2.0.1 → 2.0.3**: no state migration. Independent review became **optional by default** in 2.0.3
95
- (`review.requireReview:false`); set it to `true` to keep the old review-gated merges. 2.0.1 added
145
+ (today's `review.mode:optional`); set `review.mode:required` to keep the old review-gated merges.
146
+ 2.0.1 added
96
147
  `tower_batch` `abandon`, accepted merged members in batch close, made doctor skip torn-down worktrees,
97
148
  and fixed terminal-mission spawn/preflight edge cases. The optional `ui-bundle` 0.2.1 adds the
98
149
  persistent Tower-mode chip and mode-aware denied-tool rows.
@@ -33,15 +33,25 @@ session. Use `tower_roster` to observe activity; do not poll in a loop.
33
33
 
34
34
  ## 4. Optional: review
35
35
 
36
- Independent review is optional by default. If you want the strict workflow, enable it in the profile
37
- or repository config first (`review.requireReview:true`), then:
36
+ The review policy is one tri-state control — `off`, `optional` (the default) or `required` — on the
37
+ Plugins page in the `dsh-tower` configuration section, or `review.mode` in the profile/repository
38
+ config. It is one control, not a component row and not three rows. Set it to `required` for the strict
39
+ workflow, then:
38
40
 
39
41
  ```
40
42
  tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
41
43
  ```
42
44
 
43
45
  Reviewers use the read-only `tower-reviewer` preset. They inspect `tower_diff` and submit
44
- `tower_review`; with `review.requireReview:true`, a `clean` + `merge` verdict is required to merge.
46
+ `tower_review`; with `review.mode:required`, a `clean` + `merge` verdict is required to merge.
47
+ `optional` records reviews but never blocks a merge, and `off` is a supported mode that does not
48
+ register `tower_review`.
49
+
50
+ The old boolean review toggle is a one-release alias (`true` → `required`, `false` → `optional`;
51
+ unset → `optional`). Prefer `review.mode`. A profile that started with
52
+ `off` only registers `tower_review` after a restart/reload, although the merge gate changes
53
+ immediately. See [install, upgrade and migration](install-upgrade-migration.md) for the full
54
+ migration.
45
55
 
46
56
  ## 5. Gate, merge, clean up
47
57
 
@@ -51,10 +61,10 @@ tower_merge repoRoot=D:\work\repo missionId=M1
51
61
  tower_teardown repoRoot=D:\work\repo
52
62
  ```
53
63
 
54
- With the review gate enabled, `tower_preflight` before review reports `no-review`, and after a clean
55
- review it should be `ok:true`. With the default (`review.requireReview:false`), no review blocker is
56
- required. `tower_merge` refuses while blockers remain. `tower_teardown` removes merged/abandoned
57
- worktrees and branches.
64
+ With `review.mode:required`, `tower_preflight` before review reports `no-review`, and after a clean
65
+ review it should be `ok:true`. With the default (`optional`), no review blocker is required.
66
+ `tower_merge` refuses while blockers remain. `tower_teardown` removes merged/abandoned worktrees and
67
+ branches.
58
68
 
59
69
  ```
60
70
  /tower off
@@ -63,8 +73,8 @@ worktrees and branches.
63
73
  ## If something fails
64
74
 
65
75
  Read the blocker code and use [the blocker reference](../reference/blockers.md). The most common cases:
66
- `stale-base` (run `tower_rebase`), `no-review` (only with `review.requireReview:true`; spawn a
67
- reviewer), `merge-hook-failed` (retry, or raise `timeoutMs`), `tip-moved` (only with the review gate;
76
+ `stale-base` (run `tower_rebase`), `no-review` (only with `review.mode:required`; spawn a reviewer),
77
+ `merge-hook-failed` (retry, or raise `timeoutMs`), `tip-moved` (only when the policy is `required`;
68
78
  the branch changed after review, review again).
69
79
 
70
80
  Thrown tool failures carry a `[tower:<code>]` prefix; see [error codes](../reference/errors.md)
@@ -78,7 +88,7 @@ JSON file at `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/c
78
88
 
79
89
  ```json
80
90
  {
81
- "review": { "requireReview": true, "maxRoundsPerReviewer": 3 },
91
+ "review": { "mode": "required", "maxRoundsPerReviewer": 3 },
82
92
  "merge": { "timeoutMs": 1800000 },
83
93
  "worktrees": { "root": "D:/tower-worktrees" }
84
94
  }