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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"surfaces": {
|
|
5
5
|
"configKeys": {
|
|
6
6
|
"seed": 64,
|
|
7
|
-
"value":
|
|
7
|
+
"value": 56,
|
|
8
8
|
"decisions": [
|
|
9
9
|
{
|
|
10
10
|
"from": 64,
|
|
@@ -19,13 +19,70 @@
|
|
|
19
19
|
"date": "2026-11-03",
|
|
20
20
|
"adr": "docs/adr/0005-component-surface.md",
|
|
21
21
|
"note": "#M39: one new key, review.mode (off/optional/required), replaces the deprecated review.requireReview boolean as the review policy. It is not a fifth component row: the reviewer preset row stays a capability, and the mode decides whether it is required (required) or unused (off). review.requireReview remains accepted for one release as a normalized alias, so it is a deprecated key rather than a removed one and the surface grows by exactly one."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"from": 54,
|
|
25
|
+
"to": 57,
|
|
26
|
+
"date": "2026-11-10",
|
|
27
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
28
|
+
"note": "#148/#149: the shape redesign adds the survey routing keys spawn.surveyPreset (core) and spawn.surveyModel (tuning), plus survey.sandbox (tuning) in the new survey section. tower-survey is the capability row; the preset/model names only route the read-only agent."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"from": 57,
|
|
32
|
+
"to": 55,
|
|
33
|
+
"date": "2026-11-10",
|
|
34
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
35
|
+
"note": "Review round 2 contracts the shape surface before release: spawn.surveyPreset could never be read (a survey's preset is shape-owned, so the key could only disagree with the registry) and survey.sandbox advertised a placement W1 does not implement. Only spawn.surveyModel remains as the optional model route; both removals are recorded here rather than silently shrinking the two-way surface."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"from": 55,
|
|
39
|
+
"to": 56,
|
|
40
|
+
"date": "2026-11-10",
|
|
41
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
42
|
+
"note": "#148 W2 implements the detached survey sandbox, so survey.sandbox returns as one tuning key: none (read-only shared checkout, the default and unchanged W1 behavior) or detached (a disposable detached worktree with a write/shell-capable preset). It is placement tuning, not a capability row or a second routing key; spawn.surveyPreset stays removed because the shape resolves the preset."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"from": 56,
|
|
46
|
+
"to": 57,
|
|
47
|
+
"date": "2026-11-10",
|
|
48
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
49
|
+
"note": "#148 W2 D5 adds gates.requireFreshSurvey (default false): the survey-stale dependency warning is advisory by default and becomes a merge blocker only when this gate is true. It is a merge-gate strictness key like the other gates.* entries, not a component."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"from": 57,
|
|
53
|
+
"to": 56,
|
|
54
|
+
"date": "2026-11-10",
|
|
55
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
56
|
+
"note": "#149 W4 contracts the surface by one: `tests.autoChecks` existed only to copy a discovery projection into the completion report, which made a second evidence shape. The receipt in mission.testRuns is now the one evidence shape read by domain/evidence.js, so the key has no remaining meaning and is removed rather than kept as a dead tuning switch."
|
|
22
57
|
}
|
|
23
58
|
]
|
|
24
59
|
},
|
|
25
60
|
"sections": {
|
|
26
61
|
"seed": 15,
|
|
27
|
-
"value":
|
|
28
|
-
"decisions": [
|
|
62
|
+
"value": 15,
|
|
63
|
+
"decisions": [
|
|
64
|
+
{
|
|
65
|
+
"from": 15,
|
|
66
|
+
"to": 15,
|
|
67
|
+
"date": "2026-11-10",
|
|
68
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
69
|
+
"note": "#148/#149 restore the section count to its seed: survey.sandbox needs a home, so the survey section returns the 15 sections that #121 shrank to 14. Net zero against the seed; no other section changed."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"from": 15,
|
|
73
|
+
"to": 14,
|
|
74
|
+
"date": "2026-11-10",
|
|
75
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
76
|
+
"note": "Review round 2 removes the survey section with its only key: survey.sandbox advertised a detached read-only checkout W1 does not implement. The section existed only to hold that key, so the count returns to 14."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"from": 14,
|
|
80
|
+
"to": 15,
|
|
81
|
+
"date": "2026-11-10",
|
|
82
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
83
|
+
"note": "#148 W2 restores the survey section for its one tuning key, survey.sandbox (placement: none|detached). The section count returns to the 15 of the earlier shape cut; no other section changed and no component row was added (the sandbox preset is an internal child of the existing tower-survey group)."
|
|
84
|
+
}
|
|
85
|
+
]
|
|
29
86
|
},
|
|
30
87
|
"tools": {
|
|
31
88
|
"seed": 27,
|
|
@@ -45,7 +102,7 @@
|
|
|
45
102
|
},
|
|
46
103
|
"components": {
|
|
47
104
|
"seed": 4,
|
|
48
|
-
"value":
|
|
105
|
+
"value": 12,
|
|
49
106
|
"decisions": [
|
|
50
107
|
{
|
|
51
108
|
"from": 4,
|
|
@@ -81,6 +138,13 @@
|
|
|
81
138
|
"date": "2026-11-02",
|
|
82
139
|
"adr": "docs/adr/0005-component-surface.md",
|
|
83
140
|
"note": "#134: the gate reads every shipped cordis.patch.yml, so the tower-ui row in ui-bundle/cordis.patch.yml joins the counted surface. The row was always shipped and unclassified; counting it is not new surface, it is the existing surface becoming visible to the policy."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"from": 11,
|
|
144
|
+
"to": 12,
|
|
145
|
+
"date": "2026-11-10",
|
|
146
|
+
"adr": "docs/adr/0006-mission-shapes.md",
|
|
147
|
+
"note": "#148/#149: tower-survey is a visible optional row: off means surveys cannot be planned and no survey agent is dispatched; build missions are unaffected."
|
|
84
148
|
}
|
|
85
149
|
]
|
|
86
150
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// tower-survey-sandbox preset policy (#148 W2): a survey that reads in a
|
|
2
|
+
// disposable detached sandbox may write and run commands *inside that sandbox*,
|
|
3
|
+
// but it still owns no branch, never commits, and may not merge, rebase, review
|
|
4
|
+
// or spawn. The allow-list is the structural guarantee: the denied tools do not
|
|
5
|
+
// exist in the session, so a prompt cannot talk the agent into using them.
|
|
6
|
+
//
|
|
7
|
+
// The shared-checkout survey preset (`dsh-tower/survey`) stays strictly
|
|
8
|
+
// read-only; this module is mounted only for a detached sandbox.
|
|
9
|
+
export const name = 'dsh-tower/survey-sandbox'
|
|
10
|
+
export const inject = ['tools', 'systemPrompt']
|
|
11
|
+
|
|
12
|
+
const ALLOW = [
|
|
13
|
+
'read',
|
|
14
|
+
'write',
|
|
15
|
+
'edit',
|
|
16
|
+
'glob',
|
|
17
|
+
'grep',
|
|
18
|
+
'pwsh',
|
|
19
|
+
'tower_mission',
|
|
20
|
+
'tower_status',
|
|
21
|
+
'tower_finding',
|
|
22
|
+
'tower_findings',
|
|
23
|
+
'tower_inbox',
|
|
24
|
+
'tower_complete',
|
|
25
|
+
'skill',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const PROTOCOL = [
|
|
29
|
+
'You are a Tower survey worker reading one recorded commit in a disposable detached sandbox.',
|
|
30
|
+
'Work only inside the sandbox path named in your briefing; never modify the main checkout.',
|
|
31
|
+
'You may write files and run shell commands inside the sandbox, but do not commit: completion requires',
|
|
32
|
+
'the sandbox HEAD to still be the recorded commit and zero new commits. The sandbox is removed when the mission completes.',
|
|
33
|
+
'You cannot spawn, merge, rebase, review or diff. Acknowledge inbox mail with tower_inbox; unread messages block completion.',
|
|
34
|
+
'Finish with tower_complete (report + checks evidence, optionally result:{paths,symbols,plan,notes}); the result is survey evidence only.',
|
|
35
|
+
].join(' ')
|
|
36
|
+
|
|
37
|
+
export function apply(ctx) {
|
|
38
|
+
try {
|
|
39
|
+
ctx.tools.restrict({ allow: ALLOW })
|
|
40
|
+
} catch {
|
|
41
|
+
// a composition without the tool registry still gets the protocol text
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
ctx.systemPrompt.section({ name: 'tower:survey-sandbox-protocol', order: 12000, text: PROTOCOL })
|
|
45
|
+
} catch {
|
|
46
|
+
// prompt assembly remains valid without the section
|
|
47
|
+
}
|
|
48
|
+
}
|
package/survey.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// tower-survey preset policy: a read-only inspection surface. The allow-list is
|
|
2
|
+
// the structural read-only guarantee (#148): a survey cannot write, edit, run a
|
|
3
|
+
// shell, commit, or merge because the tools do not exist in its session.
|
|
4
|
+
export const name = 'dsh-tower/survey'
|
|
5
|
+
export const inject = ['tools', 'systemPrompt']
|
|
6
|
+
|
|
7
|
+
const ALLOW = [
|
|
8
|
+
'read',
|
|
9
|
+
'glob',
|
|
10
|
+
'grep',
|
|
11
|
+
'tower_mission',
|
|
12
|
+
'tower_status',
|
|
13
|
+
'tower_finding',
|
|
14
|
+
'tower_findings',
|
|
15
|
+
'tower_inbox',
|
|
16
|
+
'tower_complete',
|
|
17
|
+
'skill',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const PROTOCOL = [
|
|
21
|
+
'You are a Tower survey worker in a DeepSeek Harness Tower workspace.',
|
|
22
|
+
'You read one recorded commit in the shared checkout and never modify it: do not write, edit, run a shell,',
|
|
23
|
+
'commit, rebase, or merge. Your tools are read, glob, grep, skill, and the Tower mission/status/finding/inbox/complete tools.',
|
|
24
|
+
'Acknowledge inbox mail with tower_inbox (it records Tower state, not a repository write): unread messages block completion.',
|
|
25
|
+
'Finish with tower_complete (report + checks evidence, optionally result:{paths,symbols,notes}); completion requires',
|
|
26
|
+
'HEAD to still be the recorded commit, the tracked-file status set to be unchanged, and zero commits.',
|
|
27
|
+
].join(' ')
|
|
28
|
+
|
|
29
|
+
export function apply(ctx) {
|
|
30
|
+
try {
|
|
31
|
+
ctx.tools.restrict({ allow: ALLOW })
|
|
32
|
+
} catch {
|
|
33
|
+
// a composition without the tool registry still gets the protocol text
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
ctx.systemPrompt.section({ name: 'tower:survey-protocol', order: 12000, text: PROTOCOL })
|
|
37
|
+
} catch {
|
|
38
|
+
// prompt assembly remains valid without the section
|
|
39
|
+
}
|
|
40
|
+
}
|
package/tools/complete.js
CHANGED
|
@@ -13,7 +13,7 @@ export function registerCompleteTool(ctx, deps) {
|
|
|
13
13
|
define(ctx, {
|
|
14
14
|
name: 'tower_complete',
|
|
15
15
|
description:
|
|
16
|
-
'Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base, and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. Stores the full report, capped at gates.maxReportBytes (default 32768 UTF-8 bytes); a larger report is rejected with invalid-argument and an actionable nextAction, never truncated. Persists reportBytes on the stored report. completion means ready for preflight and optional review, not merged.',
|
|
16
|
+
'Worker-only: report mission completion. Enforced by the store: every task done or dropped, no blockers, the current owner, the mission branch has at least one commit since its spawn base (zero commits plus HEAD pinned to readCommit for a survey; zero commits in its detached sandbox with HEAD pinned for a sandboxed survey), and no unread inbox messages. checks records verifiable evidence (name/command/exitCode/evidence); gates.requireChecks can require at least one before merge. result is survey-only evidence ({paths?,symbols?,plan?,notes?}); a build completion that carries one is refused. Stores the full report, capped at gates.maxReportBytes (default 32768 UTF-8 bytes); a larger report is rejected with invalid-argument and an actionable nextAction, never truncated. Persists reportBytes on the stored report. completion means ready for preflight and optional review, not merged.',
|
|
17
17
|
parameters: CompleteInput,
|
|
18
18
|
aliases: {"missionId":["id"]},
|
|
19
19
|
requiresMissionId: true,
|
|
@@ -38,15 +38,15 @@ export function registerCompleteTool(ctx, deps) {
|
|
|
38
38
|
if (discovery.configured && discovery.receipt.ok !== true) {
|
|
39
39
|
throw new TowerToolError('tests-failed', `test discovery failed for ${args.missionId}: ${discovery.receipt.error}`)
|
|
40
40
|
}
|
|
41
|
-
if (discovery.configured && testsConfig.autoChecks !== false && discovery.check) {
|
|
42
|
-
if (checks.length >= 20) checks[19] = discovery.check
|
|
43
|
-
else checks.push(discovery.check)
|
|
44
|
-
}
|
|
45
41
|
}
|
|
42
|
+
// #149 W4: the receipt already lives in mission.testRuns and is read by
|
|
43
|
+
// domain/evidence.js. Copying a `tests` projection into the completion
|
|
44
|
+
// report would be a second evidence shape, so it is not done here.
|
|
46
45
|
return tower.complete({
|
|
47
46
|
missionId: args.missionId,
|
|
48
47
|
agentId: String(exec.agent.id),
|
|
49
48
|
report: args.report,
|
|
49
|
+
result: args.result,
|
|
50
50
|
checks,
|
|
51
51
|
})
|
|
52
52
|
},
|
package/tools/diff.js
CHANGED
|
@@ -18,7 +18,7 @@ import { DiffInput } from '../domain/contracts/tool-inputs/index.js'
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {any} ctx
|
|
21
|
-
* @param {{ define: import('../types/index.js').DefineTool, openTower: any }} deps
|
|
21
|
+
* @param {{ define: import('../types/index.js').DefineTool, openTower: any, actorFor: any }} deps
|
|
22
22
|
*/
|
|
23
23
|
export function registerDiffTool(ctx, deps) {
|
|
24
24
|
// openTower is injected by the composition root (#69).
|
|
@@ -31,9 +31,11 @@ export function registerDiffTool(ctx, deps) {
|
|
|
31
31
|
aliases: {"missionId":["id"]},
|
|
32
32
|
requiresMissionId: true,
|
|
33
33
|
output: output(SCHEMAS.diff),
|
|
34
|
-
async execute(args) {
|
|
34
|
+
async execute(args, exec) {
|
|
35
35
|
const tower = await deps.openTower(args.repoRoot)
|
|
36
|
-
|
|
36
|
+
// A reviewer's diff is bound to its recorded review commit (#149 W3).
|
|
37
|
+
const actor = await deps.actorFor(tower, exec)
|
|
38
|
+
return tower.diff({ missionId: args.missionId, maxBytes: args.maxBytes, actor })
|
|
37
39
|
},
|
|
38
40
|
})
|
|
39
41
|
}
|
package/tools/plan.js
CHANGED
|
@@ -25,7 +25,7 @@ export function registerPlanTool(ctx, deps) {
|
|
|
25
25
|
define(ctx, {
|
|
26
26
|
name: 'tower_plan',
|
|
27
27
|
description:
|
|
28
|
-
'Plan one or more missions in an initialized repository. Each mission has a printable-Unicode title (NFC-normalized, at most 120 characters), relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles.',
|
|
28
|
+
'Plan one or more missions in an initialized repository. Each mission has a printable-Unicode title (NFC-normalized, at most 120 characters), relative scope globs, optional task checklist, optional mission-id dependencies, and kind build|survey. Assigns M1.. ids and auto-adds a dependency when build scopes overlap (serializes merges); rejects dependency cycles. Returns survey-stale warnings for a build that depends on a survey whose recorded readCommit is not an ancestor of the base; those warnings are advisory and gates.requireFreshSurvey promotes them to merge blockers.',
|
|
29
29
|
parameters: PlanInput,
|
|
30
30
|
board: true,
|
|
31
31
|
output: output(SCHEMAS.plan),
|
package/tools/preflight.js
CHANGED
|
@@ -12,7 +12,7 @@ export function registerPreflightTool(ctx, deps) {
|
|
|
12
12
|
define(ctx, {
|
|
13
13
|
name: 'tower_preflight',
|
|
14
14
|
description:
|
|
15
|
-
'Read-only merge gate for one mission: returns ok plus stable blocker codes (branch-missing, deps-unmerged,
|
|
15
|
+
'Read-only merge gate for one mission: returns ok plus stable blocker codes (branch-missing, deps-unmerged, stale-base, out-of-scope, base-dirty, branch-owned-by-closed-missions, and mission/worker guards) and warnings (survey-stale). Review blockers (no-review, review-hold, not-clean, review-not-approved, tip-moved, review-mission-mismatch) apply only when review.mode is required; review.mode off and optional skip them. survey-stale reports a build dependency on a survey whose readCommit is not an ancestor-or-equal of the build base; it is a warning by default and a blocker only when gates.requireFreshSurvey is true. A survey has no merge gate: it is refused with invalid-transition (tower_complete checks HEAD/readCommit and zero commits). Never changes anything.',
|
|
16
16
|
parameters: PreflightInput,
|
|
17
17
|
aliases: {"missionId":["id"]},
|
|
18
18
|
requiresMissionId: true,
|
|
@@ -20,10 +20,11 @@ export function registerPreflightTool(ctx, deps) {
|
|
|
20
20
|
async execute(args) {
|
|
21
21
|
const tower = await deps.openTower(args.repoRoot)
|
|
22
22
|
const { activity, parentStatus } = await observeActivity(ctx, tower)
|
|
23
|
-
const gate = await tower.evaluateGate({ missionId: args.missionId, activity, parentStatus })
|
|
23
|
+
const gate = await tower.evaluateGate({ missionId: args.missionId, activity, parentStatus, action: 'preflight' })
|
|
24
24
|
return {
|
|
25
25
|
ok: gate.ok,
|
|
26
26
|
blockers: gate.blockers,
|
|
27
|
+
warnings: gate.warnings ?? [],
|
|
27
28
|
missionId: args.missionId,
|
|
28
29
|
branchTip: gate.branchTip ?? null,
|
|
29
30
|
baseTip: gate.baseTip ?? null,
|
package/tools/spawn.js
CHANGED
|
@@ -2,47 +2,27 @@
|
|
|
2
2
|
import { SCHEMAS, output } from '../domain/contracts/outputs.js'
|
|
3
3
|
import { TowerToolError, classifyError } from '../domain/errors.js'
|
|
4
4
|
import { SpawnInput } from '../domain/contracts/tool-inputs/index.js'
|
|
5
|
-
import {
|
|
5
|
+
import { defaultPresetFor, shapeFor } from '../domain/shapes/index.js'
|
|
6
|
+
import { DEFAULT_ROLE, ROLES as ROLE_ENUM, SPAWN_OUTCOME, isCompletedStopReason, isSpawnAdmission } from '../domain/vocabulary.js'
|
|
7
|
+
import { resolveRoleSpawn, roleSpawnDiscovery } from '../domain/roles.js'
|
|
6
8
|
import {
|
|
7
9
|
assertSpawnableMission,
|
|
8
10
|
precheckSpawnServices,
|
|
9
11
|
precheckWorkerOwnership,
|
|
10
12
|
resolveRosterName,
|
|
11
13
|
} from '../app/spawn-prechecks.js'
|
|
12
|
-
import { buildAgentOptions,
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {any}
|
|
16
|
-
* @param {{
|
|
17
|
-
* define: import('../types/index.js').DefineTool,
|
|
18
|
-
* openTower: any,
|
|
19
|
-
* actorFor: any,
|
|
20
|
-
* requireTowerActor: any,
|
|
21
|
-
* enqueueSpawn: any,
|
|
22
|
-
* observeActivity: any,
|
|
23
|
-
* spawnBackoffState: any,
|
|
24
|
-
* recordSpawnThrottle: any,
|
|
25
|
-
* recordSpawnSuccess: any,
|
|
26
|
-
* runMissionDiscovery: any,
|
|
27
|
-
* textOf: any, inspectLeases: any,
|
|
28
|
-
* leaseComponent?: () => { enabled: boolean, reason: string },
|
|
29
|
-
* }} deps
|
|
30
|
-
*/
|
|
14
|
+
import { buildAgentOptions, provisionSpawnPlacement, waitForSpawnResult } from '../app/spawn-support.js'
|
|
15
|
+
import { cleanupReviewCheckout } from '../app/reviewCheckout.js'
|
|
16
|
+
/** @param {any} ctx
|
|
17
|
+
* @param {{ define: import('../types/index.js').DefineTool, [key: string]: any }} deps */
|
|
31
18
|
export function registerSpawnTool(ctx, deps) {
|
|
32
|
-
const {
|
|
33
|
-
define, actorFor, requireTowerActor, enqueueSpawn, observeActivity,
|
|
34
|
-
spawnBackoffState, recordSpawnThrottle, recordSpawnSuccess,
|
|
35
|
-
runMissionDiscovery, textOf,
|
|
36
|
-
} = deps
|
|
19
|
+
const { define, actorFor, requireTowerActor, enqueueSpawn, observeActivity, spawnBackoffState, recordSpawnThrottle, recordSpawnSuccess, runMissionDiscovery, textOf } = deps
|
|
37
20
|
define(ctx, {
|
|
38
21
|
name: 'tower_spawn',
|
|
39
22
|
description:
|
|
40
|
-
'Spawn a worker or reviewer for a mission. Workers use the tower-worker
|
|
41
|
-
parameters: SpawnInput,
|
|
23
|
+
'Spawn a worker or reviewer for a mission. Workers use the mission shape\'s preset (tower-worker for a build, tower-survey for a read-only shared survey, tower-survey-sandbox for a detached survey with survey.sandbox=detached); reviewers require a completed build mission and use the read-only tower-reviewer preset, and the reviewer role is refused when review.mode is off. A build provisions (or reuses) the mission git worktree and branch; a shared survey runs in the shared checkout because it reads one commit; a detached survey provisions (or reuses) a disposable detached worktree at its readCommit with no branch. Generates a self-contained briefing (including dependency survey result evidence) and starts a background subagent. The child final report is delivered back to this session; tower_roster shows authoritative activity. Records the agent in the roster; reviewer spawns get a disposable detached checkout at the recorded branch tip, and the verdict is stamped with that commit (never a live worktree HEAD). Returns immediately unless wait=true.', parameters: SpawnInput,
|
|
42
24
|
aliases: {"missionId":["id"],"waitMs":["wait_ms"]},
|
|
43
|
-
requiresMissionId: true,
|
|
44
|
-
board: true,
|
|
45
|
-
output: output(SCHEMAS.spawn),
|
|
25
|
+
requiresMissionId: true, board: true, output: output(SCHEMAS.spawn),
|
|
46
26
|
async execute(args, exec) {
|
|
47
27
|
const tower = await deps.openTower(args.repoRoot)
|
|
48
28
|
const actor = await actorFor(tower, exec)
|
|
@@ -51,10 +31,18 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
51
31
|
if (!ROLE_ENUM.includes(role)) throw new TowerToolError('invalid-argument', `role must be one of ${ROLE_ENUM.join('|')}`)
|
|
52
32
|
// Repository config (if any) already overrides the profile config here.
|
|
53
33
|
const config = tower.config
|
|
54
|
-
const preset = isReviewerRole(role) ? config.spawn.reviewerPreset : config.spawn.workerPreset
|
|
55
|
-
const model = isReviewerRole(role) ? config.spawn.reviewerModel : config.spawn.workerModel
|
|
56
34
|
const context = await tower.missionContext(args.missionId)
|
|
57
35
|
assertSpawnableMission({ role, context, config })
|
|
36
|
+
const shape = shapeFor(context.mission.kind)
|
|
37
|
+
// #148 W2: the shape resolves its preset from tuning config, so a detached
|
|
38
|
+
// survey gets the sandbox preset and a shared survey stays read-only.
|
|
39
|
+
// #149 W4: role resolution (preset row, model route, placement, briefing)
|
|
40
|
+
// is one contract, orthogonal to the shape that supplies the facts.
|
|
41
|
+
const shapePreset = defaultPresetFor(context.mission.kind, config, context.mission)
|
|
42
|
+
const roleSpawn = resolveRoleSpawn(role, { config, shape, shapePreset })
|
|
43
|
+
if (!roleSpawn) throw new TowerToolError('invalid-argument', `role must be one of ${ROLE_ENUM.join('|')}`)
|
|
44
|
+
const preset = roleSpawn.preset
|
|
45
|
+
const model = roleSpawn.model
|
|
58
46
|
const spawned = await enqueueSpawn(tower.root, async () => {
|
|
59
47
|
const observed = await observeActivity(ctx, tower)
|
|
60
48
|
await tower.reconcileObservedInactive(observed.activity)
|
|
@@ -65,26 +53,25 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
65
53
|
}
|
|
66
54
|
const view = await tower.rosterView()
|
|
67
55
|
const name = resolveRosterName({ role, missionId: args.missionId, requested: args.name, roster: view.roster })
|
|
68
|
-
const takeover = await precheckWorkerOwnership({
|
|
69
|
-
role, tower, ctx, view, missionId: args.missionId, takeover: args.takeover,
|
|
70
|
-
maxLiveWorkers: config.spawn.maxLiveWorkers, budget: backoff.budget, observeActivity,
|
|
71
|
-
})
|
|
56
|
+
const takeover = await precheckWorkerOwnership({ role, tower, ctx, view, missionId: args.missionId, takeover: args.takeover, maxLiveWorkers: config.spawn.maxLiveWorkers, budget: backoff.budget, observeActivity })
|
|
72
57
|
const { subagents, provider } = await precheckSpawnServices({
|
|
73
58
|
tower, ctx, config, preset, actor, exec, inspectLeases: deps.inspectLeases,
|
|
74
59
|
// #131: the app layer only needs the row fact; the root owns the registry.
|
|
75
60
|
leaseComponent: deps.leaseComponent,
|
|
76
61
|
})
|
|
77
62
|
|
|
78
|
-
const
|
|
79
|
-
|
|
63
|
+
const { mission, worktree, cwd, branch, review, briefing, provisioning, created, sandbox } = await provisionSpawnPlacement(tower, {
|
|
64
|
+
role,
|
|
65
|
+
context,
|
|
66
|
+
missionId: /** @type {string} */ (args.missionId),
|
|
67
|
+
name,
|
|
80
68
|
worktreeRoot: args.worktreeRoot,
|
|
81
|
-
includeDirtyBase: args.includeDirtyBase
|
|
69
|
+
includeDirtyBase: args.includeDirtyBase,
|
|
70
|
+
instructions: args.instructions,
|
|
82
71
|
})
|
|
83
|
-
const { mission, worktree } = provisioned
|
|
84
|
-
const briefing = buildSpawnBriefing({ role, towerRoot: tower.root, context, worktree, instructions: args.instructions })
|
|
85
72
|
const agentOptions = buildAgentOptions(model)
|
|
86
73
|
|
|
87
|
-
if (
|
|
74
|
+
if (roleSpawnDiscovery(role, shape) && isSpawnAdmission(tower.config.tests?.admission)) {
|
|
88
75
|
const discovery = await runMissionDiscovery(tower, mission)
|
|
89
76
|
if (discovery.configured && discovery.receipt.ok !== true) {
|
|
90
77
|
throw new TowerToolError(
|
|
@@ -102,15 +89,17 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
102
89
|
name,
|
|
103
90
|
role,
|
|
104
91
|
parentSessionId: String(exec.agent.id),
|
|
105
|
-
cwd
|
|
106
|
-
branch
|
|
92
|
+
cwd,
|
|
93
|
+
branch,
|
|
94
|
+
reviewCommit: review?.commit,
|
|
95
|
+
reviewCheckout: review?.path,
|
|
107
96
|
})
|
|
108
97
|
try {
|
|
109
98
|
activation = await subagents.startActivation({
|
|
110
99
|
provider,
|
|
111
100
|
label: name,
|
|
112
101
|
request: {
|
|
113
|
-
cwd
|
|
102
|
+
cwd,
|
|
114
103
|
agentPreset: preset,
|
|
115
104
|
prompt: [{ type: 'text', text: briefing }],
|
|
116
105
|
parent: exec.agent,
|
|
@@ -127,8 +116,10 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
127
116
|
role,
|
|
128
117
|
agentId,
|
|
129
118
|
parentSessionId: String(exec.agent.id),
|
|
130
|
-
cwd
|
|
131
|
-
branch
|
|
119
|
+
cwd,
|
|
120
|
+
branch,
|
|
121
|
+
reviewCommit: review?.commit,
|
|
122
|
+
reviewCheckout: review?.path,
|
|
132
123
|
takeover,
|
|
133
124
|
})
|
|
134
125
|
recordSpawnSuccess(tower.root, config.spawn.maxLiveWorkers)
|
|
@@ -152,6 +143,10 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
152
143
|
detail: `spawn failed${agentId ? ` after activation ${agentId}` : ' before activation'}: ${String(error?.message ?? error).slice(0, 300)}`,
|
|
153
144
|
})
|
|
154
145
|
.catch(() => {})
|
|
146
|
+
// A failed review spawn must not leak its detached evidence checkout.
|
|
147
|
+
if (review) {
|
|
148
|
+
await cleanupReviewCheckout(tower, { name, missionId: mission.id }).catch(() => {})
|
|
149
|
+
}
|
|
155
150
|
throw error
|
|
156
151
|
}
|
|
157
152
|
|
|
@@ -167,8 +162,11 @@ export function registerSpawnTool(ctx, deps) {
|
|
|
167
162
|
provider,
|
|
168
163
|
maxDepth: config.spawn.maxDepth ?? 1,
|
|
169
164
|
...(Object.keys(agentOptions).length > 0 ? { agentOptions } : {}),
|
|
170
|
-
worktree: { path: worktree.path, branch: worktree.branch, baseCommit: worktree.baseCommit },
|
|
171
|
-
|
|
165
|
+
worktree: worktree ? { path: worktree.path, branch: worktree.branch, baseCommit: worktree.baseCommit } : null,
|
|
166
|
+
sandbox,
|
|
167
|
+
review: review ? { commit: review.commit, checkout: review.path } : null,
|
|
168
|
+
provisioning,
|
|
169
|
+
created,
|
|
172
170
|
briefingChars: briefing.length,
|
|
173
171
|
takeover,
|
|
174
172
|
rosterEntry: entry,
|
package/tools/test.js
CHANGED
|
@@ -46,6 +46,8 @@ export function registerTestTool(ctx, deps) {
|
|
|
46
46
|
return { ok: false, configured: false, error, tests: [], commands: [] }
|
|
47
47
|
}
|
|
48
48
|
const receipt = discovery.receipt
|
|
49
|
+
// #149 W4: the receipt is returned as recorded; there is no derived
|
|
50
|
+
// `checks` copy. Completion/batch read it through domain/evidence.js.
|
|
49
51
|
return {
|
|
50
52
|
ok: receipt.ok === true,
|
|
51
53
|
configured: true,
|
|
@@ -54,7 +56,6 @@ export function registerTestTool(ctx, deps) {
|
|
|
54
56
|
commands: receipt.commands ?? [],
|
|
55
57
|
error: receipt.error ?? undefined,
|
|
56
58
|
receipt,
|
|
57
|
-
checks: discovery.check ? [discovery.check] : [],
|
|
58
59
|
}
|
|
59
60
|
},
|
|
60
61
|
})
|
package/types/index.d.ts
CHANGED
|
@@ -149,11 +149,25 @@ export interface RebaseRecord {
|
|
|
149
149
|
baseTip?: string
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
/** Evidence a survey may record when it completes (optional). */
|
|
153
|
+
export interface SurveyResult {
|
|
154
|
+
paths?: string[]
|
|
155
|
+
symbols?: string[]
|
|
156
|
+
/** Ordered plan / next steps the read recommends (bounded array of strings). */
|
|
157
|
+
plan?: string[]
|
|
158
|
+
notes?: string
|
|
159
|
+
[key: string]: unknown
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fields every mission carries regardless of shape. The per-shape arms below
|
|
164
|
+
* declare which document dimensions a kind owns; the state schema v4 union and
|
|
165
|
+
* `domain/state/semantics.js` enforce the same split at runtime.
|
|
166
|
+
*/
|
|
167
|
+
export interface MissionBase {
|
|
153
168
|
id: string
|
|
154
169
|
title: string
|
|
155
170
|
status: MissionStatus
|
|
156
|
-
kind: MissionKind
|
|
157
171
|
scope: string[]
|
|
158
172
|
deps: string[]
|
|
159
173
|
/**
|
|
@@ -165,22 +179,59 @@ export interface Mission {
|
|
|
165
179
|
blockers?: string[]
|
|
166
180
|
reviewPolicy?: ReviewPolicy
|
|
167
181
|
reviewExemptionReason?: string | null
|
|
168
|
-
worktree?: MissionWorktree | null
|
|
169
182
|
owner?: string | null
|
|
170
183
|
notes?: string[]
|
|
171
184
|
completionReport?: CompletionReport | null
|
|
172
185
|
testRuns?: TestRun[]
|
|
173
186
|
rebase?: RebaseRecord | null
|
|
174
|
-
mergedCommit?: string | null
|
|
175
|
-
mergedAt?: number | null
|
|
176
187
|
scopeGeneration?: number
|
|
177
188
|
ownerGeneration?: number
|
|
178
189
|
pauseCause?: string | null
|
|
179
190
|
updatedAt?: number
|
|
180
191
|
references?: string[]
|
|
181
192
|
authorizationRef?: string | null
|
|
193
|
+
/**
|
|
194
|
+
* Survey evidence; the completion transition refuses a result on a shape that
|
|
195
|
+
* does not accept one, so the type stays permissive for the shared draft.
|
|
196
|
+
*/
|
|
197
|
+
result?: SurveyResult | null
|
|
182
198
|
}
|
|
183
199
|
|
|
200
|
+
/** A build mission owns a private branch/worktree and lands by merging. */
|
|
201
|
+
export interface BuildMission extends MissionBase {
|
|
202
|
+
kind: 'build'
|
|
203
|
+
worktree?: MissionWorktree | null
|
|
204
|
+
mergedCommit?: string | null
|
|
205
|
+
mergedAt?: number | null
|
|
206
|
+
readCommit?: never
|
|
207
|
+
readDirty?: never
|
|
208
|
+
sandbox?: never
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A survey reads one commit in the shared checkout; it never merges. */
|
|
212
|
+
export interface SurveyMission extends MissionBase {
|
|
213
|
+
kind: 'survey'
|
|
214
|
+
readCommit: string
|
|
215
|
+
readDirty?: string[]
|
|
216
|
+
result?: SurveyResult | null
|
|
217
|
+
/**
|
|
218
|
+
* #148 W2: the disposable detached sandbox path when `survey.sandbox` is
|
|
219
|
+
* `detached`. It has no branch and is removed on completion/abandon; a
|
|
220
|
+
* cleanup failure keeps the path plus `sandboxCleanupError` so the record and
|
|
221
|
+
* the filesystem do not silently disagree.
|
|
222
|
+
*/
|
|
223
|
+
sandbox?: string
|
|
224
|
+
sandboxCleanupError?: string
|
|
225
|
+
sandboxRemovedAt?: number
|
|
226
|
+
worktree?: never
|
|
227
|
+
branch?: never
|
|
228
|
+
baseCommit?: never
|
|
229
|
+
mergedCommit?: never
|
|
230
|
+
mergedAt?: never
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type Mission = BuildMission | SurveyMission
|
|
234
|
+
|
|
184
235
|
/**
|
|
185
236
|
* Compact archived-mission entry (`archive/index.json`). It keeps enough of the
|
|
186
237
|
* mission for ordering, dependency lookups and id allocation without loading the
|
|
@@ -255,6 +306,10 @@ export interface RosterEntry {
|
|
|
255
306
|
missionId?: string | null
|
|
256
307
|
parentSessionId?: string | null
|
|
257
308
|
spawnTip?: string | null
|
|
309
|
+
/** The commit a reviewer's verdict is stamped with (#149 W3). */
|
|
310
|
+
reviewCommit?: string | null
|
|
311
|
+
/** The disposable detached checkout the reviewer reads, when recorded. */
|
|
312
|
+
reviewCheckout?: string | null
|
|
258
313
|
stale?: boolean
|
|
259
314
|
[key: string]: unknown
|
|
260
315
|
}
|
|
@@ -326,6 +381,12 @@ export interface MissionSummary {
|
|
|
326
381
|
reviewPolicy: ReviewPolicy
|
|
327
382
|
mergedCommit: string | null
|
|
328
383
|
mergedAt: number | null
|
|
384
|
+
/** Survey projection: the commit the survey read (null for a build). */
|
|
385
|
+
readCommit?: string | null
|
|
386
|
+
/** Survey projection: the tracked-status fingerprint captured at plan time. */
|
|
387
|
+
readDirty?: string[]
|
|
388
|
+
/** Survey projection: the disposable detached sandbox path (null otherwise). */
|
|
389
|
+
sandbox?: string | null
|
|
329
390
|
/** Size of the completion report without loading its body; null when absent. */
|
|
330
391
|
reportBytes: number | null
|
|
331
392
|
updatedAt?: number
|