dsh-tower 2.5.0 → 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.
- package/CHANGELOG.md +32 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +1 -1
- package/GLOSSARY.md +6 -0
- package/PRINCIPLES.md +15 -7
- package/README.en.md +16 -15
- package/README.md +12 -10
- package/app/review.js +7 -0
- package/app/spawn-prechecks.js +12 -2
- package/app/tower.js +5 -5
- package/client.js +321 -0
- package/cordis.patch.yml +5 -3
- package/docs/adr/0005-component-surface.md +58 -13
- package/docs/architecture/overview.md +12 -6
- package/docs/examples/first-mission.md +3 -2
- package/docs/faq.md +9 -7
- package/docs/guide/install-upgrade-migration.md +32 -7
- package/docs/guide/quickstart.md +20 -10
- package/docs/guide/scenarios-and-philosophy.md +35 -28
- package/docs/operations/security.md +11 -10
- package/docs/reference/blockers.md +12 -7
- package/docs/reference/component-surface.md +14 -7
- package/docs/reference/config.generated.md +3 -2
- package/docs/reference/config.md +29 -3
- package/docs/reference/errors.generated.md +1 -0
- package/docs/reference/errors.md +2 -1
- package/docs/reference/surface-budget.md +20 -3
- package/docs/reference/testing.md +1 -1
- package/docs/reference/tools.generated.md +5 -5
- package/docs/reference/tools.md +3 -3
- package/docs/zh/faq.md +7 -5
- package/docs/zh/guide/install-upgrade-migration.md +25 -6
- package/docs/zh/guide/quickstart.md +17 -10
- package/docs/zh/guide/scenarios-and-philosophy.md +23 -15
- package/docs/zh/operations/security.md +9 -8
- package/docs/zh/reference/blockers.md +11 -7
- package/docs/zh/reference/config.md +21 -3
- package/docs/zh/reference/errors.md +2 -1
- package/docs/zh/reference/testing.md +2 -2
- package/docs/zh/reference/tools.md +3 -3
- package/domain/components.js +12 -4
- package/domain/config/schema.js +40 -5
- package/domain/contracts/outputs.js +2 -0
- package/domain/contracts/tool-inputs/missions.js +1 -1
- package/domain/errors.js +6 -0
- package/domain/gate.js +6 -3
- package/domain/integration/review.js +203 -9
- package/host/config.js +40 -3
- package/host/doctor.js +18 -7
- package/host/presets.js +9 -8
- package/host/tool-registry.js +9 -1
- package/package.json +7 -1
- package/scripts/check-components.mjs +120 -6
- package/scripts/check-pack.mjs +3 -0
- package/scripts/component-surface.json +16 -10
- package/scripts/pack-manifest.txt +1 -0
- package/scripts/surface-baseline.json +8 -1
- package/scripts/types-coverage.mjs +7 -0
- package/tools/preflight.js +1 -1
- package/tools/review.js +1 -1
- package/tools/spawn.js +2 -2
- package/tools/status.js +8 -1
- package/types/index.d.ts +10 -0
package/scripts/check-pack.mjs
CHANGED
|
@@ -16,6 +16,9 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
16
16
|
const REQUIRED_FILES = [
|
|
17
17
|
'cordis.patch.yml',
|
|
18
18
|
'index.js',
|
|
19
|
+
// #M40: the core Web Client half carries the Plugins-page review policy
|
|
20
|
+
// control; it is part of the published bundle like ui-bundle/.
|
|
21
|
+
'client.js',
|
|
19
22
|
'locale/en.json',
|
|
20
23
|
'locale/zh.json',
|
|
21
24
|
// #121: every optional capability row and both preset-declaring rows explain
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"date": "2026-
|
|
3
|
+
"date": "2026-11-01",
|
|
4
4
|
"adr": "docs/adr/0005-component-surface.md",
|
|
5
5
|
"classes": {
|
|
6
6
|
"core": "Removing it breaks the Tower contract. It is never a switch.",
|
|
7
7
|
"optional-component": "A capability whose off state leaves the rest of Tower intact. It must be a user-visible switch with a documented fallback.",
|
|
8
8
|
"optional-config": "A tuning knob, not a capability. It stays in the config form and never hides a component.",
|
|
9
|
-
"forbidden-to-disable": "The host, the health contract, the recovery escape
|
|
9
|
+
"forbidden-to-disable": "The host, the health contract, the recovery escape and the merge gates. No group, row or boolean key may turn it off."
|
|
10
10
|
},
|
|
11
11
|
"components": [
|
|
12
12
|
{
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"id": "preset-tower-reviewer",
|
|
89
89
|
"scope": "visible",
|
|
90
90
|
"kind": "row",
|
|
91
|
-
"fallback": "Declared by dsh-tower/preset-reviewer: the reviewer cannot be dispatched. Supported while review.
|
|
91
|
+
"fallback": "Declared by dsh-tower/preset-reviewer: the reviewer cannot be dispatched. Supported while review.mode is off or optional (merge is unaffected; off does not use the preset at all); with review.mode required, planning/mode entry is locked with an explanatory error."
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"id": "tower-ui",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
],
|
|
100
100
|
"gateableTools": {},
|
|
101
101
|
"behaviorKnobs": [
|
|
102
|
+
"review.mode",
|
|
102
103
|
"spawn.requireLease",
|
|
103
104
|
"tests.admission"
|
|
104
105
|
],
|
|
@@ -221,10 +222,15 @@
|
|
|
221
222
|
"class": "optional-config",
|
|
222
223
|
"note": "Bounded wait for the cross-process mode lock."
|
|
223
224
|
},
|
|
225
|
+
{
|
|
226
|
+
"name": "review.mode",
|
|
227
|
+
"class": "core",
|
|
228
|
+
"note": "The review policy: off, optional (default) or required, selected by one tri-state control on the plugin-page configuration form. It is configuration, not a component row and not three mutually exclusive rows. off intentionally removes tower_review, refuses reviewer spawn and makes the merge gate ignore review; optional never blocks a merge; required makes a clean tip-bound review a merge blocker. The reviewer preset row stays a capability the mode may leave unused."
|
|
229
|
+
},
|
|
224
230
|
{
|
|
225
231
|
"name": "review.requireReview",
|
|
226
|
-
"class": "
|
|
227
|
-
"note": "
|
|
232
|
+
"class": "core",
|
|
233
|
+
"note": "Deprecated one-release boolean alias for review.mode (true -> required, false -> optional; default optional). It is policy configuration and a deprecated alias, not a switch a row may own; false never overrides an explicit mode. Kept so 2.5 configurations keep working; remove after the 2.6 alias window."
|
|
228
234
|
},
|
|
229
235
|
{
|
|
230
236
|
"name": "review.maxRoundsPerReviewer",
|
|
@@ -435,11 +441,11 @@
|
|
|
435
441
|
},
|
|
436
442
|
{
|
|
437
443
|
"id": "review",
|
|
438
|
-
"capability": "the review
|
|
439
|
-
"reason": "
|
|
440
|
-
"tools": [
|
|
441
|
-
"rowIds": [
|
|
442
|
-
"gatedBy": ["review.
|
|
444
|
+
"capability": "the review gate in required mode",
|
|
445
|
+
"reason": "The policy is one tri-state configuration control on the plugin-page form, not a component row: review.mode is off, optional (default) or required, and off is a documented mode that removes tower_review and makes the merge gate ignore review. What no group, row or boolean key may switch off is the gate once the mode is required: a completed mission still needs a clean review bound to the branch tip, and the exemption path stays explicit. review.requireReview is a deprecated one-release alias, not a switch. The reviewer preset row remains a capability the policy may leave unused, never a way to weaken required mode.",
|
|
446
|
+
"tools": [],
|
|
447
|
+
"rowIds": [],
|
|
448
|
+
"gatedBy": ["review.mode", "review.maxRoundsPerReviewer", "gates.allowReviewExemption"]
|
|
443
449
|
},
|
|
444
450
|
{
|
|
445
451
|
"id": "merge-gates",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"surfaces": {
|
|
5
5
|
"configKeys": {
|
|
6
6
|
"seed": 64,
|
|
7
|
-
"value":
|
|
7
|
+
"value": 54,
|
|
8
8
|
"decisions": [
|
|
9
9
|
{
|
|
10
10
|
"from": 64,
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
"date": "2026-11-03",
|
|
13
13
|
"adr": "docs/adr/0005-component-surface.md",
|
|
14
14
|
"note": "Recorded contraction, not a free ratchet (#138). #121 removed the seven tools.expose* keys plus dev.hotReload, #136 replaced tests.enabled with the tower-tests row, and #133/#135 replaced docs.board/archive.enabled with the tower-board/tower-archive rows. The chain now continues here: the config-key surface is two-way like components, so any later increase or decrease needs its own dated decision."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"from": 53,
|
|
18
|
+
"to": 54,
|
|
19
|
+
"date": "2026-11-03",
|
|
20
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
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."
|
|
15
22
|
}
|
|
16
23
|
]
|
|
17
24
|
},
|
|
@@ -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/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, 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.
|
|
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
|
@@ -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)
|
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/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
|