dsh-tower 1.2.0 → 2.0.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.
- package/CHANGELOG.md +96 -0
- package/CONTRIBUTING.md +36 -0
- package/README.en.md +60 -23
- package/README.md +54 -21
- package/SECURITY.md +20 -0
- package/cordis.patch.yml +32 -0
- package/docs/README.md +17 -0
- package/docs/architecture/overview.md +70 -0
- package/docs/examples/first-mission.md +58 -0
- package/docs/faq.md +74 -0
- package/docs/guide/hot-reload.md +189 -0
- package/docs/guide/install-upgrade-migration.md +59 -0
- package/docs/guide/quickstart.md +97 -0
- package/docs/operations/runbook.md +80 -0
- package/docs/operations/security.md +62 -0
- package/docs/reference/blockers.md +58 -0
- package/docs/reference/config.md +114 -0
- package/docs/reference/doctor.md +31 -0
- package/docs/reference/errors.md +50 -0
- package/docs/reference/events.md +45 -0
- package/docs/reference/operations.md +52 -0
- package/docs/reference/testing.md +61 -0
- package/docs/reference/tools.md +75 -0
- package/docs/zh/README.md +58 -0
- package/docs/zh/faq.md +72 -0
- package/docs/zh/guide/hot-reload.md +178 -0
- package/docs/zh/guide/install-upgrade-migration.md +57 -0
- package/docs/zh/guide/quickstart.md +96 -0
- package/docs/zh/operations/security.md +42 -0
- package/docs/zh/reference/blockers.md +62 -0
- package/docs/zh/reference/config.md +113 -0
- package/docs/zh/reference/doctor.md +30 -0
- package/docs/zh/reference/errors.md +52 -0
- package/docs/zh/reference/events.md +46 -0
- package/docs/zh/reference/operations.md +48 -0
- package/docs/zh/reference/testing.md +60 -0
- package/docs/zh/reference/tools.md +75 -0
- package/index.js +293 -49
- package/lib/batch.js +578 -0
- package/lib/config.js +381 -0
- package/lib/doctor.js +165 -0
- package/lib/errors.js +111 -0
- package/lib/hmr.js +196 -0
- package/lib/leases.js +317 -0
- package/lib/legacy.js +413 -0
- package/lib/modeStore.js +47 -16
- package/lib/store.js +24 -1
- package/lib/tests.js +628 -0
- package/lib/tools.js +1594 -160
- package/lib/tower.js +1025 -101
- package/lib/workspaceLock.js +117 -0
- package/lib/worktreeOwnership.js +166 -0
- package/package.json +21 -4
- package/reviewer.js +40 -0
- package/scripts/check-docs.mjs +135 -0
- package/scripts/enable-dev-hmr.mjs +347 -0
- package/skills/tower-recovery.md +40 -0
- package/skills/tower-review.md +16 -0
- package/skills/tower-worker-protocol.md +15 -0
- package/skills.js +63 -0
- package/worker.js +7 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Example: first mission, end to end
|
|
2
|
+
|
|
3
|
+
Assume a repository at `D:\work\demo` with a clean `main` branch and one file `README.md`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/tower on
|
|
7
|
+
tower_init repoRoot=D:\work\demo
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`s` is a placeholder for tool JSON arguments; DSH accepts the object form:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"repoRoot":"D:\\work\\demo","missions":[{"title":"Add greeting","scope":["src/**"],"tasks":["write src/greet.txt","commit the change"],"kind":"build"}]}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`tower_plan` returns `M1`. Spawn the worker:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{"repoRoot":"D:\\work\\demo","missionId":"M1","role":"worker"}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Wait for the worker's final report (delivered to this session) or check `tower_roster`. The worker
|
|
23
|
+
creates `src/greet.txt`, commits on `tower/m1-add-greeting`, marks both tasks done, and calls
|
|
24
|
+
`tower_complete`.
|
|
25
|
+
|
|
26
|
+
Spawn the reviewer:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"repoRoot":"D:\\work\\demo","missionId":"M1","role":"reviewer"}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The read-only reviewer inspects `tower_diff`, then calls `tower_review` with:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{"repoRoot":"D:\\work\\demo","missionId":"M1","status":"clean","merge":"merge","findings":"none","decision":"single in-scope file; content correct"}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Gate and merge:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"repoRoot":"D:\\work\\demo","missionId":"M1"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Call `tower_preflight` with the same arguments; expect `ok: true`. Then `tower_merge`, and finally
|
|
45
|
+
`tower_teardown` with `{"repoRoot":"D:\\work\\demo"}`.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
/tower off
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Typical expected output after merge:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{"ok":true,"mergedCommit":"<sha>","baseTip":"<old>","branchTip":"<sha>","parents":["<old>","<sha>"]}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the merge hook is slow, add `"timeoutMs": 1800000` to `tower_merge`; if a merge commit landed but the
|
|
58
|
+
call failed, re-run `tower_merge` and it records the existing commit instead of merging again.
|
package/docs/faq.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# FAQ
|
|
2
|
+
|
|
3
|
+
**Why did `tower_preflight` fail with `no-review` right after the worker finished?**
|
|
4
|
+
That is expected: a completed mission awaits review. Spawn a reviewer; after a clean review the gate
|
|
5
|
+
turns green.
|
|
6
|
+
|
|
7
|
+
**Do I need to poll the worker?**
|
|
8
|
+
No. The worker's final report is delivered to this session; `tower_roster` shows live activity. Polling
|
|
9
|
+
wastes turns.
|
|
10
|
+
|
|
11
|
+
**`/tower` says the mode is on but `tower_control` is missing.**
|
|
12
|
+
`mode.allowAgentControl` is off. Set it to `true` in the profile patch and restart DSH.
|
|
13
|
+
|
|
14
|
+
**The merge hook takes minutes and `tower_merge` failed.**
|
|
15
|
+
Retry with a larger `timeoutMs`: `tower_merge({ missionId, timeoutMs: 1800000 })`. The default is 15
|
|
16
|
+
minutes and merge output is buffered up to 64 MB. A hook failure returns `merge-hook-failed` and leaves
|
|
17
|
+
the repo clean for a retry.
|
|
18
|
+
|
|
19
|
+
**A merge commit exists but Tower says the mission is not merged.**
|
|
20
|
+
Call `tower_merge` again. It detects the landed merge commit (`reconciled`) or recovers a failed call
|
|
21
|
+
whose commit exists (`recoveredFromFailure`) and records it.
|
|
22
|
+
|
|
23
|
+
**`tower_rebase` returned `alreadyMerged` and changed nothing.**
|
|
24
|
+
Correct: the branch is already contained in the base. Call `tower_merge` to record the landed merge
|
|
25
|
+
instead of rebasing.
|
|
26
|
+
|
|
27
|
+
**Can a reviewer edit files?**
|
|
28
|
+
No. Reviewers use the read-only `tower-reviewer` preset (no write/edit/pwsh); a branch changed after the
|
|
29
|
+
reviewer spawned is rejected. Reviewers file `tower_finding` for issues outside the mission.
|
|
30
|
+
|
|
31
|
+
**What is the difference between `tower_finding` and `tower_findings`?**
|
|
32
|
+
`tower_finding` creates one finding (`open`). `tower_findings` is the lifecycle tool: list/read, and
|
|
33
|
+
transition `open → triaged → fixed`/`wontfix` (reopen allowed). Only the tower or a worker may change the
|
|
34
|
+
status; a roster reviewer may list and read.
|
|
35
|
+
|
|
36
|
+
**How do I set config for one repository only?**
|
|
37
|
+
Create `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/config.json`) with the same
|
|
38
|
+
JSON groups as the profile config (`review`, `merge`, `worktrees`, `spawn`, and legacy aliases). The
|
|
39
|
+
repository layer wins over the profile; per-call tool arguments win over both. No restart is required
|
|
40
|
+
because the file is read when a Tower tool opens the repository.
|
|
41
|
+
|
|
42
|
+
**Can workers use Tower tools to merge or spawn?**
|
|
43
|
+
No. The worker preset contains mission/progress tools only; merge and spawn live in the parent tower.
|
|
44
|
+
|
|
45
|
+
**A worker is stuck or runaway. How do I stop it?**
|
|
46
|
+
Use `tower_interrupt({to:"worker-m1",reason:"..."})`; it calls the host subagent interrupt and records the
|
|
47
|
+
roster entry as `interrupted`. If you already interrupted it and want a new owner, spawn with
|
|
48
|
+
`takeover:true`. A still-running worker cannot be taken over (`takeover-refused`).
|
|
49
|
+
|
|
50
|
+
**Two sessions want to drive the same repository.**
|
|
51
|
+
Enable `mode.workspaceLock:true`: the first Tower session to call a repo-scoped tool owns
|
|
52
|
+
`<git-common-dir>/dsh-tower/tower.lock`; other sessions get `[tower:workspace-busy]` until the owner
|
|
53
|
+
runs `/tower off` or the lock is stale (`mode.workspaceStaleMs`).
|
|
54
|
+
|
|
55
|
+
**Can I reload the plugin without restarting DSH?**
|
|
56
|
+
Yes, in a profile with the DSH HMR service: `/tower reload` or the `tower_reload` tool reconciles the
|
|
57
|
+
profile and replaces the changed linked package. Run `npm run dev:hmr` once to add the repository to the
|
|
58
|
+
profile's HMR watch roots (then restart DSH once); see [hot reload](guide/hot-reload.md).
|
|
59
|
+
`plugin_manager` install/update already applies live.
|
|
60
|
+
|
|
61
|
+
**How do I make the completion evidence mandatory?**
|
|
62
|
+
Workers can pass `checks` to `tower_complete`; set `gates.requireChecks:true` to block merging when a
|
|
63
|
+
completed mission has no checks (`no-check-evidence`).
|
|
64
|
+
|
|
65
|
+
**Where does Tower store state?**
|
|
66
|
+
`<git-common-dir>/dsh-tower/state.json`, `activity.jsonl` and the human-readable `MISSIONS.md`; mode state
|
|
67
|
+
in `$DSH_HOME/tower-mode/modes.json`. Worktrees live next to the checkout in `.dsh-tower-worktrees/`.
|
|
68
|
+
|
|
69
|
+
**How do I change the review round cap or presets?**
|
|
70
|
+
Use `review.maxRoundsPerReviewer` and `spawn.workerPreset`/`spawn.reviewerPreset`; see
|
|
71
|
+
[config](reference/config.md). Restart DSH after a profile config change.
|
|
72
|
+
|
|
73
|
+
**Does Tower send data anywhere?**
|
|
74
|
+
No. It has no telemetry and no network service; all state is local.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Hot reload (development)
|
|
2
|
+
|
|
3
|
+
Tower is a Host plugin: its tools and prompt sections are loaded when DSH boots. DSH already ships
|
|
4
|
+
`@deepseek-ai/dsh-hmr`, a service that **reconciles the profile in place** — it re-reads the profile
|
|
5
|
+
patch layers, replaces modules whose files changed (including a linked package such as a `dsh-tower`
|
|
6
|
+
checkout), restarts the affected Loader entries and reports anything that needs a full restart. Tower
|
|
7
|
+
uses that service in development so Host-side changes apply without restarting DSH.
|
|
8
|
+
|
|
9
|
+
This page covers the development loop. It is not required to use Tower.
|
|
10
|
+
|
|
11
|
+
## What HMR does and does not do
|
|
12
|
+
|
|
13
|
+
`dsh --profile <name> --dump-config` prints the composed hmr row:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
- id: hmr
|
|
17
|
+
name: '@deepseek-ai/dsh-hmr'
|
|
18
|
+
disabled: !!js '!ctx.get(''profileContext'')'
|
|
19
|
+
config:
|
|
20
|
+
root: []
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `root: []` is the shipped default: **no source files are watched**. HMR is mounted but idle for
|
|
24
|
+
file-watching, so a running DSH does not reload when you edit the checkout.
|
|
25
|
+
- A reconciliation can still be requested explicitly (`/tower reload`, `tower_reload` or
|
|
26
|
+
`plugin_manager`); it compares already-loaded modules with disk and replaces the changed ones even
|
|
27
|
+
with `root: []`. Watching only makes that happen automatically after a save.
|
|
28
|
+
- HMR is only mounted for profile boots (`disabled` when there is no `profileContext`). A process
|
|
29
|
+
booted without a profile reports reload as unavailable (`tower_reload` stays registered while
|
|
30
|
+
`dev.hotReload` is on, so the tool returns `reload-unavailable` instead of disappearing).
|
|
31
|
+
- The profile patch layer is loaded when DSH boots. Editing it — in practice the hmr `root` list —
|
|
32
|
+
takes effect after **one DSH restart**. Nothing in the running process re-reads the HMR row's own
|
|
33
|
+
configuration.
|
|
34
|
+
|
|
35
|
+
## Enable watching for a checkout
|
|
36
|
+
|
|
37
|
+
From a checkout of this repository:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
npm run dev:hmr
|
|
41
|
+
# equivalent to:
|
|
42
|
+
node scripts/enable-dev-hmr.mjs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The script appends one marked block to the profile's user patch layer,
|
|
46
|
+
`<DSH_HOME>/profiles/<profile>/cordis.patch.yml` (`DSH_HOME` defaults to `~/.dsh`; on Windows
|
|
47
|
+
usually `C:\Users\<you>\.dsh`):
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# dsh-tower:dev-hmr
|
|
51
|
+
- id: hmr
|
|
52
|
+
config:
|
|
53
|
+
root:
|
|
54
|
+
- 'D:/work/dsh-tower'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
|
|
59
|
+
| Flag | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `--profile <name>` | Profile to edit. Default `desktop` (the profile the desktop app boots). |
|
|
62
|
+
| `--root <dir>` | Repository directory to watch. Default: the current working directory, canonicalised. |
|
|
63
|
+
| `--dry-run` | Print a diff of the patch file and exit without writing. |
|
|
64
|
+
| `--remove` | Delete the marked block again. |
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
|
|
68
|
+
- The script is idempotent: re-running it replaces the marked block and leaves every other line of the
|
|
69
|
+
patch file untouched. Run it from the repository root, or pass `--root D:\work\dsh-tower`.
|
|
70
|
+
- The value written is the `realpath` of the root, with forward slashes. **HMR `root` accepts
|
|
71
|
+
absolute paths**; relative entries would resolve against the profile directory and are easy to get
|
|
72
|
+
wrong, so the script always writes an absolute one.
|
|
73
|
+
- The profile must already exist. The script never creates a profile and never creates directories
|
|
74
|
+
outside `<DSH_HOME>/profiles/<profile>/`.
|
|
75
|
+
- Pick the profile the desktop app actually boots (`desktop` by default). The desktop profile is
|
|
76
|
+
managed by the Electron app, so restart it by quitting and relaunching DSH.
|
|
77
|
+
|
|
78
|
+
**Restart DSH once** after writing the block. After that restart:
|
|
79
|
+
|
|
80
|
+
- HMR watches the checkout (except `node_modules`, dot-directories, `cache` and `data`, which the
|
|
81
|
+
shipped `ignored` list excludes) and reconciles the profile after edits while DSH stays open.
|
|
82
|
+
- `/tower reload` (and the `tower_reload` tool, when registered) reconciles on demand even for files
|
|
83
|
+
the watcher does not cover.
|
|
84
|
+
|
|
85
|
+
To stop watching, run `npm run dev:hmr -- --remove` and restart DSH once.
|
|
86
|
+
|
|
87
|
+
## The edit loop
|
|
88
|
+
|
|
89
|
+
1. Edit `lib/*.js`, `index.js`, `worker.js`, `reviewer.js` or `skills.js`.
|
|
90
|
+
2. Save the file. The watcher debounces (DSH default 100 ms) and reconciles the profile; changed
|
|
91
|
+
modules are replaced and the affected Loader entries restart.
|
|
92
|
+
3. Watch the DSH log for `reload plugin at <path>` / `restart required: …` messages.
|
|
93
|
+
4. Optionally call `/tower reload` or `tower_reload` to reconcile on demand instead of waiting.
|
|
94
|
+
|
|
95
|
+
Editing the repository's bundle patch (`cordis.patch.yml`) changes the plugin declaration itself;
|
|
96
|
+
restart DSH after such a change rather than assuming a hot replacement.
|
|
97
|
+
|
|
98
|
+
The bridge behind the tool follows the `dev` config group:
|
|
99
|
+
|
|
100
|
+
| Config key | Meaning |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `dev.hotReload` | Register `tower_reload`; `/tower reload` is disabled when off (default `true`). Without the HMR service the tool is still registered so it can return `reload-unavailable`. |
|
|
103
|
+
| `dev.reloadDelayMs` | Delay before the scheduled reconciliation runs, in milliseconds (default `50`), so the tool/command result reaches the model before the plugin is replaced. |
|
|
104
|
+
| `dev.reloadRequiredIds` | Loader entry ids the profile must keep active (default `tower`, `tower-skills`, `preset-tower-worker`, `preset-tower-reviewer`). An inactive required entry is reported as a reload failure even if it was already inactive. |
|
|
105
|
+
|
|
106
|
+
A reload is **scheduled, not awaited**: `/tower reload` and `tower_reload` return immediately with
|
|
107
|
+
`scheduled`, `delayMs`, `requiredIds` (and the current `lastError` when one is pending), then HMR
|
|
108
|
+
reconciles the profile after the delay. This is deliberate — the tool result must be delivered before
|
|
109
|
+
the Tower plugin replaces itself. Two consequences:
|
|
110
|
+
|
|
111
|
+
- a burst of reload requests inside the delay coalesces into a single reconciliation;
|
|
112
|
+
- a reconciliation failure is recorded as `lastError` instead of coming back from the tool call, so
|
|
113
|
+
check `tower_mode.hotReload` and the DSH log after a reload.
|
|
114
|
+
|
|
115
|
+
### Reading a reload result
|
|
116
|
+
|
|
117
|
+
The outcome of the scheduled reconciliation surfaces in:
|
|
118
|
+
|
|
119
|
+
- **the DSH log** — `reload plugin at <path>` for replaced modules, plus HMR's warnings and
|
|
120
|
+
`restart required: <reason>` messages from its own watcher.
|
|
121
|
+
- **`tower_mode.hotReload`** — `{available, requiredIds, delayMs, scheduled, lastRunAt, lastError}`;
|
|
122
|
+
`lastError` keeps the most recent failure and is cleared by the next successful reload.
|
|
123
|
+
|
|
124
|
+
The reconciliation itself also returns `warnings`, `pending` (modules still on their previous code)
|
|
125
|
+
and an optional `restartReason`. When the reload was triggered from Tower those are not returned to
|
|
126
|
+
the model, so treat "new code does not appear" as a signal to restart DSH rather than to retry in a
|
|
127
|
+
loop.
|
|
128
|
+
|
|
129
|
+
If HMR is absent, `/tower reload` answers with an unavailable message and `tower_reload` is not
|
|
130
|
+
registered at all (both are gated on the HMR service at session start).
|
|
131
|
+
|
|
132
|
+
## Plugin manager and linked packages
|
|
133
|
+
|
|
134
|
+
`plugin_manager` actions (`install_bundle`, `set_plugin`, upgrades) apply to the running profile
|
|
135
|
+
immediately — they reconcile the profile themselves, so installing or updating a bundle does not
|
|
136
|
+
require a restart. This is the intended way to install `dsh-tower` while developing: add the checkout
|
|
137
|
+
as a bundle (`dsh plugin --profile <profile> add <repo-root>` or `install_bundle`), then let HMR and
|
|
138
|
+
the reload tool pick up code edits.
|
|
139
|
+
|
|
140
|
+
With the checkout linked as a package target, a changed file inside the checkout is replaced by the
|
|
141
|
+
next `reconcileProfile` or `plugin_manager` operation **without a restart**, even when `root: []`
|
|
142
|
+
means the change was not watched. What does need a restart:
|
|
143
|
+
|
|
144
|
+
- the hmr row's own configuration (`root`), because that list is read when the service activates;
|
|
145
|
+
- modules reported under `pending` or with a `restartReason`.
|
|
146
|
+
|
|
147
|
+
## Failure recovery
|
|
148
|
+
|
|
149
|
+
| Symptom | What to do |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `[tower:reload-unavailable]` / "Tower reload is unavailable" | This process has no HMR service (booted without a profile, HMR disabled in the profile, or the service was disposed during shutdown). Restart DSH. To inspect the hmr row, run `dsh --profile <name> --dump-config` on a non-desktop profile (the desktop profile is managed by the Electron app and refuses the dump). |
|
|
152
|
+
| `[tower:reload-failed]` in `hotReload.lastError` with a syntax/import error | Fix the file, then restart DSH. A replacement that fails to import stops the previous plugin instance and its old code no longer runs; the reload itself cannot roll back. |
|
|
153
|
+
| `HMR is disposed` | The service shut down with the process. Restart DSH. |
|
|
154
|
+
| `HMR transactions cannot be nested` | An explicit reload was requested from inside another HMR transaction; call the tool from a normal tool turn instead. |
|
|
155
|
+
| `pending` is non-empty or `restartReason` is set | Restart DSH before trusting the reload result. |
|
|
156
|
+
| Edit saved but nothing reloads | Confirm the file is under the configured `root` (absolute path), is not excluded by `ignored` (`node_modules`, dot-directories, `cache`, `data`), and that the profile patch contains the marked block. Run `/tower reload` to reconcile manually. |
|
|
157
|
+
| Reload keeps reporting an inactive required entry | That Loader entry is disabled or failing: check the profile's plugins/bundles (a `preset-tower-*` entry is missing when a Tower preset bundle was removed) and restart after fixing it. |
|
|
158
|
+
|
|
159
|
+
The offline bridge tests are part of the normal suite:
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
node --test tests/hmr.test.mjs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Client bundles (`ui-bundle`) are different
|
|
166
|
+
|
|
167
|
+
HMR replaces **Host** plugins and prompt/tool code. The optional `ui-bundle` is a Client widget: its
|
|
168
|
+
JavaScript is downloaded and evaluated by the Web page, not by the Host process. After a Host reload
|
|
169
|
+
or a Client bundle install/update, the open page may keep the old widget — refresh the Web page (and,
|
|
170
|
+
if a tool schema changed, open a new session) before assuming the change failed. The Tower UI widget
|
|
171
|
+
is a hint only and is not required for hot reload.
|
|
172
|
+
|
|
173
|
+
For a live check of the reload path against an isolated profile, use
|
|
174
|
+
[`tests/live/hmr.cjs`](../../tests/live/hmr.cjs) with `DSH_TEST_URL` set to the throwaway server URL
|
|
175
|
+
(see [tests/README.md](../../tests/README.md)); it never touches the desktop profile or live sessions.
|
|
176
|
+
|
|
177
|
+
## Security
|
|
178
|
+
|
|
179
|
+
HMR executes the code it finds in the watched roots **inside the DSH Host process**, with your OS user
|
|
180
|
+
privileges, and it reloads it on save. Treat the profile patch layer like shell startup files:
|
|
181
|
+
|
|
182
|
+
- only point `root` at repositories you trust (this checkout), and keep the list small and absolute;
|
|
183
|
+
- do not add your home directory, a downloads folder, or a shared/network path;
|
|
184
|
+
- the patched profile is same-user trusted configuration; anyone who can write it can run code in
|
|
185
|
+
your DSH process;
|
|
186
|
+
- remove the block (`--remove`) and restart once when you no longer need watching.
|
|
187
|
+
|
|
188
|
+
See also: [install, upgrade and migration](install-upgrade-migration.md),
|
|
189
|
+
[configuration reference](../reference/config.md), [error codes](../reference/errors.md).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Install, upgrade and migration
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- DSH `0.2.1-rc.2` or newer
|
|
6
|
+
- Node.js 22+
|
|
7
|
+
- Git (local worktrees; no network access is required)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add dsh-tower
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or inside DSH: `plugin_manager({ action: "install_bundle", target: "dsh-tower" })`. From a checkout:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
dsh plugin --profile web add github:louifinn/dsh-tower
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The Host plugin loads at DSH startup: **restart DSH after install or update**.
|
|
22
|
+
|
|
23
|
+
## Upgrade
|
|
24
|
+
|
|
25
|
+
1. Read [CHANGELOG.md](../../CHANGELOG.md) for behavior changes.
|
|
26
|
+
2. Update the dependency (`dsh plugin --profile web add dsh-tower@latest` or your pinned version).
|
|
27
|
+
3. Restart DSH; check `tower_mode` reports `allowAgentControl` as expected.
|
|
28
|
+
4. Existing mission state stays compatible: the state schema did not change in 1.2/1.3.
|
|
29
|
+
|
|
30
|
+
## Migration notes
|
|
31
|
+
|
|
32
|
+
- **1.x → 2.0 (unreleased)**: no state migration is needed (`schemaVersion` stays 1; new fields are
|
|
33
|
+
optional). Behavior changes to review: `mode.denyTools` now also denies `workflow`, `ralph`, `subagent`,
|
|
34
|
+
`subagent_fork`, `wait_agent` and `team_task_list` while Tower mode is active (override with an explicit
|
|
35
|
+
list); `spawn.maxLiveWorkers` defaults to 4; `mode.workspaceLock` is off by default. Workers can no
|
|
36
|
+
longer complete/abandon/scope a mission directly — use `tower_complete`. New tools: `tower_interrupt`,
|
|
37
|
+
and `tower_reload` when HMR is available. Restart DSH once after upgrading; see
|
|
38
|
+
[hot reload](hot-reload.md).
|
|
39
|
+
- **1.3 → 1.4**: no state migration is needed. New: repository-level config
|
|
40
|
+
(`<git-common-dir>/dsh-tower/config.json`), stable `[tower:<code>]` tool errors, `tower_findings`,
|
|
41
|
+
and `id`/`missionId` + snake/camel input aliases. Existing calls keep working; restart DSH to load
|
|
42
|
+
the new tool surface (`tower_findings` and the newer output schemas).
|
|
43
|
+
- **1.2 → 1.3**: reviewers moved from the shared `tower-worker` preset to the new read-only
|
|
44
|
+
`tower-reviewer` preset. Reinstall/restart so both preset rows exist. `tower_spawn role=reviewer`
|
|
45
|
+
now requires the mission to be `completed`.
|
|
46
|
+
- **1.1 → 1.2**: merge failures gained `merge-hook-failed`, `cleanup` and reconciliation behavior; no
|
|
47
|
+
state migration is needed.
|
|
48
|
+
- **1.0 → 1.1**: package name changed from `@local/dsh-tower` to `dsh-tower`. Remove the old bundle
|
|
49
|
+
(`remove_bundle` with `@local/dsh-tower`) and install `dsh-tower`.
|
|
50
|
+
|
|
51
|
+
## Uninstall
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
plugin_manager({ action: "remove_bundle", target: "dsh-tower" })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Restart DSH. Mission state under `<git-common-dir>/dsh-tower/` and worktrees under
|
|
58
|
+
`<checkout parent>/.dsh-tower-worktrees/` are left in place; remove them manually only after no mission
|
|
59
|
+
is active.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Quick start
|
|
2
|
+
|
|
3
|
+
Prerequisites: DSH `0.2.1-rc.2`+, Node.js 22+, Git. Install `dsh-tower` and restart DSH (see
|
|
4
|
+
[install](install-upgrade-migration.md)).
|
|
5
|
+
|
|
6
|
+
## 1. Enter Tower mode
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
/tower
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The session reports `Tower mode ON`. You are now the control tower; goal/todo/team delegation tools are
|
|
13
|
+
reserved for normal mode.
|
|
14
|
+
|
|
15
|
+
## 2. Initialize and plan
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
tower_init repoRoot=D:\work\repo
|
|
19
|
+
tower_plan repoRoot=D:\work\repo missions=[{"title":"Feature A","scope":["src/a/**"],"tasks":["implement the feature","commit the change"],"kind":"build"}]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`tower_plan` returns mission ids (`M1`). Each mission gets a branch and a worktree when spawned.
|
|
23
|
+
|
|
24
|
+
## 3. Run the worker
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
tower_spawn repoRoot=D:\work\repo missionId=M1 role=worker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The call returns immediately with the child agent id. The worker works inside the mission worktree,
|
|
31
|
+
commits, marks tasks done, and calls `tower_complete`; its final report is delivered back to this
|
|
32
|
+
session. Use `tower_roster` to observe activity; do not poll in a loop.
|
|
33
|
+
|
|
34
|
+
## 4. Review
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
tower_spawn repoRoot=D:\work\repo missionId=M1 role=reviewer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Reviewers use the read-only `tower-reviewer` preset. They inspect `tower_diff` and submit
|
|
41
|
+
`tower_review`. A `clean` + `merge` verdict is required to merge.
|
|
42
|
+
|
|
43
|
+
## 5. Gate, merge, clean up
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
tower_preflight repoRoot=D:\work\repo missionId=M1
|
|
47
|
+
tower_merge repoRoot=D:\work\repo missionId=M1
|
|
48
|
+
tower_teardown repoRoot=D:\work\repo
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`tower_preflight` before review is expected to report `no-review`; after a clean review it should be
|
|
52
|
+
`ok:true`. `tower_merge` refuses while blockers remain. `tower_teardown` removes merged/abandoned
|
|
53
|
+
worktrees and branches.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
/tower off
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## If something fails
|
|
60
|
+
|
|
61
|
+
Read the blocker code and use [the blocker reference](../reference/blockers.md). The most common cases:
|
|
62
|
+
`stale-base` (run `tower_rebase`), `no-review` (spawn a reviewer), `merge-hook-failed` (retry, or raise
|
|
63
|
+
`timeoutMs`), `tip-moved` (the branch changed after review; review again).
|
|
64
|
+
|
|
65
|
+
Thrown tool failures carry a `[tower:<code>]` prefix; see [error codes](../reference/errors.md)
|
|
66
|
+
(e.g. `preset-missing` means restart DSH after installing the bundle, `not-initialized` means run
|
|
67
|
+
`tower_init` first).
|
|
68
|
+
|
|
69
|
+
## Optional: per-repository config
|
|
70
|
+
|
|
71
|
+
A repository can tune review, merge, worktree and spawn policy without touching the DSH profile. Put a
|
|
72
|
+
JSON file at `<git-common-dir>/dsh-tower/config.json` (usually `.git/dsh-tower/config.json`):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"review": { "maxRoundsPerReviewer": 3 },
|
|
77
|
+
"merge": { "timeoutMs": 1800000 },
|
|
78
|
+
"worktrees": { "root": "D:/tower-worktrees" }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The repository layer overrides the profile config; per-call tool arguments override both. See the
|
|
83
|
+
[config reference](../reference/config.md) for the full table and precedence.
|
|
84
|
+
|
|
85
|
+
## Optional: findings
|
|
86
|
+
|
|
87
|
+
Out-of-scope observations go to `tower_finding`. Later, list or close them without editing code:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
tower_findings repoRoot=D:\work\repo
|
|
91
|
+
tower_findings repoRoot=D:\work\repo id=F1 status=fixed note="fixed in M2"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Statuses are `open → triaged → fixed` / `wontfix`, and `fixed`/`wontfix` can reopen to `open`. Only the
|
|
95
|
+
tower or a worker may change a finding; reviewers can list and read.
|
|
96
|
+
|
|
97
|
+
A complete copy-paste example: [examples/first-mission.md](../examples/first-mission.md).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Operations runbook
|
|
2
|
+
|
|
3
|
+
## Daily checks
|
|
4
|
+
|
|
5
|
+
- `tower_status` — counts, open tasks, roster, unread messages, recent activity.
|
|
6
|
+
- `tower_roster` — live activity; retired agents are recorded but never resumed.
|
|
7
|
+
- `tower_preflight missionId` before every merge.
|
|
8
|
+
|
|
9
|
+
## Recover from a failed merge
|
|
10
|
+
|
|
11
|
+
1. Read the blocker: `merge-hook-failed` (retryable) vs `merge-conflict` (mission blocked) vs
|
|
12
|
+
`merge-shape` (inspect manually).
|
|
13
|
+
2. For a slow `pre-merge-commit` hook, retry with a larger timeout:
|
|
14
|
+
`tower_merge({ missionId, timeoutMs: 1800000 })`.
|
|
15
|
+
3. If a merge commit landed but the call failed, re-run `tower_merge`; it records the commit
|
|
16
|
+
(`recoveredFromFailure`) or reconciles an already-landed merge (`reconciled`) without a second merge.
|
|
17
|
+
4. Never reset or rewrite the base branch by hand while Tower state exists; use Tower tools so state and
|
|
18
|
+
Git stay aligned.
|
|
19
|
+
|
|
20
|
+
## Recover from a stale branch
|
|
21
|
+
|
|
22
|
+
`tower_rebase` first. If the branch is already contained in the base it returns `alreadyMerged` and
|
|
23
|
+
leaves the branch untouched — then call `tower_merge` to record it. A conflict marks the mission
|
|
24
|
+
`blocked`; resolve in the worktree, set the mission back to `active` (or `completed`), retry.
|
|
25
|
+
|
|
26
|
+
## Interrupt, stale roster and takeover
|
|
27
|
+
|
|
28
|
+
A runaway agent: `tower_interrupt({to:"worker-m1",reason:"..."})` interrupts the host subagent turn and
|
|
29
|
+
records `state:"interrupted"`. `tower_roster` shows `stale:true` when an active entry has no catalog
|
|
30
|
+
activity beyond `spawn.staleMs`; `tower_adopt` retires such entries (`retireMissing:false` marks them
|
|
31
|
+
`missing` instead). After the old owner is interrupted/failed/missing, spawn a replacement with
|
|
32
|
+
`takeover:true`; a still-running owner returns `takeover-refused`.
|
|
33
|
+
|
|
34
|
+
## Workspace lock
|
|
35
|
+
|
|
36
|
+
With `mode.workspaceLock:true`, `<git-common-dir>/dsh-tower/tower.lock` names the owning session. A second
|
|
37
|
+
Tower session gets `[tower:workspace-busy]`. Let the owner run `/tower off`; if the owner is gone, wait
|
|
38
|
+
`mode.workspaceStaleMs` (default 5 min) for the stale takeover, or delete the lock deliberately only after
|
|
39
|
+
confirming no Tower session is active. Heartbeats refresh the lock every `mode.heartbeatMs`.
|
|
40
|
+
|
|
41
|
+
## Hot reload
|
|
42
|
+
|
|
43
|
+
`/tower reload` or `tower_reload` asks DSH HMR to reconcile the profile and reload the Host plugin; the
|
|
44
|
+
reload is scheduled after the tool result (`dev.reloadDelayMs`). It fails with `reload-unavailable` on a
|
|
45
|
+
profile without HMR. Run `npm run dev:hmr` once to add this checkout to the profile's HMR watch roots
|
|
46
|
+
(one DSH restart after the profile patch change); see [hot reload](../guide/hot-reload.md). If a reload
|
|
47
|
+
fails, the previous plugin code no longer runs: restart DSH and check the HMR log.
|
|
48
|
+
|
|
49
|
+
## State lock
|
|
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.
|
|
54
|
+
|
|
55
|
+
## Worktrees and teardown
|
|
56
|
+
|
|
57
|
+
`tower_teardown` removes worktrees only for `merged`/`abandoned` missions; a running agent is always
|
|
58
|
+
kept, and a dirty worktree requires `force: true`. Use `dry_run: true` to preview decisions. A missing
|
|
59
|
+
worktree can be re-added from its branch with `git worktree add`, but do not do this while the mission
|
|
60
|
+
is active without updating the roster.
|
|
61
|
+
|
|
62
|
+
## Stale or corrupt state
|
|
63
|
+
|
|
64
|
+
If `state.json` is unreadable, stop all agents first. The plugin reports corruption instead of guessing.
|
|
65
|
+
Restore from a backup if you have one; otherwise remove `<git-common-dir>/dsh-tower/` and re-run
|
|
66
|
+
`tower_init` — this loses mission history but not Git branches/worktrees.
|
|
67
|
+
|
|
68
|
+
## Full recovery of a repo's Tower data
|
|
69
|
+
|
|
70
|
+
Only when no mission is in flight and you accept losing history:
|
|
71
|
+
|
|
72
|
+
1. Stop all tower agents.
|
|
73
|
+
2. Remove merged/abandoned worktrees (`tower_teardown` first when the state is readable).
|
|
74
|
+
3. Remove `<git-common-dir>/dsh-tower/`.
|
|
75
|
+
4. `tower_init` again; re-plan any work still needed.
|
|
76
|
+
|
|
77
|
+
## Mode state
|
|
78
|
+
|
|
79
|
+
Tower mode is stored in `$DSH_HOME/tower-mode/modes.json` (atomic writes, capped entries). If a mode is
|
|
80
|
+
stuck, `/tower off`; if the session is gone, remove its entry with the file unlocked and restart DSH.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
Tower's guarantees are **coordination guarantees**, not a sandbox. Understand what is enforced where.
|
|
4
|
+
|
|
5
|
+
## Enforced by the plugin
|
|
6
|
+
|
|
7
|
+
- **Review binding.** `tower_review` records the branch tip it inspected; the gate rejects a moved tip
|
|
8
|
+
(`tip-moved`) unless a clean conflict-free rebase waived it. A reviewer that modifies the branch after
|
|
9
|
+
spawning is rejected (`reviewer must not modify the mission branch`).
|
|
10
|
+
- **Role enforcement.** The caller is resolved against the roster. Worker mission updates are limited to
|
|
11
|
+
task/blocker/note; `completed`, `abandoned`, scope widening and blocker clearing are tower-only, and
|
|
12
|
+
completion (with its unread-inbox/commit gates) must go through `tower_complete`. Tower-only tools reject
|
|
13
|
+
roster workers with `[tower:not-authorized]`. One live worker per mission; taking over a stopped worker
|
|
14
|
+
is explicit.
|
|
15
|
+
- **Read-only reviewer preset.** The `tower-reviewer` preset removes `write`, `edit`, `pwsh`,
|
|
16
|
+
`tower_mission` and `tower_complete` from the tool surface; reviewers can only read, diff, message,
|
|
17
|
+
file findings, and submit a review.
|
|
18
|
+
- **Scope gate.** A mission may only change files matched by its declared scope globs; out-of-scope
|
|
19
|
+
changes block the merge.
|
|
20
|
+
- **Worktree isolation.** Workers operate in a dedicated worktree and branch; the main checkout is only
|
|
21
|
+
touched by merge/teardown.
|
|
22
|
+
- **Human-owned mode.** `/tower` is user-owned. With `mode.allowAgentControl` the model may toggle it,
|
|
23
|
+
but must state a reason; the shipped patch enables that by default — set it to `false` to make the
|
|
24
|
+
mode human-only.
|
|
25
|
+
- **No automatic goal resume.** Entering pauses the active goal; leaving does not resume it.
|
|
26
|
+
- **Advisory workspace lock.** `mode.workspaceLock` prevents two Tower sessions from accidentally driving
|
|
27
|
+
one repository; it is a same-user coordination lock, not authentication between users.
|
|
28
|
+
- **Test discovery is repository code.** `tower_test`/`tower_complete` execute `package.json`
|
|
29
|
+
`tower.testDiscovery` commands in the mission worktree with the DSH process's OS permissions. Set
|
|
30
|
+
`tests.allowPackageDiscovery:false` for untrusted repositories; output/time budgets fail closed but are
|
|
31
|
+
not a sandbox.
|
|
32
|
+
- **Review exemption is opt-in.** `reviewPolicy:"none"` requires `gates.allowReviewExemption:true` and a
|
|
33
|
+
non-empty reason. It is off by default because a model-driven tower could otherwise self-exempt; the
|
|
34
|
+
exemption and its reason are recorded in state/events and never granted to workers.
|
|
35
|
+
- **Leases are coordination, not quotas.** `tower_resource` prevents accidental co-scheduling via a local
|
|
36
|
+
JSON ledger; it does not cap CPU, memory, or process count. DSH exposes no public Job-object quota API,
|
|
37
|
+
so OS-level isolation remains a deployment concern.
|
|
38
|
+
- **Legacy import is read-only.** Imported snapshots live under the canonical state directory and are never
|
|
39
|
+
activated, adopted, or resumed; an operator must explicitly re-plan/migrate work.
|
|
40
|
+
|
|
41
|
+
## Not enforced by the plugin (deployment concerns)
|
|
42
|
+
|
|
43
|
+
- **Sandboxing.** Host plugins run outside the filesystem sandbox; the reviewer's shell ability is
|
|
44
|
+
removed at the tool-surface level, but the underlying process still has whatever OS permissions DSH
|
|
45
|
+
has. Deployments that need hard isolation must restrict the process/user themselves.
|
|
46
|
+
- **Reviewer trust.** A review is an attestation by the reviewer agent/model; the plugin binds it to a
|
|
47
|
+
commit but cannot prove the reviewer actually reasoned correctly. Use independent models/roles for
|
|
48
|
+
high-value branches.
|
|
49
|
+
- **Repository hooks.** `git merge` runs the target repository's hooks (`pre-merge-commit` etc.). Tower
|
|
50
|
+
cannot make those safe; review hooks before running Tower on an untrusted repository.
|
|
51
|
+
- **Package test commands.** Declared discovery commands have the same trust as repository hooks; a
|
|
52
|
+
malicious `package.json` can run code. Disable `tests.allowPackageDiscovery` and review the file.
|
|
53
|
+
- **HMR reload.** With `dev.hotReload`, `/tower reload`/`tower_reload` asks DSH to re-import the plugin
|
|
54
|
+
from disk. Only enable HMR watch roots for checkouts you trust; a reload executes the current files.
|
|
55
|
+
- **Prompt injection.** The reviewer reads untrusted branch content (diff/comments). The read-only preset
|
|
56
|
+
limits impact, but no model-side protection is absolute.
|
|
57
|
+
- **Base branch protection.** Tower writes to the local base branch; server-side branch protection/push
|
|
58
|
+
rules are not part of the plugin.
|
|
59
|
+
|
|
60
|
+
## Reporting
|
|
61
|
+
|
|
62
|
+
Do not paste secrets, tokens, or session logs in public issues. See [SECURITY.md](../../SECURITY.md).
|