dsh-tower 2.1.0-beta.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/COMPATIBILITY.md +1 -0
  3. package/CONTRIBUTING.md +8 -1
  4. package/README.en.md +8 -2
  5. package/README.md +8 -2
  6. package/adapters/default.js +2 -0
  7. package/app/archive.js +190 -0
  8. package/app/board.js +3 -1
  9. package/app/checkCompletion.js +15 -2
  10. package/app/gate.js +31 -13
  11. package/app/init.js +45 -3
  12. package/app/merge.js +6 -0
  13. package/app/mission.js +2 -0
  14. package/app/plan.js +12 -4
  15. package/app/read.js +59 -3
  16. package/app/review.js +2 -0
  17. package/app/teardown.js +22 -1
  18. package/app/tower.js +34 -6
  19. package/docs/agents/domain.md +38 -0
  20. package/docs/agents/issue-tracker.md +46 -0
  21. package/docs/agents/triage-labels.md +15 -0
  22. package/docs/architecture/overview.md +3 -1
  23. package/docs/guide/install-upgrade-migration.md +18 -1
  24. package/docs/reference/blockers.md +1 -1
  25. package/docs/reference/config.generated.md +4 -0
  26. package/docs/reference/config.md +4 -0
  27. package/docs/reference/doctor.md +58 -8
  28. package/docs/reference/errors.generated.md +2 -0
  29. package/docs/reference/errors.md +4 -2
  30. package/docs/reference/tools.generated.md +6 -6
  31. package/docs/reference/tools.md +7 -6
  32. package/docs/zh/guide/install-upgrade-migration.md +13 -1
  33. package/docs/zh/reference/blockers.md +1 -1
  34. package/docs/zh/reference/config.md +4 -0
  35. package/docs/zh/reference/doctor.md +35 -2
  36. package/docs/zh/reference/errors.md +4 -2
  37. package/docs/zh/reference/tools.md +3 -3
  38. package/domain/config/schema.js +26 -3
  39. package/domain/contracts/outputs.js +27 -1
  40. package/domain/contracts/tool-inputs/missions.js +2 -1
  41. package/domain/errors.js +2 -0
  42. package/domain/integration/gate.js +25 -11
  43. package/domain/integration/init.js +40 -4
  44. package/domain/mission.js +1 -1
  45. package/domain/missions/completion.js +58 -5
  46. package/domain/missions/lifecycle.js +2 -1
  47. package/domain/planning.js +6 -3
  48. package/domain/read/views.js +14 -1
  49. package/domain/report.js +9 -1
  50. package/domain/scope.js +83 -11
  51. package/domain/state/archive.js +257 -0
  52. package/domain/state/migrations.js +145 -0
  53. package/domain/state/schema.js +6 -0
  54. package/domain/state.js +154 -4
  55. package/domain/summary.js +2 -0
  56. package/lib/archive.js +279 -0
  57. package/lib/caseSensitivity.js +66 -0
  58. package/lib/doctor.js +178 -0
  59. package/lib/leases.js +14 -5
  60. package/lib/legacy.js +19 -5
  61. package/lib/persistence.js +12 -5
  62. package/lib/stateFile.js +99 -0
  63. package/lib/store.js +15 -0
  64. package/lib/tests.js +6 -2
  65. package/lib/worktreeOwnership.js +18 -7
  66. package/package.json +1 -1
  67. package/scripts/pack-manifest.txt +9 -0
  68. package/tools/complete.js +1 -1
  69. package/tools/doctor.js +1 -1
  70. package/tools/init.js +1 -1
  71. package/tools/plan.js +1 -1
  72. package/tools/preflight.js +1 -0
  73. package/tools/status.js +14 -6
  74. package/tools/teardown.js +1 -1
  75. package/types/index.d.ts +34 -0
package/app/mission.js CHANGED
@@ -4,6 +4,7 @@ import { applyMissionUpdate } from '../domain/missions/lifecycle.js'
4
4
  import { validateState } from '../domain/state.js'
5
5
  import { fullMission } from '../domain/summary.js'
6
6
  import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
7
+ import { sweepArchive } from './archive.js'
7
8
 
8
9
  function fail(message, code = 'invalid-argument') {
9
10
  throw new TowerToolError(code, message)
@@ -19,6 +20,7 @@ export async function updateMission(tower, input = {}) {
19
20
  const { changes } = applyMissionUpdate({ mission, state, input, config: tower.config })
20
21
  const now = Date.now()
21
22
  mission.updatedAt = now
23
+ await sweepArchive(tower, state, { now })
22
24
  state.revision += 1
23
25
  state.updatedAt = now
24
26
 
package/app/plan.js CHANGED
@@ -9,21 +9,27 @@ import {
9
9
  import { validateState } from '../domain/state.js'
10
10
  import { fullMission } from '../domain/summary.js'
11
11
  import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
12
+ import { sweepArchive } from './archive.js'
12
13
 
13
14
  export async function plan(tower, input = {}) {
14
15
  const prepared = normalizePlanMissions(input)
15
16
  return withLock(tower.paths, async () => {
16
17
  const state = await tower.load()
17
18
  const now = Date.now()
18
- const assigned = assignMissionIds(prepared, nextMissionNumber(state.missions))
19
+ // Archived ids stay in the known set: a new mission must not reuse an id
20
+ // and a merged dependency may live in the archive index (#73).
21
+ const archive = await tower.archiveIndex()
22
+ const known = [...state.missions, ...(archive.ok ? archive.missions : [])]
23
+ const assigned = assignMissionIds(prepared, nextMissionNumber(known))
19
24
 
20
- assertKnownDependencies(assigned, state.missions)
25
+ assertKnownDependencies(assigned, known)
21
26
 
22
- addAutoDeps(assigned, state.missions)
23
- assertAcyclic([...state.missions, ...assigned])
27
+ addAutoDeps(assigned, known, { caseInsensitive: state.caseInsensitive === true })
28
+ assertAcyclic([...known, ...assigned])
24
29
 
25
30
  const created = assigned.map((mission) => newMission(mission.id, mission, now))
26
31
  state.missions.push(...created)
32
+ const swept = await sweepArchive(tower, state, { now })
27
33
  state.revision += 1
28
34
  state.updatedAt = now
29
35
 
@@ -37,6 +43,8 @@ export async function plan(tower, input = {}) {
37
43
  return {
38
44
  revision: state.revision,
39
45
  missions: created.map((mission) => fullMission(mission)),
46
+ ...(swept.archived.length > 0 ? { archived: swept.archived } : {}),
47
+ ...(swept.error ? { archiveError: swept.error } : {}),
40
48
  }
41
49
  })
42
50
  }
package/app/read.js CHANGED
@@ -2,29 +2,85 @@
2
2
  import { TowerToolError } from '../domain/errors.js'
3
3
  import { diffView, missionDetailView, statusView } from '../domain/read/views.js'
4
4
  import { readActivityTail } from '../lib/store.js'
5
+ import { archiveView, archivedSummary } from './archive.js'
5
6
 
6
7
  function fail(message, code = 'invalid-argument') {
7
8
  throw new TowerToolError(code, message)
8
9
  }
9
10
 
11
+ /**
12
+ * Full detail for one mission. A mission that was archived is reconstructed
13
+ * from its year file, including the reviews, findings and activity that left
14
+ * state.json with it. An unreadable archive is a diagnostic, never a throw:
15
+ * `tower_status` must keep rendering the hot state.
16
+ */
10
17
  export async function missionDetail(tower, missionId) {
11
18
  const state = await tower.load()
12
19
  const mission = state.missions.find((entry) => entry.id === missionId)
13
- if (!mission) fail(`unknown mission "${missionId}"`, 'mission-not-found')
14
20
  const activity = await readActivityTail(tower.paths.activity, 200)
15
- return missionDetailView({ state, mission, activity })
21
+ if (mission) return missionDetailView({ state, mission, activity })
22
+
23
+ const archived = await tower.archivedMission(missionId)
24
+ if (archived.ok && archived.entry) {
25
+ const entry = archived.entry
26
+ const hotFindings = (state.findings ?? []).filter((finding) => finding.missionId === missionId)
27
+ const byId = new Map()
28
+ for (const finding of [...(entry.findings ?? []), ...hotFindings]) {
29
+ if (finding && typeof finding.id === 'string') byId.set(finding.id, finding)
30
+ }
31
+ return {
32
+ ...missionDetailView({
33
+ state: /** @type {any} */ ({ reviews: entry.reviews ?? [], findings: [...byId.values()] }),
34
+ mission: /** @type {any} */ (entry.mission),
35
+ activity,
36
+ }),
37
+ archived: true,
38
+ archivedAt: entry.archivedAt,
39
+ archivedYear: entry.archivedYear,
40
+ branch: entry.branch ?? null,
41
+ }
42
+ }
43
+ if (archived.unreadable) {
44
+ return {
45
+ archived: true,
46
+ archivedAt: null,
47
+ branch: null,
48
+ mission: null,
49
+ reviews: [],
50
+ findings: [],
51
+ activity: [],
52
+ error: archived.error ?? `tower archive is unreadable; mission ${missionId} cannot be loaded`,
53
+ }
54
+ }
55
+ return fail(`unknown mission "${missionId}"`, 'mission-not-found')
16
56
  }
17
57
 
18
58
  export async function status(tower, input = {}) {
19
59
  const state = await tower.load()
20
60
  const activity = await readActivityTail(tower.paths.activity, 12)
61
+ const index = await tower.archiveIndex()
62
+ const activeIds = new Set(state.missions.map((mission) => mission.id))
63
+ const wanted = Array.isArray(input.missionIds) ? new Set(input.missionIds) : null
64
+ // An explicit missionId lookup includes its archived summary; the dashboard
65
+ // includes them only on demand. Archived summaries never carry report bodies.
66
+ const includeArchived = input.includeArchived === true || wanted !== null
67
+ const missions = await tower.enrichedMissions(state, input.missionIds)
68
+ if (includeArchived && index.ok) {
69
+ for (const entry of index.missions) {
70
+ if (activeIds.has(entry.id)) continue
71
+ if (wanted !== null && !wanted.has(entry.id)) continue
72
+ missions.push(archivedSummary(entry))
73
+ }
74
+ }
21
75
  return statusView({
22
76
  state,
23
77
  statePath: tower.paths.state,
24
78
  activity,
25
79
  unreadTower: tower.unreadFor(state, 'tower').length,
26
80
  config: tower.config,
27
- missions: await tower.enrichedMissions(state, input.missionIds),
81
+ missions,
82
+ archivedMissions: index.ok ? index.missions.length : null,
83
+ archive: archiveView(index),
28
84
  })
29
85
  }
30
86
 
package/app/review.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Independent review submission use case.
2
2
  import { withLock, writeJsonAtomic, appendJsonl } from '../lib/store.js'
3
+ import { sweepArchive } from './archive.js'
3
4
  import { validateState } from '../domain/state.js'
4
5
  import { TowerToolError } from '../domain/errors.js'
5
6
  import {
@@ -117,6 +118,7 @@ export async function submitReview(tower, input) {
117
118
  if (outcome.returnToActive) mission.status = 'active'
118
119
  mission.notes.push(outcome.note)
119
120
  mission.updatedAt = now
121
+ await sweepArchive(tower, state, { now })
120
122
  state.revision += 1
121
123
  state.updatedAt = now
122
124
 
package/app/teardown.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  teardownRemoveDecision,
22
22
  teardownRemovedDecision,
23
23
  } from '../domain/integration/teardown.js'
24
+ import { sweepArchive } from './archive.js'
24
25
 
25
26
  /**
26
27
  * Phase 1 (#51): decide every keep/remove outcome without holding the state
@@ -180,5 +181,25 @@ export async function teardown(tower, input) {
180
181
  setDecision(candidate.decision, teardownRemovedDecision(outcome))
181
182
  }
182
183
 
183
- return { ok: true, dryRun: false, removed, decisions: prepared.decisions }
184
+ // Archival runs under one final short lock. It is policy-driven (age or the
185
+ // hot-terminal cap), so a mission torn down a moment ago stays hot until it
186
+ // is old enough; `archive.afterDays: 0` archives it on this call.
187
+ const archive = await withLock(tower.paths, async () => {
188
+ const state = await tower.load()
189
+ const swept = await sweepArchive(tower, state, { now: Date.now(), activity: input.activity ?? {} })
190
+ if (swept.written) {
191
+ validateState(state, tower.paths.state)
192
+ await writeJsonAtomic(tower.paths.state, state)
193
+ }
194
+ return swept
195
+ })
196
+
197
+ return {
198
+ ok: true,
199
+ dryRun: false,
200
+ removed,
201
+ decisions: prepared.decisions,
202
+ archived: archive.archived,
203
+ ...(archive.error ? { archiveError: archive.error } : {}),
204
+ }
184
205
  }
package/app/tower.js CHANGED
@@ -20,6 +20,7 @@ import { checkCompletion, complete } from './checkCompletion.js'
20
20
  import { missionContext } from './missionContext.js'
21
21
  import { enrichedMissions } from './enrichedMissions.js'
22
22
  import { writeBoard, boardFile } from './board.js'
23
+ import { archiveIndex, archivedMission } from './archive.js'
23
24
  import { recordSpawnOutcome, recordInterrupt, reserveRoster, recordSpawn, rosterView } from './spawn.js'
24
25
  import { recordTestRun, recordLegacySnapshot, recordLegacyMap } from './recording.js'
25
26
 
@@ -85,6 +86,7 @@ function fail(message, code = 'invalid-argument') {
85
86
  }
86
87
 
87
88
  import { validateState } from '../domain/state.js'
89
+ import { loadStateFile } from '../lib/stateFile.js'
88
90
  import { boardMarkdown } from '../domain/report.js'
89
91
  import { diff, missionDetail, status } from './read.js'
90
92
  import { evaluateGate } from './gate.js'
@@ -142,12 +144,24 @@ export class Tower {
142
144
  return (await this.tryGit(cwd, ['merge-base', '--is-ancestor', ancestor, descendant].filter(Boolean))) !== null
143
145
  }
144
146
 
145
- async load() {
147
+ /**
148
+ * Read the state document, migrating an older schema in memory and (while the
149
+ * caller holds the state lock) persisting that migration with one revision
150
+ * bump and a `state.migrated` activity entry (#72).
151
+ * @param {{ persist?: boolean }} [options]
152
+ */
153
+ async load(options = {}) {
146
154
  if (!(await this.deps.store.exists(this.paths.state))) {
147
155
  fail(`tower is not initialized for ${this.root} — run tower_init first`, 'not-initialized')
148
156
  }
149
- const state = await this.deps.store.readJson(this.paths.state)
150
- return validateState(state, this.paths.state)
157
+ const loaded = await loadStateFile({
158
+ store: this.deps.store,
159
+ paths: this.paths,
160
+ now: this.deps.clock?.now ? this.deps.clock.now() : Date.now(),
161
+ persist: options.persist,
162
+ })
163
+ this.stateLoad = { ...loaded.version, migration: loaded.migration }
164
+ return loaded.state
151
165
  }
152
166
 
153
167
  /** #51 two-phase init: see the prepare/act/commit helpers in `./tower/init.js`. */
@@ -316,9 +330,23 @@ export class Tower {
316
330
  return missionDetail(this, missionId)
317
331
  }
318
332
 
319
- /** Human-readable mission board (markdown) for the current state. */
320
- boardMarkdown(state) {
321
- return boardMarkdown(state)
333
+ /**
334
+ * Human-readable mission board (markdown) for the current state.
335
+ * @param {import('../types/index.js').TowerState} state
336
+ * @param {{ count?: number, unreadable?: boolean, message?: string } | null} [archive]
337
+ */
338
+ boardMarkdown(state, archive = null) {
339
+ return boardMarkdown(state, archive)
340
+ }
341
+
342
+ /** Read `archive/index.json`; `ok:false` carries a diagnostic instead of throwing. */
343
+ archiveIndex() {
344
+ return archiveIndex(this)
345
+ }
346
+
347
+ /** Load one archived mission (full record) with its linked reviews and findings. */
348
+ archivedMission(missionId) {
349
+ return archivedMission(this, missionId)
322
350
  }
323
351
 
324
352
  async ensureExcluded(target) {
@@ -0,0 +1,38 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`GLOSSARY.md`** at the repo root, or
8
+ - **`GLOSSARY-MAP.md`** at the repo root if it exists: it points at one `GLOSSARY.md` per context. Read each one relevant to the topic.
9
+ - **`docs/adr/`**: read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
12
+
13
+ ## File structure
14
+
15
+ This repo is **single-context**: one `GLOSSARY.md` and one `docs/adr/` at the repo root.
16
+
17
+ ```
18
+ /
19
+ ├── GLOSSARY.md
20
+ ├── docs/adr/
21
+ │ ├── 0001-event-sourced-orders.md
22
+ │ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ (If the repo ever grows per-package contexts, switch to the multi-context layout: a root `GLOSSARY-MAP.md` pointing at per-context `GLOSSARY.md` files, with system-wide decisions in `docs/adr/` and context decisions in `src/<context>/docs/adr/`.)
27
+
28
+ ## Use the glossary's vocabulary
29
+
30
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `GLOSSARY.md`. Don't drift to synonyms the glossary explicitly avoids.
31
+
32
+ If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
33
+
34
+ ## Flag ADR conflicts
35
+
36
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
37
+
38
+ > _Contradicts ADR-0007 (event-sourced orders), but worth reopening because…_
@@ -0,0 +1,46 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Make an issue a sub-issue of a parent**: `gh issue create --parent <parent> ...`, or `gh issue edit <parent> --add-sub-issue <child>` afterwards (`gh` 2.94+). Older `gh`: `gh api --method POST repos/<owner>/<repo>/issues/<parent>/sub_issues -F sub_issue_id=<child-db-id>` (database id, as in **Blocking** below). Without sub-issues, put `Part of #<parent>` at the top of the child body.
11
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
12
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
13
+ - **Close**: `gh issue close <number> --comment "..."`
14
+
15
+ Infer the repo from `git remote -v`; `gh` does this automatically when run inside a clone. For this repo the remote is `louifinn/dsh-tower`.
16
+
17
+ ## Pull requests as a triage surface
18
+
19
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
20
+
21
+ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
22
+
23
+ - **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
24
+ - **List external PRs for triage**: `gh api --paginate 'repos/{owner}/{repo}/pulls?state=open' --jq '.[] | select(.author_association | IN("OWNER","MEMBER","COLLABORATOR") | not) | {number, title, author: .user.login, author_association, labels: [.labels[].name]}'`.
25
+ - **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
26
+
27
+ GitHub shares one number space across issues and PRs, so a bare `#42` may be either: resolve with `gh pr view 42` and fall back to `gh issue view 42`.
28
+
29
+ ## When a skill says "publish to the issue tracker"
30
+
31
+ Create a GitHub issue.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run `gh issue view <number> --comments`.
36
+
37
+ ## Wayfinding operations
38
+
39
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
40
+
41
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
42
+ - **Child ticket**: an issue linked to the map as a GitHub sub-issue (see **Make an issue a sub-issue of a parent**). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
43
+ - **Blocking**: GitHub's **native issue dependencies**, the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only, the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
44
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
45
+ - **Claim**: `gh issue edit <n> --add-assignee @me`, the session's first write.
46
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use. The right column currently equals the role name, so these are the defaults — `wontfix` already exists on the GitHub repo, while `needs-triage`, `needs-info`, `ready-for-agent`, and `ready-for-human` will be created by `/triage` on first use (or add them upfront with `gh label create`).
@@ -101,7 +101,9 @@ After the command, regardless of exit status, it inspects `HEAD`:
101
101
 
102
102
  | Path | Contents |
103
103
  |---|---|
104
- | `<git-common-dir>/dsh-tower/state.json` | missions, roster, reviews, findings, messages, reviews sequence |
104
+ | `<git-common-dir>/dsh-tower/state.json` | active missions, roster, reviews, findings, messages, reviews sequence |
105
+ | `<git-common-dir>/dsh-tower/archive/index.json` | compact terminal-mission index (ids, status, branch, timestamps) |
106
+ | `<git-common-dir>/dsh-tower/archive/<year>.json` | full archived missions plus their linked reviews/findings |
105
107
  | `<git-common-dir>/dsh-tower/activity.jsonl` | append-only activity |
106
108
  | `<git-common-dir>/dsh-tower/state.lock` | single-writer state lock (dead-PID reclaimed, live lock reported) |
107
109
  | `<git-common-dir>/dsh-tower/git.lock` | repo-scoped serialization for git mutations outside the state lock |
@@ -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
@@ -16,7 +16,7 @@ tables below define 32 stable blocker/failure codes.
16
16
  | `branch-missing` | No worktree/branch yet: spawn a worker first. |
17
17
  | `stale-base` | The branch does not contain the base tip: run `tower_rebase`. |
18
18
  | `read-only-survey` | Survey mission changed files; survey missions must be read-only. |
19
- | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. |
19
+ | `out-of-scope` | Changed files outside the mission scope; widen scope deliberately or revert. On a case-sensitive checkout a case-only mismatch is still rejected and the detail names the scope entry it would match if case were folded. |
20
20
  | `base-dirty` | The main checkout has uncommitted changes the merge would overwrite. |
21
21
  | `deps-unmerged` | A dependency mission is not merged/abandoned. |
22
22
  | `no-review` | `review.requireReview:true` and no review exists on the branch: spawn a reviewer (or disable the review gate). |
@@ -51,8 +51,12 @@
51
51
  | `gates.requireChecks` | `false` | Require at least one completion check (tower_complete.checks) before a mission can merge. |
52
52
  | `gates.requireTests` | `false` | Require a passing test-discovery receipt for the reviewed worktree tip before merge. |
53
53
  | `gates.allowReviewExemption` | `false` | Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers. |
54
+ | `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
55
  | `docs.board` | `true` | Write a human-readable MISSIONS.md after mission changes. |
55
56
  | `docs.boardLocation` | `"common"` | common = <git-common-dir>/dsh-tower/MISSIONS.md; repo = <repoRoot>/MISSIONS.md. |
57
+ | `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. |
58
+ | `archive.afterDays` | `30` | Terminal missions untouched for at least this many days are archived on the next mutation (0 archives them immediately). |
59
+ | `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
60
  | `tools.exposeOperational` | `true` | Master switch for batch/resource/import/legacy/recover/test/doctor tools. |
57
61
  | `tools.exposeBatch` | `true` | Register tower_batch. |
58
62
  | `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. |
@@ -8,26 +8,76 @@ It returns:
8
8
  "ok": true,
9
9
  "checks": [
10
10
  { "id": "git", "status": "pass", "detail": "git version 2.55.0" },
11
- { "id": "state", "status": "pass", "detail": ".../state.json revision 7" }
11
+ { "id": "state", "status": "pass", "detail": ".../state.json revision 7" },
12
+ { "id": "state-schema", "status": "pass", "detail": "schemaVersion 3 is current" },
13
+ { "id": "case-mode", "status": "pass", "detail": "caseInsensitive: true; scope matching uses case-insensitive comparison" }
12
14
  ],
15
+ "stateSchema": {
16
+ "currentVersion": 3,
17
+ "supportedVersion": 3,
18
+ "migrations": [],
19
+ "status": "current"
20
+ },
21
+ "archive": {
22
+ "missions": 3,
23
+ "archivedMissions": 12,
24
+ "stateBytes": 48211,
25
+ "archiveBytes": 190244,
26
+ "years": [2026],
27
+ "files": 2,
28
+ "unreadable": [],
29
+ "policy": { "enabled": true, "afterDays": 30, "maxMissions": 100 }
30
+ },
13
31
  "environmentContract": {
14
32
  "platform": "win32",
15
33
  "gitVersion": "git version 2.55.0",
16
34
  "repoRoot": "D:/repo",
17
35
  "commonDir": "D:/repo/.git",
18
- "stateDir": "D:/repo/.git/dsh-tower"
36
+ "stateDir": "D:/repo/.git/dsh-tower",
37
+ "caseInsensitive": true,
38
+ "comparisonMode": "case-insensitive"
19
39
  }
20
40
  }
21
41
  ```
22
42
 
43
+ `stateSchema` describes the file **on disk**, not the in-memory document: `currentVersion` is the
44
+ `schemaVersion` doctor read, `supportedVersion` is the build's `SCHEMA_VERSION`, and `migrations` is
45
+ the ordered v→v path a write would apply (`[]` when current). `status` is one of `current`,
46
+ `migratable`, `newer`, `unmigratable`, `invalid`. Doctor never migrates, so a `migratable` file stays
47
+ byte-identical and the `state-schema` check is a `warn` (the next locked write applies the migration
48
+ and keeps a `state.json.v<N>.bak` backup). A file written by a newer plugin reports `newer` and a
49
+ `fail`: this build refuses to downgrade it. A state-load failure is reported as a failed check rather
50
+ than thrown so doctor remains useful on a broken repository.
51
+
52
+ `archive` reports the terminal-mission archive: active `missions`, `archivedMissions` and `stateBytes`,
53
+ the archive directory's `archiveBytes`/`years`/`files`, every `unreadable` diagnostic, and the configured
54
+ `policy` (`enabled`, `afterDays`, `maxMissions`). A missing archive directory is a healthy empty archive
55
+ (`archive` check `pass`); a truncated index or year file is a `warn` with the diagnostic, and archiving is
56
+ paused until it is repaired rather than overwriting it. Nothing is lost while the archive is unreadable:
57
+ the eligible missions stay in `state.json`.
58
+
23
59
  Checks cover: git availability, repository identity, state schema/load, repo config presence,
24
60
  worker/reviewer preset names, subagents and HMR services, workspace lock holder/staleness, live roster,
25
- worktree existence/ownership/dirtiness, test-discovery policy, legacy snapshots, batches and leases.
26
- Missions intentionally cleaned up by `tower_teardown` are skipped (the check notes the removed count)
27
- instead of being reported as missing.
28
- `warn` is advisory; `fail` means the repository needs attention before spawning/merging. A state-load
29
- failure is reported as a failed check rather than thrown so doctor remains useful on a broken
30
- repository.
61
+ worktree existence/ownership/dirtiness, test-discovery policy, completion-report sizes, archive health, legacy`nsnapshots, batches and leases. Missions intentionally cleaned up by `tower_teardown` are skipped (the check`nnotes the removed count) instead of being reported as missing.
62
+
63
+ `caseInsensitive` is the mode `tower_init` detected once on the checkout's filesystem; `null` means
64
+ "not detected yet" and the `case-mode` check is a `warn`. `comparisonMode` is the comparison scope
65
+ matching actually uses: `case-insensitive` folds both sides, `case-sensitive` stays byte-exact. Doctor
66
+ never probes — a probe creates a temporary file, and a read-only call must not write. Run
67
+ `tower_init` on a writable checkout to record the mode; until then the safe `case-sensitive` default
68
+ applies.
69
+
70
+ Checks cover: git availability, repository identity, state schema/load, the detected case mode, repo
71
+ config presence, worker/reviewer preset names, subagents and HMR services, workspace lock
72
+ holder/staleness, live roster, worktree existence/ownership/dirtiness, test-discovery policy, legacy
73
+ snapshots, batches and leases. Missions intentionally cleaned up by `tower_teardown` are skipped (the
74
+ check notes the removed count) instead of being reported as missing.
75
+ `warn` is advisory; `fail` means the repository needs attention before spawning/merging.
76
+
77
+ The `reports` check names the largest completion report in the repository (`<bytes> B on <mission>`,
78
+ against the `gates.maxReportBytes` cap); the same numbers are returned as
79
+ `reports: { count, largest: { missionId, bytes } | null, capBytes }`. Reports written before the cap
80
+ only produce a `warn`; doctor never rewrites them.
31
81
 
32
82
  The environment contract is informational; it is not an identity or authorization proof. `tower_init`
33
83
  still requires the tower role, and `tower_spawn` still checks the live subagent catalog.
@@ -13,6 +13,8 @@
13
13
  | `stale-base` | The repository state changed while the operation was being prepared; retry. |
14
14
  | `state-locked` | Another Tower writer holds the state lock. |
15
15
  | `state-corrupt` | The Tower state file is unreadable or invalid. |
16
+ | `state-version-unsupported` | The Tower state file uses a schema version this build cannot read or downgrade. |
17
+ | `state-migration-failed` | A Tower state migration failed; the original state file was not modified. |
16
18
  | `config-invalid` | Tower configuration is invalid. |
17
19
  | `config-unknown-key` | Tower configuration has an unknown top-level key. |
18
20
  | `git-failed` | A git command failed. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tool failures are thrown errors, not `{ok:false}` results (gate outcomes are the exception: see
4
4
  [blockers.md](blockers.md)). Every thrown error carries a stable code as a message prefix. This
5
- release defines 33 tool error codes.
5
+ release defines 35 tool error codes.
6
6
 
7
7
  Every `TowerToolError` also carries `retryable` (boolean), `nextAction` (string or `null`) and `details`
8
8
  (structured context or `null`) for programmatic callers; the message prefix remains the stable contract
@@ -26,7 +26,9 @@ plain `Error` without a structured code is reported as `tool-internal`.
26
26
  | `stale-base` | The repository state changed while the operation was being prepared; retry. | two-phase mutation revalidation |
27
27
  | `worktree-dirty` | The mission worktree has uncommitted changes and cannot complete. | staged, modified, or untracked files left behind before `tower_complete` |
28
28
  | `state-locked` | Another Tower writer holds the state lock. | concurrent mutation after the lock timeout |
29
- | `state-corrupt` | The Tower state file is unreadable or invalid. | hand-edited `state.json` |
29
+ | `state-corrupt` | The Tower state file is unreadable or invalid. | hand-edited `state.json`; invalid JSON; no usable `schemaVersion` |
30
+ | `state-version-unsupported` | The state file's schema version is newer than this build supports, or cannot be migrated. | opening a repository written by a newer plugin; an unknown older version; this build refuses to downgrade |
31
+ | `state-migration-failed` | A state migration threw; the original file was not modified. | a bug in a migration step; see the pre-migration `state.json.v<N>.bak` |
30
32
  | `config-invalid` | Tower configuration is invalid. | malformed repo `config.json`, a value that violates the schema |
31
33
  | `config-unknown-key` | Tower configuration has an unknown top-level key. | a typo such as `allowAgentToggle` |
32
34
  | `git-failed` | A git command failed for a reason other than the delegated cases. | a rejected merge command, invalid ref |
@@ -6,19 +6,19 @@
6
6
  |---|---|
7
7
  | `tower_adopt` | Reconcile the roster with the durable subagent catalog after a restart or new session. Agents that are provably gone are marked retired (records are kept) or missing (retireMissing:false, so the tower can inspect/take over); agents whose parent catalog cannot be read stay unknown and are never changed. Only the tower may adopt. |
8
8
  | `tower_batch` | Manage finite evidence batches: create freezes missionIds/requiredChecks/receiptPaths, status summarizes members, close verifies every required check and receipt hash, abandon retires a stale open batch. Membership and exclusions are immutable after create; close/abandon never remove missions. |
9
- | `tower_complete` | Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report; completion means ready for preflight and optional review, not merged. |
9
+ | `tower_complete` | Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report, capped at gates.maxReportBytes (default 32768 UTF-8 bytes); a larger report is rejected with invalid-argument and an actionable nextAction, never truncated. Persists reportBytes on the stored report. completion means ready for preflight and optional review, not merged. |
10
10
  | `tower_diff` | Read-only view of a mission branch for review: merge base, changed files, commits, and a capped patch against the recorded base. Safe for reviewers; it never modifies the repository. |
11
- | `tower_doctor` | Read-only environment and repository health check: git, state schema, config, presets/subagents/HMR, workspace lock, roster staleness, worktree ownership, test discovery, leases. Returns checks with pass/warn/fail and an environment contract. Never writes state. |
11
+ | `tower_doctor` | Read-only environment and repository health check: git, state schema, case-sensitivity detection (caseInsensitive and the active scope comparison mode), config, presets/subagents/HMR, workspace lock, roster staleness, worktree ownership, test discovery, leases, and the terminal-mission archive (count, bytes, years, unreadable diagnostics and the configured afterDays/maxMissions policy). Returns checks with pass/warn/fail and an environment contract. Never writes state and never probes the filesystem. |
12
12
  | `tower_finding` | File a structured finding (bug / improve / vuln / idea) for the tower to route. Use this for anything notable OUTSIDE your mission scope instead of editing out-of-scope files. Severity is one of low|medium|high|critical. Use tower_findings to list, read, or close findings later. |
13
13
  | `tower_findings` | List, read, or update findings for a repository. Without id it lists findings newest-first with counts by status (filters: status, type, severity, missionId, open, limit). With id it returns that finding; with id plus status and/or note it records a lifecycle transition. Statuses are open -> triaged -> fixed|wontfix, and fixed/wontfix can be reopened to open. Only the tower or a worker may change a finding; reviewers may only list and read. Read-only when no id is given. |
14
14
  | `tower_import_legacy` | Read-only import of a Tower Codex (schemaVersion 1) or DSH legacy state file. Stores the full payload under <common>/dsh-tower/legacy/<sha256>.json and records only a summary in state.legacySnapshots; never activates, adopts, or resumes paused missions. |
15
15
  | `tower_inbox` | Read your tower inbox: messages addressed to you plus broadcasts, newest first. Only returned messages are acknowledged; the result reports unreadRemaining, so keep reading until it reaches 0. Set include_read for history. Unread messages block tower_complete. |
16
- | `tower_init` | Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize. |
16
+ | `tower_init` | Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Probes whether the checkout filesystem folds case and records caseInsensitive once, so scope matching follows the real filesystem (a skipped probe records null and keeps the safe case-sensitive comparison). Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize. |
17
17
  | `tower_interrupt` | Interrupt one running tower roster agent (worker or reviewer) and record it as interrupted in the roster. Only the tower may interrupt. Idempotent: an agent that is not running returns interrupted:false. Use it before takeover after a runaway worker, or to stop work that is no longer wanted. |
18
18
  | `tower_legacy_map` | Audit-only mapping between a legacy snapshot id and a current mission. map records sourceHash/oldId/newMissionId; status lists mappings. Mapping never adopts or resumes a legacy mission. |
19
19
  | `tower_merge` | Merge a mission branch into the recorded base branch with --no-ff, after the hard gate passes. Verifies the two merge parents and records the mission as merged. If the merge commit already landed (manual merge, crashed or slow pre-merge hook), it detects the merge commit and records it as reconciled instead of merging again. A failed gate returns stable blocker codes without merging; a hook failure aborts cleanly and returns "merge-hook-failed" while a real content conflict returns "merge-conflict". |
20
20
  | `tower_mission` | Update one mission: lifecycle status, decision note, blocker, clear blockers, mark a task done, drop a task with a reason, or widen the scope (tower only). "merged" is never set here; only a future tower_merge records it. Merged and abandoned are terminal. |
21
- | `tower_plan` | Plan one or more missions in an initialized repository. Each mission has a printable-ASCII title, relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles. |
21
+ | `tower_plan` | Plan one or more missions in an initialized repository. Each mission has a printable-Unicode title (NFC-normalized, at most 120 characters), relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles. |
22
22
  | `tower_preflight` | Read-only merge gate for one mission: returns ok plus stable blocker codes (branch-missing, deps-unmerged, read-only-survey, stale-base, out-of-scope, base-dirty, branch-owned-by-closed-missions, and mission/worker guards). Review blockers (no-review, review-hold, not-clean, review-not-approved, tip-moved, review-mission-mismatch) apply only when review.requireReview is on. Never changes anything. |
23
23
  | `tower_rebase` | Rebase a mission branch onto the current base tip inside its own worktree. Refuses while its worker is running or the worktree is dirty. A clean conflict-free rebase waives re-review for the old review; a conflict is aborted and the mission is marked blocked. If the branch is already contained in the base, it returns alreadyMerged and leaves the branch untouched so tower_merge can record the landed merge. |
24
24
  | `tower_recover` | Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. It never resets Tower state or worktrees. |
@@ -28,6 +28,6 @@
28
28
  | `tower_roster` | List tower roster agents with authoritative live activity from the subagent catalog (running/inactive/missing/unknown). Read-only; never resumes children. Use before completion or merge decisions. |
29
29
  | `tower_send` | Send a durable inbox message to a tower participant: a roster agent name, "tower", or "all" (broadcast). The message is persisted first; when the recipient is a currently running roster agent the tower also steers it into that child's current turn. Returns delivery results. |
30
30
  | `tower_spawn` | Spawn a worker or reviewer for a mission. Workers use the tower-worker preset; reviewers require a completed mission and use the read-only tower-reviewer preset. Provisions (or reuses) the mission git worktree and branch, generates a self-contained briefing, and starts a background subagent in that worktree. The child final report is delivered back to this session; tower_roster shows authoritative activity. Records the agent in the roster; reviewer spawns record the branch tip so a reviewer that modifies the branch cannot submit a review. Returns immediately unless wait=true. |
31
- | `tower_status` | Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, config path and the activity tail. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; format:"markdown" returns the human-readable mission board. Read-only. |
32
- | `tower_teardown` | Tear down eligible mission worktrees after their missions are merged or abandoned. A worktree whose roster agent is running is always kept (force cannot override); dirty worktrees need force; exclude keeps named missions. dry_run previews the remove/keep decisions without changing anything. |
31
+ | `tower_status` | Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, archive count, config path and the activity tail. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report; archived missions are loaded from their year file); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; includeArchived appends compact archived summaries after the active page; format:"markdown" returns the human-readable mission board (active missions plus an archived count). Read-only. |
32
+ | `tower_teardown` | Tear down eligible mission worktrees after their missions are merged or abandoned. A worktree whose roster agent is running is always kept (force cannot override); dirty worktrees need force; exclude keeps named missions. dry_run previews the remove/keep decisions without changing anything. Terminal missions whose worktree is gone and whose configured archive policy is due (archive.afterDays / archive.maxMissions) are moved to <git-common-dir>/dsh-tower/archive/ so state.json stops rewriting them; nothing is deleted. |
33
33
  | `tower_test` | Run the mission worktree's declared tower.testDiscovery (package.json) and record the receipt. Returns ok:false with diagnostics when discovery fails; does not change mission status. Use tower_complete for automatic admission checks. |