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,179 @@
|
|
|
1
|
+
// Pure shared vocabulary (#56 P5): every status/type/action enum used by the
|
|
2
|
+
// tool contracts and handlers. Keeping one canonical value set here lets
|
|
3
|
+
// tools/** import constants/predicates instead of spelling literals.
|
|
4
|
+
// No fs, git, or config.
|
|
5
|
+
// @ts-check
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FINDING_SEVERITIES,
|
|
9
|
+
FINDING_STATUSES,
|
|
10
|
+
FINDING_TYPES,
|
|
11
|
+
MERGE_VERDICTS,
|
|
12
|
+
MISSION_STATUSES,
|
|
13
|
+
PAUSE_CAUSES,
|
|
14
|
+
} from './mission.js'
|
|
15
|
+
import { ROSTER_INACTIVE_STATES } from './roster.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
FINDING_SEVERITIES,
|
|
19
|
+
FINDING_STATUSES,
|
|
20
|
+
FINDING_TYPES,
|
|
21
|
+
MERGE_VERDICTS,
|
|
22
|
+
MISSION_STATUSES,
|
|
23
|
+
PAUSE_CAUSES,
|
|
24
|
+
ROSTER_INACTIVE_STATES,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mission inputs accepted by tower_plan.
|
|
28
|
+
/** @type {readonly string[]} */
|
|
29
|
+
export const MISSION_KINDS = ['build', 'survey']
|
|
30
|
+
/** @type {readonly string[]} */
|
|
31
|
+
export const REVIEW_POLICIES = ['required-clean', 'none']
|
|
32
|
+
|
|
33
|
+
// Roster actors (the caller resolved by actorFor) and createable agent roles.
|
|
34
|
+
export const ROLE = Object.freeze({
|
|
35
|
+
tower: 'tower',
|
|
36
|
+
worker: 'worker',
|
|
37
|
+
reviewer: 'reviewer',
|
|
38
|
+
unknown: 'unknown',
|
|
39
|
+
})
|
|
40
|
+
/** @type {readonly string[]} */
|
|
41
|
+
export const ROLES = Object.freeze([ROLE.worker, ROLE.reviewer])
|
|
42
|
+
/** @type {readonly string[]} */
|
|
43
|
+
export const ACTOR_ROLES = Object.freeze([ROLE.tower, ROLE.worker, ROLE.reviewer, ROLE.unknown])
|
|
44
|
+
export const DEFAULT_ROLE = ROLE.worker
|
|
45
|
+
|
|
46
|
+
/** @param {string | null | undefined} role */
|
|
47
|
+
export function isTowerRole(role) {
|
|
48
|
+
return role === ROLE.tower
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {string | null | undefined} role */
|
|
52
|
+
export function isWorkerRole(role) {
|
|
53
|
+
return role === ROLE.worker
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {string | null | undefined} role */
|
|
57
|
+
export function isReviewerRole(role) {
|
|
58
|
+
return role === ROLE.reviewer
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string | null | undefined} role */
|
|
62
|
+
export function isUnknownRole(role) {
|
|
63
|
+
return role === ROLE.unknown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Inbox recipients: a roster name, the tower, or a broadcast.
|
|
67
|
+
export const RECIPIENT = Object.freeze({ tower: ROLE.tower, all: 'all' })
|
|
68
|
+
|
|
69
|
+
/** @param {string | null | undefined} to */
|
|
70
|
+
export function isTowerRecipient(to) {
|
|
71
|
+
return to === RECIPIENT.tower
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {string | null | undefined} to */
|
|
75
|
+
export function isBroadcast(to) {
|
|
76
|
+
return to === RECIPIENT.all
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Action discriminators for the repository tools.
|
|
80
|
+
export const BATCH_ACTION = Object.freeze({
|
|
81
|
+
create: 'create',
|
|
82
|
+
status: 'status',
|
|
83
|
+
close: 'close',
|
|
84
|
+
abandon: 'abandon',
|
|
85
|
+
})
|
|
86
|
+
/** @type {readonly string[]} */
|
|
87
|
+
export const BATCH_ACTIONS = Object.freeze(Object.values(BATCH_ACTION))
|
|
88
|
+
|
|
89
|
+
export const RECOVERY_ACTION = Object.freeze({ status: 'status', clearStaleLock: 'clear-stale-lock' })
|
|
90
|
+
/** @type {readonly string[]} */
|
|
91
|
+
export const RECOVERY_ACTIONS = Object.freeze(Object.values(RECOVERY_ACTION))
|
|
92
|
+
export const RECOVERY_CONFIRM = 'CLEAR_STALE_LOCK'
|
|
93
|
+
|
|
94
|
+
export const LEASE_ACTION = Object.freeze({ acquire: 'acquire', release: 'release', status: 'status' })
|
|
95
|
+
/** @type {readonly string[]} */
|
|
96
|
+
export const LEASE_ACTIONS = Object.freeze(Object.values(LEASE_ACTION))
|
|
97
|
+
|
|
98
|
+
export const LEGACY_MAP_ACTION = Object.freeze({ map: 'map', status: 'status' })
|
|
99
|
+
/** @type {readonly string[]} */
|
|
100
|
+
export const LEGACY_MAP_ACTIONS = Object.freeze(Object.values(LEGACY_MAP_ACTION))
|
|
101
|
+
|
|
102
|
+
export const STATUS_FORMAT = Object.freeze({ json: 'json', markdown: 'markdown' })
|
|
103
|
+
/** @type {readonly string[]} */
|
|
104
|
+
export const STATUS_FORMATS = Object.freeze(Object.values(STATUS_FORMAT))
|
|
105
|
+
|
|
106
|
+
/** @param {string | null | undefined} format */
|
|
107
|
+
export function isMarkdownFormat(format) {
|
|
108
|
+
return format === STATUS_FORMAT.markdown
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Finding filters extend the lifecycle statuses with the read-only "all".
|
|
112
|
+
export const FINDING_FILTER_ALL = 'all'
|
|
113
|
+
/** @type {readonly string[]} */
|
|
114
|
+
export const FINDING_FILTER_STATUSES = Object.freeze([...FINDING_STATUSES, FINDING_FILTER_ALL])
|
|
115
|
+
|
|
116
|
+
// Test admission modes read from config by tower_spawn/tower_complete.
|
|
117
|
+
export const TESTS_ADMISSION = Object.freeze({ off: 'off', spawn: 'spawn', complete: 'complete' })
|
|
118
|
+
/** @type {readonly string[]} */
|
|
119
|
+
export const TESTS_ADMISSIONS = Object.freeze(Object.values(TESTS_ADMISSION))
|
|
120
|
+
|
|
121
|
+
/** @param {string | null | undefined} admission */
|
|
122
|
+
export function isSpawnAdmission(admission) {
|
|
123
|
+
return admission === TESTS_ADMISSION.spawn
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @param {string | null | undefined} admission */
|
|
127
|
+
export function isCompleteAdmission(admission) {
|
|
128
|
+
return admission === TESTS_ADMISSION.complete
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Roster row states and observed catalog activity.
|
|
132
|
+
export const ROSTER_STATE = Object.freeze({
|
|
133
|
+
reserved: 'reserved',
|
|
134
|
+
active: 'active',
|
|
135
|
+
inactive: 'inactive',
|
|
136
|
+
retired: 'retired',
|
|
137
|
+
failed: 'failed',
|
|
138
|
+
interrupted: 'interrupted',
|
|
139
|
+
missing: 'missing',
|
|
140
|
+
stale: 'stale',
|
|
141
|
+
})
|
|
142
|
+
export const ROSTER_STATES = Object.freeze([...ROSTER_INACTIVE_STATES, ROSTER_STATE.reserved, ROSTER_STATE.active])
|
|
143
|
+
/** States treated as gone by roster reporting, excluding "inactive". */
|
|
144
|
+
export const ROSTER_GONE_STATES = Object.freeze([
|
|
145
|
+
ROSTER_STATE.retired,
|
|
146
|
+
ROSTER_STATE.failed,
|
|
147
|
+
ROSTER_STATE.interrupted,
|
|
148
|
+
ROSTER_STATE.missing,
|
|
149
|
+
ROSTER_STATE.stale,
|
|
150
|
+
])
|
|
151
|
+
export const ROSTER_ACTIVITY = Object.freeze({
|
|
152
|
+
running: 'running',
|
|
153
|
+
inactive: 'inactive',
|
|
154
|
+
missing: 'missing',
|
|
155
|
+
unknown: 'unknown',
|
|
156
|
+
})
|
|
157
|
+
export const PARENT_STATUS_OK = 'ok'
|
|
158
|
+
|
|
159
|
+
/** @param {string | null | undefined} state */
|
|
160
|
+
export function isReservedState(state) {
|
|
161
|
+
return state === ROSTER_STATE.reserved
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {string | null | undefined} activity */
|
|
165
|
+
export function isRunningActivity(activity) {
|
|
166
|
+
return activity === ROSTER_ACTIVITY.running
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Spawn completion outcomes recorded in the roster.
|
|
170
|
+
export const SPAWN_OUTCOME = Object.freeze({ failed: 'failed', inactive: 'inactive' })
|
|
171
|
+
export const STOP_REASON = Object.freeze({ completed: 'completed' })
|
|
172
|
+
|
|
173
|
+
/** @param {string | null | undefined} reason */
|
|
174
|
+
export function isCompletedStopReason(reason) {
|
|
175
|
+
return reason === STOP_REASON.completed
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Doctor check severities.
|
|
179
|
+
export const DOCTOR_STATUS = Object.freeze({ pass: 'pass', warn: 'warn', fail: 'fail' })
|
package/index.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
// dsh-tower — Host entry.
|
|
2
2
|
//
|
|
3
|
-
// Tower mode is a session-level stance.
|
|
4
|
-
// `/tower` command
|
|
5
|
-
// [message]`, and `/tower <message
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// Tower mode is a session-level stance. It is intentionally operable by both
|
|
4
|
+
// sides: the `/tower` command (bare `/tower`, `/tower on [message]`, `/tower
|
|
5
|
+
// off [message]`, and `/tower <message>`; trailing text is forwarded to the
|
|
6
|
+
// model as the next user message) and, when config.allowAgentControl is true
|
|
7
|
+
// (the default), the model through `tower_control`. Every change is recorded
|
|
8
|
+
// with its actor and reason; set allowAgentControl:false for human-only mode
|
|
9
|
+
// control.
|
|
9
10
|
//
|
|
10
11
|
// `/tower reload` asks DSH HMR to reconcile this profile and reload the Tower
|
|
11
12
|
// plugin from disk when the HMR service is present (dev/linked installs).
|
|
12
13
|
import fs from 'node:fs'
|
|
13
14
|
import { randomUUID } from 'node:crypto'
|
|
14
|
-
import { registerTowerTools } from './lib/
|
|
15
|
+
import { registerTowerTools } from './lib/tool-registry.js'
|
|
15
16
|
import { loadModes, saveModes, storePath } from './lib/modeStore.js'
|
|
17
|
+
import { ModeState } from './lib/modeState.js'
|
|
16
18
|
import { Config as ConfigSchema, DEFAULT_DENY_TOOLS, configureRuntime, runtimeConfig } from './lib/config.js'
|
|
17
|
-
import { TowerToolError } from './
|
|
19
|
+
import { TowerToolError } from './domain/errors.js'
|
|
18
20
|
import { resolveRepo } from './lib/git.js'
|
|
19
21
|
import {
|
|
20
22
|
acquireWorkspaceLock,
|
|
@@ -24,13 +26,19 @@ import {
|
|
|
24
26
|
workspaceLockToken,
|
|
25
27
|
} from './lib/workspaceLock.js'
|
|
26
28
|
import { createHmrBridge } from './lib/hmr.js'
|
|
29
|
+
import { assertPresetsAvailable } from './lib/presets.js'
|
|
27
30
|
|
|
28
31
|
export const name = 'dsh-tower'
|
|
29
32
|
export const inject = ['tools', 'systemPrompt']
|
|
30
33
|
export const Config = ConfigSchema
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
const modeState = new ModeState()
|
|
33
36
|
let denyTools = new Set(DEFAULT_DENY_TOOLS)
|
|
37
|
+
|
|
38
|
+
function modeOn(key) {
|
|
39
|
+
const entry = key ? modeState.get(key) : undefined
|
|
40
|
+
return Boolean(entry && entry.on === true)
|
|
41
|
+
}
|
|
34
42
|
const READ_ONLY_TOOL_NAMES = new Set([
|
|
35
43
|
'tower_status',
|
|
36
44
|
'tower_roster',
|
|
@@ -44,11 +52,11 @@ const BASE_REMINDER = [
|
|
|
44
52
|
'Tower mode is active for this session. You are the control tower: plan missions, spawn workers and reviewers, and integrate only through the Tower tools.',
|
|
45
53
|
'Mission tracking lives in tower_plan / tower_mission / tower_status, not todo_write or the Team task board.',
|
|
46
54
|
'Do not create or resume goals, do not spawn teammates, and never merge without a green tower_preflight.',
|
|
47
|
-
'
|
|
55
|
+
'Tower mode changes are explicit and recorded; the user can always use /tower on|off.',
|
|
48
56
|
].join(' ')
|
|
49
57
|
|
|
50
58
|
const AGENT_CONTROL_NOTE =
|
|
51
|
-
'
|
|
59
|
+
'You may also enter or leave Tower mode with tower_control when the user intent or task state clearly calls for it; always state a concrete reason.'
|
|
52
60
|
|
|
53
61
|
function readPluginVersion() {
|
|
54
62
|
if (pluginVersion !== null) return pluginVersion
|
|
@@ -80,7 +88,9 @@ function configSection(config, allowAgentControl) {
|
|
|
80
88
|
|
|
81
89
|
function persistModes() {
|
|
82
90
|
const config = runtimeConfig()
|
|
83
|
-
saveModes(
|
|
91
|
+
saveModes(modeState, config.mode.storePath || undefined, config.mode.maxEntries, {
|
|
92
|
+
timeoutMs: config.mode.lockTimeoutMs,
|
|
93
|
+
})
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/** Parse `/tower` arguments: bare and free text both mean "ensure on". */
|
|
@@ -150,7 +160,7 @@ function clearPlanModeForTower(ctx, agent) {
|
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
function enterMode(ctx, key, agent, by, reason) {
|
|
153
|
-
const previous =
|
|
163
|
+
const previous = modeState.get(key)
|
|
154
164
|
const wasOn = Boolean(previous && previous.on)
|
|
155
165
|
const plan = clearPlanModeForTower(ctx, agent)
|
|
156
166
|
if (plan.refused) {
|
|
@@ -171,12 +181,14 @@ function enterMode(ctx, key, agent, by, reason) {
|
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
const now = Date.now()
|
|
174
|
-
|
|
184
|
+
modeState.set(key, {
|
|
175
185
|
on: true,
|
|
176
186
|
pausedGoal,
|
|
177
187
|
lock: previous?.lock ?? null,
|
|
178
188
|
baseBranch: previous?.baseBranch ?? null,
|
|
179
189
|
planModeCleared: plan.cleared,
|
|
190
|
+
humanAckAt: by === 'user' ? previous?.humanAckAt ?? now : previous?.humanAckAt ?? null,
|
|
191
|
+
lastDisclosure: previous?.lastDisclosure ?? 'full',
|
|
180
192
|
lastChange: { by, action: 'on', reason: reason ?? null, at: now },
|
|
181
193
|
at: now,
|
|
182
194
|
})
|
|
@@ -185,14 +197,17 @@ function enterMode(ctx, key, agent, by, reason) {
|
|
|
185
197
|
}
|
|
186
198
|
|
|
187
199
|
async function exitMode(key, by, reason) {
|
|
188
|
-
const previous =
|
|
200
|
+
const previous = modeState.get(key)
|
|
189
201
|
if (!previous) return { wasOn: false, pausedGoal: null, goalNote: null }
|
|
190
|
-
|
|
202
|
+
const wasOn = previous.on === true
|
|
203
|
+
// Keep a timestamped off tombstone so a concurrent save from another
|
|
204
|
+
// session cannot resurrect this mode entry.
|
|
205
|
+
modeState.set(key, { on: false, at: Date.now(), humanAckAt: previous.humanAckAt ?? null, lastDisclosure: previous.lastDisclosure ?? 'exit' })
|
|
191
206
|
persistModes()
|
|
192
207
|
if (previous.lock) {
|
|
193
208
|
await releaseWorkspaceLock(previous.lock.commonDir, { token: previous.lock.token }).catch(() => {})
|
|
194
209
|
}
|
|
195
|
-
return { wasOn
|
|
210
|
+
return { wasOn, pausedGoal: previous.pausedGoal ?? null, goalNote: null, by, reason: reason ?? null }
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
function forwardInstruction(agent, message, attachments) {
|
|
@@ -237,7 +252,8 @@ export function apply(ctx, rawConfig) {
|
|
|
237
252
|
const config = configureRuntime(rawConfig)
|
|
238
253
|
const allowAgentControl = config.mode.allowAgentControl === true
|
|
239
254
|
const reminder = configSection(config, allowAgentControl)
|
|
240
|
-
|
|
255
|
+
modeState.replace(loadModes(config.mode.storePath || undefined))
|
|
256
|
+
if (typeof ctx.provide === 'function') ctx.provide('tower', modeState)
|
|
241
257
|
denyTools = new Set(config.mode.denyTools)
|
|
242
258
|
const hotReloadEnabled = config.dev?.hotReload !== false
|
|
243
259
|
const hmrBridge = hotReloadEnabled
|
|
@@ -255,8 +271,8 @@ export function apply(ctx, rawConfig) {
|
|
|
255
271
|
if (runtimeConfig().mode.workspaceLockScope === 'mutations' && READ_ONLY_TOOL_NAMES.has(toolName)) return
|
|
256
272
|
const key = agentKey(exec?.agent)
|
|
257
273
|
if (!key) return
|
|
258
|
-
const entry =
|
|
259
|
-
if (!entry) return
|
|
274
|
+
const entry = modeState.get(key)
|
|
275
|
+
if (!entry || entry.on !== true) return
|
|
260
276
|
const repo = await resolveRepo(repoRoot)
|
|
261
277
|
const existing = entry.lock
|
|
262
278
|
if (existing && existing.commonDir !== repo.commonDir) {
|
|
@@ -304,7 +320,26 @@ export function apply(ctx, rawConfig) {
|
|
|
304
320
|
},
|
|
305
321
|
basePreference(exec) {
|
|
306
322
|
const key = agentKey(exec?.agent)
|
|
307
|
-
return key ?
|
|
323
|
+
return key && modeOn(key) ? modeState.get(key)?.baseBranch ?? null : null
|
|
324
|
+
},
|
|
325
|
+
modeInfo(exec) {
|
|
326
|
+
const key = agentKey(exec?.agent)
|
|
327
|
+
if (!key) return null
|
|
328
|
+
const entry = modeState.get(key)
|
|
329
|
+
const history = [...modeState.values()]
|
|
330
|
+
.map((value) => value?.lastChange)
|
|
331
|
+
.filter((change) => change && typeof change.at === 'number')
|
|
332
|
+
.sort((a, b) => b.at - a.at)
|
|
333
|
+
.slice(0, 10)
|
|
334
|
+
return {
|
|
335
|
+
mode: modeOn(key) ? 'on' : entry ? 'off' : 'unknown',
|
|
336
|
+
allowAgentControl: runtimeConfig().mode.allowAgentControl === true,
|
|
337
|
+
lastChange: entry?.lastChange ?? null,
|
|
338
|
+
history,
|
|
339
|
+
workspaceLock: entry?.lock
|
|
340
|
+
? { enabled: runtimeConfig().mode.workspaceLock === true, commonDir: entry.lock.commonDir }
|
|
341
|
+
: null,
|
|
342
|
+
}
|
|
308
343
|
},
|
|
309
344
|
}
|
|
310
345
|
|
|
@@ -314,7 +349,7 @@ export function apply(ctx, rawConfig) {
|
|
|
314
349
|
// process still owns; a reloaded plugin restores the entries from modes.json.
|
|
315
350
|
ctx.effect(() => () => {
|
|
316
351
|
if (hmrBridge) hmrBridge.dispose?.()
|
|
317
|
-
for (const entry of
|
|
352
|
+
for (const entry of modeState.values()) {
|
|
318
353
|
if (entry?.lock) {
|
|
319
354
|
releaseWorkspaceLock(entry.lock.commonDir, { token: entry.lock.token }).catch(() => {})
|
|
320
355
|
}
|
|
@@ -324,7 +359,7 @@ export function apply(ctx, rawConfig) {
|
|
|
324
359
|
ctx.effect(() =>
|
|
325
360
|
ctx.tools.guard((execution) => {
|
|
326
361
|
const key = agentKey(execution.agent)
|
|
327
|
-
if (!key || !
|
|
362
|
+
if (!key || !modeOn(key)) return undefined
|
|
328
363
|
if (denyTools.has(execution.name)) {
|
|
329
364
|
return `tower mode: ${execution.name} is disabled while Tower mode is active; use the Tower tools or /tower off`
|
|
330
365
|
}
|
|
@@ -345,7 +380,7 @@ export function apply(ctx, rawConfig) {
|
|
|
345
380
|
typeof ctx.on === 'function'
|
|
346
381
|
? ctx.on('agent/pre-step', (payload, next) => {
|
|
347
382
|
const key = agentKey(payload?.agent)
|
|
348
|
-
if (key &&
|
|
383
|
+
if (key && modeOn(key)) {
|
|
349
384
|
// Once Tower is active, Tower wins regardless of entry policy: clear
|
|
350
385
|
// any plan state so the two policies cannot coexist.
|
|
351
386
|
const state = planModeState(ctx, payload.agent)
|
|
@@ -359,13 +394,13 @@ export function apply(ctx, rawConfig) {
|
|
|
359
394
|
ctx.systemPrompt.section({
|
|
360
395
|
name: 'tower:mode',
|
|
361
396
|
order: 15000,
|
|
362
|
-
text: (context) => (context &&
|
|
397
|
+
text: (context) => (context && modeOn(agentKey(context.agent)) ? reminder : ''),
|
|
363
398
|
})
|
|
364
399
|
|
|
365
400
|
// Workspace-lock heartbeat; only sessions holding a lock do I/O.
|
|
366
401
|
ctx.effect(() => {
|
|
367
402
|
const timer = setInterval(() => {
|
|
368
|
-
for (const entry of
|
|
403
|
+
for (const entry of modeState.values()) {
|
|
369
404
|
if (!entry?.lock) continue
|
|
370
405
|
touchWorkspaceLock(entry.lock.commonDir, { token: entry.lock.token }).catch(() => {})
|
|
371
406
|
}
|
|
@@ -402,12 +437,13 @@ export function apply(ctx, rawConfig) {
|
|
|
402
437
|
},
|
|
403
438
|
async execute(_args, exec) {
|
|
404
439
|
const key = agentKey(exec.agent)
|
|
405
|
-
const entry = key ?
|
|
440
|
+
const entry = key ? modeState.get(key) : undefined
|
|
406
441
|
return {
|
|
407
442
|
mode: entry && entry.on === true ? 'on' : key ? 'off' : 'unknown',
|
|
408
443
|
agentId: key,
|
|
409
444
|
allowAgentControl,
|
|
410
445
|
lastChange: entry?.lastChange ?? null,
|
|
446
|
+
lastDisclosure: entry?.lastDisclosure ?? null,
|
|
411
447
|
pausedGoal: entry?.pausedGoal ?? null,
|
|
412
448
|
deniedWhileActive: [...denyTools],
|
|
413
449
|
persistedAt: storePath(config.mode.storePath || undefined),
|
|
@@ -464,17 +500,25 @@ export function apply(ctx, rawConfig) {
|
|
|
464
500
|
const key = agentKey(exec.agent)
|
|
465
501
|
if (!key) return { ok: false, error: 'tower_control requires a live calling agent' }
|
|
466
502
|
if (typeof args.reason !== 'string' || args.reason.trim().length === 0) {
|
|
467
|
-
throw new
|
|
503
|
+
throw new TowerToolError('invalid-argument', 'tower_control requires a non-empty reason')
|
|
468
504
|
}
|
|
469
505
|
const reason = args.reason.trim()
|
|
506
|
+
if (args.action === 'enter' && !modeOn(key)) {
|
|
507
|
+
await assertPresetsAvailable(ctx, config)
|
|
508
|
+
}
|
|
470
509
|
const outcome =
|
|
471
510
|
args.action === 'enter'
|
|
472
|
-
?
|
|
511
|
+
? (() => {
|
|
512
|
+
if (!modeOn(key) && runtimeConfig().mode.requireHumanAckOnFirstEntry === true && !modeState.get(key)?.humanAckAt) {
|
|
513
|
+
throw new TowerToolError('not-authorized', 'this profile requires a human acknowledgement; run /tower ack first')
|
|
514
|
+
}
|
|
515
|
+
return enterMode(ctx, key, exec.agent, 'agent', reason)
|
|
516
|
+
})()
|
|
473
517
|
: await exitMode(key, 'agent', reason)
|
|
474
518
|
return {
|
|
475
519
|
ok: true,
|
|
476
520
|
action: args.action,
|
|
477
|
-
mode:
|
|
521
|
+
mode: modeOn(key) ? 'on' : 'off',
|
|
478
522
|
changed: args.action === 'enter' ? !outcome.wasOn : outcome.wasOn,
|
|
479
523
|
changedBy: 'agent',
|
|
480
524
|
reason,
|
|
@@ -490,14 +534,21 @@ export function apply(ctx, rawConfig) {
|
|
|
490
534
|
commandCtx.commands.register({
|
|
491
535
|
name: 'tower',
|
|
492
536
|
description: 'Enter or leave Tower mode, optionally carrying a user instruction; /tower reload reloads via HMR; /tower base <branch> sets the next init base',
|
|
493
|
-
input: { hint: '[on|off|reload|base <branch>] [message]', attachments: true },
|
|
537
|
+
input: { hint: '[on|off|reload|ack|base <branch>] [message]', attachments: true },
|
|
494
538
|
handler: async ({ agent, rawInput, attachments }) => {
|
|
495
539
|
const key = agentKey(agent)
|
|
496
540
|
if (!key) return { kind: 'error', text: 'Tower mode requires a live agent.' }
|
|
497
|
-
const
|
|
541
|
+
const raw = String(rawInput ?? '').trim()
|
|
542
|
+
if (/^ack$/i.test(raw)) {
|
|
543
|
+
const previous = modeState.get(key) ?? { on: false }
|
|
544
|
+
modeState.set(key, { ...previous, on: previous.on === true, humanAckAt: Date.now(), at: Date.now() })
|
|
545
|
+
persistModes()
|
|
546
|
+
return { kind: 'success', text: 'Human acknowledgement recorded; the model may now make the first Tower-mode entry in this session.' }
|
|
547
|
+
}
|
|
548
|
+
const baseMatch = raw.match(/^base\s+(\S+)\s*$/i)
|
|
498
549
|
if (baseMatch) {
|
|
499
|
-
const entry =
|
|
500
|
-
if (!entry) return { kind: 'error', text: 'Run /tower on first, then /tower base <branch>.' }
|
|
550
|
+
const entry = modeState.get(key)
|
|
551
|
+
if (!entry || entry.on !== true) return { kind: 'error', text: 'Run /tower on first, then /tower base <branch>.' }
|
|
501
552
|
entry.baseBranch = baseMatch[1]
|
|
502
553
|
persistModes()
|
|
503
554
|
return { kind: 'success', text: `Tower base preference set to "${entry.baseBranch}"; tower_init without an explicit base will use it.` }
|
|
@@ -517,6 +568,7 @@ export function apply(ctx, rawConfig) {
|
|
|
517
568
|
}
|
|
518
569
|
}
|
|
519
570
|
const { active, message } = parsed
|
|
571
|
+
if (active) await assertPresetsAvailable(ctx, config)
|
|
520
572
|
const outcome = active
|
|
521
573
|
? enterMode(ctx, key, agent, 'user', message || null)
|
|
522
574
|
: await exitMode(key, 'user', message || null)
|
package/lib/briefing.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Mission briefings: pure text builders for worker/reviewer prompts.
|
|
2
|
+
|
|
3
|
+
function clip(text, max = 2000) {
|
|
4
|
+
const value = String(text ?? '')
|
|
5
|
+
return value.length > max ? `${value.slice(0, max)}\n… truncated` : value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function workerBriefing({
|
|
9
|
+
repoRoot,
|
|
10
|
+
base,
|
|
11
|
+
mission,
|
|
12
|
+
deps,
|
|
13
|
+
worktree,
|
|
14
|
+
instructions,
|
|
15
|
+
reviews = [],
|
|
16
|
+
findings = [],
|
|
17
|
+
completionReport = null,
|
|
18
|
+
}) {
|
|
19
|
+
const completion = /** @type {import('../types/index.js').CompletionReport | null} */ (completionReport)
|
|
20
|
+
const lines = []
|
|
21
|
+
lines.push(`# Tower mission ${mission.id} — ${mission.title}`)
|
|
22
|
+
lines.push('')
|
|
23
|
+
lines.push(`You are the tower worker for mission ${mission.id} in repository ${repoRoot}.`)
|
|
24
|
+
lines.push('')
|
|
25
|
+
lines.push(`Work ONLY in this git worktree: ${worktree.path}`)
|
|
26
|
+
lines.push(`Branch: ${worktree.branch} (based on ${base} @ ${String(worktree.baseCommit).slice(0, 12)})`)
|
|
27
|
+
lines.push('')
|
|
28
|
+
lines.push(`Kind: ${mission.kind}`)
|
|
29
|
+
lines.push(
|
|
30
|
+
`Scope (only files you may touch): ${mission.scope.length > 0 ? mission.scope.join(', ') : '(none — survey is read-only)'}`,
|
|
31
|
+
)
|
|
32
|
+
if (mission.tasks.length > 0) {
|
|
33
|
+
lines.push('Tasks:')
|
|
34
|
+
for (const task of mission.tasks) lines.push(`- [${task.status}] ${task.text}`)
|
|
35
|
+
}
|
|
36
|
+
if (deps.length > 0) {
|
|
37
|
+
lines.push('Dependencies:')
|
|
38
|
+
for (const dep of deps) lines.push(`- ${dep.id} ${dep.title ?? ''} (${dep.status})`)
|
|
39
|
+
}
|
|
40
|
+
if (mission.context) {
|
|
41
|
+
lines.push('Context (verbatim from the user):')
|
|
42
|
+
lines.push(mission.context)
|
|
43
|
+
}
|
|
44
|
+
if ((mission.references ?? []).length > 0) {
|
|
45
|
+
lines.push('References (read-only pointers; read them before deciding):')
|
|
46
|
+
for (const reference of mission.references) lines.push(`- ${reference}`)
|
|
47
|
+
}
|
|
48
|
+
if (mission.reviewPolicy === 'none') {
|
|
49
|
+
lines.push(`Review policy: none (user exemption: ${mission.reviewExemptionReason ?? 'missing reason'})`)
|
|
50
|
+
}
|
|
51
|
+
if (completion) {
|
|
52
|
+
lines.push('')
|
|
53
|
+
lines.push(`Previous completion report (by ${completion.by ?? 'worker'}):`)
|
|
54
|
+
lines.push(clip(completion.report, 1500))
|
|
55
|
+
if ((completion.checks ?? []).length > 0) {
|
|
56
|
+
lines.push('Previous checks:')
|
|
57
|
+
for (const check of completion.checks ?? []) lines.push(`- ${check.name}${check.exitCode !== undefined ? ` (exit ${check.exitCode})` : ''}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (reviews.length > 0) {
|
|
61
|
+
lines.push('')
|
|
62
|
+
lines.push('Previous review rounds for this branch (newest first) — address every finding:')
|
|
63
|
+
for (const review of reviews) {
|
|
64
|
+
lines.push(`- R${review.round} by ${review.reviewer}: ${review.status} / ${review.mergeVerdict}`)
|
|
65
|
+
if (review.findings) lines.push(` findings: ${clip(review.findings, 1200).replaceAll('\n', '\n ')}`)
|
|
66
|
+
if (review.decision) lines.push(` decision: ${clip(review.decision, 600)}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (findings.length > 0) {
|
|
70
|
+
lines.push('')
|
|
71
|
+
lines.push('Findings related to this mission:')
|
|
72
|
+
for (const finding of findings) lines.push(`- ${finding.id} [${finding.status}] ${finding.severity ?? 'severity?'} ${finding.title}`)
|
|
73
|
+
}
|
|
74
|
+
lines.push('')
|
|
75
|
+
lines.push('Rules:')
|
|
76
|
+
lines.push(`- Pass workdir="${worktree.path}" on every shell command; never edit the main checkout.`)
|
|
77
|
+
lines.push('- Stay inside the declared scope; out-of-scope changes are rejected later.')
|
|
78
|
+
lines.push('- Commit your work on the mission branch. Do not merge, push, or spawn other agents.')
|
|
79
|
+
lines.push(
|
|
80
|
+
`- Update progress: tower_mission({repoRoot:"${repoRoot}",id:"${mission.id}",task_done:"..."}) or blocker:"...".`,
|
|
81
|
+
)
|
|
82
|
+
lines.push(
|
|
83
|
+
`- When the work is finished and every task is closed, call tower_complete({repoRoot:"${repoRoot}",missionId:"${mission.id}",report:"<full handoff>",checks:[{name,command,exitCode,evidence}]}). Unread inbox messages block completion, so read them with tower_inbox first.`,
|
|
84
|
+
)
|
|
85
|
+
lines.push('- Your final message is the full handoff: files changed, how you verified, what remains.')
|
|
86
|
+
if (instructions) {
|
|
87
|
+
lines.push('')
|
|
88
|
+
lines.push('Extra tower instructions:')
|
|
89
|
+
lines.push(instructions)
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function reviewerBriefing({
|
|
95
|
+
repoRoot,
|
|
96
|
+
base,
|
|
97
|
+
mission,
|
|
98
|
+
deps,
|
|
99
|
+
worktree,
|
|
100
|
+
instructions,
|
|
101
|
+
reviews = [],
|
|
102
|
+
findings = [],
|
|
103
|
+
completionReport = null,
|
|
104
|
+
}) {
|
|
105
|
+
const completion = /** @type {import('../types/index.js').CompletionReport | null} */ (completionReport)
|
|
106
|
+
const lines = []
|
|
107
|
+
lines.push(`# Tower review — mission ${mission.id} — ${mission.title}`)
|
|
108
|
+
lines.push('')
|
|
109
|
+
lines.push(`You are the independent reviewer for mission ${mission.id} in repository ${repoRoot}.`)
|
|
110
|
+
lines.push('')
|
|
111
|
+
lines.push(`Review branch: ${worktree.branch}`)
|
|
112
|
+
lines.push(`Worktree: ${worktree.path}`)
|
|
113
|
+
lines.push(`Reviewed base: ${base} @ ${String(worktree.baseCommit).slice(0, 12)}`)
|
|
114
|
+
lines.push('')
|
|
115
|
+
lines.push(`Kind: ${mission.kind}`)
|
|
116
|
+
if (mission.tasks.length > 0) {
|
|
117
|
+
lines.push('The worker was asked to complete:')
|
|
118
|
+
for (const task of mission.tasks) lines.push(`- [${task.status}] ${task.text}`)
|
|
119
|
+
}
|
|
120
|
+
if (deps.length > 0) {
|
|
121
|
+
lines.push('Dependencies:')
|
|
122
|
+
for (const dep of deps) lines.push(`- ${dep.id} ${dep.title ?? ''} (${dep.status})`)
|
|
123
|
+
}
|
|
124
|
+
if (mission.context) {
|
|
125
|
+
lines.push('Context (verbatim from the user):')
|
|
126
|
+
lines.push(mission.context)
|
|
127
|
+
}
|
|
128
|
+
if ((mission.references ?? []).length > 0) {
|
|
129
|
+
lines.push('References (read-only pointers):')
|
|
130
|
+
for (const reference of mission.references) lines.push(`- ${reference}`)
|
|
131
|
+
}
|
|
132
|
+
if (completion) {
|
|
133
|
+
lines.push('')
|
|
134
|
+
lines.push(`Completion report (by ${completion.by ?? 'worker'}):`)
|
|
135
|
+
lines.push(clip(completion.report, 2000))
|
|
136
|
+
if ((completion.checks ?? []).length > 0) {
|
|
137
|
+
lines.push('Completion checks the worker ran:')
|
|
138
|
+
for (const check of completion.checks ?? []) {
|
|
139
|
+
lines.push(
|
|
140
|
+
`- ${check.name}${check.exitCode !== undefined ? ` (exit ${check.exitCode})` : ''}${check.evidence ? ` — ${clip(check.evidence, 400)}` : ''}`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (reviews.length > 0) {
|
|
146
|
+
lines.push('')
|
|
147
|
+
lines.push('Prior review rounds for this branch (newest first):')
|
|
148
|
+
for (const review of reviews) {
|
|
149
|
+
lines.push(`- R${review.round} by ${review.reviewer}: ${review.status} / ${review.mergeVerdict}`)
|
|
150
|
+
if (review.findings) lines.push(` findings: ${clip(review.findings, 1200).replaceAll('\n', '\n ')}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (findings.length > 0) {
|
|
154
|
+
lines.push('')
|
|
155
|
+
lines.push('Related findings:')
|
|
156
|
+
for (const finding of findings) lines.push(`- ${finding.id} [${finding.status}] ${finding.severity ?? 'severity?'} ${finding.title}`)
|
|
157
|
+
}
|
|
158
|
+
lines.push('')
|
|
159
|
+
lines.push('Rules:')
|
|
160
|
+
lines.push('- Read-only: do not modify files, commit, or merge.')
|
|
161
|
+
lines.push('- Inspect the diff from the reviewed base to the branch tip and map every task to the diff.')
|
|
162
|
+
lines.push('- Check scope, correctness, tests, and security. Report severity and concrete evidence.')
|
|
163
|
+
lines.push(
|
|
164
|
+
`- Submit the formal verdict with tower_review({repoRoot:"${repoRoot}",missionId:"${mission.id}",status:"clean",merge:"merge",findings:"none",checks:["..."],decision:"..."}). Use status "p1-Nitems"/"p2-Nitems" and merge "fix-then-merge"/"hold" for problems; a non-clean verdict returns the mission to active.`,
|
|
165
|
+
)
|
|
166
|
+
lines.push(
|
|
167
|
+
`- File out-of-scope issues with tower_finding({repoRoot:"${repoRoot}",type:"bug",title:"...",summary:"...",details:"...",suggested_fix:"..."}) instead of fixing them.`,
|
|
168
|
+
)
|
|
169
|
+
lines.push('- Your final message is the review report; the tool call above is the formal verdict.')
|
|
170
|
+
if (instructions) {
|
|
171
|
+
lines.push('')
|
|
172
|
+
lines.push('Extra tower instructions:')
|
|
173
|
+
lines.push(instructions)
|
|
174
|
+
}
|
|
175
|
+
return lines.join('\n')
|
|
176
|
+
}
|