dsh-tower 1.3.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/CONTRIBUTING.md +1 -1
- package/README.en.md +44 -8
- package/README.md +42 -8
- package/cordis.patch.yml +11 -1
- package/docs/README.md +10 -5
- package/docs/architecture/overview.md +13 -9
- package/docs/faq.md +45 -2
- package/docs/guide/hot-reload.md +189 -0
- package/docs/guide/install-upgrade-migration.md +11 -0
- package/docs/guide/quickstart.md +33 -1
- package/docs/guide/scenarios-and-philosophy.md +309 -0
- package/docs/operations/runbook.md +23 -0
- package/docs/operations/security.md +23 -0
- package/docs/reference/blockers.md +10 -2
- package/docs/reference/config.md +65 -2
- package/docs/reference/doctor.md +33 -0
- package/docs/reference/errors.md +50 -0
- package/docs/reference/events.md +45 -0
- package/docs/reference/operations.md +55 -0
- package/docs/reference/testing.md +61 -0
- package/docs/reference/tools.md +65 -23
- package/docs/zh/README.md +60 -0
- package/docs/zh/faq.md +81 -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/guide/scenarios-and-philosophy.md +211 -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 +31 -0
- package/docs/zh/reference/errors.md +52 -0
- package/docs/zh/reference/events.md +46 -0
- package/docs/zh/reference/operations.md +50 -0
- package/docs/zh/reference/testing.md +60 -0
- package/docs/zh/reference/tools.md +75 -0
- package/index.js +261 -13
- package/lib/batch.js +578 -0
- package/lib/config.js +275 -9
- package/lib/doctor.js +171 -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 +28 -1
- package/lib/store.js +24 -1
- package/lib/tests.js +628 -0
- package/lib/tools.js +1566 -184
- package/lib/tower.js +986 -103
- package/lib/workspaceLock.js +117 -0
- package/lib/worktreeOwnership.js +166 -0
- package/package.json +4 -1
- package/reviewer.js +1 -0
- package/scripts/check-docs.mjs +135 -0
- package/scripts/enable-dev-hmr.mjs +347 -0
- package/skills/tower-review.md +4 -2
- package/skills/tower-worker-protocol.md +3 -1
- package/worker.js +4 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-tower — enable DSH HMR for this repository inside one DSH profile.
|
|
3
|
+
//
|
|
4
|
+
// DSH's `@deepseek-ai/dsh-hmr` service watches the directories listed in its
|
|
5
|
+
// `root` config and reconciles the profile when files change. The shipped
|
|
6
|
+
// profile keeps `root: []` (nothing is watched), so a development checkout has
|
|
7
|
+
// to append one loader override to the *user* patch layer:
|
|
8
|
+
//
|
|
9
|
+
// <DSH_HOME>/profiles/<profile>/cordis.patch.yml
|
|
10
|
+
//
|
|
11
|
+
// This script owns exactly one marked block in that file:
|
|
12
|
+
//
|
|
13
|
+
// # dsh-tower:dev-hmr
|
|
14
|
+
// - id: hmr
|
|
15
|
+
// config:
|
|
16
|
+
// root:
|
|
17
|
+
// - '<absolute repo path with forward slashes>'
|
|
18
|
+
//
|
|
19
|
+
// It is idempotent (re-running replaces the block), preserves every other
|
|
20
|
+
// line, supports --dry-run and --remove, and never creates a profile.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/enable-dev-hmr.mjs [--profile desktop] [--root <dir>]
|
|
24
|
+
// [--dry-run] [--remove]
|
|
25
|
+
//
|
|
26
|
+
// Exit codes: 0 = success or nothing to do, 1 = bad arguments or I/O failure.
|
|
27
|
+
import fs from 'node:fs'
|
|
28
|
+
import os from 'node:os'
|
|
29
|
+
import path from 'node:path'
|
|
30
|
+
import { pathToFileURL } from 'node:url'
|
|
31
|
+
|
|
32
|
+
/** Comment line that starts the managed block. */
|
|
33
|
+
export const MARKER = '# dsh-tower:dev-hmr'
|
|
34
|
+
const PATCH_FILENAME = 'cordis.patch.yml'
|
|
35
|
+
|
|
36
|
+
/** Render the managed block (no leading or trailing blank line). */
|
|
37
|
+
export function renderBlock(root) {
|
|
38
|
+
const quoted = String(root).replaceAll("'", "''")
|
|
39
|
+
return [MARKER, '- id: hmr', ' config:', ' root:', ` - '${quoted}'`].join('\n')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Convert a native path to the forward-slash form DSH HMR configs use. */
|
|
43
|
+
export function forwardSlashes(value) {
|
|
44
|
+
return String(value).replaceAll('\\', '/')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function eolOf(text) {
|
|
48
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Split into content lines without the trailing empty string produced by a
|
|
52
|
+
// final newline. Line endings are normalised to \n while editing.
|
|
53
|
+
function contentLines(text) {
|
|
54
|
+
const lines = String(text).replaceAll('\r\n', '\n').split('\n')
|
|
55
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
56
|
+
return lines
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serializeLines(lines, eol) {
|
|
60
|
+
return lines.length === 0 ? '' : `${lines.join(eol)}${eol}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isTopLevelEntry(line) {
|
|
64
|
+
return /^-[ \t]/.test(line) || line === '-'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Locate the managed block in a list of lines.
|
|
69
|
+
* The marker names the block; the block is the first loader entry after it
|
|
70
|
+
* plus every indented continuation line. A blank line or an unindented line
|
|
71
|
+
* ends the block, so unrelated comments and entries after it stay untouched.
|
|
72
|
+
* @returns {{ start: number, end: number } | null} half-open line range
|
|
73
|
+
*/
|
|
74
|
+
export function findBlock(lines) {
|
|
75
|
+
const start = lines.findIndex((line) => line.trim() === MARKER)
|
|
76
|
+
if (start === -1) return null
|
|
77
|
+
let index = start + 1
|
|
78
|
+
while (index < lines.length && lines[index].trim() === '') index += 1
|
|
79
|
+
if (index >= lines.length) return { start, end: lines.length }
|
|
80
|
+
if (!isTopLevelEntry(lines[index])) return { start, end: index }
|
|
81
|
+
index += 1
|
|
82
|
+
while (index < lines.length) {
|
|
83
|
+
const line = lines[index]
|
|
84
|
+
if (line.trim() === '' || !/^[ \t]/.test(line)) break
|
|
85
|
+
index += 1
|
|
86
|
+
}
|
|
87
|
+
return { start, end: index }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findAllBlocks(lines) {
|
|
91
|
+
const spans = []
|
|
92
|
+
let cursor = 0
|
|
93
|
+
while (cursor < lines.length) {
|
|
94
|
+
const span = findBlock(lines.slice(cursor))
|
|
95
|
+
if (span === null) break
|
|
96
|
+
spans.push({ start: span.start + cursor, end: span.end + cursor })
|
|
97
|
+
cursor = span.end + cursor
|
|
98
|
+
}
|
|
99
|
+
return spans
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Insert the managed block, or replace the existing one in place.
|
|
104
|
+
* All lines outside the block are preserved.
|
|
105
|
+
*/
|
|
106
|
+
export function upsertBlock(text, root) {
|
|
107
|
+
const eol = eolOf(text)
|
|
108
|
+
const lines = contentLines(text)
|
|
109
|
+
const block = renderBlock(root).split('\n')
|
|
110
|
+
const spans = findAllBlocks(lines)
|
|
111
|
+
|
|
112
|
+
if (spans.length === 0) {
|
|
113
|
+
if (lines.length > 0 && lines[lines.length - 1].trim() !== '') lines.push('')
|
|
114
|
+
lines.push(...block)
|
|
115
|
+
return serializeLines(lines, eol)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Keep the first block, drop any accidental duplicates, then replace.
|
|
119
|
+
for (let index = spans.length - 1; index >= 1; index -= 1) {
|
|
120
|
+
const span = spans[index]
|
|
121
|
+
lines.splice(span.start, span.end - span.start)
|
|
122
|
+
}
|
|
123
|
+
const first = spans[0]
|
|
124
|
+
lines.splice(first.start, first.end - first.start, ...block)
|
|
125
|
+
|
|
126
|
+
// Keep one blank line between the block and the next top-level entry so the
|
|
127
|
+
// generated file stays canonical and idempotent.
|
|
128
|
+
const next = first.start + block.length
|
|
129
|
+
if (lines[next] !== undefined && lines[next].trim() !== '') lines.splice(next, 0, '')
|
|
130
|
+
return serializeLines(lines, eol)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Remove the managed block; other lines are preserved. No block means no change. */
|
|
134
|
+
export function removeBlock(text) {
|
|
135
|
+
const eol = eolOf(text)
|
|
136
|
+
const lines = contentLines(text)
|
|
137
|
+
const spans = findAllBlocks(lines)
|
|
138
|
+
if (spans.length === 0) return text
|
|
139
|
+
for (let index = spans.length - 1; index >= 0; index -= 1) {
|
|
140
|
+
const span = spans[index]
|
|
141
|
+
lines.splice(span.start, span.end - span.start)
|
|
142
|
+
// Collapse only the blank run left at this seam; never touch blank lines
|
|
143
|
+
// elsewhere in the user's file.
|
|
144
|
+
const seam = span.start
|
|
145
|
+
if (seam > 0 && seam < lines.length && lines[seam - 1].trim() === '' && lines[seam].trim() === '') {
|
|
146
|
+
lines.splice(seam, 1)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// The append path adds one separating blank line before the block; drop a
|
|
150
|
+
// trailing blank run so removing the block restores the previous EOF.
|
|
151
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
|
|
152
|
+
return serializeLines(lines, eol)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function expandHome(value) {
|
|
156
|
+
const raw = String(value ?? '').trim()
|
|
157
|
+
if (raw === '~') return os.homedir()
|
|
158
|
+
if (raw.startsWith('~/') || raw.startsWith('~\\')) return path.join(os.homedir(), raw.slice(2))
|
|
159
|
+
return raw
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Resolve the DSH home directory exactly as `_env.cjs` does. */
|
|
163
|
+
export function resolveHome(env = process.env) {
|
|
164
|
+
const configured = expandHome(env.DSH_HOME)
|
|
165
|
+
return path.resolve(configured.length > 0 ? configured : path.join(os.homedir(), '.dsh'))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Resolve, canonicalise and forward-slash the watched repository root. */
|
|
169
|
+
export function resolveRoot(value, cwd = process.cwd()) {
|
|
170
|
+
const raw = value === undefined || value === null ? cwd : String(value)
|
|
171
|
+
if (/[\r\n]/.test(raw)) throw new Error(`--root must not contain line breaks: ${raw}`)
|
|
172
|
+
const absolute = path.resolve(cwd, raw)
|
|
173
|
+
let real
|
|
174
|
+
try {
|
|
175
|
+
real = fs.realpathSync(absolute)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`--root must be an existing directory: ${absolute} (${error.message})`)
|
|
178
|
+
}
|
|
179
|
+
if (!fs.statSync(real).isDirectory()) throw new Error(`--root is not a directory: ${real}`)
|
|
180
|
+
return forwardSlashes(real)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function parseArgs(argv) {
|
|
184
|
+
const options = { profile: 'desktop', root: undefined, dryRun: false, remove: false, help: false }
|
|
185
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
186
|
+
const arg = String(argv[index])
|
|
187
|
+
if (arg === '--dry-run') {
|
|
188
|
+
options.dryRun = true
|
|
189
|
+
} else if (arg === '--remove') {
|
|
190
|
+
options.remove = true
|
|
191
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
192
|
+
options.help = true
|
|
193
|
+
} else if (arg === '--profile' || arg === '--root') {
|
|
194
|
+
const value = argv[index + 1]
|
|
195
|
+
if (value === undefined || String(value).startsWith('--')) throw new Error(`${arg} requires a value`)
|
|
196
|
+
index += 1
|
|
197
|
+
if (arg === '--profile') options.profile = String(value)
|
|
198
|
+
else options.root = String(value)
|
|
199
|
+
} else if (arg.startsWith('--profile=')) {
|
|
200
|
+
options.profile = arg.slice('--profile='.length)
|
|
201
|
+
} else if (arg.startsWith('--root=')) {
|
|
202
|
+
options.root = arg.slice('--root='.length)
|
|
203
|
+
} else {
|
|
204
|
+
throw new Error(`unknown argument: ${arg}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return options
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printUsage() {
|
|
211
|
+
console.log(`Usage: node scripts/enable-dev-hmr.mjs [options]
|
|
212
|
+
|
|
213
|
+
Options:
|
|
214
|
+
--profile <name> DSH profile to edit (default: desktop)
|
|
215
|
+
--root <dir> repository directory DSH HMR should watch
|
|
216
|
+
(default: the current working directory, realpath'ed)
|
|
217
|
+
--dry-run print the patch diff without writing
|
|
218
|
+
--remove remove the dsh-tower dev-hmr block
|
|
219
|
+
--help show this help
|
|
220
|
+
|
|
221
|
+
Target file: <DSH_HOME>/profiles/<profile>/cordis.patch.yml
|
|
222
|
+
DSH_HOME defaults to ~/.dsh. The script never creates a profile and writes
|
|
223
|
+
nothing outside the user patch layer.`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printNextSteps(root) {
|
|
227
|
+
console.log('Restart DSH once so the profile patch (and the HMR watch roots) takes effect.')
|
|
228
|
+
console.log('After the restart:')
|
|
229
|
+
console.log(` - HMR watches ${root} and reloads Tower code after edits while DSH stays open.`)
|
|
230
|
+
console.log(' - Run /tower reload (or call the tower_reload tool) to reconcile on demand.')
|
|
231
|
+
console.log(' - Run this script with --remove to disable the watcher again (restart once after).')
|
|
232
|
+
console.log('Note: the HMR root list accepts absolute paths; the value above is absolute and uses forward slashes.')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function printDiff(writeLine, file, before, after) {
|
|
236
|
+
const beforeLines = String(before).replaceAll('\r\n', '\n').split('\n')
|
|
237
|
+
const afterLines = String(after).replaceAll('\r\n', '\n').split('\n')
|
|
238
|
+
let prefix = 0
|
|
239
|
+
while (
|
|
240
|
+
prefix < beforeLines.length &&
|
|
241
|
+
prefix < afterLines.length &&
|
|
242
|
+
beforeLines[prefix] === afterLines[prefix]
|
|
243
|
+
) {
|
|
244
|
+
prefix += 1
|
|
245
|
+
}
|
|
246
|
+
let suffix = 0
|
|
247
|
+
while (
|
|
248
|
+
suffix < beforeLines.length - prefix &&
|
|
249
|
+
suffix < afterLines.length - prefix &&
|
|
250
|
+
beforeLines[beforeLines.length - 1 - suffix] === afterLines[afterLines.length - 1 - suffix]
|
|
251
|
+
) {
|
|
252
|
+
suffix += 1
|
|
253
|
+
}
|
|
254
|
+
writeLine(`--- ${file}`)
|
|
255
|
+
writeLine(`+++ ${file} (dsh-tower dev-hmr)`)
|
|
256
|
+
writeLine(`@@ line ${prefix + 1} @@`)
|
|
257
|
+
for (let index = prefix; index < beforeLines.length - suffix; index += 1) {
|
|
258
|
+
writeLine(`-${beforeLines[index]}`)
|
|
259
|
+
}
|
|
260
|
+
for (let index = prefix; index < afterLines.length - suffix; index += 1) {
|
|
261
|
+
writeLine(`+${afterLines[index]}`)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function profilePatchPath(home, profile) {
|
|
266
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile) || profile === '.' || profile === '..') {
|
|
267
|
+
throw new Error(`invalid profile name: ${profile}`)
|
|
268
|
+
}
|
|
269
|
+
const profileDir = path.join(home, 'profiles', profile)
|
|
270
|
+
if (!fs.existsSync(profileDir) || !fs.statSync(profileDir).isDirectory()) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`DSH profile "${profile}" does not exist at ${profileDir}. ` +
|
|
273
|
+
`Create it first (for example: dsh --profile ${profile} --from-default-profile web --dump-config); ` +
|
|
274
|
+
'this script never creates a profile.',
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
return path.join(profileDir, PATCH_FILENAME)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run the CLI. Returns the process exit code; throws nothing.
|
|
282
|
+
* `env` and `cwd` are injectable so tests can isolate $DSH_HOME.
|
|
283
|
+
*/
|
|
284
|
+
export function main(argv = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
|
|
285
|
+
try {
|
|
286
|
+
const options = parseArgs(argv)
|
|
287
|
+
if (options.help) {
|
|
288
|
+
printUsage()
|
|
289
|
+
return 0
|
|
290
|
+
}
|
|
291
|
+
const home = resolveHome(env)
|
|
292
|
+
const file = profilePatchPath(home, options.profile)
|
|
293
|
+
const original = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
294
|
+
|
|
295
|
+
if (options.remove) {
|
|
296
|
+
const next = removeBlock(original)
|
|
297
|
+
if (next === original) {
|
|
298
|
+
console.log(`No dsh-tower dev-hmr block in ${file}; nothing to remove.`)
|
|
299
|
+
return 0
|
|
300
|
+
}
|
|
301
|
+
if (options.dryRun) {
|
|
302
|
+
printDiff(console.log, file, original, next)
|
|
303
|
+
console.log('Dry run: no file written.')
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
fs.writeFileSync(file, next, 'utf8')
|
|
307
|
+
console.log(`Removed the dsh-tower dev-hmr block from ${file}.`)
|
|
308
|
+
console.log('Restart DSH once so the profile patch is reloaded without the HMR root.')
|
|
309
|
+
return 0
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const root = resolveRoot(options.root, cwd)
|
|
313
|
+
const next = upsertBlock(original, root)
|
|
314
|
+
if (next === original) {
|
|
315
|
+
console.log(`The dsh-tower dev-hmr block in ${file} is already up to date for ${root}.`)
|
|
316
|
+
printNextSteps(root)
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
if (options.dryRun) {
|
|
320
|
+
printDiff(console.log, file, original, next)
|
|
321
|
+
console.log('Dry run: no file written.')
|
|
322
|
+
return 0
|
|
323
|
+
}
|
|
324
|
+
fs.writeFileSync(file, next, 'utf8')
|
|
325
|
+
console.log(`Wrote the dsh-tower dev-hmr block to ${file}`)
|
|
326
|
+
printNextSteps(root)
|
|
327
|
+
return 0
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(`enable-dev-hmr: ${error?.message ?? error}`)
|
|
330
|
+
return 1
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Only run when invoked as a script; importing this file from a test is inert.
|
|
335
|
+
const invokedDirectly = (() => {
|
|
336
|
+
try {
|
|
337
|
+
const entry = process.argv[1]
|
|
338
|
+
if (entry === undefined) return false
|
|
339
|
+
return pathToFileURL(path.resolve(entry)).href === import.meta.url
|
|
340
|
+
} catch {
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
})()
|
|
344
|
+
|
|
345
|
+
if (invokedDirectly) {
|
|
346
|
+
process.exitCode = main()
|
|
347
|
+
}
|
package/skills/tower-review.md
CHANGED
|
@@ -4,11 +4,13 @@ You are the independent reviewer of a completed Tower mission. You are read-only
|
|
|
4
4
|
|
|
5
5
|
1. Read the mission briefing and the review binding: the reviewed tip must equal the current branch tip.
|
|
6
6
|
2. Inspect the diff with `tower_diff` (files, commits, patch). Do not run write, edit, pwsh, commit, rebase, or merge.
|
|
7
|
-
3. Verify: every task is met; all changes are inside the mission scope; no unrelated or generated files; no secrets; tests or evidence are adequate.
|
|
7
|
+
3. Verify: every task is met; all changes are inside the mission scope; no unrelated or generated files; no secrets; tests or evidence are adequate. Cross-check the worker's `tower_complete.checks` evidence against the diff; do not accept claimed checks you cannot corroborate.
|
|
8
8
|
4. Check the reviewed commit is the tip you inspected. If it moved, stop and report a blocker instead of approving.
|
|
9
9
|
5. Submit exactly one `tower_review`:
|
|
10
10
|
- `clean` + `merge` + `findings: "none"` when everything passes.
|
|
11
11
|
- `p1-Nitems` or `p2-Nitems` + `hold` or `fix-then-merge` with concrete findings when it does not.
|
|
12
|
-
6. Put out-of-scope observations in `tower_finding`, never in the branch.
|
|
12
|
+
6. Put out-of-scope observations in `tower_finding`, never in the branch. `tower_findings` lists and
|
|
13
|
+
reads findings; only the tower or a worker may change a finding's status, so record evidence instead
|
|
14
|
+
of closing it yourself.
|
|
13
15
|
|
|
14
16
|
The verdict is bound to the inspected tip; do not modify the branch.
|
|
@@ -5,8 +5,10 @@ You are a Tower worker: one mission, one worktree, one commit series.
|
|
|
5
5
|
1. Read the briefing: mission id, branch, task list, scope globs, dependencies, and context.
|
|
6
6
|
2. Work only inside the assigned worktree. Never modify the main checkout or other missions.
|
|
7
7
|
3. Keep changes inside the scope globs; report anything else as a `tower_finding` instead of editing it.
|
|
8
|
+
Review your own findings later with `tower_findings` and close the resolved ones with `status: "fixed"`.
|
|
8
9
|
4. As you finish each task call `tower_mission` with `task_done` (and `note` when useful).
|
|
9
|
-
5. Run the verification available to you (tests, type check, build)
|
|
10
|
+
5. Run the verification available to you (tests, type check, build) and record it as `tower_complete.checks`
|
|
11
|
+
entries (`{name, command, exitCode, evidence}`); `gates.requireChecks` can make at least one mandatory.
|
|
10
12
|
6. Commit on the mission branch with a clear message. Do not push, spawn agents, review, or merge.
|
|
11
13
|
7. If blocked, call `tower_mission` with `blocker`, then stop and report exactly what is blocking.
|
|
12
14
|
|
package/worker.js
CHANGED
|
@@ -16,14 +16,16 @@ const ALLOW = [
|
|
|
16
16
|
'tower_inbox',
|
|
17
17
|
'tower_complete',
|
|
18
18
|
'tower_finding',
|
|
19
|
+
'tower_findings',
|
|
19
20
|
'skill',
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
const PROTOCOL = [
|
|
23
24
|
'You are a Tower worker in a DeepSeek Harness Tower workspace.',
|
|
24
25
|
'Work only inside the git worktree named in your briefing; never modify the main checkout.',
|
|
25
|
-
'Commit your work on the mission branch. Update
|
|
26
|
-
'
|
|
26
|
+
'Commit your work on the mission branch. Update progress with tower_mission (task_done/task_drop,',
|
|
27
|
+
'blocker, note); lifecycle status and scope are owned by the tower. Finish with tower_complete',
|
|
28
|
+
'(report + checks evidence), then put the full handoff in your final message: what changed,',
|
|
27
29
|
'every file path, how you verified it, and anything left undone.',
|
|
28
30
|
'Do not spawn other agents, do not review, and do not merge branches; the tower owns integration.',
|
|
29
31
|
].join(' ')
|