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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `worktree_create` tool (T07, DESIGN §5.3).
|
|
3
|
+
*
|
|
4
|
+
* Thin tool layer — parameters schema, the §5.2.0 repo-gate call, and a
|
|
5
|
+
* projection of the WorktreeService.create record into the §5.3 return
|
|
6
|
+
* shape. No git calls and no state writes happen here: the engine is the
|
|
7
|
+
* injected `service` (lib/worktree-service.js) and the repo gate is the
|
|
8
|
+
* injected `resolveRepo` (or a real resolveRepoRoot binding built from the
|
|
9
|
+
* injected `git` port + `cfg`), exactly the deps T10's apply() assembles.
|
|
10
|
+
*
|
|
11
|
+
* deps contract:
|
|
12
|
+
* - service (required) createWorktreeService() product;
|
|
13
|
+
* - resolveRepo (optional) async ({ repoArg, sessionCwd }) =>
|
|
14
|
+
* { canonical, repoKey } — a T10-prebound §5.2.0 gate;
|
|
15
|
+
* - git + cfg (fallback) when resolveRepo is absent, the REAL gate is
|
|
16
|
+
* bound here from the injected GitPort plus
|
|
17
|
+
* cfg.workspacePaths / cfg.allowedRoots;
|
|
18
|
+
* - sessionCwd (optional) fallback when the calling exec carries no
|
|
19
|
+
* session header cwd;
|
|
20
|
+
* - sessionId (optional) fallback when exec.agent.session.id is absent.
|
|
21
|
+
*
|
|
22
|
+
* json-safe discipline (E3): the return object is built with conditional
|
|
23
|
+
* spreads only — no key ever carries an `undefined` value.
|
|
24
|
+
*
|
|
25
|
+
* Error policy: engine/gate errors already start with a machine-matchable
|
|
26
|
+
* code (`branch_exists — …`, `repo_not_registered: …`) and are re-thrown
|
|
27
|
+
* untouched; anything uncoded gets a `worktree_create:` prefix so the
|
|
28
|
+
* surfaced message always names the tool.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
32
|
+
import { resolveRepoRoot } from '../repo-gate.js'
|
|
33
|
+
|
|
34
|
+
/** Registered tool name (DESIGN §5.3). */
|
|
35
|
+
const TOOL_NAME = 'worktree_create'
|
|
36
|
+
|
|
37
|
+
/** A message already starting with a stable snake_case code stays verbatim. */
|
|
38
|
+
const CODED_MESSAGE = /^[a-z][a-z0-9_]*(\s*—|:)/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Error surfacing (DESIGN §5.3 error family): coded engine/gate errors pass
|
|
42
|
+
* through unchanged; uncoded ones (e.g. a TypeError from a bad wiring) get
|
|
43
|
+
* the tool-name prefix.
|
|
44
|
+
*/
|
|
45
|
+
function surfaceError(error) {
|
|
46
|
+
const message = String(error instanceof Error ? error.message : error)
|
|
47
|
+
if (CODED_MESSAGE.test(message)) throw error
|
|
48
|
+
throw new Error(`${TOOL_NAME}: ${message}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Bind the §5.2.0 repo gate. Prefer a T10-injected `resolveRepo`; otherwise
|
|
53
|
+
* build the real `resolveRepoRoot` binding from the injected git port and
|
|
54
|
+
* cfg. This module never spawns git itself — the gate is the only seam.
|
|
55
|
+
*/
|
|
56
|
+
function makeRepoResolver(deps) {
|
|
57
|
+
if (typeof deps.resolveRepo === 'function') return deps.resolveRepo
|
|
58
|
+
const { git, cfg = {} } = deps
|
|
59
|
+
if (!git || typeof git.isGitRepo !== 'function') {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${TOOL_NAME}: deps must carry either resolveRepo(opts) or a git port with isGitRepo(cwd) so the repo gate can be bound`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
return (opts) =>
|
|
65
|
+
resolveRepoRoot({
|
|
66
|
+
repoArg: opts.repoArg,
|
|
67
|
+
sessionCwd: opts.sessionCwd,
|
|
68
|
+
workspacePaths: cfg.workspacePaths,
|
|
69
|
+
allowedRoots: cfg.allowedRoots,
|
|
70
|
+
git,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** §5.3 composition example, appended verbatim to the description. */
|
|
75
|
+
const COMPOSITION_EXAMPLE =
|
|
76
|
+
'Composition example: '
|
|
77
|
+
+ '1. worktree_create(task: "api-types") → path P1; '
|
|
78
|
+
+ '2. worktree_create(task: "docs-refresh") → path P2; '
|
|
79
|
+
+ '3. subagent(prompt: "…implement X…", cwd: P1) and subagent(prompt: "…update docs…", cwd: P2) in parallel — each task writes in its own worktree, no overlap; '
|
|
80
|
+
+ '4. worktree_merge(worktree_id: id1) → succeeded; '
|
|
81
|
+
+ '5. worktree_merge(worktree_id: id2) → conflicted (conflict list + retained scene); '
|
|
82
|
+
+ '6. worktree_queue(action: "list") → observe, resolve out-of-band in the retained worktree; '
|
|
83
|
+
+ '7. worktree_queue(action: "resolve", job_id: …) → release the integration branch; '
|
|
84
|
+
+ '8. worktree_queue(action: "retry", job_id: …) → re-queue (optional alternative); '
|
|
85
|
+
+ '9. worktree_cleanup(worktree_id: id1) → remove the merged worktree and its branch.'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register the `worktree_create` tool (DESIGN §5.3).
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} ctx host ctx (needs ctx.tools.register)
|
|
91
|
+
* @param {Object} deps see the module header for the contract
|
|
92
|
+
*/
|
|
93
|
+
export function registerWorktreeCreateTool(ctx, deps = {}) {
|
|
94
|
+
const { service } = deps
|
|
95
|
+
if (!service || typeof service.create !== 'function') {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${TOOL_NAME}: deps.service must be a WorktreeService (createWorktreeService product)`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
const resolveRepo = makeRepoResolver(deps)
|
|
101
|
+
|
|
102
|
+
ctx.tools.register(defineTool({
|
|
103
|
+
name: TOOL_NAME,
|
|
104
|
+
description:
|
|
105
|
+
'Create an isolated git worktree for ONE parallel task: a dedicated branch dsh-wt/<session-short>/<task> checked out at a private path under the plugin worktree root, so concurrently dispatched subagents never write into the same working directory. '
|
|
106
|
+
+ 'The base ref is resolved to a concrete commit oid at create time (never a mutable HEAD), and the returned path is where the task must do all of its work. '
|
|
107
|
+
+ COMPOSITION_EXAMPLE,
|
|
108
|
+
parameters: {
|
|
109
|
+
task: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
required: true,
|
|
112
|
+
description:
|
|
113
|
+
'Short task slug identifying the parallel task this worktree isolates (e.g. "refactor-auth", "fix-231"). Sanitized into the branch name dsh-wt/<session>/<task>.',
|
|
114
|
+
},
|
|
115
|
+
repo_root: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description:
|
|
118
|
+
'Absolute git repository root. Defaults to the current session working directory. Must resolve inside a registered workspace, the session cwd subtree, or a configured allowedRoot.',
|
|
119
|
+
},
|
|
120
|
+
base_ref: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description:
|
|
123
|
+
'Git ref to branch from (branch / tag / commit oid / HEAD). Defaults to HEAD of the repository. Never falls back to a mutable HEAD when the queue expects a fixed base.',
|
|
124
|
+
},
|
|
125
|
+
integration_branch: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description:
|
|
128
|
+
'Branch that worktree_merge will integrate into (default: dsh-wt/integration/<session>, bootstrapped from repo HEAD on first merge). Must not be a branch already checked out in another worktree of this repo.',
|
|
129
|
+
},
|
|
130
|
+
note: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Optional free-text note stored on the worktree record.',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
output: {
|
|
136
|
+
schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
properties: {
|
|
140
|
+
kind: { type: 'string', required: true, const: 'worktree' },
|
|
141
|
+
id: { type: 'string', required: true },
|
|
142
|
+
repo_root: { type: 'string', required: true },
|
|
143
|
+
branch: { type: 'string', required: true },
|
|
144
|
+
path: { type: 'string', required: true },
|
|
145
|
+
base_commit: { type: 'string', required: true },
|
|
146
|
+
integration_branch: { type: 'string', required: true },
|
|
147
|
+
delegate_hint: { type: 'string', required: true },
|
|
148
|
+
note: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
render: (_args, value) => [{
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: `created worktree ${value.id} (branch ${value.branch}) at ${value.path}; delegate via the subagent tool with cwd: ${value.path}`,
|
|
154
|
+
}],
|
|
155
|
+
},
|
|
156
|
+
// State write with a check-then-act section (the maxWorktrees gate), so
|
|
157
|
+
// sibling calls must not overlap (undeclared would mean the same — this
|
|
158
|
+
// is just explicit; DESIGN §7.2 reserves the writeup for worktree_merge).
|
|
159
|
+
isConcurrencySafe: () => false,
|
|
160
|
+
async execute(args, exec) {
|
|
161
|
+
try {
|
|
162
|
+
const session = exec && exec.agent ? exec.agent.session : undefined
|
|
163
|
+
const sessionCwd =
|
|
164
|
+
(session && session.header && session.header.cwd) || deps.sessionCwd
|
|
165
|
+
|
|
166
|
+
// §5.2.0 gate FIRST (its repo_unresolved / repo_unknown /
|
|
167
|
+
// not_a_git_repo / repo_not_registered codes are the tool's front
|
|
168
|
+
// door), then the session-id requirement the branch name derives
|
|
169
|
+
// from.
|
|
170
|
+
const { canonical, repoKey } = await resolveRepo({
|
|
171
|
+
repoArg: args.repo_root,
|
|
172
|
+
...(sessionCwd !== undefined ? { sessionCwd } : {}),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const sessionId = (session && session.id) || deps.sessionId
|
|
176
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`${TOOL_NAME}: session_unresolved — no calling agent session id (exec.agent.session.id) and none injected; the task branch name derives from it`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const record = await service.create({
|
|
183
|
+
task: args.task,
|
|
184
|
+
repoRoot: canonical,
|
|
185
|
+
repoKey,
|
|
186
|
+
...(args.base_ref !== undefined ? { baseRef: args.base_ref } : {}),
|
|
187
|
+
...(args.integration_branch !== undefined
|
|
188
|
+
? { integrationBranch: args.integration_branch }
|
|
189
|
+
: {}),
|
|
190
|
+
...(args.note !== undefined ? { note: args.note } : {}),
|
|
191
|
+
sessionId,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
kind: 'worktree',
|
|
196
|
+
id: record.id,
|
|
197
|
+
repo_root: record.repoRoot,
|
|
198
|
+
branch: record.branch,
|
|
199
|
+
path: record.path,
|
|
200
|
+
base_commit: record.baseCommit,
|
|
201
|
+
integration_branch: record.integrationBranch,
|
|
202
|
+
delegate_hint:
|
|
203
|
+
`Delegate parallel work here via the subagent tool with cwd: ${record.path} `
|
|
204
|
+
+ '(each parallel task gets its own worktree; integrate with worktree_merge).',
|
|
205
|
+
...(typeof record.note === 'string' ? { note: record.note } : {}),
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
surfaceError(error)
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}))
|
|
212
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `worktree_list` tool (T07, DESIGN §5.4).
|
|
3
|
+
*
|
|
4
|
+
* Thin tool layer: parameters schema, the §5.2.0 repo-gate call (repo_root
|
|
5
|
+
* is resolved + admitted so the filter is a canonical path), the §5.4
|
|
6
|
+
* worktree/queue projections, and the `include_integration` extension that
|
|
7
|
+
* surfaces conflict-retained integration worktrees. No git calls, no state
|
|
8
|
+
* writes — `service.list` (engine) and the injected `store` (read-only
|
|
9
|
+
* projections the service does not expose) are the only collaborators.
|
|
10
|
+
*
|
|
11
|
+
* deps contract:
|
|
12
|
+
* - service (required) createWorktreeService() product;
|
|
13
|
+
* - store (optional, needed for include_integration) the SAME
|
|
14
|
+
* StateStore instance the service was built with (read-only
|
|
15
|
+
* access for the retained-worktree projection);
|
|
16
|
+
* - resolveRepo (optional) prebound §5.2.0 gate ({ repoArg, sessionCwd });
|
|
17
|
+
* - git + cfg (fallback) real resolveRepoRoot binding when resolveRepo
|
|
18
|
+
* is absent;
|
|
19
|
+
* - sessionCwd (optional) fallback when the calling exec has no session
|
|
20
|
+
* header cwd.
|
|
21
|
+
*
|
|
22
|
+
* json-safe discipline (E3): conditional spreads everywhere; a canonical
|
|
23
|
+
* repo_root key only appears when the caller supplied repo_root.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
27
|
+
import { resolveRepoRoot } from '../repo-gate.js'
|
|
28
|
+
|
|
29
|
+
/** Registered tool name (DESIGN §5.4). */
|
|
30
|
+
const TOOL_NAME = 'worktree_list'
|
|
31
|
+
|
|
32
|
+
/** A message already starting with a stable snake_case code stays verbatim. */
|
|
33
|
+
const CODED_MESSAGE = /^[a-z][a-z0-9_]*(\s*—|:)/
|
|
34
|
+
|
|
35
|
+
/** Coded engine/gate errors pass through; uncoded ones get the tool prefix. */
|
|
36
|
+
function surfaceError(error) {
|
|
37
|
+
const message = String(error instanceof Error ? error.message : error)
|
|
38
|
+
if (CODED_MESSAGE.test(message)) throw error
|
|
39
|
+
throw new Error(`${TOOL_NAME}: ${message}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bind the §5.2.0 gate: injected prebound resolver, else the real gate. */
|
|
43
|
+
function makeRepoResolver(deps) {
|
|
44
|
+
if (typeof deps.resolveRepo === 'function') return deps.resolveRepo
|
|
45
|
+
const { git, cfg = {} } = deps
|
|
46
|
+
if (!git || typeof git.isGitRepo !== 'function') {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${TOOL_NAME}: deps must carry either resolveRepo(opts) or a git port with isGitRepo(cwd) so the repo gate can be bound`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return (opts) =>
|
|
52
|
+
resolveRepoRoot({
|
|
53
|
+
repoArg: opts.repoArg,
|
|
54
|
+
sessionCwd: opts.sessionCwd,
|
|
55
|
+
workspacePaths: cfg.workspacePaths,
|
|
56
|
+
allowedRoots: cfg.allowedRoots,
|
|
57
|
+
git,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Conflict-retained integration worktrees, deduped, ascending by path:
|
|
63
|
+
* every conflicted job's recorded integrationWorktree (a `.integration/`
|
|
64
|
+
* path retained as the live conflict scene; DESIGN §6.3 step 10).
|
|
65
|
+
*/
|
|
66
|
+
function retainedIntegrationWorktrees(store) {
|
|
67
|
+
const paths = new Set()
|
|
68
|
+
for (const job of Object.values(store.jobs)) {
|
|
69
|
+
if (job.state !== 'conflicted') continue
|
|
70
|
+
if (typeof job.integrationWorktree === 'string' && job.integrationWorktree.length > 0) {
|
|
71
|
+
paths.add(job.integrationWorktree)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...paths].sort()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register the `worktree_list` tool (DESIGN §5.4).
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} ctx host ctx (needs ctx.tools.register)
|
|
81
|
+
* @param {Object} deps see the module header for the contract
|
|
82
|
+
*/
|
|
83
|
+
export function registerWorktreeListTool(ctx, deps = {}) {
|
|
84
|
+
const { service } = deps
|
|
85
|
+
if (!service || typeof service.list !== 'function') {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`${TOOL_NAME}: deps.service must be a WorktreeService (createWorktreeService product)`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
const resolveRepo = makeRepoResolver(deps)
|
|
91
|
+
|
|
92
|
+
ctx.tools.register(defineTool({
|
|
93
|
+
name: TOOL_NAME,
|
|
94
|
+
description:
|
|
95
|
+
'List the git worktrees managed by this plugin (optionally filtered by repository) with each record\'s state and an `orphaned` flag when the path no longer appears in `git worktree list` (crash / manual removal), plus a merge-queue summary. '
|
|
96
|
+
+ 'Use this after worktree_create to review the fleet, and before worktree_cleanup to find records worth pruning. Set include_integration true to also list temporary integration worktrees retained from conflicts (their scenes are out-of-band resolvable; see worktree_queue).',
|
|
97
|
+
parameters: {
|
|
98
|
+
repo_root: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description:
|
|
101
|
+
'Filter by repository (defaults: all repos known to the state store).',
|
|
102
|
+
},
|
|
103
|
+
include_integration: {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description:
|
|
106
|
+
'Also list temporary integration worktrees retained from conflicts (default false).',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
output: {
|
|
110
|
+
schema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
properties: {
|
|
114
|
+
kind: { type: 'string', required: true, const: 'list' },
|
|
115
|
+
repo_root: { type: 'string' },
|
|
116
|
+
worktrees: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
required: true,
|
|
119
|
+
items: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
additionalProperties: true,
|
|
122
|
+
properties: {
|
|
123
|
+
id: { type: 'string', required: true },
|
|
124
|
+
task: { type: 'string', required: true },
|
|
125
|
+
branch: { type: 'string', required: true },
|
|
126
|
+
path: { type: 'string', required: true },
|
|
127
|
+
state: { type: 'string', required: true },
|
|
128
|
+
head_commit: { type: 'string' },
|
|
129
|
+
dirty: { type: 'boolean' },
|
|
130
|
+
merge_state: { type: 'string' },
|
|
131
|
+
orphaned: { type: 'boolean' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
retained_integration_worktrees: {
|
|
136
|
+
type: 'array',
|
|
137
|
+
items: { type: 'string' },
|
|
138
|
+
},
|
|
139
|
+
queue_summary: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
required: true,
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
properties: {
|
|
144
|
+
active_jobs: {
|
|
145
|
+
type: 'array',
|
|
146
|
+
required: true,
|
|
147
|
+
items: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
repo_root: {
|
|
152
|
+
required: true,
|
|
153
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
154
|
+
},
|
|
155
|
+
integration_branch: { type: 'string', required: true },
|
|
156
|
+
job_id: { type: 'string', required: true },
|
|
157
|
+
state: { type: 'string', required: true },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
queued_count: { type: 'integer', required: true },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
render: (_args, value) => [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `${value.worktrees.length} worktree(s)`
|
|
169
|
+
+ (value.retained_integration_worktrees
|
|
170
|
+
? `; ${value.retained_integration_worktrees.length} retained integration worktree(s)`
|
|
171
|
+
: '')
|
|
172
|
+
+ `; queue: ${value.queue_summary.queued_count} queued`,
|
|
173
|
+
}],
|
|
174
|
+
},
|
|
175
|
+
isConcurrencySafe: () => true,
|
|
176
|
+
async execute(args, exec) {
|
|
177
|
+
try {
|
|
178
|
+
let canonical
|
|
179
|
+
if (args.repo_root !== undefined) {
|
|
180
|
+
const session = exec && exec.agent ? exec.agent.session : undefined
|
|
181
|
+
const sessionCwd =
|
|
182
|
+
(session && session.header && session.header.cwd) || deps.sessionCwd
|
|
183
|
+
canonical = (await resolveRepo({
|
|
184
|
+
repoArg: args.repo_root,
|
|
185
|
+
...(sessionCwd !== undefined ? { sessionCwd } : {}),
|
|
186
|
+
})).canonical
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { worktrees, queueSummary } = await service.list(
|
|
190
|
+
canonical !== undefined ? { repoRoot: canonical } : {},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
let retained
|
|
194
|
+
if (args.include_integration === true) {
|
|
195
|
+
const store = deps.store
|
|
196
|
+
if (!store || typeof store.jobs !== 'object') {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`${TOOL_NAME}: deps.store (the StateStore the service was built with) is required for include_integration`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
retained = retainedIntegrationWorktrees(store)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
kind: 'list',
|
|
206
|
+
...(canonical !== undefined ? { repo_root: canonical } : {}),
|
|
207
|
+
worktrees: worktrees.map((record) => ({
|
|
208
|
+
id: record.id,
|
|
209
|
+
task: record.task,
|
|
210
|
+
branch: record.branch,
|
|
211
|
+
path: record.path,
|
|
212
|
+
state: record.state,
|
|
213
|
+
...(record.headCommit !== undefined ? { head_commit: record.headCommit } : {}),
|
|
214
|
+
...(record.dirty !== undefined ? { dirty: record.dirty } : {}),
|
|
215
|
+
...(record.mergeJobId !== undefined ? { merge_state: record.mergeJobId } : {}),
|
|
216
|
+
...(record.orphaned !== undefined ? { orphaned: record.orphaned } : {}),
|
|
217
|
+
})),
|
|
218
|
+
...(retained !== undefined ? { retained_integration_worktrees: retained } : {}),
|
|
219
|
+
queue_summary: {
|
|
220
|
+
active_jobs: queueSummary.activeJobs.map((job) => ({
|
|
221
|
+
repo_root: job.repoRoot ?? null,
|
|
222
|
+
integration_branch: job.integrationBranch,
|
|
223
|
+
job_id: job.jobId,
|
|
224
|
+
state: job.state,
|
|
225
|
+
})),
|
|
226
|
+
queued_count: queueSummary.queuedCount,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
surfaceError(error)
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
}))
|
|
234
|
+
}
|