dsh-tower 2.1.0 → 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 +103 -0
- package/COMPATIBILITY.md +5 -1
- package/CONTRIBUTING.md +64 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +7 -4
- package/README.md +7 -4
- package/SECURITY.md +6 -0
- package/{lib → adapters}/archive.js +1 -1
- package/{lib → adapters}/caseSensitivity.js +1 -1
- package/adapters/default.js +51 -10
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +1 -1
- package/{lib → adapters}/legacy.js +1 -1
- package/adapters/persistence.js +543 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +9 -5
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +1 -1
- package/app/archive.js +4 -6
- package/app/batch.js +9 -10
- package/app/board.js +2 -4
- package/app/checkCompletion.js +9 -10
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +1 -2
- package/app/init.js +24 -29
- package/app/merge.js +58 -51
- package/app/messages.js +7 -8
- package/app/mission.js +5 -5
- package/app/plan.js +8 -5
- package/app/provision.js +12 -14
- package/app/read.js +6 -3
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +15 -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 +8 -9
- package/app/tower.js +89 -35
- 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/workflow.md +51 -0
- package/docs/architecture/overview.md +167 -36
- package/docs/guide/hot-reload.md +1 -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 +2 -2
- package/docs/reference/config.generated.md +1 -0
- package/docs/reference/doctor.md +48 -2
- package/docs/reference/errors.generated.md +41 -37
- package/docs/reference/errors.md +53 -41
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +128 -0
- package/docs/reference/tools.generated.md +5 -4
- package/docs/reference/tools.md +4 -4
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/doctor.md +29 -1
- package/docs/zh/reference/tools.md +4 -4
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +25 -1
- package/domain/contracts/outputs.js +6 -2
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -42
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +17 -2
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/missions/completion.js +14 -0
- package/domain/missions/lifecycle.js +9 -3
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/read/views.js +3 -0
- package/domain/state/archive.js +1 -1
- package/domain/state.js +1 -1
- package/domain/surface.js +248 -0
- package/{lib → host}/doctor.js +116 -9
- 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 +58 -22
- 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 +2 -3
- 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 +3 -6
- 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 +6 -5
- package/tools/preflight.js +2 -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 +3 -3
- package/tools/teardown.js +2 -3
- package/tools/test.js +2 -2
- package/types/index.d.ts +1 -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/persistence.js +0 -356
- package/lib/store.js +0 -133
- /package/{lib → adapters}/stateFile.js +0 -0
- /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
package/app/tower.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Tower domain: repo-scoped mission state machine, validation, and gates.
|
|
2
|
+
//
|
|
3
|
+
// #69: the app layer depends on the ports in ports/ and receives their
|
|
4
|
+
// implementations through the deps object assembled by the composition root.
|
|
5
|
+
// It imports no adapter and no infrastructure module.
|
|
2
6
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
import { defaultDeps } from '../adapters/default.js'
|
|
7
|
+
import { assertPorts } from '../ports/index.js'
|
|
5
8
|
import { validateCreate as validateBatchCreate, validateClose as validateBatchClose, summarize as summarizeBatch } from '../domain/batch.js'
|
|
6
|
-
import { assertOwnership } from '../lib/worktreeOwnership.js'
|
|
7
9
|
|
|
8
|
-
import { runtimeConfig, resolveRepoConfig } from '../lib/config.js'
|
|
9
10
|
import { rebaseWaived } from '../domain/gate.js'
|
|
11
|
+
import { DEFAULT_MERGE_TIMEOUT_MS, gitLockTimeoutMs } from '../domain/lockBudget.js'
|
|
10
12
|
export { ROSTER_INACTIVE_STATES } from '../domain/roster.js'
|
|
11
13
|
|
|
12
14
|
export { fileInScope, scopesOverlap, validateTitle, normalizeScope } from '../domain/scope.js'
|
|
@@ -14,6 +16,8 @@ export { missionSummary } from '../domain/summary.js'
|
|
|
14
16
|
|
|
15
17
|
export { MISSION_STATUSES, SCHEMA_VERSION, MERGE_VERDICTS, FINDING_TYPES, FINDING_SEVERITIES, FINDING_STATUSES, FINDING_TRANSITIONS } from '../domain/mission.js'
|
|
16
18
|
import { TowerToolError } from '../domain/errors.js'
|
|
19
|
+
import { buildMissionEvent } from '../domain/events.js'
|
|
20
|
+
import { applyLandedMerge } from '../domain/integration/merge.js'
|
|
17
21
|
import { plan } from './plan.js'
|
|
18
22
|
import { updateMission } from './mission.js'
|
|
19
23
|
import { checkCompletion, complete } from './checkCompletion.js'
|
|
@@ -26,12 +30,11 @@ import { recordTestRun, recordLegacySnapshot, recordLegacyMap } from './recordin
|
|
|
26
30
|
|
|
27
31
|
export const ROLES = ['worker', 'reviewer']
|
|
28
32
|
const MAX_REVIEW_ROUNDS = 5
|
|
29
|
-
const DEFAULT_MERGE_TIMEOUT_MS = 15 * 60 * 1000
|
|
30
33
|
const MERGE_MAX_BUFFER = 64 * 1024 * 1024
|
|
31
34
|
|
|
32
35
|
function mergeTimeoutMs(value, config) {
|
|
33
36
|
if (value === undefined || value === null) {
|
|
34
|
-
const configured = (config ??
|
|
37
|
+
const configured = (config ?? {}).merge?.timeoutMs
|
|
35
38
|
return Number.isInteger(configured) && configured >= 1000 ? configured : DEFAULT_MERGE_TIMEOUT_MS
|
|
36
39
|
}
|
|
37
40
|
const ms = Number(value)
|
|
@@ -39,8 +42,8 @@ function mergeTimeoutMs(value, config) {
|
|
|
39
42
|
return ms
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
async function commitParents(cwd, sha) {
|
|
43
|
-
const line = await
|
|
45
|
+
async function commitParents(tower, cwd, sha) {
|
|
46
|
+
const line = await tower.deps.vcs.tryGit(cwd, ['rev-list', '--parents', '-n', '1', sha])
|
|
44
47
|
return line ? line.trim().split(/\s+/).slice(1) : []
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -49,10 +52,10 @@ async function commitParents(cwd, sha) {
|
|
|
49
52
|
* `--no-ff` merges put the integrated branch on the second parent, so this is
|
|
50
53
|
* the concrete evidence that a mission's work already landed in the base.
|
|
51
54
|
*/
|
|
52
|
-
async function findLandedMerge(cwd, baseTip, tips) {
|
|
53
|
-
const revs = (await
|
|
55
|
+
async function findLandedMerge(tower, cwd, baseTip, tips) {
|
|
56
|
+
const revs = (await tower.deps.vcs.tryGit(cwd, ['rev-list', '--merges', baseTip])) ?? ''
|
|
54
57
|
for (const sha of revs.split('\n').filter(Boolean)) {
|
|
55
|
-
const parents = await commitParents(cwd, sha)
|
|
58
|
+
const parents = await commitParents(tower, cwd, sha)
|
|
56
59
|
if (parents.length >= 2 && tips.has(parents[1])) return { commit: sha, parents }
|
|
57
60
|
}
|
|
58
61
|
return null
|
|
@@ -65,14 +68,14 @@ function describeGitFailure(error) {
|
|
|
65
68
|
|
|
66
69
|
/** Remove only lock files that are provably old. A fresh lock may belong to a
|
|
67
70
|
* concurrent Git process; deleting it would let two writers into the index. */
|
|
68
|
-
async function removeStaleGitLocks(commonDir, maxAgeMs = 60000) {
|
|
71
|
+
async function removeStaleGitLocks(tower, commonDir, maxAgeMs = 60000) {
|
|
69
72
|
const removed = []
|
|
70
73
|
for (const name of ['index.lock', 'HEAD.lock', 'MERGE_HEAD.lock', 'ORIG_HEAD.lock']) {
|
|
71
74
|
const file = path.join(commonDir, name)
|
|
72
75
|
try {
|
|
73
|
-
const stat = await
|
|
76
|
+
const stat = await tower.deps.store.stat(file)
|
|
74
77
|
if (Date.now() - stat.mtimeMs < maxAgeMs) continue
|
|
75
|
-
await
|
|
78
|
+
await tower.deps.store.unlink(file)
|
|
76
79
|
removed.push(name)
|
|
77
80
|
} catch {
|
|
78
81
|
// absent or in use; never force it
|
|
@@ -86,7 +89,6 @@ function fail(message, code = 'invalid-argument') {
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
import { validateState } from '../domain/state.js'
|
|
89
|
-
import { loadStateFile } from '../lib/stateFile.js'
|
|
90
92
|
import { boardMarkdown } from '../domain/report.js'
|
|
91
93
|
import { diff, missionDetail, status } from './read.js'
|
|
92
94
|
import { evaluateGate } from './gate.js'
|
|
@@ -106,28 +108,59 @@ export class Tower {
|
|
|
106
108
|
/**
|
|
107
109
|
* @param {{ root: string, commonDir: string, branch?: string | null }} repo
|
|
108
110
|
* @param {import('../types/index.js').TowerConfig} [config]
|
|
111
|
+
* @param {Record<string, any>} [deps] ports assembled by the composition root
|
|
112
|
+
* (the host entry); required.
|
|
109
113
|
*/
|
|
110
|
-
constructor(repo, config, deps
|
|
114
|
+
constructor(repo, config, deps) {
|
|
115
|
+
assertPorts(deps)
|
|
111
116
|
this.root = repo.root
|
|
112
117
|
this.commonDir = repo.commonDir
|
|
113
118
|
this.branch = repo.branch
|
|
114
|
-
this.deps = deps
|
|
119
|
+
this.deps = /** @type {Record<string, any>} */ (deps)
|
|
115
120
|
this.paths = this.deps.store.pathsFor(repo.commonDir)
|
|
116
121
|
// Effective config for this repository: code defaults < profile config <
|
|
117
122
|
// repo config file. Per-call tool arguments are applied by the caller.
|
|
118
|
-
this.config = /** @type {import('../types/index.js').TowerConfig} */ (
|
|
123
|
+
this.config = /** @type {import('../types/index.js').TowerConfig} */ (
|
|
124
|
+
config ?? { ...this.deps.config.runtimeConfig(), configPath: null, repoConfigPresent: false }
|
|
125
|
+
)
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the repository and config through the injected ports.
|
|
130
|
+
* @param {string} repoRoot
|
|
131
|
+
* @param {Record<string, any>} deps
|
|
132
|
+
*/
|
|
133
|
+
static async open(repoRoot, deps) {
|
|
134
|
+
assertPorts(deps)
|
|
135
|
+
const repo = await deps.vcs.resolveRepo(repoRoot)
|
|
136
|
+
const config = await deps.config.resolveRepoConfig(repo)
|
|
137
|
+
return new Tower(repo, config, deps)
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
git(cwd, args, options = {}) {
|
|
128
141
|
return this.deps.vcs.git(cwd, args, { timeoutMs: this.config.git?.timeoutMs, ...options })
|
|
129
142
|
}
|
|
130
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Options for one git-mutex section: the phase, the wait budget derived from
|
|
146
|
+
* the merge timeout, and the injected clock so waiting is testable in virtual
|
|
147
|
+
* time. The budget itself is the pure domain policy in ../domain/lockBudget.js
|
|
148
|
+
* (#93); this method only adds the Tower config and the injected clock.
|
|
149
|
+
* @param {{ phase?: string, mergeTimeoutMs?: number, timeoutMs?: number, now?: Function }} [options]
|
|
150
|
+
* @returns {Record<string, any>}
|
|
151
|
+
*/
|
|
152
|
+
gitMutexOptions(options = {}) {
|
|
153
|
+
const { phase, mergeTimeoutMs, timeoutMs, ...rest } = options
|
|
154
|
+
const clock = this.deps.clock
|
|
155
|
+
const clockNow = clock && typeof clock.now === 'function' ? clock.now.bind(clock) : null
|
|
156
|
+
return {
|
|
157
|
+
...rest,
|
|
158
|
+
...(phase ? { phase } : {}),
|
|
159
|
+
timeoutMs: timeoutMs ?? gitLockTimeoutMs(this.config, mergeTimeoutMs),
|
|
160
|
+
...(clockNow ? { now: clockNow } : {}),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
131
164
|
tryGit(cwd, args, options = {}) {
|
|
132
165
|
return this.deps.vcs.tryGit(cwd, args, { timeoutMs: this.config.git?.timeoutMs, ...options })
|
|
133
166
|
}
|
|
@@ -154,7 +187,7 @@ export class Tower {
|
|
|
154
187
|
if (!(await this.deps.store.exists(this.paths.state))) {
|
|
155
188
|
fail(`tower is not initialized for ${this.root} — run tower_init first`, 'not-initialized')
|
|
156
189
|
}
|
|
157
|
-
const loaded = await loadStateFile({
|
|
190
|
+
const loaded = await this.deps.state.loadStateFile({
|
|
158
191
|
store: this.deps.store,
|
|
159
192
|
paths: this.paths,
|
|
160
193
|
now: this.deps.clock?.now ? this.deps.clock.now() : Date.now(),
|
|
@@ -169,6 +202,27 @@ export class Tower {
|
|
|
169
202
|
return init(this, input)
|
|
170
203
|
}
|
|
171
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Persist-then-emit (#75): the caller has already written `state.json` and the
|
|
207
|
+
* activity log, so each domain transition descriptor is finalised with the
|
|
208
|
+
* persisted revision and emitted through the event port. The event channel is
|
|
209
|
+
* best effort — a listener (or an embedder without one) never fails the tool.
|
|
210
|
+
* @param {Array<{ type: string, missionId: string, commit?: string }>} descriptors
|
|
211
|
+
* @param {{ revision: number, at: number }} context
|
|
212
|
+
*/
|
|
213
|
+
emitEvents(descriptors, context) {
|
|
214
|
+
const events = this.deps.events
|
|
215
|
+
if (!events || typeof events.emit !== 'function') return
|
|
216
|
+
for (const descriptor of descriptors ?? []) {
|
|
217
|
+
try {
|
|
218
|
+
const event = buildMissionEvent(/** @type {any} */ (descriptor), context)
|
|
219
|
+
events.emit(event.name, event.payload)
|
|
220
|
+
} catch {
|
|
221
|
+
// events describe a fact that is already durable; never fail the tool
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
172
226
|
async plan(input = {}) {
|
|
173
227
|
return plan(this, input)
|
|
174
228
|
}
|
|
@@ -194,7 +248,7 @@ export class Tower {
|
|
|
194
248
|
|
|
195
249
|
async assertWorktree(worktree) {
|
|
196
250
|
if (!worktree || typeof worktree.path !== 'string') fail('mission has no worktree')
|
|
197
|
-
const owned = await assertOwnership({
|
|
251
|
+
const owned = await this.deps.ownership.assertOwnership({
|
|
198
252
|
storedPath: worktree.path,
|
|
199
253
|
storedRealPath: worktree.realPath,
|
|
200
254
|
branch: worktree.branch,
|
|
@@ -361,14 +415,14 @@ export class Tower {
|
|
|
361
415
|
await this.deps.store.ensureDir(path.dirname(file))
|
|
362
416
|
let text = ''
|
|
363
417
|
try {
|
|
364
|
-
text = await
|
|
418
|
+
text = await this.deps.store.readText(file)
|
|
365
419
|
} catch {
|
|
366
420
|
text = ''
|
|
367
421
|
}
|
|
368
422
|
const lines = text.split(/\r?\n/)
|
|
369
423
|
if (lines.includes(pattern) || lines.includes(`${pattern}/`)) return { inside: true, excluded: true, pattern }
|
|
370
424
|
const suffix = text === '' || text.endsWith('\n') ? '' : '\n'
|
|
371
|
-
await
|
|
425
|
+
await this.deps.store.appendText(file, `${suffix}${pattern}\n`)
|
|
372
426
|
return { inside: true, excluded: true, pattern }
|
|
373
427
|
}
|
|
374
428
|
|
|
@@ -426,6 +480,7 @@ export class Tower {
|
|
|
426
480
|
landed = null // no merge commit was created; the intent is safe to drop
|
|
427
481
|
} else {
|
|
428
482
|
landed = await findLandedMerge(
|
|
483
|
+
this,
|
|
429
484
|
this.root,
|
|
430
485
|
head,
|
|
431
486
|
new Set([intent.branchTip, intent.baseTip].filter(Boolean)),
|
|
@@ -489,20 +544,18 @@ export class Tower {
|
|
|
489
544
|
return null
|
|
490
545
|
}
|
|
491
546
|
const tips = new Set([branchTip, reviewedTip].filter(Boolean))
|
|
492
|
-
const found = await findLandedMerge(this.root, baseTip, tips)
|
|
547
|
+
const found = await findLandedMerge(this, this.root, baseTip, tips)
|
|
493
548
|
const mergedCommit = found ? found.commit : branchTip
|
|
494
549
|
const now = Date.now()
|
|
495
550
|
if (state.mergeIntent?.missionId === mission.id) delete state.mergeIntent
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
mission.notes.push(
|
|
501
|
-
found
|
|
551
|
+
const applied = applyLandedMerge({
|
|
552
|
+
mission,
|
|
553
|
+
mergedCommit,
|
|
554
|
+
note: found
|
|
502
555
|
? `reconciled already-landed merge ${mergedCommit.slice(0, 7)} with parents [${found.parents.map((parent) => parent.slice(0, 7)).join(', ')}]`
|
|
503
556
|
: `reconciled already-landed merge: ${mergedCommit.slice(0, 7)} is contained in ${state.base} (no merge commit found)`,
|
|
504
|
-
|
|
505
|
-
|
|
557
|
+
now,
|
|
558
|
+
})
|
|
506
559
|
state.revision += 1
|
|
507
560
|
state.updatedAt = now
|
|
508
561
|
validateState(state, this.paths.state)
|
|
@@ -513,6 +566,7 @@ export class Tower {
|
|
|
513
566
|
missionId: mission.id,
|
|
514
567
|
text: `reconciled already-landed ${mergedCommit.slice(0, 7)} on ${state.base}`,
|
|
515
568
|
})
|
|
569
|
+
this.emitEvents(applied.events, { revision: state.revision, at: now })
|
|
516
570
|
return {
|
|
517
571
|
ok: true,
|
|
518
572
|
reconciled: true,
|
package/docs/README.md
CHANGED
|
@@ -8,8 +8,8 @@ Choose a path:
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| New user | [When to use Tower](guide/scenarios-and-philosophy.md) → [Quick start](guide/quickstart.md) → [Tools](reference/tools.md) → [Example](examples/first-mission.md) |
|
|
10
10
|
| Choosing a workflow | [When to use Tower, and how it differs from Agent Teams, Ralph, Workflow and parallel subagents](guide/scenarios-and-philosophy.md) |
|
|
11
|
-
| Operator | [Runbook](operations/runbook.md) → [Blockers](reference/blockers.md) → [Error codes](reference/errors.md) → [Events](reference/events.md) → [Doctor](reference/doctor.md) → [Testing](reference/testing.md) → [Batches/leases/legacy](reference/operations.md) → [Security](operations/security.md) |
|
|
12
|
-
| Maintainer | [Architecture](architecture/overview.md) → [Config](reference/config.md) → [Hot reload](guide/hot-reload.md) → [Release checklist](guide/release.md) → [Tracker board](board.md) → [Contributing](../CONTRIBUTING.md) |
|
|
11
|
+
| Operator | [Runbook](operations/runbook.md) → [Blockers](reference/blockers.md) → [Error codes](reference/errors.md) → [Events](reference/events.md) → [Doctor](reference/doctor.md) → [Testing](reference/testing.md) → [Batches/leases/legacy](reference/operations.md) → [Security](operations/security.md) → [Incidents and regressions](operations/incident.md) |
|
|
12
|
+
| Maintainer | [Engineering principles](../PRINCIPLES.md) → [ADRs](adr/README.md) → [Architecture](architecture/overview.md) → [Surface budget](reference/surface-budget.md) → [Config](reference/config.md) → [Hot reload](guide/hot-reload.md) → [Release checklist](guide/release.md) → [Tracker board](board.md) → [Contributing](../CONTRIBUTING.md) |
|
|
13
13
|
|
|
14
14
|
Reference pages are the source of truth for the current release; the [tools reference](reference/tools.md),
|
|
15
15
|
[blocker list](reference/blockers.md) and [error codes](reference/errors.md) are checked against the
|
|
@@ -36,13 +36,17 @@ Other useful pages: [When to use Tower](guide/scenarios-and-philosophy.md), [Ins
|
|
|
36
36
|
- [Testing and evidence](reference/testing.md)
|
|
37
37
|
- [Batches, leases and legacy import](reference/operations.md)
|
|
38
38
|
- [Doctor](reference/doctor.md)
|
|
39
|
+
- [Surface budget and classification](reference/surface-budget.md)
|
|
39
40
|
|
|
40
41
|
**Operations and background**
|
|
41
42
|
|
|
42
43
|
- [Operations runbook](operations/runbook.md)
|
|
44
|
+
- [Incidents and regressions](operations/incident.md)
|
|
43
45
|
- [Security model](operations/security.md)
|
|
44
46
|
- [Release checklist](guide/release.md)
|
|
45
47
|
- [Tracker board](board.md)
|
|
48
|
+
- [Engineering principles](../PRINCIPLES.md)
|
|
49
|
+
- [Architecture decision records](adr/README.md)
|
|
46
50
|
- [Architecture and state machine](architecture/overview.md)
|
|
47
51
|
- [First mission example](examples/first-mission.md)
|
|
48
52
|
- [FAQ](faq.md)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 0001 — Ports and adapters are data, not a DI container
|
|
2
|
+
|
|
3
|
+
- Date: 2026-10-23
|
|
4
|
+
- Status: Accepted
|
|
5
|
+
- Deciders: maintainer
|
|
6
|
+
- Supersedes: none
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Tower needs a real seam between the pure app/domain logic and the effectful
|
|
11
|
+
world (filesystem, Git, clocks, ids). The seam has to survive a test that replaces
|
|
12
|
+
every adapter with an in-memory double, and it has to be obvious to a reader why
|
|
13
|
+
a tool can run without touching a repository. A conventional dependency-injection
|
|
14
|
+
container would add a framework, decorators, and reflection to a plugin that must
|
|
15
|
+
load inside an arbitrary DSH host version.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
`ports/` declares a frozen `{ name, methods }` runtime contract per capability and
|
|
20
|
+
an `assert*Port` helper; `ports/index.js` exports the `PORTS` table and
|
|
21
|
+
`assertPorts(deps)`. The composition root builds one plain object of adapter
|
|
22
|
+
implementations (`createDefaultDeps`) and passes it explicitly. `app/**` imports
|
|
23
|
+
only app siblings, `domain/**`, `ports/**` and `node:path`. There is no service
|
|
24
|
+
locator and no runtime reflection; a missing method fails at the seam with a
|
|
25
|
+
readable error.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- Every effectful call is visible as `deps.<port>.<method>`, so the contract test
|
|
30
|
+
can walk the source and assert each method is declared.
|
|
31
|
+
- Tests substitute the same plain object; the in-memory lifecycle is a real test
|
|
32
|
+
rather than a mock framework feature.
|
|
33
|
+
- Adding a capability means adding a port and an implementation, not registering
|
|
34
|
+
a class with a container.
|
|
35
|
+
- A downstream embedder that wants a different implementation passes a new
|
|
36
|
+
object to `registerTowerTools`; it cannot monkey-patch module state.
|
|
37
|
+
|
|
38
|
+
## Rejected alternatives
|
|
39
|
+
|
|
40
|
+
- **A dependency-injection container** (`reflect-metadata`, decorators, a
|
|
41
|
+
string-keyed registry) — adds a load-time framework dependency, hides the
|
|
42
|
+
dependency graph behind reflection, and fails indistinguishably when a host
|
|
43
|
+
bundle is mounted twice.
|
|
44
|
+
- **Direct adapter imports from `app/**`** — simpler at first, but it makes the
|
|
45
|
+
pure app test need a filesystem and a Git binary, and the architecture rule
|
|
46
|
+
`app-ports-only` would have nothing to enforce.
|
|
47
|
+
- **Module-level singletons** — one adapter instance would be shared across
|
|
48
|
+
repositories and test cases, which is the ambient-state problem the host-scope
|
|
49
|
+
rule exists to prevent.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 0002 — One state.json is the mission persistence contract
|
|
2
|
+
|
|
3
|
+
- Date: 2026-10-23
|
|
4
|
+
- Status: Accepted
|
|
5
|
+
- Deciders: maintainer
|
|
6
|
+
- Supersedes: none
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Mission state carries active missions, roster entries, reviews, findings, messages
|
|
11
|
+
and batches. The obvious alternative is one file per mission (or one directory
|
|
12
|
+
per mission), which would make every mission a small git-diffable record. Tower is
|
|
13
|
+
not a git-backed issue tracker: it mutates state from several tools on every
|
|
14
|
+
operation, under one process, and a crash between two file writes must not leave
|
|
15
|
+
missions half-valid.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
One `<git-common-dir>/dsh-tower/state.json` holds the active document. It is
|
|
20
|
+
read/written through one token-checked lock and one atomic temp+rename write with
|
|
21
|
+
a heartbeat-based stale window. Terminal missions whose worktree is gone are moved
|
|
22
|
+
out to `archive/<year>.json` files under the archive policy; that keeps `state.json`
|
|
23
|
+
bounded without changing the live contract. `modes.json` is a separate per-agent
|
|
24
|
+
mode store and follows the same atomic-write primitives.
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
- A mutation is one lock acquisition and one atomic write, so a reader never sees a
|
|
29
|
+
half-applied mission graph.
|
|
30
|
+
- Cross-mission invariants (unique mission ids, dependencies, roster ownership,
|
|
31
|
+
review binding) are checked in one place before the write.
|
|
32
|
+
- `state.json` is not line-diffable per mission. The board (`MISSIONS.md`), the
|
|
33
|
+
read tools and the archive year files are the human- and diff-facing views.
|
|
34
|
+
- Archiving is a move, never a delete: an unreadable archive pauses archiving
|
|
35
|
+
rather than losing the entries.
|
|
36
|
+
- Two writers are prevented by the lock, not by "please run one tower".
|
|
37
|
+
|
|
38
|
+
## Rejected alternatives
|
|
39
|
+
|
|
40
|
+
- **One JSON file per mission** — makes each mission diffable, but every command
|
|
41
|
+
that touches several missions (plan, roster, batch, gate) must coordinate N
|
|
42
|
+
files with no cross-file atomicity; a crash leaves a partial graph.
|
|
43
|
+
- **A database (SQLite or a bundled store)** — adds a native dependency to a plugin
|
|
44
|
+
that must mount inside arbitrary host builds, and moves the schema migration
|
|
45
|
+
problem out of a file that a user can inspect and back up.
|
|
46
|
+
- **Append-only event log as the source of truth** — good for audit, bad for the
|
|
47
|
+
read path this plugin actually has: every status call would replay history, and
|
|
48
|
+
compaction would become a second correctness problem.
|
|
49
|
+
- **Git-committed mission records** — would make Tower write to the user's history
|
|
50
|
+
on every tool call, which the "never modify the main checkout" boundary refuses.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 0003 — allowAgentControl defaults to true
|
|
2
|
+
|
|
3
|
+
- Date: 2026-10-23
|
|
4
|
+
- Status: Accepted
|
|
5
|
+
- Deciders: maintainer
|
|
6
|
+
- Supersedes: none
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Tower's product claim is a human-owned orchestration mode: the user decides when
|
|
11
|
+
to enter it, and the merge gate is the final authority. The config key
|
|
12
|
+
`mode.allowAgentControl` decides whether the model may call `tower_control` to
|
|
13
|
+
enter or leave the mode. Making it `false` by default would sound safer, but it
|
|
14
|
+
would also mean the feature does not work until the user discovers and flips a
|
|
15
|
+
key, and the model would have no way to leave a mode the user asked it to enter.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
`mode.allowAgentControl` defaults to `true`, and the legacy top-level
|
|
20
|
+
`allowAgentControl` alias keeps the shipped patch behavior. The default is paired
|
|
21
|
+
with independent checks rather than with trust: entering records a reason, a
|
|
22
|
+
user-paused goal is never auto-resumed without an authorization reference, the
|
|
23
|
+
first entry can require a human `/tower ack`, one Tower session per repository can
|
|
24
|
+
hold the workspace lock, and `tower_merge` still runs the preflight gate.
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
- The default profile works: a user can say "use Tower" and the model can comply.
|
|
29
|
+
- Human ownership is enforced by refusal paths (ack, authorization, workspace
|
|
30
|
+
lock, preflight) instead of by a disabled-by-default switch that users would
|
|
31
|
+
turn on anyway.
|
|
32
|
+
- A deployment that wants human-only mode control pins
|
|
33
|
+
`mode.allowAgentControl:false`; doctor warns when a profile still relies on a
|
|
34
|
+
reversed default instead of pinning it.
|
|
35
|
+
- The default is a product decision with a written rationale, so it can be
|
|
36
|
+
revisited on evidence rather than by citing "secure by default".
|
|
37
|
+
|
|
38
|
+
## Rejected alternatives
|
|
39
|
+
|
|
40
|
+
- **Default `false`** — the plugin would appear broken on first use, and the
|
|
41
|
+
existing reversal would be dropped only to look conservative.
|
|
42
|
+
- **No config key at all** — the model could always toggle the mode, with no way
|
|
43
|
+
for a human-only deployment to refuse it.
|
|
44
|
+
- **Require an approval prompt for every entry** — the prompt is an environment
|
|
45
|
+
behavior, not a plugin guarantee; the ack and authorization fields are the
|
|
46
|
+
guarantees this plugin can actually enforce.
|
|
47
|
+
- **Let the model resume a paused goal on exit** — exit is not consent; resuming
|
|
48
|
+
stays a human decision.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 0004 — The concept surface is frozen by a committed baseline
|
|
2
|
+
|
|
3
|
+
- Date: 2026-10-23
|
|
4
|
+
- Status: Accepted
|
|
5
|
+
- Deciders: maintainer
|
|
6
|
+
- Supersedes: none
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Four days after the first publish the plugin carried 60+ config keys and 28 tools.
|
|
11
|
+
The fitness suite measures file shape (line ceilings, pure-domain share) but not
|
|
12
|
+
how many concepts a user has to hold. Line counts can stay green while the real
|
|
13
|
+
surface grows, and every config key is four promises: a reference row, a validator
|
|
14
|
+
branch, a default that must keep working, and an answer to "why did this change my
|
|
15
|
+
behaviour". The same is true of every tool.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Count three surfaces — config leaf keys, config sections and registered tools — in
|
|
20
|
+
one pure module (`domain/surface.js`). Commit the counts in
|
|
21
|
+
`scripts/surface-baseline.json` with a date. `npm run surface:check` fails when a
|
|
22
|
+
live count differs from the baseline; a decision that raises a count must name an
|
|
23
|
+
existing ADR under `docs/adr/` or deletions that are provably absent from the live
|
|
24
|
+
surface. `tower_doctor` prints the counts, the generated config/tools references
|
|
25
|
+
print the same counts plus the baseline date, and every key/tool is classified
|
|
26
|
+
`core`, `tuning` or `niche` in `docs/reference/surface-budget.md`, with a removal
|
|
27
|
+
note for each niche key.
|
|
28
|
+
|
|
29
|
+
## Consequences
|
|
30
|
+
|
|
31
|
+
- Growth is visible in a diff instead of arriving one reasonable-looking key at a
|
|
32
|
+
time.
|
|
33
|
+
- A reduction is cheap: the baseline can be ratcheted down with no ADR.
|
|
34
|
+
- The classification table is auditable, not decorative: the check fails when a
|
|
35
|
+
live key or tool is missing from it.
|
|
36
|
+
- The numbers are three lines in `tower_doctor` output, not a new dashboard.
|
|
37
|
+
- This does not remove any key in this release; it is a brake, not a clean-up.
|
|
38
|
+
|
|
39
|
+
## Rejected alternatives
|
|
40
|
+
|
|
41
|
+
- **A line-count or file-size rule** — the project already has those and the
|
|
42
|
+
surface grew while they stayed green; they measure the wrong thing.
|
|
43
|
+
- **A hard cap with no escape hatch** — a real capability would be blocked, and the
|
|
44
|
+
first exception would be added as an unexplained waiver.
|
|
45
|
+
- **A per-key count in each generated reference** — two references can disagree;
|
|
46
|
+
one shared baseline plus one check keeps the counts equal.
|
|
47
|
+
- **Only an ADR requirement** — a pure deletion would need paperwork even though it
|
|
48
|
+
shrinks the surface; the matching-deletion path keeps reductions cheap.
|
|
49
|
+
- **A GitHub issue label instead of a repo file** — the brake must run offline in
|
|
50
|
+
`npm test` and in CI without a token.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Architecture decision records
|
|
2
|
+
|
|
3
|
+
An ADR is a dated record of a decision that is already load-bearing. It is not a
|
|
4
|
+
proposal: the status says whether the decision is accepted, and the rejected
|
|
5
|
+
alternatives exist so a future reader can see what was actually weighed.
|
|
6
|
+
|
|
7
|
+
The engineering principles that these records serve are in
|
|
8
|
+
[../PRINCIPLES.md](../PRINCIPLES.md). `npm run docs:check:philosophy` requires at
|
|
9
|
+
least three numbered ADRs, each with Context, Decision, Consequences, Rejected
|
|
10
|
+
alternatives, Date and Status.
|
|
11
|
+
|
|
12
|
+
| ADR | Decision | Date |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| [0001](0001-ports-adapters-as-data.md) | Ports and adapters are data, not a dependency-injection container | 2026-10-23 |
|
|
15
|
+
| [0002](0002-single-state-json.md) | One `state.json` is the mission persistence contract | 2026-10-23 |
|
|
16
|
+
| [0003](0003-allow-agent-control-default-true.md) | `allowAgentControl` defaults to `true` | 2026-10-23 |
|
|
17
|
+
| [0004](0004-surface-budget.md) | The concept surface is frozen by a committed baseline | 2026-10-23 |
|
|
18
|
+
|
|
19
|
+
## Template
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# NNNN — Decision title
|
|
23
|
+
|
|
24
|
+
- Date: YYYY-MM-DD
|
|
25
|
+
- Status: Proposed | Accepted | Superseded by NNNN
|
|
26
|
+
- Deciders: <who>
|
|
27
|
+
|
|
28
|
+
## Context
|
|
29
|
+
|
|
30
|
+
What forced the decision.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
What was decided, in one paragraph.
|
|
35
|
+
|
|
36
|
+
## Consequences
|
|
37
|
+
|
|
38
|
+
What becomes easier, what becomes harder, and what is now refused.
|
|
39
|
+
|
|
40
|
+
## Rejected alternatives
|
|
41
|
+
|
|
42
|
+
- **Alternative** — why it was not chosen.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Non-goals
|
|
46
|
+
|
|
47
|
+
These records do not form an RFC queue, an architecture review board, or a
|
|
48
|
+
process for approving ordinary changes.
|
|
49
|
+
|
|
50
|
+
Unowned. This is a documentation convention, not a gate; nothing in CI blocks a
|
|
51
|
+
new ADR or rewards one.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Execution workflow: Matt Pocock skills × Tower
|
|
2
|
+
|
|
3
|
+
This repository uses two workflows together. They are not alternatives: the **Matt Pocock skills** own thinking, planning, review discipline and retros; **Tower** owns delivery — isolated worktrees, scope fences, review gates, merges and durable mission state. **GitHub Issues** is the single source of truth for what is planned, in progress, blocked and shipped.
|
|
4
|
+
|
|
5
|
+
## Who owns what
|
|
6
|
+
|
|
7
|
+
| Stage | Owner | Artifact |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Intake | Matt Pocock `/triage` for raw external reports; milestones + `P*`/`area/*` labels for self-authored findings | issue with owner-facing state |
|
|
10
|
+
| Plan | `/to-spec` → `/to-tickets` (or an existing issue whose acceptance criteria are already ticket-shaped) | spec issue, tickets with native `blocked_by` edges |
|
|
11
|
+
| Execute | `/implement-spec` subagents for one-session waves; **Tower missions** for parallel or long-lived work | integration branch + PRs, or mission worktrees + merge gate |
|
|
12
|
+
| Build each unit | `/tdd` inside the ticket/mission | failing test first, then green |
|
|
13
|
+
| Verify | gates + required CI checks + `/code-review` (Standards + Spec) | green PR with two-axis review |
|
|
14
|
+
| Land | protected `main`, four-line PR body, `verified-by:`/`commit:` closure | merged PR, closed issues |
|
|
15
|
+
| Release | `release:check`, CHANGELOG, COMPATIBILITY, tag; publish only when planned | tag-only or published release |
|
|
16
|
+
| Learn | `/retro`; mechanical findings become checks, judgement becomes standards | CONTRIBUTING/AGENTS/scanner updates |
|
|
17
|
+
|
|
18
|
+
## The flow
|
|
19
|
+
|
|
20
|
+
1. **Intake.** Raw external bug reports and feature requests go through `/triage` (they are not ours). Findings we file ourselves (audits, retros) skip triage and get a priority, an area and a milestone; they are already acceptance-criteria-shaped.
|
|
21
|
+
2. **Plan.** Multi-session work goes `/to-spec` → `/to-tickets`. Tickets are tracer bullets with native blocking edges; the frontier is whatever has no open blockers. Small work may skip the spec when the issue already states acceptance criteria (e.g. a patch ticket).
|
|
22
|
+
3. **Choose the orchestrator.**
|
|
23
|
+
- **One-session wave (≤3 tickets, mostly independent):** `/implement-spec`-style: an integration branch, one implementer subagent per ticket in its own worktree, merger merges to the integration branch, one PR to `main`.
|
|
24
|
+
- **Parallel or long-lived wave (architecture epics, >3 tickets, review needed):** **Tower**. `tower_init` → `tower_plan` with one mission per ticket, `scope` = the ticket's write fence, `deps` = the ticket's blocking edges → `tower_spawn` a worker per mission → optional reviewer (`tower-reviewer`) → `tower_merge` (gate checks scope, branch freshness, review, tasks) → `tower_teardown`. Tower's persistent state makes the wave resumable after a restart.
|
|
25
|
+
- Never run both orchestrators over the same files at once.
|
|
26
|
+
4. **Build.** Each unit starts with `/tdd`: one failing test, then the implementation, then the next slice. Commit messages carry `Refs #n`; the completing commit uses `Fixes #n`. Workers do not merge; they hand off a branch.
|
|
27
|
+
5. **Verify before landing.** Run `npm test`, `npm run docs:check`, all four `docs:check:generated*`, `npm run pack:check`, `npm run architecture:check`, `npm run types:check`, `npm run domain:check`. Bundle changes also run `npm run smoke:mount`. CI must be green on the required checks (`test (22)`, `test (24)`, `test (22, windows)`, `mount`). Then `/code-review` runs both axes on the diff (Standards against CONTRIBUTING/architecture docs, Spec against the ticket); every finding is fixed in the same PR or explicitly deferred with a reason.
|
|
28
|
+
6. **Land.** PR body follows the four-line contract (failure mode → change → test that fails without it → blast radius). `main` is protected: PR + required checks, admins included, no force-push. A closure comment carries `verified-by: <test file>` and `commit: <sha>`; `npm run audit:issues` flags closures whose evidence is missing.
|
|
29
|
+
7. **Release.** `release:check` compares the version, the previous tag and the CHANGELOG; a tag push fails when the version did not move or the tag does not match. Releases are either **tag-only** (version + changelog + compatibility row + annotated tag; no npm publish, no GitHub Release) or **published** (adds the human `npm publish --tag latest` with 2FA, `dist-tags` verification, then the GitHub Release). The mode is recorded in the milestone/plan.
|
|
30
|
+
8. **Learn.** After a release (or a wave that went sideways) run `/retro`: mechanical mistakes become deterministic checks (scripts, CI jobs, preflight), judgement calls become written standards. Periodically run `/improve-codebase-architecture` for deepening opportunities and `/domain-modeling` to keep the vocabulary and ADRs (2.3.0) honest.
|
|
31
|
+
|
|
32
|
+
## Decision table
|
|
33
|
+
|
|
34
|
+
| Situation | Flow |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Raw external issue | `/triage` → then plan |
|
|
37
|
+
| Design question you cannot settle on paper | `/grill-with-docs`, detour through `/prototype` if it needs a runnable answer |
|
|
38
|
+
| Multi-session feature | `/to-spec` → `/to-tickets` → `/implement-spec` |
|
|
39
|
+
| Small fix with acceptance criteria | ticket → `/implement` (+ `/tdd`) |
|
|
40
|
+
| Architecture epic / parallel missions | Tower plan → missions → review gate → merge |
|
|
41
|
+
| Something broken, cause unknown | `/diagnosing-bugs` (tight red loop first) |
|
|
42
|
+
| Repeated mistakes | `/retro` → checks/standards |
|
|
43
|
+
| Release | release plan: tag-only or publish |
|
|
44
|
+
|
|
45
|
+
## Invariants
|
|
46
|
+
|
|
47
|
+
- One ticket per PR series; a branch never touches two tickets.
|
|
48
|
+
- Tracked blocking edges, not prose: work only the frontier.
|
|
49
|
+
- Tower missions: `scope` is the write fence, a mission that outgrows it is re-scoped, not silently widened; the workspace lock keeps one Tower session per repository.
|
|
50
|
+
- Human-only gates: npm publish/2FA, branch-protection changes, anything behind a third-party credential. Stop and hand over rather than inventing a credential path.
|
|
51
|
+
- Context hygiene: keep planning in one window until tickets exist; start each implementation unit from the ticket, not from the previous unit's context.
|