@norman-else/dsh-claude 0.1.36 → 0.1.38
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/INSTALL.md +55 -55
- package/LICENSE +21 -21
- package/cordis.patch.yml +12 -12
- package/legacy-preset/agent.cordis.yml +11 -11
- package/legacy-preset/preset.yml +4 -4
- package/lib/bin.mjs +0 -0
- package/lib/bin.mjs.map +1 -1
- package/lib/client.d.ts +4 -0
- package/lib/client.js +176 -33
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +217 -57
- package/lib/index.mjs.map +1 -1
- package/lib/presenters-BBoM1Ju1.mjs.map +1 -1
- package/lib/preset-installer-loenwnLS.mjs.map +1 -1
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +195 -189
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
package/lib/index.mjs
CHANGED
|
@@ -3,12 +3,12 @@ import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicP
|
|
|
3
3
|
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-loenwnLS.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, mkdir, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
-
import {
|
|
11
|
+
import { LlmAdapter, ReasoningEffortId, ToolCallId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -2067,7 +2067,7 @@ var ClaudeSupervisor = class {
|
|
|
2067
2067
|
await active.agent.session.append("tool/call", {
|
|
2068
2068
|
turn: active.cursor.turn,
|
|
2069
2069
|
step: active.cursor.step,
|
|
2070
|
-
callId:
|
|
2070
|
+
callId: ToolCallId(message.toolUseId),
|
|
2071
2071
|
name: message.toolName,
|
|
2072
2072
|
arguments: safeDetail(message.input) ?? "{}"
|
|
2073
2073
|
});
|
|
@@ -2080,7 +2080,7 @@ var ClaudeSupervisor = class {
|
|
|
2080
2080
|
turn: active.cursor.turn,
|
|
2081
2081
|
step: active.cursor.step,
|
|
2082
2082
|
message: createToolResultMessage({
|
|
2083
|
-
callId:
|
|
2083
|
+
callId: ToolCallId(message.toolUseId),
|
|
2084
2084
|
content: [{
|
|
2085
2085
|
type: "text",
|
|
2086
2086
|
text
|
|
@@ -3352,16 +3352,23 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3352
3352
|
const info = (message) => {
|
|
3353
3353
|
ctx.logger?.info?.(message);
|
|
3354
3354
|
};
|
|
3355
|
-
|
|
3355
|
+
/** Everything the host already knows, without touching the repository. */
|
|
3356
|
+
const localMeta = (sessionId) => {
|
|
3356
3357
|
const owned = ownsSession(sessionId);
|
|
3357
|
-
const repository = owned ? await repositoryForSession(sessionId) : void 0;
|
|
3358
3358
|
return {
|
|
3359
3359
|
owned,
|
|
3360
3360
|
commands: commandsForSession(sessionId),
|
|
3361
|
-
...repository === void 0 ? {} : { repository },
|
|
3362
3361
|
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
3363
3362
|
};
|
|
3364
3363
|
};
|
|
3364
|
+
const assembleMeta = async (sessionId) => {
|
|
3365
|
+
const meta = localMeta(sessionId);
|
|
3366
|
+
const repository = meta.owned ? await repositoryForSession(sessionId) : void 0;
|
|
3367
|
+
return {
|
|
3368
|
+
...meta,
|
|
3369
|
+
...repository === void 0 ? {} : { repository }
|
|
3370
|
+
};
|
|
3371
|
+
};
|
|
3365
3372
|
const streamMulti = async (res, io, sessionIds) => {
|
|
3366
3373
|
info(`dsh-claude: projection stream opened for ${sessionIds.length} session(s)`);
|
|
3367
3374
|
let textDeltas = 0;
|
|
@@ -3383,8 +3390,20 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3383
3390
|
}
|
|
3384
3391
|
};
|
|
3385
3392
|
const metas = /* @__PURE__ */ new Map();
|
|
3393
|
+
const writeMeta = (sessionId, meta) => {
|
|
3394
|
+
metas.set(sessionId, meta);
|
|
3395
|
+
writeLine({
|
|
3396
|
+
type: "meta",
|
|
3397
|
+
session: sessionId,
|
|
3398
|
+
owned: meta.owned,
|
|
3399
|
+
commands: meta.commands,
|
|
3400
|
+
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
3401
|
+
reviewComments: meta.reviewComments
|
|
3402
|
+
});
|
|
3403
|
+
};
|
|
3386
3404
|
const writeSnapshot = async (sessionId) => {
|
|
3387
|
-
const
|
|
3405
|
+
const projection = await sidecar.read(sessionId);
|
|
3406
|
+
const meta = metas.get(sessionId) ?? localMeta(sessionId);
|
|
3388
3407
|
metas.set(sessionId, meta);
|
|
3389
3408
|
writeLine({
|
|
3390
3409
|
type: "snapshot",
|
|
@@ -3392,6 +3411,9 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3392
3411
|
seq: sidecar.sequence(sessionId),
|
|
3393
3412
|
...envelope(projection, meta)
|
|
3394
3413
|
});
|
|
3414
|
+
const probed = await assembleMeta(sessionId);
|
|
3415
|
+
if (closed || JSON.stringify(probed) === JSON.stringify(metas.get(sessionId))) return;
|
|
3416
|
+
writeMeta(sessionId, probed);
|
|
3395
3417
|
};
|
|
3396
3418
|
const unsubscribes = sessionIds.map((sessionId) => sidecar.subscribe(sessionId, (delta) => {
|
|
3397
3419
|
switch (delta.kind) {
|
|
@@ -3458,15 +3480,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3458
3480
|
const next = await assembleMeta(sessionId);
|
|
3459
3481
|
if (closed) return;
|
|
3460
3482
|
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
3461
|
-
|
|
3462
|
-
writeLine({
|
|
3463
|
-
type: "meta",
|
|
3464
|
-
session: sessionId,
|
|
3465
|
-
owned: next.owned,
|
|
3466
|
-
commands: next.commands,
|
|
3467
|
-
...next.repository === void 0 ? {} : { repository: next.repository },
|
|
3468
|
-
reviewComments: next.reviewComments
|
|
3469
|
-
});
|
|
3483
|
+
writeMeta(sessionId, next);
|
|
3470
3484
|
}
|
|
3471
3485
|
writeLine({ type: "ping" });
|
|
3472
3486
|
})().catch(() => void 0);
|
|
@@ -3977,6 +3991,90 @@ var RepositoryStatusService = class {
|
|
|
3977
3991
|
}
|
|
3978
3992
|
};
|
|
3979
3993
|
//#endregion
|
|
3994
|
+
//#region src/branch-name.ts
|
|
3995
|
+
/** Name a generated worktree branch after what the user is about to ask for.
|
|
3996
|
+
*
|
|
3997
|
+
* The composer draft is the only description of the work that exists before
|
|
3998
|
+
* the session starts, and it is usually not English and never branch-safe, so
|
|
3999
|
+
* a throwaway Haiku turn compresses it into a slug. Naming is a nicety: every
|
|
4000
|
+
* failure here returns `undefined` and the caller keeps its timestamped name. */
|
|
4001
|
+
/** Cheapest model that can translate and compress a sentence. */
|
|
4002
|
+
const BRANCH_SUMMARY_MODEL = "haiku";
|
|
4003
|
+
/** A branch name is not worth blocking worktree creation on for long. */
|
|
4004
|
+
const BRANCH_SUMMARY_TIMEOUT_MS = 15e3;
|
|
4005
|
+
const MAX_INTENT_CHARS = 2e3;
|
|
4006
|
+
/** A compliant reply is one short fragment; anything longer is prose. */
|
|
4007
|
+
const MAX_REPLY_CHARS$2 = 80;
|
|
4008
|
+
/** More words than this is a sentence, not the fragment we asked for. */
|
|
4009
|
+
const MAX_SLUG_WORDS = 6;
|
|
4010
|
+
const MAX_SLUG_CHARS = 48;
|
|
4011
|
+
function branchSummaryPrompt(intent) {
|
|
4012
|
+
return [
|
|
4013
|
+
"Summarize this software task as a Git branch name fragment.",
|
|
4014
|
+
"Reply with 2-5 lowercase English words joined by hyphens and nothing else:",
|
|
4015
|
+
"no quotes, no slashes, no prefix, no punctuation, no explanation.",
|
|
4016
|
+
"Translate the task to English if it is written in another language.",
|
|
4017
|
+
"",
|
|
4018
|
+
"Task:",
|
|
4019
|
+
`"""\n${intent.replaceAll("\"\"\"", "\" \" \"")}\n"""`
|
|
4020
|
+
].join("\n");
|
|
4021
|
+
}
|
|
4022
|
+
/** Branch-safe slug for a model reply, or `undefined` when the reply is not
|
|
4023
|
+
* the fragment we asked for. Mangling a refusal or a paragraph into a slug
|
|
4024
|
+
* would produce a worse name than the timestamped fallback. */
|
|
4025
|
+
function branchSlug(reply) {
|
|
4026
|
+
const line = reply.trim();
|
|
4027
|
+
if (line.length === 0 || line.length > MAX_REPLY_CHARS$2 || /[\r\n]/u.test(line)) return void 0;
|
|
4028
|
+
const words = line.toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/gu, "-").split("-").filter((word) => word.length > 0);
|
|
4029
|
+
if (words.length === 0 || words.length > MAX_SLUG_WORDS) return void 0;
|
|
4030
|
+
const slug = words.join("-").slice(0, MAX_SLUG_CHARS).replace(/-+$/u, "");
|
|
4031
|
+
return slug.length === 0 ? void 0 : slug;
|
|
4032
|
+
}
|
|
4033
|
+
/** First free name in `<candidate>`, `<candidate>-2`, `<candidate>-3`, … */
|
|
4034
|
+
function uniqueBranchName(candidate, taken) {
|
|
4035
|
+
const existing = new Set(taken);
|
|
4036
|
+
if (!existing.has(candidate)) return candidate;
|
|
4037
|
+
for (let index = 2; index < 100; index += 1) {
|
|
4038
|
+
const name = `${candidate}-${index}`;
|
|
4039
|
+
if (!existing.has(name)) return name;
|
|
4040
|
+
}
|
|
4041
|
+
return candidate;
|
|
4042
|
+
}
|
|
4043
|
+
/** Compress a composer draft into a branch slug with a throwaway Claude turn.
|
|
4044
|
+
*
|
|
4045
|
+
* Deliberately NOT routed through the supervisor, for the same reasons as the
|
|
4046
|
+
* plan-usage probe: there is no session to borrow yet. The turn is isolated
|
|
4047
|
+
* from filesystem settings as well, because a CLAUDE.md instruction ("always
|
|
4048
|
+
* reply in the user's language") turns the answer into an unusable slug. */
|
|
4049
|
+
async function summarizeBranchSlug(executablePath, intent, factory = query) {
|
|
4050
|
+
const task = intent.trim().slice(0, MAX_INTENT_CHARS);
|
|
4051
|
+
if (task.length === 0) return void 0;
|
|
4052
|
+
const lifetime = new AbortController();
|
|
4053
|
+
const timer = setTimeout(() => lifetime.abort(), BRANCH_SUMMARY_TIMEOUT_MS);
|
|
4054
|
+
timer.unref?.();
|
|
4055
|
+
try {
|
|
4056
|
+
const query = factory({
|
|
4057
|
+
prompt: branchSummaryPrompt(task),
|
|
4058
|
+
options: {
|
|
4059
|
+
cwd: process.cwd(),
|
|
4060
|
+
abortController: lifetime,
|
|
4061
|
+
model: BRANCH_SUMMARY_MODEL,
|
|
4062
|
+
allowedTools: [],
|
|
4063
|
+
settingSources: [],
|
|
4064
|
+
maxTurns: 1,
|
|
4065
|
+
...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
|
|
4066
|
+
}
|
|
4067
|
+
});
|
|
4068
|
+
for await (const message of query) if (message.type === "result" && message.subtype === "success") return branchSlug(message.result);
|
|
4069
|
+
return;
|
|
4070
|
+
} catch {
|
|
4071
|
+
return;
|
|
4072
|
+
} finally {
|
|
4073
|
+
clearTimeout(timer);
|
|
4074
|
+
lifetime.abort();
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
//#endregion
|
|
3980
4078
|
//#region src/repository-setup.ts
|
|
3981
4079
|
const MAX_OUTPUT_BYTES$3 = 131072;
|
|
3982
4080
|
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
@@ -4054,6 +4152,7 @@ var RepositorySetupService = class {
|
|
|
4054
4152
|
#leasePath;
|
|
4055
4153
|
#worktreeRoot;
|
|
4056
4154
|
#branchPrefix;
|
|
4155
|
+
#summarizeBranch;
|
|
4057
4156
|
#cleanupGraceMs;
|
|
4058
4157
|
#gitPath;
|
|
4059
4158
|
#pending = Promise.resolve();
|
|
@@ -4062,6 +4161,7 @@ var RepositorySetupService = class {
|
|
|
4062
4161
|
this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
|
|
4063
4162
|
this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
|
|
4064
4163
|
this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
|
|
4164
|
+
this.#summarizeBranch = options.summarizeBranch ?? (async () => void 0);
|
|
4065
4165
|
this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
|
|
4066
4166
|
}
|
|
4067
4167
|
async listBranches(cwd) {
|
|
@@ -4108,7 +4208,7 @@ var RepositorySetupService = class {
|
|
|
4108
4208
|
remoteBranches
|
|
4109
4209
|
};
|
|
4110
4210
|
}
|
|
4111
|
-
async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}) {
|
|
4211
|
+
async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}, intent) {
|
|
4112
4212
|
progress("inspecting");
|
|
4113
4213
|
const branch = safeBranch(branchValue);
|
|
4114
4214
|
const info = await this.listBranches(cwd);
|
|
@@ -4116,7 +4216,7 @@ var RepositorySetupService = class {
|
|
|
4116
4216
|
const remote = info.remoteBranches.includes(branch);
|
|
4117
4217
|
if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
|
|
4118
4218
|
const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
|
|
4119
|
-
if (useWorktree) return this.#createWorktree(info
|
|
4219
|
+
if (useWorktree) return this.#createWorktree(info, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, requestedBranch !== void 0 && info.branches.includes(requestedBranch), progress, intent);
|
|
4120
4220
|
progress("switching-branch");
|
|
4121
4221
|
return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
|
|
4122
4222
|
}
|
|
@@ -4199,11 +4299,17 @@ var RepositorySetupService = class {
|
|
|
4199
4299
|
});
|
|
4200
4300
|
}
|
|
4201
4301
|
/** Reconcile leases against the set of directories still referenced by a
|
|
4202
|
-
* workspace
|
|
4203
|
-
*
|
|
4204
|
-
*
|
|
4205
|
-
*
|
|
4206
|
-
|
|
4302
|
+
* workspace. Deleting a workspace is the user saying they are done with it,
|
|
4303
|
+
* so an unreferenced worktree goes even with uncommitted changes; its
|
|
4304
|
+
* sessions are archived first, through `archiveSessions`, or the Host
|
|
4305
|
+
* rebuilds the deleted workspace from their headers on the next boot.
|
|
4306
|
+
* Fresh leases are retained for a grace period so a worktree created
|
|
4307
|
+
* moments ago cannot be swept before its workspace registration lands.
|
|
4308
|
+
*
|
|
4309
|
+
* Every command runs from the repository root, never from the worktree
|
|
4310
|
+
* being removed: spawning in a directory that is already gone throws
|
|
4311
|
+
* ENOENT, which used to strand the lease forever. */
|
|
4312
|
+
cleanupOrphans(activePaths, archiveSessions) {
|
|
4207
4313
|
const active = new Set(activePaths.map(comparablePath));
|
|
4208
4314
|
return this.#serialize(async () => {
|
|
4209
4315
|
const leases = await this.#readLeases();
|
|
@@ -4218,32 +4324,19 @@ var RepositorySetupService = class {
|
|
|
4218
4324
|
continue;
|
|
4219
4325
|
}
|
|
4220
4326
|
try {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4327
|
+
await archiveSessions?.(item.path).catch(() => void 0);
|
|
4328
|
+
if (await pathExists(item.path)) {
|
|
4329
|
+
if ((await this.#run(git, [
|
|
4330
|
+
"worktree",
|
|
4331
|
+
"remove",
|
|
4332
|
+
"--force",
|
|
4333
|
+
"--",
|
|
4334
|
+
item.path
|
|
4335
|
+
], item.root)).exitCode !== 0) {
|
|
4336
|
+
retained.push(item);
|
|
4337
|
+
continue;
|
|
4231
4338
|
}
|
|
4232
|
-
|
|
4233
|
-
}
|
|
4234
|
-
if (status.stdout.trim().length > 0) {
|
|
4235
|
-
retained.push(item);
|
|
4236
|
-
continue;
|
|
4237
|
-
}
|
|
4238
|
-
if ((await this.#run(git, [
|
|
4239
|
-
"worktree",
|
|
4240
|
-
"remove",
|
|
4241
|
-
"--",
|
|
4242
|
-
item.path
|
|
4243
|
-
], item.root)).exitCode !== 0) {
|
|
4244
|
-
retained.push(item);
|
|
4245
|
-
continue;
|
|
4246
|
-
}
|
|
4339
|
+
} else await this.#run(git, ["worktree", "prune"], item.root).catch(() => void 0);
|
|
4247
4340
|
if (item.pluginGeneratedBranch) await this.#run(git, [
|
|
4248
4341
|
"branch",
|
|
4249
4342
|
"-D",
|
|
@@ -4257,8 +4350,39 @@ var RepositorySetupService = class {
|
|
|
4257
4350
|
}
|
|
4258
4351
|
}
|
|
4259
4352
|
if (changed) await this.#writeLeases(retained);
|
|
4353
|
+
await this.#removeUnleasedDirectories(retained, active, now, archiveSessions);
|
|
4260
4354
|
});
|
|
4261
4355
|
}
|
|
4356
|
+
/** Remove directories under the plugin's own worktree root that no lease and
|
|
4357
|
+
* no workspace claims. A lease file lost to a crash, or a worktree whose
|
|
4358
|
+
* lease write failed, otherwise leaves a directory nothing will ever sweep.
|
|
4359
|
+
* The grace period covers the gap between `worktree add` and the lease
|
|
4360
|
+
* write, so a worktree being created right now is never taken. */
|
|
4361
|
+
async #removeUnleasedDirectories(retained, active, now, archiveSessions) {
|
|
4362
|
+
const leased = new Set(retained.map((item) => comparablePath(item.path)));
|
|
4363
|
+
let entries;
|
|
4364
|
+
try {
|
|
4365
|
+
entries = await readdir(this.#worktreeRoot);
|
|
4366
|
+
} catch {
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
for (const entry of entries) {
|
|
4370
|
+
const path = join(this.#worktreeRoot, entry);
|
|
4371
|
+
const key = comparablePath(path);
|
|
4372
|
+
if (leased.has(key) || active.has(key)) continue;
|
|
4373
|
+
try {
|
|
4374
|
+
const info = await stat(path);
|
|
4375
|
+
if (!info.isDirectory()) continue;
|
|
4376
|
+
const created = info.birthtimeMs > 0 ? info.birthtimeMs : info.mtimeMs;
|
|
4377
|
+
if (Math.max(0, now - created) < this.#cleanupGraceMs) continue;
|
|
4378
|
+
await archiveSessions?.(path).catch(() => void 0);
|
|
4379
|
+
await rm(path, {
|
|
4380
|
+
recursive: true,
|
|
4381
|
+
force: true
|
|
4382
|
+
});
|
|
4383
|
+
} catch {}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4262
4386
|
async #checkoutRemote(info, remoteBranch) {
|
|
4263
4387
|
const separator = remoteBranch.indexOf("/");
|
|
4264
4388
|
if (separator <= 0 || separator === remoteBranch.length - 1) throw new RepositorySetupError("invalid-branch", "The remote-tracking branch name is invalid.");
|
|
@@ -4325,13 +4449,14 @@ var RepositorySetupService = class {
|
|
|
4325
4449
|
branch
|
|
4326
4450
|
};
|
|
4327
4451
|
}
|
|
4328
|
-
async #createWorktree(
|
|
4452
|
+
async #createWorktree(info, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress, intent) {
|
|
4453
|
+
const { root } = info;
|
|
4329
4454
|
const git = await this.#git();
|
|
4330
4455
|
progress("fetching");
|
|
4331
4456
|
await this.#fetchRemotes(git, root);
|
|
4332
4457
|
const suffix = randomUUID().slice(0, 8);
|
|
4333
4458
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
|
|
4334
|
-
const branch = explicitBranchName ??
|
|
4459
|
+
const branch = explicitBranchName ?? await this.#generatedBranch(info, baseBranch, intent, stamp, suffix, progress);
|
|
4335
4460
|
const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
|
|
4336
4461
|
progress("creating-worktree");
|
|
4337
4462
|
await mkdir(this.#worktreeRoot, { recursive: true });
|
|
@@ -4392,6 +4517,18 @@ var RepositorySetupService = class {
|
|
|
4392
4517
|
leaseId: item.id
|
|
4393
4518
|
};
|
|
4394
4519
|
}
|
|
4520
|
+
/** `<prefix>/<what the draft is about>`, falling back to
|
|
4521
|
+
* `<prefix>/<base branch>-<stamp>-<random>` whenever the summary is missing
|
|
4522
|
+
* or unusable. The prefix and the fallback shape are unchanged. */
|
|
4523
|
+
async #generatedBranch(info, baseBranch, intent, stamp, suffix, progress) {
|
|
4524
|
+
const prefix = safeBranch(await this.#branchPrefix());
|
|
4525
|
+
if (intent !== void 0 && intent.trim().length > 0) {
|
|
4526
|
+
progress("summarizing");
|
|
4527
|
+
const summary = await this.#summarizeBranch(intent).catch(() => void 0);
|
|
4528
|
+
if (summary !== void 0) return uniqueBranchName(`${prefix}/${summary}`, info.branches);
|
|
4529
|
+
}
|
|
4530
|
+
return `${prefix}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
|
|
4531
|
+
}
|
|
4395
4532
|
async #repositoryRoot(git, cwd) {
|
|
4396
4533
|
const result = await this.#run(git, [
|
|
4397
4534
|
"rev-parse",
|
|
@@ -4964,7 +5101,7 @@ async function streamSetup(res, service, input) {
|
|
|
4964
5101
|
type: "progress",
|
|
4965
5102
|
stage
|
|
4966
5103
|
});
|
|
4967
|
-
})
|
|
5104
|
+
}, optionalString$1(input, "intent"))
|
|
4968
5105
|
});
|
|
4969
5106
|
} catch (error) {
|
|
4970
5107
|
const setupError = error instanceof RepositorySetupError ? error : void 0;
|
|
@@ -4983,7 +5120,7 @@ async function streamSetup(res, service, input) {
|
|
|
4983
5120
|
* The prefix is registered as a stream because the setup POST holds its
|
|
4984
5121
|
* connection open for the whole worktree build; the short sibling paths ride
|
|
4985
5122
|
* the same registration and answer with `json` before releasing it. */
|
|
4986
|
-
function registerRepositorySetupRoute(ctx, service) {
|
|
5123
|
+
function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
4987
5124
|
registerPluginRoute(ctx, {
|
|
4988
5125
|
mode: "stream",
|
|
4989
5126
|
kind: "prefix",
|
|
@@ -5017,6 +5154,11 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
5017
5154
|
const input = await readJson$5(io);
|
|
5018
5155
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
5019
5156
|
}
|
|
5157
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
|
|
5158
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5159
|
+
sweep?.();
|
|
5160
|
+
return json(res, 200, { ok: true });
|
|
5161
|
+
}
|
|
5020
5162
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
5021
5163
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5022
5164
|
const input = await readJson$5(io);
|
|
@@ -7650,7 +7792,10 @@ async function apply(ctx, config) {
|
|
|
7650
7792
|
await applySettingsOverrides();
|
|
7651
7793
|
const sidecar = new ClaudeSidecarRepository();
|
|
7652
7794
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
7653
|
-
const repositorySetup = new RepositorySetupService(ctx.subprocess, {
|
|
7795
|
+
const repositorySetup = new RepositorySetupService(ctx.subprocess, {
|
|
7796
|
+
branchPrefix: () => readWorktreeBranchPrefix(),
|
|
7797
|
+
summarizeBranch: (intent) => summarizeBranchSlug(supervisorConfig.executablePath, intent)
|
|
7798
|
+
});
|
|
7654
7799
|
const reviewComments = new ReviewCommentStore();
|
|
7655
7800
|
const commandCatalogs = /* @__PURE__ */ new Map();
|
|
7656
7801
|
const supervisor = new ClaudeSupervisor({
|
|
@@ -7728,19 +7873,34 @@ async function apply(ctx, config) {
|
|
|
7728
7873
|
reviewComments.disposeSession(agent.id);
|
|
7729
7874
|
await supervisor.disposeSession(agent.id);
|
|
7730
7875
|
});
|
|
7876
|
+
let sweepWorktrees;
|
|
7731
7877
|
const injectWorkspaceRegistry = ctx.inject;
|
|
7732
7878
|
injectWorkspaceRegistry(["workspaceRegistry"], (sweepCtx) => {
|
|
7879
|
+
const archiveSessions = async (worktreePath) => {
|
|
7880
|
+
const persistence = sweepCtx.get("sessionPersistence");
|
|
7881
|
+
if (persistence === void 0) return;
|
|
7882
|
+
const target = comparablePath(worktreePath);
|
|
7883
|
+
for (const header of await persistence.list()) {
|
|
7884
|
+
if (typeof header.id !== "string" || typeof header.cwd !== "string") continue;
|
|
7885
|
+
if (comparablePath(header.cwd) !== target) continue;
|
|
7886
|
+
await sweepCtx.workspaceRegistry.archiveSession(header.id).catch(() => void 0);
|
|
7887
|
+
}
|
|
7888
|
+
};
|
|
7733
7889
|
const sweep = () => {
|
|
7734
7890
|
try {
|
|
7735
7891
|
const paths = sweepCtx.workspaceRegistry.list().map((workspace) => workspace.path);
|
|
7736
|
-
repositorySetup.cleanupOrphans(paths).catch(() => void 0);
|
|
7892
|
+
repositorySetup.cleanupOrphans(paths, archiveSessions).catch(() => void 0);
|
|
7737
7893
|
} catch {}
|
|
7738
7894
|
};
|
|
7739
7895
|
sweepCtx.effect(() => {
|
|
7740
7896
|
sweep();
|
|
7897
|
+
sweepWorktrees = sweep;
|
|
7741
7898
|
const timer = setInterval(sweep, 6e4);
|
|
7742
7899
|
timer.unref?.();
|
|
7743
|
-
return () =>
|
|
7900
|
+
return () => {
|
|
7901
|
+
sweepWorktrees = void 0;
|
|
7902
|
+
clearInterval(timer);
|
|
7903
|
+
};
|
|
7744
7904
|
}, "dsh-claude: worktree reconciliation");
|
|
7745
7905
|
});
|
|
7746
7906
|
ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
|
|
@@ -7755,7 +7915,7 @@ async function apply(ctx, config) {
|
|
|
7755
7915
|
defaultLimits,
|
|
7756
7916
|
onUpdated: applySettingsOverrides
|
|
7757
7917
|
});
|
|
7758
|
-
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
7918
|
+
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
|
|
7759
7919
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
7760
7920
|
registerRepositoryFileRoute(webCtx, repositoryStatus);
|
|
7761
7921
|
registerJiraRoute(webCtx, new JiraService());
|