dsh-tower 2.0.4 → 2.1.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 +152 -0
- package/COMPATIBILITY.md +39 -0
- package/CONTRIBUTING.md +157 -2
- package/README.en.md +46 -3
- package/README.md +44 -3
- package/SECURITY.md +27 -0
- package/adapters/default.js +12 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/archive.js +190 -0
- package/app/batch.js +79 -0
- package/app/board.js +27 -0
- package/app/checkCompletion.js +89 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +126 -0
- package/app/init.js +291 -0
- package/app/merge.js +573 -0
- package/app/messages.js +66 -0
- package/app/mission.js +37 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +50 -0
- package/app/provision.js +193 -0
- package/app/read.js +113 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +147 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +205 -0
- package/app/tower.js +552 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +3 -1
- 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/architecture/overview.md +114 -13
- package/docs/board.md +72 -0
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +25 -1
- package/docs/guide/release.md +125 -0
- package/docs/operations/security.md +47 -2
- package/docs/reference/blockers.md +1 -1
- package/docs/reference/config.generated.md +70 -0
- package/docs/reference/config.md +8 -1
- package/docs/reference/doctor.md +58 -8
- package/docs/reference/errors.generated.md +41 -0
- package/docs/reference/errors.md +7 -3
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +7 -6
- package/docs/zh/README.md +3 -2
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +18 -1
- package/docs/zh/operations/security.md +30 -1
- package/docs/zh/reference/blockers.md +1 -1
- package/docs/zh/reference/config.md +8 -1
- package/docs/zh/reference/doctor.md +35 -2
- package/docs/zh/reference/errors.md +6 -2
- package/docs/zh/reference/tools.md +3 -3
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +427 -0
- package/domain/contracts/outputs.js +305 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +95 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +98 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +190 -0
- package/domain/integration/init.js +261 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +167 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +186 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +121 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +141 -0
- package/domain/report.js +67 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +177 -0
- package/domain/state/archive.js +257 -0
- package/domain/state/migrations.js +145 -0
- package/domain/state/schema.js +239 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +183 -0
- package/domain/summary.js +116 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +63 -20
- package/lib/archive.js +279 -0
- package/lib/briefing.js +176 -0
- package/lib/caseSensitivity.js +66 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +33 -375
- package/lib/doctor.js +275 -4
- package/lib/git.js +60 -16
- package/lib/hmr.js +4 -3
- package/lib/leases.js +17 -7
- package/lib/legacy.js +39 -14
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +11 -43
- package/lib/persistence.js +356 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/stateFile.js +99 -0
- package/lib/store.js +67 -135
- package/lib/tests.js +17 -12
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +2 -2
- package/lib/worktreeOwnership.js +22 -10
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +32 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +9 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +212 -0
- package/scripts/release-check.mjs +303 -0
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +45 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +102 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +373 -0
- package/lib/errors.js +0 -123
- package/lib/tools.js +0 -2460
- package/lib/tower.js +0 -2912
- /package/{lib → domain}/batch.js +0 -0
package/app/init.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// `tower_init` use case, split into three phases (#51):
|
|
2
|
+
//
|
|
3
|
+
// prepare — dirty/committed/base/snapshot decisions, all lock-free (no state
|
|
4
|
+
// lock and no git lock; git reads only).
|
|
5
|
+
// act — the only Git mutations (stash/update-ref/for-each-ref) run under
|
|
6
|
+
// the repo-scoped git mutex, never under the state lock.
|
|
7
|
+
// commit — one short state lock writes/claims/reanchors, revalidating the
|
|
8
|
+
// state revision captured by prepare.
|
|
9
|
+
import { withLock, withGitLock, exists, ensureDir, writeJsonAtomic, appendJsonl } from '../lib/store.js'
|
|
10
|
+
import { detectCaseSensitivity } from '../lib/caseSensitivity.js'
|
|
11
|
+
import { validateState } from '../domain/state.js'
|
|
12
|
+
import { TowerToolError } from '../domain/errors.js'
|
|
13
|
+
import {
|
|
14
|
+
SNAPSHOT_REF_PREFIX,
|
|
15
|
+
caseProbeActivityText,
|
|
16
|
+
claimCaller,
|
|
17
|
+
createdInitResult,
|
|
18
|
+
existingInitResult,
|
|
19
|
+
existingInitWriteResult,
|
|
20
|
+
explicitBase,
|
|
21
|
+
fallbackBase,
|
|
22
|
+
initActivityText,
|
|
23
|
+
initialState,
|
|
24
|
+
invalidBaseMessage,
|
|
25
|
+
ownedByOther,
|
|
26
|
+
ownerClaimActivityText,
|
|
27
|
+
reanchorActivityText,
|
|
28
|
+
reanchorBlockedMessage,
|
|
29
|
+
shouldReanchor,
|
|
30
|
+
shouldSnapshot,
|
|
31
|
+
snapshotRefFor,
|
|
32
|
+
} from '../domain/integration/init.js'
|
|
33
|
+
|
|
34
|
+
function fail(message, code = 'invalid-argument') {
|
|
35
|
+
throw new TowerToolError(code, message)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Probe the checkout's own filesystem through the injected seam. The default
|
|
40
|
+
* composition uses the real probe; a composition that predates #90 keeps
|
|
41
|
+
* working through the fallback.
|
|
42
|
+
* @param {any} tower
|
|
43
|
+
* @returns {Promise<boolean | null>}
|
|
44
|
+
*/
|
|
45
|
+
async function detectCaseMode(tower) {
|
|
46
|
+
const detect = tower.deps?.caseSensitivity?.detect
|
|
47
|
+
if (typeof detect === 'function') return detect(tower.root)
|
|
48
|
+
return detectCaseSensitivity(tower.root)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Phase 1 of `Tower.init` (#51). A read-only init returns its result here. */
|
|
52
|
+
async function prepareInit(tower, input) {
|
|
53
|
+
const dirty = !(await tower.isClean(tower.root))
|
|
54
|
+
const committed = ((await tower.tryGit(tower.root, ['rev-list', '-n', '1', '--all'])) ?? '').trim().length > 0
|
|
55
|
+
|
|
56
|
+
const resolveBase = async () => {
|
|
57
|
+
const explicit = explicitBase(input.base)
|
|
58
|
+
if (explicit) {
|
|
59
|
+
const message = invalidBaseMessage(explicit)
|
|
60
|
+
if (message) fail(message)
|
|
61
|
+
if (committed && (await tower.tryGit(tower.root, ['show-ref', '--verify', '--quiet', `refs/heads/${explicit}`])) === null) {
|
|
62
|
+
fail(`base branch "${explicit}" does not exist locally`)
|
|
63
|
+
}
|
|
64
|
+
return explicit
|
|
65
|
+
}
|
|
66
|
+
const fallback = fallbackBase({ branch: tower.branch, committed })
|
|
67
|
+
if ('error' in fallback) fail(fallback.error)
|
|
68
|
+
return /** @type {{ base: string }} */ (fallback).base
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (await exists(tower.paths.state)) {
|
|
72
|
+
const state = await tower.load()
|
|
73
|
+
const owner = state.towerAgentId ? String(state.towerAgentId) : null
|
|
74
|
+
const caller = input.towerAgentId ? String(input.towerAgentId) : null
|
|
75
|
+
if (ownedByOther(owner, caller)) {
|
|
76
|
+
fail(`tower state is owned by another agent; ${caller} is not the tower`, 'not-authorized')
|
|
77
|
+
}
|
|
78
|
+
// "Detect once": a state that already carries a boolean is never re-probed,
|
|
79
|
+
// so a transient probe failure cannot silently flip the comparison mode.
|
|
80
|
+
const known = state.caseInsensitive === true || state.caseInsensitive === false
|
|
81
|
+
const caseInsensitive = known ? null : await detectCaseMode(tower)
|
|
82
|
+
let reanchorBase = null
|
|
83
|
+
if (input.reanchor === true) {
|
|
84
|
+
const refusal = reanchorBlockedMessage(state.missions)
|
|
85
|
+
if (refusal) fail(refusal, 'invalid-transition')
|
|
86
|
+
reanchorBase = await resolveBase()
|
|
87
|
+
}
|
|
88
|
+
const claim = claimCaller(owner, caller) !== null
|
|
89
|
+
const reanchor = shouldReanchor({ reanchor: input.reanchor, reanchorBase, currentBase: state.base })
|
|
90
|
+
if (!claim && !reanchor && caseInsensitive === null) {
|
|
91
|
+
return { early: existingInitResult({ state, statePath: tower.paths.state, dirty }) }
|
|
92
|
+
}
|
|
93
|
+
return { stateRevision: state.revision, claim, reanchor, base: reanchorBase, dirty, caseInsensitive }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
create: true,
|
|
98
|
+
base: await resolveBase(),
|
|
99
|
+
snapshot: shouldSnapshot({ snapshotDirty: input.snapshotDirty, dirty }),
|
|
100
|
+
dirty,
|
|
101
|
+
caseInsensitive: await detectCaseMode(tower),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Phase 2a of `Tower.init` (#51): create the dirty snapshot and its keeping ref
|
|
107
|
+
* under the git mutex. The prepared "no state file yet" decision is
|
|
108
|
+
* revalidated first so a concurrent init cannot leave an unreferenced snapshot.
|
|
109
|
+
*/
|
|
110
|
+
async function actInitCreate(tower, prepared) {
|
|
111
|
+
return withGitLock(tower.paths, async () => {
|
|
112
|
+
if (await exists(tower.paths.state)) return { stale: true }
|
|
113
|
+
if (!prepared.snapshot) return { snapshotCommit: null, snapshotRef: null }
|
|
114
|
+
const snapshotCommit = ((await tower.tryGit(tower.root, ['stash', 'create'])) ?? '').trim() || null
|
|
115
|
+
if (!snapshotCommit) {
|
|
116
|
+
fail('snapshotDirty was requested but no snapshot commit could be created; commit or stash the working tree first')
|
|
117
|
+
}
|
|
118
|
+
// A dangling commit can be pruned by git gc; keep a ref so the mission
|
|
119
|
+
// baseline stays reachable for the life of the state.
|
|
120
|
+
const snapshotRef = snapshotRefFor(snapshotCommit)
|
|
121
|
+
await tower.git(tower.root, ['update-ref', snapshotRef, snapshotCommit])
|
|
122
|
+
return { snapshotCommit, snapshotRef }
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Phase 2b of `Tower.init` (#51): snapshot-ref cleanup for a base reanchor. */
|
|
127
|
+
async function actInitReanchor(tower, prepared) {
|
|
128
|
+
return withGitLock(tower.paths, async () => {
|
|
129
|
+
const state = await tower.load()
|
|
130
|
+
if (state.revision !== prepared.stateRevision) return { staleRevision: state.revision }
|
|
131
|
+
if (state.base === prepared.base) return { deleted: [] }
|
|
132
|
+
const refs =
|
|
133
|
+
(await tower.tryGit(tower.root, ['for-each-ref', '--format=%(refname) %(objectname)', SNAPSHOT_REF_PREFIX])) ?? ''
|
|
134
|
+
const deleted = []
|
|
135
|
+
for (const line of refs.split('\n').map((entry) => entry.trim()).filter(Boolean)) {
|
|
136
|
+
const [ref, sha] = line.split(' ')
|
|
137
|
+
if (!ref) continue
|
|
138
|
+
try {
|
|
139
|
+
await tower.git(tower.root, ['update-ref', '-d', ref])
|
|
140
|
+
deleted.push({ ref, sha })
|
|
141
|
+
} catch {
|
|
142
|
+
// best effort: the single-phase code never failed init on a stale snapshot ref
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { deleted }
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Restore snapshot refs when the reanchor commit is rejected after phase 2. */
|
|
150
|
+
async function restoreInitRefs(tower, deleted) {
|
|
151
|
+
const entries = deleted.filter((entry) => typeof entry.sha === 'string' && entry.sha.length > 0)
|
|
152
|
+
if (entries.length === 0) return
|
|
153
|
+
await withGitLock(tower.paths, async () => {
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
await tower.git(tower.root, ['update-ref', entry.ref, entry.sha]).catch(() => {})
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Phase 3a of `Tower.init` (#51): write the initial state under a short lock. */
|
|
161
|
+
async function commitInitCreate(tower, prepared, acted, input) {
|
|
162
|
+
return withLock(tower.paths, async () => {
|
|
163
|
+
// Revision revalidation for the create path: prepare saw "absent", so a
|
|
164
|
+
// state file that appeared meanwhile must never be overwritten.
|
|
165
|
+
if (await exists(tower.paths.state)) {
|
|
166
|
+
throw new TowerToolError('stale-base', 'tower state appeared while init was being prepared; retry')
|
|
167
|
+
}
|
|
168
|
+
const now = Date.now()
|
|
169
|
+
const state = initialState({
|
|
170
|
+
repoRoot: tower.root,
|
|
171
|
+
commonDir: tower.commonDir,
|
|
172
|
+
base: prepared.base,
|
|
173
|
+
dirty: prepared.dirty,
|
|
174
|
+
towerAgentId: input.towerAgentId ? String(input.towerAgentId) : null,
|
|
175
|
+
snapshotCommit: acted.snapshotCommit,
|
|
176
|
+
snapshotRef: acted.snapshotRef,
|
|
177
|
+
caseInsensitive: prepared.caseInsensitive,
|
|
178
|
+
now,
|
|
179
|
+
})
|
|
180
|
+
validateState(state, tower.paths.state)
|
|
181
|
+
await writeJsonAtomic(tower.paths.state, state)
|
|
182
|
+
await appendJsonl(tower.paths.activity, {
|
|
183
|
+
at: now,
|
|
184
|
+
type: 'init',
|
|
185
|
+
text: initActivityText({
|
|
186
|
+
base: prepared.base,
|
|
187
|
+
snapshotCommit: acted.snapshotCommit,
|
|
188
|
+
dirty: prepared.dirty,
|
|
189
|
+
caseInsensitive: prepared.caseInsensitive,
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
return createdInitResult({
|
|
193
|
+
repoRoot: tower.root,
|
|
194
|
+
commonDir: tower.commonDir,
|
|
195
|
+
statePath: tower.paths.state,
|
|
196
|
+
base: prepared.base,
|
|
197
|
+
dirty: prepared.dirty,
|
|
198
|
+
snapshotCommit: acted.snapshotCommit,
|
|
199
|
+
snapshotRef: acted.snapshotRef,
|
|
200
|
+
caseInsensitive: prepared.caseInsensitive,
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Phase 3b of `Tower.init` (#51): owner claim and reanchor writes under a short lock. */
|
|
206
|
+
async function commitInitExisting(tower, prepared, input) {
|
|
207
|
+
return withLock(tower.paths, async () => {
|
|
208
|
+
const state = await tower.load()
|
|
209
|
+
if (state.revision !== prepared.stateRevision) {
|
|
210
|
+
throw new TowerToolError(
|
|
211
|
+
'stale-base',
|
|
212
|
+
`tower state changed while init was being prepared (${prepared.stateRevision} -> ${state.revision}); retry`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
const now = Date.now()
|
|
216
|
+
const owner = state.towerAgentId ? String(state.towerAgentId) : null
|
|
217
|
+
const caller = input.towerAgentId ? String(input.towerAgentId) : null
|
|
218
|
+
if (ownedByOther(owner, caller)) {
|
|
219
|
+
fail(`tower state is owned by another agent; ${caller} is not the tower`, 'not-authorized')
|
|
220
|
+
}
|
|
221
|
+
const claimant = claimCaller(owner, caller)
|
|
222
|
+
if (claimant) {
|
|
223
|
+
state.towerAgentId = claimant
|
|
224
|
+
state.revision += 1
|
|
225
|
+
state.updatedAt = now
|
|
226
|
+
validateState(state, tower.paths.state)
|
|
227
|
+
await writeJsonAtomic(tower.paths.state, state)
|
|
228
|
+
await appendJsonl(tower.paths.activity, {
|
|
229
|
+
at: now,
|
|
230
|
+
type: 'init',
|
|
231
|
+
text: ownerClaimActivityText(claimant),
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
let reanchored = false
|
|
235
|
+
// Record the one-time case-sensitivity probe (#90). An unknown result is
|
|
236
|
+
// not persisted, so a later init can retry it without bumping the revision
|
|
237
|
+
// on every call.
|
|
238
|
+
if (prepared.caseInsensitive !== null && state.caseInsensitive !== prepared.caseInsensitive) {
|
|
239
|
+
state.caseInsensitive = prepared.caseInsensitive
|
|
240
|
+
state.revision += 1
|
|
241
|
+
state.updatedAt = now
|
|
242
|
+
validateState(state, tower.paths.state)
|
|
243
|
+
await writeJsonAtomic(tower.paths.state, state)
|
|
244
|
+
await appendJsonl(tower.paths.activity, {
|
|
245
|
+
at: now,
|
|
246
|
+
type: 'init',
|
|
247
|
+
text: caseProbeActivityText(prepared.caseInsensitive),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
if (prepared.reanchor && state.base !== prepared.base) {
|
|
251
|
+
state.base = prepared.base
|
|
252
|
+
state.baseChangedAt = now
|
|
253
|
+
delete state.snapshotCommit
|
|
254
|
+
delete state.snapshotRef
|
|
255
|
+
reanchored = true
|
|
256
|
+
state.revision += 1
|
|
257
|
+
state.updatedAt = now
|
|
258
|
+
validateState(state, tower.paths.state)
|
|
259
|
+
await writeJsonAtomic(tower.paths.state, state)
|
|
260
|
+
await appendJsonl(tower.paths.activity, { at: now, type: 'init', text: reanchorActivityText(prepared.base) })
|
|
261
|
+
}
|
|
262
|
+
return existingInitWriteResult({ state, statePath: tower.paths.state, dirty: prepared.dirty, reanchored })
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** #51 two-phase init: prepare/act/commit with stale-state retries. */
|
|
267
|
+
export async function init(tower, input = {}) {
|
|
268
|
+
await ensureDir(tower.paths.dir)
|
|
269
|
+
const prepared = await prepareInit(tower, input)
|
|
270
|
+
if (prepared.early) return prepared.early
|
|
271
|
+
if (prepared.create) {
|
|
272
|
+
const acted = await actInitCreate(tower, prepared)
|
|
273
|
+
if (acted.stale) {
|
|
274
|
+
throw new TowerToolError('stale-base', 'tower state appeared while init was being prepared; retry')
|
|
275
|
+
}
|
|
276
|
+
return commitInitCreate(tower, prepared, acted, input)
|
|
277
|
+
}
|
|
278
|
+
const acted = prepared.reanchor ? await actInitReanchor(tower, prepared) : { deleted: [] }
|
|
279
|
+
if (acted.staleRevision !== undefined) {
|
|
280
|
+
throw new TowerToolError(
|
|
281
|
+
'stale-base',
|
|
282
|
+
`tower state changed while init was being prepared (${prepared.stateRevision} -> ${acted.staleRevision}); retry`,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return await commitInitExisting(tower, prepared, input)
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (acted.deleted.length > 0) await restoreInitRefs(tower, acted.deleted)
|
|
289
|
+
throw error
|
|
290
|
+
}
|
|
291
|
+
}
|