dsh-tower 2.0.3 → 2.1.0-beta.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 +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /package/{lib → domain}/batch.js +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Release gate (#97, #76): a tag is a claim, so make it cost evidence.
|
|
3
|
+
//
|
|
4
|
+
// The gate compares three facts on every run:
|
|
5
|
+
//
|
|
6
|
+
// 1. `package.json` version vs the previous reachable `v<semver>` tag,
|
|
7
|
+
// 2. whether the CHANGELOG carries release content (a non-empty Unreleased
|
|
8
|
+
// section, or a non-empty section whose heading names the version),
|
|
9
|
+
// 3. on a tag push, that the pushed tag is exactly `v<version>`.
|
|
10
|
+
//
|
|
11
|
+
// It always prints `git describe`, the previous tag, the commit count since
|
|
12
|
+
// that tag and whether Unreleased is non-empty, so a large backlog is visible
|
|
13
|
+
// before an audit finds it. On a PR/branch push findings are warnings (exit 0);
|
|
14
|
+
// on a tag push a contradiction is an error (exit 1).
|
|
15
|
+
//
|
|
16
|
+
// Tag-push pitfall: `git describe --tags --abbrev=0` returns the *pushed* tag
|
|
17
|
+
// itself (and otherwise the nearest tag of any shape), so the previous tag is
|
|
18
|
+
// resolved from `<tag>^` and filtered to `v<semver>` (falling back to the newest
|
|
19
|
+
// version tag that is an ancestor).
|
|
20
|
+
//
|
|
21
|
+
// Usage:
|
|
22
|
+
// node scripts/release-check.mjs [--mode pr|tag] [--tag <name>] [--json]
|
|
23
|
+
//
|
|
24
|
+
// On CI the mode comes from GITHUB_REF_TYPE (`tag` on a tag push) and the tag
|
|
25
|
+
// name from GITHUB_REF_NAME. Locally, verify before tagging with
|
|
26
|
+
// `npm run release:check -- --mode tag --tag v<version>`.
|
|
27
|
+
import fs from 'node:fs'
|
|
28
|
+
import path from 'node:path'
|
|
29
|
+
import { spawnSync } from 'node:child_process'
|
|
30
|
+
import { compareVersions } from '../lib/compatibility.js'
|
|
31
|
+
import { findingRecorder, parseArgs, runMain, UsageError } from './lib/cli.mjs'
|
|
32
|
+
|
|
33
|
+
const USAGE = `usage: node scripts/release-check.mjs [--mode pr|tag] [--tag <name>] [--json] [--help]
|
|
34
|
+
|
|
35
|
+
--mode "pr" warns only (default); "tag" fails on a contradiction
|
|
36
|
+
--tag pushed tag name (default: GITHUB_REF_NAME in tag mode)
|
|
37
|
+
--json print the facts and findings as JSON
|
|
38
|
+
--help print this text`
|
|
39
|
+
|
|
40
|
+
const VERSION_TAG = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
41
|
+
|
|
42
|
+
// A version is stored without its tag prefix; a tag is `v` + that version. The
|
|
43
|
+
// release gate talks about both, so the conversion lives in one place.
|
|
44
|
+
function versionTag(version) {
|
|
45
|
+
return `v${stripTag(version)}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripTag(tag) {
|
|
49
|
+
return String(tag).replace(/^v/i, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function escapeRegExp(value) {
|
|
53
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function trimSection(lines) {
|
|
57
|
+
return lines.join('\n').trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sectionBody(text, headingMatches) {
|
|
61
|
+
const lines = String(text ?? '').split(/\r?\n/)
|
|
62
|
+
const start = lines.findIndex((line) => /^##\s+/.test(line) && headingMatches(line))
|
|
63
|
+
if (start < 0) return null
|
|
64
|
+
const body = []
|
|
65
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
66
|
+
if (/^##\s+/.test(lines[index])) break
|
|
67
|
+
body.push(lines[index])
|
|
68
|
+
}
|
|
69
|
+
return trimSection(body)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function unreleasedSection(changelog) {
|
|
73
|
+
return sectionBody(changelog, (line) => /unreleased|未发布/i.test(line))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function versionSection(changelog, version) {
|
|
77
|
+
if (!version) return null
|
|
78
|
+
const pattern = new RegExp(`(?:^|[^0-9A-Za-z.-])${escapeRegExp(version)}(?![0-9A-Za-z.-])`)
|
|
79
|
+
return sectionBody(changelog, (line) => pattern.test(line.replace(/^##\s+/, '')))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A section that only holds an HTML comment or a horizontal rule is a
|
|
83
|
+
// placeholder, not release notes.
|
|
84
|
+
export function hasContent(text) {
|
|
85
|
+
if (text === null || text === undefined) return false
|
|
86
|
+
const cleaned = String(text)
|
|
87
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
88
|
+
.replace(/^\s*[-*_]{3,}\s*$/gm, '')
|
|
89
|
+
.trim()
|
|
90
|
+
return cleaned.length > 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function evaluateRelease(input = {}) {
|
|
94
|
+
const version = String(input.version ?? '').trim()
|
|
95
|
+
if (!version) throw new UsageError('package.json has no version')
|
|
96
|
+
const mode = input.mode ?? 'pr'
|
|
97
|
+
if (mode !== 'pr' && mode !== 'tag') throw new UsageError(`--mode must be "pr" or "tag", not ${JSON.stringify(mode)}`)
|
|
98
|
+
const tag = input.tag ? String(input.tag) : null
|
|
99
|
+
const previousTag = input.previousTag ? String(input.previousTag) : null
|
|
100
|
+
const changelog = input.changelog ?? ''
|
|
101
|
+
const commitsSince = Number.isFinite(input.commitsSince) ? input.commitsSince : null
|
|
102
|
+
const describe = input.describe ?? null
|
|
103
|
+
const isTag = mode === 'tag'
|
|
104
|
+
const level = isTag ? 'error' : 'warning'
|
|
105
|
+
const suffix = isTag ? '' : ' (warning only outside a tag push)'
|
|
106
|
+
const count = commitsSince === null ? 'unknown' : String(commitsSince)
|
|
107
|
+
|
|
108
|
+
const unreleasedText = unreleasedSection(changelog)
|
|
109
|
+
const unreleased = hasContent(unreleasedText)
|
|
110
|
+
const versionNotes = versionSection(changelog, version)
|
|
111
|
+
const notesKind = unreleased ? 'unreleased' : hasContent(versionNotes) ? 'version-section' : 'none'
|
|
112
|
+
const previousVersion = previousTag ? stripTag(previousTag) : null
|
|
113
|
+
const findings = []
|
|
114
|
+
const record = findingRecorder(findings)
|
|
115
|
+
|
|
116
|
+
if (!previousTag) {
|
|
117
|
+
record('no-baseline', 'warning', `no reachable previous tag; cannot compare version ${version} against a release${suffix}`)
|
|
118
|
+
} else {
|
|
119
|
+
const direction = compareVersions(version, previousVersion)
|
|
120
|
+
// An unparseable version pair is treated as "did not move": publishing a
|
|
121
|
+
// version the release math cannot reason about is not evidence.
|
|
122
|
+
if (direction === null || direction <= 0) {
|
|
123
|
+
const relation = direction === 0 || direction === null ? 'equals' : 'is older than'
|
|
124
|
+
record(
|
|
125
|
+
'version-not-bumped',
|
|
126
|
+
level,
|
|
127
|
+
`version did not move: package.json ${version} ${relation} the previous tag ${previousTag}; ` +
|
|
128
|
+
`${count} commits since that tag; Unreleased is ${unreleased ? 'non-empty' : 'empty'}${suffix}`,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (notesKind === 'none') {
|
|
133
|
+
record(
|
|
134
|
+
'no-release-notes',
|
|
135
|
+
level,
|
|
136
|
+
`CHANGELOG has no release notes for ${version}: the Unreleased section is empty and there is ` +
|
|
137
|
+
`no non-empty "## ${version}(…)" section${suffix}`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isTag) {
|
|
142
|
+
if (!tag) {
|
|
143
|
+
record('tag-missing', 'error', 'tag push mode needs the tag name (GITHUB_REF_NAME or --tag)')
|
|
144
|
+
} else if (tag !== versionTag(version)) {
|
|
145
|
+
record(
|
|
146
|
+
'tag-name-mismatch',
|
|
147
|
+
'error',
|
|
148
|
+
`tag ${tag} does not match package version ${version}; expected ${versionTag(version)}`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const errors = findings.filter((finding) => finding.level === 'error').length
|
|
154
|
+
const warnings = findings.filter((finding) => finding.level === 'warning').length
|
|
155
|
+
return {
|
|
156
|
+
ok: errors === 0,
|
|
157
|
+
mode,
|
|
158
|
+
findings,
|
|
159
|
+
errors,
|
|
160
|
+
warnings,
|
|
161
|
+
facts: { version, mode, tag, previousTag, previousVersion, commitsSince, describe, unreleased, notesKind },
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Git facts ---------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function runGit(root, args) {
|
|
168
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' })
|
|
169
|
+
if (result.error || result.status !== 0) return null
|
|
170
|
+
return (result.stdout ?? '').trim()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function versionTags(root) {
|
|
174
|
+
const output = runGit(root, ['tag', '--list'])
|
|
175
|
+
if (!output) return []
|
|
176
|
+
return output.split('\n').map((line) => line.trim()).filter((tag) => VERSION_TAG.test(tag))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function newestAncestorTag(root, revision, exclude) {
|
|
180
|
+
const tags = versionTags(root).filter((tag) => tag !== exclude && runGit(root, ['merge-base', '--is-ancestor', tag, revision]) !== null)
|
|
181
|
+
tags.sort((left, right) => (compareVersions(right, left) ?? 0))
|
|
182
|
+
return tags[0] ?? null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The latest reachable *version* tag. `git describe` happily reports any tag
|
|
186
|
+
// near HEAD, so a maintenance tag (`ui-0.2.2`) must be filtered out here or the
|
|
187
|
+
// release math compares a version against a non-version string.
|
|
188
|
+
function describeVersionTag(root, revision) {
|
|
189
|
+
const described = runGit(root, ['describe', '--tags', '--abbrev=0', '--match', 'v[0-9]*', revision])
|
|
190
|
+
return described && VERSION_TAG.test(described) ? described : null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function resolvePreviousTag(root, tag) {
|
|
194
|
+
const tagExists = tag ? runGit(root, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]) !== null : false
|
|
195
|
+
const revision = tag && tagExists ? tag : 'HEAD'
|
|
196
|
+
// Never compare against the pushed tag itself: it is the release, not its
|
|
197
|
+
// predecessor. `<tag>^` is the commit that was HEAD before the tag existed.
|
|
198
|
+
if (tagExists) {
|
|
199
|
+
const parent = describeVersionTag(root, `${tag}^`)
|
|
200
|
+
if (parent && parent !== tag) return parent
|
|
201
|
+
}
|
|
202
|
+
const described = describeVersionTag(root, revision)
|
|
203
|
+
if (described && described !== tag) return described
|
|
204
|
+
return newestAncestorTag(root, revision, tag)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function collectFacts(root, options = {}) {
|
|
208
|
+
if (runGit(root, ['rev-parse', '--git-dir']) === null) {
|
|
209
|
+
throw new UsageError(`${root} is not a Git repository; the release gate compares the version against tags`)
|
|
210
|
+
}
|
|
211
|
+
const previousTag = options.previousTag ?? resolvePreviousTag(root, options.tag ?? null)
|
|
212
|
+
const commitsSince = previousTag ? Number(runGit(root, ['rev-list', '--count', `${previousTag}..HEAD`])) : null
|
|
213
|
+
const describe = runGit(root, ['describe', '--tags', '--always'])
|
|
214
|
+
return {
|
|
215
|
+
previousTag,
|
|
216
|
+
commitsSince: Number.isFinite(commitsSince) ? commitsSince : null,
|
|
217
|
+
describe: describe ?? null,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- CLI ---------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function printFacts(facts) {
|
|
224
|
+
const dash = (value) => (value === null || value === undefined || value === '' ? '-' : value)
|
|
225
|
+
console.log(
|
|
226
|
+
`release-check: version=${facts.version} mode=${facts.mode} tag=${dash(facts.tag)} ` +
|
|
227
|
+
`previous-tag=${dash(facts.previousTag)} commits-since=${dash(facts.commitsSince)} ` +
|
|
228
|
+
`describe=${dash(facts.describe)} unreleased=${facts.unreleased ? 'yes' : 'no'} notes=${facts.notesKind}`,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function cli(argv) {
|
|
233
|
+
const options = parseArgs(argv, {
|
|
234
|
+
initial: { mode: null, tag: null, json: false, help: false },
|
|
235
|
+
values: { '--mode': 'mode', '--tag': 'tag' },
|
|
236
|
+
flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
|
|
237
|
+
})
|
|
238
|
+
if (options.help) {
|
|
239
|
+
console.log(USAGE)
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
const mode = options.mode ?? (process.env.GITHUB_REF_TYPE === 'tag' ? 'tag' : 'pr')
|
|
243
|
+
let tag = options.tag
|
|
244
|
+
if (!tag && mode === 'tag' && process.env.GITHUB_REF_TYPE === 'tag') tag = process.env.GITHUB_REF_NAME || null
|
|
245
|
+
|
|
246
|
+
const root = process.cwd()
|
|
247
|
+
const packagePath = path.join(root, 'package.json')
|
|
248
|
+
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
249
|
+
let version
|
|
250
|
+
try {
|
|
251
|
+
version = JSON.parse(fs.readFileSync(packagePath, 'utf8')).version
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`release-check: cannot read ${packagePath}: ${error?.message ?? error}`)
|
|
254
|
+
return 2
|
|
255
|
+
}
|
|
256
|
+
let changelog
|
|
257
|
+
try {
|
|
258
|
+
changelog = fs.readFileSync(changelogPath, 'utf8')
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error(`release-check: cannot read ${changelogPath}: ${error?.message ?? error}`)
|
|
261
|
+
return 2
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let result
|
|
265
|
+
try {
|
|
266
|
+
const gitFacts = collectFacts(root, { tag })
|
|
267
|
+
result = evaluateRelease({ version, mode, tag, changelog, ...gitFacts })
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`release-check: ${error.message}`)
|
|
270
|
+
return 2
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (options.json) {
|
|
274
|
+
console.log(
|
|
275
|
+
JSON.stringify(
|
|
276
|
+
{
|
|
277
|
+
root,
|
|
278
|
+
package: packagePath,
|
|
279
|
+
changelog: changelogPath,
|
|
280
|
+
...result,
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2,
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
return result.ok ? 0 : 1
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
printFacts(result.facts)
|
|
290
|
+
for (const finding of result.findings) console.log(`${finding.level} [${finding.code}] ${finding.message}`)
|
|
291
|
+
if (result.ok) {
|
|
292
|
+
console.log(
|
|
293
|
+
result.warnings > 0
|
|
294
|
+
? `release-check: ok (${result.warnings} warning(s), no tag blocking this run)`
|
|
295
|
+
: 'release-check: ok',
|
|
296
|
+
)
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`release-check: ${result.errors} error(s), ${result.warnings} warning(s); refusing the release claim`)
|
|
299
|
+
}
|
|
300
|
+
return result.ok ? 0 : 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await runMain(import.meta.url, () => cli(process.argv.slice(2)), { errorPrefix: 'release-check', usage: USAGE })
|
package/skills/tower-recovery.md
CHANGED
|
@@ -24,8 +24,9 @@ untouched — call `tower_merge` to record it.
|
|
|
24
24
|
|
|
25
25
|
## `state.lock` reports another writer
|
|
26
26
|
|
|
27
|
-
Wait
|
|
28
|
-
|
|
27
|
+
Wait and retry; a live writer must never be interrupted. If the holder is provably dead, use a
|
|
28
|
+
supported recovery path (or ask the human to run one) that verifies the PID is gone and takes a
|
|
29
|
+
backup first. **Do not delete `state.lock` or the state directory from this playbook.**
|
|
29
30
|
|
|
30
31
|
## Worktree missing or dirty
|
|
31
32
|
|
|
@@ -35,6 +36,7 @@ explicit teardown `force` decision.
|
|
|
35
36
|
|
|
36
37
|
## Reset a repository's Tower state
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
**Do not delete `<git-common-dir>/dsh-tower/` from this playbook.** That directory contains mission,
|
|
40
|
+
review, roster, finding and activity history; deleting it can lose unrecoverable work. Stop and ask
|
|
41
|
+
the human to inspect `tower_doctor`, take a backup, and decide on a controlled repair. Worktrees and
|
|
42
|
+
branches must be cleaned only after that decision.
|
package/tools/adopt.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// tower_adopt: reconcile the roster with the subagent catalog.
|
|
2
|
+
import { Tower } from '../app/tower.js'
|
|
3
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
5
|
+
import { AdoptInput } from '../domain/contracts/tool-inputs/index.js'
|
|
6
|
+
import { PARENT_STATUS_OK, ROSTER_ACTIVITY, ROSTER_STATE } from '../domain/vocabulary.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} ctx
|
|
10
|
+
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any }} deps
|
|
11
|
+
*/
|
|
12
|
+
export function registerAdoptTool(ctx, deps) {
|
|
13
|
+
const { define, actorFor, requireTowerActor } = deps
|
|
14
|
+
define(ctx, {
|
|
15
|
+
name: 'tower_adopt',
|
|
16
|
+
description:
|
|
17
|
+
'Reconcile the roster with the durable subagent catalog after a restart or new session. Agents that are provably gone are marked retired (records are kept) or missing (retireMissing:false, so the tower can inspect/take over); agents whose parent catalog cannot be read stay unknown and are never changed. Only the tower may adopt.',
|
|
18
|
+
parameters: AdoptInput,
|
|
19
|
+
aliases: { retire_missing: ['retireMissing'] },
|
|
20
|
+
output: output(SCHEMAS.adopt),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
const tower = await Tower.open(args.repoRoot)
|
|
23
|
+
const actor = await actorFor(tower, exec)
|
|
24
|
+
requireTowerActor(actor, 'adopt the roster')
|
|
25
|
+
const view = await tower.rosterView()
|
|
26
|
+
const subagents = ctx.get('subagents')
|
|
27
|
+
if (!subagents) throw new TowerToolError('spawn-unavailable', 'subagents service is unavailable; cannot adopt')
|
|
28
|
+
|
|
29
|
+
const parents = [...new Set(view.roster.map((entry) => entry.parentSessionId).filter(Boolean))]
|
|
30
|
+
const activity = new Map()
|
|
31
|
+
const parentStatus = {}
|
|
32
|
+
for (const parent of parents) {
|
|
33
|
+
try {
|
|
34
|
+
const rows = await subagents.listDescendants(parent)
|
|
35
|
+
for (const row of rows) activity.set(String(row.id), row.activity ?? ROSTER_ACTIVITY.unknown)
|
|
36
|
+
parentStatus[parent] = PARENT_STATUS_OK
|
|
37
|
+
} catch (error) {
|
|
38
|
+
parentStatus[parent] = `unavailable: ${String(error?.message ?? error)}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const retiredCandidates = []
|
|
43
|
+
const presentIds = []
|
|
44
|
+
const absentIds = []
|
|
45
|
+
for (const entry of view.roster) {
|
|
46
|
+
if (entry.state === ROSTER_STATE.retired) continue
|
|
47
|
+
if (parentStatus[entry.parentSessionId] !== PARENT_STATUS_OK) continue
|
|
48
|
+
if (activity.has(entry.agentId)) {
|
|
49
|
+
presentIds.push(entry.agentId)
|
|
50
|
+
} else {
|
|
51
|
+
absentIds.push(entry.agentId)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const observations = await tower.recordRosterObservations({ present: presentIds, absent: absentIds, confirmations: 2 })
|
|
55
|
+
retiredCandidates.push(...observations.candidates)
|
|
56
|
+
const retireMissing = args.retireMissing !== false
|
|
57
|
+
const { retired } = retireMissing ? await tower.markRetired(retiredCandidates) : { retired: [] }
|
|
58
|
+
const { missing } = retireMissing ? { missing: [] } : await tower.markMissing(retiredCandidates)
|
|
59
|
+
const after = await tower.rosterView()
|
|
60
|
+
return {
|
|
61
|
+
retired,
|
|
62
|
+
missing,
|
|
63
|
+
unconfirmed: absentIds.filter((agentId) => !retiredCandidates.includes(agentId)),
|
|
64
|
+
skippedUnknownParents: Object.entries(parentStatus)
|
|
65
|
+
.filter(([, status]) => status !== PARENT_STATUS_OK)
|
|
66
|
+
.map(([parent]) => parent),
|
|
67
|
+
roster: after.roster,
|
|
68
|
+
parentStatus,
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
package/tools/batch.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// tower_batch: finite evidence batches.
|
|
2
|
+
import { Tower } from '../app/tower.js'
|
|
3
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
5
|
+
import { BatchInput } from '../domain/contracts/tool-inputs/index.js'
|
|
6
|
+
import { BATCH_ACTION } from '../domain/vocabulary.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} ctx
|
|
10
|
+
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireTowerActor: any, summarizeBatch: any, verifyBatchReceiptFiles: any }} deps
|
|
11
|
+
*/
|
|
12
|
+
export function registerBatchTool(ctx, deps) {
|
|
13
|
+
const { define, actorFor, requireTowerActor, summarizeBatch, verifyBatchReceiptFiles } = deps
|
|
14
|
+
define(ctx, {
|
|
15
|
+
name: 'tower_batch',
|
|
16
|
+
description:
|
|
17
|
+
'Manage finite evidence batches: create freezes missionIds/requiredChecks/receiptPaths, status summarizes members, close verifies every required check and receipt hash, abandon retires a stale open batch. Membership and exclusions are immutable after create; close/abandon never remove missions.',
|
|
18
|
+
parameters: BatchInput,
|
|
19
|
+
board: true,
|
|
20
|
+
output: output(SCHEMAS.batch),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
const tower = await Tower.open(args.repoRoot)
|
|
23
|
+
const actor = await actorFor(tower, exec)
|
|
24
|
+
if (args.action === BATCH_ACTION.status) {
|
|
25
|
+
const state = await tower.load()
|
|
26
|
+
const batch = args.batchId
|
|
27
|
+
? (state.batches ?? []).find((entry) => entry.id === args.batchId)
|
|
28
|
+
: null
|
|
29
|
+
if (args.batchId && !batch) throw new TowerToolError('invalid-argument', `unknown batch "${args.batchId}"`)
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
action: BATCH_ACTION.status,
|
|
33
|
+
batches: (state.batches ?? []).map((entry) => summarizeBatch(state, entry)),
|
|
34
|
+
...(batch ? { batch: summarizeBatch(state, batch) } : {}),
|
|
35
|
+
...(args.batchId ? { summary: summarizeBatch(state, batch) } : {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
requireTowerActor(actor, args.action === BATCH_ACTION.create ? 'create a batch' : args.action === BATCH_ACTION.abandon ? 'abandon a batch' : 'close a batch')
|
|
39
|
+
if (args.action === BATCH_ACTION.create) {
|
|
40
|
+
const batch = await tower.createBatch(args, actor.name)
|
|
41
|
+
return { ok: true, action: BATCH_ACTION.create, batch, batches: (await tower.batchStatus()).batches }
|
|
42
|
+
}
|
|
43
|
+
if (typeof args.batchId !== 'string' || args.batchId.trim().length === 0) {
|
|
44
|
+
throw new TowerToolError('invalid-argument', `tower_batch ${args.action} requires batchId`)
|
|
45
|
+
}
|
|
46
|
+
const batchId = args.batchId.trim()
|
|
47
|
+
if (args.action === BATCH_ACTION.abandon) {
|
|
48
|
+
const result = await tower.abandonBatch(batchId, actor.name, /** @type {any} */ (args.reason))
|
|
49
|
+
return { ok: true, action: BATCH_ACTION.abandon, batch: result.batch, blockers: [] }
|
|
50
|
+
}
|
|
51
|
+
const closeState = await tower.load()
|
|
52
|
+
const closeBatch = (closeState.batches ?? []).find((entry) => entry.id === batchId)
|
|
53
|
+
if (closeBatch) {
|
|
54
|
+
const receiptProblems = await verifyBatchReceiptFiles(tower.root, closeBatch, args)
|
|
55
|
+
if (receiptProblems.length > 0) {
|
|
56
|
+
return { ok: false, action: BATCH_ACTION.close, batch: closeBatch, blockers: receiptProblems, checks: [] }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const result = await tower.closeBatch(batchId, {
|
|
60
|
+
checks: args.checks,
|
|
61
|
+
treeTip: args.treeTip,
|
|
62
|
+
receiptHashes: args.receiptHashes,
|
|
63
|
+
}, actor.name)
|
|
64
|
+
return {
|
|
65
|
+
ok: result.ok,
|
|
66
|
+
action: BATCH_ACTION.close,
|
|
67
|
+
batch: result.batch,
|
|
68
|
+
blockers: (result.blockers ?? []).map((blocker) => ({
|
|
69
|
+
code: blocker.code ?? 'batch-invalid',
|
|
70
|
+
detail: blocker.detail ?? blocker.message ?? String(blocker),
|
|
71
|
+
...(blocker.cleanup ? { cleanup: blocker.cleanup } : {}),
|
|
72
|
+
...(blocker.timeout ? { timeout: true } : {}),
|
|
73
|
+
})),
|
|
74
|
+
checks: result.checks ?? [],
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// tower_complete: worker completion admission.
|
|
2
|
+
import { Tower } from '../app/tower.js'
|
|
3
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
5
|
+
import { CompleteInput } from '../domain/contracts/tool-inputs/index.js'
|
|
6
|
+
import { isCompleteAdmission } from '../domain/vocabulary.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} ctx
|
|
10
|
+
* @param {{ define: import('../types/index.js').DefineTool, runMissionDiscovery: any }} deps
|
|
11
|
+
*/
|
|
12
|
+
export function registerCompleteTool(ctx, deps) {
|
|
13
|
+
const { define, runMissionDiscovery } = deps
|
|
14
|
+
define(ctx, {
|
|
15
|
+
name: 'tower_complete',
|
|
16
|
+
description:
|
|
17
|
+
'Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report; completion means ready for preflight and optional review, not merged.',
|
|
18
|
+
parameters: CompleteInput,
|
|
19
|
+
aliases: {"missionId":["id"]},
|
|
20
|
+
requiresMissionId: true,
|
|
21
|
+
board: true,
|
|
22
|
+
output: output(SCHEMAS.complete),
|
|
23
|
+
async execute(args, exec) {
|
|
24
|
+
const tower = await Tower.open(args.repoRoot)
|
|
25
|
+
// Validate authorization/state before executing repository-controlled
|
|
26
|
+
// test code or writing a test receipt.
|
|
27
|
+
await tower.checkCompletion({
|
|
28
|
+
missionId: args.missionId,
|
|
29
|
+
agentId: String(exec.agent.id),
|
|
30
|
+
report: args.report,
|
|
31
|
+
})
|
|
32
|
+
const context = await tower.missionContext(args.missionId)
|
|
33
|
+
const checks = [...(Array.isArray(args.checks) ? args.checks : [])]
|
|
34
|
+
const testsConfig = /** @type {any} */ (tower.config.tests ?? {})
|
|
35
|
+
if (testsConfig.enabled !== false && isCompleteAdmission(testsConfig.admission)) {
|
|
36
|
+
const discovery = await runMissionDiscovery(tower, { id: args.missionId, worktree: context.mission.worktree })
|
|
37
|
+
if (discovery.configured && discovery.receipt.ok !== true) {
|
|
38
|
+
throw new TowerToolError('tests-failed', `test discovery failed for ${args.missionId}: ${discovery.receipt.error}`)
|
|
39
|
+
}
|
|
40
|
+
if (discovery.configured && testsConfig.autoChecks !== false && discovery.check) {
|
|
41
|
+
if (checks.length >= 20) checks[19] = discovery.check
|
|
42
|
+
else checks.push(discovery.check)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return tower.complete({
|
|
46
|
+
missionId: args.missionId,
|
|
47
|
+
agentId: String(exec.agent.id),
|
|
48
|
+
report: args.report,
|
|
49
|
+
checks,
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
}
|
package/tools/diff.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The result is bound to the recorded reviewed tip only when requested.
|
|
2
|
+
// Callers must treat the patch as read-only evidence.
|
|
3
|
+
// The tool never checks out or mutates the mission branch.
|
|
4
|
+
// Reviewers may use it without holding the state lock.
|
|
5
|
+
// tower_diff — read-only mission branch view.
|
|
6
|
+
//
|
|
7
|
+
// Input: DiffInput (missionId/id, maxBytes) from domain contracts.
|
|
8
|
+
// Output: merge base, changed files, commits, capped patch.
|
|
9
|
+
// Authorization: tower, reviewer, or assigned worker may inspect.
|
|
10
|
+
// Side effects: none; it never writes state, git, or the repository.
|
|
11
|
+
// Failure modes: unknown mission, missing worktree, stale branch.
|
|
12
|
+
// Errors are classified as [tower:<code>] by the registry.
|
|
13
|
+
// The patch cap is enforced by the pure read projection.
|
|
14
|
+
// See docs/reference/tools.generated.md for the public shape.
|
|
15
|
+
// tower_diff: read-only mission branch view.
|
|
16
|
+
import { Tower } from '../app/tower.js'
|
|
17
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
18
|
+
import { DiffInput } from '../domain/contracts/tool-inputs/index.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {any} ctx
|
|
22
|
+
* @param {{ define: import('../types/index.js').DefineTool }} deps
|
|
23
|
+
*/
|
|
24
|
+
export function registerDiffTool(ctx, deps) {
|
|
25
|
+
const { define } = deps
|
|
26
|
+
define(ctx, {
|
|
27
|
+
name: 'tower_diff',
|
|
28
|
+
description:
|
|
29
|
+
'Read-only view of a mission branch for review: merge base, changed files, commits, and a capped patch against the recorded base. Safe for reviewers; it never modifies the repository.',
|
|
30
|
+
parameters: DiffInput,
|
|
31
|
+
aliases: {"missionId":["id"]},
|
|
32
|
+
requiresMissionId: true,
|
|
33
|
+
output: output(SCHEMAS.diff),
|
|
34
|
+
async execute(args) {
|
|
35
|
+
const tower = await Tower.open(args.repoRoot)
|
|
36
|
+
return tower.diff({ missionId: args.missionId, maxBytes: args.maxBytes })
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
package/tools/doctor.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// tower_doctor: read-only health check.
|
|
2
|
+
import { Tower } from '../app/tower.js'
|
|
3
|
+
import { resolveRepo } from '../lib/git.js'
|
|
4
|
+
import { runtimeConfig } from '../lib/config.js'
|
|
5
|
+
import { runDoctor } from '../lib/doctor.js'
|
|
6
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
7
|
+
import { DoctorInput } from '../domain/contracts/tool-inputs/index.js'
|
|
8
|
+
import { DOCTOR_STATUS } from '../domain/vocabulary.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {any} ctx
|
|
12
|
+
* @param {import('../types/index.js').DefineTool} define
|
|
13
|
+
*/
|
|
14
|
+
export function registerDoctorTool(ctx, define) {
|
|
15
|
+
define(ctx, {
|
|
16
|
+
name: 'tower_doctor',
|
|
17
|
+
description:
|
|
18
|
+
'Read-only environment and repository health check: git, state schema, config, presets/subagents/HMR, workspace lock, roster staleness, worktree ownership, test discovery, leases. Returns checks with pass/warn/fail and an environment contract. Never writes state.',
|
|
19
|
+
parameters: DoctorInput,
|
|
20
|
+
output: output(SCHEMAS.doctor),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
let tower
|
|
23
|
+
let configError = null
|
|
24
|
+
try {
|
|
25
|
+
tower = await Tower.open(args.repoRoot)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = String(error?.message ?? error)
|
|
28
|
+
if (error?.code !== 'config-invalid' && error?.code !== 'config-unknown-key') throw error
|
|
29
|
+
configError = message
|
|
30
|
+
const repo = await resolveRepo(args.repoRoot)
|
|
31
|
+
tower = new Tower(repo, { ...runtimeConfig(), configPath: null, repoConfigPresent: true })
|
|
32
|
+
}
|
|
33
|
+
const report = await runDoctor(/** @type {any} */ ({ tower, ctx, agent: exec.agent }))
|
|
34
|
+
if (!configError) return report
|
|
35
|
+
return {
|
|
36
|
+
...report,
|
|
37
|
+
ok: false,
|
|
38
|
+
checks: [
|
|
39
|
+
{
|
|
40
|
+
id: 'config',
|
|
41
|
+
status: DOCTOR_STATUS.fail,
|
|
42
|
+
detail: configError,
|
|
43
|
+
fix: 'repair <git-common-dir>/dsh-tower/config.json and re-run tower_doctor',
|
|
44
|
+
},
|
|
45
|
+
...(report.checks ?? []),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
package/tools/finding.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// tower_finding: file one structured finding.
|
|
2
|
+
import { Tower } from '../app/tower.js'
|
|
3
|
+
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
4
|
+
import { FindingInput } from '../domain/contracts/tool-inputs/index.js'
|
|
5
|
+
import { ROLE } from '../domain/vocabulary.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {any} ctx
|
|
9
|
+
* @param {{ define: import('../types/index.js').DefineTool, actorFor: any, requireKnownActor: any }} deps
|
|
10
|
+
*/
|
|
11
|
+
export function registerFindingTool(ctx, deps) {
|
|
12
|
+
const { define, actorFor, requireKnownActor } = deps
|
|
13
|
+
define(ctx, {
|
|
14
|
+
name: 'tower_finding',
|
|
15
|
+
description:
|
|
16
|
+
'File a structured finding (bug / improve / vuln / idea) for the tower to route. Use this for anything notable OUTSIDE your mission scope instead of editing out-of-scope files. Severity is one of low|medium|high|critical. Use tower_findings to list, read, or close findings later.',
|
|
17
|
+
parameters: FindingInput,
|
|
18
|
+
aliases: {"missionId":["mission_id"]},
|
|
19
|
+
board: true,
|
|
20
|
+
output: output(SCHEMAS.finding),
|
|
21
|
+
async execute(args, exec) {
|
|
22
|
+
const tower = await Tower.open(args.repoRoot)
|
|
23
|
+
const actor = await actorFor(tower, exec)
|
|
24
|
+
requireKnownActor(actor, 'tower_finding')
|
|
25
|
+
const view = await tower.rosterView()
|
|
26
|
+
const self = view.roster.find((entry) => entry.agentId === String(exec.agent.id))
|
|
27
|
+
return tower.addFinding({
|
|
28
|
+
type: args.type,
|
|
29
|
+
title: args.title,
|
|
30
|
+
severity: args.severity,
|
|
31
|
+
summary: args.summary,
|
|
32
|
+
location: args.location,
|
|
33
|
+
details: args.details,
|
|
34
|
+
suggested_fix: args.suggested_fix,
|
|
35
|
+
missionId: args.missionId,
|
|
36
|
+
reportedBy: self ? self.name : ROLE.tower,
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|