@norman-else/dsh-claude 0.1.21 → 0.1.23

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/lib/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { S as CLAUDE_UPDATE_PATH, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REVIEW_COMMENT_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_ACTION_PATH, w as TASK_TOOL_NAMES, x as CLAUDE_UPDATE_CHECK_PATH, y as CLAUDE_REPOSITORY_SETUP_PATH } from "./events-BdDs9ebF.mjs";
2
- import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-C10-lz6A.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DMANIjwu.mjs";
1
+ import { C as CLAUDE_UPDATE_PATH, S as CLAUDE_UPDATE_CHECK_PATH, T as TASK_TOOL_NAMES, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_SETUP_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_ACTION_PATH, x as CLAUDE_REVIEW_COMMENT_PATH, y as CLAUDE_REPOSITORY_FEEDBACK_PATH } from "./events-DhrAr5gh.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-Br25ODdm.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-B_Cb0RG6.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, writeFile } from "node:fs/promises";
6
+ import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
8
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
9
  import { homedir } from "node:os";
@@ -25,7 +25,7 @@ function emptyProjection() {
25
25
  activities: []
26
26
  };
27
27
  }
28
- function record$5(value) {
28
+ function record$6(value) {
29
29
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
30
30
  }
31
31
  function finiteInteger(value) {
@@ -35,7 +35,7 @@ function string$3(value, max) {
35
35
  return typeof value === "string" && value.length > 0 && value.length <= max;
36
36
  }
37
37
  function binding(value) {
38
- const input = record$5(value);
38
+ const input = record$6(value);
39
39
  if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
40
40
  return {
41
41
  claudeSessionId: input.claudeSessionId,
@@ -65,22 +65,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
65
65
  "failed"
66
66
  ]);
67
67
  function activity(value) {
68
- const input = record$5(value);
68
+ const input = record$6(value);
69
69
  if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
70
70
  return normalizeActivity(input);
71
71
  }
72
72
  function contextUsage(value) {
73
- const input = record$5(value);
73
+ const input = record$6(value);
74
74
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
75
75
  return normalizeContextUsage(input);
76
76
  }
77
77
  function tasks(value) {
78
- const input = record$5(value);
78
+ const input = record$6(value);
79
79
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
80
80
  return normalizeTasksEvent(input.tasks);
81
81
  }
82
82
  function parseClaudeSidecar(value) {
83
- const input = record$5(value);
83
+ const input = record$6(value);
84
84
  if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
85
85
  const activities = input.activities.map(activity);
86
86
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -653,7 +653,7 @@ function createUserQuestionBridge(userQuestions, activeContext) {
653
653
  }
654
654
  //#endregion
655
655
  //#region src/sdk-messages.ts
656
- function record$4(value) {
656
+ function record$5(value) {
657
657
  return value !== null && typeof value === "object" ? value : void 0;
658
658
  }
659
659
  function string$2(value) {
@@ -668,7 +668,7 @@ function taskUsageOf(usage) {
668
668
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
669
669
  }
670
670
  function resultUsage(message) {
671
- const usage = record$4(message.usage);
671
+ const usage = record$5(message.usage);
672
672
  const normalized = {};
673
673
  if (usage !== void 0) {
674
674
  if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
@@ -680,7 +680,7 @@ function resultUsage(message) {
680
680
  return normalized;
681
681
  }
682
682
  function normalizeAssistant(message) {
683
- const content = record$4(message.message)?.content;
683
+ const content = record$5(message.message)?.content;
684
684
  if (!Array.isArray(content)) return [{
685
685
  kind: "protocol-error",
686
686
  title: "Malformed Claude assistant message",
@@ -689,7 +689,7 @@ function normalizeAssistant(message) {
689
689
  const parentToolUseId = string$2(message.parent_tool_use_id);
690
690
  const normalized = [];
691
691
  for (const item of content) {
692
- const block = record$4(item);
692
+ const block = record$5(item);
693
693
  if (block === void 0) continue;
694
694
  if (block.type === "text") {
695
695
  const text = string$2(block.text);
@@ -722,7 +722,7 @@ function normalizeAssistant(message) {
722
722
  }
723
723
  function normalizeUser(message) {
724
724
  if (message.isReplay === true) return [];
725
- const content = record$4(message.message)?.content;
725
+ const content = record$5(message.message)?.content;
726
726
  if (typeof content === "string") return [];
727
727
  if (!Array.isArray(content)) return [{
728
728
  kind: "protocol-error",
@@ -732,7 +732,7 @@ function normalizeUser(message) {
732
732
  const parentToolUseId = string$2(message.parent_tool_use_id);
733
733
  const normalized = [];
734
734
  for (const item of content) {
735
- const block = record$4(item);
735
+ const block = record$5(item);
736
736
  if (block?.type !== "tool_result") continue;
737
737
  const toolUseId = string$2(block.tool_use_id);
738
738
  if (toolUseId === void 0) continue;
@@ -813,7 +813,7 @@ function normalizeSystem(message) {
813
813
  const summary = string$2(message.summary);
814
814
  const subagentType = string$2(message.subagent_type);
815
815
  const lastToolName = string$2(message.last_tool_name);
816
- const usage = taskUsageOf(record$4(message.usage));
816
+ const usage = taskUsageOf(record$5(message.usage));
817
817
  return [{
818
818
  kind: "subagent",
819
819
  title: summary ?? description ?? "Claude subagent update",
@@ -829,7 +829,7 @@ function normalizeSystem(message) {
829
829
  }];
830
830
  }
831
831
  if (subtype === "task_updated") {
832
- const patch = record$4(message.patch);
832
+ const patch = record$5(message.patch);
833
833
  const status = string$2(patch?.status);
834
834
  const taskId = string$2(message.task_id);
835
835
  const description = string$2(patch?.description);
@@ -852,7 +852,7 @@ function normalizeSystem(message) {
852
852
  const taskId = string$2(message.task_id);
853
853
  const summary = string$2(message.summary);
854
854
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
855
- const usage = taskUsageOf(record$4(message.usage));
855
+ const usage = taskUsageOf(record$5(message.usage));
856
856
  return [{
857
857
  kind: "subagent",
858
858
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -867,7 +867,7 @@ function normalizeSystem(message) {
867
867
  if (subtype === "background_tasks_changed") return [{
868
868
  kind: "background-tasks",
869
869
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
870
- const entry = record$4(item);
870
+ const entry = record$5(item);
871
871
  const taskId = string$2(entry?.task_id);
872
872
  const description = string$2(entry?.description);
873
873
  const taskType = string$2(entry?.task_type);
@@ -910,10 +910,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
910
910
  function normalizeSdkMessage(message) {
911
911
  const value = message;
912
912
  if (value.type === "stream_event") {
913
- const event = record$4(value.event);
913
+ const event = record$5(value.event);
914
914
  const parentToolUseId = string$2(value.parent_tool_use_id);
915
915
  if (event?.type === "content_block_delta") {
916
- const delta = record$4(event.delta);
916
+ const delta = record$5(event.delta);
917
917
  if (delta?.type === "text_delta") {
918
918
  const text = string$2(delta.text);
919
919
  return text === void 0 ? [] : [{
@@ -948,7 +948,7 @@ function normalizeSdkMessage(message) {
948
948
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
949
949
  const terminalReason = string$2(value.terminal_reason);
950
950
  const userMessageUuid = string$2(value.user_message_uuid);
951
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$4(item)).filter((item) => item !== void 0).map((item) => {
951
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$5(item)).filter((item) => item !== void 0).map((item) => {
952
952
  const toolName = string$2(item.tool_name);
953
953
  const toolUseId = string$2(item.tool_use_id);
954
954
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
@@ -974,7 +974,7 @@ function normalizeSdkMessage(message) {
974
974
  detail: value.error ?? value.output
975
975
  }];
976
976
  if (value.type === "rate_limit_event") {
977
- const status = string$2(record$4(value.rate_limit_info)?.status);
977
+ const status = string$2(record$5(value.rate_limit_info)?.status);
978
978
  return [{
979
979
  kind: "status",
980
980
  title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
@@ -2510,7 +2510,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2510
2510
  }
2511
2511
  //#endregion
2512
2512
  //#region src/projection-routes.ts
2513
- const MAX_SESSION_ID_CHARS$2 = 1024;
2513
+ const MAX_SESSION_ID_CHARS$3 = 1024;
2514
2514
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
2515
2515
  * the transcript hot path so git/gh latency never delays visible text. */
2516
2516
  const META_REFRESH_MS = 5e3;
@@ -2524,7 +2524,7 @@ function targetFromUrl(rawUrl) {
2524
2524
  if (stream) encoded = encoded.slice(0, -7);
2525
2525
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2526
2526
  const sessionId = decodeURIComponent(encoded);
2527
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
2527
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$3) return void 0;
2528
2528
  return {
2529
2529
  sessionId,
2530
2530
  stream
@@ -2684,17 +2684,17 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2684
2684
  }
2685
2685
  //#endregion
2686
2686
  //#region src/repository-status.ts
2687
- const MAX_OUTPUT_BYTES$2 = 65536;
2687
+ const MAX_OUTPUT_BYTES$3 = 65536;
2688
2688
  const MAX_DIFF_BYTES = 262144;
2689
2689
  const MAX_UNTRACKED_DIFFS = 50;
2690
- const GIT_TIMEOUT_MS$2 = 5e3;
2691
- const GH_TIMEOUT_MS = 8e3;
2690
+ const GIT_TIMEOUT_MS$3 = 5e3;
2691
+ const GH_TIMEOUT_MS$1 = 8e3;
2692
2692
  const CACHE_TTL_MS = 5e3;
2693
2693
  const MAX_TEXT_CHARS = 1024;
2694
2694
  function bounded(value) {
2695
2695
  return value.trim().slice(0, MAX_TEXT_CHARS);
2696
2696
  }
2697
- async function collect$2(handle) {
2697
+ async function collect$3(handle) {
2698
2698
  const outcome = await handle.done;
2699
2699
  const stdout = handle.collected.stdout?.readFrom(0);
2700
2700
  return {
@@ -2703,15 +2703,15 @@ async function collect$2(handle) {
2703
2703
  lossy: stdout?.lossy === true
2704
2704
  };
2705
2705
  }
2706
- async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
2706
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$3) {
2707
2707
  const signal = AbortSignal.timeout(timeoutMs);
2708
- return collect$2(runtime.spawn({
2708
+ return collect$3(runtime.spawn({
2709
2709
  argv: [executable, ...args],
2710
2710
  cwd,
2711
2711
  stdio: {
2712
2712
  stdin: "ignore",
2713
2713
  stdout: { maxBytes },
2714
- stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
2714
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
2715
2715
  },
2716
2716
  graceMs: 1e3,
2717
2717
  signal,
@@ -2781,14 +2781,14 @@ function parseGitHubRemote(value) {
2781
2781
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2782
2782
  return `${match[1]}/${match[2]}`;
2783
2783
  }
2784
- function record$3(value) {
2784
+ function record$4(value) {
2785
2785
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2786
2786
  }
2787
2787
  function aggregateChecks(value) {
2788
2788
  if (!Array.isArray(value) || value.length === 0) return "none";
2789
2789
  let pending = false;
2790
2790
  for (const item of value) {
2791
- const check = record$3(item);
2791
+ const check = record$4(item);
2792
2792
  if (check === void 0) continue;
2793
2793
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2794
2794
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2812,7 +2812,7 @@ function reviewState(value) {
2812
2812
  return "none";
2813
2813
  }
2814
2814
  function parsePullRequest(value) {
2815
- const input = record$3(value);
2815
+ const input = record$4(value);
2816
2816
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2817
2817
  let url;
2818
2818
  try {
@@ -2833,7 +2833,7 @@ function parsePullRequest(value) {
2833
2833
  review: reviewState(input.reviewDecision),
2834
2834
  checks: aggregateChecks(input.statusCheckRollup),
2835
2835
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2836
- ...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
2836
+ ...typeof record$4(input.author)?.login === "string" ? { author: bounded(String(record$4(input.author)?.login)) } : {},
2837
2837
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2838
2838
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2839
2839
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
@@ -2906,7 +2906,7 @@ var RepositoryStatusService = class {
2906
2906
  "--show-toplevel",
2907
2907
  "--absolute-git-dir",
2908
2908
  "--git-common-dir"
2909
- ], cwd, GIT_TIMEOUT_MS$2);
2909
+ ], cwd, GIT_TIMEOUT_MS$3);
2910
2910
  if (paths.exitCode !== 0) return {
2911
2911
  status: "not-repository",
2912
2912
  cwd: safeCwd
@@ -2924,7 +2924,7 @@ var RepositoryStatusService = class {
2924
2924
  "--porcelain=v2",
2925
2925
  "--branch",
2926
2926
  "--untracked-files=normal"
2927
- ], cwd, GIT_TIMEOUT_MS$2);
2927
+ ], cwd, GIT_TIMEOUT_MS$3);
2928
2928
  if (statusResult.exitCode !== 0) return {
2929
2929
  status: "unavailable",
2930
2930
  cwd: safeCwd
@@ -2934,7 +2934,7 @@ var RepositoryStatusService = class {
2934
2934
  "remote",
2935
2935
  "get-url",
2936
2936
  "origin"
2937
- ], cwd, GIT_TIMEOUT_MS$2);
2937
+ ], cwd, GIT_TIMEOUT_MS$3);
2938
2938
  const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
2939
2939
  const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
2940
2940
  const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
@@ -2944,6 +2944,7 @@ var RepositoryStatusService = class {
2944
2944
  files: 0,
2945
2945
  truncated: false
2946
2946
  };
2947
+ const baseBehind = pullRequest?.state === "open" && pullRequest.baseBranch !== void 0 ? await this.#baseBehind(cwd, git, pullRequest.baseBranch) : void 0;
2947
2948
  return {
2948
2949
  status: "ready",
2949
2950
  cwd: safeCwd,
@@ -2952,7 +2953,8 @@ var RepositoryStatusService = class {
2952
2953
  worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
2953
2954
  ...remote === void 0 ? {} : { remote },
2954
2955
  ...pullRequest === void 0 ? {} : { pullRequest },
2955
- ...diff === void 0 ? {} : { diff }
2956
+ ...diff === void 0 ? {} : { diff },
2957
+ ...baseBehind === void 0 ? {} : { baseBehind }
2956
2958
  };
2957
2959
  } catch {
2958
2960
  return {
@@ -2961,13 +2963,28 @@ var RepositoryStatusService = class {
2961
2963
  };
2962
2964
  }
2963
2965
  }
2966
+ async #baseBehind(cwd, git, baseBranch) {
2967
+ try {
2968
+ const result = await run(this.#runtime, git, [
2969
+ "rev-list",
2970
+ "--count",
2971
+ `HEAD..refs/remotes/origin/${baseBranch}`,
2972
+ "--"
2973
+ ], cwd, GIT_TIMEOUT_MS$3);
2974
+ if (result.exitCode !== 0 || result.lossy) return void 0;
2975
+ const count = Number(bounded(result.stdout));
2976
+ return Number.isSafeInteger(count) && count >= 0 ? count : void 0;
2977
+ } catch {
2978
+ return;
2979
+ }
2980
+ }
2964
2981
  async #mergeBase(cwd, git, baseBranch) {
2965
2982
  try {
2966
2983
  const result = await run(this.#runtime, git, [
2967
2984
  "merge-base",
2968
2985
  "HEAD",
2969
2986
  `refs/remotes/origin/${baseBranch}`
2970
- ], cwd, GIT_TIMEOUT_MS$2);
2987
+ ], cwd, GIT_TIMEOUT_MS$3);
2971
2988
  if (result.exitCode !== 0 || result.lossy) return void 0;
2972
2989
  const oid = bounded(result.stdout);
2973
2990
  return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
@@ -2983,7 +3000,7 @@ var RepositoryStatusService = class {
2983
3000
  "--numstat",
2984
3001
  base,
2985
3002
  "--"
2986
- ], cwd, GIT_TIMEOUT_MS$2);
3003
+ ], cwd, GIT_TIMEOUT_MS$3);
2987
3004
  if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
2988
3005
  const summary = parseDiffNumstat(numstat.stdout);
2989
3006
  const patch = await run(this.#runtime, git, [
@@ -2993,7 +3010,7 @@ var RepositoryStatusService = class {
2993
3010
  "--unified=3",
2994
3011
  base,
2995
3012
  "--"
2996
- ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
3013
+ ], cwd, GIT_TIMEOUT_MS$3, MAX_DIFF_BYTES);
2997
3014
  if (patch.exitCode !== 0) return {
2998
3015
  ...summary,
2999
3016
  truncated: true
@@ -3017,7 +3034,7 @@ var RepositoryStatusService = class {
3017
3034
  "--others",
3018
3035
  "--exclude-standard",
3019
3036
  "-z"
3020
- ], cwd, GIT_TIMEOUT_MS$2);
3037
+ ], cwd, GIT_TIMEOUT_MS$3);
3021
3038
  if (listed.exitCode !== 0 || listed.lossy) return {
3022
3039
  additions: 0,
3023
3040
  files: 0,
@@ -3040,7 +3057,7 @@ var RepositoryStatusService = class {
3040
3057
  "--",
3041
3058
  "/dev/null",
3042
3059
  path
3043
- ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
3060
+ ], cwd, GIT_TIMEOUT_MS$3, MAX_DIFF_BYTES);
3044
3061
  if (result.exitCode !== 0 && result.exitCode !== 1) {
3045
3062
  truncated = true;
3046
3063
  continue;
@@ -3069,7 +3086,7 @@ var RepositoryStatusService = class {
3069
3086
  repository,
3070
3087
  "--json",
3071
3088
  "number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName"
3072
- ], cwd, GH_TIMEOUT_MS);
3089
+ ], cwd, GH_TIMEOUT_MS$1);
3073
3090
  if (result.exitCode !== 0) return void 0;
3074
3091
  return parsePullRequest(JSON.parse(result.stdout));
3075
3092
  } catch {
@@ -3079,12 +3096,13 @@ var RepositoryStatusService = class {
3079
3096
  };
3080
3097
  //#endregion
3081
3098
  //#region src/repository-setup.ts
3082
- const MAX_OUTPUT_BYTES$1 = 131072;
3083
- const GIT_TIMEOUT_MS$1 = 1e4;
3099
+ const MAX_OUTPUT_BYTES$2 = 131072;
3100
+ const GIT_TIMEOUT_MS$2 = 1e4;
3084
3101
  const GIT_FETCH_TIMEOUT_MS = 6e4;
3085
3102
  const MAX_PATH_CHARS = 4096;
3086
3103
  const MAX_BRANCH_CHARS = 512;
3087
3104
  const LEASE_SCHEMA_VERSION = 1;
3105
+ const CLEANUP_GRACE_MS = 12e4;
3088
3106
  var RepositorySetupError = class extends Error {
3089
3107
  code;
3090
3108
  constructor(code, message) {
@@ -3093,7 +3111,7 @@ var RepositorySetupError = class extends Error {
3093
3111
  this.code = code;
3094
3112
  }
3095
3113
  };
3096
- async function collect$1(handle) {
3114
+ async function collect$2(handle) {
3097
3115
  const outcome = await handle.done;
3098
3116
  const stdout = handle.collected.stdout?.readFrom(0);
3099
3117
  const stderr = handle.collected.stderr?.readFrom(0);
@@ -3127,6 +3145,14 @@ function parseWorktreeBranches(value) {
3127
3145
  function slug(value, fallback) {
3128
3146
  return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
3129
3147
  }
3148
+ /** Comparable form for path identity: resolved, forward slashes, case-folded
3149
+ * so Windows drive-letter or case spelling differences cannot hide a match. */
3150
+ function comparablePath(value) {
3151
+ return resolve(value).replaceAll("\\", "/").toLocaleLowerCase("en-US");
3152
+ }
3153
+ function pathExists(value) {
3154
+ return stat(value).then(() => true, () => false);
3155
+ }
3130
3156
  function lease(value) {
3131
3157
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
3132
3158
  const input = value;
@@ -3146,6 +3172,7 @@ var RepositorySetupService = class {
3146
3172
  #leasePath;
3147
3173
  #worktreeRoot;
3148
3174
  #branchPrefix;
3175
+ #cleanupGraceMs;
3149
3176
  #gitPath;
3150
3177
  #pending = Promise.resolve();
3151
3178
  constructor(runtime, options = {}) {
@@ -3153,6 +3180,7 @@ var RepositorySetupService = class {
3153
3180
  this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
3154
3181
  this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
3155
3182
  this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
3183
+ this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
3156
3184
  }
3157
3185
  async listBranches(cwd) {
3158
3186
  const git = await this.#git();
@@ -3215,14 +3243,22 @@ var RepositorySetupService = class {
3215
3243
  await this.#writeLeases(leases);
3216
3244
  });
3217
3245
  }
3218
- cleanupSession(sessionId) {
3246
+ /** Reconcile leases against the set of directories still referenced by a
3247
+ * workspace: an unreferenced lease's clean worktree is removed. Fresh
3248
+ * leases are retained for a grace period so a worktree created moments ago
3249
+ * cannot be swept before its workspace registration lands, and dirty
3250
+ * worktrees are always retained to avoid losing uncommitted work. */
3251
+ cleanupOrphans(activePaths) {
3252
+ const active = new Set(activePaths.map(comparablePath));
3219
3253
  return this.#serialize(async () => {
3220
3254
  const leases = await this.#readLeases();
3221
3255
  const retained = [];
3222
3256
  let changed = false;
3223
3257
  const git = await this.#git();
3258
+ const now = Date.now();
3224
3259
  for (const item of leases) {
3225
- if (item.sessionId !== sessionId) {
3260
+ const age = now - Date.parse(item.createdAt);
3261
+ if (active.has(comparablePath(item.path)) || !(age >= this.#cleanupGraceMs)) {
3226
3262
  retained.push(item);
3227
3263
  continue;
3228
3264
  }
@@ -3232,7 +3268,15 @@ var RepositorySetupService = class {
3232
3268
  "--porcelain=v1",
3233
3269
  "--untracked-files=normal"
3234
3270
  ], item.path);
3235
- if (status.exitCode !== 0 || status.lossy || status.stdout.trim().length > 0) {
3271
+ if (status.exitCode !== 0 || status.lossy) {
3272
+ if (await pathExists(item.path)) retained.push(item);
3273
+ else {
3274
+ await this.#run(git, ["worktree", "prune"], item.root).catch(() => void 0);
3275
+ changed = true;
3276
+ }
3277
+ continue;
3278
+ }
3279
+ if (status.stdout.trim().length > 0) {
3236
3280
  retained.push(item);
3237
3281
  continue;
3238
3282
  }
@@ -3253,7 +3297,8 @@ var RepositorySetupService = class {
3253
3297
  ], item.root).catch(() => void 0);
3254
3298
  changed = true;
3255
3299
  } catch {
3256
- retained.push(item);
3300
+ if (await pathExists(item.root)) retained.push(item);
3301
+ else changed = true;
3257
3302
  }
3258
3303
  }
3259
3304
  if (changed) await this.#writeLeases(retained);
@@ -3386,14 +3431,14 @@ var RepositorySetupService = class {
3386
3431
  this.#gitPath ??= this.#runtime.resolveExecutable("git");
3387
3432
  return this.#gitPath;
3388
3433
  }
3389
- async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
3390
- return collect$1(this.#runtime.spawn({
3434
+ async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$2) {
3435
+ return collect$2(this.#runtime.spawn({
3391
3436
  argv: [executable, ...args],
3392
3437
  cwd,
3393
3438
  stdio: {
3394
3439
  stdin: "ignore",
3395
- stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
3396
- stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
3440
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$2 },
3441
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
3397
3442
  },
3398
3443
  graceMs: 1e3,
3399
3444
  signal: AbortSignal.timeout(timeoutMs),
@@ -3434,12 +3479,12 @@ var RepositorySetupService = class {
3434
3479
  };
3435
3480
  //#endregion
3436
3481
  //#region src/repository-actions.ts
3437
- const MAX_OUTPUT_BYTES = 262144;
3482
+ const MAX_OUTPUT_BYTES$1 = 262144;
3438
3483
  const MAX_PATCH_CHARS = 65536;
3439
3484
  const MAX_MESSAGE_CHARS = 512;
3440
3485
  const MAX_PR_TEXT_CHARS = 8192;
3441
3486
  const MAX_UNPUSHED_COMMITS = 20;
3442
- const GIT_TIMEOUT_MS = 15e3;
3487
+ const GIT_TIMEOUT_MS$1 = 15e3;
3443
3488
  const REMOTE_TIMEOUT_MS = 6e4;
3444
3489
  const GENERATE_TIMEOUT_MS = 6e4;
3445
3490
  var RepositoryActionError = class extends Error {
@@ -3452,7 +3497,7 @@ var RepositoryActionError = class extends Error {
3452
3497
  if (commit !== void 0) this.commit = commit;
3453
3498
  }
3454
3499
  };
3455
- async function collect(handle) {
3500
+ async function collect$1(handle) {
3456
3501
  const outcome = await handle.done;
3457
3502
  const stdout = handle.collected.stdout?.readFrom(0);
3458
3503
  const stderr = handle.collected.stderr?.readFrom(0);
@@ -3579,6 +3624,77 @@ var RepositoryActionService = class {
3579
3624
  pushed: true
3580
3625
  };
3581
3626
  }
3627
+ if (request.action === "merge-pr") {
3628
+ const method = request.mergeMethod;
3629
+ if (method !== "merge" && method !== "squash" && method !== "rebase") throw new RepositoryActionError("invalid-request", "The merge method is invalid.");
3630
+ let gh;
3631
+ try {
3632
+ gh = await this.#gh();
3633
+ } catch (error) {
3634
+ throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.");
3635
+ }
3636
+ const merged = await this.#run(gh, [
3637
+ "pr",
3638
+ "merge",
3639
+ `--${method}`
3640
+ ], before.root, REMOTE_TIMEOUT_MS);
3641
+ if (merged.exitCode !== 0 || merged.lossy) {
3642
+ const reason = merged.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
3643
+ throw new RepositoryActionError("merge-failed", reason === void 0 || reason.length === 0 ? "The pull request could not be merged." : reason);
3644
+ }
3645
+ this.#invalidate(before.root);
3646
+ return {
3647
+ commit: before.head,
3648
+ pushed: true
3649
+ };
3650
+ }
3651
+ if (request.action === "update-branch") {
3652
+ const base = safeText(request.baseBranch ?? "", 512, "Base branch");
3653
+ if (before.files.length > 0) throw new RepositoryActionError("dirty-workspace", "Commit or stash workspace changes before updating the branch.");
3654
+ const git = await this.#git();
3655
+ await this.#mustRun(git, [
3656
+ "fetch",
3657
+ "origin",
3658
+ "--",
3659
+ base
3660
+ ], before.root, REMOTE_TIMEOUT_MS, "fetch-failed", "Git could not fetch the base branch.");
3661
+ const merged = await this.#run(git, [
3662
+ "merge",
3663
+ "--no-edit",
3664
+ "--",
3665
+ `origin/${base}`
3666
+ ], before.root, REMOTE_TIMEOUT_MS);
3667
+ if (merged.exitCode !== 0 || merged.lossy) {
3668
+ const conflicted = await this.#run(git, [
3669
+ "diff",
3670
+ "--name-only",
3671
+ "--diff-filter=U",
3672
+ "--"
3673
+ ], before.root, GIT_TIMEOUT_MS$1);
3674
+ const conflicts = conflicted.exitCode === 0 && !conflicted.lossy ? conflicted.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100) : [];
3675
+ if (conflicts.length === 0) {
3676
+ await this.#run(git, ["merge", "--abort"], before.root, GIT_TIMEOUT_MS$1).catch(() => void 0);
3677
+ throw new RepositoryActionError("merge-failed", "Git could not merge the base branch.");
3678
+ }
3679
+ this.#invalidate(before.root);
3680
+ return {
3681
+ commit: before.head,
3682
+ pushed: false,
3683
+ conflicts
3684
+ };
3685
+ }
3686
+ const mergedHead = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS$1, "merge-failed", "The merge commit could not be verified.")).stdout.trim();
3687
+ try {
3688
+ await this.#push(git, before.root, before.branch);
3689
+ } catch (error) {
3690
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", mergedHead);
3691
+ }
3692
+ this.#invalidate(before.root);
3693
+ return {
3694
+ commit: mergedHead,
3695
+ pushed: true
3696
+ };
3697
+ }
3582
3698
  const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
3583
3699
  if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
3584
3700
  const git = await this.#git();
@@ -3591,7 +3707,7 @@ var RepositoryActionService = class {
3591
3707
  "add",
3592
3708
  "--",
3593
3709
  ...paths
3594
- ], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
3710
+ ], before.root, GIT_TIMEOUT_MS$1, "stage-failed", "Changes could not be staged.");
3595
3711
  }
3596
3712
  await this.#rejectStagedWarp(git, before.root);
3597
3713
  const staged = await this.#run(git, [
@@ -3600,7 +3716,7 @@ var RepositoryActionService = class {
3600
3716
  "--quiet",
3601
3717
  "--exit-code",
3602
3718
  "--"
3603
- ], before.root, GIT_TIMEOUT_MS);
3719
+ ], before.root, GIT_TIMEOUT_MS$1);
3604
3720
  if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
3605
3721
  if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
3606
3722
  await this.#mustRun(git, [
@@ -3608,8 +3724,8 @@ var RepositoryActionService = class {
3608
3724
  "-m",
3609
3725
  message,
3610
3726
  "--"
3611
- ], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
3612
- oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
3727
+ ], before.root, GIT_TIMEOUT_MS$1, "commit-failed", "Git commit failed.");
3728
+ oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS$1, "commit-failed", "The new commit could not be verified.")).stdout.trim();
3613
3729
  this.#invalidate(before.root);
3614
3730
  if (request.action === "commit") return {
3615
3731
  commit: oid,
@@ -3660,21 +3776,21 @@ var RepositoryActionService = class {
3660
3776
  "rev-parse",
3661
3777
  "--path-format=absolute",
3662
3778
  "--show-toplevel"
3663
- ], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
3779
+ ], cwd, GIT_TIMEOUT_MS$1, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
3664
3780
  const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
3665
3781
  this.#run(git, [
3666
3782
  "symbolic-ref",
3667
3783
  "--quiet",
3668
3784
  "--short",
3669
3785
  "HEAD"
3670
- ], root, GIT_TIMEOUT_MS),
3671
- this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
3786
+ ], root, GIT_TIMEOUT_MS$1),
3787
+ this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS$1),
3672
3788
  this.#run(git, [
3673
3789
  "status",
3674
3790
  "--porcelain=v1",
3675
3791
  "-z",
3676
3792
  "--untracked-files=all"
3677
- ], root, GIT_TIMEOUT_MS),
3793
+ ], root, GIT_TIMEOUT_MS$1),
3678
3794
  this.#run(git, [
3679
3795
  "diff",
3680
3796
  "--cached",
@@ -3684,7 +3800,7 @@ var RepositoryActionService = class {
3684
3800
  "--",
3685
3801
  ":(exclude)WARP.md",
3686
3802
  ":(exclude)**/WARP.md"
3687
- ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
3803
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
3688
3804
  this.#run(git, [
3689
3805
  "diff",
3690
3806
  "--no-ext-diff",
@@ -3693,7 +3809,7 @@ var RepositoryActionService = class {
3693
3809
  "--",
3694
3810
  ":(exclude)WARP.md",
3695
3811
  ":(exclude)**/WARP.md"
3696
- ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
3812
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
3697
3813
  ]);
3698
3814
  if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
3699
3815
  if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
@@ -3713,7 +3829,7 @@ var RepositoryActionService = class {
3713
3829
  "--abbrev-ref",
3714
3830
  "--symbolic-full-name",
3715
3831
  "@{upstream}"
3716
- ], root, GIT_TIMEOUT_MS);
3832
+ ], root, GIT_TIMEOUT_MS$1);
3717
3833
  const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
3718
3834
  const logResult = await this.#run(git, [
3719
3835
  "log",
@@ -3722,7 +3838,7 @@ var RepositoryActionService = class {
3722
3838
  String(21),
3723
3839
  upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
3724
3840
  "--"
3725
- ], root, GIT_TIMEOUT_MS);
3841
+ ], root, GIT_TIMEOUT_MS$1);
3726
3842
  const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
3727
3843
  const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
3728
3844
  const tab = line.indexOf(" ");
@@ -3754,7 +3870,7 @@ var RepositoryActionService = class {
3754
3870
  "--cached",
3755
3871
  "--name-only",
3756
3872
  "--"
3757
- ], cwd, GIT_TIMEOUT_MS);
3873
+ ], cwd, GIT_TIMEOUT_MS$1);
3758
3874
  if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
3759
3875
  if (staged.stdout.split(/\r?\n/u).some((path) => path.length > 0 && isProtectedWarpPath(path))) throw new RepositoryActionError("protected-warp-file", "WARP.md files cannot be committed. Unstage them before continuing.");
3760
3876
  }
@@ -3764,7 +3880,7 @@ var RepositoryActionService = class {
3764
3880
  "--abbrev-ref",
3765
3881
  "--symbolic-full-name",
3766
3882
  "@{upstream}"
3767
- ], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
3883
+ ], cwd, GIT_TIMEOUT_MS$1)).exitCode === 0 ? ["push"] : [
3768
3884
  "push",
3769
3885
  "--set-upstream",
3770
3886
  "origin",
@@ -3787,14 +3903,14 @@ var RepositoryActionService = class {
3787
3903
  if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
3788
3904
  return result;
3789
3905
  }
3790
- #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
3791
- return collect(this.#runtime.spawn({
3906
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
3907
+ return collect$1(this.#runtime.spawn({
3792
3908
  argv: [executable, ...args],
3793
3909
  cwd,
3794
3910
  stdio: {
3795
3911
  stdin: "ignore",
3796
3912
  stdout: { maxBytes },
3797
- stderr: { maxBytes: MAX_OUTPUT_BYTES }
3913
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
3798
3914
  },
3799
3915
  graceMs: 1e3,
3800
3916
  signal: AbortSignal.timeout(timeoutMs),
@@ -3805,7 +3921,7 @@ var RepositoryActionService = class {
3805
3921
  //#endregion
3806
3922
  //#region src/repository-setup-routes.ts
3807
3923
  const MAX_BODY_BYTES$2 = 16384;
3808
- function record$2(value) {
3924
+ function record$3(value) {
3809
3925
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3810
3926
  }
3811
3927
  async function readJson$2(req) {
@@ -3817,7 +3933,7 @@ async function readJson$2(req) {
3817
3933
  if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3818
3934
  chunks.push(buffer);
3819
3935
  }
3820
- const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3936
+ const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3821
3937
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3822
3938
  return value;
3823
3939
  }
@@ -3911,14 +4027,16 @@ function registerRepositorySetupRoute(ctx, service) {
3911
4027
  //#endregion
3912
4028
  //#region src/repository-action-routes.ts
3913
4029
  const MAX_BODY_BYTES$1 = 16384;
3914
- const MAX_SESSION_ID_CHARS$1 = 1024;
4030
+ const MAX_SESSION_ID_CHARS$2 = 1024;
3915
4031
  const ACTIONS = /* @__PURE__ */ new Set([
3916
4032
  "commit",
3917
4033
  "commit-push",
3918
4034
  "push",
3919
- "create-pr"
4035
+ "create-pr",
4036
+ "merge-pr",
4037
+ "update-branch"
3920
4038
  ]);
3921
- function record$1(value) {
4039
+ function record$2(value) {
3922
4040
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3923
4041
  }
3924
4042
  async function readJson$1(req) {
@@ -3930,13 +4048,13 @@ async function readJson$1(req) {
3930
4048
  if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
3931
4049
  chunks.push(buffer);
3932
4050
  }
3933
- const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4051
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3934
4052
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
3935
4053
  return value;
3936
4054
  }
3937
- function sessionId(url) {
4055
+ function sessionId$1(url) {
3938
4056
  const value = url.searchParams.get("sessionId");
3939
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4057
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new RepositoryActionError("invalid-session", "The session is invalid.");
3940
4058
  return value;
3941
4059
  }
3942
4060
  function string(input, key) {
@@ -3956,13 +4074,16 @@ function actionRequest(input) {
3956
4074
  return {
3957
4075
  action,
3958
4076
  fingerprint: string(input, "fingerprint"),
3959
- message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
4077
+ message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string(input, "message"),
3960
4078
  includeUnstaged: input.includeUnstaged,
3961
4079
  ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
3962
4080
  ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
3963
4081
  ...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
3964
4082
  ...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
3965
4083
  throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
4084
+ })(),
4085
+ ...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
4086
+ throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
3966
4087
  })()
3967
4088
  };
3968
4089
  }
@@ -3974,7 +4095,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
3974
4095
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3975
4096
  const url = new URL(req.url ?? "/", "http://localhost");
3976
4097
  try {
3977
- const cwd = cwdForSession(sessionId(url));
4098
+ const cwd = cwdForSession(sessionId$1(url));
3978
4099
  if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
3979
4100
  if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
3980
4101
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
@@ -4006,6 +4127,218 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4006
4127
  }), "dsh-claude: repository action route");
4007
4128
  }
4008
4129
  //#endregion
4130
+ //#region src/pr-feedback.ts
4131
+ const MAX_OUTPUT_BYTES = 524288;
4132
+ const GIT_TIMEOUT_MS = 1e4;
4133
+ const GH_TIMEOUT_MS = 6e4;
4134
+ const MAX_COMMENTS = 100;
4135
+ const MAX_COMMENT_CHARS = 4096;
4136
+ const MAX_FAILING_CHECKS = 3;
4137
+ const MAX_LOG_CHARS = 8192;
4138
+ var PullRequestFeedbackError = class extends Error {
4139
+ code;
4140
+ constructor(code, message) {
4141
+ super(message);
4142
+ this.name = "PullRequestFeedbackError";
4143
+ this.code = code;
4144
+ }
4145
+ };
4146
+ async function collect(handle) {
4147
+ const outcome = await handle.done;
4148
+ const stdout = handle.collected.stdout?.readFrom(0);
4149
+ return {
4150
+ exitCode: outcome.exitCode,
4151
+ stdout: stdout?.text ?? "",
4152
+ lossy: stdout?.lossy === true
4153
+ };
4154
+ }
4155
+ function record$1(value) {
4156
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4157
+ }
4158
+ function parseReviewComments(value) {
4159
+ if (!Array.isArray(value)) return [];
4160
+ const comments = [];
4161
+ for (const item of value) {
4162
+ const input = record$1(item);
4163
+ if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4164
+ const body = typeof input.body === "string" ? input.body.trim() : "";
4165
+ const path = typeof input.path === "string" ? input.path : "";
4166
+ const url = typeof input.html_url === "string" ? input.html_url : "";
4167
+ if (body.length === 0 || path.length === 0) continue;
4168
+ const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.original_line) ? Number(input.original_line) : void 0;
4169
+ comments.push({
4170
+ id: Number(input.id),
4171
+ path,
4172
+ ...line === void 0 ? {} : { line },
4173
+ side: input.side === "LEFT" ? "old" : "new",
4174
+ author: typeof record$1(input.user)?.login === "string" ? String(record$1(input.user)?.login) : "unknown",
4175
+ body: body.slice(0, MAX_COMMENT_CHARS),
4176
+ url
4177
+ });
4178
+ if (comments.length >= MAX_COMMENTS) break;
4179
+ }
4180
+ return comments;
4181
+ }
4182
+ /** Extract the Actions job id from a check run link, when it is one. */
4183
+ function actionsJobId(link) {
4184
+ if (link === void 0) return void 0;
4185
+ return /\/actions\/runs\/\d+\/jobs?\/(\d+)/u.exec(link)?.[1];
4186
+ }
4187
+ function parseFailingChecks(value) {
4188
+ if (!Array.isArray(value)) return [];
4189
+ const failing = [];
4190
+ for (const item of value) {
4191
+ const input = record$1(item);
4192
+ if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4193
+ failing.push({
4194
+ name: input.name,
4195
+ ...typeof input.link === "string" && input.link.length > 0 ? { link: input.link } : {},
4196
+ ...typeof input.description === "string" && input.description.length > 0 ? { description: input.description } : {}
4197
+ });
4198
+ }
4199
+ return failing;
4200
+ }
4201
+ /** Keep the informative end of a failed-job log within the size budget. */
4202
+ function boundedLogTail(value) {
4203
+ const text = value.replaceAll("\0", "").trimEnd();
4204
+ return text.length <= MAX_LOG_CHARS ? text : text.slice(text.length - MAX_LOG_CHARS);
4205
+ }
4206
+ var PullRequestFeedbackService = class {
4207
+ #runtime;
4208
+ #gitExecutable;
4209
+ #ghExecutable;
4210
+ constructor(runtime) {
4211
+ this.#runtime = runtime;
4212
+ }
4213
+ async comments(cwd, pullNumber) {
4214
+ const repository = await this.#repository(cwd);
4215
+ const gh = await this.#gh();
4216
+ const result = await this.#run(gh, [
4217
+ "api",
4218
+ `repos/${repository}/pulls/${pullNumber}/comments`,
4219
+ "--paginate"
4220
+ ], cwd, GH_TIMEOUT_MS);
4221
+ if (result.exitCode !== 0) throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be loaded.");
4222
+ try {
4223
+ const pages = result.stdout.replaceAll("][", ",");
4224
+ return parseReviewComments(JSON.parse(pages));
4225
+ } catch {
4226
+ throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be parsed.");
4227
+ }
4228
+ }
4229
+ async failingChecks(cwd, pullNumber) {
4230
+ const gh = await this.#gh();
4231
+ const result = await this.#run(gh, [
4232
+ "pr",
4233
+ "checks",
4234
+ String(pullNumber),
4235
+ "--json",
4236
+ "name,state,link,description,bucket"
4237
+ ], cwd, GH_TIMEOUT_MS);
4238
+ let checks;
4239
+ try {
4240
+ checks = parseFailingChecks(JSON.parse(result.stdout));
4241
+ } catch {
4242
+ throw new PullRequestFeedbackError("checks-unavailable", "Pull request checks could not be loaded.");
4243
+ }
4244
+ const detailed = [];
4245
+ for (const check of checks) {
4246
+ const jobId = detailed.length < MAX_FAILING_CHECKS ? actionsJobId(check.link) : void 0;
4247
+ if (jobId === void 0) {
4248
+ detailed.push(check);
4249
+ continue;
4250
+ }
4251
+ const log = await this.#run(gh, [
4252
+ "run",
4253
+ "view",
4254
+ "--job",
4255
+ jobId,
4256
+ "--log-failed"
4257
+ ], cwd, GH_TIMEOUT_MS);
4258
+ detailed.push(log.exitCode === 0 && log.stdout.trim().length > 0 ? {
4259
+ ...check,
4260
+ log: boundedLogTail(log.stdout)
4261
+ } : check);
4262
+ }
4263
+ return detailed;
4264
+ }
4265
+ async #repository(cwd) {
4266
+ const git = await this.#git();
4267
+ const remote = await this.#run(git, [
4268
+ "remote",
4269
+ "get-url",
4270
+ "origin"
4271
+ ], cwd, GIT_TIMEOUT_MS);
4272
+ const repository = remote.exitCode === 0 ? parseGitHubRemote(remote.stdout) : void 0;
4273
+ if (repository === void 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
4274
+ return repository;
4275
+ }
4276
+ #git() {
4277
+ this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
4278
+ return this.#gitExecutable;
4279
+ }
4280
+ #gh() {
4281
+ this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
4282
+ throw new PullRequestFeedbackError("gh-unavailable", "GitHub CLI is unavailable.");
4283
+ });
4284
+ return this.#ghExecutable;
4285
+ }
4286
+ #run(executable, args, cwd, timeoutMs) {
4287
+ return collect(this.#runtime.spawn({
4288
+ argv: [executable, ...args],
4289
+ cwd,
4290
+ stdio: {
4291
+ stdin: "ignore",
4292
+ stdout: { maxBytes: MAX_OUTPUT_BYTES },
4293
+ stderr: { maxBytes: 65536 }
4294
+ },
4295
+ graceMs: 1e3,
4296
+ signal: AbortSignal.timeout(timeoutMs),
4297
+ env: {}
4298
+ }));
4299
+ }
4300
+ };
4301
+ //#endregion
4302
+ //#region src/pr-feedback-routes.ts
4303
+ const MAX_SESSION_ID_CHARS$1 = 1024;
4304
+ function sessionId(url) {
4305
+ const value = url.searchParams.get("sessionId");
4306
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4307
+ return value;
4308
+ }
4309
+ function pullNumber(url) {
4310
+ const value = Number(url.searchParams.get("number"));
4311
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 1e9) throw new PullRequestFeedbackError("invalid-request", "The pull request number is invalid.");
4312
+ return value;
4313
+ }
4314
+ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
4315
+ ctx.effect(() => ctx.webServer.register({
4316
+ kind: "prefix",
4317
+ path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
4318
+ handler: async (req, res) => {
4319
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4320
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4321
+ const url = new URL(req.url ?? "/", "http://localhost");
4322
+ try {
4323
+ const cwd = cwdForSession(sessionId(url));
4324
+ if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
4325
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) return json(res, 200, { comments: await service.comments(cwd, pullNumber(url)) });
4326
+ if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) return json(res, 200, { checks: await service.failingChecks(cwd, pullNumber(url)) });
4327
+ return json(res, 404, { error: "not found" });
4328
+ } catch (error) {
4329
+ if (error instanceof PullRequestFeedbackError) return json(res, 409, {
4330
+ error: error.code,
4331
+ message: error.message
4332
+ });
4333
+ return json(res, 500, {
4334
+ error: "pr-feedback-unavailable",
4335
+ message: "Pull request feedback is unavailable."
4336
+ });
4337
+ }
4338
+ }
4339
+ }), "dsh-claude: pull request feedback route");
4340
+ }
4341
+ //#endregion
4009
4342
  //#region src/review-comment-routes.ts
4010
4343
  const MAX_BODY_BYTES = 16384;
4011
4344
  const MAX_SESSION_ID_CHARS = 1024;
@@ -4346,6 +4679,12 @@ const BUILTIN_OUTPUT_STYLES = [
4346
4679
  const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
4347
4680
  const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
4348
4681
  const MAX_BRANCH_PREFIX_CHARS = 128;
4682
+ const MAX_PROCESSES_LIMIT = 16;
4683
+ const MAX_IDLE_TIMEOUT_MINUTES = 1440;
4684
+ const DEFAULT_LIMITS = {
4685
+ maxProcesses: 4,
4686
+ idleTimeoutMs: 18e5
4687
+ };
4349
4688
  function pathsFor(deps) {
4350
4689
  const root = join(homedir(), ".claude");
4351
4690
  return {
@@ -4451,19 +4790,45 @@ const WORKTREE_BRANCH_PREFIX = {
4451
4790
  else document.worktreeBranchPrefix = value;
4452
4791
  }
4453
4792
  };
4454
- const DESCRIPTORS = [OUTPUT_STYLE, WORKTREE_BRANCH_PREFIX];
4793
+ function isBoundedInteger(value, min, max) {
4794
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
4795
+ }
4796
+ function integerSetting(key, min, max, defaultFor) {
4797
+ return {
4798
+ key,
4799
+ kind: "text",
4800
+ document: "plugin",
4801
+ effect: "new-session",
4802
+ maxLength: String(max).length,
4803
+ read(document, defaults = DEFAULT_LIMITS) {
4804
+ const value = document[key];
4805
+ return String(isBoundedInteger(value, min, max) ? value : defaultFor(defaults));
4806
+ },
4807
+ apply(document, value) {
4808
+ const parsed = typeof value === "string" && /^\d{1,6}$/u.test(value.trim()) ? Number(value.trim()) : value;
4809
+ if (!isBoundedInteger(parsed, min, max)) throw new Error(`Invalid value for global setting ${key}`);
4810
+ document[key] = parsed;
4811
+ }
4812
+ };
4813
+ }
4814
+ const DESCRIPTORS = [
4815
+ OUTPUT_STYLE,
4816
+ WORKTREE_BRANCH_PREFIX,
4817
+ integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
4818
+ integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
4819
+ ];
4455
4820
  const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
4456
4821
  let pendingWrite = Promise.resolve();
4457
4822
  function documentFor(descriptor, documents) {
4458
4823
  return documents[descriptor.document];
4459
4824
  }
4460
- async function views(documents, paths) {
4825
+ async function views(documents, paths, defaults) {
4461
4826
  return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
4462
4827
  const document = documentFor(descriptor, documents);
4463
4828
  if (descriptor.kind === "text") return {
4464
4829
  key: descriptor.key,
4465
4830
  kind: descriptor.kind,
4466
- value: descriptor.read(document),
4831
+ value: descriptor.read(document, defaults),
4467
4832
  maxLength: descriptor.maxLength,
4468
4833
  effect: descriptor.effect
4469
4834
  };
@@ -4492,7 +4857,22 @@ async function readDocuments(paths) {
4492
4857
  }
4493
4858
  async function readGlobalSettings(deps = {}) {
4494
4859
  const paths = pathsFor(deps);
4495
- return views(await readDocuments(paths), paths);
4860
+ return views(await readDocuments(paths), paths, deps.defaultLimits ?? DEFAULT_LIMITS);
4861
+ }
4862
+ /** Supervisor limits the user overrode in Settings; absent keys fall back to the plugin config. */
4863
+ async function readSupervisorLimitOverrides(deps = {}) {
4864
+ let document;
4865
+ try {
4866
+ document = await readDocument(pathsFor(deps).pluginSettingsFile);
4867
+ } catch {
4868
+ return {};
4869
+ }
4870
+ const maxProcesses = document.maxProcesses;
4871
+ const idleTimeoutMinutes = document.idleTimeoutMinutes;
4872
+ return {
4873
+ ...isBoundedInteger(maxProcesses, 1, MAX_PROCESSES_LIMIT) ? { maxProcesses } : {},
4874
+ ...isBoundedInteger(idleTimeoutMinutes, 1, MAX_IDLE_TIMEOUT_MINUTES) ? { idleTimeoutMs: idleTimeoutMinutes * 6e4 } : {}
4875
+ };
4496
4876
  }
4497
4877
  async function readWorktreeBranchPrefix(deps = {}) {
4498
4878
  const paths = pathsFor(deps);
@@ -4534,7 +4914,7 @@ function updateGlobalSettings(changes, deps = {}) {
4534
4914
  }
4535
4915
  if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
4536
4916
  if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
4537
- return views(documents, paths);
4917
+ return views(documents, paths, deps.defaultLimits ?? DEFAULT_LIMITS);
4538
4918
  });
4539
4919
  pendingWrite = operation;
4540
4920
  return operation;
@@ -4562,7 +4942,9 @@ function registerClaudeGlobalSettingsRoute(ctx, deps = {}) {
4562
4942
  if (req.method !== "GET" && req.method !== "PATCH") return json(res, 405, { error: "method not allowed" });
4563
4943
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4564
4944
  try {
4565
- json(res, 200, req.method === "GET" ? await readGlobalSettings(deps) : await updateGlobalSettings(await requestJson(req), deps));
4945
+ const result = req.method === "GET" ? await readGlobalSettings(deps) : await updateGlobalSettings(await requestJson(req), deps);
4946
+ if (req.method === "PATCH") await deps.onUpdated?.();
4947
+ json(res, 200, result);
4566
4948
  } catch (error) {
4567
4949
  json(res, 400, { error: error instanceof Error ? error.message : "Invalid global settings request" });
4568
4950
  }
@@ -4692,12 +5074,21 @@ async function installManagedPresetCompatibility(logger, install = ensureManaged
4692
5074
  }
4693
5075
  async function apply(ctx, config) {
4694
5076
  await installManagedPresetCompatibility(ctx.logger);
5077
+ const defaultLimits = {
5078
+ idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
5079
+ maxProcesses: config.maxProcesses ?? 4
5080
+ };
4695
5081
  const supervisorConfig = {
4696
5082
  executablePath: "",
4697
5083
  defaultModel: config.model ?? "default",
4698
- idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
4699
- maxProcesses: config.maxProcesses ?? 4
5084
+ ...defaultLimits
4700
5085
  };
5086
+ const applyLimitOverrides = async () => {
5087
+ const overrides = await readSupervisorLimitOverrides();
5088
+ supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
5089
+ supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
5090
+ };
5091
+ await applyLimitOverrides();
4701
5092
  const sidecar = new ClaudeSidecarRepository();
4702
5093
  const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
4703
5094
  const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
@@ -4777,7 +5168,21 @@ async function apply(ctx, config) {
4777
5168
  ctx.on("agent/disposed", async ({ agent }) => {
4778
5169
  reviewComments.disposeSession(agent.id);
4779
5170
  await supervisor.disposeSession(agent.id);
4780
- await repositorySetup.cleanupSession(agent.id);
5171
+ });
5172
+ const injectWorkspaceRegistry = ctx.inject;
5173
+ injectWorkspaceRegistry(["workspaceRegistry"], (sweepCtx) => {
5174
+ const sweep = () => {
5175
+ try {
5176
+ const paths = sweepCtx.workspaceRegistry.list().map((workspace) => workspace.path);
5177
+ repositorySetup.cleanupOrphans(paths).catch(() => void 0);
5178
+ } catch {}
5179
+ };
5180
+ sweepCtx.effect(() => {
5181
+ sweep();
5182
+ const timer = setInterval(sweep, 6e4);
5183
+ timer.unref?.();
5184
+ return () => clearInterval(timer);
5185
+ }, "dsh-claude: worktree reconciliation");
4781
5186
  });
4782
5187
  ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
4783
5188
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
@@ -4786,13 +5191,19 @@ async function apply(ctx, config) {
4786
5191
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
4787
5192
  const desktopActions = webCtx.get("desktopActions");
4788
5193
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
4789
- registerClaudeGlobalSettingsRoute(webCtx);
5194
+ registerClaudeGlobalSettingsRoute(webCtx, {
5195
+ defaultLimits,
5196
+ onUpdated: applyLimitOverrides
5197
+ });
4790
5198
  registerRepositorySetupRoute(webCtx, repositorySetup);
4791
- registerRepositoryActionRoute(webCtx, new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd)), (sessionId) => {
5199
+ const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
5200
+ const cwdForClaudeSession = (sessionId) => {
4792
5201
  const agent = webCtx.agents.get(sessionId);
4793
5202
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
4794
5203
  return agent.session.header.cwd;
4795
- });
5204
+ };
5205
+ registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
5206
+ registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
4796
5207
  const ownsClaudeSession = (sessionId) => {
4797
5208
  const agent = webCtx.agents.get(sessionId);
4798
5209
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";