dsh-tower 2.1.0-beta.1 → 2.3.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 +154 -0
- package/COMPATIBILITY.md +6 -1
- package/CONTRIBUTING.md +71 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +14 -5
- package/README.md +14 -5
- package/SECURITY.md +6 -0
- package/adapters/archive.js +279 -0
- package/adapters/caseSensitivity.js +66 -0
- package/adapters/default.js +51 -8
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +15 -6
- package/{lib → adapters}/legacy.js +20 -6
- package/adapters/persistence.js +543 -0
- package/adapters/stateFile.js +99 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +15 -7
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +18 -7
- package/app/archive.js +188 -0
- package/app/batch.js +9 -10
- package/app/board.js +4 -4
- package/app/checkCompletion.js +23 -11
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +32 -15
- package/app/init.js +59 -22
- package/app/merge.js +64 -51
- package/app/messages.js +7 -8
- package/app/mission.js +7 -5
- package/app/plan.js +20 -9
- package/app/provision.js +12 -14
- package/app/read.js +65 -6
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +17 -8
- package/app/roster.js +14 -15
- package/{lib → app}/spawn-prechecks.js +4 -3
- package/app/spawn.js +12 -13
- package/app/teardown.js +28 -8
- package/app/tower.js +121 -39
- package/docs/README.md +6 -2
- package/docs/adr/0001-ports-adapters-as-data.md +49 -0
- package/docs/adr/0002-single-state-json.md +50 -0
- package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
- package/docs/adr/0004-surface-budget.md +50 -0
- package/docs/adr/README.md +51 -0
- package/docs/agents/domain.md +38 -0
- package/docs/agents/issue-tracker.md +46 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +170 -37
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +18 -1
- package/docs/guide/release.md +1 -1
- package/docs/operations/incident.md +82 -0
- package/docs/operations/runbook.md +12 -3
- package/docs/reference/blockers.md +3 -3
- package/docs/reference/config.generated.md +5 -0
- package/docs/reference/config.md +4 -0
- package/docs/reference/doctor.md +104 -8
- package/docs/reference/errors.generated.md +41 -35
- package/docs/reference/errors.md +54 -40
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +128 -0
- package/docs/reference/tools.generated.md +9 -8
- package/docs/reference/tools.md +9 -8
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +13 -1
- package/docs/zh/reference/blockers.md +3 -3
- package/docs/zh/reference/config.md +4 -0
- package/docs/zh/reference/doctor.md +63 -2
- package/docs/zh/reference/errors.md +4 -2
- package/docs/zh/reference/tools.md +6 -6
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +51 -4
- package/domain/contracts/outputs.js +33 -3
- package/domain/contracts/tool-inputs/missions.js +2 -1
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -40
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +41 -12
- package/domain/integration/init.js +40 -4
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/mission.js +1 -1
- package/domain/missions/completion.js +72 -5
- package/domain/missions/lifecycle.js +11 -4
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/planning.js +6 -3
- package/domain/read/views.js +17 -1
- package/domain/report.js +9 -1
- package/domain/scope.js +83 -11
- package/domain/state/archive.js +257 -0
- package/domain/state/migrations.js +145 -0
- package/domain/state/schema.js +6 -0
- package/domain/state.js +154 -4
- package/domain/summary.js +2 -0
- package/domain/surface.js +248 -0
- package/host/doctor.js +555 -0
- package/{lib → host}/modeStore.js +1 -1
- package/{lib → host}/tool-registry.js +106 -31
- package/host/tower.js +32 -0
- package/index.js +12 -9
- package/package.json +10 -3
- package/ports/archive.js +18 -0
- package/ports/caseSensitivity.js +15 -0
- package/ports/clock.js +10 -2
- package/ports/config.js +19 -0
- package/ports/contract.js +38 -0
- package/ports/events.js +24 -0
- package/ports/ids.js +10 -2
- package/ports/index.js +54 -2
- package/ports/ownership.js +17 -0
- package/ports/state.js +16 -0
- package/ports/store.js +52 -3
- package/ports/vcs.js +25 -4
- package/scripts/architecture-waivers.json +19 -0
- package/scripts/architecture.mjs +634 -23
- package/scripts/check-docs.mjs +8 -35
- package/scripts/check-pack.mjs +68 -39
- package/scripts/check-philosophy.mjs +266 -0
- package/scripts/domain-metrics.mjs +13 -4
- package/scripts/generate-config-reference.mjs +14 -1
- package/scripts/generate-error-reference.mjs +68 -14
- package/scripts/generate-events-reference.mjs +83 -0
- package/scripts/generate-tools-reference.mjs +11 -0
- package/scripts/issue-audit.mjs +2 -2
- package/scripts/lib/surface.mjs +87 -0
- package/scripts/pack-manifest.txt +64 -19
- package/scripts/regression-audit.mjs +164 -0
- package/scripts/release-check.mjs +1 -1
- package/scripts/surface-baseline.json +21 -0
- package/scripts/surface-baseline.mjs +120 -0
- package/tools/adopt.js +2 -3
- package/tools/batch.js +2 -3
- package/tools/complete.js +3 -4
- package/tools/diff.js +3 -3
- package/tools/doctor.js +7 -10
- package/tools/finding.js +2 -3
- package/tools/findings.js +3 -3
- package/tools/import-legacy.js +3 -5
- package/tools/inbox.js +3 -3
- package/tools/index.js +7 -7
- package/tools/init.js +4 -7
- package/tools/interrupt.js +2 -3
- package/tools/legacy-map.js +3 -5
- package/tools/merge.js +3 -3
- package/tools/mission.js +2 -3
- package/tools/plan.js +7 -6
- package/tools/preflight.js +3 -3
- package/tools/rebase.js +3 -3
- package/tools/recover.js +38 -13
- package/tools/reload.js +3 -6
- package/tools/resource.js +3 -5
- package/tools/review.js +3 -4
- package/tools/roster.js +2 -3
- package/tools/send.js +2 -2
- package/tools/spawn.js +8 -5
- package/tools/status.js +16 -8
- package/tools/teardown.js +3 -4
- package/tools/test.js +2 -2
- package/types/index.d.ts +35 -1
- package/ui-bundle/README.md +37 -3
- package/ui-bundle/client.cards.js +356 -0
- package/ui-bundle/client.denied.js +142 -0
- package/ui-bundle/client.dock.js +333 -0
- package/ui-bundle/client.helpers.js +271 -0
- package/ui-bundle/client.js +79 -1747
- package/ui-bundle/client.locales.js +247 -0
- package/ui-bundle/client.mode.js +206 -0
- package/ui-bundle/client.overlay.js +64 -0
- package/ui-bundle/client.styles.js +151 -0
- package/ui-bundle/client.toolview.js +82 -0
- package/ui-bundle/package.json +1 -0
- package/lib/doctor.js +0 -270
- package/lib/persistence.js +0 -349
- package/lib/store.js +0 -118
- /package/{lib → app}/briefing.js +0 -0
- /package/{lib → app}/spawn-support.js +0 -0
- /package/{lib → host}/compatibility.js +0 -0
- /package/{lib → host}/config.js +0 -0
- /package/{lib → host}/hmr.js +0 -0
- /package/{lib → host}/modeState.js +0 -0
- /package/{lib → host}/presets.js +0 -0
|
@@ -1,33 +1,34 @@
|
|
|
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: 64 · sections: 15 · tools: 27 · baseline 2026-10-23. Generated from `scripts/surface-baseline.json`; do not edit by hand.
|
|
4
5
|
|
|
5
6
|
| Tool | Description |
|
|
6
7
|
|---|---|
|
|
7
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. |
|
|
8
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. |
|
|
9
|
-
| `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; 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, 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
11
|
| `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. |
|
|
11
|
-
| `tower_doctor` | Read-only environment and repository health check: git, state schema, config, presets/subagents/HMR, workspace lock, roster staleness, worktree ownership, test discovery, leases. Returns checks with pass/warn/fail and an environment contract. Never writes state. |
|
|
12
|
+
| `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. |
|
|
12
13
|
| `tower_finding` | File a structured finding (bug / improve / vuln / idea) for the tower to route. Use this for anything notable OUTSIDE your mission scope instead of editing out-of-scope files. Severity is one of low|medium|high|critical. Use tower_findings to list, read, or close findings later. |
|
|
13
14
|
| `tower_findings` | List, read, or update findings for a repository. Without id it lists findings newest-first with counts by status (filters: status, type, severity, missionId, open, limit). With id it returns that finding; with id plus status and/or note it records a lifecycle transition. Statuses are open -> triaged -> fixed|wontfix, and fixed/wontfix can be reopened to open. Only the tower or a worker may change a finding; reviewers may only list and read. Read-only when no id is given. |
|
|
14
15
|
| `tower_import_legacy` | Read-only import of a Tower Codex (schemaVersion 1) or DSH legacy state file. Stores the full payload under <common>/dsh-tower/legacy/<sha256>.json and records only a summary in state.legacySnapshots; never activates, adopts, or resumes paused missions. |
|
|
15
16
|
| `tower_inbox` | Read your tower inbox: messages addressed to you plus broadcasts, newest first. Only returned messages are acknowledged; the result reports unreadRemaining, so keep reading until it reaches 0. Set include_read for history. Unread messages block tower_complete. |
|
|
16
|
-
| `tower_init` | Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize. |
|
|
17
|
+
| `tower_init` | Initialize Tower mission state for a repository (absolute repoRoot). Creates <git-common-dir>/dsh-tower/state.json and an activity log; idempotent on an existing workspace. Probes whether the checkout filesystem folds case and records caseInsensitive once, so scope matching follows the real filesystem (a skipped probe records null and keeps the safe case-sensitive comparison). Options: autoGitInit (git init + empty initial commit when the directory is not a repository), snapshotDirty (snapshot uncommitted work into a dangling commit used as the mission baseline), reanchor (change base when every mission is merged/abandoned). Only the tower may initialize. |
|
|
17
18
|
| `tower_interrupt` | Interrupt one running tower roster agent (worker or reviewer) and record it as interrupted in the roster. Only the tower may interrupt. Idempotent: an agent that is not running returns interrupted:false. Use it before takeover after a runaway worker, or to stop work that is no longer wanted. |
|
|
18
19
|
| `tower_legacy_map` | Audit-only mapping between a legacy snapshot id and a current mission. map records sourceHash/oldId/newMissionId; status lists mappings. Mapping never adopts or resumes a legacy mission. |
|
|
19
20
|
| `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". |
|
|
20
21
|
| `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. |
|
|
21
|
-
| `tower_plan` | Plan one or more missions in an initialized repository. Each mission has a printable-
|
|
22
|
+
| `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. |
|
|
22
23
|
| `tower_preflight` | 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. |
|
|
23
24
|
| `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. |
|
|
24
|
-
| `tower_recover` | Read-only recovery diagnostics and an explicit stale-state-lock repair. status reports the state lock owner; clear-stale-lock requires confirm:"CLEAR_STALE_LOCK" and only moves a lock whose PID is provably dead to a backup file. It never resets Tower state or worktrees. |
|
|
25
|
+
| `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. |
|
|
25
26
|
| `tower_reload` | Dev-only: ask DSH HMR to reconcile this profile and reload the Tower Host plugin from disk. 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. Registered while dev.hotReload is on; fails with reload-unavailable when the HMR service is absent. |
|
|
26
27
|
| `tower_resource` | Advisory resource leases per repository (coordination only; no OS CPU/memory hard cap). acquire/release require a token and are tower-only; status is read-only. Expired leases are never taken over silently. |
|
|
27
|
-
| `tower_review` | Submit an independent review verdict
|
|
28
|
+
| `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. |
|
|
28
29
|
| `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. |
|
|
29
30
|
| `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. |
|
|
30
31
|
| `tower_spawn` | 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. |
|
|
31
|
-
| `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, config path and the activity tail. Pass missionId for one full mission (tasks, notes, reviews, findings, completion report); offset/limit page the mission list (default 20, max 100); includeGate adds preflight gate digests for the page; format:"markdown" returns the human-readable mission board. Read-only. |
|
|
32
|
-
| `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. |
|
|
32
|
+
| `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. 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. |
|
|
33
|
+
| `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. |
|
|
33
34
|
| `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
|
@@ -11,7 +11,8 @@ The build exposes up to 29 tools: 19 core repository tools below (including `tow
|
|
|
11
11
|
`task_drop_reason`/`taskDropReason`, `clear_blockers`/`clearBlockers`,
|
|
12
12
|
`include_read`/`includeRead`, `dry_run`/`dryRun`, `timeoutMs`/`timeout_ms`,
|
|
13
13
|
`waitMs`/`wait_ms`, `mission_id`/`missionId` on findings, `agent_id`/`agentId` on interrupt,
|
|
14
|
-
`include_gate`/`includeGate
|
|
14
|
+
`include_gate`/`includeGate`, `include_archived`/`includeArchived` on status,
|
|
15
|
+
`retire_missing`/`retireMissing` on adopt. Outputs use
|
|
15
16
|
canonical camelCase names.
|
|
16
17
|
|
|
17
18
|
## Roles
|
|
@@ -31,29 +32,29 @@ findings but not mutate them. Violations throw `[tower:not-authorized]` or
|
|
|
31
32
|
| Tool | Parameters | Purpose / notes |
|
|
32
33
|
|---|---|---|
|
|
33
34
|
| `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`. |
|
|
34
|
-
| `tower_plan` | `missions[]` (`title
|
|
35
|
+
| `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`. |
|
|
35
36
|
| `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`. |
|
|
36
|
-
|
|
37
|
+
\|\ `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\.\ \|
|
|
37
38
|
| `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`. `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. |
|
|
38
39
|
| `tower_roster` | — | Roster with live activity (`running`/`inactive`/`missing`/`unknown`) and `stale` flags from the subagent catalog. Read-only. |
|
|
39
40
|
| `tower_adopt` | `retireMissing?` | Reconcile roster entries with the durable subagent catalog. Provably gone agents are retired (default) or marked `missing` (`retireMissing:false`) so they can be inspected/taken over. |
|
|
40
41
|
| `tower_diff` | `missionId` (alias `id`), `maxBytes?` | Read-only merge-base diff: files, commits, capped patch. Safe for reviewers. |
|
|
41
42
|
| `tower_send` | `to`, `subject`, `body`, `scope?`, `interrupt?`, `consentRef?` | Durable message to a roster name, `tower`, or `all`; delivers live to a running agent. `interrupt:true` interrupts the running turn first (Kimi-style), then delivers; `consentRef` is recorded for audit. Rate-limited by `send.maxPerMinute`. |
|
|
42
43
|
| `tower_inbox` | `limit?`, `include_read?` | Read/ack messages addressed to the caller; unread messages block `tower_complete`. |
|
|
43
|
-
| `tower_complete` | `missionId` (alias `id`), `report`, `checks?` | Worker reports completion (mission becomes `completed`, ready for preflight and optional review). Requires current owner, no open tasks/blockers, ≥1 commit, no unread inbox. `checks` records `{name, command?, exitCode?, evidence?}` evidence; `gates.requireChecks` can demand it. When configured, test discovery runs automatically and adds a `tests` check; failure throws `[tower:tests-failed]`. |
|
|
44
|
+
| `tower_complete` | `missionId` (alias `id`), `report`, `checks?` | Worker reports completion (mission becomes `completed`, ready for preflight and optional review). Requires current owner, no open tasks/blockers, ≥1 commit, no unread inbox. `report` is capped at `gates.maxReportBytes` (default 32768 UTF-8 bytes): a larger report is rejected with `invalid-argument` and a `nextAction` to file the full text in the mission worktree, never truncated; the stored report records `reportBytes`. `checks` records `{name, command?, exitCode?, evidence?}` evidence; `gates.requireChecks` can demand it. When configured, test discovery runs automatically and adds a `tests` check; failure throws `[tower:tests-failed]`. |
|
|
44
45
|
| `tower_interrupt` | `to?` (roster name), `agentId?`, `reason?` | Interrupt one running roster agent via `subagents.interrupt`, record `state:"interrupted"`. Idempotent (not running → `interrupted:false`). |
|
|
45
46
|
| `tower_finding` | `type`, `title`, `summary`, `details`, `suggested_fix`, `severity?`, `missionId?`, `location?` | Record an out-of-scope issue; reviewers use this instead of editing. Finds start as `open`. |
|
|
46
47
|
| `tower_findings` | `id?`, `status?`, `note?`, filters (`type?`, `severity?`, `missionId?`, `open?`, `limit?`) | Lifecycle tool: list/read/transition. Statuses `open → triaged → fixed`/`wontfix`, reopen allowed. Only the tower or a worker may mutate; a reviewer can list/read. |
|
|
47
|
-
| `tower_review` | `missionId` (alias `id`), `status` (`clean`/`p1-Nitems`/`p2-Nitems`), `merge` (`merge`/`fix-then-merge`/`hold`), `findings`, `decision`, `checks?`, `breakGlassReason?` | Reviewer verdict bound to the current tip. Reviewer must be the assigned reviewer (or tower break-glass). |
|
|
48
|
+
| `tower_review` | `missionId` (alias `id`), `status` (`clean`/`p1-Nitems`/`p2-Nitems`), `merge` (`merge`/`fix-then-merge`/`hold`), `findings`, `decision`, `checks?`, `breakGlassReason?` | Reviewer verdict bound to the current tip. Reviewer must be the assigned reviewer (or tower break-glass). A later route may record a verdict for the same reviewed tip while the mission is active: it appends to that round (`appended:true`) without changing the status; a verdict for a moved tip is rejected with an explanation. |
|
|
48
49
|
| `tower_preflight` | `missionId` (alias `id`) | Read-only gate: returns `{ok, blockers[], branchTip, baseTip, changedFiles, review}`. |
|
|
49
50
|
| `tower_merge` | `missionId` (alias `id`), `timeoutMs?` (alias `timeout_ms`) | Merge after a green gate. Records already-landed merges (`reconciled`), recovers a failed call whose commit exists (`recoveredFromFailure`), and separates `merge-hook-failed` from `merge-conflict`. |
|
|
50
51
|
| `tower_rebase` | `missionId` (alias `id`) | Rebase the branch onto the base in its worktree. Refuses while the worker runs or the worktree is dirty. Already-contained branches return `alreadyMerged` untouched. |
|
|
51
|
-
| `tower_teardown` | `force?`, `exclude?`, `dry_run?` (alias `dryRun`) | Remove eligible merged/abandoned worktrees and branches; running agents are always kept. |
|
|
52
|
+
| `tower_teardown` | `force?`, `exclude?`, `dry_run?` (alias `dryRun`) | Remove eligible merged/abandoned worktrees and branches; running agents are always kept. The final pass applies the `archive` policy: terminal missions whose worktree is gone are moved to `<git-common-dir>/dsh-tower/archive/` once `archive.afterDays`/`archive.maxMissions` says so, and `archived` lists their ids. Nothing is deleted; `archive.afterDays:0` archives a just-torn-down mission immediately. |
|
|
52
53
|
| `tower_mode` | — | Report mode, last change, paused goal, denied tools, store path, plugin version, HMR availability, workspace-lock holder. |
|
|
53
54
|
| `tower_control` | `action` (`enter`/`exit`), `reason` | Model-initiated mode change; registered by default; disable with `mode.allowAgentControl:false`; reason is mandatory. |
|
|
54
55
|
| `tower_reload` | `repoRoot?` | Dev-only: ask DSH HMR to reconcile the profile and reload the Host plugin from disk. Registered while `dev.hotReload` is on; scheduled after the result (default 50 ms); fails with `reload-unavailable` without HMR. When `repoRoot` is supplied the caller must be the tower. |
|
|
55
|
-
| `tower_doctor` | — (takes `repoRoot`) | Read-only health contract: git, state schema, config, presets/subagents/HMR, workspace lock, roster, worktree realpath/ownership, test config, leases. Returns `{ok, checks[{id,status,detail}], environmentContract}`. Never writes. |
|
|
56
|
-
| `tower_recover` | `action` (`status`/`clear-stale-lock`), `confirm?` | 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 backup
|
|
56
|
+
| `tower_doctor` | — (takes `repoRoot`) | Read-only health contract: the concept-budget surface (`config keys: N · sections: N · tools: N`), git, state schema, config, presets/subagents/HMR, the state lock's age and heartbeat freshness, the repo-scoped git lock holder, workspace lock, roster, worktree realpath/ownership, test config, leases and the terminal-mission archive. Returns `{ok, checks[{id,status,detail}], surface, stateLock, gitLock, stateSchema, archive, environmentContract}`; `surface` reports `{configKeys, sections, tools, baselineDate, line}` so growth against `scripts/surface-baseline.json` is visible in the output, `stateLock` reports `{held, pid, startedAt, at, alive, ageMs, heartbeatAgeMs, staleAfterMs, stale}` so a stuck tower is diagnosable without reading the file, and `gitLock` reports `{held, pid, phase, heldMs}` (`{held:false}` when free) with a `git-lock` check. `archive` reports `{missions, archivedMissions, stateBytes, archiveBytes, years, files, unreadable, policy{enabled,afterDays,maxMissions}}`. A missing archive directory is healthy; a corrupt file is a `warn` with its diagnostic. Never writes. |
|
|
57
|
+
| `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. |
|
|
57
58
|
| `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. |
|
|
58
59
|
| `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. |
|
|
59
60
|
| `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 leases require explicit `force` and the correct token. |
|
|
@@ -82,7 +82,7 @@ Windows 上通常是 `C:\Users\<you>\.dsh`):
|
|
|
82
82
|
|
|
83
83
|
## 编辑循环
|
|
84
84
|
|
|
85
|
-
1. 编辑 `
|
|
85
|
+
1. 编辑 `host/*.js`、`adapters/*.js`、`app/*.js`、`domain/**/*.js`、`index.js`、`worker.js`、`reviewer.js` 或 `skills.js`。
|
|
86
86
|
2. 保存文件。监视器会去抖(DSH 默认 100 ms)并对账 profile;变更的模块被替换,受影响的 Loader
|
|
87
87
|
条目重启。
|
|
88
88
|
3. 观察 DSH 日志中的 `reload plugin at <path>` / `restart required: …` 消息。
|
|
@@ -31,7 +31,19 @@ Host 插件在 DSH 启动时加载:**安装或更新后需要重启 DSH**。
|
|
|
31
31
|
|
|
32
32
|
## 迁移说明
|
|
33
33
|
|
|
34
|
-
- **
|
|
34
|
+
- **state schema v1 → v2(未发布)**:`state.json` 会自动迁移。升级后的第一次**写入**会保留
|
|
35
|
+
`state.json.v1.bak` 备份、`revision` 只 +1,并向 `activity.jsonl` 追加 `state.migrated {from,to}`;
|
|
36
|
+
只读工具仍可读取 v1 文件且不改写它。`tower_doctor` 报告磁盘上的版本与待执行的迁移。由更新插件
|
|
37
|
+
写入的文件会以 `[tower:state-version-unsupported]` 拒绝,绝不会被重写或降级;迁移抛错会报
|
|
38
|
+
`[tower:state-migration-failed]`,原文件保持字节不变。
|
|
39
|
+
- **state schema v2 → v3(未发布,#90)**:scope 匹配跟随检出目录所在文件系统的行为。迁移把
|
|
40
|
+
`caseInsensitive` 回填为 `null`(“尚未探测”);迁移不会读取文件系统,因此升级后的第一次
|
|
41
|
+
`tower_init` 会探测一次并记录 `true`/`false`。在此之前使用安全的 `case-sensitive` 比较,
|
|
42
|
+
`tower_doctor` 会把未知模式报告为 `warn`(`case-mode`)。在大小写不敏感的检出上,
|
|
43
|
+
`src/Api/**` 与 `src/api/**` 从此视为重叠,仅大小写不同的改动不再被 out-of-scope 门禁拒绝。
|
|
44
|
+
存储的 scope 文本永远不会被改写:看板仍显示操作者输入的原样。
|
|
45
|
+
- **2.0.4 → 2.1.0-beta.1**:不需要迁移 config;state 会按上述说明自动迁移(v1/v2 → v3)。
|
|
46
|
+
这是发布到 `next` dist-tag 的**预发布**,
|
|
35
47
|
`latest` 仍指向 2.0.4——请用 `dsh plugin --profile web add dsh-tower@next`(或精确版本号)安装。
|
|
36
48
|
运行时行为未变;本版本承载的是交付闸门(CI 中的 mount smoke、`npm run release:check`、证据约定
|
|
37
49
|
与追踪策略),而不是工具变化。本文档先于人工发布步骤合入:只有在[发布清单](release.md)执行
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
| `branch-missing` | 还没有 worktree/分支:先 spawn worker。 |
|
|
18
18
|
| `stale-base` | 分支不包含基线 tip:执行 `tower_rebase`。 |
|
|
19
19
|
| `read-only-survey` | survey mission 修改了文件;survey mission 必须只读。 |
|
|
20
|
-
| `out-of-scope` | 改动文件超出 mission scope;有意为之就扩大 scope
|
|
20
|
+
| `out-of-scope` | 改动文件超出 mission scope;有意为之就扩大 scope,否则回退。在大小写敏感的检出上,仅大小写不同的路径仍会被拒绝,detail 会指出折叠大小写后本可匹配的 scope 条目。 |
|
|
21
21
|
| `base-dirty` | 主检出存在未提交改动,合并会覆盖它们。 |
|
|
22
|
-
| `deps-unmerged` | 依赖 mission
|
|
22
|
+
| `deps-unmerged` | 依赖 mission 尚未满足:build 依赖须 `merged`/`abandoned`;survey 依赖在 `completed` 即视为满足(含归档记录)。 |
|
|
23
23
|
| `no-review` | `review.requireReview:true` 且分支上没有审查:spawn reviewer(或关闭审查门禁)。 |
|
|
24
24
|
| `review-mission-mismatch` | `review.requireReview:true` 且最新审查属于另一个 mission。 |
|
|
25
|
-
| `not-clean` | `review.requireReview:true` 且最新审查是 `p1-Nitems`/`p2-Nitems
|
|
25
|
+
| `not-clean` | `review.requireReview:true` 且最新审查是 `p1-Nitems`/`p2-Nitems`;需要再来一轮(同一轮的各 route 结论会追加记录,不会让状态反复翻转)。 |
|
|
26
26
|
| `review-hold` | `review.requireReview:true` 且最新审查建议 hold;必须由后续的干净审查取代。 |
|
|
27
27
|
| `review-not-approved` | `review.requireReview:true` 且最新干净审查给出的是 `fix-then-merge` 或 `hold`;只有 `merge` 才算批准。 |
|
|
28
28
|
| `tip-moved` | `review.requireReview:true` 且审查之后分支发生变化;重新审查(或使用干净 rebase 的免审豁免)。 |
|
|
@@ -82,8 +82,12 @@ merge、worktree 与 spawn 策略。它使用与 profile 配置相同的 JSON
|
|
|
82
82
|
| `gates.requireChecks` | `false` | 合并前要求至少一条 `tower_complete.checks` 证据(`no-check-evidence`)。 |
|
|
83
83
|
| `gates.requireTests` | `false` | 要求针对当前 mission 分支 tip 的通过测试回执(`tests-missing`);与审查门禁独立。 |
|
|
84
84
|
| `gates.allowReviewExemption` | `false` | 在 `review.requireReview:true` 时,允许用 `reviewPolicy:"none"` + 非空理由跳过审查 blocker;默认故意关闭;全局审查关闭时无影响。 |
|
|
85
|
+
| `gates.maxReportBytes` | `32768`(32 KiB) | `tower_complete.report` 的 UTF-8 字节硬上限。超出时以 `invalid-argument` 拒绝并给出可执行的 `nextAction`(把全文写进 mission worktree 的文件并引用路径),绝不截断。已存在的超限报告仍可读取。 |
|
|
85
86
|
| `docs.board` | `true` | 变更后写入 `MISSIONS.md`。 |
|
|
86
87
|
| `docs.boardLocation` | `common` | `common` = `<git-common-dir>/dsh-tower/MISSIONS.md`;`repo` = `<repoRoot>/MISSIONS.md`。 |
|
|
88
|
+
| `archive.enabled` | `true` | 在变更边界把 worktree 已移除的终态(`merged`/`abandoned`)任务移入 `<git-common-dir>/dsh-tower/archive/`。归档任务保留 tasks、reviews、findings 与完成报告;这是移动,不是删除。 |
|
|
89
|
+
| `archive.afterDays` | `30` | 年龄触发:终态任务超过这么多天未更新,会在下次变更时归档(`0` 表示立即归档)。 |
|
|
90
|
+
| `archive.maxMissions` | `100` | 数量触发:热状态中的终态任务超过此数时,从最旧的开始归档直到满足上限(`0` 表示归档所有符合条件的终态任务)。worktree 仍存在的任务永不归档。 |
|
|
87
91
|
| `tools.exposeOperational` | `true` | 注册 batch/resource/import/legacy/recover/test/doctor 工具;`false` 可缩小固定工具 schema 上下文。 |
|
|
88
92
|
| `leases.ttlMs` | `900000` | 默认咨询式租约寿命;过期不会自动释放。 |
|
|
89
93
|
| `dev.hotReload` | `true` | 注册 `tower_reload`;关闭时 `/tower reload` 也被禁用。无 HMR 服务时调用返回 `reload-unavailable`。 |
|
|
@@ -11,21 +11,82 @@
|
|
|
11
11
|
{ "id": "git", "status": "pass", "detail": "git version 2.55.0" },
|
|
12
12
|
{ "id": "state", "status": "pass", "detail": ".../state.json revision 7" }
|
|
13
13
|
],
|
|
14
|
+
"stateLock": {
|
|
15
|
+
"held": true,
|
|
16
|
+
"pid": 4242,
|
|
17
|
+
"startedAt": 1761280000000,
|
|
18
|
+
"at": 1761280120000,
|
|
19
|
+
"alive": true,
|
|
20
|
+
"ageMs": 121000,
|
|
21
|
+
"heartbeatAgeMs": 1000,
|
|
22
|
+
"staleAfterMs": 3000,
|
|
23
|
+
"stale": false
|
|
24
|
+
},
|
|
25
|
+
"gitLock": {
|
|
26
|
+
"held": true,
|
|
27
|
+
"pid": 4242,
|
|
28
|
+
"phase": "merge",
|
|
29
|
+
"heldMs": 92000
|
|
30
|
+
},
|
|
31
|
+
"archive": {
|
|
32
|
+
"missions": 3,
|
|
33
|
+
"archivedMissions": 12,
|
|
34
|
+
"stateBytes": 48211,
|
|
35
|
+
"archiveBytes": 190244,
|
|
36
|
+
"years": [2026],
|
|
37
|
+
"files": 2,
|
|
38
|
+
"unreadable": [],
|
|
39
|
+
"policy": { "enabled": true, "afterDays": 30, "maxMissions": 100 }
|
|
40
|
+
},
|
|
14
41
|
"environmentContract": {
|
|
15
42
|
"platform": "win32",
|
|
16
43
|
"gitVersion": "git version 2.55.0",
|
|
17
44
|
"repoRoot": "D:/repo",
|
|
18
45
|
"commonDir": "D:/repo/.git",
|
|
19
|
-
"stateDir": "D:/repo/.git/dsh-tower"
|
|
46
|
+
"stateDir": "D:/repo/.git/dsh-tower",
|
|
47
|
+
"caseInsensitive": true,
|
|
48
|
+
"comparisonMode": "case-insensitive"
|
|
20
49
|
}
|
|
21
50
|
}
|
|
22
51
|
```
|
|
23
52
|
|
|
53
|
+
`stateLock`(以及 `state-lock` 检查)报告锁的年龄(`ageMs`,从记录的 `since` 起算)与心跳新鲜度
|
|
54
|
+
(`heartbeatAgeMs`,从最后一次 `at` 起算),并对照过期窗口(`staleAfterMs = 3 × 1000 ms`)。
|
|
55
|
+
PID 存活并不等于写者存活:停止心跳的持有者会报告 `stale: true`,下一次 mutation 会回收它(或用
|
|
56
|
+
`tower_recover clear-stale-lock force:true` 移到备份);心跳新鲜则报告为持有中,绝不会被抢走。
|
|
57
|
+
这样无需手工读取 `state.lock` 就能诊断 PID 被复用的情况。
|
|
58
|
+
|
|
59
|
+
`gitLock`(以及 `git-lock` 检查)报告仓库级 git 互斥锁:被 merge/rebase/init/teardown/provision 持有时为
|
|
60
|
+
`{held, pid, phase, heldMs}`,空闲时为 `{held: false}`。merge 可以合法持有到配置的 merge 超时(默认 15 分钟),
|
|
61
|
+
因此“被持有”是 `warn` 而不是失败;持有者的阶段与时长让卡住的控制塔可被诊断。`tower_status` 返回同一个
|
|
62
|
+
`gitLock` 对象。
|
|
63
|
+
|
|
64
|
+
`archive` 报告终态任务归档:活跃 `missions`、`archivedMissions` 与 `stateBytes`,归档目录的
|
|
65
|
+
`archiveBytes`/`years`/`files`,每条 `unreadable` 诊断,以及配置的 `policy`(`enabled`、`afterDays`、
|
|
66
|
+
`maxMissions`)。归档目录不存在是健康的空归档(`archive` 检查为 `pass`);截断的 index/year 文件是
|
|
67
|
+
`warn` 并附带诊断,且归档会暂停而不是覆盖它——归档不可读期间没有数据会丢失,符合条件的 mission 仍留在
|
|
68
|
+
`state.json`。
|
|
69
|
+
|
|
24
70
|
检查覆盖:git 可用性、仓库身份、state schema/加载、仓库配置是否存在、worker/reviewer preset 名称、
|
|
25
71
|
subagents 与 HMR 服务、workspace 锁持有者/是否过期、实时 roster、worktree 是否存在/归属/是否脏、
|
|
26
|
-
test-discovery
|
|
72
|
+
test-discovery 策略、完成报告大小、归档健康度、旧版快照、batch 与租约。被 `tower_teardown` 正常清理的 mission worktree 会被
|
|
27
73
|
跳过(检查中会注明已移除数量),不会被误报为 missing。`warn` 是提示;`fail` 表示仓库在 spawn/merge 前需要
|
|
74
|
+
|
|
75
|
+
`caseInsensitive` 是 `tower_init` 在检出目录所在文件系统上一次性探测的结果;`null` 表示“尚未探测”,
|
|
76
|
+
此时 `case-mode` 检查为 `warn`。`comparisonMode` 是 scope 匹配实际使用的比较方式:
|
|
77
|
+
`case-insensitive` 会折叠双方大小写,`case-sensitive` 则保持逐字节精确。doctor 从不执行探测(探测会
|
|
78
|
+
创建临时文件,只读调用不能写入);在没有可用写权限的检出上运行 `tower_init` 会记录该模式,在此之前
|
|
79
|
+
使用安全的 `case-sensitive` 默认值。
|
|
80
|
+
|
|
81
|
+
检查覆盖:git 可用性、仓库身份、state schema/加载、已探测的大小写模式、仓库配置是否存在、
|
|
82
|
+
worker/reviewer preset 名称、subagents 与 HMR 服务、state 锁年龄/心跳、workspace 锁持有者/是否过期、实时 roster、
|
|
83
|
+
worktree 是否存在/归属/是否脏、test-discovery 策略、旧版快照、batch 与租约。被 `tower_teardown`
|
|
84
|
+
正常清理的 mission worktree 会被跳过(检查中会注明已移除数量),不会被误报为 missing。`warn` 是提示;`fail` 表示仓库在 spawn/merge 前需要
|
|
28
85
|
处理。state 加载失败会作为失败的检查报告而不是抛出,因此 doctor 在损坏的仓库上仍然有用。
|
|
29
86
|
|
|
87
|
+
`reports` 检查会指出仓库中最大的完成报告(`<bytes> B on <mission>`,对照 `gates.maxReportBytes` 上限),
|
|
88
|
+
同样的数据也以 `reports: { count, largest: { missionId, bytes } | null, capBytes }` 返回。上限生效前写入的
|
|
89
|
+
报告只会产生 `warn`,doctor 绝不会重写它们。
|
|
90
|
+
|
|
30
91
|
环境契约仅供参考;它不是身份或授权证明。`tower_init` 仍要求控制塔角色,`tower_spawn` 仍会检查
|
|
31
92
|
实时子代理目录。
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[English](../../reference/errors.md) | **简体中文**
|
|
4
4
|
|
|
5
5
|
工具失败是抛出的错误,不是 `{ok:false}` 结果(门禁结果是例外:见 [blockers.md](blockers.md))。
|
|
6
|
-
每个抛出的错误都以稳定 code 作为消息前缀。本版本定义
|
|
6
|
+
每个抛出的错误都以稳定 code 作为消息前缀。本版本定义 35 个工具错误码。
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
[tower:mission-not-found] unknown mission "M9"
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
| `stale-base` | 操作准备期间仓库状态已变化,需要重试。 | two-phase mutation 重校验 |
|
|
24
24
|
| `worktree-dirty` | mission worktree 有未提交改动,不能完成。 | `tower_complete` 前遗留 staged/modified/untracked 文件 |
|
|
25
25
|
| `state-locked` | 另一个 Tower 写入者持有 state 锁。 | 锁超时后的并发写 |
|
|
26
|
-
| `state-corrupt` | Tower state 文件不可读或无效。 | 手工编辑过的 `state.json` |
|
|
26
|
+
| `state-corrupt` | Tower state 文件不可读或无效。 | 手工编辑过的 `state.json`;非法 JSON;没有可用的 `schemaVersion` |
|
|
27
|
+
| `state-version-unsupported` | state 文件的 schema 版本高于本构建支持的版本,或无法迁移。 | 打开由更新插件写入的仓库;未知的旧版本;本构建拒绝降级 |
|
|
28
|
+
| `state-migration-failed` | state 迁移抛出异常;原文件未被修改。 | 迁移步骤中的 bug;查看迁移前的 `state.json.v<N>.bak` |
|
|
27
29
|
| `config-invalid` | Tower 配置无效。 | 格式错误的仓库 `config.json`、违反 schema 的值 |
|
|
28
30
|
| `config-unknown-key` | Tower 配置存在未知顶层键。 | 例如把键拼成 `allowAgentToggle` |
|
|
29
31
|
| `git-failed` | git 命令因未归入其他分类的原因失败。 | 被拒绝的合并命令、非法 ref |
|
|
@@ -32,20 +32,20 @@ roster 角色由 `exec.agent` 与 roster 共同决定。mission 级工具(`tow
|
|
|
32
32
|
| 工具 | 参数 | 用途 / 说明 |
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| `tower_init` | `base?`、`autoGitInit?`、`snapshotDirty?`、`reanchor?` | 创建 `<git-common-dir>/dsh-tower/state.json`。`autoGitInit` 执行 `git init` 并创建空的初始提交;`snapshotDirty` 记录一个游离快照提交作为 mission 基线;`reanchor` 仅在没有非终态 mission 时修改基线。返回 `dirty`、`snapshotCommit`、`reanchored`。 |
|
|
35
|
-
| `tower_plan` | `missions[]`(`title
|
|
35
|
+
| `tower_plan` | `missions[]`(`title`——可打印 Unicode,NFC 规范化,≤120 字符、`scope[]`、`tasks[]?`、`deps[]?`、`kind?`、`context?`、`references[]?` ≤20、`reviewPolicy?`、`reviewExemptionReason?`) | 创建 mission `M1..`;重叠的 build scope 会自动加依赖;拒绝未知依赖与依赖循环。mission id 保持 ASCII(`M1`、`M2`);不可见字符与超长标题会以 `invalid-argument` 拒绝。`reviewPolicy:"none"` 需要非空理由;审查门禁开启时,preflight 还要求 `gates.allowReviewExemption`。 |
|
|
36
36
|
| `tower_mission` | `id`(别名 `missionId`)、`status?`、`task_done?`、`task_drop?`+`task_drop_reason?`、`blocker?`、`clear_blockers?`、`note?`、`scope?`、`pause_cause?`、`authorization?` | 更新单个 mission。worker 可安全使用的字段是 task/blocker/note;不能在这里设置 `merged`。暂停的 mission 记录 `pause_cause`(user/failure/resource/dependency);恢复用户暂停的 mission 会遵循 `mode.requireResumeAuthorization`。 |
|
|
37
|
-
| `tower_status` | `missionId?`、`offset?`、`limit?`、`includeGate?`、`format?` |
|
|
37
|
+
| `tower_status` | `missionId?`、`offset?`、`limit?`、`includeGate?`、`format?` | 默认返回面板。分页 mission 只带 `reportBytes`;`missionId` 返回完整详情(任务/备注/审查/finding/完成报告正文)。`offset`/`limit` 对 mission 分页(默认 20,最大 100),`includeGate` 附带门禁摘要,`format:"markdown"` 返回看板。host 提供 mode hook 时还会包含 `mode.lastChange` 与最近 `history`。还返回仓库级 git 互斥锁 `gitLock`(`{held, pid, phase, heldMs}`,空闲时 `{held:false}`)。 |
|
|
38
38
|
| `tower_spawn` | `missionId`(别名 `id`)、`role`(`worker`/`reviewer`)、`name?`、`instructions?`、`worktreeRoot?`、`wait?`、`waitMs?`、`takeover?`、`includeDirtyBase?` | 准备或复用 worktree 并启动子代理。worker 使用 `tower-worker`;reviewer 要求 mission 为 `completed`,并使用只读的 `tower-reviewer`。`takeover:true` 替换已不再运行的 worker 条目(ownerGeneration 递增)。每个 mission 只能有一个活跃 worker;`spawn.maxLiveWorkers` 限制整个仓库。 |
|
|
39
39
|
| `tower_roster` | — | roster 及来自子代理目录的实时活动(`running`/`inactive`/`missing`/`unknown`)与 `stale` 标记。只读。 |
|
|
40
40
|
| `tower_adopt` | `retireMissing?` | 把 roster 条目与持久子代理目录对账。可证明已消失的 agent 会被标记为 retired(默认),或标记为 `missing`(`retireMissing:false`)以便检查/接管。 |
|
|
41
41
|
| `tower_diff` | `missionId`(别名 `id`)、`maxBytes?` | 只读 merge-base diff:文件、提交、有上限的 patch。供 reviewer 安全使用。 |
|
|
42
42
|
| `tower_send` | `to`、`subject`、`body`、`scope?`、`interrupt?`、`consentRef?` | 向 roster 名称、`tower` 或 `all` 发送持久消息;对运行中的 agent 会即时投递。`interrupt:true` 先中断运行中的回合(Kimi 风格)再投递;`consentRef` 记录用于审计。受 `send.maxPerMinute` 限速。 |
|
|
43
43
|
| `tower_inbox` | `limit?`、`include_read?` | 读取并确认发给调用者的消息;未读消息会阻止 `tower_complete`。 |
|
|
44
|
-
| `tower_complete` | `missionId`(别名 `id`)、`report`、`checks?` | worker 报告完成(mission 变为 `completed`,可进入 preflight 与可选审查)。要求当前 owner、无未完成任务/blocker、≥1 个提交、无未读消息。`checks` 记录 `{name, command?, exitCode?, evidence?}` 证据;`gates.requireChecks` 可以强制要求。配置了测试发现时还会自动运行并添加 `tests` 检查;失败会抛出 `[tower:tests-failed]`。 |
|
|
44
|
+
| `tower_complete` | `missionId`(别名 `id`)、`report`、`checks?` | worker 报告完成(mission 变为 `completed`,可进入 preflight 与可选审查)。要求当前 owner、无未完成任务/blocker、≥1 个提交、无未读消息。`report` 受 `gates.maxReportBytes`(默认 32768 UTF-8 字节)硬上限约束:超限会以 `invalid-argument` 拒绝并给出 `nextAction`(把全文写进 mission worktree 的文件),绝不截断;存储时会记录 `reportBytes`。`checks` 记录 `{name, command?, exitCode?, evidence?}` 证据;`gates.requireChecks` 可以强制要求。配置了测试发现时还会自动运行并添加 `tests` 检查;失败会抛出 `[tower:tests-failed]`。 |
|
|
45
45
|
| `tower_interrupt` | `to?`(roster 名称)、`agentId?`、`reason?` | 通过 `subagents.interrupt` 中断一个运行中的 roster agent,并记录 `state:"interrupted"`。幂等(未运行 → `interrupted:false`)。 |
|
|
46
46
|
| `tower_finding` | `type`、`title`、`summary`、`details`、`suggested_fix`、`severity?`、`missionId?`、`location?` | 记录 scope 之外的问题;reviewer 用它代替直接改文件。新 finding 初始为 `open`。 |
|
|
47
47
|
| `tower_findings` | `id?`、`status?`、`note?`、过滤(`type?`、`severity?`、`missionId?`、`open?`、`limit?`) | 生命周期工具:列出/读取/流转。状态为 `open → triaged → fixed`/`wontfix`,允许重新打开。只有控制塔或 worker 能修改;reviewer 只能列出/读取。 |
|
|
48
|
-
| `tower_review` | `missionId`(别名 `id`)、`status`(`clean`/`p1-Nitems`/`p2-Nitems`)、`merge`(`merge`/`fix-then-merge`/`hold`)、`findings`、`decision`、`checks?`、`breakGlassReason?` | 绑定当前 tip 的 reviewer 结论。reviewer 必须是获派的 reviewer(或由控制塔使用 break-glass
|
|
48
|
+
| `tower_review` | `missionId`(别名 `id`)、`status`(`clean`/`p1-Nitems`/`p2-Nitems`)、`merge`(`merge`/`fix-then-merge`/`hold`)、`findings`、`decision`、`checks?`、`breakGlassReason?` | 绑定当前 tip 的 reviewer 结论。reviewer 必须是获派的 reviewer(或由控制塔使用 break-glass)。非 clean 结论使 mission 回到 active 后,其他 route 仍可为同一 reviewed tip 提交结论:追加到同一轮(`appended:true`),不再改变状态;tip 已移动时会被明确拒绝。 |
|
|
49
49
|
| `tower_preflight` | `missionId`(别名 `id`) | 只读门禁:返回 `{ok, blockers[], branchTip, baseTip, changedFiles, review}`。 |
|
|
50
50
|
| `tower_merge` | `missionId`(别名 `id`)、`timeoutMs?`(别名 `timeout_ms`) | 门禁全绿后合并。若合并提交已落地则记录(`reconciled`);若命令失败但提交已存在则恢复(`recoveredFromFailure`);区分 `merge-hook-failed` 与 `merge-conflict`。 |
|
|
51
51
|
| `tower_rebase` | `missionId`(别名 `id`) | 在 worktree 内把分支 rebase 到基线。worker 运行中或 worktree 脏时拒绝。分支已包含在基线中时返回 `alreadyMerged` 且保持分支不动。 |
|
|
@@ -53,8 +53,8 @@ roster 角色由 `exec.agent` 与 roster 共同决定。mission 级工具(`tow
|
|
|
53
53
|
| `tower_mode` | — | 报告模式、最近修改、被暂停的 goal、被拒绝的工具、store 路径、插件版本、HMR 可用性、workspace 锁持有者。 |
|
|
54
54
|
| `tower_control` | `action`(`enter`/`exit`)、`reason` | 模型主动切换模式;默认注册,设 `mode.allowAgentControl:false` 可禁用;`reason` 必填。 |
|
|
55
55
|
| `tower_reload` | `repoRoot?` | 仅开发用:请求 DSH HMR 对账 profile 并从磁盘重载 Host 插件。`dev.hotReload` 开启时注册;在工具结果返回后调度(默认 50 ms);没有 HMR 时以 `reload-unavailable` 失败。传入 `repoRoot` 时调用者必须是控制塔。 |
|
|
56
|
-
| `tower_doctor` | —(需要 `repoRoot`) | 只读健康契约:git、state schema、config、preset/subagents/HMR、workspace 锁、roster、worktree realpath/归属、测试配置、leases。返回 `{ok, checks[{id,status,detail}], environmentContract}
|
|
57
|
-
| `tower_recover` | `action`(`status`/`clear-stale-lock`)、`confirm?` | 恢复诊断与显式 stale state-lock 修复。`clear-stale-lock` 需要 `confirm:"CLEAR_STALE_LOCK"`,只把 PID
|
|
56
|
+
| `tower_doctor` | —(需要 `repoRoot`) | 只读健康契约:git、state schema、config、preset/subagents/HMR、state 锁的年龄与心跳新鲜度、仓库级 git 锁持有者、workspace 锁、roster、worktree realpath/归属、测试配置、leases。返回 `{ok, checks[{id,status,detail}], stateLock, gitLock, environmentContract}`;`stateLock` 报告 `{held, pid, startedAt, at, alive, ageMs, heartbeatAgeMs, staleAfterMs, stale}`,`gitLock` 报告 `{held, pid, phase, heldMs}`(空闲时 `{held:false}`)并附带 `git-lock` 检查,无需读取文件即可诊断卡住的控制塔。从不写状态。 |
|
|
57
|
+
| `tower_recover` | `action`(`status`/`clear-stale-lock`)、`confirm?`、`force?` | 恢复诊断与显式 stale state-lock 修复。`clear-stale-lock` 需要 `confirm:"CLEAR_STALE_LOCK"`,只把 PID 已死的锁改名为 `.recovered-<ts>` 备份。`force:true` 还能清理 PID 仍存活但心跳静默(PID 被复用)的锁:输出 `{pid, startedAt, ageMs, heartbeatAgeMs}`,记录仍只改名、绝不 unlink。不会重置状态或 worktree。 |
|
|
58
58
|
| `tower_test` | `missionId`(别名 `id`) | 运行 worktree 的 `package.json` 中 `tower.testDiscovery` 配置的发现命令并记录回执;失败时 `ok:false`。自动准入检查请用 `tower_complete`。 |
|
|
59
59
|
| `tower_batch` | `action`(`create`/`status`/`close`/`abandon`)、`missionIds?`、`exclusions?`、`requiredChecks?`、`receiptPaths?`、`batchId?`、`checks?`、`treeTip?`、`receiptHashes?`、`reason?` | 有限证据批次。create 冻结成员/检查/回执;close 校验每条必需检查与回执哈希;`merged` 成员也可参与 close;abandon 可退掉过期的 open batch 且不删除 mission。成员关系不可变。 |
|
|
60
60
|
| `tower_resource` | `action`(`acquire`/`release`/`status`)、`name?`、`holder?`、`token?`、`ttlMs?`、`force?` | 每个仓库的咨询式租约,位于 `<common>/dsh-tower/leases.json`。没有 OS CPU/内存硬上限;过期的租约需要显式 `force` 与正确 token。 |
|
package/domain/batch.js
CHANGED
|
@@ -120,7 +120,7 @@ function normalizeReceiptPathOrNull(value, where) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Legacy snapshots imported by
|
|
123
|
+
/** Legacy snapshots imported by adapters/legacy.js; only paused missions may be excluded. */
|
|
124
124
|
function pausedLegacyIds(state) {
|
|
125
125
|
const ids = new Set()
|
|
126
126
|
const collectMission = (mission) => {
|
|
@@ -138,7 +138,7 @@ function pausedLegacyIds(state) {
|
|
|
138
138
|
collectList(snapshot.missions)
|
|
139
139
|
if (isPlainObject(snapshot.snapshot)) collectList(snapshot.snapshot.missions)
|
|
140
140
|
if (isPlainObject(snapshot.payload)) collectList(snapshot.payload.missions)
|
|
141
|
-
//
|
|
141
|
+
// adapters/legacy.js stores imported snapshots with `pausedMissionIds`; older,
|
|
142
142
|
// hand-written fixtures may use `pausedIds`.
|
|
143
143
|
for (const key of ['pausedIds', 'pausedMissionIds']) {
|
|
144
144
|
if (Array.isArray(snapshot[key])) {
|
package/domain/config/schema.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This module is pure: it declares the Schemastery schema, validates raw config
|
|
4
4
|
// objects, and materializes defaults. Profile and repository loading (fs and the
|
|
5
|
-
// process-local profile snapshot) live in
|
|
5
|
+
// process-local profile snapshot) live in host/config.js, which re-exports
|
|
6
6
|
// `Config` and `resolveTowerConfig` for compatibility.
|
|
7
7
|
//
|
|
8
8
|
// Layers, lowest precedence first:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// `allowAgentControl: true` via the legacy top-level key.
|
|
16
16
|
import Schema from '@deepseek-ai/schemastery'
|
|
17
17
|
import { TowerToolError } from '../errors.js'
|
|
18
|
+
import { DEFAULT_MAX_REPORT_BYTES } from '../missions/completion.js'
|
|
18
19
|
|
|
19
20
|
export const DEFAULT_DENY_TOOLS = [
|
|
20
21
|
'create_goal',
|
|
@@ -32,7 +33,7 @@ export const DEFAULT_DENY_TOOLS = [
|
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
const LEGACY_KEYS = ['section', 'allowAgentControl']
|
|
35
|
-
const GROUP_KEYS = ['init', 'mode', 'review', 'merge', 'git', 'worktrees', 'spawn', 'send', 'tests', 'gates', 'docs', 'leases', 'dev', 'tools']
|
|
36
|
+
const GROUP_KEYS = ['init', 'mode', 'review', 'merge', 'git', 'worktrees', 'spawn', 'send', 'tests', 'gates', 'docs', 'archive', 'leases', 'dev', 'tools']
|
|
36
37
|
|
|
37
38
|
const SECTION_KEYS = {
|
|
38
39
|
init: ['autoGitInit', 'snapshotDirty', 'autoExclude'],
|
|
@@ -71,8 +72,9 @@ const SECTION_KEYS = {
|
|
|
71
72
|
],
|
|
72
73
|
send: ['maxPerMinute', 'interruptDefault'],
|
|
73
74
|
tests: ['enabled', 'admission', 'timeoutMs', 'maxBytes', 'autoChecks', 'allowPackageDiscovery'],
|
|
74
|
-
gates: ['requireChecks', 'requireTests', 'allowReviewExemption'],
|
|
75
|
+
gates: ['requireChecks', 'requireTests', 'allowReviewExemption', 'maxReportBytes'],
|
|
75
76
|
docs: ['board', 'boardLocation'],
|
|
77
|
+
archive: ['enabled', 'afterDays', 'maxMissions'],
|
|
76
78
|
tools: ['exposeOperational', 'exposeBatch', 'exposeImport', 'exposeLeases', 'exposeTests', 'exposeRecovery', 'exposeDoctor'],
|
|
77
79
|
leases: ['ttlMs'],
|
|
78
80
|
dev: ['hotReload', 'reloadDelayMs', 'reloadRequiredIds'],
|
|
@@ -226,6 +228,11 @@ export const Config = Schema.object({
|
|
|
226
228
|
allowReviewExemption: Schema.boolean()
|
|
227
229
|
.default(false)
|
|
228
230
|
.description('Allow reviewPolicy:none missions with a non-empty reviewExemptionReason to skip review blockers.'),
|
|
231
|
+
maxReportBytes: Schema.natural()
|
|
232
|
+
.min(1024)
|
|
233
|
+
.max(16 * 1024 * 1024)
|
|
234
|
+
.default(DEFAULT_MAX_REPORT_BYTES)
|
|
235
|
+
.description('Hard cap, in UTF-8 bytes, for tower_complete.report; a larger report is rejected with an actionable nextAction, never truncated.'),
|
|
229
236
|
})
|
|
230
237
|
.default({})
|
|
231
238
|
.description('Merge-gate strictness.'),
|
|
@@ -239,6 +246,21 @@ export const Config = Schema.object({
|
|
|
239
246
|
})
|
|
240
247
|
.default({})
|
|
241
248
|
.description('Human-readable artifacts.'),
|
|
249
|
+
archive: Schema.object({
|
|
250
|
+
enabled: Schema.boolean()
|
|
251
|
+
.default(true)
|
|
252
|
+
.description('Move terminal (merged/abandoned) missions whose worktree is gone into <git-common-dir>/dsh-tower/archive/ at the next mutation, so state.json stops carrying their full history.'),
|
|
253
|
+
afterDays: Schema.natural()
|
|
254
|
+
.min(0)
|
|
255
|
+
.default(30)
|
|
256
|
+
.description('Terminal missions untouched for at least this many days are archived on the next mutation (0 archives them immediately).'),
|
|
257
|
+
maxMissions: Schema.natural()
|
|
258
|
+
.min(0)
|
|
259
|
+
.default(100)
|
|
260
|
+
.description('Maximum terminal missions kept hot; once exceeded, the oldest eligible terminal missions are archived until the cap holds (0 archives every eligible terminal mission).'),
|
|
261
|
+
})
|
|
262
|
+
.default({})
|
|
263
|
+
.description('Terminal-mission archival policy. Archived missions are moved, never deleted.'),
|
|
242
264
|
tools: Schema.object({
|
|
243
265
|
exposeOperational: Schema.boolean()
|
|
244
266
|
.default(true)
|
|
@@ -268,6 +290,30 @@ export const Config = Schema.object({
|
|
|
268
290
|
.description('Development and hot-reload behavior.'),
|
|
269
291
|
}).description('DSH Tower configuration')
|
|
270
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The user-facing config surface (#84): every leaf key one level deep plus the
|
|
295
|
+
* names of the object sections. The generated config reference, `tower_doctor`
|
|
296
|
+
* and the surface baseline all count this same shape, so "how many concepts"
|
|
297
|
+
* is one number rather than three near-identical ones.
|
|
298
|
+
*
|
|
299
|
+
* @param {any} [config] the Schemastery root (defaults to the live `Config`)
|
|
300
|
+
* @returns {{ keys: string[], sections: string[], count: number }}
|
|
301
|
+
*/
|
|
302
|
+
export function configSurface(config = Config) {
|
|
303
|
+
const keys = []
|
|
304
|
+
const sections = []
|
|
305
|
+
for (const [key, schema] of Object.entries(config?.dict ?? {})) {
|
|
306
|
+
const dict = schema?.dict
|
|
307
|
+
if (dict && typeof dict === 'object') {
|
|
308
|
+
sections.push(key)
|
|
309
|
+
for (const child of Object.keys(dict)) keys.push(`${key}.${child}`)
|
|
310
|
+
} else {
|
|
311
|
+
keys.push(key)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { keys, sections, count: keys.length }
|
|
315
|
+
}
|
|
316
|
+
|
|
271
317
|
export function clone(value) {
|
|
272
318
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value))
|
|
273
319
|
}
|
|
@@ -316,7 +362,7 @@ export function assertKnownKeys(raw, label) {
|
|
|
316
362
|
if (unknown.length > 0) {
|
|
317
363
|
throw new TowerToolError(
|
|
318
364
|
'config-unknown-key',
|
|
319
|
-
`${label} has unknown key(s) ${unknown.join(', ')} — config is { section?, allowAgentControl?, init?, mode?, review?, merge?, git?, worktrees?, spawn?, send?, tests?, gates?, docs?, leases?, dev? }`,
|
|
365
|
+
`${label} has unknown key(s) ${unknown.join(', ')} — config is { section?, allowAgentControl?, init?, mode?, review?, merge?, git?, worktrees?, spawn?, send?, tests?, gates?, docs?, archive?, leases?, dev?, tools? }`,
|
|
320
366
|
)
|
|
321
367
|
}
|
|
322
368
|
for (const [group, allowed] of Object.entries(SECTION_KEYS)) {
|
|
@@ -397,6 +443,7 @@ export function resolveTowerConfig(rawConfig) {
|
|
|
397
443
|
tests: clone(parsed.tests ?? {}),
|
|
398
444
|
gates: clone(parsed.gates ?? {}),
|
|
399
445
|
docs: clone(parsed.docs ?? {}),
|
|
446
|
+
archive: clone(parsed.archive ?? {}),
|
|
400
447
|
tools: clone(parsed.tools ?? {}),
|
|
401
448
|
leases: clone(parsed.leases ?? {}),
|
|
402
449
|
dev: clone(parsed.dev ?? {}),
|
|
@@ -54,6 +54,7 @@ const SCHEMAS = {
|
|
|
54
54
|
base: S.str,
|
|
55
55
|
revision: S.int,
|
|
56
56
|
missions: S.int,
|
|
57
|
+
caseInsensitive: S.any,
|
|
57
58
|
},
|
|
58
59
|
['created', 'repoRoot', 'commonDir', 'statePath', 'base', 'revision', 'missions'],
|
|
59
60
|
),
|
|
@@ -78,6 +79,7 @@ const SCHEMAS = {
|
|
|
78
79
|
recentFindings: S.arr(S.obj()),
|
|
79
80
|
missions: S.arr(S.obj()),
|
|
80
81
|
activity: S.arr(S.obj()),
|
|
82
|
+
gitLock: S.obj({ held: S.bool, pid: S.any, phase: S.any, heldMs: S.any }),
|
|
81
83
|
mode: S.any,
|
|
82
84
|
spawn: S.any,
|
|
83
85
|
towerAgentId: S.any,
|
|
@@ -168,7 +170,7 @@ const SCHEMAS = {
|
|
|
168
170
|
['counts', 'total'],
|
|
169
171
|
),
|
|
170
172
|
review: S.obj(
|
|
171
|
-
{ review: S.obj(), branchTip: S.str, missionStatus: S.str, round: S.int, reviewerRounds: S.int },
|
|
173
|
+
{ review: S.obj(), branchTip: S.str, missionStatus: S.str, round: S.int, appended: S.bool, reviewerRounds: S.int },
|
|
172
174
|
['review', 'branchTip', 'missionStatus', 'round', 'reviewerRounds'],
|
|
173
175
|
),
|
|
174
176
|
preflight: S.obj(
|
|
@@ -238,9 +240,37 @@ const SCHEMAS = {
|
|
|
238
240
|
},
|
|
239
241
|
['ok', 'scheduled'],
|
|
240
242
|
),
|
|
241
|
-
doctor: S.obj(
|
|
243
|
+
doctor: S.obj(
|
|
244
|
+
{
|
|
245
|
+
ok: S.bool,
|
|
246
|
+
checks: S.arr(S.obj()),
|
|
247
|
+
stateLock: S.any,
|
|
248
|
+
gitLock: S.any,
|
|
249
|
+
stateSchema: S.obj({
|
|
250
|
+
currentVersion: S.any,
|
|
251
|
+
supportedVersion: S.int,
|
|
252
|
+
migrations: S.arr(S.obj({ from: S.int, to: S.int })),
|
|
253
|
+
status: S.str,
|
|
254
|
+
}),
|
|
255
|
+
reports: S.any,
|
|
256
|
+
surface: S.any,
|
|
257
|
+
archive: S.obj({
|
|
258
|
+
missions: S.any,
|
|
259
|
+
archivedMissions: S.int,
|
|
260
|
+
stateBytes: S.any,
|
|
261
|
+
archiveBytes: S.int,
|
|
262
|
+
dir: S.str,
|
|
263
|
+
years: S.arr(S.int),
|
|
264
|
+
files: S.int,
|
|
265
|
+
unreadable: S.arr(S.str),
|
|
266
|
+
policy: S.obj(),
|
|
267
|
+
}),
|
|
268
|
+
environmentContract: S.obj(),
|
|
269
|
+
},
|
|
270
|
+
['ok', 'checks'],
|
|
271
|
+
),
|
|
242
272
|
recover: S.obj(
|
|
243
|
-
{ ok: S.bool, action: S.str, stateLock: S.any, cleared: S.bool, backup: S.any, error: S.str },
|
|
273
|
+
{ ok: S.bool, action: S.str, stateLock: S.any, cleared: S.bool, backup: S.any, forced: S.bool, error: S.str },
|
|
244
274
|
['ok', 'action'],
|
|
245
275
|
),
|
|
246
276
|
test: S.obj(
|
|
@@ -14,7 +14,7 @@ export const PlanInput = Type.Object({
|
|
|
14
14
|
repoRoot: Type.String({ description: 'Absolute path inside the target Git repository.' }),
|
|
15
15
|
missions: Type.Array(
|
|
16
16
|
Type.Object({
|
|
17
|
-
title: Type.String({ description: 'Printable
|
|
17
|
+
title: Type.String({ description: 'Printable Unicode mission title, NFC-normalized, at most 120 characters; used as the mission label.' }),
|
|
18
18
|
scope: Type.Optional(Type.Array(Type.String(), { description: 'Relative file globs this mission may touch; empty only for survey.' })),
|
|
19
19
|
tasks: Type.Optional(Type.Array(Type.String(), { description: 'Verifiable task items the worker ticks off.' })),
|
|
20
20
|
deps: Type.Optional(Type.Array(Type.String(), { description: 'Mission ids that must merge before this one.' })),
|
|
@@ -54,6 +54,7 @@ export const StatusInput = Type.Object({
|
|
|
54
54
|
offset: Type.Optional(Type.Integer({ minimum: 0, description: 'Mission list offset (default 0).' })),
|
|
55
55
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, description: 'Mission list page size (default 20).' })),
|
|
56
56
|
includeGate: Type.Optional(Type.Boolean({ description: 'Compute tower_preflight-style gate digests for missions on this page.' })),
|
|
57
|
+
includeArchived: Type.Optional(Type.Boolean({ description: 'Append archived terminal missions (compact summaries, marked archived:true) after the active page. The default dashboard and board list active missions only.' })),
|
|
57
58
|
format: Type.Optional(enumOf(STATUS_FORMATS, { description: 'markdown returns the MISSIONS.md board text.' })),
|
|
58
59
|
}, { additionalProperties: false })
|
|
59
60
|
|
|
@@ -25,6 +25,7 @@ export const RecoverInput = Type.Object({
|
|
|
25
25
|
repoRoot: Type.String({ description: 'Absolute path inside the target Git repository.' }),
|
|
26
26
|
action: enumOf(RECOVERY_ACTIONS, { description: 'Recovery action.' }),
|
|
27
27
|
confirm: Type.Optional(Type.String({ description: 'Required literal CLEAR_STALE_LOCK for clear-stale-lock.' })),
|
|
28
|
+
force: Type.Optional(Type.Boolean({ description: 'clear-stale-lock: clear the lock even when its recorded PID is alive (a recycled PID); requires confirm and still moves the record to a .recovered-<ts> backup.' })),
|
|
28
29
|
}, { additionalProperties: false })
|
|
29
30
|
|
|
30
31
|
export const TestInput = Type.Object({
|