dsh-tower 2.6.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 +82 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +1 -1
- package/README.en.md +2 -1
- package/README.md +6 -1
- 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 +97 -61
- package/app/reviewCheckout.js +133 -0
- package/app/sandbox.js +121 -0
- package/app/sharedCheckout.js +90 -0
- package/app/spawn-prechecks.js +25 -24
- package/app/spawn-support.js +59 -8
- package/app/spawn.js +3 -0
- package/app/teardown.js +119 -2
- package/app/tower.js +29 -1
- package/cordis.patch.yml +50 -0
- package/docs/adr/0006-mission-shapes.md +306 -0
- package/docs/architecture/overview.md +6 -4
- package/docs/guide/install-upgrade-migration.md +26 -0
- package/docs/operations/runbook.md +1 -1
- package/docs/reference/blockers.md +8 -4
- package/docs/reference/component-surface.md +7 -2
- package/docs/reference/config.generated.md +6 -4
- package/docs/reference/config.md +1 -2
- package/docs/reference/doctor.md +7 -7
- package/docs/reference/operations.md +1 -1
- package/docs/reference/shapes.md +123 -0
- package/docs/reference/surface-budget.md +33 -1
- package/docs/reference/testing.md +4 -2
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +2 -2
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/config.md +1 -2
- package/docs/zh/reference/testing.md +3 -2
- package/domain/batch.js +75 -105
- package/domain/collab/batch.js +30 -10
- package/domain/collab/spawn.js +53 -16
- package/domain/config/schema.js +47 -17
- package/domain/contracts/outputs.js +0 -1
- package/domain/contracts/tool-inputs/missions.js +9 -1
- package/domain/evidence.js +283 -0
- package/domain/gate.js +11 -31
- 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/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/doctor.js +2 -1
- package/host/preset-survey-sandbox.js +19 -0
- package/host/preset-survey.js +18 -0
- package/host/presets.js +8 -3
- package/host/tool-registry.js +14 -12
- 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 +13 -2
- package/ports/state.js +8 -1
- package/scripts/architecture-waivers.json +3 -3
- package/scripts/check-docs.mjs +1 -1
- package/scripts/check-pack.mjs +2 -0
- package/scripts/component-surface.json +68 -20
- 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 +21 -0
- package/scripts/shape-audit.mjs +361 -0
- package/scripts/surface-baseline.json +68 -4
- 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/spawn.js +47 -49
- package/tools/test.js +2 -1
- package/types/index.d.ts +66 -5
package/domain/collab/batch.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Pure batch lifecycle decisions and status projection.
|
|
2
2
|
//
|
|
3
|
-
// `domain/batch.js` owns the admission/close evidence
|
|
4
|
-
// owns the id assignment, close/abandon
|
|
5
|
-
// that app/batch.js used to keep inline.
|
|
6
|
-
// layer.
|
|
3
|
+
// `domain/batch.js` owns the lifecycle vocabulary, admission/close evidence
|
|
4
|
+
// validation and normalization; this module owns the id assignment, close/abandon
|
|
5
|
+
// record transitions, and status payload that app/batch.js used to keep inline.
|
|
6
|
+
// Receipt file reads stay in the tool layer. Every status move goes through the
|
|
7
|
+
// lifecycle module's `canTransitionBatch`, so this file never names a status
|
|
8
|
+
// literal of its own (#149 W3).
|
|
7
9
|
// @ts-check
|
|
8
10
|
import { TowerToolError } from '../errors.js'
|
|
9
|
-
import { summarize } from '../batch.js'
|
|
11
|
+
import { BATCH_STATUS, canTransitionBatch, isBatchTerminal, normalizeBatchStatus, summarize } from '../batch.js'
|
|
10
12
|
|
|
11
13
|
/** @typedef {import('../../types/index.js').TowerState} TowerState */
|
|
12
14
|
|
|
@@ -19,6 +21,21 @@ function fail(message, code = 'batch-invalid') {
|
|
|
19
21
|
throw new TowerToolError(code, message)
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Move a draft batch to `to`, refusing a transition the lifecycle does not
|
|
26
|
+
* allow. A no-op (already at the target) is handled by the callers, not here.
|
|
27
|
+
* @param {any} batch
|
|
28
|
+
* @param {string} to
|
|
29
|
+
* @param {string} batchId
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
function assertBatchMove(batch, to, batchId) {
|
|
33
|
+
const from = normalizeBatchStatus(batch.status)
|
|
34
|
+
if (!canTransitionBatch(from, to)) {
|
|
35
|
+
fail(`batch ${batchId} is ${from}; it cannot move to ${to}${isBatchTerminal(from) ? ' (a batch is closed or abandoned exactly once)' : ''}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Next free `B<n>` id.
|
|
24
41
|
* @param {any[]} batches
|
|
@@ -43,7 +60,7 @@ export function nextBatchId(batches) {
|
|
|
43
60
|
export function createBatchRecord(id, normalized, actor, now) {
|
|
44
61
|
return {
|
|
45
62
|
id,
|
|
46
|
-
status:
|
|
63
|
+
status: BATCH_STATUS.OPEN,
|
|
47
64
|
...normalized,
|
|
48
65
|
createdBy: actor,
|
|
49
66
|
createdAt: now,
|
|
@@ -59,7 +76,8 @@ export function createBatchRecord(id, normalized, actor, now) {
|
|
|
59
76
|
* @returns {any}
|
|
60
77
|
*/
|
|
61
78
|
export function closeBatchRecord(batch, options, actor, now) {
|
|
62
|
-
batch.
|
|
79
|
+
assertBatchMove(batch, BATCH_STATUS.CLOSED, String(batch.id ?? ''))
|
|
80
|
+
batch.status = BATCH_STATUS.CLOSED
|
|
63
81
|
batch.closedAt = now
|
|
64
82
|
batch.closedBy = actor
|
|
65
83
|
batch.close = {
|
|
@@ -78,8 +96,9 @@ export function closeBatchRecord(batch, options, actor, now) {
|
|
|
78
96
|
* @returns {{ alreadyAbandoned: boolean }}
|
|
79
97
|
*/
|
|
80
98
|
export function abandonBatchCheck(batch, batchId) {
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
const status = normalizeBatchStatus(batch.status)
|
|
100
|
+
if (status === BATCH_STATUS.CLOSED) fail(`batch ${batchId} is already closed`)
|
|
101
|
+
return { alreadyAbandoned: status === BATCH_STATUS.ABANDONED }
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/**
|
|
@@ -88,7 +107,8 @@ export function abandonBatchCheck(batch, batchId) {
|
|
|
88
107
|
* @returns {any}
|
|
89
108
|
*/
|
|
90
109
|
export function applyBatchAbandon({ batch, actor, reason, now }) {
|
|
91
|
-
batch.
|
|
110
|
+
assertBatchMove(batch, BATCH_STATUS.ABANDONED, String(batch.id ?? ''))
|
|
111
|
+
batch.status = BATCH_STATUS.ABANDONED
|
|
92
112
|
batch.abandonedAt = now
|
|
93
113
|
batch.abandonedBy = actor
|
|
94
114
|
batch.abandonReason = typeof reason === 'string' && reason.trim() ? reason.trim() : null
|
package/domain/collab/spawn.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// @ts-check
|
|
8
8
|
import { TowerToolError } from '../errors.js'
|
|
9
9
|
import { ROSTER_INACTIVE, countLiveWorkers, workerEntries } from '../roster.js'
|
|
10
|
+
import { isReviewerRole, isWorkerRole } from '../roles.js'
|
|
11
|
+
import { shapeFor } from '../shapes/index.js'
|
|
10
12
|
import { clearStale } from './roster.js'
|
|
11
13
|
|
|
12
14
|
/** @typedef {import('../../types/index.js').Mission} Mission */
|
|
@@ -14,19 +16,14 @@ import { clearStale } from './roster.js'
|
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* @param {string} message
|
|
19
|
+
* @param {string} [code]
|
|
17
20
|
* @returns {never}
|
|
18
21
|
*/
|
|
19
|
-
function fail(message) {
|
|
20
|
-
throw new TowerToolError(
|
|
22
|
+
function fail(message, code = 'invalid-argument') {
|
|
23
|
+
throw new TowerToolError(code, message)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
* @param {unknown} role
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
export function isWorkerRole(role) {
|
|
28
|
-
return role === 'worker'
|
|
29
|
-
}
|
|
26
|
+
export { isWorkerRole } from '../roles.js'
|
|
30
27
|
|
|
31
28
|
/**
|
|
32
29
|
* The configured live-worker ceiling, defaulting to four.
|
|
@@ -67,6 +64,26 @@ export function applyInterrupt(entry, input, now) {
|
|
|
67
64
|
return entry
|
|
68
65
|
}
|
|
69
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A reviewer's evidence is the commit recorded here, never a live branch tip
|
|
69
|
+
* read later (#149 W3). The checkout is the disposable detached worktree the
|
|
70
|
+
* reviewer reads in; it is cleared when the verdict is recorded.
|
|
71
|
+
* @param {any} input
|
|
72
|
+
* @param {{ requireReviewCommit?: boolean }} [options]
|
|
73
|
+
* @returns {{ reviewCommit: string | null, reviewCheckout: string | null }}
|
|
74
|
+
*/
|
|
75
|
+
function reviewEvidence(input, options = {}) {
|
|
76
|
+
const reviewCommit = typeof input.reviewCommit === 'string' && input.reviewCommit.length > 0 ? input.reviewCommit : null
|
|
77
|
+
const reviewCheckout = typeof input.reviewCheckout === 'string' && input.reviewCheckout.length > 0 ? input.reviewCheckout : null
|
|
78
|
+
if (options.requireReviewCommit && isReviewerRole(input.role) && !reviewCommit) {
|
|
79
|
+
throw new TowerToolError(
|
|
80
|
+
'invalid-argument',
|
|
81
|
+
`reviewer ${input.name} has no recorded review commit; spawn reviewers through tower_spawn (a review verdict is stamped with a commit, not a live branch tip)`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return { reviewCommit, reviewCheckout }
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
/**
|
|
71
88
|
* A reserved roster row: the name is claimed before activation so a failed
|
|
72
89
|
* spawn remains recoverable.
|
|
@@ -75,6 +92,7 @@ export function applyInterrupt(entry, input, now) {
|
|
|
75
92
|
* @returns {RosterEntry}
|
|
76
93
|
*/
|
|
77
94
|
export function reservedRosterEntry(input, now) {
|
|
95
|
+
const evidence = reviewEvidence(input, { requireReviewCommit: true })
|
|
78
96
|
return {
|
|
79
97
|
name: input.name,
|
|
80
98
|
role: input.role,
|
|
@@ -84,6 +102,8 @@ export function reservedRosterEntry(input, now) {
|
|
|
84
102
|
missionId: input.missionId,
|
|
85
103
|
cwd: input.cwd ?? null,
|
|
86
104
|
branch: input.branch ?? null,
|
|
105
|
+
...(evidence.reviewCommit ? { reviewCommit: evidence.reviewCommit } : {}),
|
|
106
|
+
...(evidence.reviewCheckout ? { reviewCheckout: evidence.reviewCheckout } : {}),
|
|
87
107
|
spawnTip: null,
|
|
88
108
|
state: 'reserved',
|
|
89
109
|
reservedAt: now,
|
|
@@ -104,10 +124,15 @@ export function reserveRosterEntry({ roster, input, now }) {
|
|
|
104
124
|
/**
|
|
105
125
|
* Validate and apply a spawn record. Mutates the roster (retiring prior worker
|
|
106
126
|
* rows and inserting/reusing the entry) and the mission owner/status/gen.
|
|
107
|
-
*
|
|
127
|
+
*
|
|
128
|
+
* `missions` is the state's mission list, not just the spawned mission: the live
|
|
129
|
+
* worker count resolves every roster row's kind through the shape registry, so a
|
|
130
|
+
* survey row must not be counted as a build worker (#148). Callers that omit it
|
|
131
|
+
* (direct domain tests) keep the historical roster-only treatment.
|
|
132
|
+
* @param {{ roster: RosterEntry[], missions?: Mission[], mission: Mission, input: any, spawnTip: string | null, maxLiveWorkers: unknown, now: number }} params
|
|
108
133
|
* @returns {RosterEntry}
|
|
109
134
|
*/
|
|
110
|
-
export function recordSpawnEntry({ roster, mission, input, spawnTip, maxLiveWorkers, now }) {
|
|
135
|
+
export function recordSpawnEntry({ roster, missions = [], mission, input, spawnTip, maxLiveWorkers, now }) {
|
|
111
136
|
const reserved = roster.find((entry) => entry.name === input.name && entry.state === 'reserved' && !entry.agentId)
|
|
112
137
|
if (!reserved && roster.some((entry) => entry.name === input.name)) {
|
|
113
138
|
fail(`roster name already used: ${input.name}`)
|
|
@@ -115,7 +140,9 @@ export function recordSpawnEntry({ roster, mission, input, spawnTip, maxLiveWork
|
|
|
115
140
|
if (roster.some((entry) => entry.agentId && entry.agentId === input.agentId)) {
|
|
116
141
|
fail(`agent ${input.agentId} is already in the roster`)
|
|
117
142
|
}
|
|
118
|
-
/** @type {
|
|
143
|
+
/** @type {{ reviewCommit?: string | null, reviewCheckout?: string | null }} */
|
|
144
|
+
const evidence = reviewEvidence(input, { requireReviewCommit: true })
|
|
145
|
+
/** @type {RosterEntry & { generation?: number }} */
|
|
119
146
|
const entry = {
|
|
120
147
|
name: input.name,
|
|
121
148
|
role: input.role,
|
|
@@ -125,12 +152,14 @@ export function recordSpawnEntry({ roster, mission, input, spawnTip, maxLiveWork
|
|
|
125
152
|
missionId: input.missionId,
|
|
126
153
|
cwd: input.cwd ?? null,
|
|
127
154
|
branch: input.branch ?? null,
|
|
155
|
+
...(evidence.reviewCommit ? { reviewCommit: evidence.reviewCommit } : {}),
|
|
156
|
+
...(evidence.reviewCheckout ? { reviewCheckout: evidence.reviewCheckout } : {}),
|
|
128
157
|
spawnTip,
|
|
129
158
|
state: 'active',
|
|
130
159
|
spawnedAt: now,
|
|
131
160
|
}
|
|
132
161
|
if (isWorkerRole(input.role)) {
|
|
133
|
-
const snapshot = /** @type {any} */ ({ roster })
|
|
162
|
+
const snapshot = /** @type {any} */ ({ roster, missions })
|
|
134
163
|
const previous = workerEntries(snapshot, mission.id)
|
|
135
164
|
const live = previous.find((row) => !ROSTER_INACTIVE.has(row.state) && row.state !== 'reserved')
|
|
136
165
|
if (live && !input.takeover) {
|
|
@@ -148,9 +177,17 @@ export function recordSpawnEntry({ roster, mission, input, spawnTip, maxLiveWork
|
|
|
148
177
|
old.outcomeAt = now
|
|
149
178
|
clearStale(old)
|
|
150
179
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
180
|
+
// A shape that does not count toward the live-worker budget (the read-only
|
|
181
|
+
// survey) must pass even when every build slot is busy; otherwise the
|
|
182
|
+
// precheck and the record-level check disagree (#148).
|
|
183
|
+
if (shapeFor(mission.kind).spawn.countsTowardWorkerBudget !== false) {
|
|
184
|
+
const max = liveWorkerLimit(maxLiveWorkers)
|
|
185
|
+
if (countLiveWorkers(snapshot) >= max) {
|
|
186
|
+
fail(
|
|
187
|
+
`live worker limit reached (${max}); stop a worker or raise spawn.maxLiveWorkers`,
|
|
188
|
+
'worker-limit',
|
|
189
|
+
)
|
|
190
|
+
}
|
|
154
191
|
}
|
|
155
192
|
const generation = (mission.ownerGeneration ?? 0) + 1
|
|
156
193
|
entry.generation = generation
|
package/domain/config/schema.js
CHANGED
|
@@ -37,7 +37,7 @@ const LEGACY_KEYS = ['section', 'allowAgentControl']
|
|
|
37
37
|
// #121: the `tools` group is gone. Every capability that used to be a
|
|
38
38
|
// `tools.expose*` boolean is now a component row in `cordis.patch.yml`, and a
|
|
39
39
|
// component row is the switch. What remains here is genuine tuning policy.
|
|
40
|
-
const GROUP_KEYS = ['init', 'mode', 'review', 'merge', 'git', 'worktrees', 'spawn', 'send', 'tests', 'gates', 'docs', 'archive', 'leases', 'dev']
|
|
40
|
+
const GROUP_KEYS = ['init', 'mode', 'review', 'merge', 'git', 'worktrees', 'spawn', 'send', 'survey', 'tests', 'gates', 'docs', 'archive', 'leases', 'dev']
|
|
41
41
|
|
|
42
42
|
const SECTION_KEYS = {
|
|
43
43
|
init: ['autoGitInit', 'snapshotDirty', 'autoExclude'],
|
|
@@ -69,40 +69,54 @@ const SECTION_KEYS = {
|
|
|
69
69
|
'provider',
|
|
70
70
|
'workerModel',
|
|
71
71
|
'reviewerModel',
|
|
72
|
+
'surveyModel',
|
|
72
73
|
'maxLiveWorkers',
|
|
73
74
|
'staleMs',
|
|
74
75
|
'maxWaitMs',
|
|
75
76
|
'requireLease',
|
|
76
77
|
],
|
|
77
78
|
send: ['maxPerMinute', 'interruptDefault'],
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// #148 W2: where a survey reads. This is tuning, not a component: the
|
|
80
|
+
// tower-survey row is the switch; the placement decides whether write/shell
|
|
81
|
+
// side effects are rejected (shared) or absorbed by a disposable checkout.
|
|
82
|
+
survey: ['sandbox'],
|
|
83
|
+
tests: ['admission', 'timeoutMs', 'maxBytes', 'allowPackageDiscovery'],
|
|
84
|
+
gates: ['requireChecks', 'requireTests', 'allowReviewExemption', 'requireFreshSurvey', 'maxReportBytes'],
|
|
80
85
|
docs: ['boardLocation'],
|
|
81
86
|
archive: ['afterDays', 'maxMissions'],
|
|
82
87
|
leases: ['ttlMs'],
|
|
83
88
|
dev: ['reloadDelayMs', 'reloadRequiredIds'],
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
// #133/#135:
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
+
// #133/#135/#149: a removed key keeps failing as an unknown key — with an
|
|
92
|
+
// upgrade note naming what replaced it. `docs.board` and `archive.enabled`
|
|
93
|
+
// were component-shaped switches hiding in the config form, so their note
|
|
94
|
+
// names the component row. `tests.autoChecks` existed only to copy a discovery
|
|
95
|
+
// projection into the completion report; its replacement is the `testRuns`
|
|
96
|
+
// receipt (whose concrete record path belongs to domain/evidence.js), so the
|
|
97
|
+
// note names the receipt and the one reader instead of a row.
|
|
98
|
+
/** @type {Record<string, { component?: string, note?: string }>} */
|
|
91
99
|
const REMOVED_KEYS = {
|
|
92
|
-
'docs.board': 'tower-board',
|
|
93
|
-
'archive.enabled': 'tower-archive',
|
|
100
|
+
'docs.board': { component: 'tower-board' },
|
|
101
|
+
'archive.enabled': { component: 'tower-archive' },
|
|
102
|
+
'tests.autoChecks': {
|
|
103
|
+
note: 'tests.autoChecks was removed: test evidence is the testRuns receipt read by domain/evidence.js (see docs/adr/0006-mission-shapes.md)',
|
|
104
|
+
},
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
/**
|
|
97
|
-
* Upgrade note for a removed
|
|
108
|
+
* Upgrade note for a removed key, appended to the unknown-key error.
|
|
98
109
|
* @param {string} group
|
|
99
110
|
* @param {string} key
|
|
100
111
|
* @returns {string}
|
|
101
112
|
*/
|
|
102
113
|
function removedKeyNote(group, key) {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
105
|
-
|
|
114
|
+
const entry = REMOVED_KEYS[`${group}.${key}`]
|
|
115
|
+
if (!entry) return ''
|
|
116
|
+
if (entry.component) {
|
|
117
|
+
return ` — ${group}.${key} was removed: the ${entry.component} component row in cordis.patch.yml is the switch (see docs/adr/0005-component-surface.md)`
|
|
118
|
+
}
|
|
119
|
+
return ` — ${entry.note}`
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
const MODEL_KEYS = ['provider', 'model', 'reasoningEffort', 'maxTokens']
|
|
@@ -216,12 +230,13 @@ export const Config = Schema.object({
|
|
|
216
230
|
.default({})
|
|
217
231
|
.description('Worktree placement.'),
|
|
218
232
|
spawn: Schema.object({
|
|
219
|
-
workerPreset: Schema.string().default('tower-worker').description('Agent preset for
|
|
233
|
+
workerPreset: Schema.string().default('tower-worker').description('Agent preset for build workers.'),
|
|
220
234
|
reviewerPreset: Schema.string().default('tower-reviewer').description('Agent preset for reviewers.'),
|
|
221
235
|
maxDepth: Schema.natural().min(1).default(1).description('Subagent depth requested when spawning tower agents.'),
|
|
222
236
|
provider: Schema.string().description('Subagent provider id; empty picks spawn, then the first registered provider.'),
|
|
223
237
|
workerModel: ModelSchema().description('Model route for workers; omitted inherits the parent session.'),
|
|
224
238
|
reviewerModel: ModelSchema().description('Model route for reviewers; omitted inherits the parent session (recommended).'),
|
|
239
|
+
surveyModel: ModelSchema().description('Model route for survey workers; omitted inherits the parent session.'),
|
|
225
240
|
maxLiveWorkers: Schema.natural()
|
|
226
241
|
.min(1)
|
|
227
242
|
.max(64)
|
|
@@ -251,6 +266,15 @@ export const Config = Schema.object({
|
|
|
251
266
|
})
|
|
252
267
|
.default({})
|
|
253
268
|
.description('Message delivery policy.'),
|
|
269
|
+
survey: Schema.object({
|
|
270
|
+
sandbox: Schema.union(['none', 'detached'])
|
|
271
|
+
.default('none')
|
|
272
|
+
.description(
|
|
273
|
+
'Where a survey reads. none = the shared checkout with a read-only preset (writes and shell commands are impossible); detached = a disposable detached git worktree created at the recorded commit, with a sandbox preset that may write and run commands inside it. A detached sandbox is removed when the survey completes or is abandoned; it has no branch, never participates in merge/stale-base, and cannot commit its evidence.',
|
|
274
|
+
),
|
|
275
|
+
})
|
|
276
|
+
.default({})
|
|
277
|
+
.description('Survey placement. Tuning only: the tower-survey component row is the capability switch.'),
|
|
254
278
|
tests: Schema.object({
|
|
255
279
|
// #136: no `enabled` switch. The `tower-tests` component row *is* the
|
|
256
280
|
// switch; a second boolean could only disagree with it, and a row-off
|
|
@@ -260,7 +284,6 @@ export const Config = Schema.object({
|
|
|
260
284
|
.description('Run discovery before spawning a worker, when completing, or only on explicit tower_test (default off: repository code runs only on an explicit tower_test).'),
|
|
261
285
|
timeoutMs: Schema.natural().min(1000).max(1800000).default(60000).description('Per discovery command timeout.'),
|
|
262
286
|
maxBytes: Schema.natural().min(1024).max(16 * 1024 * 1024).default(16 * 1024 * 1024).description('Maximum captured stdout bytes per command.'),
|
|
263
|
-
autoChecks: Schema.boolean().default(true).description('Attach a tests check to the completion report when discovery runs.'),
|
|
264
287
|
allowPackageDiscovery: Schema.boolean()
|
|
265
288
|
.default(true)
|
|
266
289
|
.description('Allow package.json tower.testDiscovery commands from the mission worktree (executes repository code; see security docs).'),
|
|
@@ -270,13 +293,16 @@ export const Config = Schema.object({
|
|
|
270
293
|
gates: Schema.object({
|
|
271
294
|
requireChecks: Schema.boolean()
|
|
272
295
|
.default(false)
|
|
273
|
-
.description('Require at least one
|
|
296
|
+
.description('Require at least one recorded check (tower_complete.checks) or test-discovery receipt before a mission can merge.'),
|
|
274
297
|
requireTests: Schema.boolean()
|
|
275
298
|
.default(false)
|
|
276
299
|
.description('Require a passing test-discovery receipt for the reviewed worktree tip before merge.'),
|
|
277
300
|
allowReviewExemption: Schema.boolean()
|
|
278
301
|
.default(false)
|
|
279
302
|
.description('Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers.'),
|
|
303
|
+
requireFreshSurvey: Schema.boolean()
|
|
304
|
+
.default(false)
|
|
305
|
+
.description('Promote the advisory survey-stale dependency warning to a merge blocker: a build may only merge when every survey dependency read a commit that is an ancestor-or-equal of the build base. Default false (warning only).'),
|
|
280
306
|
maxReportBytes: Schema.natural()
|
|
281
307
|
.min(1024)
|
|
282
308
|
.max(16 * 1024 * 1024)
|
|
@@ -429,6 +455,9 @@ export function assertKnownKeys(raw, label) {
|
|
|
429
455
|
if ((group === 'spawn') && value.reviewerModel && typeof value.reviewerModel === 'object') {
|
|
430
456
|
assertModelKeys(value.reviewerModel, `${label} spawn.reviewerModel`)
|
|
431
457
|
}
|
|
458
|
+
if ((group === 'spawn') && value.surveyModel && typeof value.surveyModel === 'object') {
|
|
459
|
+
assertModelKeys(value.surveyModel, `${label} spawn.surveyModel`)
|
|
460
|
+
}
|
|
432
461
|
}
|
|
433
462
|
}
|
|
434
463
|
|
|
@@ -509,6 +538,7 @@ export function resolveTowerConfig(rawConfig) {
|
|
|
509
538
|
worktrees: clone(parsed.worktrees ?? {}),
|
|
510
539
|
spawn: clone(parsed.spawn ?? {}),
|
|
511
540
|
send: clone(parsed.send ?? {}),
|
|
541
|
+
survey: clone(parsed.survey ?? {}),
|
|
512
542
|
tests: clone(parsed.tests ?? {}),
|
|
513
543
|
gates: clone(parsed.gates ?? {}),
|
|
514
544
|
docs: clone(parsed.docs ?? {}),
|
|
@@ -17,7 +17,7 @@ export const PlanInput = Type.Object({
|
|
|
17
17
|
title: Type.String({ description: 'Printable Unicode mission title, NFC-normalized, at most 120 characters; used as the mission label.' }),
|
|
18
18
|
scope: Type.Optional(Type.Array(Type.String(), { description: 'Relative file globs this mission may touch; empty only for survey.' })),
|
|
19
19
|
tasks: Type.Optional(Type.Array(Type.String(), { description: 'Verifiable task items the worker ticks off.' })),
|
|
20
|
-
deps: Type.Optional(Type.Array(Type.String(), { description: 'Mission ids that must
|
|
20
|
+
deps: Type.Optional(Type.Array(Type.String(), { description: 'Mission ids that must be satisfied before this one: merged for a build, completed for a survey.' })),
|
|
21
21
|
kind: Type.Optional(enumOf(MISSION_KINDS, { description: 'survey = read-only investigation that reserves no scope.' })),
|
|
22
22
|
context: Type.Optional(Type.String({ description: "The user's own key sentences, verbatim (max 4096 chars)." })),
|
|
23
23
|
references: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: 'Read-only pointers/files the worker should read (max 20); copied into the briefing.' })),
|
|
@@ -63,6 +63,14 @@ export const CompleteInput = Type.Object({
|
|
|
63
63
|
missionId: Type.Optional(Type.String({ description: 'Mission id, e.g. M1 (canonical; alias: id).' })),
|
|
64
64
|
id: Type.Optional(Type.String({ description: 'Alias for missionId; both are accepted.' })),
|
|
65
65
|
report: Type.String({ description: 'Full completion report for the tower and the next review round.' }),
|
|
66
|
+
result: Type.Optional(
|
|
67
|
+
Type.Object({
|
|
68
|
+
paths: Type.Optional(Type.Array(Type.String(), { maxItems: 200, description: 'Survey only: paths the read touched or found relevant.' })),
|
|
69
|
+
symbols: Type.Optional(Type.Array(Type.String(), { maxItems: 200, description: 'Survey only: symbols the read identified.' })),
|
|
70
|
+
plan: Type.Optional(Type.Array(Type.String(), { maxItems: 200, description: 'Survey only: the ordered plan or next steps the read recommends.' })),
|
|
71
|
+
notes: Type.Optional(Type.String({ description: 'Survey only: short free-form evidence (max 4000 chars).' })),
|
|
72
|
+
}, { additionalProperties: false, description: 'Survey evidence; a build completion may not carry one.' }),
|
|
73
|
+
),
|
|
66
74
|
checks: Type.Optional(
|
|
67
75
|
Type.Array(
|
|
68
76
|
Type.Object({
|