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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Mission shapes (generated)
|
|
2
|
+
|
|
3
|
+
> Generated by `npm run docs:generate:shapes` from `domain/shapes/index.js`. Do not edit by hand.
|
|
4
|
+
|
|
5
|
+
A mission shape is the single place its kind's contract lives (#148/#149). Generic
|
|
6
|
+
layers resolve `shapeFor(mission.kind)` and never branch on a kind literal; the
|
|
7
|
+
shape audit (`npm run shape:audit`) refuses kind and role comparisons outside
|
|
8
|
+
`domain/shapes/`, the global `TERMINAL_STATUSES`/`TRANSITIONS` vocabulary,
|
|
9
|
+
one-argument `isTerminal`/`canTransition` calls, and the dimension regressions
|
|
10
|
+
recorded in the inventory below (review tip, batch status, lease ledger, blocked
|
|
11
|
+
writes, evidence reads, legacy vocabulary, path permission and role/shape
|
|
12
|
+
conflation).
|
|
13
|
+
|
|
14
|
+
## Kinds
|
|
15
|
+
|
|
16
|
+
| Kind | Statuses | Has terminal | Provisioning | Can merge | Can rebase | Reach (owns / fence) | Component | Accepts result |
|
|
17
|
+
|---|---|---|---|---|---|---|---|---|
|
|
18
|
+
| `build` | `planned`, `active`, `completed`, `blocked`, `paused`, `merged`, `abandoned` | yes | `branch-worktree` | true | true | true / `scope` | — | false |
|
|
19
|
+
| `survey` | `planned`, `active`, `completed`, `blocked`, `paused`, `abandoned` | yes | `shared` / `detached` | false | false | false / `read-only` | tower-survey | true |
|
|
20
|
+
|
|
21
|
+
## Transitions
|
|
22
|
+
|
|
23
|
+
### build
|
|
24
|
+
|
|
25
|
+
| From | To |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `planned` | `active`, `blocked`, `abandoned` |
|
|
28
|
+
| `active` | `completed`, `blocked`, `paused`, `abandoned` |
|
|
29
|
+
| `completed` | `active`, `blocked`, `abandoned` |
|
|
30
|
+
| `blocked` | `active`, `abandoned` |
|
|
31
|
+
| `paused` | `active`, `blocked`, `abandoned` |
|
|
32
|
+
| `merged` | terminal |
|
|
33
|
+
| `abandoned` | terminal |
|
|
34
|
+
|
|
35
|
+
### survey
|
|
36
|
+
|
|
37
|
+
| From | To |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `planned` | `active`, `blocked`, `abandoned` |
|
|
40
|
+
| `active` | `completed`, `blocked`, `paused`, `abandoned` |
|
|
41
|
+
| `completed` | terminal |
|
|
42
|
+
| `blocked` | `active`, `abandoned` |
|
|
43
|
+
| `paused` | `active`, `blocked`, `abandoned` |
|
|
44
|
+
| `abandoned` | terminal |
|
|
45
|
+
|
|
46
|
+
## Survey placement (#148 W2)
|
|
47
|
+
|
|
48
|
+
`survey.sandbox` is tuning, not a component: the `tower-survey` row stays the
|
|
49
|
+
switch. The shape resolves both the placement and the preset, so generic layers
|
|
50
|
+
never read the raw key or compare a kind.
|
|
51
|
+
|
|
52
|
+
The decision is made once, at provisioning time. A mission with a recorded
|
|
53
|
+
`mission.sandbox` keeps the detached placement and its completion invariants
|
|
54
|
+
even if `survey.sandbox` later changes — a config flip can neither skip the
|
|
55
|
+
sandbox HEAD/zero-commit checks nor strand the disposable checkout. A survey
|
|
56
|
+
that never got a sandbox keeps the shared invariants for the same reason.
|
|
57
|
+
|
|
58
|
+
| `survey.sandbox` | Provisioning | Preset | Completion invariants |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| `none` (default) | `shared`: no checkout, the agent reads the main checkout | `tower-survey` (strictly read-only: no write, edit, or shell tool) | HEAD equals `readCommit`, tracked-status fingerprint unchanged, zero commits |
|
|
61
|
+
| `detached` | `detached`: `git worktree add --detach <worktreeRoot>/<id>-sandbox <readCommit>` (no branch, no `baseCommit`, never merged) | `tower-survey-sandbox` (read/write/edit/pwsh plus the mission tools; spawn/merge/rebase/review/diff still absent) | sandbox HEAD equals `readCommit`, zero commits; disposable write/shell side effects are ignored; sandbox removed on completion, abandon or teardown with a bounded retry |
|
|
62
|
+
|
|
63
|
+
## Completion invariants
|
|
64
|
+
|
|
65
|
+
| Kind | Invariants | Check |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `build` | requiresWorktree=true, requiresCommits=true, requiresClean=true, requiresHeadMatch=false, requiresTrackedStatus=false | `checkCompletion` |
|
|
68
|
+
| `survey` | requiresWorktree=false, requiresSandbox=false, requiresCommits=false, requiresClean=false, requiresHeadMatch=true, requiresTrackedStatus=true | `checkCompletion` |
|
|
69
|
+
|
|
70
|
+
## Role
|
|
71
|
+
|
|
72
|
+
| Contract | Owner |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Status, transitions, terminality, merge/rebase, provisioning, reach, plan fields, completion, dependencies, briefing, summary | `domain/shapes/<kind>.js` |
|
|
75
|
+
| Kind resolution and the action guard | `domain/shapes/index.js` |
|
|
76
|
+
| Role vocabulary and the role contract (preset, model, placement, briefing, prerequisites) | `domain/roles.js` |
|
|
77
|
+
| Generated reference and the audit | `scripts/generate-shapes-reference.mjs`, `scripts/shape-audit.mjs` |
|
|
78
|
+
|
|
79
|
+
## Dimension inventory (#149)
|
|
80
|
+
|
|
81
|
+
Every dimension the registry audit covers. A **shape-owned** verdict lives in
|
|
82
|
+
`domain/shapes/<kind>.js`; a **correctly generic** verdict has one owner module
|
|
83
|
+
outside the shapes; a **redesigned** verdict replaced an earlier ambiguous
|
|
84
|
+
contract. The fitness rule is the mechanical check the audit (or the generated
|
|
85
|
+
invariant) enforces.
|
|
86
|
+
|
|
87
|
+
| Dimension | Verdict | Owner module | Fitness rule |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| Mission lifecycle | **Shape-owned.** Statuses, transitions and terminality are per kind. | `domain/shapes/<kind>.js` | `kind-literal`, `global-status-vocabulary`, `shape-entry-point` |
|
|
90
|
+
| Role vocabulary | **Correctly generic.** Roles are actors, not mission kinds. | `domain/roles.js` | `role-literal` |
|
|
91
|
+
| Provisioning | **Shape-owned.** A build gets a branch worktree; a survey is shared or a detached sandbox. | `domain/shapes/<kind>.js` | Generated invariant: only a build creates a branch/worktree; a recorded `mission.sandbox` decides a survey's placement. |
|
|
92
|
+
| Read-only completion | **Shape-owned.** `resolveCompletion` derives invariants from the recorded placement. | `domain/shapes/survey.js` | Generated invariant: shared survey keeps HEAD and tracked status; detached survey keeps HEAD pinned; both make zero commits. |
|
|
93
|
+
| Dependency | **Shape-owned.** `dependencySatisfied` names the terminal statuses that satisfy a dependent. | `domain/shapes/<kind>.js` | Generated invariant: a completed survey satisfies a dependent build; a merged build does too. |
|
|
94
|
+
| Archive / reanchor / planning | **Shape-owned.** Terminality is the one archive/reanchor predicate. | `domain/shapes/<kind>.js` | Generated invariant: a terminal shape leaves open work and never blocks reanchor. |
|
|
95
|
+
| Completion result | **Shape-owned.** Only a survey may record a completion result. | `domain/shapes/<kind>.js` | Generated invariant: `shape.acceptsResult`. |
|
|
96
|
+
| Required component | **Shape-owned.** A survey needs the `tower-survey` row. | `domain/shapes/<kind>.js` | Generated invariant: planning refuses with an actionable code while the required component is off. |
|
|
97
|
+
| Review | **Redesigned.** Commit-stamped evidence: a reviewer reads a disposable checkout at the recorded review commit; only a shape with a branch is reviewable. | `app/review.js`, `app/reviewCheckout.js`, `domain/missions/review.js` | `review-tip-read` — A review never reads a live worktree HEAD; verdicts are stamped with a recorded commit. |
|
|
98
|
+
| Batch | **Correctly generic second lifecycle.** A batch is finite evidence over missions, not a mission kind; no shape knows about batches. | `domain/batch.js`, `domain/collab/batch.js`, `app/batch.js` | `batch-status-literal` — Batch status literals live in domain/batch.js; every other layer uses its constants. |
|
|
99
|
+
| Leases | **Correctly generic.** One fail-closed admission point; expiry is recovered only by an explicit `force` release. | `adapters/leases.js`, `app/spawn-prechecks.js` | `lease-ledger` — Only adapters/leases.js reads or writes the lease ledger. |
|
|
100
|
+
| Paused vs blocked | **Redesigned.** `blocked` is derived machine state; `paused` is a deliberate stop with a cause. | `domain/missions/lifecycle.js`, `domain/state/semantics.js` | `blocked-status-write` — blocked is derived from open blockers; only domain/missions/lifecycle.js writes it. |
|
|
101
|
+
| Test receipts / evidence | **Redesigned — one reader.** Completion checks and discovery receipts are one normalized evidence contract in `domain/evidence.js`; the shape declares whether a receipt is tip-bound. `tower_complete` no longer copies a synthetic `tests` check into the report. | `domain/evidence.js`; `domain/shapes/<kind>.js` declares `evidence` | `evidence-single-read` — Completion checks and test receipts have one reader: domain/evidence.js. |
|
|
102
|
+
| Legacy import | **Redesigned — strict adapter.** One payload is one declared format; status remaps, assumed kinds and non-carried fields are reported as `remapped`/`unresolved`/`dropped` instead of approximated. Import stays read-only and never resumes a paused mission. | `adapters/legacy.js`; `domain/batch.js` reads only the canonical `pausedMissionIds` | `legacy-vocabulary` — One live vocabulary: legacy formats/statuses are read only by adapters/legacy.js. |
|
|
103
|
+
| Scope ownership | **Redesigned — shape.reach.** The shape owns path permission (`reach.ownsPaths`, `reach.fence`); the gate, planner, lifecycle and auto-deps derive from it through one fence. | `domain/scope.js` + `shape.reach` in `domain/shapes/<kind>.js` | `scope-reach` — Path permission derives from shape.reach through domain/scope.js; no module owns or re-implements it. |
|
|
104
|
+
| Role vs kind | **Redesigned — orthogonal role contract.** Spawn, preset, model, placement, briefing, roster budget and gates resolve the role through `domain/roles.js`; the shape only supplies facts. | `domain/roles.js`; callers `tools/spawn.js`, `app/spawn-prechecks.js`, `app/spawn-support.js`, `domain/roster.js`, `host/presets.js` | `role-shape-conflation` — A role decision never combines with a shape decision; role facts live in domain/roles.js. |
|
|
105
|
+
|
|
106
|
+
## Fitness rules
|
|
107
|
+
|
|
108
|
+
Generated from `scripts/shape-audit.mjs` `RULES`; the audit is wired into `npm run check`.
|
|
109
|
+
|
|
110
|
+
| Rule | Owner module | Contract |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `kind-literal` | `domain/shapes/<kind>.js` | Generic layers never compare mission.kind with a literal. |
|
|
113
|
+
| `role-literal` | `domain/roles.js` | Generic layers use the vocabulary role predicates, never a role literal. |
|
|
114
|
+
| `global-status-vocabulary` | `domain/shapes/<kind>.js` | No module reaches for the global TERMINAL_STATUSES/TRANSITIONS tables. |
|
|
115
|
+
| `shape-entry-point` | `domain/shapes/index.js` | isTerminal(kind, status) and canTransition(kind, from, to) are the only entry points. |
|
|
116
|
+
| `review-tip-read` | `app/reviewCheckout.js` | A review never reads a live worktree HEAD; verdicts are stamped with a recorded commit. |
|
|
117
|
+
| `batch-status-literal` | `domain/batch.js` | Batch status literals live in domain/batch.js; every other layer uses its constants. |
|
|
118
|
+
| `lease-ledger` | `adapters/leases.js` | Only adapters/leases.js reads or writes the lease ledger. |
|
|
119
|
+
| `blocked-status-write` | `domain/missions/lifecycle.js` | blocked is derived from open blockers; only domain/missions/lifecycle.js writes it. |
|
|
120
|
+
| `evidence-single-read` | `domain/evidence.js` | Completion checks and test receipts have one reader: domain/evidence.js. |
|
|
121
|
+
| `legacy-vocabulary` | `adapters/legacy.js` | One live vocabulary: legacy formats/statuses are read only by adapters/legacy.js. |
|
|
122
|
+
| `scope-reach` | `domain/scope.js` | Path permission derives from shape.reach through domain/scope.js; no module owns or re-implements it. |
|
|
123
|
+
| `role-shape-conflation` | `domain/roles.js` | A role decision never combines with a shape decision; role facts live in domain/roles.js. |
|
|
@@ -84,6 +84,36 @@ plugin-page form, not a row and not one row per mode; the review gate in
|
|
|
84
84
|
non-disableable. The component and config-key counts do not move (11 visible
|
|
85
85
|
components, 54 keys).
|
|
86
86
|
|
|
87
|
+
#148/#149 grow the surface by exactly one key and one visible component,
|
|
88
|
+
recorded in [ADR 0006](../adr/0006-mission-shapes.md): config keys 54 → 55,
|
|
89
|
+
sections stay 14, components 11 → 12. A first cut of the shape work added three
|
|
90
|
+
keys and a `survey` section, then contracted 57 → 55 before release in a second
|
|
91
|
+
dated decision: a survey's preset is shape-owned
|
|
92
|
+
(`SHAPES.survey.spawn.defaultPreset`), so a `spawn.surveyPreset` key could only
|
|
93
|
+
disagree with the registry, and `survey.sandbox` advertised a placement W1 does
|
|
94
|
+
not implement. What remains is `spawn.surveyModel`, the optional model route for
|
|
95
|
+
survey workers (the survey row owns the capability, not the key name). The
|
|
96
|
+
`tower-survey` row is the switch: off means surveys cannot be planned and no
|
|
97
|
+
survey agent is dispatched; build missions are unaffected. No existing key
|
|
98
|
+
changed class.
|
|
99
|
+
|
|
100
|
+
#148 W2 implements the detached survey sandbox, so it restores exactly the
|
|
101
|
+
placement key that W1 correctly withheld: `survey.sandbox` (`none`|`detached`,
|
|
102
|
+
default `none`) returns as tuning, and `gates.requireFreshSurvey` (default
|
|
103
|
+
`false`) joins it as merge-gate strictness; config keys 55 → 57 and sections
|
|
104
|
+
14 → 15. It
|
|
105
|
+
is not a capability: the `tower-survey` row stays the single switch and the
|
|
106
|
+
sandbox preset is an internal child of it, so the visible component count does
|
|
107
|
+
not move (12). `spawn.surveyPreset` stays removed: the shape resolves the preset
|
|
108
|
+
from `survey.sandbox`, so the key could still only disagree with the registry.
|
|
109
|
+
|
|
110
|
+
#149 W4 contracts the config surface by one: config keys 57 → 56. `tests.autoChecks`
|
|
111
|
+
only chose whether a discovery projection was copied into the completion report,
|
|
112
|
+
and that copy was the second evidence shape the W4 audit removed. The receipt in
|
|
113
|
+
`mission.testRuns` is the one evidence record now, so the switch had no meaning
|
|
114
|
+
left; it is deleted rather than kept as a dead tuning key. Sections and the
|
|
115
|
+
visible component count do not move.
|
|
116
|
+
|
|
87
117
|
## Config keys
|
|
88
118
|
|
|
89
119
|
| Key | Class | Note |
|
|
@@ -116,6 +146,8 @@ components, 54 keys).
|
|
|
116
146
|
| `git.timeoutMs` | tuning | Default Git command timeout outside merge. |
|
|
117
147
|
| `worktrees.root` | tuning | Default worktree placement. |
|
|
118
148
|
| `spawn.workerPreset` | core | Chooses the tool surface a worker receives. |
|
|
149
|
+
| `spawn.surveyModel` | tuning | Optional model route for survey agents. |
|
|
150
|
+
| `survey.sandbox` | tuning | Placement only: shared read-only checkout (default) or a disposable detached sandbox. The tower-survey row is still the switch. |
|
|
119
151
|
| `spawn.reviewerPreset` | core | Chooses the read-only reviewer surface. |
|
|
120
152
|
| `spawn.maxDepth` | tuning | Subagent depth requested at spawn. |
|
|
121
153
|
| `spawn.provider` | tuning | Provider route for spawned agents. |
|
|
@@ -130,11 +162,11 @@ components, 54 keys).
|
|
|
130
162
|
| `tests.admission` | core | Chooses when repository code is executed. |
|
|
131
163
|
| `tests.timeoutMs` | tuning | Per-command discovery timeout. |
|
|
132
164
|
| `tests.maxBytes` | tuning | Captured discovery output cap. |
|
|
133
|
-
| `tests.autoChecks` | tuning | Attaches a tests check to completion. |
|
|
134
165
|
| `tests.allowPackageDiscovery` | core | Allows repository code to execute; a security contract. |
|
|
135
166
|
| `gates.requireChecks` | core | Requires completion evidence before merge. |
|
|
136
167
|
| `gates.requireTests` | core | Requires a test receipt before merge. |
|
|
137
168
|
| `gates.allowReviewExemption` | core | Allows a review exemption and its reason. |
|
|
169
|
+
| `gates.requireFreshSurvey` | core | Promotes the survey-stale dependency warning to a merge blocker; `false` keeps it advisory. |
|
|
138
170
|
| `gates.maxReportBytes` | tuning | Report size cap; the refusal itself is not configurable. |
|
|
139
171
|
| `docs.boardLocation` | tuning | Chooses where the board is written; whether it is written is the tower-board row. |
|
|
140
172
|
| `archive.afterDays` | tuning | Archive age threshold; archival itself is the tower-archive row. |
|
|
@@ -48,8 +48,10 @@ overall `ok`. `state.missions[].testRuns` keeps the most recent 50 receipts. The
|
|
|
48
48
|
discovery fails.
|
|
49
49
|
|
|
50
50
|
- `tower_test({repoRoot,missionId})` runs discovery on demand and records a receipt.
|
|
51
|
-
- `
|
|
52
|
-
|
|
51
|
+
- The receipt in `testRuns` is the only evidence shape. `tower_complete` does not copy a `tests`
|
|
52
|
+
check into the completion report; completion checks, batch close and `gates.requireChecks` read the
|
|
53
|
+
same receipt.
|
|
54
|
+
- `gates.requireChecks` requires any recorded check or receipt; `gates.requireTests` requires a passing receipt
|
|
53
55
|
for the current mission branch tip (`tests-missing`). Review is independent of this gate: with
|
|
54
56
|
`review.mode:optional` (the default) or `off`, the tests gate still checks the branch tip.
|
|
55
57
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Tools overview (generated)
|
|
2
2
|
|
|
3
3
|
> Generated by `npm run docs:generate:tools` from the registered tool definitions. Do not edit by hand.
|
|
4
|
-
> Surface: config keys:
|
|
4
|
+
> Surface: config keys: 56 · sections: 15 · tools: 29 · baseline 2026-10-23. Generated from `scripts/surface-baseline.json`; do not edit by hand.
|
|
5
5
|
|
|
6
6
|
| Tool | Description |
|
|
7
7
|
|---|---|
|
|
8
8
|
| `tower_adopt` | Reconcile the roster with the durable subagent catalog after a restart or new session. Agents that are provably gone are marked retired (records are kept) or missing (retireMissing:false, so the tower can inspect/take over); agents whose parent catalog cannot be read stay unknown and are never changed. Only the tower may adopt. |
|
|
9
9
|
| `tower_batch` | Manage finite evidence batches: create freezes missionIds/requiredChecks/receiptPaths, status summarizes members, close verifies every required check and receipt hash, abandon retires a stale open batch. Membership and exclusions are immutable after create; close/abandon never remove missions. |
|
|
10
|
-
| `tower_complete` | 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. |
|
|
10
|
+
| `tower_complete` | 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. |
|
|
11
11
|
| `tower_control` | Enter or leave Tower mode for the calling agent. Allowed only when the deployment enables allowAgentControl; use it when the user intent or task state clearly calls for Tower orchestration or a return to normal collaboration, and always state a concrete reason. |
|
|
12
12
|
| `tower_diff` | Read-only view of a mission branch for review: merge base, changed files, commits, and a capped patch against the recorded base. Safe for reviewers; it never modifies the repository. |
|
|
13
13
|
| `tower_doctor` | Read-only environment and repository health check: the concept-budget surface (config keys/sections/tools), git, state schema, case-sensitivity detection (caseInsensitive and the active scope comparison mode), config, presets/subagents/HMR, the state lock age/heartbeat freshness, the repo-scoped git lock holder (gitLock: {held,pid,phase,heldMs}), workspace lock, roster staleness, worktree ownership, test discovery, leases, and the terminal-mission archive (count, bytes, years, unreadable diagnostics and the configured afterDays/maxMissions policy). Returns checks with pass/warn/fail, state-lock and git-lock reports and an environment contract. Never writes state and never probes the filesystem. |
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
| `tower_merge` | Merge a mission branch into the recorded base branch with --no-ff, after the hard gate passes. Verifies the two merge parents and records the mission as merged. If the merge commit already landed (manual merge, crashed or slow pre-merge hook), it detects the merge commit and records it as reconciled instead of merging again. A failed gate returns stable blocker codes without merging; a hook failure aborts cleanly and returns "merge-hook-failed" while a real content conflict returns "merge-conflict". |
|
|
22
22
|
| `tower_mission` | Update one mission: lifecycle status, decision note, blocker, clear blockers, mark a task done, drop a task with a reason, or widen the scope (tower only). "merged" is never set here; only a future tower_merge records it. Merged and abandoned are terminal. |
|
|
23
23
|
| `tower_mode` | Report whether Tower mode is active for the calling agent, who changed it last, whether the model may toggle it, any goal paused on entry, the workspace lock, HMR availability and the plugin version. |
|
|
24
|
-
| `tower_plan` | 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. |
|
|
25
|
-
| `tower_preflight` | Read-only merge gate for one mission: returns ok plus stable blocker codes (branch-missing, deps-unmerged,
|
|
24
|
+
| `tower_plan` | 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. |
|
|
25
|
+
| `tower_preflight` | 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. |
|
|
26
26
|
| `tower_rebase` | Rebase a mission branch onto the current base tip inside its own worktree. Refuses while its worker is running or the worktree is dirty. A clean conflict-free rebase waives re-review for the old review; a conflict is aborted and the mission is marked blocked. If the branch is already contained in the base, it returns alreadyMerged and leaves the branch untouched so tower_merge can record the landed merge. |
|
|
27
27
|
| `tower_recover` | Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner with its age, heartbeat age and staleness; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. force:true additionally clears a live-but-silent PID (a recycled PID) and still renames the record to a .recovered-<ts> backup. It never resets Tower state or worktrees. |
|
|
28
28
|
| `tower_reload` | Dev-only: ask DSH HMR to reconcile this profile and reload the Tower Host plugin from disk. Registered only while the tower-hmr component is on (the row is shipped off). The reload is scheduled a few milliseconds after this call returns (so the result reaches the model first). Use after editing the plugin source in a linked profile; plugin_manager install/update already triggers reconciliation by itself. Fails with reload-unavailable when the HMR service is absent. |
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
| `tower_review` | 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. |
|
|
31
31
|
| `tower_roster` | List tower roster agents with authoritative live activity from the subagent catalog (running/inactive/missing/unknown). Read-only; never resumes children. Use before completion or merge decisions. |
|
|
32
32
|
| `tower_send` | Send a durable inbox message to a tower participant: a roster agent name, "tower", or "all" (broadcast). The message is persisted first; when the recipient is a currently running roster agent the tower also steers it into that child's current turn. Returns delivery results. |
|
|
33
|
-
| `tower_spawn` | Spawn a worker or reviewer for a mission. Workers use the tower-worker
|
|
33
|
+
| `tower_spawn` | 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. |
|
|
34
34
|
| `tower_status` | 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. |
|
|
35
35
|
| `tower_teardown` | Tear down eligible mission worktrees after their missions are merged or abandoned. A worktree whose roster agent is running is always kept (force cannot override); dirty worktrees need force; exclude keeps named missions. dry_run previews the remove/keep decisions without changing anything. Terminal missions whose worktree is gone and whose configured archive policy is due (archive.afterDays / archive.maxMissions) are moved to <git-common-dir>/dsh-tower/archive/ so state.json stops rewriting them; nothing is deleted. |
|
|
36
36
|
| `tower_test` | Run the mission worktree's declared tower.testDiscovery (package.json) and record the receipt. Returns ok:false with diagnostics when discovery fails; does not change mission status. Use tower_complete for automatic admission checks. |
|
package/docs/reference/tools.md
CHANGED
|
@@ -36,7 +36,7 @@ findings but not mutate them. Violations throw `[tower:not-authorized]` or
|
|
|
36
36
|
|---|---|---|
|
|
37
37
|
| `tower_init` | `base?`, `autoGitInit?`, `snapshotDirty?`, `reanchor?` | Create `<git-common-dir>/dsh-tower/state.json`. `autoGitInit` runs `git init` + empty initial commit; `snapshotDirty` records a dangling snapshot commit of uncommitted tracked changes used as the mission baseline; `reanchor` changes base only when no non-terminal mission exists. Returns `dirty`, `snapshotCommit`, `reanchored`. |
|
|
38
38
|
| `tower_plan` | `missions[]` (`title` — printable Unicode, NFC-normalized, ≤120 characters, `scope[]`, `tasks[]?`, `deps[]?`, `kind?`, `context?`, `references[]?` ≤20, `reviewPolicy?`, `reviewExemptionReason?`) | Create missions `M1..`; overlapping build scopes auto-add a dependency; rejects unknown deps and cycles. Mission ids stay ASCII (`M1`, `M2`); invisible characters and over-long titles are rejected with `invalid-argument`. `reviewPolicy:"none"` requires a non-empty reason; when the review gate is on, preflight also requires `gates.allowReviewExemption`. |
|
|
39
|
-
| `tower_mission` | `id` (alias `missionId`), `status?`, `task_done?`, `task_drop?`+`task_drop_reason?`, `blocker?`, `clear_blockers?`, `note?`, `scope?`, `pause_cause?`, `authorization?` | Update one mission. Worker-safe fields are task/blocker/note; `merged` cannot be set here. Paused missions record `pause_cause` (user/failure/resource/dependency); resuming a user-paused mission honors `mode.requireResumeAuthorization`. |
|
|
39
|
+
| `tower_mission` | `id` (alias `missionId`), `status?`, `task_done?`, `task_drop?`+`task_drop_reason?`, `blocker?`, `clear_blockers?`, `note?`, `scope?`, `pause_cause?`, `authorization?` | Update one mission. Worker-safe fields are task/blocker/note; `merged` cannot be set here. `blocked` is derived from an open blocker (record `blocker`, never `status:"blocked"`); `clear_blockers` resumes it to `active`. Paused missions record `pause_cause` (user/failure/resource/dependency); resuming a user-paused mission honors `mode.requireResumeAuthorization`. |
|
|
40
40
|
\|\ `tower_status`\ \|\ `missionId\?`,\ `offset\?`,\ `limit\?`,\ `includeGate\?`,\ `includeArchived\?`,\ `format\?`\ \|\ Dashboard\ by\ default\.\ Paged\ missions\ carry\ `reportBytes`\ only;\ `missionId`\ returns\ full\ detail\ \(tasks/notes/reviews/findings/completionReport\ body\),\ and\ an\ archived\ mission\ is\ loaded\ from\ its\ year\ file\.\ `offset`/`limit`\ page\ active\ missions\ \(default\ 20,\ max\ 100\),\ `includeGate`\ adds\ gate\ digests,\ `includeArchived`\ appends\ compact\ archived\ summaries\ \(flagged\ `archived:true`,\ no\ report\ body\)\ after\ the\ active\ page,\ and\ `format:"markdown"`\ returns\ the\ board\ \(active\ rows\ plus\ an\ archived\ count\)\.\ Includes\ `mode`\ with\ `lastChange`\ and\ recent\ `history`\ when\ the\ host\ mode\ hook\ is\ available\.\ `archive`\ reports\ `\{count,\ years,\ unreadable,\ message}`;\ a\ corrupt\ archive\ degrades\ to\ `unreadable:true`\ instead\ of\ failing\ the\ dashboard\.\ Also\ returns\ `gitLock`\ \(`\{held,\ pid,\ phase,\ heldMs}`\)\ for\ the\ repo-scoped\ git\ mutex,\ and\ `\{held:\ false}`\ when\ it\ is\ free\.\ \|
|
|
41
41
|
| `tower_spawn` | `missionId` (alias `id`), `role` (`worker`/`reviewer`), `name?`, `instructions?`, `worktreeRoot?`, `wait?`, `waitMs?`, `takeover?`, `includeDirtyBase?` | Provision/reuse the worktree and start a subagent. Worker uses `tower-worker`; reviewer requires a `completed` mission and uses the read-only `tower-reviewer`; with `review.mode:off` the reviewer role is refused with `[tower:review-disabled]`. `takeover:true` replaces a live worker entry that is no longer running (increments ownerGeneration). Only one live worker per mission; `spawn.maxLiveWorkers` caps the repository. |
|
|
42
42
|
| `tower_roster` | — | Roster with live activity (`running`/`inactive`/`missing`/`unknown`) and `stale` flags from the subagent catalog. Read-only. |
|
|
@@ -60,7 +60,7 @@ findings but not mutate them. Violations throw `[tower:not-authorized]` or
|
|
|
60
60
|
| `tower_recover` | `action` (`status`/`clear-stale-lock`), `confirm?`, `force?` | Recovery diagnostics plus an explicit stale state-lock repair. `clear-stale-lock` requires `confirm:"CLEAR_STALE_LOCK"` and only moves a lock whose PID is provably dead to a `.recovered-<ts>` backup. `force:true` additionally clears a live-but-silent PID (a recycled PID): the output names `{pid, startedAt, ageMs, heartbeatAgeMs}` and the record is still renamed, never unlinked. It never resets state/worktrees. |
|
|
61
61
|
| `tower_test` | `missionId` (alias `id`) | Run the worktree's `package.json` `tower.testDiscovery` and record a receipt; `ok:false` on failure. Use `tower_complete` for automatic admission checks. |
|
|
62
62
|
| `tower_batch` | `action` (`create`/`status`/`close`/`abandon`), `missionIds?`, `exclusions?`, `requiredChecks?`, `receiptPaths?`, `batchId?`, `checks?`, `treeTip?`, `receiptHashes?`, `reason?` | Finite evidence batch. Create freezes members/checks/receipts; close verifies every required check and receipt hash; merged members are eligible; abandon retires a stale open batch without deleting missions. Membership is immutable. |
|
|
63
|
-
| `tower_resource` | `action` (`acquire`/`release`/`status`), `name?`, `holder?`, `token?`, `ttlMs?`, `force?` | Advisory per-repository lease in `<common>/dsh-tower/leases.json`. No OS CPU/memory hard cap; expired
|
|
63
|
+
| `tower_resource` | `action` (`acquire`/`release`/`status`), `name?`, `holder?`, `token?`, `ttlMs?`, `force?` | Advisory per-repository lease in `<common>/dsh-tower/leases.json`. No OS CPU/memory hard cap. A live lease is released only by its holder token; an **expired** lease is recovered by an explicit `force: true` release (never taken over automatically). |
|
|
64
64
|
| `tower_import_legacy` | `sourcePath` | Read-only import of a Tower Codex `schemaVersion:1` or DSH legacy state. Full payload goes under `<common>/dsh-tower/legacy/<sha256>.json`; state stores a summary only. Never activates/adopts/resumes paused missions. |
|
|
65
65
|
| `tower_legacy_map` | `action` (`map`/`status`), `sourceHash?`, `oldId?`, `newMissionId?` | Audit-only legacy-id mapping; mappings are append-only and never adopt a mission. |
|
|
66
66
|
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
| `no-check-evidence` | 启用了 `gates.requireChecks` 且完成报告没有 `checks`。把 completed mission 退回 `active`,带 checks 重新调用 `tower_complete`,然后重新审查。 |
|
|
21
21
|
| `branch-missing` | 还没有 worktree/分支:先 spawn worker。 |
|
|
22
22
|
| `stale-base` | 分支不包含基线 tip:执行 `tower_rebase`。 |
|
|
23
|
-
| `read-only-survey` | survey mission 修改了文件;survey mission 必须只读。 |
|
|
24
23
|
| `out-of-scope` | 改动文件超出 mission scope;有意为之就扩大 scope,否则回退。在大小写敏感的检出上,仅大小写不同的路径仍会被拒绝,detail 会指出折叠大小写后本可匹配的 scope 条目。 |
|
|
25
24
|
| `base-dirty` | 主检出存在未提交改动,合并会覆盖它们。 |
|
|
26
25
|
| `deps-unmerged` | 依赖 mission 尚未满足:build 依赖须 `merged`/`abandoned`;survey 依赖在 `completed` 即视为满足(含归档记录)。 |
|
|
26
|
+
| `survey-stale` | build 依赖的 survey 的 `readCommit` 不是 build base 的祖先或同一提交。默认只是 `plan`/`preflight` 的 `warnings[]` 条目;`gates.requireFreshSurvey:true` 时同一 code 阻止合并。针对当前 base 重新规划 survey,或记录为何该读取仍然有效。 |
|
|
27
27
|
| `no-review` | `review.mode:required` 且分支上没有审查:spawn reviewer(或关闭审查门禁)。 |
|
|
28
28
|
| `review-mission-mismatch` | `review.mode:required` 且最新审查属于另一个 mission。 |
|
|
29
29
|
| `not-clean` | `review.mode:required` 且最新审查是 `p1-Nitems`/`p2-Nitems`;需要再来一轮(同一轮的各 route 结论会追加记录,不会让状态反复翻转)。 |
|
|
@@ -63,4 +63,4 @@
|
|
|
63
63
|
分支已包含在基线中时,`tower_rebase` 返回 `alreadyMerged: true`;此时调用 `tower_merge` 记录这次
|
|
64
64
|
已落地的合并。
|
|
65
65
|
|
|
66
|
-
稳定的 blocker code 共
|
|
66
|
+
稳定的 blocker code 共 31 个(25 个门禁 + 4 个合并失败 + 2 个 rebase 专属;`worker-busy` 与门禁共用)。
|
|
@@ -99,9 +99,8 @@ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON
|
|
|
99
99
|
| `tests.admission` | `off` | `spawn` 在启动 worker 前运行发现(fail-closed,不启动子代理);`complete` 在 `tower_complete` 时运行;`off` 只在显式 `tower_test` 时运行(默认;不会自动执行仓库代码)。 |
|
|
100
100
|
| `tests.timeoutMs` | `60000` | 每条发现命令的超时。 |
|
|
101
101
|
| `tests.maxBytes` | `16777216`(16 MiB) | 每条命令捕获的 stdout 上限。 |
|
|
102
|
-
| `tests.autoChecks` | `true` | 向完成报告附加自动 `tests` 检查。 |
|
|
103
102
|
| `tests.allowPackageDiscovery` | `true` | 允许仓库控制的 `tower.testDiscovery` 命令(会执行 worktree 代码;见安全页)。 |
|
|
104
|
-
| `gates.requireChecks` | `false` |
|
|
103
|
+
| `gates.requireChecks` | `false` | 合并前要求至少一条已记录检查(`tower_complete.checks`)或测试回执(`no-check-evidence`)。 |
|
|
105
104
|
| `gates.requireTests` | `false` | 要求针对当前 mission 分支 tip 的通过测试回执(`tests-missing`);与审查门禁独立。`tower-tests` 组件行关闭时,规划/进入模式会带解释拒绝该组合。 |
|
|
106
105
|
| `gates.allowReviewExemption` | `false` | 在 `review.mode:required` 时,允许用 `reviewPolicy:"none"` + 非空理由跳过审查 blocker;默认故意关闭;`review.mode` 为 `off` 或 `optional` 时无影响(不会产生审查 blocker)。 |
|
|
107
106
|
| `gates.maxReportBytes` | `32768`(32 KiB) | `tower_complete.report` 的 UTF-8 字节硬上限。超出时以 `invalid-argument` 拒绝并给出可执行的 `nextAction`(把全文写进 mission worktree 的文件并引用路径),绝不截断。已存在的超限报告仍可读取。 |
|
|
@@ -48,8 +48,9 @@ stdout/stderr 文件、SHA-256 哈希、退出码、计数、required 名称、w
|
|
|
48
48
|
`tower_complete` 会抛出 `[tower:tests-failed]`。
|
|
49
49
|
|
|
50
50
|
- `tower_test({repoRoot,missionId})` 按需运行发现并记录回执。
|
|
51
|
-
- `
|
|
52
|
-
|
|
51
|
+
- `testRuns` 中的回执是唯一的证据形态;`tower_complete` 不再向完成报告复制 `tests` 检查,完成检查、批次关闭
|
|
52
|
+
与 `gates.requireChecks` 读取同一份回执。
|
|
53
|
+
- `gates.requireChecks` 要求任意已记录检查或回执;`gates.requireTests` 要求针对当前 mission 分支 tip 的
|
|
53
54
|
通过回执(`tests-missing`)。测试门禁与审查独立:即使策略为 `review.mode:optional`(默认)或
|
|
54
55
|
`off`,测试门禁仍检查分支 tip。
|
|
55
56
|
|
package/domain/batch.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Batch admission packets (2.1): pure create/close/summarize validation.
|
|
2
2
|
//
|
|
3
|
+
// A batch is a second, generic lifecycle layered over missions (#149 W3): it is
|
|
4
|
+
// not a mission shape and never gets a kind. Its whole lifecycle — statuses,
|
|
5
|
+
// transition edges, terminality, legacy-status normalization and the status
|
|
6
|
+
// vocabulary — lives in this module (see `BATCH_STATUS*`), so no other module
|
|
7
|
+
// compares or writes `batch.status` to a string literal.
|
|
8
|
+
//
|
|
3
9
|
// This module performs no filesystem, git, or subprocess I/O. The tower tool
|
|
4
10
|
// layer reads the worktree, receipt files, and test results, then passes what
|
|
5
11
|
// it observed into `validateClose`. Receipt hashes may be supplied by the
|
|
@@ -11,7 +17,7 @@
|
|
|
11
17
|
// state.missions[] -> { id, status, mergedCommit?, completionReport?, testRuns? }
|
|
12
18
|
// state.batches[] -> { id, status: 'open'|'closed'|'abandoned', missionIds,
|
|
13
19
|
// exclusions, requiredChecks, receiptPaths, receiptHashes? }
|
|
14
|
-
// state.legacySnapshots[] -> {
|
|
20
|
+
// state.legacySnapshots[] -> { sourceHash, pausedMissionIds: string[] }
|
|
15
21
|
// A batch record is the normalized validateCreate result plus `id`, `status`,
|
|
16
22
|
// `createdAt`, and the optional `receiptHashes` expected-hash map.
|
|
17
23
|
//
|
|
@@ -23,16 +29,64 @@
|
|
|
23
29
|
// `input.treeTip` to bind recorded check `treeCommit` values to the current
|
|
24
30
|
// HEAD.
|
|
25
31
|
import path from 'node:path'
|
|
32
|
+
import { findEvidence } from './evidence.js'
|
|
33
|
+
import { shapeFor } from './shapes/index.js'
|
|
26
34
|
|
|
27
35
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/i
|
|
28
36
|
const MAX_MISSIONS = 50
|
|
29
37
|
const MAX_CHECKS = 50
|
|
30
38
|
const MAX_RECEIPTS = 200
|
|
31
39
|
const MAX_EXCLUSIONS = 50
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
// The batch lifecycle contract (#149 W3). A batch is opened by tower_batch
|
|
42
|
+
// create and leaves `open` exactly once, to `closed` (every member verified) or
|
|
43
|
+
// `abandoned` (retired without verification). There is no third stop state and
|
|
44
|
+
// no per-shape variant.
|
|
45
|
+
export const BATCH_STATUS = Object.freeze({ OPEN: 'open', CLOSED: 'closed', ABANDONED: 'abandoned' })
|
|
46
|
+
/** @type {readonly string[]} */
|
|
47
|
+
export const BATCH_STATUSES = Object.freeze([BATCH_STATUS.OPEN, BATCH_STATUS.CLOSED, BATCH_STATUS.ABANDONED])
|
|
48
|
+
export const BATCH_TRANSITIONS = Object.freeze({
|
|
49
|
+
[BATCH_STATUS.OPEN]: Object.freeze([BATCH_STATUS.CLOSED, BATCH_STATUS.ABANDONED]),
|
|
50
|
+
[BATCH_STATUS.CLOSED]: Object.freeze([]),
|
|
51
|
+
[BATCH_STATUS.ABANDONED]: Object.freeze([]),
|
|
52
|
+
})
|
|
53
|
+
// Pre-v5 documents (and hand-written fixtures) used synonyms for `open`; v5
|
|
54
|
+
// normalizes them once in the state migration and nothing else accepts them.
|
|
55
|
+
/** @type {readonly string[]} */
|
|
56
|
+
const LEGACY_OPEN_BATCH_STATUSES = Object.freeze(['created', 'active', 'pending'])
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {unknown} value
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function isBatchTerminal(value) {
|
|
63
|
+
return value === BATCH_STATUS.CLOSED || value === BATCH_STATUS.ABANDONED
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {unknown} from
|
|
68
|
+
* @param {unknown} to
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function canTransitionBatch(from, to) {
|
|
72
|
+
return (BATCH_TRANSITIONS[/** @type {string} */ (from)] ?? []).includes(/** @type {string} */ (to))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize a recorded batch status to the canonical vocabulary. An absent
|
|
77
|
+
* status means `open`; a legacy synonym means `open`; an unknown value is left
|
|
78
|
+
* as-is so validation can report it instead of silently rewriting it.
|
|
79
|
+
* @param {unknown} value
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function normalizeBatchStatus(value) {
|
|
83
|
+
if (value === undefined || value === null || value === '') return BATCH_STATUS.OPEN
|
|
84
|
+
const status = String(value)
|
|
85
|
+
if (BATCH_STATUSES.includes(status)) return status
|
|
86
|
+
if (LEGACY_OPEN_BATCH_STATUSES.includes(status)) return BATCH_STATUS.OPEN
|
|
87
|
+
return status
|
|
88
|
+
}
|
|
89
|
+
|
|
36
90
|
|
|
37
91
|
/** Stable batch error; add `batch-invalid` to ERROR_CODES to map it directly. */
|
|
38
92
|
export class BatchError extends Error {
|
|
@@ -123,28 +177,13 @@ function normalizeReceiptPathOrNull(value, where) {
|
|
|
123
177
|
/** Legacy snapshots imported by adapters/legacy.js; only paused missions may be excluded. */
|
|
124
178
|
function pausedLegacyIds(state) {
|
|
125
179
|
const ids = new Set()
|
|
126
|
-
const collectMission = (mission) => {
|
|
127
|
-
if (isPlainObject(mission) && nonEmptyString(mission.id) && mission.status === 'paused') {
|
|
128
|
-
ids.add(mission.id.trim())
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const collectList = (value) => {
|
|
132
|
-
if (Array.isArray(value)) value.forEach(collectMission)
|
|
133
|
-
else if (isPlainObject(value)) Object.values(value).forEach(collectMission)
|
|
134
|
-
}
|
|
135
180
|
const snapshots = Array.isArray(state.legacySnapshots) ? state.legacySnapshots : []
|
|
136
181
|
for (const snapshot of snapshots) {
|
|
137
182
|
if (!isPlainObject(snapshot)) continue
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
// hand-written fixtures may use `pausedIds`.
|
|
143
|
-
for (const key of ['pausedIds', 'pausedMissionIds']) {
|
|
144
|
-
if (Array.isArray(snapshot[key])) {
|
|
145
|
-
for (const id of snapshot[key]) if (nonEmptyString(id)) ids.add(id.trim())
|
|
146
|
-
}
|
|
147
|
-
}
|
|
183
|
+
// #149 W4: `pausedMissionIds` is the one canonical field the legacy adapter
|
|
184
|
+
// writes. Reading any other shape here would be a second live vocabulary.
|
|
185
|
+
if (!Array.isArray(snapshot.pausedMissionIds)) continue
|
|
186
|
+
for (const id of snapshot.pausedMissionIds) if (nonEmptyString(id)) ids.add(id.trim())
|
|
148
187
|
}
|
|
149
188
|
return ids
|
|
150
189
|
}
|
|
@@ -153,82 +192,13 @@ function missionInOpenBatch(state, missionId) {
|
|
|
153
192
|
const batches = Array.isArray(state.batches) ? state.batches : []
|
|
154
193
|
for (const batch of batches) {
|
|
155
194
|
if (!isPlainObject(batch)) continue
|
|
156
|
-
const status = batch.status ??
|
|
157
|
-
if (
|
|
195
|
+
const status = normalizeBatchStatus(batch.status ?? BATCH_STATUS.OPEN)
|
|
196
|
+
if (isBatchTerminal(status)) continue
|
|
158
197
|
if (Array.isArray(batch.missionIds) && batch.missionIds.includes(missionId)) return batch
|
|
159
198
|
}
|
|
160
199
|
return null
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
function checkNames(entry) {
|
|
164
|
-
if (!isPlainObject(entry)) return []
|
|
165
|
-
for (const candidate of [entry.name, entry.id, entry.check]) {
|
|
166
|
-
if (nonEmptyString(candidate)) return [candidate.trim()]
|
|
167
|
-
}
|
|
168
|
-
// A nameless runDiscovery receipt stored in mission.testRuns is the built-in
|
|
169
|
-
// test tool evidence. It answers to `tests` (the tool-layer auto check name)
|
|
170
|
-
// and to `tower_test` (the synthetic name used by earlier integrations).
|
|
171
|
-
if (Array.isArray(entry.commands) && Array.isArray(entry.tests)) return ['tests', 'tower_test']
|
|
172
|
-
return []
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function checkOutcome(entry) {
|
|
176
|
-
if (!isPlainObject(entry)) return null
|
|
177
|
-
if (typeof entry.status === 'string') {
|
|
178
|
-
const status = entry.status.trim().toLowerCase()
|
|
179
|
-
if (PASS_STATUSES.has(status)) return true
|
|
180
|
-
if (FAIL_STATUSES.has(status)) return false
|
|
181
|
-
}
|
|
182
|
-
if (typeof entry.ok === 'boolean') return entry.ok
|
|
183
|
-
if (typeof entry.pass === 'boolean') return entry.pass
|
|
184
|
-
if (typeof entry.success === 'boolean') return entry.success
|
|
185
|
-
if (entry.exitCode !== undefined && entry.exitCode !== null) {
|
|
186
|
-
const exitCode = typeof entry.exitCode === 'number' ? entry.exitCode : Number(entry.exitCode)
|
|
187
|
-
if (Number.isFinite(exitCode)) return exitCode === 0
|
|
188
|
-
}
|
|
189
|
-
if (entry.error !== undefined && entry.error !== null && entry.error !== '') return false
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Find the most authoritative recorded outcome for `name` on one mission:
|
|
195
|
-
* completion report first, then testRuns newest-first, then caller-supplied
|
|
196
|
-
* `extraChecks` (optionally scoped by `missionId`).
|
|
197
|
-
*/
|
|
198
|
-
function findCheck(mission, name, extraChecks, missionId) {
|
|
199
|
-
const sources = []
|
|
200
|
-
const reportChecks = Array.isArray(mission?.completionReport?.checks) ? mission.completionReport.checks : []
|
|
201
|
-
for (let index = reportChecks.length - 1; index >= 0; index -= 1) {
|
|
202
|
-
sources.push({ source: 'completionReport', entry: reportChecks[index] })
|
|
203
|
-
}
|
|
204
|
-
const testRuns = Array.isArray(mission?.testRuns) ? mission.testRuns : []
|
|
205
|
-
for (let index = testRuns.length - 1; index >= 0; index -= 1) {
|
|
206
|
-
const run = testRuns[index]
|
|
207
|
-
if (!isPlainObject(run)) continue
|
|
208
|
-
if (Array.isArray(run.checks) && run.checks.length > 0) {
|
|
209
|
-
for (let inner = run.checks.length - 1; inner >= 0; inner -= 1) {
|
|
210
|
-
sources.push({ source: 'testRuns', entry: run.checks[inner] })
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
sources.push({ source: 'testRuns', entry: run })
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (let index = extraChecks.length - 1; index >= 0; index -= 1) {
|
|
217
|
-
const entry = extraChecks[index]
|
|
218
|
-
if (!isPlainObject(entry)) continue
|
|
219
|
-
const scope = entry.missionId ?? entry.mission_id
|
|
220
|
-
if (scope !== undefined && scope !== null && String(scope) !== missionId) continue
|
|
221
|
-
sources.push({ source: 'input', entry })
|
|
222
|
-
}
|
|
223
|
-
for (const source of sources) {
|
|
224
|
-
if (!checkNames(source.entry).includes(name)) continue
|
|
225
|
-
const outcome = checkOutcome(source.entry)
|
|
226
|
-
if (outcome === null) continue
|
|
227
|
-
return { outcome, source: source.source, entry: source.entry }
|
|
228
|
-
}
|
|
229
|
-
return null
|
|
230
|
-
}
|
|
231
|
-
|
|
232
202
|
function blocker(message, code = 'batch-invalid') {
|
|
233
203
|
return { code, message }
|
|
234
204
|
}
|
|
@@ -414,9 +384,9 @@ export function validateClose(state, batch, input = {}) {
|
|
|
414
384
|
const receiptPaths = receiptPathsRaw.map((value, index) => normalizeReceiptPath(value, `batch.receiptPaths[${index}]`))
|
|
415
385
|
|
|
416
386
|
const blockers = []
|
|
417
|
-
const status = batch.status ??
|
|
418
|
-
if (status ===
|
|
419
|
-
else if (status ===
|
|
387
|
+
const status = normalizeBatchStatus(batch.status ?? BATCH_STATUS.OPEN)
|
|
388
|
+
if (status === BATCH_STATUS.CLOSED) blockers.push(blocker(`batch ${batch.id ?? ''} is already closed`.trim()))
|
|
389
|
+
else if (status === BATCH_STATUS.ABANDONED) blockers.push(blocker(`batch ${batch.id ?? ''} is abandoned`.trim()))
|
|
420
390
|
|
|
421
391
|
const missions = Array.isArray(state.missions) ? state.missions : fail('state.missions must be an array')
|
|
422
392
|
const byId = new Map()
|
|
@@ -447,13 +417,13 @@ export function validateClose(state, batch, input = {}) {
|
|
|
447
417
|
results.push({ missionId, name, ok: true, outcome: 'skipped', source: 'abandoned' })
|
|
448
418
|
continue
|
|
449
419
|
}
|
|
450
|
-
const found =
|
|
420
|
+
const found = findEvidence(shapeFor(mission.kind).evidence, mission, name, { extraChecks, missionId })
|
|
451
421
|
if (!found) {
|
|
452
422
|
results.push({ missionId, name, ok: false, outcome: 'missing', source: null })
|
|
453
423
|
blockers.push(blocker(`mission ${missionId} has no recorded check "${name}"`, 'tests-missing'))
|
|
454
424
|
continue
|
|
455
425
|
}
|
|
456
|
-
if (found.
|
|
426
|
+
if (found.ok !== true) {
|
|
457
427
|
results.push({ missionId, name, ok: false, outcome: 'failed', source: found.source })
|
|
458
428
|
blockers.push(blocker(`mission ${missionId} check "${name}" did not pass`, 'tests-failed'))
|
|
459
429
|
continue
|
|
@@ -507,14 +477,14 @@ export function summarize(state, batch) {
|
|
|
507
477
|
if (!Array.isArray(receiptPathsRaw)) fail('batch.receiptPaths must be an array')
|
|
508
478
|
const receiptPaths = receiptPathsRaw.map((value, index) => normalizeReceiptPath(value, `batch.receiptPaths[${index}]`))
|
|
509
479
|
const exclusions = Array.isArray(batch.exclusions) ? batch.exclusions : []
|
|
510
|
-
const status = batch.status ??
|
|
480
|
+
const status = normalizeBatchStatus(batch.status ?? BATCH_STATUS.OPEN)
|
|
511
481
|
|
|
512
482
|
const missions = Array.isArray(state.missions) ? state.missions : fail('state.missions must be an array')
|
|
513
483
|
const byId = new Map()
|
|
514
484
|
for (const mission of missions) if (isPlainObject(mission) && nonEmptyString(mission.id)) byId.set(mission.id, mission)
|
|
515
485
|
|
|
516
486
|
const blockers = []
|
|
517
|
-
if (
|
|
487
|
+
if (isBatchTerminal(status)) blockers.push(blocker(`batch ${batch.id ?? ''} is already ${status}`.trim()))
|
|
518
488
|
const members = []
|
|
519
489
|
const checks = []
|
|
520
490
|
for (const missionId of missionIds) {
|
|
@@ -539,7 +509,7 @@ export function summarize(state, batch) {
|
|
|
539
509
|
checks.push({ missionId, name, ok: true, outcome: 'skipped', source: 'abandoned' })
|
|
540
510
|
continue
|
|
541
511
|
}
|
|
542
|
-
const found =
|
|
512
|
+
const found = findEvidence(shapeFor(mission.kind).evidence, mission, name, { missionId })
|
|
543
513
|
if (!found) {
|
|
544
514
|
checks.push({ missionId, name, ok: false, outcome: 'missing', source: null })
|
|
545
515
|
continue
|
|
@@ -547,8 +517,8 @@ export function summarize(state, batch) {
|
|
|
547
517
|
checks.push({
|
|
548
518
|
missionId,
|
|
549
519
|
name,
|
|
550
|
-
ok: found.
|
|
551
|
-
outcome: found.
|
|
520
|
+
ok: found.ok === true,
|
|
521
|
+
outcome: found.ok === true ? 'passed' : 'failed',
|
|
552
522
|
source: found.source,
|
|
553
523
|
})
|
|
554
524
|
}
|