@getpaseo/server 0.1.88 → 0.1.89

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.
Files changed (60) hide show
  1. package/dist/server/server/agent/agent-manager.js +4 -1
  2. package/dist/server/server/agent/agent-storage.d.ts +22 -22
  3. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  4. package/dist/server/server/agent/create-agent/create.js +16 -5
  5. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
  6. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
  7. package/dist/server/server/agent/mcp-server.d.ts +1 -0
  8. package/dist/server/server/agent/mcp-server.js +113 -70
  9. package/dist/server/server/agent/providers/pi/agent.js +13 -0
  10. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
  11. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
  12. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
  13. package/dist/server/server/bootstrap.d.ts +7 -2
  14. package/dist/server/server/bootstrap.js +152 -115
  15. package/dist/server/server/config.js +41 -0
  16. package/dist/server/server/loop-service.d.ts +22 -22
  17. package/dist/server/server/package-version.d.ts +2 -2
  18. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
  19. package/dist/server/server/paseo-worktree-archive-service.js +28 -9
  20. package/dist/server/server/persisted-config.d.ts +84 -28
  21. package/dist/server/server/persisted-config.js +17 -0
  22. package/dist/server/server/pid-lock.d.ts +2 -2
  23. package/dist/server/server/script-health-monitor.d.ts +4 -4
  24. package/dist/server/server/script-health-monitor.js +6 -6
  25. package/dist/server/server/script-proxy.d.ts +2 -39
  26. package/dist/server/server/script-proxy.js +1 -244
  27. package/dist/server/server/script-route-branch-handler.d.ts +2 -2
  28. package/dist/server/server/script-route-branch-handler.js +3 -37
  29. package/dist/server/server/script-status-projection.d.ts +6 -4
  30. package/dist/server/server/script-status-projection.js +85 -37
  31. package/dist/server/server/service-proxy.d.ts +237 -0
  32. package/dist/server/server/service-proxy.js +714 -0
  33. package/dist/server/server/session.d.ts +7 -3
  34. package/dist/server/server/session.js +22 -10
  35. package/dist/server/server/websocket-server.d.ts +7 -4
  36. package/dist/server/server/websocket-server.js +9 -4
  37. package/dist/server/server/workspace-directory.js +4 -0
  38. package/dist/server/server/workspace-git-service.d.ts +3 -0
  39. package/dist/server/server/workspace-git-service.js +53 -12
  40. package/dist/server/server/workspace-registry.d.ts +2 -2
  41. package/dist/server/server/workspace-service-env.d.ts +1 -0
  42. package/dist/server/server/workspace-service-env.js +23 -18
  43. package/dist/server/server/worktree/commands.d.ts +2 -0
  44. package/dist/server/server/worktree/commands.js +4 -1
  45. package/dist/server/server/worktree-bootstrap.d.ts +4 -3
  46. package/dist/server/server/worktree-bootstrap.js +14 -13
  47. package/dist/server/server/worktree-core.d.ts +1 -0
  48. package/dist/server/server/worktree-core.js +2 -0
  49. package/dist/server/server/worktree-session.d.ts +6 -2
  50. package/dist/server/server/worktree-session.js +3 -0
  51. package/dist/server/services/github-service.d.ts +1 -0
  52. package/dist/server/services/github-service.js +7 -1
  53. package/dist/server/utils/checkout-git.d.ts +6 -2
  54. package/dist/server/utils/checkout-git.js +17 -7
  55. package/dist/server/utils/worktree.d.ts +17 -12
  56. package/dist/server/utils/worktree.js +39 -22
  57. package/dist/src/server/persisted-config.js +17 -0
  58. package/package.json +5 -5
  59. package/dist/server/utils/script-hostname.d.ts +0 -8
  60. package/dist/server/utils/script-hostname.js +0 -14
@@ -17,6 +17,7 @@ export async function createPaseoWorktreeCommand(dependencies, input) {
17
17
  ...input,
18
18
  runSetup: false,
19
19
  paseoHome: input.paseoHome ?? dependencies.paseoHome,
20
+ worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
20
21
  });
21
22
  return { ok: true, createdWorktree };
22
23
  }
@@ -32,6 +33,7 @@ export async function archivePaseoWorktreeCommand(dependencies, input) {
32
33
  const resolvedTarget = await resolveArchiveTarget(dependencies, input);
33
34
  const ownership = await isPaseoOwnedWorktreeCwd(resolvedTarget.targetPath, {
34
35
  paseoHome: dependencies.paseoHome,
36
+ worktreesRoot: dependencies.worktreesRoot,
35
37
  });
36
38
  if (!ownership.allowed) {
37
39
  return {
@@ -46,6 +48,7 @@ export async function archivePaseoWorktreeCommand(dependencies, input) {
46
48
  targetPath: resolvedTarget.targetPath,
47
49
  repoRoot,
48
50
  worktreesRoot: ownership.worktreeRoot,
51
+ worktreesBaseRoot: dependencies.worktreesRoot,
49
52
  requestId: input.requestId,
50
53
  });
51
54
  return {
@@ -78,7 +81,7 @@ async function resolveArchiveTarget(dependencies, input) {
78
81
  throw new Error("worktreePath, worktreeSlug, or repoRoot+branchName is required");
79
82
  }
80
83
  async function resolveWorktreeSlugPath(dependencies, repoRoot, worktreeSlug) {
81
- const worktreesRoot = await getPaseoWorktreesRoot(repoRoot, dependencies.paseoHome);
84
+ const worktreesRoot = await getPaseoWorktreesRoot(repoRoot, dependencies.paseoHome, dependencies.worktreesRoot);
82
85
  return join(worktreesRoot, worktreeSlug);
83
86
  }
84
87
  //# sourceMappingURL=commands.js.map
@@ -1,7 +1,7 @@
1
1
  import type { Logger } from "pino";
2
2
  import type { TerminalManager } from "../terminal/terminal-manager.js";
3
3
  import { runWorktreeSetupCommands, type WorktreeConfig, type WorktreeSetupCommandResult } from "../utils/worktree.js";
4
- import { type ScriptRouteStore } from "./script-proxy.js";
4
+ import { type ServiceProxySubsystem } from "./service-proxy.js";
5
5
  import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
6
6
  import type { AgentTimelineItem, ToolCallDetail } from "./agent/agent-sdk-types.js";
7
7
  export interface WorktreeBootstrapTerminalResult {
@@ -58,7 +58,8 @@ interface SpawnWorkspaceScriptOptions {
58
58
  scriptName: string;
59
59
  daemonPort?: number | null;
60
60
  daemonListenHost?: string | null;
61
- routeStore: ScriptRouteStore;
61
+ serviceProxyPublicBaseUrl?: string | null;
62
+ serviceProxy: ServiceProxySubsystem;
62
63
  runtimeStore: WorkspaceScriptRuntimeStore;
63
64
  terminalManager: TerminalManager;
64
65
  logger?: Logger;
@@ -67,7 +68,7 @@ interface SpawnWorkspaceScriptOptions {
67
68
  export declare function spawnWorkspaceScript(options: SpawnWorkspaceScriptOptions): Promise<WorktreeScriptResult>;
68
69
  export declare function teardownWorktreeScripts(options: {
69
70
  hostnames: string[];
70
- routeStore: ScriptRouteStore;
71
+ serviceProxy: Pick<ServiceProxySubsystem, "removeServiceRoutesByHostnames">;
71
72
  logger: Logger;
72
73
  }): void;
73
74
  export {};
@@ -1,7 +1,6 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
- import { buildScriptHostname } from "../utils/script-hostname.js";
3
2
  import { getScriptConfigs, getWorktreeTerminalSpecs, isServiceScript, paseoConfigParseError, processCarriageReturns, readPaseoConfig, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
4
- import { findFreePort } from "./script-proxy.js";
3
+ import { findFreePort } from "./service-proxy.js";
5
4
  import { assertNoServiceEnvNameCollisions, buildWorkspaceServiceEnv, } from "./workspace-service-env.js";
6
5
  import { ensureWorkspaceServicePortPlan, requirePlannedWorkspaceServicePort, refreshWorkspaceServicePort, } from "./workspace-service-port-registry.js";
7
6
  const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
@@ -501,8 +500,7 @@ export async function runAsyncWorktreeBootstrap(options) {
501
500
  await runWorktreeTerminalBootstrap(options, runtimeEnv);
502
501
  }
503
502
  async function setupServiceScriptRoute(params) {
504
- const { scriptConfigs, config, scriptName, projectSlug, branchName, workspaceId, daemonPort, daemonListenHost, existingRuntimeEntry, routeStore, } = params;
505
- const hostname = buildScriptHostname({ projectSlug, branchName, scriptName });
503
+ const { scriptConfigs, config, scriptName, projectSlug, branchName, workspaceId, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, existingRuntimeEntry, serviceProxy, } = params;
506
504
  const serviceDeclarations = [];
507
505
  for (const [configuredScriptName, scriptConfig] of scriptConfigs) {
508
506
  if (isServiceScript(scriptConfig)) {
@@ -538,16 +536,18 @@ async function setupServiceScriptRoute(params) {
538
536
  branchName,
539
537
  daemonPort,
540
538
  daemonListenHost,
539
+ serviceProxyPublicBaseUrl,
541
540
  peers,
542
541
  });
543
- routeStore.registerRoute({
544
- hostname,
542
+ const registeredRoute = serviceProxy.registerWorkspaceService({
545
543
  port,
546
544
  workspaceId,
547
545
  projectSlug,
546
+ branchName,
548
547
  scriptName,
548
+ publicBaseUrl: serviceProxyPublicBaseUrl ?? null,
549
549
  });
550
- return { hostname, port, env };
550
+ return { hostname: registeredRoute.hostname, port, env };
551
551
  }
552
552
  async function acquireWorkspaceScriptTerminal(params) {
553
553
  const { serviceScript, existingRuntimeEntry, terminalManager, repoRoot, scriptName, env } = params;
@@ -565,7 +565,7 @@ async function acquireWorkspaceScriptTerminal(params) {
565
565
  return { terminal, reusableTerminal };
566
566
  }
567
567
  export async function spawnWorkspaceScript(options) {
568
- const { repoRoot, workspaceId, projectSlug, branchName, scriptName, daemonPort, daemonListenHost, routeStore, runtimeStore, terminalManager, logger, onLifecycleChanged, } = options;
568
+ const { repoRoot, workspaceId, projectSlug, branchName, scriptName, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, serviceProxy, runtimeStore, terminalManager, logger, onLifecycleChanged, } = options;
569
569
  const configResult = readPaseoConfig(repoRoot);
570
570
  if (!configResult.ok) {
571
571
  throw paseoConfigParseError(configResult);
@@ -598,8 +598,9 @@ export async function spawnWorkspaceScript(options) {
598
598
  workspaceId,
599
599
  daemonPort,
600
600
  daemonListenHost,
601
+ serviceProxyPublicBaseUrl,
601
602
  existingRuntimeEntry,
602
- routeStore,
603
+ serviceProxy,
603
604
  });
604
605
  hostname = serviceSetup.hostname;
605
606
  port = serviceSetup.port;
@@ -631,7 +632,7 @@ export async function spawnWorkspaceScript(options) {
631
632
  disposeLifecycleListeners?.();
632
633
  disposeLifecycleListeners = null;
633
634
  if (input.removeRoute && hostname) {
634
- routeStore.removeRouteForWorkspaceScript({ workspaceId, scriptName });
635
+ serviceProxy.removeWorkspaceService({ workspaceId, scriptName });
635
636
  }
636
637
  runtimeStore.set({
637
638
  workspaceId,
@@ -689,7 +690,7 @@ export async function spawnWorkspaceScript(options) {
689
690
  catch (error) {
690
691
  disposeLifecycleListeners?.();
691
692
  if (routeRegistered && hostname) {
692
- routeStore.removeRoute(hostname);
693
+ serviceProxy.removeServiceRoutesByHostnames([hostname]);
693
694
  }
694
695
  if (runtimeRegistered) {
695
696
  runtimeStore.remove({ workspaceId, scriptName });
@@ -707,9 +708,9 @@ export async function spawnWorkspaceScript(options) {
707
708
  }
708
709
  }
709
710
  export function teardownWorktreeScripts(options) {
710
- const { hostnames, routeStore, logger } = options;
711
+ const { hostnames, serviceProxy, logger } = options;
712
+ serviceProxy.removeServiceRoutesByHostnames(hostnames);
711
713
  for (const hostname of hostnames) {
712
- routeStore.removeRoute(hostname);
713
714
  logger.info({ hostname }, "Removed script proxy route");
714
715
  }
715
716
  }
@@ -11,6 +11,7 @@ export interface CreateWorktreeCoreInput {
11
11
  githubPrNumber?: number;
12
12
  firstAgentContext?: FirstAgentContext;
13
13
  paseoHome?: string;
14
+ worktreesRoot?: string;
14
15
  runSetup?: boolean;
15
16
  }
16
17
  export interface CreateWorktreeCoreDeps {
@@ -54,6 +54,7 @@ export async function createWorktreeCore(input, deps) {
54
54
  slug: normalizedSlug,
55
55
  repoRoot,
56
56
  paseoHome: input.paseoHome,
57
+ worktreesRoot: input.worktreesRoot,
57
58
  });
58
59
  if (existingWorktree) {
59
60
  return { worktree: existingWorktree, intent, repoRoot, created: false };
@@ -65,6 +66,7 @@ export async function createWorktreeCore(input, deps) {
65
66
  source: intent,
66
67
  runSetup: input.runSetup ?? true,
67
68
  paseoHome: input.paseoHome,
69
+ worktreesRoot: input.worktreesRoot,
68
70
  }),
69
71
  intent,
70
72
  repoRoot,
@@ -5,7 +5,7 @@ import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
5
5
  import type { WorkspaceGitService } from "./workspace-git-service.js";
6
6
  import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
7
7
  import type { TerminalManager } from "../terminal/terminal-manager.js";
8
- import type { ScriptRouteStore } from "./script-proxy.js";
8
+ import type { ServiceProxySubsystem } from "./service-proxy.js";
9
9
  import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
10
10
  import type { GitHubService } from "../services/github-service.js";
11
11
  import type { CheckoutExistingBranchResult } from "../utils/checkout-git.js";
@@ -31,6 +31,7 @@ type AgentWorktreeSetupTimelineWriter = (input: {
31
31
  }) => Promise<boolean>;
32
32
  interface BuildAgentSessionConfigDependencies {
33
33
  paseoHome?: string;
34
+ worktreesRoot?: string;
34
35
  sessionLogger: Logger;
35
36
  workspaceGitService?: WorkspaceGitService;
36
37
  createPaseoWorktree: (input: CreatePaseoWorktreeInput, options?: {
@@ -47,6 +48,7 @@ interface BuildAgentSessionConfigDependencies {
47
48
  }
48
49
  interface CreatePaseoWorktreeInBackgroundDependencies {
49
50
  paseoHome?: string;
51
+ worktreesRoot?: string;
50
52
  emitWorkspaceUpdateForCwd: (cwd: string, options?: {
51
53
  dedupeGitState?: boolean;
52
54
  }) => Promise<void>;
@@ -55,10 +57,11 @@ interface CreatePaseoWorktreeInBackgroundDependencies {
55
57
  sessionLogger: Logger;
56
58
  terminalManager: TerminalManager | null;
57
59
  archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
58
- scriptRouteStore: ScriptRouteStore | null;
60
+ serviceProxy: ServiceProxySubsystem | null;
59
61
  scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
60
62
  getDaemonTcpPort: (() => number | null) | null;
61
63
  getDaemonTcpHost: (() => string | null) | null;
64
+ serviceProxyPublicBaseUrl?: string | null;
62
65
  onScriptsChanged: ((workspaceId: string, workspaceDirectory: string) => void) | null;
63
66
  }
64
67
  interface CreatePaseoWorktreeWorkflowDependencies extends CreatePaseoWorktreeInBackgroundDependencies {
@@ -100,6 +103,7 @@ interface HandleWorkspaceSetupStatusRequestDependencies {
100
103
  }
101
104
  interface HandleCreatePaseoWorktreeRequestDependencies {
102
105
  paseoHome?: string;
106
+ worktreesRoot?: string;
103
107
  describeWorkspaceRecord: (result: CreatePaseoWorktreeResult) => Promise<WorkspaceDescriptorPayload>;
104
108
  emit: EmitSessionMessage;
105
109
  sessionLogger: Logger;
@@ -41,6 +41,7 @@ export async function buildAgentSessionConfig(dependencies, config, gitOptions,
41
41
  firstAgentContext,
42
42
  runSetup: false,
43
43
  paseoHome: dependencies.paseoHome,
44
+ worktreesRoot: dependencies.worktreesRoot,
44
45
  }, {
45
46
  resolveDefaultBranch: normalized.baseBranch
46
47
  ? async () => normalized.baseBranch
@@ -241,6 +242,7 @@ export async function handleCreatePaseoWorktreeRequest(dependencies, request) {
241
242
  try {
242
243
  const commandResult = await createPaseoWorktreeCommand({
243
244
  paseoHome: dependencies.paseoHome,
245
+ worktreesRoot: dependencies.worktreesRoot,
244
246
  createPaseoWorktreeWorkflow: dependencies.createPaseoWorktreeWorkflow,
245
247
  }, {
246
248
  cwd: request.cwd,
@@ -304,6 +306,7 @@ export async function createPaseoWorktreeWorkflow(dependencies, input, options)
304
306
  ...input,
305
307
  runSetup: false,
306
308
  paseoHome: input.paseoHome ?? dependencies.paseoHome,
309
+ worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
307
310
  }, options?.resolveDefaultBranch
308
311
  ? { resolveDefaultBranch: options.resolveDefaultBranch }
309
312
  : undefined);
@@ -228,6 +228,7 @@ export interface GitHubService {
228
228
  retainCurrentPullRequestStatusPoll?(options: {
229
229
  cwd: string;
230
230
  headRef: string;
231
+ headRepositoryOwner?: string;
231
232
  onStatus?: (status: GitHubCurrentPullRequestStatus | null) => void;
232
233
  onError?: (error: unknown) => void;
233
234
  }): {
@@ -425,7 +425,10 @@ export function createGitHubService(options = {}) {
425
425
  return buildCacheKey({
426
426
  cwd: target.cwd,
427
427
  method: "getCurrentPullRequestStatus",
428
- args: { headRef: target.headRef },
428
+ args: {
429
+ headRef: target.headRef,
430
+ headRepositoryOwner: target.headRepositoryOwner,
431
+ },
429
432
  });
430
433
  }
431
434
  function updatePollTargetAfterSuccess(update) {
@@ -465,6 +468,7 @@ export function createGitHubService(options = {}) {
465
468
  await api.getCurrentPullRequestStatus({
466
469
  cwd: target.cwd,
467
470
  headRef: target.headRef,
471
+ headRepositoryOwner: target.headRepositoryOwner,
468
472
  reason: "self-heal-github",
469
473
  });
470
474
  }
@@ -601,6 +605,7 @@ export function createGitHubService(options = {}) {
601
605
  updatePollTargetAfterSuccess({
602
606
  cwd: input.cwd,
603
607
  headRef: input.headRef,
608
+ headRepositoryOwner: input.headRepositoryOwner,
604
609
  status,
605
610
  notify: input.reason === "self-heal-github",
606
611
  });
@@ -795,6 +800,7 @@ export function createGitHubService(options = {}) {
795
800
  target = {
796
801
  cwd: input.cwd,
797
802
  headRef: input.headRef,
803
+ headRepositoryOwner: input.headRepositoryOwner,
798
804
  retainCount: 0,
799
805
  timer: null,
800
806
  latestStatus: null,
@@ -130,6 +130,7 @@ export interface MergeFromBaseOptions {
130
130
  }
131
131
  export interface CheckoutContext {
132
132
  paseoHome?: string;
133
+ worktreesRoot?: string;
133
134
  logger?: Pick<Logger, "trace">;
134
135
  facts?: CheckoutSnapshotFacts | null;
135
136
  }
@@ -159,8 +160,11 @@ export interface GitWorktreeEntry {
159
160
  branchRef?: string;
160
161
  isBare?: boolean;
161
162
  }
162
- /** Check whether a path contains a `.paseo/worktrees/` segment (both `/` and `\`). */
163
- export declare function isPaseoWorktreePath(p: string): boolean;
163
+ /** Check whether a path is under Paseo's worktree root. */
164
+ export declare function isPaseoWorktreePath(p: string, options?: {
165
+ paseoHome?: string;
166
+ worktreesRoot?: string;
167
+ }): boolean;
164
168
  /** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
165
169
  export declare function isDescendantPath(child: string, parent: string): boolean;
166
170
  export declare function parseWorktreeList(output: string): GitWorktreeEntry[];
@@ -7,7 +7,7 @@ import { parseGitHubRepoFromRemote } from "../server/workspace-git-metadata.js";
7
7
  import { GitHubAuthenticationError, GitHubCliMissingError, GitHubCommandError, createGitHubService, resolveGitHubRepo, } from "../services/github-service.js";
8
8
  import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
9
9
  import { runGitCommand } from "./run-git-command.js";
10
- import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
10
+ import { isPaseoOwnedWorktreeCwd, resolvePaseoWorktreesBaseRoot } from "./worktree.js";
11
11
  import { readPaseoWorktreeMetadata } from "./worktree-metadata.js";
12
12
  const READ_ONLY_GIT_ENV = {
13
13
  GIT_OPTIONAL_LOCKS: "0",
@@ -570,7 +570,7 @@ export async function getMainRepoRoot(cwd) {
570
570
  });
571
571
  return getMainRepoRootFromCommonDir(cwd, resolveGitRevParsePath(cwd, commonDirOut));
572
572
  }
573
- async function getMainRepoRootFromCommonDir(cwd, commonDir) {
573
+ async function getMainRepoRootFromCommonDir(cwd, commonDir, context) {
574
574
  if (!commonDir) {
575
575
  throw new Error("Not in a git repository");
576
576
  }
@@ -583,13 +583,20 @@ async function getMainRepoRootFromCommonDir(cwd, commonDir) {
583
583
  envOverlay: READ_ONLY_GIT_ENV,
584
584
  });
585
585
  const worktrees = parseWorktreeList(worktreeOut);
586
- const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare && !isPaseoWorktreePath(wt.path));
586
+ const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare &&
587
+ !isPaseoWorktreePath(wt.path, {
588
+ paseoHome: context?.paseoHome,
589
+ worktreesRoot: context?.worktreesRoot,
590
+ }));
587
591
  const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized));
588
592
  const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
589
593
  return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
590
594
  }
591
- /** Check whether a path contains a `.paseo/worktrees/` segment (both `/` and `\`). */
592
- export function isPaseoWorktreePath(p) {
595
+ /** Check whether a path is under Paseo's worktree root. */
596
+ export function isPaseoWorktreePath(p, options) {
597
+ if (options?.worktreesRoot || options?.paseoHome) {
598
+ return isDescendantPath(p, resolvePaseoWorktreesBaseRoot(options));
599
+ }
593
600
  return /[/\\]\.paseo[/\\]worktrees[/\\]/.test(p);
594
601
  }
595
602
  /** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
@@ -669,7 +676,10 @@ async function getPaseoWorktreeForCwd(cwd, context, knownWorktreeRoot) {
669
676
  if (!/[\\/]worktrees[\\/]/.test(cwd)) {
670
677
  return { isPaseoOwnedWorktree: false };
671
678
  }
672
- const ownership = await isPaseoOwnedWorktreeCwd(cwd, { paseoHome: context?.paseoHome });
679
+ const ownership = await isPaseoOwnedWorktreeCwd(cwd, {
680
+ paseoHome: context?.paseoHome,
681
+ worktreesRoot: context?.worktreesRoot,
682
+ });
673
683
  if (!ownership.allowed) {
674
684
  return { isPaseoOwnedWorktree: false };
675
685
  }
@@ -1076,7 +1086,7 @@ export async function getCheckoutSnapshotFacts(cwd, context) {
1076
1086
  ? readPaseoWorktreeBaseRef(inspected.paseoWorktree.worktreeRoot)
1077
1087
  : null;
1078
1088
  const resolvedBaseRef = storedBaseRef ?? (await resolveBaseRef(cwd));
1079
- const mainRepoRoot = await getMainRepoRootFromCommonDir(cwd, inspected.gitCommonDir).catch(() => null);
1089
+ const mainRepoRoot = await getMainRepoRootFromCommonDir(cwd, inspected.gitCommonDir, context).catch(() => null);
1080
1090
  let comparisonBaseRef = null;
1081
1091
  if (resolvedBaseRef &&
1082
1092
  inspected.currentBranch &&
@@ -83,6 +83,10 @@ export interface PaseoWorktreeOwnership {
83
83
  worktreeRoot?: string;
84
84
  worktreePath?: string;
85
85
  }
86
+ export interface WorktreeRootOptions {
87
+ paseoHome?: string;
88
+ worktreesRoot?: string;
89
+ }
86
90
  export type WorktreeSource = {
87
91
  kind: "branch-off";
88
92
  baseBranch: string;
@@ -104,11 +108,13 @@ export interface CreateWorktreeOptions {
104
108
  source: WorktreeSource;
105
109
  runSetup: boolean;
106
110
  paseoHome?: string;
111
+ worktreesRoot?: string;
107
112
  }
108
113
  interface ResolveExistingWorktreeForSlugOptions {
109
114
  slug: string;
110
115
  repoRoot: string;
111
116
  paseoHome?: string;
117
+ worktreesRoot?: string;
112
118
  }
113
119
  export declare class BranchAlreadyCheckedOutError extends Error {
114
120
  readonly branchName: string;
@@ -164,32 +170,31 @@ export declare function runWorktreeTeardownCommands(options: {
164
170
  */
165
171
  export declare function getGitCommonDir(cwd: string): Promise<string>;
166
172
  export declare function deriveWorktreeProjectHash(cwd: string): Promise<string>;
167
- export declare function getPaseoWorktreesRoot(cwd: string, paseoHome?: string): Promise<string>;
168
- export declare function computeWorktreePath(cwd: string, slug: string, paseoHome?: string): Promise<string>;
169
- export declare function isPaseoOwnedWorktreeCwd(cwd: string, options?: {
170
- paseoHome?: string;
171
- }): Promise<PaseoWorktreeOwnership>;
172
- export declare function listPaseoWorktrees({ cwd, paseoHome, }: {
173
+ export declare function resolvePaseoWorktreesBaseRoot(options?: WorktreeRootOptions): string;
174
+ export declare function getPaseoWorktreesRoot(cwd: string, paseoHome?: string, worktreesRoot?: string): Promise<string>;
175
+ export declare function computeWorktreePath(cwd: string, slug: string, paseoHome?: string, worktreesRoot?: string): Promise<string>;
176
+ export declare function isPaseoOwnedWorktreeCwd(cwd: string, options?: WorktreeRootOptions): Promise<PaseoWorktreeOwnership>;
177
+ export declare function listPaseoWorktrees({ cwd, paseoHome, worktreesRoot, }: {
173
178
  cwd: string;
174
179
  paseoHome?: string;
180
+ worktreesRoot?: string;
175
181
  }): Promise<PaseoWorktreeInfo[]>;
176
- export declare function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, }: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null>;
177
- export declare function resolvePaseoWorktreeRootForCwd(cwd: string, options?: {
178
- paseoHome?: string;
179
- }): Promise<{
182
+ export declare function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, worktreesRoot, }: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null>;
183
+ export declare function resolvePaseoWorktreeRootForCwd(cwd: string, options?: WorktreeRootOptions): Promise<{
180
184
  repoRoot: string;
181
185
  worktreeRoot: string;
182
186
  worktreePath: string;
183
187
  } | null>;
184
- export declare function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, }: {
188
+ export declare function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, }: {
185
189
  cwd: string | null;
186
190
  worktreePath?: string;
187
191
  worktreeSlug?: string;
188
192
  worktreesRoot?: string;
189
193
  paseoHome?: string;
194
+ worktreesBaseRoot?: string;
190
195
  }): Promise<void>;
191
196
  /**
192
197
  * Create a git worktree with proper naming conventions
193
198
  */
194
- export declare const createWorktree: ({ cwd, source, worktreeSlug, runSetup, paseoHome, }: CreateWorktreeOptions) => Promise<WorktreeConfig>;
199
+ export declare const createWorktree: ({ cwd, source, worktreeSlug, runSetup, paseoHome, worktreesRoot, }: CreateWorktreeOptions) => Promise<WorktreeConfig>;
195
200
  //# sourceMappingURL=worktree.d.ts.map
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { existsSync, mkdirSync, realpathSync, rmSync, statSync } from "fs";
4
4
  import { copyFile, rm, stat } from "fs/promises";
5
- import { join, basename, dirname, resolve, sep } from "path";
5
+ import { join, basename, dirname, isAbsolute, resolve, sep } from "path";
6
6
  import net from "node:net";
7
7
  import { createHash } from "node:crypto";
8
8
  import stripAnsi from "strip-ansi";
@@ -17,6 +17,7 @@ import { resolvePaseoHome } from "../server/paseo-home.js";
17
17
  import { createExternalProcessEnv } from "../server/paseo-env.js";
18
18
  import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
19
19
  import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
20
+ import { expandTilde } from "./path.js";
20
21
  export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
21
22
  const execFileAsync = promisify(execFile);
22
23
  const READ_ONLY_GIT_ENV = {
@@ -529,14 +530,26 @@ export async function deriveWorktreeProjectHash(cwd) {
529
530
  return deriveShortAlphanumericHash(normalizePathForOwnership(cwd));
530
531
  }
531
532
  }
532
- export async function getPaseoWorktreesRoot(cwd, paseoHome) {
533
- const home = paseoHome ? resolve(paseoHome) : resolvePaseoHome();
533
+ export function resolvePaseoWorktreesBaseRoot(options) {
534
+ if (options?.worktreesRoot) {
535
+ const expandedRoot = expandTilde(options.worktreesRoot);
536
+ if (isAbsolute(expandedRoot)) {
537
+ return resolve(expandedRoot);
538
+ }
539
+ const home = options.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
540
+ return resolve(home, expandedRoot);
541
+ }
542
+ const home = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
543
+ return join(home, "worktrees");
544
+ }
545
+ export async function getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot) {
546
+ const baseRoot = resolvePaseoWorktreesBaseRoot({ paseoHome, worktreesRoot });
534
547
  const projectHash = await deriveWorktreeProjectHash(cwd);
535
- return join(home, "worktrees", projectHash);
548
+ return join(baseRoot, projectHash);
536
549
  }
537
- export async function computeWorktreePath(cwd, slug, paseoHome) {
538
- const worktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
539
- return join(worktreesRoot, slug);
550
+ export async function computeWorktreePath(cwd, slug, paseoHome, worktreesRoot) {
551
+ const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
552
+ return join(projectWorktreesRoot, slug);
540
553
  }
541
554
  function normalizePathForOwnership(input) {
542
555
  try {
@@ -565,9 +578,9 @@ export async function isPaseoOwnedWorktreeCwd(cwd, options) {
565
578
  catch {
566
579
  // ignore
567
580
  }
568
- const paseoHome = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
569
- const paseoWorktreesPrefix = normalizePathForOwnership(join(paseoHome, "worktrees")) + sep;
570
- // Ownership is defined by the path living under $PASEO_HOME/worktrees/<hash>/<slug>[/...].
581
+ const worktreesBaseRoot = resolvePaseoWorktreesBaseRoot(options);
582
+ const paseoWorktreesPrefix = normalizePathForOwnership(worktreesBaseRoot) + sep;
583
+ // Ownership is defined by the path living under <worktrees-root>/<hash>/<slug>[/...].
571
584
  // The <hash>/<slug> prefix is Paseo-private — nothing else writes there — so the
572
585
  // path shape alone is sufficient proof of ownership, even when git has already
573
586
  // forgotten about the worktree.
@@ -587,7 +600,7 @@ export async function isPaseoOwnedWorktreeCwd(cwd, options) {
587
600
  worktreePath: resolvedCwd,
588
601
  };
589
602
  }
590
- const worktreesRoot = join(paseoHome, "worktrees", parts[0]);
603
+ const worktreesRoot = join(worktreesBaseRoot, parts[0]);
591
604
  return {
592
605
  allowed: true,
593
606
  ...(repoRoot !== undefined ? { repoRoot } : {}),
@@ -639,22 +652,23 @@ function resolveWorktreeCreatedAtIso(worktreePath) {
639
652
  return new Date(0).toISOString();
640
653
  }
641
654
  }
642
- export async function listPaseoWorktrees({ cwd, paseoHome, }) {
643
- const worktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
655
+ export async function listPaseoWorktrees({ cwd, paseoHome, worktreesRoot, }) {
656
+ const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
644
657
  const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
645
658
  cwd,
646
659
  envOverlay: READ_ONLY_GIT_ENV,
647
660
  });
648
- const rootPrefix = normalizePathForOwnership(worktreesRoot) + sep;
661
+ const rootPrefix = normalizePathForOwnership(projectWorktreesRoot) + sep;
649
662
  return parseWorktreeList(stdout)
650
663
  .map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) }))
651
664
  .filter((entry) => entry.path.startsWith(rootPrefix))
652
665
  .map((entry) => Object.assign({}, entry, { createdAt: resolveWorktreeCreatedAtIso(entry.path) }));
653
666
  }
654
- export async function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, }) {
667
+ export async function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, worktreesRoot, }) {
655
668
  const worktrees = await listPaseoWorktrees({
656
669
  cwd: repoRoot,
657
670
  paseoHome,
671
+ worktreesRoot,
658
672
  });
659
673
  const slugSuffix = `${sep}${slug}`;
660
674
  const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
@@ -682,7 +696,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
682
696
  catch {
683
697
  return null;
684
698
  }
685
- const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome);
699
+ const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome, options?.worktreesRoot);
686
700
  const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
687
701
  let worktreeRoot = null;
688
702
  try {
@@ -705,6 +719,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
705
719
  const knownWorktrees = await listPaseoWorktrees({
706
720
  cwd,
707
721
  paseoHome: options?.paseoHome,
722
+ worktreesRoot: options?.worktreesRoot,
708
723
  });
709
724
  const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
710
725
  if (!match) {
@@ -716,7 +731,7 @@ export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
716
731
  worktreePath: match.path,
717
732
  };
718
733
  }
719
- export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, }) {
734
+ export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, }) {
720
735
  if (!worktreePath && !worktreeSlug) {
721
736
  throw new Error("worktreePath or worktreeSlug is required");
722
737
  }
@@ -728,7 +743,7 @@ export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, wor
728
743
  resolvedWorktreesRoot = worktreesRoot;
729
744
  }
730
745
  else if (cwd) {
731
- resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
746
+ resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesBaseRoot);
732
747
  }
733
748
  else {
734
749
  throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree");
@@ -736,8 +751,10 @@ export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, wor
736
751
  const resolvedRoot = normalizePathForOwnership(resolvedWorktreesRoot) + sep;
737
752
  const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug);
738
753
  const resolvedRequested = normalizePathForOwnership(requestedPath);
739
- const resolvedWorktree = (await resolvePaseoWorktreeRootForCwd(requestedPath, { paseoHome }))?.worktreePath ??
740
- resolvedRequested;
754
+ const resolvedWorktree = (await resolvePaseoWorktreeRootForCwd(requestedPath, {
755
+ paseoHome,
756
+ worktreesRoot: worktreesBaseRoot,
757
+ }))?.worktreePath ?? resolvedRequested;
741
758
  if (!resolvedWorktree.startsWith(resolvedRoot)) {
742
759
  throw new Error("Refusing to delete non-Paseo worktree");
743
760
  }
@@ -812,9 +829,9 @@ async function removeDirectoryWithRetries(path) {
812
829
  /**
813
830
  * Create a git worktree with proper naming conventions
814
831
  */
815
- export const createWorktree = async ({ cwd, source, worktreeSlug, runSetup, paseoHome, }) => {
832
+ export const createWorktree = async ({ cwd, source, worktreeSlug, runSetup, paseoHome, worktreesRoot, }) => {
816
833
  const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
817
- let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome), worktreeSlug);
834
+ let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug);
818
835
  mkdirSync(dirname(worktreePath), { recursive: true });
819
836
  // Also handle worktree path collision
820
837
  let finalWorktreePath = worktreePath;
@@ -49,6 +49,11 @@ const ProvidersSchema = z
49
49
  local: LocalSpeechProviderSchema.optional(),
50
50
  })
51
51
  .strict();
52
+ const WorktreesConfigSchema = z
53
+ .object({
54
+ root: z.string().min(1).optional(),
55
+ })
56
+ .strict();
52
57
  const BcryptHashSchema = z.string().regex(/^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/, {
53
58
  message: "Expected a bcrypt hash",
54
59
  });
@@ -197,6 +202,17 @@ export const PersistedConfigSchema = z
197
202
  })
198
203
  .strict()
199
204
  .optional(),
205
+ serviceProxy: z
206
+ .object({
207
+ // COMPAT(serviceProxyEnabled): added 2026-06-02, remove after 2026-12-02.
208
+ // Parsed only to suppress optional public/listen layers for old configs;
209
+ // localhost service proxying remains always enabled.
210
+ enabled: z.boolean().optional(),
211
+ listen: z.string().optional(),
212
+ publicBaseUrl: z.string().url().optional(),
213
+ })
214
+ .strict()
215
+ .optional(),
200
216
  auth: DaemonAuthSchema.optional(),
201
217
  })
202
218
  .strict()
@@ -212,6 +228,7 @@ export const PersistedConfigSchema = z
212
228
  .strict()
213
229
  .optional(),
214
230
  providers: ProvidersSchema.optional(),
231
+ worktrees: WorktreesConfigSchema.optional(),
215
232
  agents: z
216
233
  .object({
217
234
  providers: z.preprocess(normalizeAgentProviders, ProviderOverridesSchema).optional(),