@agimon-ai/po-git 0.0.0-alpha.7 → 0.0.0-alpha.71

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.
@@ -21,13 +21,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
  //#endregion
23
23
  let node_child_process = require("node:child_process");
24
- let node_fs = require("node:fs");
25
- node_fs = __toESM(node_fs, 1);
24
+ let node_fs_promises = require("node:fs/promises");
25
+ node_fs_promises = __toESM(node_fs_promises, 1);
26
26
  let node_crypto = require("node:crypto");
27
- let node_os = require("node:os");
28
- node_os = __toESM(node_os, 1);
29
27
  let node_path = require("node:path");
30
28
  node_path = __toESM(node_path, 1);
29
+ let js_yaml = require("js-yaml");
30
+ let node_os = require("node:os");
31
+ node_os = __toESM(node_os, 1);
31
32
  let node_util = require("node:util");
32
33
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
33
34
  let node_timers_promises = require("node:timers/promises");
@@ -44,7 +45,7 @@ let node_timers_promises = require("node:timers/promises");
44
45
  */
45
46
  const READ_TIMEOUT_MS = 3e4;
46
47
  const ADD_TIMEOUT_MS = 3e5;
47
- const REMOVE_TIMEOUT_MS = 3e5;
48
+ const REMOVE_TIMEOUT_MS$1 = 3e5;
48
49
  const PRUNE_TIMEOUT_MS = 15e3;
49
50
  /** fetch and push: bounded by the network, so long enough for a slow link, short enough to notice a dead one. */
50
51
  const NETWORK_TIMEOUT_MS = 12e4;
@@ -197,9 +198,9 @@ function createWorktreeGit() {
197
198
  "remove",
198
199
  ...force ? ["--force"] : [],
199
200
  path
200
- ], REMOVE_TIMEOUT_MS);
201
+ ], REMOVE_TIMEOUT_MS$1);
201
202
  if (result.code === 0) return;
202
- if (missingWorktree(result.stderr) && !node_fs.default.existsSync(path)) {
203
+ if (missingWorktree(result.stderr) && !await node_fs_promises.default.access(path).then(() => true, () => false)) {
203
204
  await prune(repositoryRoot);
204
205
  return;
205
206
  }
@@ -285,6 +286,25 @@ function createWorktreeGit() {
285
286
  };
286
287
  }
287
288
  //#endregion
289
+ //#region src/constants/git.ts
290
+ /** Workspace-mounted routes behind the new-session dialog. */
291
+ const GIT_BRANCHES_PATH = "/branches";
292
+ const GIT_SESSIONS_PATH = "/sessions";
293
+ /** Workspace-mounted: that workspace's remote auth for manual pull, push and rebase. */
294
+ const GIT_AUTH_PATH = "/auth";
295
+ /** Session-mounted: the session's change against its base, and one file's hunks. */
296
+ const GIT_REVIEW_PATH = "/review";
297
+ const GIT_REVIEW_FILE_PATH = "/review/file";
298
+ /** Repository file, relative to the checkout root, whose `setup` commands run in every new worktree. */
299
+ const WORKTREE_CONFIG_FILE = ".po/worktree.yaml";
300
+ /**
301
+ * po-runner's session command, so setup runs as a runner of the worktree session and its log
302
+ * shows in Runners. Mirrors po-runner's `runnerLaunchLine`; the command follows the separator.
303
+ */
304
+ const WORKTREE_SETUP_LAUNCH = "/runners start name=worktree-setup --";
305
+ /** Installs the rift CLI. Shown to the person to copy, and named in the error an agent reads. */
306
+ const RIFT_INSTALL_COMMAND = "npm install -g rift-snapshot";
307
+ //#endregion
288
308
  //#region src/services/errors/index.ts
289
309
  var PoGitExpectedError = class extends Error {
290
310
  code;
@@ -320,12 +340,12 @@ const OWNER_ONLY_FILE = 384;
320
340
  * record in it. Writing beside the target and renaming makes the swap atomic on
321
341
  * the same filesystem, so a reader sees either the old file or the new one.
322
342
  */
323
- function writeJsonAtomic(file, value) {
343
+ async function writeJsonAtomic(file, value) {
324
344
  const directory = node_path.default.dirname(file);
325
- node_fs.default.mkdirSync(directory, { recursive: true });
326
- const temporary = node_path.default.join(directory, `.${node_path.default.basename(file)}.${String(process.pid)}.tmp`);
327
- node_fs.default.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: OWNER_ONLY_FILE });
328
- node_fs.default.renameSync(temporary, file);
345
+ await node_fs_promises.default.mkdir(directory, { recursive: true });
346
+ const temporary = node_path.default.join(directory, `.${node_path.default.basename(file)}.${(0, node_crypto.randomUUID)()}.tmp`);
347
+ await node_fs_promises.default.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: OWNER_ONLY_FILE });
348
+ await node_fs_promises.default.rename(temporary, file);
329
349
  }
330
350
  /**
331
351
  * Reads JSON, treating every failure as absent.
@@ -334,9 +354,9 @@ function writeJsonAtomic(file, value) {
334
354
  * here: there is nothing to act on. Distinguishing them would only offer a
335
355
  * choice no caller wants to make.
336
356
  */
337
- function readJson(file) {
357
+ async function readJson(file) {
338
358
  try {
339
- return JSON.parse(node_fs.default.readFileSync(file, "utf8"));
359
+ return JSON.parse(await node_fs_promises.default.readFile(file, "utf8"));
340
360
  } catch {
341
361
  return;
342
362
  }
@@ -355,12 +375,12 @@ const MAX_LABEL_LENGTH = 32;
355
375
  * the registry is per repository and must be found from inside a worktree too.
356
376
  * A session may run in a subdirectory, so the nearest enclosing checkout is used.
357
377
  */
358
- function gitCommonDirectory(directory) {
378
+ async function gitCommonDirectory(directory) {
359
379
  let root = node_path.default.resolve(directory);
360
380
  let marker = node_path.default.join(root, ".git");
361
381
  let stat;
362
382
  for (;;) try {
363
- stat = node_fs.default.statSync(marker);
383
+ stat = await node_fs_promises.default.stat(marker);
364
384
  break;
365
385
  } catch {
366
386
  const parent = node_path.default.dirname(root);
@@ -371,14 +391,14 @@ function gitCommonDirectory(directory) {
371
391
  if (stat.isDirectory()) return marker;
372
392
  let pointer;
373
393
  try {
374
- pointer = node_fs.default.readFileSync(marker, "utf8").trim();
394
+ pointer = (await node_fs_promises.default.readFile(marker, "utf8")).trim();
375
395
  } catch {
376
396
  return;
377
397
  }
378
398
  if (!pointer.startsWith("gitdir:")) return void 0;
379
399
  const gitDir = node_path.default.resolve(root, pointer.slice(7).trim());
380
400
  try {
381
- const common = node_fs.default.readFileSync(node_path.default.join(gitDir, "commondir"), "utf8").trim();
401
+ const common = (await node_fs_promises.default.readFile(node_path.default.join(gitDir, "commondir"), "utf8")).trim();
382
402
  return node_path.default.resolve(gitDir, common);
383
403
  } catch {
384
404
  return gitDir;
@@ -397,8 +417,8 @@ function repositoryLabel(root) {
397
417
  * registry. Falling back to the path keeps a non-repository directory usable
398
418
  * instead of failing, though nothing here should reach that case.
399
419
  */
400
- function repositoryId(root) {
401
- const common = gitCommonDirectory(root);
420
+ async function repositoryId(root) {
421
+ const common = await gitCommonDirectory(root);
402
422
  const token = common === void 0 ? `root:${node_path.default.resolve(root)}` : `git:${common}`;
403
423
  return (0, node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, ID_LENGTH);
404
424
  }
@@ -434,22 +454,26 @@ function credentialsFile(homeDir = node_os.default.homedir()) {
434
454
  return node_path.default.join(poGitRoot(homeDir), "credentials.json");
435
455
  }
436
456
  /** One registry file per repository, keyed by the shared git directory. */
437
- function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
438
- const id = repositoryId(repositoryRoot);
457
+ async function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
458
+ const id = await repositoryId(repositoryRoot);
439
459
  const root = node_path.default.join(poGitRoot(homeDir), "registry");
440
460
  const canonical = node_path.default.join(root, id, "worktrees.json");
441
- if (node_fs.default.existsSync(canonical)) return canonical;
461
+ if (await node_fs_promises.default.access(canonical).then(() => true, () => false)) return canonical;
442
462
  let names;
443
463
  try {
444
- names = node_fs.default.readdirSync(root);
464
+ names = await node_fs_promises.default.readdir(root);
445
465
  } catch {
446
466
  return canonical;
447
467
  }
448
- const legacyFiles = names.filter((entry) => entry.endsWith(`--${id}`)).sort().map((entry) => node_path.default.join(root, entry, "worktrees.json")).filter((file) => node_fs.default.existsSync(file));
468
+ const legacyFiles = [];
469
+ for (const entry of names.filter((name) => name.endsWith(`--${id}`)).sort()) {
470
+ const file = node_path.default.join(root, entry, "worktrees.json");
471
+ if (await node_fs_promises.default.access(file).then(() => true, () => false)) legacyFiles.push(file);
472
+ }
449
473
  if (legacyFiles.length === 0) return canonical;
450
474
  const entries = /* @__PURE__ */ new Map();
451
475
  for (const file of legacyFiles) {
452
- const legacy = readJson(file);
476
+ const legacy = await readJson(file);
453
477
  if (legacy?.version !== 1 || !Array.isArray(legacy.entries)) continue;
454
478
  for (const value of legacy.entries) {
455
479
  if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || value.id === "") continue;
@@ -460,20 +484,119 @@ function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
460
484
  }
461
485
  }
462
486
  const temporary = `${canonical}.${(0, node_crypto.randomUUID)()}.migration`;
463
- writeJsonAtomic(temporary, {
487
+ await writeJsonAtomic(temporary, {
464
488
  version: 1,
465
489
  entries: [...entries.values()]
466
490
  });
467
491
  try {
468
- node_fs.default.linkSync(temporary, canonical);
492
+ await node_fs_promises.default.link(temporary, canonical);
469
493
  } catch (error) {
470
494
  if (error.code !== "EEXIST") throw error;
471
495
  } finally {
472
- node_fs.default.rmSync(temporary);
496
+ await node_fs_promises.default.rm(temporary);
473
497
  }
474
498
  return canonical;
475
499
  }
476
500
  //#endregion
501
+ //#region src/services/riftCli/index.ts
502
+ /** Present at a repository root, it makes worktrees Rift copy-on-write workspaces instead of git worktrees. */
503
+ const RIFT_CONFIG_FILE = ".rift.toml";
504
+ /** Rift's identity marker; a root without it was never `rift init`ed. */
505
+ const RIFT_MARKER = ".rift";
506
+ /** Cloning is instant, but `.rift.toml` hooks such as an install run inside create. */
507
+ const CREATE_TIMEOUT_MS = 6e5;
508
+ const REMOVE_TIMEOUT_MS = 3e5;
509
+ /**
510
+ * Runs the rift CLI. Stderr is dropped rather than piped: hooks inherit it and may keep it open
511
+ * after rift exits (a `docker compose up -d`), and raw output never reaches a tool result anyway.
512
+ */
513
+ function rift(cwd, args, timeout) {
514
+ return new Promise((resolve, reject) => {
515
+ const child = (0, node_child_process.spawn)("rift", [...args], {
516
+ cwd,
517
+ stdio: [
518
+ "ignore",
519
+ "pipe",
520
+ "ignore"
521
+ ],
522
+ timeout
523
+ });
524
+ let stdout = "";
525
+ child.stdout.on("data", (chunk) => stdout += String(chunk));
526
+ child.on("error", (error) => reject(error.code === "ENOENT" ? new PoGitExpectedError("rift_missing", `This repository has ${RIFT_CONFIG_FILE}, but the rift CLI is not installed.`, false, `Install it with \`${RIFT_INSTALL_COMMAND}\`, run \`rift init\` in the repository root, then try again. An agent asks the person with ask_user_question before installing. Removing ${RIFT_CONFIG_FILE} falls back to git worktrees.`) : error));
527
+ child.on("exit", (code) => resolve({
528
+ code: code ?? 1,
529
+ stdout
530
+ }));
531
+ });
532
+ }
533
+ const exists = (file) => node_fs_promises.default.access(file).then(() => true, () => false);
534
+ function createWorktreeRift() {
535
+ const remove = async (target) => {
536
+ if (!await exists(target)) return;
537
+ const result = await rift(node_path.default.dirname(target), ["remove", target], REMOVE_TIMEOUT_MS);
538
+ if (result.code !== 0) throw new Error(`rift remove failed (exit ${String(result.code)})`);
539
+ };
540
+ return {
541
+ enabled: (repositoryRoot) => exists(node_path.default.join(repositoryRoot, RIFT_CONFIG_FILE)),
542
+ async create({ repositoryRoot, path: target, branch, baseRef, existing, remote }) {
543
+ if (!await exists(node_path.default.join(repositoryRoot, RIFT_MARKER))) {
544
+ await rift(repositoryRoot, ["--help"], READ_TIMEOUT_MS);
545
+ throw new PoGitExpectedError("invalid_request", `This repository has ${RIFT_CONFIG_FILE}, but Rift is not initialized for ${repositoryRoot}.`, false, "Run `rift init` in the repository root (not inside a git worktree), then try again.");
546
+ }
547
+ if (await exists(target)) throw new Error(`The worktree path ${target} already exists.`);
548
+ await node_fs_promises.default.mkdir(node_path.default.dirname(target), { recursive: true });
549
+ const created = await rift(repositoryRoot, [
550
+ "create",
551
+ repositoryRoot,
552
+ "--into",
553
+ node_path.default.dirname(target),
554
+ "--name",
555
+ node_path.default.basename(target),
556
+ "--copy-all"
557
+ ], CREATE_TIMEOUT_MS);
558
+ if (created.code !== 0) {
559
+ await remove(target).catch(() => void 0);
560
+ throw new Error(`rift create failed (exit ${String(created.code)})`);
561
+ }
562
+ const switchArgs = existing ? remote === void 0 ? [
563
+ "switch",
564
+ "--discard-changes",
565
+ branch
566
+ ] : [
567
+ "switch",
568
+ "--discard-changes",
569
+ "--track",
570
+ "-c",
571
+ branch,
572
+ `${remote}/${branch}`
573
+ ] : [
574
+ "switch",
575
+ "--discard-changes",
576
+ "-c",
577
+ branch,
578
+ baseRef
579
+ ];
580
+ for (const args of [switchArgs, ["clean", "-fd"]]) {
581
+ const result = await runGit(target, args, { timeout: READ_TIMEOUT_MS });
582
+ if (result.code !== 0) {
583
+ await remove(target).catch(() => void 0);
584
+ throw new Error(`git ${String(args[0])} in the rift failed (exit ${String(result.code)})`);
585
+ }
586
+ }
587
+ },
588
+ remove,
589
+ async fetchBranch({ repositoryRoot, path: source, branch }) {
590
+ const result = await runGit(repositoryRoot, [
591
+ "fetch",
592
+ source,
593
+ `refs/heads/${branch}:refs/heads/${branch}`
594
+ ], { timeout: READ_TIMEOUT_MS });
595
+ if (result.code !== 0) throw new Error(`git fetch from the rift failed (exit ${String(result.code)})`);
596
+ }
597
+ };
598
+ }
599
+ //#endregion
477
600
  //#region src/services/worktreeEvents/index.ts
478
601
  const GIT_WORKTREE_LIFECYCLE_EVENT = "git_worktree_lifecycle";
479
602
  const GIT_WORKTREE_MESSAGE_EVENT = "git_worktree_message";
@@ -588,18 +711,22 @@ function refuseClose(input) {
588
711
  * uncommitted work, so losing the record would lose the only pointer to it.
589
712
  * Deletion is always a separate, named decision.
590
713
  */
591
- function reconcile(records, probe) {
714
+ async function reconcile(records, probe) {
592
715
  const changed = [];
716
+ const next = [];
717
+ for (const record of records) {
718
+ if (record.status === "orphaned" || await probe.exists(record.path) && probe.alive(record)) {
719
+ next.push(record);
720
+ continue;
721
+ }
722
+ changed.push(record.id);
723
+ next.push({
724
+ ...record,
725
+ status: "orphaned"
726
+ });
727
+ }
593
728
  return {
594
- records: records.map((record) => {
595
- if (record.status === "orphaned") return record;
596
- if (!(!probe.exists(record.path) || !probe.alive(record))) return record;
597
- changed.push(record.id);
598
- return {
599
- ...record,
600
- status: "orphaned"
601
- };
602
- }),
729
+ records: next,
603
730
  changed
604
731
  };
605
732
  }
@@ -659,25 +786,25 @@ function isRecord(value) {
659
786
  */
660
787
  function createWorktreeRegistry(file) {
661
788
  return {
662
- list() {
663
- const parsed = readJson(file);
789
+ async list() {
790
+ const parsed = await readJson(file);
664
791
  if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) return [];
665
792
  return parsed.entries.filter(isRecord);
666
793
  },
667
- replace(entries) {
668
- writeJsonAtomic(file, {
794
+ async replace(entries) {
795
+ await writeJsonAtomic(file, {
669
796
  version: 1,
670
797
  entries
671
798
  });
672
799
  },
673
800
  async transaction(operation, signal) {
674
801
  const lock = `${file}.lock`;
675
- node_fs.default.mkdirSync(node_path.default.dirname(file), { recursive: true });
802
+ await node_fs_promises.default.mkdir(node_path.default.dirname(file), { recursive: true });
676
803
  const deadline = Date.now() + LOCK_WAIT_MS;
677
804
  for (;;) {
678
805
  signal?.throwIfAborted();
679
806
  try {
680
- node_fs.default.mkdirSync(lock, { mode: 448 });
807
+ await node_fs_promises.default.mkdir(lock, { mode: 448 });
681
808
  break;
682
809
  } catch (error) {
683
810
  if (error.code !== "EEXIST") throw error;
@@ -686,14 +813,14 @@ function createWorktreeRegistry(file) {
686
813
  }
687
814
  }
688
815
  try {
689
- node_fs.default.writeFileSync(node_path.default.join(lock, "owner.json"), JSON.stringify({
816
+ await node_fs_promises.default.writeFile(node_path.default.join(lock, "owner.json"), JSON.stringify({
690
817
  pid: process.pid,
691
818
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
692
819
  }), { mode: 384 });
693
820
  signal?.throwIfAborted();
694
821
  return await operation();
695
822
  } finally {
696
- node_fs.default.rmSync(lock, { recursive: true });
823
+ await node_fs_promises.default.rm(lock, { recursive: true });
697
824
  }
698
825
  }
699
826
  };
@@ -703,8 +830,26 @@ function createWorktreeRegistry(file) {
703
830
  const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
704
831
  const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
705
832
  const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
833
+ /**
834
+ * The repository's worktree setup as one shell command, or undefined when it asks for none.
835
+ * Read from the checkout the spawn starts in, so an uncommitted edit applies at once.
836
+ */
837
+ async function readWorktreeSetup(root) {
838
+ const file = node_path.default.join(root, WORKTREE_CONFIG_FILE);
839
+ let setup;
840
+ try {
841
+ setup = (0, js_yaml.load)(await node_fs_promises.default.readFile(file, "utf8"))?.setup;
842
+ } catch (error) {
843
+ if (error.code === "ENOENT") return void 0;
844
+ throw new PoGitExpectedError("invalid_request", `${WORKTREE_CONFIG_FILE} is not valid YAML (${error.message}).`, false, "Fix the file and try again.");
845
+ }
846
+ const commands = [setup ?? []].flat();
847
+ if (!commands.every((command) => typeof command === "string")) throw new PoGitExpectedError("invalid_request", `${WORKTREE_CONFIG_FILE} setup must be a command or a list of commands.`, false, "Fix the file and try again.");
848
+ return commands.filter((command) => command.trim() !== "").join(" && ") || void 0;
849
+ }
706
850
  function createWorktreeOperations(deps) {
707
851
  const { git } = deps;
852
+ const rift = deps.rift ?? createWorktreeRift();
708
853
  const sessionService = deps.sessionService;
709
854
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
710
855
  const requireSessionService = () => {
@@ -723,8 +868,19 @@ function createWorktreeOperations(deps) {
723
868
  };
724
869
  const probe = {
725
870
  alive: (record) => requireSessionService().isLive(record.sessionId),
726
- exists: (path) => node_fs.default.existsSync(path)
871
+ exists: (path) => node_fs_promises.default.access(path).then(() => true, () => false)
727
872
  };
873
+ /** What git lists as worktrees, plus Rift workspaces, which git cannot see. */
874
+ const listedPaths = async (root, records) => {
875
+ const rifts = records.filter((record) => record.backend === "rift");
876
+ const present = await Promise.all(rifts.map((record) => probe.exists(record.path)));
877
+ return [...await git.listWorktreePaths(root), ...rifts.filter((_, index) => present[index]).map((r) => r.path)];
878
+ };
879
+ const removeCheckout = (root, record, force) => record.backend === "rift" ? rift.remove(record.path) : git.removeWorktree({
880
+ repositoryRoot: root,
881
+ path: record.path,
882
+ force
883
+ });
728
884
  /**
729
885
  * Everything an action needs about the repository it is acting on.
730
886
  *
@@ -735,11 +891,11 @@ function createWorktreeOperations(deps) {
735
891
  const resolve = async (context) => {
736
892
  const root = await git.repositoryRoot(context.cwd);
737
893
  if (root === void 0) throw new PoGitExpectedError("not_a_repository", `${context.cwd} is not inside a git repository.`, false, "Run this from a repository checkout.");
738
- const store = createWorktreeRegistry(registryFile(root, deps.homeDir));
894
+ const store = createWorktreeRegistry(await registryFile(root, deps.homeDir));
739
895
  return {
740
896
  root,
741
897
  store,
742
- records: store.list()
898
+ records: await store.list()
743
899
  };
744
900
  };
745
901
  const require$1 = (records, id) => {
@@ -790,11 +946,11 @@ function createWorktreeOperations(deps) {
790
946
  const expectedPath = worktreeDirectory({
791
947
  worktreesRoot: worktreesRoot(deps.homeDir),
792
948
  repositoryLabel: repositoryLabel(root),
793
- repositoryId: repositoryId(root),
949
+ repositoryId: await repositoryId(root),
794
950
  branch: request.branch,
795
951
  shortId: shortId(reserved.sessionId)
796
952
  });
797
- if (existing.parentSessionId !== parentSessionId || existing.repositoryRoot !== root || existing.branch !== request.branch || existing.path !== expectedPath || existing.path !== reserved?.cwd || request.baseRef !== void 0 && existing.baseRef !== request.baseRef || !node_fs.default.existsSync(existing.path) || !(await git.listWorktreePaths(root)).includes(existing.path) || await git.repositoryRoot(existing.path) !== existing.path || await git.currentBranch(existing.path) !== request.branch) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing branch and checkout; they will not be replaced.");
953
+ if (existing.parentSessionId !== parentSessionId || existing.repositoryRoot !== root || existing.branch !== request.branch || existing.path !== expectedPath || existing.path !== reserved?.cwd || request.baseRef !== void 0 && existing.baseRef !== request.baseRef || !await probe.exists(existing.path) || !(await listedPaths(root, records)).includes(existing.path) || await git.repositoryRoot(existing.path) !== existing.path || await git.currentBranch(existing.path) !== request.branch) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing branch and checkout; they will not be replaced.");
798
954
  if (!requireSessionService().isLive(existing.sessionId)) {
799
955
  if ((await requireSessionService().create({
800
956
  cwd: existing.path,
@@ -814,18 +970,20 @@ function createWorktreeOperations(deps) {
814
970
  });
815
971
  if (refusal !== void 0) throw new PoGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
816
972
  const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
973
+ const setup = await readWorktreeSetup(root);
974
+ const useRift = await rift.enabled(root);
817
975
  const existingBranch = request.checkout === "existing";
818
976
  const id = shortId(reserved?.sessionId ?? (0, node_crypto.randomUUID)());
819
977
  let path = worktreeDirectory({
820
978
  worktreesRoot: worktreesRoot(deps.homeDir),
821
979
  repositoryLabel: repositoryLabel(root),
822
- repositoryId: repositoryId(root),
980
+ repositoryId: await repositoryId(root),
823
981
  branch: request.branch,
824
982
  shortId: id
825
983
  });
826
984
  if (request.reservationId !== void 0) path = (await reservations.prepare(request.reservationId, parentSessionId, path)).cwd;
827
- const recovering = reserved?.cwd !== void 0 && node_fs.default.existsSync(path);
828
- if (recovering && (!(await git.listWorktreePaths(root)).includes(path) || await git.currentBranch(path) !== request.branch)) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing directory; it will not be replaced.");
985
+ const recovering = reserved?.cwd !== void 0 && await probe.exists(path);
986
+ if (recovering && (!(useRift ? await git.repositoryRoot(path) === path : (await git.listWorktreePaths(root)).includes(path)) || await git.currentBranch(path) !== request.branch)) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing directory; it will not be replaced.");
829
987
  if (!recovering) {
830
988
  if (existingBranch) {
831
989
  const branches = await git.listBranches(root);
@@ -838,11 +996,11 @@ function createWorktreeOperations(deps) {
838
996
  const cancelled = () => options?.signal?.aborted === true;
839
997
  const rollback = async () => {
840
998
  if (reserved) return ` The reserved checkout at ${path} was retained for recovery.`;
841
- await git.removeWorktree({
842
- repositoryRoot: root,
999
+ await removeCheckout(root, {
843
1000
  path,
844
- force: true
845
- }).catch(() => void 0);
1001
+ ...useRift ? { backend: "rift" } : {}
1002
+ }, true).catch(() => void 0);
1003
+ if (useRift) return "";
846
1004
  if (existingBranch && request.remote === void 0) return "";
847
1005
  return await git.deleteBranch({
848
1006
  repositoryRoot: root,
@@ -851,7 +1009,15 @@ function createWorktreeOperations(deps) {
851
1009
  };
852
1010
  options?.onProgress?.(`creating branch ${request.branch}\u2026`);
853
1011
  if (!recovering) {
854
- if (existingBranch) await git.addExistingWorktree({
1012
+ if (useRift) await rift.create({
1013
+ repositoryRoot: root,
1014
+ path,
1015
+ branch: request.branch,
1016
+ baseRef,
1017
+ existing: existingBranch,
1018
+ ...request.remote === void 0 ? {} : { remote: request.remote }
1019
+ });
1020
+ else if (existingBranch) await git.addExistingWorktree({
855
1021
  repositoryRoot: root,
856
1022
  path,
857
1023
  branch: request.branch,
@@ -893,20 +1059,30 @@ function createWorktreeOperations(deps) {
893
1059
  sessionId,
894
1060
  ...parented,
895
1061
  status: "running",
1062
+ ...useRift ? { backend: "rift" } : {},
896
1063
  createdAt: now().toISOString()
897
1064
  };
898
1065
  try {
899
- store.replace([...records, record]);
1066
+ await store.replace([...records, record]);
900
1067
  } catch (error) {
901
1068
  await requireSessionService().close(sessionId).catch(() => void 0);
902
1069
  const kept = await rollback();
903
1070
  throw new PoGitExpectedError("registry_write_failed", `The worktree was created but the registry could not be written (${error.message}).${kept}`, true, "Check the registry file for permissions or free space, then try again.");
904
1071
  }
1072
+ let setupStarted = false;
1073
+ if (setup !== void 0) try {
1074
+ const service = requireSessionService();
1075
+ if (service.prompt === void 0) throw new Error("this host cannot run session commands");
1076
+ await service.prompt(sessionId, `${WORKTREE_SETUP_LAUNCH} ${setup}`);
1077
+ setupStarted = true;
1078
+ } catch (error) {
1079
+ options?.onProgress?.(`worktree setup did not start (${String(error)})`);
1080
+ }
905
1081
  if (request.task !== void 0 && taskDelivery !== void 0) try {
906
1082
  const receipt = await taskDelivery.deliver({
907
1083
  recipientKey: sessionId,
908
1084
  kind: GIT_WORKTREE_DELEGATION_KIND,
909
- prompt: request.task,
1085
+ prompt: setupStarted ? `${request.task}\n\nThe worktree setup (${setup}) is running as the runner "worktree-setup". Check it with po-runner before relying on installed dependencies or builds.` : request.task,
910
1086
  metadata: {
911
1087
  worktreeId: record.id,
912
1088
  fromRole: "parent",
@@ -929,23 +1105,19 @@ function createWorktreeOperations(deps) {
929
1105
  requireOwner(context, record, "close");
930
1106
  const refusal = refuseClose({
931
1107
  record,
932
- dirtyFiles: node_fs.default.existsSync(record.path) ? await git.dirtyFiles(record.path) : [],
1108
+ dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : [],
933
1109
  force
934
1110
  });
935
1111
  if (refusal !== void 0) throw new PoGitExpectedError("worktree_dirty", refusal, false, "Commit the work, or pass force: true.");
936
1112
  await requireSessionService().close(record.sessionId);
937
- await git.removeWorktree({
938
- repositoryRoot: root,
939
- path: record.path,
940
- force
941
- });
942
- store.replace(records.filter((entry) => entry.id !== id));
1113
+ await removeCheckout(root, record, force);
1114
+ await store.replace(records.filter((entry) => entry.id !== id));
943
1115
  return record;
944
1116
  },
945
1117
  async list(context) {
946
1118
  const { store, records } = await resolve(context);
947
- const outcome = reconcile(records, probe);
948
- if (outcome.changed.length > 0) store.replace(outcome.records);
1119
+ const outcome = await reconcile(records, probe);
1120
+ if (outcome.changed.length > 0) await store.replace(outcome.records);
949
1121
  return outcome.records;
950
1122
  },
951
1123
  async status(context, id) {
@@ -953,7 +1125,7 @@ function createWorktreeOperations(deps) {
953
1125
  const record = require$1(records, id);
954
1126
  return {
955
1127
  record,
956
- dirtyFiles: node_fs.default.existsSync(record.path) ? await git.dirtyFiles(record.path) : []
1128
+ dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : []
957
1129
  };
958
1130
  },
959
1131
  async merge(context, id, message) {
@@ -962,6 +1134,11 @@ function createWorktreeOperations(deps) {
962
1134
  requireOwner(context, record, "merge");
963
1135
  const parentDirty = await git.dirtyFiles(root);
964
1136
  if (parentDirty.length > 0) throw new PoGitExpectedError("worktree_dirty", `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(", ")}.`, false, "Commit or stash the parent checkout first.");
1137
+ if (record.backend === "rift") await rift.fetchBranch({
1138
+ repositoryRoot: root,
1139
+ path: record.path,
1140
+ branch: record.branch
1141
+ });
965
1142
  await git.mergeBranch({
966
1143
  repositoryRoot: root,
967
1144
  branch: record.branch,
@@ -1026,10 +1203,10 @@ function createWorktreeOperations(deps) {
1026
1203
  },
1027
1204
  async prune(context, dryRun) {
1028
1205
  const { root, store, records } = await resolve(context);
1029
- const reconciled = reconcile(records, probe);
1030
- const gitPaths = await git.listWorktreePaths(root);
1206
+ const reconciled = await reconcile(records, probe);
1207
+ const gitPaths = await listedPaths(root, reconciled.records);
1031
1208
  const dirtyByPath = /* @__PURE__ */ new Map();
1032
- for (const record of reconciled.records) if (record.status === "orphaned" && node_fs.default.existsSync(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
1209
+ for (const record of reconciled.records) if (record.status === "orphaned" && await probe.exists(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
1033
1210
  const plan = planPrune({
1034
1211
  records: reconciled.records,
1035
1212
  gitPaths,
@@ -1037,20 +1214,16 @@ function createWorktreeOperations(deps) {
1037
1214
  dirtyByPath
1038
1215
  });
1039
1216
  if (dryRun) {
1040
- store.replace(reconciled.records);
1217
+ await store.replace(reconciled.records);
1041
1218
  return plan;
1042
1219
  }
1043
- for (const record of plan.remove) await git.removeWorktree({
1044
- repositoryRoot: root,
1045
- path: record.path,
1046
- force: false
1047
- });
1220
+ for (const record of plan.remove) await removeCheckout(root, record, false);
1048
1221
  for (const record of [...plan.remove, ...plan.forget]) if (!await git.deleteBranch({
1049
1222
  repositoryRoot: root,
1050
1223
  branch: record.branch
1051
- }).catch(() => false)) plan.keptBranches.push(record.branch);
1224
+ }).catch(() => false) && record.backend !== "rift") plan.keptBranches.push(record.branch);
1052
1225
  const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));
1053
- store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
1226
+ await store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
1054
1227
  return plan;
1055
1228
  }
1056
1229
  };
@@ -1072,7 +1245,7 @@ function createWorktreeOperations(deps) {
1072
1245
  return {
1073
1246
  ...operations,
1074
1247
  spawn: (context, request, options) => {
1075
- if (sessionService === void 0) return Promise.reject(new PoGitExpectedError("hub_unavailable", "The cockpit session service is unavailable.", true, "Use the cockpit worktree tool after its Git server facet is available."));
1248
+ if (sessionService === void 0) return Promise.reject(new PoGitExpectedError("hub_unavailable", "The cockpit session service is unavailable.", true, "Use the cockpit worktree tool after its Git server plugin is available."));
1076
1249
  if (!validRefName(request.branch) || request.baseRef !== void 0 && !validRefName(request.baseRef)) return Promise.reject(new PoGitExpectedError("invalid_request", "That is not a valid branch name.", false, "Pick another name."));
1077
1250
  if (request.reservationId !== void 0 && context.sessionId === void 0) return Promise.reject(new PoGitExpectedError("invalid_request", "A reserved worktree session requires its owning parent.", false, "Create the session from its pending setup card."));
1078
1251
  return locked(context, () => operations.spawn(context, request, options), options?.signal);
@@ -1084,6 +1257,36 @@ function createWorktreeOperations(deps) {
1084
1257
  };
1085
1258
  }
1086
1259
  //#endregion
1260
+ Object.defineProperty(exports, "GIT_AUTH_PATH", {
1261
+ enumerable: true,
1262
+ get: function() {
1263
+ return GIT_AUTH_PATH;
1264
+ }
1265
+ });
1266
+ Object.defineProperty(exports, "GIT_BRANCHES_PATH", {
1267
+ enumerable: true,
1268
+ get: function() {
1269
+ return GIT_BRANCHES_PATH;
1270
+ }
1271
+ });
1272
+ Object.defineProperty(exports, "GIT_REVIEW_FILE_PATH", {
1273
+ enumerable: true,
1274
+ get: function() {
1275
+ return GIT_REVIEW_FILE_PATH;
1276
+ }
1277
+ });
1278
+ Object.defineProperty(exports, "GIT_REVIEW_PATH", {
1279
+ enumerable: true,
1280
+ get: function() {
1281
+ return GIT_REVIEW_PATH;
1282
+ }
1283
+ });
1284
+ Object.defineProperty(exports, "GIT_SESSIONS_PATH", {
1285
+ enumerable: true,
1286
+ get: function() {
1287
+ return GIT_SESSIONS_PATH;
1288
+ }
1289
+ });
1087
1290
  Object.defineProperty(exports, "GIT_SESSION_CHANGED_EVENT", {
1088
1291
  enumerable: true,
1089
1292
  get: function() {
@@ -1217,4 +1420,4 @@ Object.defineProperty(exports, "writeJsonAtomic", {
1217
1420
  }
1218
1421
  });
1219
1422
 
1220
- //# sourceMappingURL=worktreeOperations-DGUbUnYU.cjs.map
1423
+ //# sourceMappingURL=worktreeOperations-CD7s0YnO.cjs.map