dsh-plugin-worktrees 0.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/AGENTS.md +120 -0
- package/CHANGELOG.md +266 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/README.zh.md +151 -0
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +30 -0
- package/docs/DESIGN.md +783 -0
- package/docs/TASKS.md +164 -0
- package/lib/config.js +126 -0
- package/lib/engine-face.js +328 -0
- package/lib/git-port.js +773 -0
- package/lib/index.js +402 -0
- package/lib/merge-queue.js +832 -0
- package/lib/naming.js +107 -0
- package/lib/repo-gate.js +202 -0
- package/lib/state-store.js +512 -0
- package/lib/tools/worktree-cleanup.js +127 -0
- package/lib/tools/worktree-create.js +212 -0
- package/lib/tools/worktree-list.js +234 -0
- package/lib/tools/worktree-merge.js +396 -0
- package/lib/tools/worktree-queue.js +330 -0
- package/lib/tools/worktree-status.js +194 -0
- package/lib/worktree-service.js +673 -0
- package/package.json +55 -0
- package/scripts/link-harness-dsh-tools.sh +95 -0
- package/scripts/lint.js +142 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-worktrees — apply() assembly (T10, DESIGN §3 / §7.3 / §8.1 / §8.4).
|
|
3
|
+
*
|
|
4
|
+
* Single instance: git port / StateStore / WorktreeService / MergeQueue are
|
|
5
|
+
* created exactly once inside the apply() closure (red line: shared state is
|
|
6
|
+
* held by the single instance — the same discipline as the engine layer's
|
|
7
|
+
* "no host service imports" seam for a future dag-orchestrator, §10).
|
|
8
|
+
*
|
|
9
|
+
* apply() order (DESIGN §3 diagram + §7.3 "before tool registration"):
|
|
10
|
+
*
|
|
11
|
+
* a. validateConfig (lib/config.js zod strict; unknown keys fail loud);
|
|
12
|
+
* b. assertSingleDshToolsInstance — the @deepseek-ai/dsh-tools dual-
|
|
13
|
+
* instance self-check (§8.4, pattern carried over verbatim from
|
|
14
|
+
* dsh-plugin-subagents lib/index.js): the host ToolRuntime carries its
|
|
15
|
+
* scheduler under the dsh-tools module-level TOOL_RUNTIME_SCHEDULER
|
|
16
|
+
* Symbol; a second physical copy of the package makes
|
|
17
|
+
* `ctx.tools[Symbol]` undefined and every tool call dies with
|
|
18
|
+
* "Cannot read properties of undefined (reading 'prepare')" — fatal at
|
|
19
|
+
* apply time, with the setup:peer re-link hint;
|
|
20
|
+
* c. createGitPort (timeoutMs = gitTimeoutMs) + createStateStore +
|
|
21
|
+
* store.load() (a corrupt state file aborts the boot loudly);
|
|
22
|
+
* d. reconcile — the crash reconciliation (§7.3), BEFORE any tool is
|
|
23
|
+
* registered, so the first model-visible call already sees a truthful
|
|
24
|
+
* state: per repo — unreachable repo ⇒ `orphaned: true` MARKER on its
|
|
25
|
+
* worktree records (state kept verbatim — orphaned is a §5.4 flag, not
|
|
26
|
+
* a §5.2.2 state) + applying jobs failed; reachable repo ⇒ records
|
|
27
|
+
* whose path left `git worktree list` become `vanished`, applying jobs
|
|
28
|
+
* become failed (their still-live integration worktree retained for
|
|
29
|
+
* inspection), conflicted jobs survive verbatim, and orphan
|
|
30
|
+
* `.integration/` worktrees are best-effort removed;
|
|
31
|
+
* e. createWorktreeService + createMergeQueue over the SAME store/git,
|
|
32
|
+
* then provide the §10 `worktreesEngine` service face (the DAG
|
|
33
|
+
* composition seam) over those same singletons;
|
|
34
|
+
* f. the §5.2.0 gate prebound once (workspaceRegistry probed OPTIONALLY —
|
|
35
|
+
* it is not in `inject`, so headless hosts degrade to the session-cwd
|
|
36
|
+
* + allowedRoots arms) and shared by all six tools;
|
|
37
|
+
* g. register the six tools per the register switches (default all on).
|
|
38
|
+
*
|
|
39
|
+
* Return value: ALWAYS undefined — the loader treats the plugin callback's
|
|
40
|
+
* return as a disposable (a non-nullable non-function fails real boots with
|
|
41
|
+
* "TypeError: Invalid effect"). Introspection goes through the registered
|
|
42
|
+
* tools (fake ctx) or the internal modules, never the return value.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { TOOL_RUNTIME_SCHEDULER } from '@deepseek-ai/dsh-tools'
|
|
46
|
+
import path from 'node:path'
|
|
47
|
+
import { realpathSync } from 'node:fs'
|
|
48
|
+
import { validateConfig } from './config.js'
|
|
49
|
+
import { createGitPort } from './git-port.js'
|
|
50
|
+
import { createStateStore, ACTIVE_JOB_STATES } from './state-store.js'
|
|
51
|
+
import { createWorktreeService } from './worktree-service.js'
|
|
52
|
+
import { createMergeQueue } from './merge-queue.js'
|
|
53
|
+
import { createEngineFace } from './engine-face.js'
|
|
54
|
+
import { resolveRepoRoot } from './repo-gate.js'
|
|
55
|
+
import { registerWorktreeCreateTool } from './tools/worktree-create.js'
|
|
56
|
+
import { registerWorktreeListTool } from './tools/worktree-list.js'
|
|
57
|
+
import { registerWorktreeStatusTool } from './tools/worktree-status.js'
|
|
58
|
+
import { registerWorktreeMergeTool } from './tools/worktree-merge.js'
|
|
59
|
+
import { registerWorktreeQueueTool } from './tools/worktree-queue.js'
|
|
60
|
+
import { registerWorktreeCleanupTool } from './tools/worktree-cleanup.js'
|
|
61
|
+
|
|
62
|
+
export const name = 'dsh-worktrees'
|
|
63
|
+
|
|
64
|
+
// workspaceRegistry is used through OPTIONAL probing (§8.2 peer-face
|
|
65
|
+
// minimisation: headless hosts without a workspace registry still boot) —
|
|
66
|
+
// only `tools` is a hard injection.
|
|
67
|
+
export const inject = ['tools']
|
|
68
|
+
|
|
69
|
+
/** Error message of an unknown throw. */
|
|
70
|
+
function errorText(error) {
|
|
71
|
+
return error instanceof Error ? error.message : String(error ?? '')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** realpath() that yields null instead of throwing (missing-path probe). */
|
|
75
|
+
function realPathBestEffort(target) {
|
|
76
|
+
try {
|
|
77
|
+
return realpathSync(target)
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* dsh-tools dual-instance self-check (§8.4; the verbatim probe pattern from
|
|
85
|
+
* dsh-plugin-subagents lib/index.js):
|
|
86
|
+
*
|
|
87
|
+
* 1. `ctx.tools[TOOL_RUNTIME_SCHEDULER] !== undefined` → same physical
|
|
88
|
+
* module — healthy;
|
|
89
|
+
* 2. Symbol absent but ctx.tools looks like a real ToolRuntime
|
|
90
|
+
* (view + schemas) → a true second copy: logger.fatal + throw (apply
|
|
91
|
+
* failing beats every tool call dying after load), pointing at the
|
|
92
|
+
* peer re-link (npm run setup:peer);
|
|
93
|
+
* 3. anything else (fake ctx / an unseen host shape) → cannot reliably
|
|
94
|
+
* judge; warn only. Tests and non-standard hosts must not be killed by
|
|
95
|
+
* a false positive.
|
|
96
|
+
*/
|
|
97
|
+
function assertSingleDshToolsInstance(ctx) {
|
|
98
|
+
const tools = ctx && ctx.tools
|
|
99
|
+
if (!tools || typeof tools.register !== 'function') return
|
|
100
|
+
if (tools[TOOL_RUNTIME_SCHEDULER] !== undefined) return // same physical module — healthy
|
|
101
|
+
const looksLikeToolRuntime =
|
|
102
|
+
typeof tools.view === 'function' && typeof tools.schemas === 'function'
|
|
103
|
+
if (looksLikeToolRuntime) {
|
|
104
|
+
const detail =
|
|
105
|
+
'dsh-worktrees: detected a second @deepseek-ai/dsh-tools module instance — '
|
|
106
|
+
+ "every tool call from this plugin would die with \"Cannot read properties of undefined (reading 'prepare')\". "
|
|
107
|
+
+ 'Run npm run setup:peer (scripts/link-harness-dsh-tools.sh) in the dsh-worktrees package '
|
|
108
|
+
+ 'so its dsh-tools copy resolves to the live harness root, then restart dsh.'
|
|
109
|
+
if (ctx.logger && typeof ctx.logger.fatal === 'function') ctx.logger.fatal(detail)
|
|
110
|
+
throw new Error(detail)
|
|
111
|
+
}
|
|
112
|
+
if (ctx.logger && typeof ctx.logger.warn === 'function') {
|
|
113
|
+
ctx.logger.warn(
|
|
114
|
+
'dsh-worktrees: could not reliably verify the @deepseek-ai/dsh-tools single-instance invariant '
|
|
115
|
+
+ 'from this ctx (no scheduler symbol, no ToolRuntime shape) — run npm run setup:peer to check the dedupe link',
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Probe the OPTIONAL workspace registry (§5.2.0 gate arm b): absent host
|
|
122
|
+
* face, non-array result, or a throwing list() all degrade to [] (the gate
|
|
123
|
+
* then runs on the session-cwd + allowedRoots arms alone).
|
|
124
|
+
*/
|
|
125
|
+
function probeWorkspacePaths(ctx) {
|
|
126
|
+
try {
|
|
127
|
+
const list = ctx && ctx.workspaceRegistry ? ctx.workspaceRegistry.list : undefined
|
|
128
|
+
if (typeof list !== 'function') return []
|
|
129
|
+
const entries = list.call(ctx.workspaceRegistry)
|
|
130
|
+
if (!Array.isArray(entries)) return []
|
|
131
|
+
return entries
|
|
132
|
+
.map((entry) => (entry && typeof entry === 'object' ? entry.path : undefined))
|
|
133
|
+
.filter((p) => typeof p === 'string' && p.length > 0)
|
|
134
|
+
} catch {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** True when a stored path matches the live set (raw OR realpath form). */
|
|
140
|
+
function pathInLiveSet(storedPath, liveSet) {
|
|
141
|
+
if (liveSet.has(storedPath)) return true
|
|
142
|
+
const real = realPathBestEffort(storedPath)
|
|
143
|
+
return real !== null && liveSet.has(real)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Crash reconciliation for ONE repo (DESIGN §7.3, step by step).
|
|
148
|
+
*
|
|
149
|
+
* Marking only — the ONE destructive action is the best-effort removal of
|
|
150
|
+
* orphan integration worktrees; everything destructive stays behind the
|
|
151
|
+
* explicit worktree_cleanup tool.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} args { ctx, cfg, git, store, repoKey, repoRoot }
|
|
154
|
+
*/
|
|
155
|
+
async function reconcileRepo({ ctx, cfg, git, store, repoKey, repoRoot }) {
|
|
156
|
+
const recordsOfRepo = () =>
|
|
157
|
+
Object.values(store.worktrees).filter((record) => record.repoKey === repoKey)
|
|
158
|
+
const jobsOfRepo = () => Object.values(store.jobs).filter((job) => job.repoKey === repoKey)
|
|
159
|
+
|
|
160
|
+
// §7.3 branch 1 — repo unreachable (deleted / unmounted / not a git repo):
|
|
161
|
+
// every worktree record gets the `orphaned: true` MARKER with its state
|
|
162
|
+
// kept verbatim (orphaned is the §5.4 flag, not a §5.2.2 state — writing
|
|
163
|
+
// it into `state` would break the state machine), and applying jobs fail
|
|
164
|
+
// with the restart provenance.
|
|
165
|
+
if (!(await git.isGitRepo(repoRoot))) {
|
|
166
|
+
for (const record of recordsOfRepo()) {
|
|
167
|
+
if (record.orphaned === true) continue
|
|
168
|
+
store.upsertWorktree({ ...record, orphaned: true })
|
|
169
|
+
}
|
|
170
|
+
for (const job of jobsOfRepo()) {
|
|
171
|
+
if (job.state !== 'applying') continue
|
|
172
|
+
store.upsertJob({ ...job, state: 'failed', error: 'host restart; repo unreachable' })
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// §7.3 branch 2 — repo reachable: reconcile against the LIVE worktree
|
|
178
|
+
// list (git-port returns the parsed entries already).
|
|
179
|
+
const live = await git.listWorktrees(repoRoot)
|
|
180
|
+
const livePaths = new Set(live.map((entry) => entry.path))
|
|
181
|
+
|
|
182
|
+
// (a) vanished: record present, scene gone (manual prune / disk cleared).
|
|
183
|
+
// A record that is live again sheds a stale persisted orphaned marker
|
|
184
|
+
// (the marker would otherwise leak through every later list projection).
|
|
185
|
+
for (const record of recordsOfRepo()) {
|
|
186
|
+
const liveNow = pathInLiveSet(record.path, livePaths)
|
|
187
|
+
if (!liveNow) {
|
|
188
|
+
if (record.state !== 'vanished') {
|
|
189
|
+
store.upsertWorktree({ ...record, state: 'vanished' })
|
|
190
|
+
}
|
|
191
|
+
} else if (record.orphaned !== undefined) {
|
|
192
|
+
const { orphaned, ...rest } = record
|
|
193
|
+
void orphaned
|
|
194
|
+
store.upsertWorktree({ ...rest, state: rest.state })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// (b) applying jobs: the apply died with the host. Failed + provenance;
|
|
199
|
+
// a still-live integration worktree is RETAINED and annotated (§6.3's
|
|
200
|
+
// write-ahead means the record already points at the partial scene — the
|
|
201
|
+
// operator may inspect it). The task record mirrors merging → active,
|
|
202
|
+
// the same transition the engine's own failure path makes (markFailedOutcome).
|
|
203
|
+
const retainedForInspection = new Set()
|
|
204
|
+
for (const job of jobsOfRepo()) {
|
|
205
|
+
if (job.state !== 'applying') continue
|
|
206
|
+
let error =
|
|
207
|
+
'host restarted during apply — the recorded integrationWorktree may hold a partial state'
|
|
208
|
+
if (
|
|
209
|
+
typeof job.integrationWorktree === 'string'
|
|
210
|
+
&& job.integrationWorktree.length > 0
|
|
211
|
+
&& pathInLiveSet(job.integrationWorktree, livePaths)
|
|
212
|
+
) {
|
|
213
|
+
retainedForInspection.add(
|
|
214
|
+
realPathBestEffort(job.integrationWorktree) ?? job.integrationWorktree,
|
|
215
|
+
)
|
|
216
|
+
error += `; the integration worktree is retained at ${job.integrationWorktree} for inspection`
|
|
217
|
+
}
|
|
218
|
+
store.upsertJob({ ...job, state: 'failed', error })
|
|
219
|
+
const taskRecord = job.worktreeId ? store.worktrees[job.worktreeId] : undefined
|
|
220
|
+
if (taskRecord !== undefined && taskRecord.repoKey === repoKey && taskRecord.state === 'merging') {
|
|
221
|
+
store.upsertWorktree({ ...taskRecord, state: 'active' })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// (c) conflicted jobs survive verbatim — the retained scene IS the job's
|
|
226
|
+
// state (source semantics); nothing to do, stated explicitly.
|
|
227
|
+
|
|
228
|
+
// (d) orphan integration worktrees: live paths inside OUR
|
|
229
|
+
// `<worktreeRoot>/<repoKey>/.integration` namespace (realpath-compared —
|
|
230
|
+
// git reports realpaths while worktreeRoot may be a symlinked prefix)
|
|
231
|
+
// that no non-terminal job references. Best-effort removal, failures
|
|
232
|
+
// swallowed. Worktrees referenced by a job failed in THIS pass are
|
|
233
|
+
// protected: the error text just promised the operator an inspection
|
|
234
|
+
// scene — deleting it in the same breath would falsify it. On a later
|
|
235
|
+
// run (no applying jobs) such a path is a plain orphan and is reclaimed,
|
|
236
|
+
// mirroring the engine's releaseStaleIntegrationHolds (only live
|
|
237
|
+
// conflicted scenes persist indefinitely).
|
|
238
|
+
const namespaceReal = realPathBestEffort(path.join(cfg.worktreeRoot, repoKey, '.integration'))
|
|
239
|
+
if (namespaceReal !== null) {
|
|
240
|
+
const referenced = new Set(retainedForInspection)
|
|
241
|
+
for (const job of jobsOfRepo()) {
|
|
242
|
+
if (!ACTIVE_JOB_STATES.has(job.state)) continue
|
|
243
|
+
if (typeof job.integrationWorktree === 'string' && job.integrationWorktree.length > 0) {
|
|
244
|
+
referenced.add(realPathBestEffort(job.integrationWorktree) ?? job.integrationWorktree)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const entry of live) {
|
|
248
|
+
if (path.dirname(entry.path) !== namespaceReal) continue
|
|
249
|
+
if (referenced.has(entry.path)) continue
|
|
250
|
+
await git.removeWorktree(repoRoot, entry.path).catch(() => {})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The crash reconciliation pass (§7.3): per-repo reconcile with per-repo
|
|
257
|
+
* isolation — one broken repo (a probe failure, a git error) logs a warning
|
|
258
|
+
* and never aborts the whole apply. One persist at the end flushes every
|
|
259
|
+
* marking this pass made.
|
|
260
|
+
*/
|
|
261
|
+
async function reconcile({ ctx, cfg, git, store }) {
|
|
262
|
+
for (const [repoKey, repo] of Object.entries(store.repos)) {
|
|
263
|
+
const repoRoot = repo && typeof repo.root === 'string' ? repo.root : ''
|
|
264
|
+
if (repoRoot === '') {
|
|
265
|
+
ctx.logger?.warn?.(`dsh-worktrees: reconcile skipped for repo ${repoKey} (no root recorded)`)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await reconcileRepo({ ctx, cfg, git, store, repoKey, repoRoot })
|
|
270
|
+
} catch (error) {
|
|
271
|
+
ctx.logger?.warn?.(
|
|
272
|
+
`dsh-worktrees: reconcile skipped for repo ${repoKey} (${repoRoot}): ${errorText(error)}`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
store.persist()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Plugin entry (T10). See the module header for the assembly order.
|
|
281
|
+
*
|
|
282
|
+
* @param {Object} ctx Cordis ctx (needs ctx.tools.register; logger +
|
|
283
|
+
* workspaceRegistry are optional faces)
|
|
284
|
+
* @param {Object} [config] raw plugin config (validated here; zod strict)
|
|
285
|
+
* @returns {Promise<undefined>} always undefined — the loader treats the
|
|
286
|
+
* plugin callback's return value as a disposable
|
|
287
|
+
*/
|
|
288
|
+
export async function apply(ctx, config = {}) {
|
|
289
|
+
// a. config (zod strict, defaults resolved — lib/config.js)
|
|
290
|
+
const cfg = validateConfig(config)
|
|
291
|
+
|
|
292
|
+
// b. peer dual-instance defence (§8.4)
|
|
293
|
+
assertSingleDshToolsInstance(ctx)
|
|
294
|
+
|
|
295
|
+
// c. engine singletons — created exactly once per apply() closure
|
|
296
|
+
const git = createGitPort({ timeoutMs: cfg.gitTimeoutMs })
|
|
297
|
+
const store = createStateStore({ path: cfg.statePath })
|
|
298
|
+
store.load() // loud on a corrupt state file: a bad boot beats silent garbage
|
|
299
|
+
|
|
300
|
+
// d. crash reconciliation BEFORE any tool registration (§7.3)
|
|
301
|
+
await reconcile({ ctx, cfg, git, store })
|
|
302
|
+
|
|
303
|
+
// e. shared engines over the same store/git singletons. The queue's
|
|
304
|
+
// swallowed background-drain failures surface through the host logger.
|
|
305
|
+
const service = createWorktreeService({ git, store, config: cfg })
|
|
306
|
+
const queue = createMergeQueue({
|
|
307
|
+
git,
|
|
308
|
+
store,
|
|
309
|
+
config: {
|
|
310
|
+
...cfg,
|
|
311
|
+
onError: (message) => {
|
|
312
|
+
try {
|
|
313
|
+
ctx.logger?.warn?.(message)
|
|
314
|
+
} catch {
|
|
315
|
+
/* logging must never throw */
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// e2. the §10 composition seam: provide `worktreesEngine` on the ctx so
|
|
322
|
+
// dsh-dag-orchestrator's opportunistic `ctx.get('worktreesEngine')`
|
|
323
|
+
// probe finds the SAME service/queue singletons the tools use (red
|
|
324
|
+
// line 10: no second write entrance). The face adapts the DAG's
|
|
325
|
+
// four-key enqueue / five-state DrainOutcome dialect on top of the
|
|
326
|
+
// live engines (lib/engine-face.js) — the tool-layer contracts stay
|
|
327
|
+
// untouched. provide() is probed: a host ctx without the service
|
|
328
|
+
// face (fakes) degrades silently.
|
|
329
|
+
try {
|
|
330
|
+
if (ctx && typeof ctx.provide === 'function') {
|
|
331
|
+
ctx.provide(
|
|
332
|
+
'worktreesEngine',
|
|
333
|
+
createEngineFace({ service, queue, store, git, engineSessionId: 'dsh-dag-engine' }),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
ctx.logger?.warn?.(
|
|
338
|
+
`dsh-worktrees: could not provide the worktreesEngine service face: ${errorText(error)}`,
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// f. the §5.2.0 gate, prebound ONCE and shared by every tool. The tools
|
|
343
|
+
// pass { repoArg, sessionCwd } (sessionCwd extracted from their exec);
|
|
344
|
+
// workspacePaths comes from the OPTIONAL registry probe. With
|
|
345
|
+
// requireWorkspaceRegistration === false the workspace arm is disabled —
|
|
346
|
+
// an empty list can never admit, which is exactly "skip that tier" (the
|
|
347
|
+
// session-cwd and allowedRoots arms still apply; fail-closed default).
|
|
348
|
+
const workspacePaths = probeWorkspacePaths(ctx)
|
|
349
|
+
const gatedWorkspacePaths = cfg.requireWorkspaceRegistration === false ? [] : workspacePaths
|
|
350
|
+
const resolveRepo = (opts) =>
|
|
351
|
+
resolveRepoRoot({
|
|
352
|
+
repoArg: opts.repoArg,
|
|
353
|
+
sessionCwd: opts.sessionCwd,
|
|
354
|
+
workspacePaths: gatedWorkspacePaths,
|
|
355
|
+
allowedRoots: cfg.allowedRoots,
|
|
356
|
+
git,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// g. register the six tools per the register switches (default all on).
|
|
360
|
+
// deps shape per each tool's own contract (service/queue/store +
|
|
361
|
+
// resolveRepo; the git+cfg fallback stays available for parity).
|
|
362
|
+
const shared = {
|
|
363
|
+
service,
|
|
364
|
+
queue,
|
|
365
|
+
store,
|
|
366
|
+
git,
|
|
367
|
+
cfg: { workspacePaths: gatedWorkspacePaths, allowedRoots: cfg.allowedRoots },
|
|
368
|
+
resolveRepo,
|
|
369
|
+
}
|
|
370
|
+
let registered = 0
|
|
371
|
+
if (cfg.register.create) {
|
|
372
|
+
registerWorktreeCreateTool(ctx, shared)
|
|
373
|
+
registered += 1
|
|
374
|
+
}
|
|
375
|
+
if (cfg.register.list) {
|
|
376
|
+
registerWorktreeListTool(ctx, shared)
|
|
377
|
+
registered += 1
|
|
378
|
+
}
|
|
379
|
+
if (cfg.register.status) {
|
|
380
|
+
registerWorktreeStatusTool(ctx, shared)
|
|
381
|
+
registered += 1
|
|
382
|
+
}
|
|
383
|
+
if (cfg.register.merge) {
|
|
384
|
+
registerWorktreeMergeTool(ctx, shared)
|
|
385
|
+
registered += 1
|
|
386
|
+
}
|
|
387
|
+
if (cfg.register.queue) {
|
|
388
|
+
registerWorktreeQueueTool(ctx, shared)
|
|
389
|
+
registered += 1
|
|
390
|
+
}
|
|
391
|
+
if (cfg.register.cleanup) {
|
|
392
|
+
registerWorktreeCleanupTool(ctx, shared)
|
|
393
|
+
registered += 1
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
ctx.logger?.info?.(
|
|
397
|
+
`dsh-worktrees: applied (${registered} tool${registered === 1 ? '' : 's'}; `
|
|
398
|
+
+ `state ${cfg.statePath}; worktree root ${cfg.worktreeRoot})`,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return undefined
|
|
402
|
+
}
|