dsh-tower 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/COMPATIBILITY.md +2 -0
- package/CONTRIBUTING.md +1 -1
- package/GLOSSARY.md +6 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +18 -16
- package/README.md +18 -11
- package/adapters/leases.js +20 -6
- package/adapters/legacy.js +149 -37
- package/adapters/stateFile.js +11 -1
- package/app/archive.js +30 -5
- package/app/briefing.js +95 -12
- package/app/checkCompletion.js +107 -18
- package/app/enrichedMissions.js +1 -1
- package/app/freshness.js +77 -0
- package/app/gate.js +26 -12
- package/app/merge.js +7 -4
- package/app/mission.js +24 -1
- package/app/missionContext.js +2 -1
- package/app/plan.js +64 -1
- package/app/provision.js +133 -22
- package/app/read.js +38 -5
- package/app/rebase.js +10 -8
- package/app/review.js +104 -61
- package/app/reviewCheckout.js +133 -0
- package/app/sandbox.js +121 -0
- package/app/sharedCheckout.js +90 -0
- package/app/spawn-prechecks.js +31 -20
- package/app/spawn-support.js +59 -8
- package/app/spawn.js +3 -0
- package/app/teardown.js +119 -2
- package/app/tower.js +34 -6
- package/client.js +321 -0
- package/cordis.patch.yml +55 -3
- package/docs/adr/0005-component-surface.md +58 -13
- package/docs/adr/0006-mission-shapes.md +306 -0
- package/docs/architecture/overview.md +18 -10
- package/docs/examples/first-mission.md +3 -2
- package/docs/faq.md +9 -7
- package/docs/guide/install-upgrade-migration.md +58 -7
- package/docs/guide/quickstart.md +20 -10
- package/docs/guide/scenarios-and-philosophy.md +35 -28
- package/docs/operations/runbook.md +1 -1
- package/docs/operations/security.md +11 -10
- package/docs/reference/blockers.md +20 -11
- package/docs/reference/component-surface.md +20 -8
- package/docs/reference/config.generated.md +8 -5
- package/docs/reference/config.md +30 -5
- package/docs/reference/doctor.md +7 -7
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/operations.md +1 -1
- package/docs/reference/shapes.md +123 -0
- package/docs/reference/surface-budget.md +53 -4
- package/docs/reference/testing.md +5 -3
- package/docs/reference/tools.generated.md +7 -7
- package/docs/reference/tools.md +5 -5
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +25 -6
- package/docs/zh/guide/quickstart.md +17 -10
- package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
- package/docs/zh/operations/security.md +9 -8
- package/docs/zh/reference/blockers.md +13 -9
- package/docs/zh/reference/config.md +22 -5
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +5 -4
- package/docs/zh/reference/tools.md +3 -3
- package/domain/batch.js +75 -105
- package/domain/collab/batch.js +30 -10
- package/domain/collab/spawn.js +53 -16
- package/domain/components.js +12 -4
- package/domain/config/schema.js +87 -22
- package/domain/contracts/outputs.js +2 -1
- package/domain/contracts/tool-inputs/missions.js +10 -2
- package/domain/errors.js +6 -0
- package/domain/evidence.js +283 -0
- package/domain/gate.js +17 -34
- package/domain/integration/gate.js +17 -43
- package/domain/integration/init.js +3 -2
- package/domain/integration/provision.js +51 -0
- package/domain/integration/rebase.js +16 -1
- package/domain/integration/review.js +203 -9
- package/domain/mission.js +16 -33
- package/domain/missions/completion.js +46 -8
- package/domain/missions/findings.js +2 -1
- package/domain/missions/lifecycle.js +131 -35
- package/domain/missions/plan.js +30 -6
- package/domain/missions/review.js +2 -18
- package/domain/planning.js +5 -5
- package/domain/read/missionContext.js +17 -3
- package/domain/read/views.js +5 -1
- package/domain/roles.js +222 -0
- package/domain/roster.js +9 -3
- package/domain/scope.js +51 -0
- package/domain/shapes/build.js +254 -0
- package/domain/shapes/index.js +171 -0
- package/domain/shapes/survey.js +445 -0
- package/domain/state/archive.js +28 -4
- package/domain/state/migrations.js +111 -4
- package/domain/state/schema.js +69 -7
- package/domain/state/semantics.js +93 -13
- package/domain/state.js +5 -1
- package/domain/summary.js +5 -11
- package/domain/vocabulary.js +15 -33
- package/host/capability-registry.js +25 -9
- package/host/config.js +40 -3
- package/host/doctor.js +20 -8
- package/host/preset-survey-sandbox.js +19 -0
- package/host/preset-survey.js +18 -0
- package/host/presets.js +17 -11
- package/host/tool-registry.js +23 -13
- package/locale/en.json +16 -1
- package/locale/preset-survey/en.json +6 -0
- package/locale/preset-survey/zh.json +6 -0
- package/locale/preset-survey-sandbox/en.json +6 -0
- package/locale/preset-survey-sandbox/zh.json +6 -0
- package/locale/zh.json +16 -1
- package/package.json +19 -2
- package/ports/state.js +8 -1
- package/scripts/architecture-waivers.json +3 -3
- package/scripts/check-components.mjs +120 -6
- package/scripts/check-docs.mjs +1 -1
- package/scripts/check-pack.mjs +5 -0
- package/scripts/component-surface.json +83 -29
- package/scripts/generate-capability-locale.mjs +5 -0
- package/scripts/generate-shapes-reference.mjs +171 -0
- package/scripts/mount-smoke/surface.mjs +3 -0
- package/scripts/pack-manifest.txt +22 -0
- package/scripts/shape-audit.mjs +361 -0
- package/scripts/surface-baseline.json +75 -4
- package/scripts/types-coverage.mjs +7 -0
- package/survey-sandbox.js +48 -0
- package/survey.js +40 -0
- package/tools/complete.js +5 -5
- package/tools/diff.js +5 -3
- package/tools/plan.js +1 -1
- package/tools/preflight.js +3 -2
- package/tools/review.js +1 -1
- package/tools/spawn.js +48 -50
- package/tools/status.js +8 -1
- package/tools/test.js +2 -1
- package/types/index.d.ts +76 -5
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Shape audit (#149): one shape registry means generic layers must not branch on
|
|
2
|
+
// a kind or a role, and the old global status vocabulary must not come back.
|
|
3
|
+
//
|
|
4
|
+
// The audit is the ongoing mechanism, not a one-off cleanup: it fails on
|
|
5
|
+
// - a comparison against a kind literal ('build' | 'survey'),
|
|
6
|
+
// - a comparison against a role literal ('worker' | 'reviewer' | 'tower'),
|
|
7
|
+
// - the global TERMINAL_STATUSES / TRANSITIONS identifiers,
|
|
8
|
+
// - a one-argument isTerminal(...) or one-argument canTransition(...) call,
|
|
9
|
+
// - reading a review tip from a live worktree HEAD (review evidence is a commit),
|
|
10
|
+
// - comparing a batch status against a literal outside the batch owner module,
|
|
11
|
+
// - naming the lease ledger outside the lease adapter,
|
|
12
|
+
// - reading completion checks / test receipts outside domain/evidence.js,
|
|
13
|
+
// - the legacy format/status vocabulary outside adapters/legacy.js,
|
|
14
|
+
// - owning or re-implementing path permission instead of shape.reach + domain/scope.js,
|
|
15
|
+
// - combining a role decision with a shape decision outside domain/roles.js.
|
|
16
|
+
//
|
|
17
|
+
// Exemptions live in scripts/architecture-waivers.json under `shapeWaivers` and
|
|
18
|
+
// require a reason plus an owner or a ref, exactly like the architecture gate.
|
|
19
|
+
// A waiver with no reason is a contract failure, and a waiver for a path that no
|
|
20
|
+
// longer violates anything is reported as stale.
|
|
21
|
+
//
|
|
22
|
+
// Usage: node scripts/shape-audit.mjs [--root <path>] [--json]
|
|
23
|
+
import fs from 'node:fs'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
|
|
27
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
28
|
+
export const WAIVER_FILE = 'scripts/architecture-waivers.json'
|
|
29
|
+
export const WAIVER_KEY = 'shapeWaivers'
|
|
30
|
+
export const SCAN_DIRS = ['domain', 'app', 'tools', 'host', 'adapters', 'ports']
|
|
31
|
+
export const SHAPE_DIR = 'domain/shapes'
|
|
32
|
+
/**
|
|
33
|
+
* The fitness rules this audit enforces. `docs/reference/shapes.md` is generated
|
|
34
|
+
* from this list, so a verdict cannot name a rule the audit does not run.
|
|
35
|
+
*/
|
|
36
|
+
export const RULES = [
|
|
37
|
+
{
|
|
38
|
+
id: 'kind-literal',
|
|
39
|
+
rule: 'Generic layers never compare mission.kind with a literal.',
|
|
40
|
+
owner: 'domain/shapes/<kind>.js',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'role-literal',
|
|
44
|
+
rule: 'Generic layers use the vocabulary role predicates, never a role literal.',
|
|
45
|
+
owner: 'domain/roles.js',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'global-status-vocabulary',
|
|
49
|
+
rule: 'No module reaches for the global TERMINAL_STATUSES/TRANSITIONS tables.',
|
|
50
|
+
owner: 'domain/shapes/<kind>.js',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'shape-entry-point',
|
|
54
|
+
rule: 'isTerminal(kind, status) and canTransition(kind, from, to) are the only entry points.',
|
|
55
|
+
owner: 'domain/shapes/index.js',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'review-tip-read',
|
|
59
|
+
rule: 'A review never reads a live worktree HEAD; verdicts are stamped with a recorded commit.',
|
|
60
|
+
owner: 'app/reviewCheckout.js',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'batch-status-literal',
|
|
64
|
+
rule: 'Batch status literals live in domain/batch.js; every other layer uses its constants.',
|
|
65
|
+
owner: 'domain/batch.js',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'lease-ledger',
|
|
69
|
+
rule: 'Only adapters/leases.js reads or writes the lease ledger.',
|
|
70
|
+
owner: 'adapters/leases.js',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'blocked-status-write',
|
|
74
|
+
rule: 'blocked is derived from open blockers; only domain/missions/lifecycle.js writes it.',
|
|
75
|
+
owner: 'domain/missions/lifecycle.js',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'evidence-single-read',
|
|
79
|
+
rule: 'Completion checks and test receipts have one reader: domain/evidence.js.',
|
|
80
|
+
owner: 'domain/evidence.js',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'legacy-vocabulary',
|
|
84
|
+
rule: 'One live vocabulary: legacy formats/statuses are read only by adapters/legacy.js.',
|
|
85
|
+
owner: 'adapters/legacy.js',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'scope-reach',
|
|
89
|
+
rule: 'Path permission derives from shape.reach through domain/scope.js; no module owns or re-implements it.',
|
|
90
|
+
owner: 'domain/scope.js',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'role-shape-conflation',
|
|
94
|
+
rule: 'A role decision never combines with a shape decision; role facts live in domain/roles.js.',
|
|
95
|
+
owner: 'domain/roles.js',
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
/** Rules whose implementation module necessarily names the vocabulary it owns. */
|
|
99
|
+
export const RULE_EXEMPT_PATHS = {
|
|
100
|
+
'batch-status-literal': ['domain/batch.js'],
|
|
101
|
+
'lease-ledger': ['adapters/leases.js'],
|
|
102
|
+
'blocked-status-write': ['domain/missions/lifecycle.js'],
|
|
103
|
+
'evidence-single-read': ['domain/evidence.js', 'app/recording.js', 'domain/summary.js', 'domain/state/schema.js'],
|
|
104
|
+
'legacy-vocabulary': ['adapters/legacy.js'],
|
|
105
|
+
'scope-reach': ['domain/scope.js'],
|
|
106
|
+
'role-shape-conflation': ['domain/roles.js'],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const KIND_PATTERN = /(?:\bkind\b\s*[!=]==?\s*['"](?:build|survey)['"])|(?:['"](?:build|survey)['"]\s*[!=]==?\s*\bkind\b)/
|
|
110
|
+
const ROLE_PATTERN = /(?:\brole\b\s*[!=]==?\s*['"](?:worker|reviewer|tower)['"])|(?:['"](?:worker|reviewer|tower)['"]\s*[!=]==?\s*\brole\b)/
|
|
111
|
+
const GLOBAL_VOCAB_PATTERN = /\b(?:TERMINAL_STATUSES|TRANSITIONS)\b/
|
|
112
|
+
const CALL_PATTERN = /(?<!\.)\b(isTerminal|canTransition)\s*\(([^()]*)\)/g
|
|
113
|
+
const REVIEW_TIP_PATTERN = /\brevParse\s*\(\s*[^)]*worktree\.path\s*,\s*['"]HEAD['"]\s*\)/
|
|
114
|
+
const BATCH_STATUS_PATTERN =
|
|
115
|
+
/(?:\bbatch\.status\s*[!=]==?\s*['"](?:open|closed|abandoned|created|active|pending)['"])|(?:['"](?:open|closed|abandoned|created|active|pending)['"]\s*[!=]==?\s*\bbatch\.status\b)/
|
|
116
|
+
const LEASE_LEDGER_PATTERN = /['"](?:leases\.json|leases\.json\.lock|leases\.lock)['"]/
|
|
117
|
+
const BLOCKED_WRITE_PATTERN = /(?:\bstatus\s*=\s*['"]blocked['"])|(?:['"]status['"]\s*:\s*['"]blocked['"])/
|
|
118
|
+
const EVIDENCE_READ_PATTERN = /\bmission\.testRuns\b|\btestRuns\s*[.[]|completionReport\?\.checks|completionReport\.checks/
|
|
119
|
+
const LEGACY_VOCABULARY_PATTERN = /\btower-codex\b|\bpausedIds\b|\bCODEX_(?:STATUSES|ID_RE)\b/
|
|
120
|
+
const SCOPE_FENCE_PATTERN = /\bownsScope\b|\bfileInScope\s*\(/
|
|
121
|
+
const ROLE_SHAPE_PATTERN = /\b(?:isWorkerRole|isReviewerRole|isTowerRole)\s*\(|\bROLE\.(?:worker|reviewer|tower)\b/
|
|
122
|
+
const SHAPE_DECISION_PATTERN = /\bshapeFor\s*\(|\bshape\s*[.[]/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Count top-level arguments in a call's argument text.
|
|
126
|
+
* @param {string} args
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
function argumentCount(args) {
|
|
130
|
+
const trimmed = args.trim()
|
|
131
|
+
if (trimmed.length === 0) return 0
|
|
132
|
+
let depth = 0
|
|
133
|
+
let count = 1
|
|
134
|
+
for (const char of trimmed) {
|
|
135
|
+
if (char === '(' || char === '[' || char === '{') depth += 1
|
|
136
|
+
else if (char === ')' || char === ']' || char === '}') depth -= 1
|
|
137
|
+
else if (char === ',' && depth === 0) count += 1
|
|
138
|
+
}
|
|
139
|
+
return count
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} source
|
|
144
|
+
* @returns {Array<{ rule: string, detail: string }>}
|
|
145
|
+
*/
|
|
146
|
+
export function inspectSource(source) {
|
|
147
|
+
const violations = []
|
|
148
|
+
const lines = source.split('\n')
|
|
149
|
+
lines.forEach((line, index) => {
|
|
150
|
+
const at = `line ${index + 1}`
|
|
151
|
+
const trimmed = line.trim()
|
|
152
|
+
// Documentation may quote the vocabulary it forbids; only code counts.
|
|
153
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) return
|
|
154
|
+
if (KIND_PATTERN.test(line)) {
|
|
155
|
+
violations.push({ rule: 'kind-literal', detail: `${at}: ${line.trim()} — resolve shapeFor(mission.kind) instead` })
|
|
156
|
+
}
|
|
157
|
+
if (ROLE_PATTERN.test(line)) {
|
|
158
|
+
violations.push({ rule: 'role-literal', detail: `${at}: ${line.trim()} — use the vocabulary role predicates instead` })
|
|
159
|
+
}
|
|
160
|
+
if (GLOBAL_VOCAB_PATTERN.test(line)) {
|
|
161
|
+
violations.push({ rule: 'global-status-vocabulary', detail: `${at}: ${line.trim()} — use shape.isTerminal(status) / shape.transitions` })
|
|
162
|
+
}
|
|
163
|
+
if (REVIEW_TIP_PATTERN.test(line)) {
|
|
164
|
+
violations.push({
|
|
165
|
+
rule: 'review-tip-read',
|
|
166
|
+
detail: `${at}: ${line.trim()} — stamp reviews with a recorded commit (tower.branchTip(mission)) instead of a live worktree HEAD`,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
if (BATCH_STATUS_PATTERN.test(line)) {
|
|
170
|
+
violations.push({
|
|
171
|
+
rule: 'batch-status-literal',
|
|
172
|
+
detail: `${at}: ${line.trim()} — use BATCH_STATUS / canTransitionBatch from domain/batch.js`,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
if (LEASE_LEDGER_PATTERN.test(line)) {
|
|
176
|
+
violations.push({
|
|
177
|
+
rule: 'lease-ledger',
|
|
178
|
+
detail: `${at}: ${line.trim()} — the lease ledger is owned by adapters/leases.js`,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
if (BLOCKED_WRITE_PATTERN.test(line)) {
|
|
182
|
+
violations.push({
|
|
183
|
+
rule: 'blocked-status-write',
|
|
184
|
+
detail: `${at}: ${line.trim()} — blocked is derived from open blockers: use markBlocked(mission, text)`,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
if (EVIDENCE_READ_PATTERN.test(line)) {
|
|
188
|
+
violations.push({
|
|
189
|
+
rule: 'evidence-single-read',
|
|
190
|
+
detail: `${at}: ${line.trim()} — read evidence through domain/evidence.js (findEvidence/passingDiscovery/hasCheckEvidence)`,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
if (LEGACY_VOCABULARY_PATTERN.test(line)) {
|
|
194
|
+
violations.push({
|
|
195
|
+
rule: 'legacy-vocabulary',
|
|
196
|
+
detail: `${at}: ${line.trim()} — legacy formats/statuses are normalised by adapters/legacy.js; do not introduce a second vocabulary`,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
if (SCOPE_FENCE_PATTERN.test(line)) {
|
|
200
|
+
violations.push({
|
|
201
|
+
rule: 'scope-reach',
|
|
202
|
+
detail: `${at}: ${line.trim()} — path permission is shape.reach through scopeFenceBlocker(); no ownsScope flag or direct matcher`,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
if (ROLE_SHAPE_PATTERN.test(line) && SHAPE_DECISION_PATTERN.test(line)) {
|
|
206
|
+
violations.push({
|
|
207
|
+
rule: 'role-shape-conflation',
|
|
208
|
+
detail: `${at}: ${line.trim()} — resolve the role through domain/roles.js; the shape supplies facts, not role branches`,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
for (const match of line.matchAll(CALL_PATTERN)) {
|
|
212
|
+
const expected = match[1] === 'isTerminal' ? 2 : 3
|
|
213
|
+
if (argumentCount(match[2]) < expected) {
|
|
214
|
+
violations.push({
|
|
215
|
+
rule: 'shape-entry-point',
|
|
216
|
+
detail: `${at}: ${match[0]} — the only entry points are isTerminal(kind, status) and canTransition(kind, from, to)`,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
return violations
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} start
|
|
226
|
+
* @returns {string[]}
|
|
227
|
+
*/
|
|
228
|
+
function walk(start) {
|
|
229
|
+
const files = []
|
|
230
|
+
if (!fs.existsSync(start)) return files
|
|
231
|
+
for (const entry of fs.readdirSync(start, { withFileTypes: true })) {
|
|
232
|
+
const full = path.join(start, entry.name)
|
|
233
|
+
if (entry.isDirectory()) {
|
|
234
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
235
|
+
files.push(...walk(full))
|
|
236
|
+
} else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) {
|
|
237
|
+
files.push(full)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return files
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {string} repoRoot
|
|
245
|
+
* @returns {{ path: string, rule: string, detail: string }[]}
|
|
246
|
+
*/
|
|
247
|
+
export function scan(repoRoot) {
|
|
248
|
+
const results = []
|
|
249
|
+
for (const dir of SCAN_DIRS) {
|
|
250
|
+
for (const file of walk(path.join(repoRoot, dir))) {
|
|
251
|
+
const relative = path.relative(repoRoot, file).split(path.sep).join('/')
|
|
252
|
+
if (relative.startsWith(`${SHAPE_DIR}/`)) continue
|
|
253
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
254
|
+
for (const violation of inspectSource(source)) {
|
|
255
|
+
const exempt = RULE_EXEMPT_PATHS[violation.rule] ?? []
|
|
256
|
+
if (exempt.includes(relative)) continue
|
|
257
|
+
results.push({ path: relative, ...violation })
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return results
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} repoRoot
|
|
266
|
+
* @returns {{ waivers: Array<{ rule?: string, path?: string, reason?: string, owner?: string, ref?: string, match?: string }>, errors: string[] }}
|
|
267
|
+
*/
|
|
268
|
+
export function loadShapeWaivers(repoRoot) {
|
|
269
|
+
const file = path.join(repoRoot, WAIVER_FILE)
|
|
270
|
+
if (!fs.existsSync(file)) return { waivers: [], errors: [`${WAIVER_FILE}: missing`] }
|
|
271
|
+
let document
|
|
272
|
+
try {
|
|
273
|
+
document = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return { waivers: [], errors: [`${WAIVER_FILE}: not valid JSON (${String(error?.message ?? error)})`] }
|
|
276
|
+
}
|
|
277
|
+
const entries = document?.[WAIVER_KEY] ?? []
|
|
278
|
+
if (!Array.isArray(entries)) return { waivers: [], errors: [`${WAIVER_FILE}: ${WAIVER_KEY} must be an array`] }
|
|
279
|
+
const errors = []
|
|
280
|
+
for (const [index, entry] of entries.entries()) {
|
|
281
|
+
const where = `${WAIVER_FILE} ${WAIVER_KEY}[${index}]`
|
|
282
|
+
if (!entry || typeof entry !== 'object') {
|
|
283
|
+
errors.push(`${where}: must be an object`)
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
if (typeof entry.rule !== 'string' || entry.rule.length === 0) errors.push(`${where}: rule is required`)
|
|
287
|
+
if (typeof entry.path !== 'string' || entry.path.length === 0) errors.push(`${where}: path is required`)
|
|
288
|
+
if (typeof entry.reason !== 'string' || entry.reason.length === 0) errors.push(`${where}: reason is required`)
|
|
289
|
+
if (!entry.owner && !entry.ref) errors.push(`${where}: owner or ref is required`)
|
|
290
|
+
}
|
|
291
|
+
return { waivers: errors.length === 0 ? entries : [], errors }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {{ path: string, rule: string, detail: string }[]} violations
|
|
296
|
+
* @param {Array<{ rule?: string, path?: string, match?: string }>} waivers
|
|
297
|
+
* @returns {{ violations: typeof violations, waived: typeof violations, unused: Array<{ rule?: string, path?: string }> }}
|
|
298
|
+
*/
|
|
299
|
+
export function applyWaivers(violations, waivers) {
|
|
300
|
+
const waived = []
|
|
301
|
+
const used = new Set()
|
|
302
|
+
const remaining = []
|
|
303
|
+
for (const violation of violations) {
|
|
304
|
+
const index = waivers.findIndex((waiver) => {
|
|
305
|
+
if (waiver.path !== violation.path) return false
|
|
306
|
+
if (waiver.rule !== violation.rule) return false
|
|
307
|
+
if (waiver.match && !violation.detail.includes(waiver.match)) return false
|
|
308
|
+
return true
|
|
309
|
+
})
|
|
310
|
+
if (index >= 0) {
|
|
311
|
+
used.add(index)
|
|
312
|
+
waived.push(violation)
|
|
313
|
+
} else {
|
|
314
|
+
remaining.push(violation)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const unused = waivers.filter((_, index) => !used.has(index))
|
|
318
|
+
return { violations: remaining, waived, unused }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {string} repoRoot
|
|
323
|
+
* @returns {{ ok: boolean, violations: any[], waived: any[], errors: string[], unused: any[] }}
|
|
324
|
+
*/
|
|
325
|
+
export function evaluator(repoRoot = root) {
|
|
326
|
+
const reported = []
|
|
327
|
+
const waivers = loadShapeWaivers(repoRoot)
|
|
328
|
+
if (waivers.errors.length > 0) return { ok: false, violations: [], waived: [], errors: waivers.errors, unused: [] }
|
|
329
|
+
const applied = applyWaivers(scan(repoRoot), waivers.waivers)
|
|
330
|
+
for (const violation of applied.violations) {
|
|
331
|
+
reported.push(`shape-audit error: [${violation.rule}] ${violation.path} ${violation.detail}`)
|
|
332
|
+
}
|
|
333
|
+
if (applied.unused.length > 0) {
|
|
334
|
+
for (const waiver of applied.unused) {
|
|
335
|
+
reported.push(`shape-audit error: [stale-waiver] ${waiver.path} ${waiver.rule} no longer matches a violation; remove it`)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
ok: reported.length === 0,
|
|
340
|
+
violations: applied.violations,
|
|
341
|
+
waived: applied.waived,
|
|
342
|
+
errors: reported,
|
|
343
|
+
unused: applied.unused,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function main(argv) {
|
|
348
|
+
const rootIndex = argv.indexOf('--root')
|
|
349
|
+
const repoRoot = rootIndex >= 0 ? path.resolve(argv[rootIndex + 1]) : root
|
|
350
|
+
const result = evaluator(repoRoot)
|
|
351
|
+
if (argv.includes('--json')) {
|
|
352
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
353
|
+
} else if (result.ok) {
|
|
354
|
+
process.stdout.write(`shape-audit: ok (waived: ${result.waived.length})\n`)
|
|
355
|
+
} else {
|
|
356
|
+
for (const line of result.errors) process.stderr.write(`${line}\n`)
|
|
357
|
+
}
|
|
358
|
+
process.exitCode = result.ok ? 0 : 1
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(process.argv.slice(2))
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"surfaces": {
|
|
5
5
|
"configKeys": {
|
|
6
6
|
"seed": 64,
|
|
7
|
-
"value":
|
|
7
|
+
"value": 56,
|
|
8
8
|
"decisions": [
|
|
9
9
|
{
|
|
10
10
|
"from": 64,
|
|
@@ -12,13 +12,77 @@
|
|
|
12
12
|
"date": "2026-11-03",
|
|
13
13
|
"adr": "docs/adr/0005-component-surface.md",
|
|
14
14
|
"note": "Recorded contraction, not a free ratchet (#138). #121 removed the seven tools.expose* keys plus dev.hotReload, #136 replaced tests.enabled with the tower-tests row, and #133/#135 replaced docs.board/archive.enabled with the tower-board/tower-archive rows. The chain now continues here: the config-key surface is two-way like components, so any later increase or decrease needs its own dated decision."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"from": 53,
|
|
18
|
+
"to": 54,
|
|
19
|
+
"date": "2026-11-03",
|
|
20
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
21
|
+
"note": "#M39: one new key, review.mode (off/optional/required), replaces the deprecated review.requireReview boolean as the review policy. It is not a fifth component row: the reviewer preset row stays a capability, and the mode decides whether it is required (required) or unused (off). review.requireReview remains accepted for one release as a normalized alias, so it is a deprecated key rather than a removed one and the surface grows by exactly one."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"from": 54,
|
|
25
|
+
"to": 57,
|
|
26
|
+
"date": "2026-11-10",
|
|
27
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
28
|
+
"note": "#148/#149: the shape redesign adds the survey routing keys spawn.surveyPreset (core) and spawn.surveyModel (tuning), plus survey.sandbox (tuning) in the new survey section. tower-survey is the capability row; the preset/model names only route the read-only agent."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"from": 57,
|
|
32
|
+
"to": 55,
|
|
33
|
+
"date": "2026-11-10",
|
|
34
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
35
|
+
"note": "Review round 2 contracts the shape surface before release: spawn.surveyPreset could never be read (a survey's preset is shape-owned, so the key could only disagree with the registry) and survey.sandbox advertised a placement W1 does not implement. Only spawn.surveyModel remains as the optional model route; both removals are recorded here rather than silently shrinking the two-way surface."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"from": 55,
|
|
39
|
+
"to": 56,
|
|
40
|
+
"date": "2026-11-10",
|
|
41
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
42
|
+
"note": "#148 W2 implements the detached survey sandbox, so survey.sandbox returns as one tuning key: none (read-only shared checkout, the default and unchanged W1 behavior) or detached (a disposable detached worktree with a write/shell-capable preset). It is placement tuning, not a capability row or a second routing key; spawn.surveyPreset stays removed because the shape resolves the preset."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"from": 56,
|
|
46
|
+
"to": 57,
|
|
47
|
+
"date": "2026-11-10",
|
|
48
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
49
|
+
"note": "#148 W2 D5 adds gates.requireFreshSurvey (default false): the survey-stale dependency warning is advisory by default and becomes a merge blocker only when this gate is true. It is a merge-gate strictness key like the other gates.* entries, not a component."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"from": 57,
|
|
53
|
+
"to": 56,
|
|
54
|
+
"date": "2026-11-10",
|
|
55
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
56
|
+
"note": "#149 W4 contracts the surface by one: `tests.autoChecks` existed only to copy a discovery projection into the completion report, which made a second evidence shape. The receipt in mission.testRuns is now the one evidence shape read by domain/evidence.js, so the key has no remaining meaning and is removed rather than kept as a dead tuning switch."
|
|
15
57
|
}
|
|
16
58
|
]
|
|
17
59
|
},
|
|
18
60
|
"sections": {
|
|
19
61
|
"seed": 15,
|
|
20
|
-
"value":
|
|
21
|
-
"decisions": [
|
|
62
|
+
"value": 15,
|
|
63
|
+
"decisions": [
|
|
64
|
+
{
|
|
65
|
+
"from": 15,
|
|
66
|
+
"to": 15,
|
|
67
|
+
"date": "2026-11-10",
|
|
68
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
69
|
+
"note": "#148/#149 restore the section count to its seed: survey.sandbox needs a home, so the survey section returns the 15 sections that #121 shrank to 14. Net zero against the seed; no other section changed."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"from": 15,
|
|
73
|
+
"to": 14,
|
|
74
|
+
"date": "2026-11-10",
|
|
75
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
76
|
+
"note": "Review round 2 removes the survey section with its only key: survey.sandbox advertised a detached read-only checkout W1 does not implement. The section existed only to hold that key, so the count returns to 14."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"from": 14,
|
|
80
|
+
"to": 15,
|
|
81
|
+
"date": "2026-11-10",
|
|
82
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
83
|
+
"note": "#148 W2 restores the survey section for its one tuning key, survey.sandbox (placement: none|detached). The section count returns to the 15 of the earlier shape cut; no other section changed and no component row was added (the sandbox preset is an internal child of the existing tower-survey group)."
|
|
84
|
+
}
|
|
85
|
+
]
|
|
22
86
|
},
|
|
23
87
|
"tools": {
|
|
24
88
|
"seed": 27,
|
|
@@ -38,7 +102,7 @@
|
|
|
38
102
|
},
|
|
39
103
|
"components": {
|
|
40
104
|
"seed": 4,
|
|
41
|
-
"value":
|
|
105
|
+
"value": 12,
|
|
42
106
|
"decisions": [
|
|
43
107
|
{
|
|
44
108
|
"from": 4,
|
|
@@ -74,6 +138,13 @@
|
|
|
74
138
|
"date": "2026-11-02",
|
|
75
139
|
"adr": "docs/adr/0005-component-surface.md",
|
|
76
140
|
"note": "#134: the gate reads every shipped cordis.patch.yml, so the tower-ui row in ui-bundle/cordis.patch.yml joins the counted surface. The row was always shipped and unclassified; counting it is not new surface, it is the existing surface becoming visible to the policy."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"from": 11,
|
|
144
|
+
"to": 12,
|
|
145
|
+
"date": "2026-11-10",
|
|
146
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
147
|
+
"note": "#148/#149: tower-survey is a visible optional row: off means surveys cannot be planned and no survey agent is dispatched; build missions are unaffected."
|
|
77
148
|
}
|
|
78
149
|
]
|
|
79
150
|
}
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// changing the tsconfig is enough and this gate follows.
|
|
15
15
|
//
|
|
16
16
|
// Excluded tiers are data with a reason, not silent gaps:
|
|
17
|
+
// - `client.js` is the core Web Client half (#M40): a browser artifact checked
|
|
18
|
+
// by tests/plugin-config-client.test.mjs and the client rules, not by the
|
|
19
|
+
// Node tsc program;
|
|
17
20
|
// - `ui-bundle/**` is the optional Web client: a browser artifact with DOM
|
|
18
21
|
// types and no Node runtime, checked by tests/ui-bundle.test.mjs and the
|
|
19
22
|
// architecture client rules instead of the Node tsc program;
|
|
@@ -30,6 +33,10 @@ const WALK_SKIP = new Set(['node_modules', '.git', '.dsh-tower-worktrees', 'cove
|
|
|
30
33
|
|
|
31
34
|
/** Shipped `.js` tiers that are deliberately outside the Node tsc program. */
|
|
32
35
|
export const NOT_CHECKED_TIERS = [
|
|
36
|
+
{
|
|
37
|
+
pattern: 'client.js',
|
|
38
|
+
reason: 'core Web client: browser artifact, checked by tests/plugin-config-client.test.mjs',
|
|
39
|
+
},
|
|
33
40
|
{
|
|
34
41
|
pattern: 'ui-bundle/**',
|
|
35
42
|
reason: 'optional Web client: browser artifact, checked by tests/ui-bundle.test.mjs and the client architecture rules',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// tower-survey-sandbox preset policy (#148 W2): a survey that reads in a
|
|
2
|
+
// disposable detached sandbox may write and run commands *inside that sandbox*,
|
|
3
|
+
// but it still owns no branch, never commits, and may not merge, rebase, review
|
|
4
|
+
// or spawn. The allow-list is the structural guarantee: the denied tools do not
|
|
5
|
+
// exist in the session, so a prompt cannot talk the agent into using them.
|
|
6
|
+
//
|
|
7
|
+
// The shared-checkout survey preset (`dsh-tower/survey`) stays strictly
|
|
8
|
+
// read-only; this module is mounted only for a detached sandbox.
|
|
9
|
+
export const name = 'dsh-tower/survey-sandbox'
|
|
10
|
+
export const inject = ['tools', 'systemPrompt']
|
|
11
|
+
|
|
12
|
+
const ALLOW = [
|
|
13
|
+
'read',
|
|
14
|
+
'write',
|
|
15
|
+
'edit',
|
|
16
|
+
'glob',
|
|
17
|
+
'grep',
|
|
18
|
+
'pwsh',
|
|
19
|
+
'tower_mission',
|
|
20
|
+
'tower_status',
|
|
21
|
+
'tower_finding',
|
|
22
|
+
'tower_findings',
|
|
23
|
+
'tower_inbox',
|
|
24
|
+
'tower_complete',
|
|
25
|
+
'skill',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const PROTOCOL = [
|
|
29
|
+
'You are a Tower survey worker reading one recorded commit in a disposable detached sandbox.',
|
|
30
|
+
'Work only inside the sandbox path named in your briefing; never modify the main checkout.',
|
|
31
|
+
'You may write files and run shell commands inside the sandbox, but do not commit: completion requires',
|
|
32
|
+
'the sandbox HEAD to still be the recorded commit and zero new commits. The sandbox is removed when the mission completes.',
|
|
33
|
+
'You cannot spawn, merge, rebase, review or diff. Acknowledge inbox mail with tower_inbox; unread messages block completion.',
|
|
34
|
+
'Finish with tower_complete (report + checks evidence, optionally result:{paths,symbols,plan,notes}); the result is survey evidence only.',
|
|
35
|
+
].join(' ')
|
|
36
|
+
|
|
37
|
+
export function apply(ctx) {
|
|
38
|
+
try {
|
|
39
|
+
ctx.tools.restrict({ allow: ALLOW })
|
|
40
|
+
} catch {
|
|
41
|
+
// a composition without the tool registry still gets the protocol text
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
ctx.systemPrompt.section({ name: 'tower:survey-sandbox-protocol', order: 12000, text: PROTOCOL })
|
|
45
|
+
} catch {
|
|
46
|
+
// prompt assembly remains valid without the section
|
|
47
|
+
}
|
|
48
|
+
}
|
package/survey.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// tower-survey preset policy: a read-only inspection surface. The allow-list is
|
|
2
|
+
// the structural read-only guarantee (#148): a survey cannot write, edit, run a
|
|
3
|
+
// shell, commit, or merge because the tools do not exist in its session.
|
|
4
|
+
export const name = 'dsh-tower/survey'
|
|
5
|
+
export const inject = ['tools', 'systemPrompt']
|
|
6
|
+
|
|
7
|
+
const ALLOW = [
|
|
8
|
+
'read',
|
|
9
|
+
'glob',
|
|
10
|
+
'grep',
|
|
11
|
+
'tower_mission',
|
|
12
|
+
'tower_status',
|
|
13
|
+
'tower_finding',
|
|
14
|
+
'tower_findings',
|
|
15
|
+
'tower_inbox',
|
|
16
|
+
'tower_complete',
|
|
17
|
+
'skill',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const PROTOCOL = [
|
|
21
|
+
'You are a Tower survey worker in a DeepSeek Harness Tower workspace.',
|
|
22
|
+
'You read one recorded commit in the shared checkout and never modify it: do not write, edit, run a shell,',
|
|
23
|
+
'commit, rebase, or merge. Your tools are read, glob, grep, skill, and the Tower mission/status/finding/inbox/complete tools.',
|
|
24
|
+
'Acknowledge inbox mail with tower_inbox (it records Tower state, not a repository write): unread messages block completion.',
|
|
25
|
+
'Finish with tower_complete (report + checks evidence, optionally result:{paths,symbols,notes}); completion requires',
|
|
26
|
+
'HEAD to still be the recorded commit, the tracked-file status set to be unchanged, and zero commits.',
|
|
27
|
+
].join(' ')
|
|
28
|
+
|
|
29
|
+
export function apply(ctx) {
|
|
30
|
+
try {
|
|
31
|
+
ctx.tools.restrict({ allow: ALLOW })
|
|
32
|
+
} catch {
|
|
33
|
+
// a composition without the tool registry still gets the protocol text
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
ctx.systemPrompt.section({ name: 'tower:survey-protocol', order: 12000, text: PROTOCOL })
|
|
37
|
+
} catch {
|
|
38
|
+
// prompt assembly remains valid without the section
|
|
39
|
+
}
|
|
40
|
+
}
|
package/tools/complete.js
CHANGED
|
@@ -13,7 +13,7 @@ export function registerCompleteTool(ctx, deps) {
|
|
|
13
13
|
define(ctx, {
|
|
14
14
|
name: 'tower_complete',
|
|
15
15
|
description:
|
|
16
|
-
'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.',
|
|
16
|
+
'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 (zero commits plus HEAD pinned to readCommit for a survey; zero commits in its detached sandbox with HEAD pinned for a sandboxed survey), and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. result is survey-only evidence ({paths?,symbols?,plan?,notes?}); a build completion that carries one is refused. 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.',
|
|
17
17
|
parameters: CompleteInput,
|
|
18
18
|
aliases: {"missionId":["id"]},
|
|
19
19
|
requiresMissionId: true,
|
|
@@ -38,15 +38,15 @@ export function registerCompleteTool(ctx, deps) {
|
|
|
38
38
|
if (discovery.configured && discovery.receipt.ok !== true) {
|
|
39
39
|
throw new TowerToolError('tests-failed', `test discovery failed for ${args.missionId}: ${discovery.receipt.error}`)
|
|
40
40
|
}
|
|
41
|
-
if (discovery.configured && testsConfig.autoChecks !== false && discovery.check) {
|
|
42
|
-
if (checks.length >= 20) checks[19] = discovery.check
|
|
43
|
-
else checks.push(discovery.check)
|
|
44
|
-
}
|
|
45
41
|
}
|
|
42
|
+
// #149 W4: the receipt already lives in mission.testRuns and is read by
|
|
43
|
+
// domain/evidence.js. Copying a `tests` projection into the completion
|
|
44
|
+
// report would be a second evidence shape, so it is not done here.
|
|
46
45
|
return tower.complete({
|
|
47
46
|
missionId: args.missionId,
|
|
48
47
|
agentId: String(exec.agent.id),
|
|
49
48
|
report: args.report,
|
|
49
|
+
result: args.result,
|
|
50
50
|
checks,
|
|
51
51
|
})
|
|
52
52
|
},
|
package/tools/diff.js
CHANGED
|
@@ -18,7 +18,7 @@ import { DiffInput } from '../domain/contracts/tool-inputs/index.js'
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {any} ctx
|
|
21
|
-
* @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
|
|
21
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any }} deps
|
|
22
22
|
*/
|
|
23
23
|
export function registerDiffTool(ctx, deps) {
|
|
24
24
|
// openTower is injected by the composition root (#69).
|
|
@@ -31,9 +31,11 @@ export function registerDiffTool(ctx, deps) {
|
|
|
31
31
|
aliases: {"missionId":["id"]},
|
|
32
32
|
requiresMissionId: true,
|
|
33
33
|
output: output(SCHEMAS.diff),
|
|
34
|
-
async execute(args) {
|
|
34
|
+
async execute(args, exec) {
|
|
35
35
|
const tower = await deps.openTower(args.repoRoot)
|
|
36
|
-
|
|
36
|
+
// A reviewer's diff is bound to its recorded review commit (#149 W3).
|
|
37
|
+
const actor = await deps.actorFor(tower, exec)
|
|
38
|
+
return tower.diff({ missionId: args.missionId, maxBytes: args.maxBytes, actor })
|
|
37
39
|
},
|
|
38
40
|
})
|
|
39
41
|
}
|
package/tools/plan.js
CHANGED
|
@@ -25,7 +25,7 @@ export function registerPlanTool(ctx, deps) {
|
|
|
25
25
|
define(ctx, {
|
|
26
26
|
name: 'tower_plan',
|
|
27
27
|
description:
|
|
28
|
-
'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.',
|
|
28
|
+
'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. Returns survey-stale warnings for a build that depends on a survey whose recorded readCommit is not an ancestor of the base; those warnings are advisory and gates.requireFreshSurvey promotes them to merge blockers.',
|
|
29
29
|
parameters: PlanInput,
|
|
30
30
|
board: true,
|
|
31
31
|
output: output(SCHEMAS.plan),
|