dsh-tower 2.1.0-beta.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMPATIBILITY.md +6 -1
  3. package/CONTRIBUTING.md +71 -5
  4. package/PRINCIPLES.md +145 -0
  5. package/README.en.md +14 -5
  6. package/README.md +14 -5
  7. package/SECURITY.md +6 -0
  8. package/adapters/archive.js +279 -0
  9. package/adapters/caseSensitivity.js +66 -0
  10. package/adapters/default.js +51 -8
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +15 -6
  13. package/{lib → adapters}/legacy.js +20 -6
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/stateFile.js +99 -0
  16. package/adapters/store-fs.js +28 -1
  17. package/adapters/store.js +204 -0
  18. package/{lib → adapters}/tests.js +15 -7
  19. package/adapters/vcs-git.js +15 -1
  20. package/{lib → adapters}/workspaceLock.js +1 -1
  21. package/{lib → adapters}/worktreeOwnership.js +18 -7
  22. package/app/archive.js +188 -0
  23. package/app/batch.js +9 -10
  24. package/app/board.js +4 -4
  25. package/app/checkCompletion.js +23 -11
  26. package/app/enrichedMissions.js +2 -4
  27. package/app/findings.js +6 -7
  28. package/app/gate.js +32 -15
  29. package/app/init.js +59 -22
  30. package/app/merge.js +64 -51
  31. package/app/messages.js +7 -8
  32. package/app/mission.js +7 -5
  33. package/app/plan.js +20 -9
  34. package/app/provision.js +12 -14
  35. package/app/read.js +65 -6
  36. package/app/rebase.js +14 -16
  37. package/app/recording.js +7 -8
  38. package/app/review.js +17 -8
  39. package/app/roster.js +14 -15
  40. package/{lib → app}/spawn-prechecks.js +4 -3
  41. package/app/spawn.js +12 -13
  42. package/app/teardown.js +28 -8
  43. package/app/tower.js +121 -39
  44. package/docs/README.md +6 -2
  45. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  46. package/docs/adr/0002-single-state-json.md +50 -0
  47. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  48. package/docs/adr/0004-surface-budget.md +50 -0
  49. package/docs/adr/README.md +51 -0
  50. package/docs/agents/domain.md +38 -0
  51. package/docs/agents/issue-tracker.md +46 -0
  52. package/docs/agents/triage-labels.md +15 -0
  53. package/docs/agents/workflow.md +51 -0
  54. package/docs/architecture/overview.md +170 -37
  55. package/docs/guide/hot-reload.md +1 -1
  56. package/docs/guide/install-upgrade-migration.md +18 -1
  57. package/docs/guide/release.md +1 -1
  58. package/docs/operations/incident.md +82 -0
  59. package/docs/operations/runbook.md +12 -3
  60. package/docs/reference/blockers.md +3 -3
  61. package/docs/reference/config.generated.md +5 -0
  62. package/docs/reference/config.md +4 -0
  63. package/docs/reference/doctor.md +104 -8
  64. package/docs/reference/errors.generated.md +41 -35
  65. package/docs/reference/errors.md +54 -40
  66. package/docs/reference/events.md +38 -6
  67. package/docs/reference/surface-budget.md +128 -0
  68. package/docs/reference/tools.generated.md +9 -8
  69. package/docs/reference/tools.md +9 -8
  70. package/docs/zh/guide/hot-reload.md +1 -1
  71. package/docs/zh/guide/install-upgrade-migration.md +13 -1
  72. package/docs/zh/reference/blockers.md +3 -3
  73. package/docs/zh/reference/config.md +4 -0
  74. package/docs/zh/reference/doctor.md +63 -2
  75. package/docs/zh/reference/errors.md +4 -2
  76. package/docs/zh/reference/tools.md +6 -6
  77. package/domain/batch.js +2 -2
  78. package/domain/config/schema.js +51 -4
  79. package/domain/contracts/outputs.js +33 -3
  80. package/domain/contracts/tool-inputs/missions.js +2 -1
  81. package/domain/contracts/tool-inputs/ops.js +1 -0
  82. package/domain/errors.js +297 -40
  83. package/domain/events.js +207 -0
  84. package/domain/integration/gate.js +41 -12
  85. package/domain/integration/init.js +40 -4
  86. package/domain/integration/merge.js +35 -0
  87. package/domain/lockBudget.js +30 -0
  88. package/domain/mission.js +1 -1
  89. package/domain/missions/completion.js +72 -5
  90. package/domain/missions/lifecycle.js +11 -4
  91. package/domain/missions/plan.js +14 -0
  92. package/domain/missions/review.js +90 -7
  93. package/domain/planning.js +6 -3
  94. package/domain/read/views.js +17 -1
  95. package/domain/report.js +9 -1
  96. package/domain/scope.js +83 -11
  97. package/domain/state/archive.js +257 -0
  98. package/domain/state/migrations.js +145 -0
  99. package/domain/state/schema.js +6 -0
  100. package/domain/state.js +154 -4
  101. package/domain/summary.js +2 -0
  102. package/domain/surface.js +248 -0
  103. package/host/doctor.js +555 -0
  104. package/{lib → host}/modeStore.js +1 -1
  105. package/{lib → host}/tool-registry.js +106 -31
  106. package/host/tower.js +32 -0
  107. package/index.js +12 -9
  108. package/package.json +10 -3
  109. package/ports/archive.js +18 -0
  110. package/ports/caseSensitivity.js +15 -0
  111. package/ports/clock.js +10 -2
  112. package/ports/config.js +19 -0
  113. package/ports/contract.js +38 -0
  114. package/ports/events.js +24 -0
  115. package/ports/ids.js +10 -2
  116. package/ports/index.js +54 -2
  117. package/ports/ownership.js +17 -0
  118. package/ports/state.js +16 -0
  119. package/ports/store.js +52 -3
  120. package/ports/vcs.js +25 -4
  121. package/scripts/architecture-waivers.json +19 -0
  122. package/scripts/architecture.mjs +634 -23
  123. package/scripts/check-docs.mjs +8 -35
  124. package/scripts/check-pack.mjs +68 -39
  125. package/scripts/check-philosophy.mjs +266 -0
  126. package/scripts/domain-metrics.mjs +13 -4
  127. package/scripts/generate-config-reference.mjs +14 -1
  128. package/scripts/generate-error-reference.mjs +68 -14
  129. package/scripts/generate-events-reference.mjs +83 -0
  130. package/scripts/generate-tools-reference.mjs +11 -0
  131. package/scripts/issue-audit.mjs +2 -2
  132. package/scripts/lib/surface.mjs +87 -0
  133. package/scripts/pack-manifest.txt +64 -19
  134. package/scripts/regression-audit.mjs +164 -0
  135. package/scripts/release-check.mjs +1 -1
  136. package/scripts/surface-baseline.json +21 -0
  137. package/scripts/surface-baseline.mjs +120 -0
  138. package/tools/adopt.js +2 -3
  139. package/tools/batch.js +2 -3
  140. package/tools/complete.js +3 -4
  141. package/tools/diff.js +3 -3
  142. package/tools/doctor.js +7 -10
  143. package/tools/finding.js +2 -3
  144. package/tools/findings.js +3 -3
  145. package/tools/import-legacy.js +3 -5
  146. package/tools/inbox.js +3 -3
  147. package/tools/index.js +7 -7
  148. package/tools/init.js +4 -7
  149. package/tools/interrupt.js +2 -3
  150. package/tools/legacy-map.js +3 -5
  151. package/tools/merge.js +3 -3
  152. package/tools/mission.js +2 -3
  153. package/tools/plan.js +7 -6
  154. package/tools/preflight.js +3 -3
  155. package/tools/rebase.js +3 -3
  156. package/tools/recover.js +38 -13
  157. package/tools/reload.js +3 -6
  158. package/tools/resource.js +3 -5
  159. package/tools/review.js +3 -4
  160. package/tools/roster.js +2 -3
  161. package/tools/send.js +2 -2
  162. package/tools/spawn.js +8 -5
  163. package/tools/status.js +16 -8
  164. package/tools/teardown.js +3 -4
  165. package/tools/test.js +2 -2
  166. package/types/index.d.ts +35 -1
  167. package/ui-bundle/README.md +37 -3
  168. package/ui-bundle/client.cards.js +356 -0
  169. package/ui-bundle/client.denied.js +142 -0
  170. package/ui-bundle/client.dock.js +333 -0
  171. package/ui-bundle/client.helpers.js +271 -0
  172. package/ui-bundle/client.js +79 -1747
  173. package/ui-bundle/client.locales.js +247 -0
  174. package/ui-bundle/client.mode.js +206 -0
  175. package/ui-bundle/client.overlay.js +64 -0
  176. package/ui-bundle/client.styles.js +151 -0
  177. package/ui-bundle/client.toolview.js +82 -0
  178. package/ui-bundle/package.json +1 -0
  179. package/lib/doctor.js +0 -270
  180. package/lib/persistence.js +0 -349
  181. package/lib/store.js +0 -118
  182. /package/{lib → app}/briefing.js +0 -0
  183. /package/{lib → app}/spawn-support.js +0 -0
  184. /package/{lib → host}/compatibility.js +0 -0
  185. /package/{lib → host}/config.js +0 -0
  186. /package/{lib → host}/hmr.js +0 -0
  187. /package/{lib → host}/modeState.js +0 -0
  188. /package/{lib → host}/presets.js +0 -0
@@ -0,0 +1,46 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Make an issue a sub-issue of a parent**: `gh issue create --parent <parent> ...`, or `gh issue edit <parent> --add-sub-issue <child>` afterwards (`gh` 2.94+). Older `gh`: `gh api --method POST repos/<owner>/<repo>/issues/<parent>/sub_issues -F sub_issue_id=<child-db-id>` (database id, as in **Blocking** below). Without sub-issues, put `Part of #<parent>` at the top of the child body.
11
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
12
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
13
+ - **Close**: `gh issue close <number> --comment "..."`
14
+
15
+ Infer the repo from `git remote -v`; `gh` does this automatically when run inside a clone. For this repo the remote is `louifinn/dsh-tower`.
16
+
17
+ ## Pull requests as a triage surface
18
+
19
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
20
+
21
+ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
22
+
23
+ - **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
24
+ - **List external PRs for triage**: `gh api --paginate 'repos/{owner}/{repo}/pulls?state=open' --jq '.[] | select(.author_association | IN("OWNER","MEMBER","COLLABORATOR") | not) | {number, title, author: .user.login, author_association, labels: [.labels[].name]}'`.
25
+ - **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
26
+
27
+ GitHub shares one number space across issues and PRs, so a bare `#42` may be either: resolve with `gh pr view 42` and fall back to `gh issue view 42`.
28
+
29
+ ## When a skill says "publish to the issue tracker"
30
+
31
+ Create a GitHub issue.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run `gh issue view <number> --comments`.
36
+
37
+ ## Wayfinding operations
38
+
39
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
40
+
41
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
42
+ - **Child ticket**: an issue linked to the map as a GitHub sub-issue (see **Make an issue a sub-issue of a parent**). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
43
+ - **Blocking**: GitHub's **native issue dependencies**, the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only, the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
44
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
45
+ - **Claim**: `gh issue edit <n> --add-assignee @me`, the session's first write.
46
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use. The right column currently equals the role name, so these are the defaults — `wontfix` already exists on the GitHub repo, while `needs-triage`, `needs-info`, `ready-for-agent`, and `ready-for-human` will be created by `/triage` on first use (or add them upfront with `gh label create`).
@@ -0,0 +1,51 @@
1
+ # Execution workflow: Matt Pocock skills × Tower
2
+
3
+ This repository uses two workflows together. They are not alternatives: the **Matt Pocock skills** own thinking, planning, review discipline and retros; **Tower** owns delivery — isolated worktrees, scope fences, review gates, merges and durable mission state. **GitHub Issues** is the single source of truth for what is planned, in progress, blocked and shipped.
4
+
5
+ ## Who owns what
6
+
7
+ | Stage | Owner | Artifact |
8
+ |---|---|---|
9
+ | Intake | Matt Pocock `/triage` for raw external reports; milestones + `P*`/`area/*` labels for self-authored findings | issue with owner-facing state |
10
+ | Plan | `/to-spec` → `/to-tickets` (or an existing issue whose acceptance criteria are already ticket-shaped) | spec issue, tickets with native `blocked_by` edges |
11
+ | Execute | `/implement-spec` subagents for one-session waves; **Tower missions** for parallel or long-lived work | integration branch + PRs, or mission worktrees + merge gate |
12
+ | Build each unit | `/tdd` inside the ticket/mission | failing test first, then green |
13
+ | Verify | gates + required CI checks + `/code-review` (Standards + Spec) | green PR with two-axis review |
14
+ | Land | protected `main`, four-line PR body, `verified-by:`/`commit:` closure | merged PR, closed issues |
15
+ | Release | `release:check`, CHANGELOG, COMPATIBILITY, tag; publish only when planned | tag-only or published release |
16
+ | Learn | `/retro`; mechanical findings become checks, judgement becomes standards | CONTRIBUTING/AGENTS/scanner updates |
17
+
18
+ ## The flow
19
+
20
+ 1. **Intake.** Raw external bug reports and feature requests go through `/triage` (they are not ours). Findings we file ourselves (audits, retros) skip triage and get a priority, an area and a milestone; they are already acceptance-criteria-shaped.
21
+ 2. **Plan.** Multi-session work goes `/to-spec` → `/to-tickets`. Tickets are tracer bullets with native blocking edges; the frontier is whatever has no open blockers. Small work may skip the spec when the issue already states acceptance criteria (e.g. a patch ticket).
22
+ 3. **Choose the orchestrator.**
23
+ - **One-session wave (≤3 tickets, mostly independent):** `/implement-spec`-style: an integration branch, one implementer subagent per ticket in its own worktree, merger merges to the integration branch, one PR to `main`.
24
+ - **Parallel or long-lived wave (architecture epics, >3 tickets, review needed):** **Tower**. `tower_init` → `tower_plan` with one mission per ticket, `scope` = the ticket's write fence, `deps` = the ticket's blocking edges → `tower_spawn` a worker per mission → optional reviewer (`tower-reviewer`) → `tower_merge` (gate checks scope, branch freshness, review, tasks) → `tower_teardown`. Tower's persistent state makes the wave resumable after a restart.
25
+ - Never run both orchestrators over the same files at once.
26
+ 4. **Build.** Each unit starts with `/tdd`: one failing test, then the implementation, then the next slice. Commit messages carry `Refs #n`; the completing commit uses `Fixes #n`. Workers do not merge; they hand off a branch.
27
+ 5. **Verify before landing.** Run `npm test`, `npm run docs:check`, all four `docs:check:generated*`, `npm run pack:check`, `npm run architecture:check`, `npm run types:check`, `npm run domain:check`. Bundle changes also run `npm run smoke:mount`. CI must be green on the required checks (`test (22)`, `test (24)`, `test (22, windows)`, `mount`). Then `/code-review` runs both axes on the diff (Standards against CONTRIBUTING/architecture docs, Spec against the ticket); every finding is fixed in the same PR or explicitly deferred with a reason.
28
+ 6. **Land.** PR body follows the four-line contract (failure mode → change → test that fails without it → blast radius). `main` is protected: PR + required checks, admins included, no force-push. A closure comment carries `verified-by: <test file>` and `commit: <sha>`; `npm run audit:issues` flags closures whose evidence is missing.
29
+ 7. **Release.** `release:check` compares the version, the previous tag and the CHANGELOG; a tag push fails when the version did not move or the tag does not match. Releases are either **tag-only** (version + changelog + compatibility row + annotated tag; no npm publish, no GitHub Release) or **published** (adds the human `npm publish --tag latest` with 2FA, `dist-tags` verification, then the GitHub Release). The mode is recorded in the milestone/plan.
30
+ 8. **Learn.** After a release (or a wave that went sideways) run `/retro`: mechanical mistakes become deterministic checks (scripts, CI jobs, preflight), judgement calls become written standards. Periodically run `/improve-codebase-architecture` for deepening opportunities and `/domain-modeling` to keep the vocabulary and ADRs (2.3.0) honest.
31
+
32
+ ## Decision table
33
+
34
+ | Situation | Flow |
35
+ |---|---|
36
+ | Raw external issue | `/triage` → then plan |
37
+ | Design question you cannot settle on paper | `/grill-with-docs`, detour through `/prototype` if it needs a runnable answer |
38
+ | Multi-session feature | `/to-spec` → `/to-tickets` → `/implement-spec` |
39
+ | Small fix with acceptance criteria | ticket → `/implement` (+ `/tdd`) |
40
+ | Architecture epic / parallel missions | Tower plan → missions → review gate → merge |
41
+ | Something broken, cause unknown | `/diagnosing-bugs` (tight red loop first) |
42
+ | Repeated mistakes | `/retro` → checks/standards |
43
+ | Release | release plan: tag-only or publish |
44
+
45
+ ## Invariants
46
+
47
+ - One ticket per PR series; a branch never touches two tickets.
48
+ - Tracked blocking edges, not prose: work only the frontier.
49
+ - Tower missions: `scope` is the write fence, a mission that outgrows it is re-scoped, not silently widened; the workspace lock keeps one Tower session per repository.
50
+ - Human-only gates: npm publish/2FA, branch-protection changes, anything behind a third-party credential. Stop and hand over rather than inventing a credential path.
51
+ - Context hygiene: keep planning in one window until tickets exist; start each implementation unit from the ticket, not from the previous unit's context.
@@ -10,7 +10,7 @@ or skill can remain mounted while the `tower_*` tools are gone.
10
10
  ```
11
11
  Host plugin (index.js)
12
12
  ├─ /tower command + Tower mode (guard, prompt section, tower_mode/tower_control, /tower reload)
13
- ├─ lib/tool-registry.js -> registration aggregator, actor guards, shared helpers
13
+ ├─ host/tool-registry.js -> registration aggregator, actor guards, shared helpers
14
14
  │ (+ 27 registered repository tools)
15
15
  │ (+ tower_reload while dev.hotReload is on; fails reload-unavailable without HMR)
16
16
  ├─ tools/index.js -> thin public wiring (re-exports the registry + briefings)
@@ -18,20 +18,37 @@ Host plugin (index.js)
18
18
  ├─ app/init.js, provision.js, merge.js, rebase.js, teardown.js
19
19
  ├─ app/review.js, findings.js, batch.js, messages.js, roster.js
20
20
  ├─ app/plan.js, mission.js, checkCompletion.js, spawn.js, recording.js
21
- └─ app/gate.js, read.js, board.js, missionContext.js, enrichedMissions.js
22
- ports (explicit contracts):
23
- ports/store.js vcs.js clock.js ids.js
24
- adapters (the only I/O):
25
- adapters/store-fs.js the only module importing node:fs
26
- adapters/vcs-git.js the only module importing node:child_process
27
- adapters/default.js composition root: store, vcs, clock, ids
28
- infrastructure (lib/):
29
- lib/store.js repo-scoped lock + atomic JSON + activity.jsonl
30
- lib/git.js bounded git commands + timeout classification
31
- lib/config.js profile/repo config layers over the pure schema
32
- lib/workspaceLock.js optional one-Tower-session-per-repository advisory lock
33
- lib/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
34
- lib/modeStore.js per-agent Tower mode persistence
21
+ ├─ app/spawn-prechecks.js, spawn-support.js, briefing.js
22
+ └─ app/archive.js, gate.js, read.js, board.js, missionContext.js, enrichedMissions.js
23
+ host (composition root + process/session glue; the only layer that may hold ambient state):
24
+ host/tool-registry.js registration aggregator + shared guards/helpers
25
+ host/config.js profile/repo config layers over the pure schema
26
+ host/doctor.js read-only environment contract and health checks
27
+ host/hmr.js optional DSH HMR bridge (tower_reload / /tower reload)
28
+ host/modeStore.js per-agent Tower mode persistence
29
+ host/modeState.js owned in-memory mode state
30
+ host/presets.js cross-field preset validation before work starts
31
+ host/compatibility.js COMPATIBILITY.md provenance
32
+ host/tower.js default-deps Tower binding for tests/embedders
33
+ ports (explicit runtime contracts, no implementation):
34
+ ports/contract.js definePort/assertPort runtime contract primitives
35
+ ports/{store,vcs,clock,ids,config,state,ownership,caseSensitivity,archive}.js
36
+ ports/index.js PORTS table + assertPorts(deps) injection check
37
+ adapters (the only I/O; they implement the ports):
38
+ adapters/store-fs.js the only module importing node:fs; createStorePort()
39
+ adapters/vcs-git.js the only module importing node:child_process; createVcsPort()
40
+ adapters/git.js bounded git commands + timeout classification
41
+ adapters/store.js repo-scoped lock + atomic JSON + activity.jsonl
42
+ adapters/persistence.js shared atomic write / lock / heartbeat primitives
43
+ adapters/archive.js terminal-mission archive files
44
+ adapters/leases.js repo-scoped advisory lease ledger
45
+ adapters/workspaceLock.js optional one-Tower-session-per-repository lock
46
+ adapters/worktreeOwnership.js worktree identity verification
47
+ adapters/stateFile.js state-file load/migrate/persist
48
+ adapters/caseSensitivity.js filesystem case-folding probe
49
+ adapters/legacy.js read-only legacy snapshot validation + storage
50
+ adapters/tests.js test discovery execution + TAP/JSON parsing
51
+ adapters/default.js createDefaultDeps(): the one default port implementation set
35
52
  pure domain modules (no fs/git):
36
53
  domain/errors.js stable, structural tool error catalog
37
54
  domain/batch.js batch admission create/close/summary validation
@@ -48,7 +65,6 @@ Host plugin (index.js)
48
65
  domain/report.js board rendering
49
66
  domain/roster.js roster predicates
50
67
  tools layer:
51
- lib/tool-registry.js registration aggregator + shared guards/helpers
52
68
  tools/index.js thin public wiring (re-exports only)
53
69
  tools/*.js one module per tool, handler + imported input schema
54
70
  Subagents
@@ -61,6 +77,54 @@ The main checkout is the control-tower repo; each mission has a branch `tower/<i
61
77
  worktree in `.dsh-tower-worktrees/<hash>/<missionId>`. The plugin never operates on the main checkout's
62
78
  working tree except merge and the read-only diff.
63
79
 
80
+ ## Ports and the composition root (#69)
81
+
82
+ The seam is real, not documented-only. `ports/` exports **runtime contracts**: each module defines a
83
+ frozen `{ name, methods }` descriptor plus an `assert*Port` helper, and `ports/index.js` exposes the
84
+ `PORTS` table and `assertPorts(deps)`.
85
+
86
+ `app/**` imports only app siblings, `domain/**`, `ports/**` and `node:path`. Every effectful call goes
87
+ through the `deps` object:
88
+
89
+ | deps member | contract | default implementation |
90
+ |---|---|---|
91
+ | `store` | StorePort: `pathsFor/exists/ensureDir/readJson/writeJsonAtomic/appendJsonl/withLock/withGitLock/readActivityTail` plus the raw-file and lock primitives `readText/writeText/appendText/rename/renameWithRetry/stat/unlink/readGitLock/gitLockView` | `adapters/store-fs.js` `createStorePort()` |
92
+ | `vcs` | VcsPort: `git/tryGit/revParse/isClean/isAncestor/resolveRepo/currentBranch/countCommits/listWorktrees` | `adapters/vcs-git.js` `createVcsPort()` |
93
+ | `clock`, `ids` | ClockPort / IdsPort | `adapters/default.js` |
94
+ | `config` | ConfigPort: `runtimeConfig/resolveRepoConfig` | `host/config.js` |
95
+ | `state` | StatePort: `loadStateFile` | `adapters/stateFile.js` |
96
+ | `archive` | ArchivePort: `readIndex/readEntry/writeEntries` | `adapters/archive.js` |
97
+ | `ownership` | OwnershipPort: `assertOwnership/canonicalPath` | `adapters/worktreeOwnership.js` |
98
+ | `caseSensitivity` | CaseSensitivityPort: `detect` | `adapters/caseSensitivity.js` |
99
+
100
+ `index.js` is the composition root: it calls `createDefaultDeps()` once and passes the object to
101
+ `registerTowerTools`. The registry adopts and validates that object (`assertPorts`) instead of building
102
+ its own adapter set, hands every tool an `openTower(repoRoot)` factory bound to those ports, and
103
+ `Tower.open(repoRoot, deps)` / `new Tower(repo, config, deps)` validates the object again before use.
104
+ `app/tower.js` itself imports no adapter and no `host/` module. The one piece of lock policy the app and
105
+ the store share (the git-mutex budget, #93) is pure and lives in `domain/lockBudget.js`;
106
+ `adapters/store.js` re-exports it for the infrastructure callers.
107
+
108
+ `store.rename` and `store.renameWithRetry` are both declared because they are not interchangeable: the
109
+ retrying helper has temp-file semantics and removes its source when the rename finally fails, which is
110
+ right for a disposable temp file but would destroy the lock bytes `tower_recover` must keep auditable.
111
+ The audit rename therefore uses the plain `rename`. The contract test scans `app/**` **and** `tools/**`
112
+ for every `deps.<port>.<method>` call and fails if the named port does not declare it, so a tool that
113
+ reaches a new port method cannot slip past `assertPorts`.
114
+
115
+ `lib/` no longer exists (#70): every module was `git mv`-ed into the layer that owns it. The I/O
116
+ implementations now sit in `adapters/` (`store`, `persistence`, `git`, `archive`, `stateFile`,
117
+ `leases`, `workspaceLock`, `worktreeOwnership`, `caseSensitivity`, `legacy`, `tests`), the
118
+ composition root and process/session glue in `host/`, and spawn policy in `app/`. `host/**` is the
119
+ only source root, besides `adapters/**` itself, allowed to import an adapter; `app/**` and `domain/**`
120
+ may not import `lib/**` (rule `no-lib-from-core`) and may not hold ambient module state
121
+ (`host-scope`). Tools receive effectful helpers from the registry instead of importing an adapter.
122
+
123
+ A full mission lifecycle (init → plan → provision → spawn → complete → review → gate → merge →
124
+ teardown) runs through the real app use cases on an in-memory store and a scripted vcs:
125
+ `tests/ports-lifecycle.test.mjs`. The default adapter set and the in-memory doubles are both asserted
126
+ against the same port contracts in `tests/memory-adapters.contract.test.mjs`.
127
+
64
128
  ## Mission state machine
65
129
 
66
130
  ```
@@ -82,7 +146,7 @@ set by `tower_merge`/reconciliation.
82
146
  against the merge base.
83
147
  3. Survey missions must not change files; build missions must stay inside `scope`.
84
148
  4. The main checkout must be clean for the files this merge would overwrite and on the base branch.
85
- 5. Dependency missions must be merged/abandoned.
149
+ 5. Dependency missions must be merged/abandoned, except a completed survey dependency which is its terminal success state (#107).
86
150
  6. If `review.requireReview:true` (default false), a review must exist, belong to this mission, be
87
151
  clean, approve merge, and be bound to the tip (unless a clean rebase waived it). No worker may be
88
152
  running.
@@ -101,9 +165,11 @@ After the command, regardless of exit status, it inspects `HEAD`:
101
165
 
102
166
  | Path | Contents |
103
167
  |---|---|
104
- | `<git-common-dir>/dsh-tower/state.json` | missions, roster, reviews, findings, messages, reviews sequence |
168
+ | `<git-common-dir>/dsh-tower/state.json` | active missions, roster, reviews, findings, messages, reviews sequence |
169
+ | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
170
+ | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
105
171
  | `<git-common-dir>/dsh-tower/activity.jsonl` | append-only activity |
106
- | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (dead-PID reclaimed, live lock reported) |
172
+ | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (heartbeat reclaim; a recycled PID cannot pin it) |
107
173
  | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
108
174
  | `<git-common-dir>/dsh-tower/lock-holds.jsonl` | state/git lock-hold telemetry |
109
175
  | `<checkout parent>/.dsh-tower-worktrees/<hash>/<missionId>` | mission worktree |
@@ -111,8 +177,10 @@ After the command, regardless of exit status, it inspects `HEAD`:
111
177
  | `$DSH_HOME/tower-mode/modes.lock` | mode-store single-writer lock |
112
178
 
113
179
  `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.
180
+ heartbeat-based reclamation (a live PID with a silent heartbeat is reclaimed after byte verification;
181
+ a live holder that keeps heartbeating is never stolen), and bounded lock waits. `state.json` is the
182
+ mission persistence contract; `modes.json` is the per-agent mode contract; neither is written without
183
+ its own lock.
116
184
 
117
185
  ## Layering rules
118
186
 
@@ -121,17 +189,66 @@ dead-PID reclamation, and bounded lock waits. `state.json` is the mission persis
121
189
  - `domain/**` must not import I/O or adapters (`domain-pure`).
122
190
  - Only `adapters/vcs-git.js` may import `node:child_process`, and only
123
191
  `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/`,
192
+ The rule is enforced over the source roots — `host/`, `app/`, `domain/`, `tools/`,
125
193
  `adapters/`, `ports/` — and does not cover `scripts/**`, which holds maintenance
126
194
  entry points (release check, mount smoke, docs generators) that may spawn git or npm.
195
+ The optional Web client (`ui-bundle/`) is its own tier with its own rules.
127
196
  - `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.
197
+ `tools/index.js` is the thin public wiring module (it imports `host/tool-registry.js`, not a
198
+ `node:` module), while registration/shared machinery lives in `host/tool-registry.js`.
199
+ Tools receive effectful helpers (repository resolution, leases, recovery, legacy import) from
200
+ the registry instead of importing an adapter.
201
+ - Aggregate files have size ceilings; top-level mutable state is limited to `host/**`
202
+ (`aggregate-size`, `host-scope`). The per-tool ceiling is 200 lines; `tools/index.js`
203
+ currently sits inside the 40-200 range, so the tools allow-list is empty rather than silently
204
+ exempting the wiring module.
205
+ - `app/**` may import only app siblings, `domain/**`, `ports/**` and `node:path` (`app-ports-only`);
206
+ an app module that reaches an adapter, a `host/` module or `lib/` fails the gate.
207
+ - `app/**` and `domain/**` may not import the retired `lib/` layer (`no-lib-from-core`); with the
208
+ layer gone this keeps it from creeping back.
209
+ - `host/**` is the only source root that may hold ambient module state (`host-scope`);
210
+ `app/**` and `domain/**` must stay stateless at module scope. Only `host/**` and `adapters/**`
211
+ may import an adapter (`composition-root`); `app/`, `tools/`, `domain/` and `ports/` never may.
212
+ - Every `ports/**` module must export a runtime contract (`const`/`function`/`class`) and must not
213
+ import an implementation (`port-shape`); a typedef plus `export {}` is no longer accepted.
214
+ - `ui-bundle/**` is a browser tier, not host code (#71): no `node:*` import
215
+ (`client-no-node` — DOM globals are fine; host capabilities must come through the
216
+ plugin's registration surface), every client file stays at or below 800 lines
217
+ (`client-size`), and module-level mutable state is banned exactly as it is for
218
+ `app/**` (`client-ambient`); the split surfaces keep their state inside the
219
+ `create*` closure the entry composes. `ui-bundle/` is also excluded from
220
+ `aggregate-size`, which it would otherwise duplicate.
133
221
 
134
- A fixture test seeds one violation per rule, so a rule that cannot fail is itself a test failure.
222
+ ### Exemptions are justified data
223
+
224
+ The gate's only exemptions live in `scripts/architecture-waivers.json`; `scripts/architecture.mjs`
225
+ keeps no allow-lists of its own. Since #70 the file has exactly two entries, both I/O adapter
226
+ boundaries (`adapters/vcs-git.js` for `node:child_process` and `adapters/store-fs.js` for
227
+ `node:fs`). Each entry names the `rule` and `path` and must carry a non-empty `reason` plus an
228
+ `owner` or `ref`. An entry missing any of those is itself a blocking `waiver-contract` violation
229
+ that names the rule and the path, so a bare path cannot turn the gate green. The waiver file is a
230
+ two-key path in `CONTRIBUTING.md`; there is no `reviewBy`/expiry field in this release, so the
231
+ reason and the owner are the reviewed decision.
232
+
233
+ `host-scope` replaces the old `ambient-state` waivers (#70). It scans `app/**` and `domain/**` and
234
+ records every module-level `let`; `host/**` owns the process/session state and needs no waiver. The
235
+ scan is statement-aware rather than line-based: it walks the module's token stream and records every
236
+ binding of every top-level `let` declaration. A second declarator (`let a = 1, b = 2`), a second
237
+ statement on one line (`let a = 1; let b = 2`), and any new top-level `let` in `app/` or `domain/`
238
+ are each their own violation. Strings, comments, template literals and regex literals are skipped,
239
+ and `let` inside a function, block, class or `for (…)` header is not module-level state.
240
+
241
+ `domain-pure`, `host-scope`, `no-lib-from-core`, and anything under `domain/**` are load-bearing: no
242
+ waiver may target them, even a justified one. A `waiver-contract` violation rejects that entry and
243
+ the underlying rule still fails.
244
+
245
+ A green run prints `architecture: ok (waivers: N)`; N is the number of entries that actually
246
+ suppressed a violation, so a stale waiver is visible in the output and in the tests. The fixture
247
+ suite seeds an unjustified waiver (it fails), a justified one (it passes), a `host-scope` fixture
248
+ that puts ambient state in `host/` (allowed) and in `app/`/`domain/` (rejected), a `no-lib-from-core`
249
+ import from `app/` (rejected), a same-line second binding (rejected), and a new violation inside
250
+ each rule. A separate fixture seeds one violation per rule, so a rule that cannot fail is itself a
251
+ test failure.
135
252
 
136
253
  ## Mount smoke and host internals
137
254
 
@@ -147,24 +264,40 @@ probe adjustments in the same change.
147
264
 
148
265
  All epic DoD items now have an executable gate:
149
266
 
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;
267
+ - Pure domain share: **39.5%** (7,475 / 18,936 host source lines), enforced by
268
+ `npm run domain:check` (threshold 35%). `npm run domain:metrics` also prints a
269
+ separate `ui-bundle` line, so Web-client growth is visible in CI output while the
270
+ share itself still measures the Host architecture.
271
+ - `domain/**` imports no fs, child_process, adapters, app, tools, or host infrastructure;
153
272
  `domain-pure` is a blocking architecture rule.
154
273
  - 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
274
+ `adapters/store-fs.js` imports `node:fs`; both are blocking rules whose files are the only
275
+ `child-process-surface`/`fs-surface` entries in `scripts/architecture-waivers.json`.
276
+ - `host`, `app`, and `domain` files stay <= 800 lines; the same ceiling applies to
277
+ `ui-bundle/**` (`client-size`); every `tools/**` file stays
157
278
  40-200 lines (one handler module per tool; `tools/index.js` is 40-line wiring).
158
279
  - No `withLock` scope starts a process; the two-phase tests instrument every git call.
159
280
  - `tools/**` has no status/type/enum literal; vocabulary is owned by
160
281
  `domain/vocabulary.js`; the `tools-vocabulary` rule is blocking.
161
282
  - `validateState` has a four-line body over `domain/state/schema.js` and
162
283
  `domain/state/semantics.js`.
163
- - `state.json` and `modes.json` share `lib/persistence.js` atomic write, token-owned
284
+ - `state.json` and `modes.json` share `adapters/persistence.js` atomic write, token-owned
164
285
  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.
286
+ - The in-memory lifecycle runs the real app use cases (init → plan → provision → spawn → complete →
287
+ review → gate → merge → teardown) on an in-memory store and scripted vcs supplied through the
288
+ composition root, with no disk, no git and no `defaultDeps` (#69).
289
+ - The eleven host fitness rules are errors, each with a seeded failing fixture; the
290
+ three `client-*` rules form the Web-client tier with one fixture each, and the
291
+ `waiver-contract` rule makes an unjustified exemption blocking as well.
292
+
293
+ ## Non-goals
294
+
295
+ This layer map is not a dependency-injection container, a service locator, or a
296
+ place to register classes by reflection. Ports stay runtime contracts and the
297
+ composition root stays the only place that assembles implementations.
298
+
299
+ Enforced by: `scripts/architecture.mjs` and `ports/index.js`; tests in
300
+ `tests/architecture.test.mjs`; gate: `npm run architecture:check`.
168
301
 
169
302
 
170
303
 
@@ -86,7 +86,7 @@ To stop watching, run `npm run dev:hmr -- --remove` and restart DSH once.
86
86
 
87
87
  ## The edit loop
88
88
 
89
- 1. Edit `lib/*.js`, `index.js`, `worker.js`, `reviewer.js` or `skills.js`.
89
+ 1. Edit `host/*.js`, `adapters/*.js`, `app/*.js`, `domain/**/*.js`, `index.js`, `worker.js`, `reviewer.js` or `skills.js`.
90
90
  2. Save the file. The watcher debounces (DSH default 100 ms) and reconciles the profile; changed
91
91
  modules are replaced and the affected Loader entries restart.
92
92
  3. Watch the DSH log for `reload plugin at <path>` / `restart required: …` messages.
@@ -29,7 +29,24 @@ 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
32
+ - **state schema v1 → v2 (unreleased)**: `state.json` migrates automatically. The first **write** after
33
+ upgrading keeps a `state.json.v1.bak` backup, advances `revision` once, and appends
34
+ `state.migrated {from,to}` to `activity.jsonl`; read-only tools keep reading a v1 file without
35
+ rewriting it. `tower_doctor` reports the on-disk version and the pending migration. A file written by
36
+ a newer plugin is refused with `[tower:state-version-unsupported]` and is never rewritten or
37
+ downgraded; a migration that throws reports `[tower:state-migration-failed]` and leaves the original
38
+ file byte-identical.
39
+
40
+ - **state schema v2 → v3 (unreleased, #90)**: scope matching follows the checkout's filesystem. The
41
+ migration backfills `caseInsensitive: null` ("not detected"); a migration never reads the filesystem,
42
+ so the first `tower_init` after upgrading probes the checkout once and records `true`/`false`. Until
43
+ then the safe `case-sensitive` comparison applies, and `tower_doctor` reports the unknown mode as a
44
+ `warn` (`case-mode`). On a case-insensitive checkout, `src/Api/**` and `src/api/**` then count as
45
+ overlapping and case-only mismatches are no longer refused by the out-of-scope gate. Stored scope
46
+ strings are never rewritten: the board keeps showing what the operator typed.
47
+
48
+ - **2.0.4 → 2.1.0-beta.1**: no config migration; the state file migrates automatically (v1/v2 → v3) as
49
+ described above. This is a **pre-release** published on the
33
50
  `next` dist-tag, so `latest` still resolves to 2.0.4 — install it with
34
51
  `dsh plugin --profile web add dsh-tower@next` (or the exact version). The runtime behavior is
35
52
  unchanged; the release carries the delivery gate (mount smoke in CI, `npm run release:check`, the
@@ -58,7 +58,7 @@ Put the **host build in cell 0** and the **tag in cell 1**:
58
58
  | 0.2.1-rc.2 | v2.1.0-beta.1 | 2026-10-22 | `npm test`, `npm run docs:check`, mount smoke |
59
59
  ```
60
60
 
61
- `lib/compatibility.js#parseCompatibility` reads cells 0–3 only: never insert a column before
61
+ `host/compatibility.js#parseCompatibility` reads cells 0–3 only: never insert a column before
62
62
  the tag and never add one before "what was run", or `tower_doctor` stops seeing the row. The
63
63
  host pin in `scripts/mount-smoke/host.json` must equal cell 0 of the newest row (a unit test
64
64
  enforces it).
@@ -0,0 +1,82 @@
1
+ # Incidents and regressions
2
+
3
+ A **regression** is something that *worked in a released version, then stopped*.
4
+ It is not a feature request and not an ordinary bug: a user could rely on the old
5
+ behavior, so the record must say which release broke it, who was affected, and
6
+ which test now prevents it from happening again.
7
+
8
+ Only shipped regressions get this record. Ordinary bugs, missing features and
9
+ questions use the normal issue flow.
10
+
11
+ ## The six-field record
12
+
13
+ Copy this into the issue body (or the closing comment) when a regression is
14
+ confirmed. All six lines are part of the closure; the `Regression test:` line is
15
+ the one a machine can check.
16
+
17
+ ```text
18
+ Symptom: what the user sees, with the exact error text
19
+ Introduced in: version / commit range
20
+ Blast radius: who is affected (all sessions? restored ones? one platform?)
21
+ Root cause: one paragraph, mechanism not blame; include the fix
22
+ Regression test: path to the test that fails without the fix
23
+ Released in: version that carries the fix, and how it was verified
24
+ ```
25
+
26
+ The fields answer, in order: what broke, which release introduced it, the user
27
+ impact, the cause and the fix, the proof, and the release that carries the fix.
28
+ `Introduced in` and `Released in` are different answers; do not fill both with the
29
+ current version.
30
+
31
+ ## Label
32
+
33
+ The `regression` label means exactly **"worked in a released version, then
34
+ stopped"**. It is mutually exclusive with `bug` for a shipped break: the regression
35
+ label is the narrower claim. The issue title still describes the symptom.
36
+
37
+ ## Closing rule
38
+
39
+ A `regression` issue may not be closed while any of these is true:
40
+
41
+ - no `Regression test:` line names a path;
42
+ - the path does not exist in the repository;
43
+ - the path is not a test file (a script or a doc is not evidence).
44
+
45
+ The offline audit reads a local tracker export, so it needs no network and no
46
+ token:
47
+
48
+ ```powershell
49
+ # 1) Export (network, once) — same export as the P0/P1 audit
50
+ New-Item -ItemType Directory -Force tracker | Out-Null
51
+ $all = foreach ($n in (gh issue list --repo louifinn/dsh-tower --state all --limit 500 --json number --jq '.[].number')) {
52
+ gh issue view $n --repo louifinn/dsh-tower --json number,title,state,labels,closedAt,body,comments | ConvertFrom-Json
53
+ }
54
+ @{ issues = @($all) } | ConvertTo-Json -Depth 8 | Set-Content tracker/issues.json -Encoding utf8
55
+
56
+ # 2) Audit (offline)
57
+ npm run audit:regressions
58
+ npm run audit:regressions -- --tracker path/to/export.json --json
59
+ ```
60
+
61
+ Exit codes: `0` — every closed regression names an existing test; `1` — at least
62
+ one is missing or points at a nonexistent/non-test path; `2` — the export is
63
+ missing or malformed.
64
+
65
+ The audit is a script because a checklist is only as reliable as the reader.
66
+ `tests/regression-audit.test.mjs` proves the check fails for a seeded missing path
67
+ and passes for a real one, so the rule cannot silently become prose.
68
+
69
+ ## Where the practice starts
70
+
71
+ The first real entry is #68 (all tower write tools failed with `unknown` after a
72
+ session restore). The tower retro-fills that issue with these six fields; this
73
+ document is the template it used.
74
+
75
+ ## Non-goals
76
+
77
+ No blameless-postmortem ceremony for ordinary bugs, no SLA commitments, no
78
+ five-whys, and no incident commander. Only shipped regressions get the six fields.
79
+
80
+ Unowned. This is a writing habit and a closing convention; no runtime code
81
+ enforces it. The one checkable part — the `Regression test:` path — is enforced by
82
+ `scripts/regression-audit.mjs`.
@@ -48,9 +48,18 @@ fails, the previous plugin code no longer runs: restart DSH and check the HMR lo
48
48
 
49
49
  ## State lock
50
50
 
51
- `tower state is locked by another writer` means an operation holds `state.lock`. Wait and retry. Only if
52
- the owning process is gone, remove `<git-common-dir>/dsh-tower/state.lock` deliberately; never delete it
53
- while an agent may still be running.
51
+ `tower state is locked by another writer` means an operation holds `state.lock`. Wait and retry: a
52
+ live holder rewrites the record's `at` heartbeat every second, so it is never stolen while it runs.
53
+ A crashed holder stops heartbeating; once its last heartbeat is older than three intervals (3 s), the
54
+ next mutation reclaims the record after byte verification. A reclaimed live-PID record is kept as
55
+ `state.lock.reclaimed-<ts>-<id>` for audit; a provably dead PID (ESRCH) is reclaimed immediately.
56
+
57
+ If a lock never clears, `tower_recover({action:"status"})` reports the owner's `pid`, `startedAt`,
58
+ `ageMs` and `heartbeatAgeMs`, and `tower_doctor` reports the same. `tower_recover
59
+ clear-stale-lock` with `confirm:"CLEAR_STALE_LOCK"` moves a provably dead lock to
60
+ `state.lock.recovered-<ts>`; add `force: true` to clear a live-but-silent PID (the recycled-PID case)
61
+ after confirming no writer is active. The repair always renames, never deletes, so the bytes stay
62
+ auditable. Never delete `state.lock` by hand while an agent may still be running.
54
63
 
55
64
  ## Worktrees and teardown
56
65
 
@@ -16,12 +16,12 @@ tables below define 32 stable blocker/failure codes.
16
16
  | `branch-missing` | No worktree/branch yet: spawn a worker first. |
17
17
  | `stale-base` | The branch does not contain the base tip: run `tower_rebase`. |
18
18
  | `read-only-survey` | Survey mission changed files; survey missions must be read-only. |
19
- | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. |
19
+ | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. On a case-sensitive checkout a case-only mismatch is still rejected and the detail names the scope entry it would match if case were folded. |
20
20
  | `base-dirty` | The main checkout has uncommitted changes the merge would overwrite. |
21
- | `deps-unmerged` | A dependency mission is not merged/abandoned. |
21
+ | `deps-unmerged` | A dependency mission is not satisfied: a build dependency must be `merged`/`abandoned`, while a survey dependency is satisfied at `completed` (archived entries included). |
22
22
  | `no-review` | `review.requireReview:true` and no review exists on the branch: spawn a reviewer (or disable the review gate). |
23
23
  | `review-mission-mismatch` | `review.requireReview:true` and the latest review belongs to another mission. |
24
- | `not-clean` | `review.requireReview:true` and latest review is `p1-Nitems`/`p2-Nitems`; another round is required. |
24
+ | `not-clean` | `review.requireReview:true` and latest review is `p1-Nitems`/`p2-Nitems`; another round is required. Verdicts from later routes of the same round are appended to it instead of flipping the mission again. |
25
25
  | `review-hold` | `review.requireReview:true` and latest review recommends hold; a later clean review must supersede it. |
26
26
  | `review-not-approved` | `review.requireReview:true` and latest clean review says `fix-then-merge` or `hold`; only `merge` approves. |
27
27
  | `tip-moved` | `review.requireReview:true` and the branch changed after review; review again (or use a clean rebase waiver). |
@@ -1,6 +1,7 @@
1
1
  # Config reference (generated)
2
2
 
3
3
  > Generated by `npm run docs:generate:config` from `domain/config/schema.js`. Values are defaults before repo config.
4
+ > Surface: config keys: 64 · sections: 15 · tools: 27 · baseline 2026-10-23. Generated from `scripts/surface-baseline.json`; do not edit by hand.
4
5
 
5
6
  | Key | Default | Description |
6
7
  |---|---|---|
@@ -51,8 +52,12 @@
51
52
  | `gates.requireChecks` | `false` | Require at least one completion check (tower_complete.checks) before a mission can merge. |
52
53
  | `gates.requireTests` | `false` | Require a passing test-discovery receipt for the reviewed worktree tip before merge. |
53
54
  | `gates.allowReviewExemption` | `false` | Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers. |
55
+ | `gates.maxReportBytes` | `32768` | Hard cap, in UTF-8 bytes, for tower_complete.report; a larger report is rejected with an actionable nextAction, never truncated. |
54
56
  | `docs.board` | `true` | Write a human-readable MISSIONS.md after mission changes. |
55
57
  | `docs.boardLocation` | `"common"` | common = <git-common-dir>/dsh-tower/MISSIONS.md; repo = <repoRoot>/MISSIONS.md. |
58
+ | `archive.enabled` | `true` | Move terminal (merged/abandoned) missions whose worktree is gone into <git-common-dir>/dsh-tower/archive/ at the next mutation, so state.json stops carrying their full history. |
59
+ | `archive.afterDays` | `30` | Terminal missions untouched for at least this many days are archived on the next mutation (0 archives them immediately). |
60
+ | `archive.maxMissions` | `100` | Maximum terminal missions kept hot; once exceeded, the oldest eligible terminal missions are archived until the cap holds (0 archives every eligible terminal mission). |
56
61
  | `tools.exposeOperational` | `true` | Master switch for batch/resource/import/legacy/recover/test/doctor tools. |
57
62
  | `tools.exposeBatch` | `true` | Register tower_batch. |
58
63
  | `tools.exposeImport` | `true` | Register tower_import_legacy/tower_legacy_map. |
@@ -83,8 +83,12 @@ violates the declared schema (for example `merge.timeoutMs` below 1000) fail wit
83
83
  | `gates.requireChecks` | `false` | Require at least one `tower_complete.checks` entry before merge (`no-check-evidence`). |
84
84
  | `gates.requireTests` | `false` | Require a passing discovery receipt for the current mission branch tip (`tests-missing`); independent of the review gate. |
85
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. |
86
+ | `gates.maxReportBytes` | `32768` (32 KiB) | Hard cap in UTF-8 bytes for `tower_complete.report`. A larger report is rejected with `invalid-argument` and an actionable `nextAction` (file the full text in the mission worktree and reference its path); it is never truncated. Existing larger reports still load. |
86
87
  | `docs.board` | `true` | Write `MISSIONS.md` after mutations. |
87
88
  | `docs.boardLocation` | `common` | `common` = `<git-common-dir>/dsh-tower/MISSIONS.md`; `repo` = `<repoRoot>/MISSIONS.md`. |
89
+ | `archive.enabled` | `true` | Move terminal (`merged`/`abandoned`) missions whose worktree is gone into `<git-common-dir>/dsh-tower/archive/` at a mutation boundary. Archived missions keep tasks, reviews, findings and the completion report; they are moved, never deleted. |
90
+ | `archive.afterDays` | `30` | Age trigger: a terminal mission untouched for at least this many days is archived on the next mutation (`0` archives immediately). |
91
+ | `archive.maxMissions` | `100` | Cap trigger: once more than this many terminal missions are hot, the oldest eligible ones are archived until the cap holds (`0` archives every eligible terminal mission). A mission with a live worktree is never archived. |
88
92
  | `tools.exposeOperational` | `true` | Register batch/resource/import/legacy/recover/test/doctor tools; `false` shrinks the fixed tool-schema context. |
89
93
  | `leases.ttlMs` | `900000` | Default advisory lease lifetime; expiry never auto-releases. |
90
94
  | `dev.hotReload` | `true` | Register `tower_reload`; `/tower reload` is disabled when off. It returns `reload-unavailable` when the profile has no HMR service. |