dsh-tower 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/COMPATIBILITY.md +2 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/GLOSSARY.md +6 -0
  5. package/PRINCIPLES.md +15 -7
  6. package/README.en.md +18 -16
  7. package/README.md +18 -11
  8. package/adapters/leases.js +20 -6
  9. package/adapters/legacy.js +149 -37
  10. package/adapters/stateFile.js +11 -1
  11. package/app/archive.js +30 -5
  12. package/app/briefing.js +95 -12
  13. package/app/checkCompletion.js +107 -18
  14. package/app/enrichedMissions.js +1 -1
  15. package/app/freshness.js +77 -0
  16. package/app/gate.js +26 -12
  17. package/app/merge.js +7 -4
  18. package/app/mission.js +24 -1
  19. package/app/missionContext.js +2 -1
  20. package/app/plan.js +64 -1
  21. package/app/provision.js +133 -22
  22. package/app/read.js +38 -5
  23. package/app/rebase.js +10 -8
  24. package/app/review.js +104 -61
  25. package/app/reviewCheckout.js +133 -0
  26. package/app/sandbox.js +121 -0
  27. package/app/sharedCheckout.js +90 -0
  28. package/app/spawn-prechecks.js +31 -20
  29. package/app/spawn-support.js +59 -8
  30. package/app/spawn.js +3 -0
  31. package/app/teardown.js +119 -2
  32. package/app/tower.js +34 -6
  33. package/client.js +321 -0
  34. package/cordis.patch.yml +55 -3
  35. package/docs/adr/0005-component-surface.md +58 -13
  36. package/docs/adr/0006-mission-shapes.md +306 -0
  37. package/docs/architecture/overview.md +18 -10
  38. package/docs/examples/first-mission.md +3 -2
  39. package/docs/faq.md +9 -7
  40. package/docs/guide/install-upgrade-migration.md +58 -7
  41. package/docs/guide/quickstart.md +20 -10
  42. package/docs/guide/scenarios-and-philosophy.md +35 -28
  43. package/docs/operations/runbook.md +1 -1
  44. package/docs/operations/security.md +11 -10
  45. package/docs/reference/blockers.md +20 -11
  46. package/docs/reference/component-surface.md +20 -8
  47. package/docs/reference/config.generated.md +8 -5
  48. package/docs/reference/config.md +30 -5
  49. package/docs/reference/doctor.md +7 -7
  50. package/docs/reference/errors.generated.md +1 -0
  51. package/docs/reference/errors.md +2 -1
  52. package/docs/reference/operations.md +1 -1
  53. package/docs/reference/shapes.md +123 -0
  54. package/docs/reference/surface-budget.md +53 -4
  55. package/docs/reference/testing.md +5 -3
  56. package/docs/reference/tools.generated.md +7 -7
  57. package/docs/reference/tools.md +5 -5
  58. package/docs/zh/faq.md +7 -5
  59. package/docs/zh/guide/install-upgrade-migration.md +25 -6
  60. package/docs/zh/guide/quickstart.md +17 -10
  61. package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
  62. package/docs/zh/operations/security.md +9 -8
  63. package/docs/zh/reference/blockers.md +13 -9
  64. package/docs/zh/reference/config.md +22 -5
  65. package/docs/zh/reference/errors.md +2 -1
  66. package/docs/zh/reference/testing.md +5 -4
  67. package/docs/zh/reference/tools.md +3 -3
  68. package/domain/batch.js +75 -105
  69. package/domain/collab/batch.js +30 -10
  70. package/domain/collab/spawn.js +53 -16
  71. package/domain/components.js +12 -4
  72. package/domain/config/schema.js +87 -22
  73. package/domain/contracts/outputs.js +2 -1
  74. package/domain/contracts/tool-inputs/missions.js +10 -2
  75. package/domain/errors.js +6 -0
  76. package/domain/evidence.js +283 -0
  77. package/domain/gate.js +17 -34
  78. package/domain/integration/gate.js +17 -43
  79. package/domain/integration/init.js +3 -2
  80. package/domain/integration/provision.js +51 -0
  81. package/domain/integration/rebase.js +16 -1
  82. package/domain/integration/review.js +203 -9
  83. package/domain/mission.js +16 -33
  84. package/domain/missions/completion.js +46 -8
  85. package/domain/missions/findings.js +2 -1
  86. package/domain/missions/lifecycle.js +131 -35
  87. package/domain/missions/plan.js +30 -6
  88. package/domain/missions/review.js +2 -18
  89. package/domain/planning.js +5 -5
  90. package/domain/read/missionContext.js +17 -3
  91. package/domain/read/views.js +5 -1
  92. package/domain/roles.js +222 -0
  93. package/domain/roster.js +9 -3
  94. package/domain/scope.js +51 -0
  95. package/domain/shapes/build.js +254 -0
  96. package/domain/shapes/index.js +171 -0
  97. package/domain/shapes/survey.js +445 -0
  98. package/domain/state/archive.js +28 -4
  99. package/domain/state/migrations.js +111 -4
  100. package/domain/state/schema.js +69 -7
  101. package/domain/state/semantics.js +93 -13
  102. package/domain/state.js +5 -1
  103. package/domain/summary.js +5 -11
  104. package/domain/vocabulary.js +15 -33
  105. package/host/capability-registry.js +25 -9
  106. package/host/config.js +40 -3
  107. package/host/doctor.js +20 -8
  108. package/host/preset-survey-sandbox.js +19 -0
  109. package/host/preset-survey.js +18 -0
  110. package/host/presets.js +17 -11
  111. package/host/tool-registry.js +23 -13
  112. package/locale/en.json +16 -1
  113. package/locale/preset-survey/en.json +6 -0
  114. package/locale/preset-survey/zh.json +6 -0
  115. package/locale/preset-survey-sandbox/en.json +6 -0
  116. package/locale/preset-survey-sandbox/zh.json +6 -0
  117. package/locale/zh.json +16 -1
  118. package/package.json +19 -2
  119. package/ports/state.js +8 -1
  120. package/scripts/architecture-waivers.json +3 -3
  121. package/scripts/check-components.mjs +120 -6
  122. package/scripts/check-docs.mjs +1 -1
  123. package/scripts/check-pack.mjs +5 -0
  124. package/scripts/component-surface.json +83 -29
  125. package/scripts/generate-capability-locale.mjs +5 -0
  126. package/scripts/generate-shapes-reference.mjs +171 -0
  127. package/scripts/mount-smoke/surface.mjs +3 -0
  128. package/scripts/pack-manifest.txt +22 -0
  129. package/scripts/shape-audit.mjs +361 -0
  130. package/scripts/surface-baseline.json +75 -4
  131. package/scripts/types-coverage.mjs +7 -0
  132. package/survey-sandbox.js +48 -0
  133. package/survey.js +40 -0
  134. package/tools/complete.js +5 -5
  135. package/tools/diff.js +5 -3
  136. package/tools/plan.js +1 -1
  137. package/tools/preflight.js +3 -2
  138. package/tools/review.js +1 -1
  139. package/tools/spawn.js +48 -50
  140. package/tools/status.js +8 -1
  141. package/tools/test.js +2 -1
  142. package/types/index.d.ts +76 -5
@@ -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, read-only-survey, stale-base, out-of-scope, base-dirty, branch-owned-by-closed-missions, and mission/worker guards). Review blockers (no-review, review-hold, not-clean, review-not-approved, tip-moved, review-mission-mismatch) apply only when review.requireReview is on. Never changes anything.',
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/review.js CHANGED
@@ -13,7 +13,7 @@ export function registerReviewTool(ctx, deps) {
13
13
  define(ctx, {
14
14
  name: 'tower_review',
15
15
  description:
16
- 'Submit an independent review verdict bound to the current branch tip: a completed mission starts a review round, and while the mission is active a later route may still record a verdict for that same reviewed tip, appending to the same round without changing the status (a moved tip is rejected with an explanation). status is "clean", "p1-Nitems", or "p2-Nitems"; merge is "merge", "fix-then-merge", or "hold" (only clean + merge is later approval). Non-clean returns the mission to active. Reviewers are capped at 5 rounds per branch; a tower-submitted review is break-glass and requires breakGlassReason.',
16
+ 'Submit an independent review verdict bound to the current branch tip: a completed mission starts a review round, and while the mission is active a later route may still record a verdict for that same reviewed tip, appending to the same round without changing the status (a moved tip is rejected with an explanation). status is "clean", "p1-Nitems", or "p2-Nitems"; merge is "merge", "fix-then-merge", or "hold" (only clean + merge is later approval). Non-clean returns the mission to active. Reviewers are capped at 5 rounds per branch; a tower-submitted review is break-glass and requires breakGlassReason. The tool is not registered when review.mode is off.',
17
17
  parameters: ReviewInput,
18
18
  aliases: {"missionId":["id"]},
19
19
  requiresMissionId: true,
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 { DEFAULT_ROLE, ROLES as ROLE_ENUM, SPAWN_OUTCOME, isCompletedStopReason, isReviewerRole, isSpawnAdmission, isWorkerRole } from '../domain/vocabulary.js'
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, buildSpawnBriefing, waitForSpawnResult } from '../app/spawn-support.js'
13
-
14
- /**
15
- * @param {any} ctx
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 preset; reviewers require a completed mission and use the read-only tower-reviewer preset. Provisions (or reuses) the mission git worktree and branch, generates a self-contained briefing, and starts a background subagent in that worktree. The child final report is delivered back to this session; tower_roster shows authoritative activity. Records the agent in the roster; reviewer spawns record the branch tip so a reviewer that modifies the branch cannot submit a review. Returns immediately unless wait=true.',
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
- assertSpawnableMission({ role, context })
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 provisioned = await tower.provision({
79
- missionId: args.missionId,
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 !== false,
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 (isWorkerRole(role) && isSpawnAdmission(tower.config.tests?.admission)) {
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: worktree.path,
106
- branch: worktree.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: worktree.path,
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: worktree.path,
131
- branch: worktree.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
- created: provisioned.created,
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/status.js CHANGED
@@ -4,6 +4,7 @@ import { classifyError } from '../domain/errors.js'
4
4
  import { StatusInput } from '../domain/contracts/tool-inputs/index.js'
5
5
  import { ROSTER_INACTIVE_STATES, isMarkdownFormat, isReservedState, isWorkerRole } from '../domain/vocabulary.js'
6
6
  import { archiveView } from '../app/archive.js'
7
+ import { reviewPolicyView } from '../domain/integration/review.js'
7
8
 
8
9
  /**
9
10
  * @param {any} ctx
@@ -21,7 +22,7 @@ export function registerStatusTool(ctx, deps) {
21
22
  define(ctx, {
22
23
  name: 'tower_status',
23
24
  description:
24
- 'Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, archive count, config path and the activity tail. Also reports the repo-scoped git lock holder (`gitLock: {held,pid,phase,heldMs}`) so a merge holding the mutex is visible. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report; archived missions are loaded from their year file); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; includeArchived appends compact archived summaries after the active page; format:"markdown" returns the human-readable mission board (active missions plus an archived count). Read-only.',
25
+ 'Read the Tower dashboard for a repository: state path, base branch, revision, mission status counts, paged mission summaries (scope/deps/tasks/worktree/owner), roster size, findings, archive count, config path and the activity tail. Also reports the repo-scoped git lock holder (`gitLock: {held,pid,phase,heldMs}`) so a merge holding the mutex is visible, and the resolved review policy (`review: {mode, available, required, detail}`; mode off means review is intentionally unavailable). Pass missionId for one full mission (tasks, notes, reviews, findings, completion report; archived missions are loaded from their year file); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; includeArchived appends compact archived summaries after the active page; format:"markdown" returns the human-readable mission board (active missions plus an archived count). Read-only.',
25
26
  parameters: StatusInput,
26
27
  aliases: { mission_id: ['missionId'], include_gate: ['includeGate'], include_archived: ['includeArchived'] },
27
28
  output: output(SCHEMAS.status),
@@ -38,6 +39,10 @@ export function registerStatusTool(ctx, deps) {
38
39
  (entry) => isWorkerRole(entry.role) && !ROSTER_INACTIVE_STATES.includes(entry.state) && !isReservedState(entry.state),
39
40
  ).length
40
41
  const spawn = spawnBackoffView(tower.root, tower.config.spawn?.maxLiveWorkers, inflight)
42
+ // #M39: the resolved review policy (`{mode, available, required, detail}`)
43
+ // is a first-class dashboard fact. In `off` the detail says review is
44
+ // intentionally unavailable rather than leaving it as a silent absence.
45
+ const review = reviewPolicyView(tower.config)
41
46
  if (args.missionId) {
42
47
  const [status, detail] = await Promise.all([
43
48
  tower.status({ missionIds: [args.missionId] }),
@@ -51,6 +56,7 @@ export function registerStatusTool(ctx, deps) {
51
56
  boardPath,
52
57
  mode,
53
58
  spawn,
59
+ review,
54
60
  components,
55
61
  }
56
62
  }
@@ -96,6 +102,7 @@ export function registerStatusTool(ctx, deps) {
96
102
  boardPath,
97
103
  mode,
98
104
  spawn,
105
+ review,
99
106
  components,
100
107
  }
101
108
  if (isMarkdownFormat(args.format)) {
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
@@ -57,6 +57,7 @@ export type ErrorCode =
57
57
  | 'provider-cooldown'
58
58
  | 'preset-missing'
59
59
  | 'reviewer-prerequisite'
60
+ | 'review-disabled'
60
61
  | 'not-authorized'
61
62
  | 'workspace-busy'
62
63
  | 'worker-limit'
@@ -73,6 +74,15 @@ export type ErrorCode =
73
74
  | 'tool-internal'
74
75
  // generated:vocabulary:end
75
76
 
77
+ /**
78
+ * Review policy mode (#M39): `off` (review intentionally unavailable),
79
+ * `optional` (available, never required; default) or `required` (a clean review
80
+ * bound to the branch tip is a merge blocker). The deprecated
81
+ * `review.requireReview` boolean normalizes to this: true -> required,
82
+ * false -> optional.
83
+ */
84
+ export type ReviewMode = 'off' | 'optional' | 'required'
85
+
76
86
  /** One machine-readable gate/completion blocker: a stable code plus a detail. */
77
87
  export interface Blocker {
78
88
  code: string
@@ -139,11 +149,25 @@ export interface RebaseRecord {
139
149
  baseTip?: string
140
150
  }
141
151
 
142
- export interface Mission {
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 {
143
168
  id: string
144
169
  title: string
145
170
  status: MissionStatus
146
- kind: MissionKind
147
171
  scope: string[]
148
172
  deps: string[]
149
173
  /**
@@ -155,22 +179,59 @@ export interface Mission {
155
179
  blockers?: string[]
156
180
  reviewPolicy?: ReviewPolicy
157
181
  reviewExemptionReason?: string | null
158
- worktree?: MissionWorktree | null
159
182
  owner?: string | null
160
183
  notes?: string[]
161
184
  completionReport?: CompletionReport | null
162
185
  testRuns?: TestRun[]
163
186
  rebase?: RebaseRecord | null
164
- mergedCommit?: string | null
165
- mergedAt?: number | null
166
187
  scopeGeneration?: number
167
188
  ownerGeneration?: number
168
189
  pauseCause?: string | null
169
190
  updatedAt?: number
170
191
  references?: string[]
171
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
198
+ }
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
172
231
  }
173
232
 
233
+ export type Mission = BuildMission | SurveyMission
234
+
174
235
  /**
175
236
  * Compact archived-mission entry (`archive/index.json`). It keeps enough of the
176
237
  * mission for ordering, dependency lookups and id allocation without loading the
@@ -245,6 +306,10 @@ export interface RosterEntry {
245
306
  missionId?: string | null
246
307
  parentSessionId?: string | null
247
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
248
313
  stale?: boolean
249
314
  [key: string]: unknown
250
315
  }
@@ -316,6 +381,12 @@ export interface MissionSummary {
316
381
  reviewPolicy: ReviewPolicy
317
382
  mergedCommit: string | null
318
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
319
390
  /** Size of the completion report without loading its body; null when absent. */
320
391
  reportBytes: number | null
321
392
  updatedAt?: number