dsh-tower 2.4.2 → 2.6.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 (108) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/COMPATIBILITY.md +2 -0
  3. package/CONTRIBUTING.md +5 -3
  4. package/GLOSSARY.md +73 -0
  5. package/PRINCIPLES.md +15 -7
  6. package/README.en.md +16 -15
  7. package/README.md +18 -10
  8. package/app/archive.js +16 -1
  9. package/app/board.js +16 -1
  10. package/app/gate.js +14 -0
  11. package/app/review.js +7 -0
  12. package/app/spawn-prechecks.js +30 -5
  13. package/app/tower.js +5 -5
  14. package/client.js +321 -0
  15. package/cordis.patch.yml +26 -9
  16. package/docs/adr/0004-surface-budget.md +10 -4
  17. package/docs/adr/0005-component-surface.md +222 -11
  18. package/docs/architecture/overview.md +12 -6
  19. package/docs/examples/first-mission.md +3 -2
  20. package/docs/faq.md +9 -7
  21. package/docs/guide/install-upgrade-migration.md +53 -7
  22. package/docs/guide/quickstart.md +91 -10
  23. package/docs/guide/release.md +60 -19
  24. package/docs/guide/scenarios-and-philosophy.md +35 -28
  25. package/docs/operations/security.md +11 -10
  26. package/docs/reference/blockers.md +12 -7
  27. package/docs/reference/component-surface.md +40 -21
  28. package/docs/reference/config.generated.md +6 -8
  29. package/docs/reference/config.md +36 -8
  30. package/docs/reference/doctor.md +11 -5
  31. package/docs/reference/errors.generated.md +1 -0
  32. package/docs/reference/errors.md +2 -1
  33. package/docs/reference/surface-budget.md +47 -12
  34. package/docs/reference/testing.md +1 -1
  35. package/docs/reference/tools.generated.md +5 -5
  36. package/docs/reference/tools.md +5 -5
  37. package/docs/zh/faq.md +7 -5
  38. package/docs/zh/guide/install-upgrade-migration.md +26 -7
  39. package/docs/zh/guide/quickstart.md +17 -10
  40. package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
  41. package/docs/zh/operations/security.md +9 -8
  42. package/docs/zh/reference/blockers.md +11 -7
  43. package/docs/zh/reference/config.md +28 -8
  44. package/docs/zh/reference/doctor.md +7 -2
  45. package/docs/zh/reference/errors.md +2 -1
  46. package/docs/zh/reference/testing.md +2 -2
  47. package/docs/zh/reference/tools.md +6 -5
  48. package/domain/component-rules.js +216 -0
  49. package/domain/components.js +181 -53
  50. package/domain/config/schema.js +86 -28
  51. package/domain/contracts/outputs.js +2 -0
  52. package/domain/contracts/tool-inputs/missions.js +1 -1
  53. package/domain/errors.js +6 -0
  54. package/domain/gate.js +23 -5
  55. package/domain/integration/review.js +203 -9
  56. package/domain/scope.js +176 -5
  57. package/domain/surface.js +21 -7
  58. package/host/capability-archive.js +18 -0
  59. package/host/capability-board.js +18 -0
  60. package/host/capability-registry.js +4 -0
  61. package/host/config.js +40 -3
  62. package/host/doctor.js +68 -12
  63. package/host/presets.js +26 -8
  64. package/host/tool-registry.js +30 -5
  65. package/index.js +17 -6
  66. package/locale/archive/en.json +6 -0
  67. package/locale/archive/zh.json +6 -0
  68. package/locale/batch/en.json +1 -1
  69. package/locale/batch/zh.json +1 -1
  70. package/locale/board/en.json +6 -0
  71. package/locale/board/zh.json +6 -0
  72. package/locale/en.json +37 -22
  73. package/locale/hmr/en.json +2 -2
  74. package/locale/hmr/zh.json +1 -1
  75. package/locale/import/en.json +1 -1
  76. package/locale/import/zh.json +1 -1
  77. package/locale/leases/en.json +2 -2
  78. package/locale/leases/zh.json +2 -2
  79. package/locale/preset-reviewer/en.json +1 -1
  80. package/locale/preset-reviewer/zh.json +1 -1
  81. package/locale/preset-worker/en.json +1 -1
  82. package/locale/preset-worker/zh.json +1 -1
  83. package/locale/skills/en.json +1 -1
  84. package/locale/skills/zh.json +1 -1
  85. package/locale/tests/en.json +2 -2
  86. package/locale/tests/zh.json +2 -2
  87. package/locale/zh.json +36 -21
  88. package/package.json +17 -2
  89. package/scripts/check-components.mjs +698 -44
  90. package/scripts/check-pack.mjs +3 -0
  91. package/scripts/component-surface.json +101 -37
  92. package/scripts/generate-capability-locale.mjs +3 -0
  93. package/scripts/mount-smoke/surface.mjs +2 -0
  94. package/scripts/pack-manifest.txt +10 -0
  95. package/scripts/release-notes.mjs +256 -0
  96. package/scripts/surface-baseline.json +39 -3
  97. package/scripts/surface-baseline.mjs +15 -5
  98. package/scripts/types-coverage.mjs +7 -0
  99. package/skills.js +4 -1
  100. package/tools/complete.js +3 -1
  101. package/tools/preflight.js +1 -1
  102. package/tools/review.js +1 -1
  103. package/tools/spawn.js +7 -7
  104. package/tools/status.js +8 -1
  105. package/tools/test.js +7 -7
  106. package/types/index.d.ts +10 -0
  107. package/ui-bundle/locale/en.json +1 -1
  108. package/ui-bundle/locale/zh.json +1 -1
@@ -8,8 +8,9 @@
8
8
  // tools every tool the Host entry registers, including the
9
9
  // tower_mode/tower_control pair that index.js registers
10
10
  // directly rather than through define() (#118)
11
- // components user-visible rows in cordis.patch.yml (#120), so the 2.3.0
12
- // 4 -> 1 contraction and any future move need a written decision
11
+ // components user-visible rows in every shipped cordis.patch.yml (#120,
12
+ // #134), so the 2.3.0 4 -> 1 contraction and any future move
13
+ // need a written decision
13
14
  //
14
15
  // `scripts/surface-baseline.json` freezes those counts. A PR that grows a count
15
16
  // fails here until the baseline carries a decision with either an ADR under
@@ -24,7 +25,7 @@ import fs from 'node:fs'
24
25
  import path from 'node:path'
25
26
  import { fileURLToPath } from 'node:url'
26
27
  import { configSurface } from '../domain/config/schema.js'
27
- import { parseCordisPatch } from '../domain/components.js'
28
+ import { COMPONENT_PATCH_FILES, mergeCordisPatches, parseCordisPatch } from '../domain/components.js'
28
29
  import { SURFACE_CLASSIFICATION_FILE, SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
29
30
  import { loadShippedToolSurface } from './lib/tool-surface.mjs'
30
31
  import { checkSurfaceBudget } from './lib/surface.mjs'
@@ -42,7 +43,8 @@ with a decision that names an existing docs/adr/ ADR, deletions that are no
42
43
  longer in the live surface, or a measurement-correction record with a reason and
43
44
  date. The classification table in
44
45
  ${SURFACE_CLASSIFICATION_FILE} must cover every key and tool exactly once, and
45
- the component count comes from the visible rows in cordis.patch.yml.`
46
+ the component count comes from the visible rows in every shipped
47
+ cordis.patch.yml.`
46
48
 
47
49
  /**
48
50
  * The live tool names: apply the shipped Host entry so every registration path
@@ -65,7 +67,15 @@ function liveToolNames() {
65
67
  export function liveSurface(root = PACKAGE_ROOT) {
66
68
  const config = configSurface()
67
69
  const tools = liveToolNames()
68
- const patch = parseCordisPatch(fs.readFileSync(path.join(root, 'cordis.patch.yml'), 'utf8'))
70
+ // #134: the counted component surface is every shipped patch, not only the
71
+ // root one; `ui-bundle/cordis.patch.yml` ships the tower-ui row.
72
+ const patches = {}
73
+ for (const file of COMPONENT_PATCH_FILES) {
74
+ const patchFile = path.join(root, file)
75
+ if (!fs.existsSync(patchFile)) continue
76
+ patches[file] = parseCordisPatch(fs.readFileSync(patchFile, 'utf8'))
77
+ }
78
+ const patch = mergeCordisPatches(patches)
69
79
  return {
70
80
  configKeys: config.count,
71
81
  sections: config.sections.length,
@@ -14,6 +14,9 @@
14
14
  // changing the tsconfig is enough and this gate follows.
15
15
  //
16
16
  // Excluded tiers are data with a reason, not silent gaps:
17
+ // - `client.js` is the core Web Client half (#M40): a browser artifact checked
18
+ // by tests/plugin-config-client.test.mjs and the client rules, not by the
19
+ // Node tsc program;
17
20
  // - `ui-bundle/**` is the optional Web client: a browser artifact with DOM
18
21
  // types and no Node runtime, checked by tests/ui-bundle.test.mjs and the
19
22
  // architecture client rules instead of the Node tsc program;
@@ -30,6 +33,10 @@ const WALK_SKIP = new Set(['node_modules', '.git', '.dsh-tower-worktrees', 'cove
30
33
 
31
34
  /** Shipped `.js` tiers that are deliberately outside the Node tsc program. */
32
35
  export const NOT_CHECKED_TIERS = [
36
+ {
37
+ pattern: 'client.js',
38
+ reason: 'core Web client: browser artifact, checked by tests/plugin-config-client.test.mjs',
39
+ },
33
40
  {
34
41
  pattern: 'ui-bundle/**',
35
42
  reason: 'optional Web client: browser artifact, checked by tests/ui-bundle.test.mjs and the client architecture rules',
package/skills.js CHANGED
@@ -66,5 +66,8 @@ export function apply(ctx) {
66
66
  // lets tower_status/tower_doctor report "disabled by component tower-skills"
67
67
  // when the row is off instead of leaving the missing catalog unexplained.
68
68
  holdComponent(ctx, 'tower-skills')
69
- ctx.skills.registerProvider(() => provider)
69
+ // #139: the provider must be unregistered with the plugin, so the catalog stops
70
+ // serving after unload/reload instead of keeping a dead provider alive. Host
71
+ // registries may or may not return a disposer; `ctx.effect` accepts both.
72
+ ctx.effect(() => ctx.skills.registerProvider(() => provider))
70
73
  }
package/tools/complete.js CHANGED
@@ -31,7 +31,9 @@ export function registerCompleteTool(ctx, deps) {
31
31
  const context = await tower.missionContext(args.missionId)
32
32
  const checks = [...(Array.isArray(args.checks) ? args.checks : [])]
33
33
  const testsConfig = /** @type {any} */ (tower.config.tests ?? {})
34
- if (testsConfig.enabled !== false && isCompleteAdmission(testsConfig.admission)) {
34
+ // #136: the `tower-tests` component row is the only switch; admission
35
+ // decides when discovery runs, not whether it may run.
36
+ if (isCompleteAdmission(testsConfig.admission)) {
35
37
  const discovery = await runMissionDiscovery(tower, { id: args.missionId, worktree: context.mission.worktree })
36
38
  if (discovery.configured && discovery.receipt.ok !== true) {
37
39
  throw new TowerToolError('tests-failed', `test discovery failed for ${args.missionId}: ${discovery.receipt.error}`)
@@ -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, 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.mode is required; review.mode off and optional skip them. Never changes anything.',
16
16
  parameters: PreflightInput,
17
17
  aliases: {"missionId":["id"]},
18
18
  requiresMissionId: true,
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
@@ -24,8 +24,8 @@ import { buildAgentOptions, buildSpawnBriefing, waitForSpawnResult } from '../ap
24
24
  * recordSpawnThrottle: any,
25
25
  * recordSpawnSuccess: any,
26
26
  * runMissionDiscovery: any,
27
- * textOf: any,
28
- * inspectLeases: any,
27
+ * textOf: any, inspectLeases: any,
28
+ * leaseComponent?: () => { enabled: boolean, reason: string },
29
29
  * }} deps
30
30
  */
31
31
  export function registerSpawnTool(ctx, deps) {
@@ -37,7 +37,7 @@ export function registerSpawnTool(ctx, deps) {
37
37
  define(ctx, {
38
38
  name: 'tower_spawn',
39
39
  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.',
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, and the reviewer role is refused when review.mode is off. 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
41
  parameters: SpawnInput,
42
42
  aliases: {"missionId":["id"],"waitMs":["wait_ms"]},
43
43
  requiresMissionId: true,
@@ -54,7 +54,7 @@ export function registerSpawnTool(ctx, deps) {
54
54
  const preset = isReviewerRole(role) ? config.spawn.reviewerPreset : config.spawn.workerPreset
55
55
  const model = isReviewerRole(role) ? config.spawn.reviewerModel : config.spawn.workerModel
56
56
  const context = await tower.missionContext(args.missionId)
57
- assertSpawnableMission({ role, context })
57
+ assertSpawnableMission({ role, context, config })
58
58
  const spawned = await enqueueSpawn(tower.root, async () => {
59
59
  const observed = await observeActivity(ctx, tower)
60
60
  await tower.reconcileObservedInactive(observed.activity)
@@ -71,6 +71,8 @@ export function registerSpawnTool(ctx, deps) {
71
71
  })
72
72
  const { subagents, provider } = await precheckSpawnServices({
73
73
  tower, ctx, config, preset, actor, exec, inspectLeases: deps.inspectLeases,
74
+ // #131: the app layer only needs the row fact; the root owns the registry.
75
+ leaseComponent: deps.leaseComponent,
74
76
  })
75
77
 
76
78
  const provisioned = await tower.provision({
@@ -134,9 +136,7 @@ export function registerSpawnTool(ctx, deps) {
134
136
  if (classifyError(error).retryable === true) {
135
137
  recordSpawnThrottle(tower.root, config.spawn.maxLiveWorkers)
136
138
  }
137
- // The child may already be running even when recordSpawn failed: stop
138
- // it and mark the reserved entry failed so it cannot keep writing
139
- // unobserved.
139
+ // The child may already be running: stop it and fail the reserved entry.
140
140
  if (agentId) {
141
141
  try {
142
142
  await Promise.resolve(subagents.interrupt(agentId, { kind: 'ancestor', agent: exec.agent }))
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
@@ -37,13 +37,13 @@ export function registerTestTool(ctx, deps) {
37
37
  await tower.assertWorktree(context.mission.worktree)
38
38
  const discovery = await runMissionDiscovery(tower, context.mission)
39
39
  if (!discovery.configured) {
40
- return {
41
- ok: false,
42
- configured: false,
43
- error: discovery.error ?? 'tower.testDiscovery is not configured in the worktree package.json',
44
- tests: [],
45
- commands: [],
46
- }
40
+ // #136: the `tower-tests` row is the switch. When the row is off, say so
41
+ // and say how to turn it back on; never report the repository as
42
+ // "not configured" for a component cause.
43
+ const error = discovery.disabledBy
44
+ ? `test discovery is unavailable: ${discovery.reason ?? `disabled by component ${discovery.disabledBy}`}; enable the ${discovery.disabledBy} component in the plugin page and restart DSH`
45
+ : discovery.error ?? 'tower.testDiscovery is not configured in the worktree package.json'
46
+ return { ok: false, configured: false, error, tests: [], commands: [] }
47
47
  }
48
48
  const receipt = discovery.receipt
49
49
  return {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Tower UI",
4
- "description": "Shows Tower tool cards, a composer-dock tower summary, and a transient Tower status strip."
4
+ "description": "Adds Tower cards for tool results, a compact status summary above the message box, and a short status strip, in the web client. When off: The web interface piece is not loaded: no Tower cards, status summary or status strip. The Tower host, missions, state and the board keep working."
5
5
  }
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Tower 界面",
4
- "description": "显示 Tower 工具卡片、输入框区域的 Tower 状态摘要,以及短暂的 Tower 状态提示条。"
4
+ "description": "在 Web 客户端中显示 Tower 工具结果卡片、输入框上方的简要状态摘要,以及简短的状态提示条。关闭后:界面插件不再加载,不再有 Tower 卡片、状态摘要或提示条。Tower 核心、任务线、状态与看板继续工作。"
5
5
  }
6
6
  }