@norman-else/dsh-claude 0.1.22 → 0.1.24

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);
@@ -3603,6 +3648,53 @@ var RepositoryActionService = class {
3603
3648
  pushed: true
3604
3649
  };
3605
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
+ }
3606
3698
  const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
3607
3699
  if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
3608
3700
  const git = await this.#git();
@@ -3615,7 +3707,7 @@ var RepositoryActionService = class {
3615
3707
  "add",
3616
3708
  "--",
3617
3709
  ...paths
3618
- ], 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.");
3619
3711
  }
3620
3712
  await this.#rejectStagedWarp(git, before.root);
3621
3713
  const staged = await this.#run(git, [
@@ -3624,7 +3716,7 @@ var RepositoryActionService = class {
3624
3716
  "--quiet",
3625
3717
  "--exit-code",
3626
3718
  "--"
3627
- ], before.root, GIT_TIMEOUT_MS);
3719
+ ], before.root, GIT_TIMEOUT_MS$1);
3628
3720
  if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
3629
3721
  if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
3630
3722
  await this.#mustRun(git, [
@@ -3632,8 +3724,8 @@ var RepositoryActionService = class {
3632
3724
  "-m",
3633
3725
  message,
3634
3726
  "--"
3635
- ], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
3636
- 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();
3637
3729
  this.#invalidate(before.root);
3638
3730
  if (request.action === "commit") return {
3639
3731
  commit: oid,
@@ -3684,21 +3776,21 @@ var RepositoryActionService = class {
3684
3776
  "rev-parse",
3685
3777
  "--path-format=absolute",
3686
3778
  "--show-toplevel"
3687
- ], 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();
3688
3780
  const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
3689
3781
  this.#run(git, [
3690
3782
  "symbolic-ref",
3691
3783
  "--quiet",
3692
3784
  "--short",
3693
3785
  "HEAD"
3694
- ], root, GIT_TIMEOUT_MS),
3695
- 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),
3696
3788
  this.#run(git, [
3697
3789
  "status",
3698
3790
  "--porcelain=v1",
3699
3791
  "-z",
3700
3792
  "--untracked-files=all"
3701
- ], root, GIT_TIMEOUT_MS),
3793
+ ], root, GIT_TIMEOUT_MS$1),
3702
3794
  this.#run(git, [
3703
3795
  "diff",
3704
3796
  "--cached",
@@ -3708,7 +3800,7 @@ var RepositoryActionService = class {
3708
3800
  "--",
3709
3801
  ":(exclude)WARP.md",
3710
3802
  ":(exclude)**/WARP.md"
3711
- ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
3803
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
3712
3804
  this.#run(git, [
3713
3805
  "diff",
3714
3806
  "--no-ext-diff",
@@ -3717,7 +3809,7 @@ var RepositoryActionService = class {
3717
3809
  "--",
3718
3810
  ":(exclude)WARP.md",
3719
3811
  ":(exclude)**/WARP.md"
3720
- ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
3812
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
3721
3813
  ]);
3722
3814
  if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
3723
3815
  if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
@@ -3737,7 +3829,7 @@ var RepositoryActionService = class {
3737
3829
  "--abbrev-ref",
3738
3830
  "--symbolic-full-name",
3739
3831
  "@{upstream}"
3740
- ], root, GIT_TIMEOUT_MS);
3832
+ ], root, GIT_TIMEOUT_MS$1);
3741
3833
  const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
3742
3834
  const logResult = await this.#run(git, [
3743
3835
  "log",
@@ -3746,7 +3838,7 @@ var RepositoryActionService = class {
3746
3838
  String(21),
3747
3839
  upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
3748
3840
  "--"
3749
- ], root, GIT_TIMEOUT_MS);
3841
+ ], root, GIT_TIMEOUT_MS$1);
3750
3842
  const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
3751
3843
  const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
3752
3844
  const tab = line.indexOf(" ");
@@ -3778,7 +3870,7 @@ var RepositoryActionService = class {
3778
3870
  "--cached",
3779
3871
  "--name-only",
3780
3872
  "--"
3781
- ], cwd, GIT_TIMEOUT_MS);
3873
+ ], cwd, GIT_TIMEOUT_MS$1);
3782
3874
  if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
3783
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.");
3784
3876
  }
@@ -3788,7 +3880,7 @@ var RepositoryActionService = class {
3788
3880
  "--abbrev-ref",
3789
3881
  "--symbolic-full-name",
3790
3882
  "@{upstream}"
3791
- ], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
3883
+ ], cwd, GIT_TIMEOUT_MS$1)).exitCode === 0 ? ["push"] : [
3792
3884
  "push",
3793
3885
  "--set-upstream",
3794
3886
  "origin",
@@ -3811,14 +3903,14 @@ var RepositoryActionService = class {
3811
3903
  if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
3812
3904
  return result;
3813
3905
  }
3814
- #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
3815
- return collect(this.#runtime.spawn({
3906
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
3907
+ return collect$1(this.#runtime.spawn({
3816
3908
  argv: [executable, ...args],
3817
3909
  cwd,
3818
3910
  stdio: {
3819
3911
  stdin: "ignore",
3820
3912
  stdout: { maxBytes },
3821
- stderr: { maxBytes: MAX_OUTPUT_BYTES }
3913
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
3822
3914
  },
3823
3915
  graceMs: 1e3,
3824
3916
  signal: AbortSignal.timeout(timeoutMs),
@@ -3829,7 +3921,7 @@ var RepositoryActionService = class {
3829
3921
  //#endregion
3830
3922
  //#region src/repository-setup-routes.ts
3831
3923
  const MAX_BODY_BYTES$2 = 16384;
3832
- function record$2(value) {
3924
+ function record$3(value) {
3833
3925
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3834
3926
  }
3835
3927
  async function readJson$2(req) {
@@ -3841,7 +3933,7 @@ async function readJson$2(req) {
3841
3933
  if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3842
3934
  chunks.push(buffer);
3843
3935
  }
3844
- const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3936
+ const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3845
3937
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3846
3938
  return value;
3847
3939
  }
@@ -3935,15 +4027,16 @@ function registerRepositorySetupRoute(ctx, service) {
3935
4027
  //#endregion
3936
4028
  //#region src/repository-action-routes.ts
3937
4029
  const MAX_BODY_BYTES$1 = 16384;
3938
- const MAX_SESSION_ID_CHARS$1 = 1024;
4030
+ const MAX_SESSION_ID_CHARS$2 = 1024;
3939
4031
  const ACTIONS = /* @__PURE__ */ new Set([
3940
4032
  "commit",
3941
4033
  "commit-push",
3942
4034
  "push",
3943
4035
  "create-pr",
3944
- "merge-pr"
4036
+ "merge-pr",
4037
+ "update-branch"
3945
4038
  ]);
3946
- function record$1(value) {
4039
+ function record$2(value) {
3947
4040
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3948
4041
  }
3949
4042
  async function readJson$1(req) {
@@ -3955,13 +4048,13 @@ async function readJson$1(req) {
3955
4048
  if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
3956
4049
  chunks.push(buffer);
3957
4050
  }
3958
- const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4051
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3959
4052
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
3960
4053
  return value;
3961
4054
  }
3962
- function sessionId(url) {
4055
+ function sessionId$1(url) {
3963
4056
  const value = url.searchParams.get("sessionId");
3964
- 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.");
3965
4058
  return value;
3966
4059
  }
3967
4060
  function string(input, key) {
@@ -3981,7 +4074,7 @@ function actionRequest(input) {
3981
4074
  return {
3982
4075
  action,
3983
4076
  fingerprint: string(input, "fingerprint"),
3984
- message: action === "push" || action === "merge-pr" ? optionalString(input, "message") ?? "" : string(input, "message"),
4077
+ message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string(input, "message"),
3985
4078
  includeUnstaged: input.includeUnstaged,
3986
4079
  ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
3987
4080
  ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
@@ -4002,7 +4095,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4002
4095
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4003
4096
  const url = new URL(req.url ?? "/", "http://localhost");
4004
4097
  try {
4005
- const cwd = cwdForSession(sessionId(url));
4098
+ const cwd = cwdForSession(sessionId$1(url));
4006
4099
  if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
4007
4100
  if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
4008
4101
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
@@ -4034,6 +4127,218 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4034
4127
  }), "dsh-claude: repository action route");
4035
4128
  }
4036
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
4037
4342
  //#region src/review-comment-routes.ts
4038
4343
  const MAX_BODY_BYTES = 16384;
4039
4344
  const MAX_SESSION_ID_CHARS = 1024;
@@ -4374,6 +4679,12 @@ const BUILTIN_OUTPUT_STYLES = [
4374
4679
  const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
4375
4680
  const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
4376
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
+ };
4377
4688
  function pathsFor(deps) {
4378
4689
  const root = join(homedir(), ".claude");
4379
4690
  return {
@@ -4479,19 +4790,45 @@ const WORKTREE_BRANCH_PREFIX = {
4479
4790
  else document.worktreeBranchPrefix = value;
4480
4791
  }
4481
4792
  };
4482
- 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
+ ];
4483
4820
  const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
4484
4821
  let pendingWrite = Promise.resolve();
4485
4822
  function documentFor(descriptor, documents) {
4486
4823
  return documents[descriptor.document];
4487
4824
  }
4488
- async function views(documents, paths) {
4825
+ async function views(documents, paths, defaults) {
4489
4826
  return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
4490
4827
  const document = documentFor(descriptor, documents);
4491
4828
  if (descriptor.kind === "text") return {
4492
4829
  key: descriptor.key,
4493
4830
  kind: descriptor.kind,
4494
- value: descriptor.read(document),
4831
+ value: descriptor.read(document, defaults),
4495
4832
  maxLength: descriptor.maxLength,
4496
4833
  effect: descriptor.effect
4497
4834
  };
@@ -4520,7 +4857,22 @@ async function readDocuments(paths) {
4520
4857
  }
4521
4858
  async function readGlobalSettings(deps = {}) {
4522
4859
  const paths = pathsFor(deps);
4523
- 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
+ };
4524
4876
  }
4525
4877
  async function readWorktreeBranchPrefix(deps = {}) {
4526
4878
  const paths = pathsFor(deps);
@@ -4562,7 +4914,7 @@ function updateGlobalSettings(changes, deps = {}) {
4562
4914
  }
4563
4915
  if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
4564
4916
  if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
4565
- return views(documents, paths);
4917
+ return views(documents, paths, deps.defaultLimits ?? DEFAULT_LIMITS);
4566
4918
  });
4567
4919
  pendingWrite = operation;
4568
4920
  return operation;
@@ -4590,7 +4942,9 @@ function registerClaudeGlobalSettingsRoute(ctx, deps = {}) {
4590
4942
  if (req.method !== "GET" && req.method !== "PATCH") return json(res, 405, { error: "method not allowed" });
4591
4943
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4592
4944
  try {
4593
- 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);
4594
4948
  } catch (error) {
4595
4949
  json(res, 400, { error: error instanceof Error ? error.message : "Invalid global settings request" });
4596
4950
  }
@@ -4720,12 +5074,21 @@ async function installManagedPresetCompatibility(logger, install = ensureManaged
4720
5074
  }
4721
5075
  async function apply(ctx, config) {
4722
5076
  await installManagedPresetCompatibility(ctx.logger);
5077
+ const defaultLimits = {
5078
+ idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
5079
+ maxProcesses: config.maxProcesses ?? 4
5080
+ };
4723
5081
  const supervisorConfig = {
4724
5082
  executablePath: "",
4725
5083
  defaultModel: config.model ?? "default",
4726
- idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
4727
- maxProcesses: config.maxProcesses ?? 4
5084
+ ...defaultLimits
5085
+ };
5086
+ const applyLimitOverrides = async () => {
5087
+ const overrides = await readSupervisorLimitOverrides();
5088
+ supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
5089
+ supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
4728
5090
  };
5091
+ await applyLimitOverrides();
4729
5092
  const sidecar = new ClaudeSidecarRepository();
4730
5093
  const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
4731
5094
  const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
@@ -4805,7 +5168,21 @@ async function apply(ctx, config) {
4805
5168
  ctx.on("agent/disposed", async ({ agent }) => {
4806
5169
  reviewComments.disposeSession(agent.id);
4807
5170
  await supervisor.disposeSession(agent.id);
4808
- 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");
4809
5186
  });
4810
5187
  ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
4811
5188
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
@@ -4814,13 +5191,19 @@ async function apply(ctx, config) {
4814
5191
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
4815
5192
  const desktopActions = webCtx.get("desktopActions");
4816
5193
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
4817
- registerClaudeGlobalSettingsRoute(webCtx);
5194
+ registerClaudeGlobalSettingsRoute(webCtx, {
5195
+ defaultLimits,
5196
+ onUpdated: applyLimitOverrides
5197
+ });
4818
5198
  registerRepositorySetupRoute(webCtx, repositorySetup);
4819
- 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) => {
4820
5201
  const agent = webCtx.agents.get(sessionId);
4821
5202
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
4822
5203
  return agent.session.header.cwd;
4823
- });
5204
+ };
5205
+ registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
5206
+ registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
4824
5207
  const ownsClaudeSession = (sessionId) => {
4825
5208
  const agent = webCtx.agents.get(sessionId);
4826
5209
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";