@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.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worktreeOperations = require("./worktreeOperations-DGUbUnYU.cjs");
2
+ const require_worktreeOperations = require("./worktreeOperations-CD7s0YnO.cjs");
3
3
  //#region src/services/extensionService/index.ts
4
4
  var DefaultGitExtensionService = class {
5
5
  async execute() {
package/dist/index.d.mts CHANGED
@@ -42,6 +42,8 @@ interface WorktreeRecord {
42
42
  */
43
43
  parentSessionId?: string;
44
44
  status: WorktreeStatus;
45
+ /** `rift`: a Rift copy-on-write workspace with its own `.git`, not a linked git worktree. */
46
+ backend?: 'rift';
45
47
  /** ISO 8601. */
46
48
  createdAt: string;
47
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":["_$___worktreeEvents0.WorktreeMessage"],"sources":["../src/services/worktreeEvents/index.ts","../src/types/worktreeRegistry.ts","../src/services/worktreeNaming/index.ts","../src/services/worktreeOperations/index.ts","../src/types/extension.ts","../src/services/extensionService/index.ts","../src/services/gitDependencies/index.ts"],"mappings":";;;cASa;KASD;UAEK;WACN,gBAAgB;WAChB;WACA;WACA,MAAM;WACN;WACA;;;;;;;;;;;KCRC;cAEC;UAEI;EACf,gBAAgB;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA;EACA,QAAQ;;EAER;;;;UC8De;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;iBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX;;;UCpHa;;EAEf;;;;;EAKA;;UAGe;EACf;EACA;EACA;EACA;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf,SAAS;;EAET,cAAc;;UAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9BA;;;;KCrDR;UAEK;EACf;EACA,OAAO;;UAGQ;EACf,WAAW,QAAQ;;UAGJ;EACf,SAAS;EACT,YAAY;;;;qBCbD,sCAAsC;EAC3C,WAAW,QAAQ;;;;wBCEX,sBAAsB,YAAW,QAAQ,4BAAiC"}
1
+ {"version":3,"file":"index.d.mts","names":["_$___worktreeEvents0.WorktreeMessage"],"sources":["../src/services/worktreeEvents/index.ts","../src/types/worktreeRegistry.ts","../src/services/worktreeNaming/index.ts","../src/services/worktreeOperations/index.ts","../src/types/extension.ts","../src/services/extensionService/index.ts","../src/services/gitDependencies/index.ts"],"mappings":";;;cASa;KASD;UAEK;WACN,gBAAgB;WAChB;WACA;WACA,MAAM;WACN;WACA;;;;;;;;;;;KCRC;cAEC;UAEI;EACf,gBAAgB;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA;EACA,QAAQ;;EAER;;EAEA;;;;UC8De;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;iBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX;;;UCpFa;;EAEf;;;;;EAKA;;UAGe;EACf;EACA;EACA;EACA;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf,SAAS;;EAET,cAAc;;UAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9BA;;;;KCvFR;UAEK;EACf;EACA,OAAO;;UAGQ;EACf,WAAW,QAAQ;;UAGJ;EACf,SAAS;EACT,YAAY;;;;qBCbD,sCAAsC;EAC3C,WAAW,QAAQ;;;;wBCEX,sBAAsB,YAAW,QAAQ,4BAAiC"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { b as createWorktreeGit, t as createWorktreeOperations } from "./worktreeOperations-BSeyxUai.mjs";
1
+ import { T as createWorktreeGit, t as createWorktreeOperations } from "./worktreeOperations-B3E4JY4H.mjs";
2
2
  //#region src/services/extensionService/index.ts
3
3
  var DefaultGitExtensionService = class {
4
4
  async execute() {
@@ -1,8 +1,9 @@
1
- import { execFile } from "node:child_process";
2
- import fs from "node:fs";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import os from "node:os";
5
4
  import path from "node:path";
5
+ import { load } from "js-yaml";
6
+ import os from "node:os";
6
7
  import { isDeepStrictEqual } from "node:util";
7
8
  import { poHomeDirectory } from "@agimon-ai/po-core/piSettings";
8
9
  import { setTimeout } from "node:timers/promises";
@@ -19,7 +20,7 @@ import { setTimeout } from "node:timers/promises";
19
20
  */
20
21
  const READ_TIMEOUT_MS = 3e4;
21
22
  const ADD_TIMEOUT_MS = 3e5;
22
- const REMOVE_TIMEOUT_MS = 3e5;
23
+ const REMOVE_TIMEOUT_MS$1 = 3e5;
23
24
  const PRUNE_TIMEOUT_MS = 15e3;
24
25
  /** fetch and push: bounded by the network, so long enough for a slow link, short enough to notice a dead one. */
25
26
  const NETWORK_TIMEOUT_MS = 12e4;
@@ -172,9 +173,9 @@ function createWorktreeGit() {
172
173
  "remove",
173
174
  ...force ? ["--force"] : [],
174
175
  path
175
- ], REMOVE_TIMEOUT_MS);
176
+ ], REMOVE_TIMEOUT_MS$1);
176
177
  if (result.code === 0) return;
177
- if (missingWorktree(result.stderr) && !fs.existsSync(path)) {
178
+ if (missingWorktree(result.stderr) && !await fs.access(path).then(() => true, () => false)) {
178
179
  await prune(repositoryRoot);
179
180
  return;
180
181
  }
@@ -260,6 +261,25 @@ function createWorktreeGit() {
260
261
  };
261
262
  }
262
263
  //#endregion
264
+ //#region src/constants/git.ts
265
+ /** Workspace-mounted routes behind the new-session dialog. */
266
+ const GIT_BRANCHES_PATH = "/branches";
267
+ const GIT_SESSIONS_PATH = "/sessions";
268
+ /** Workspace-mounted: that workspace's remote auth for manual pull, push and rebase. */
269
+ const GIT_AUTH_PATH = "/auth";
270
+ /** Session-mounted: the session's change against its base, and one file's hunks. */
271
+ const GIT_REVIEW_PATH = "/review";
272
+ const GIT_REVIEW_FILE_PATH = "/review/file";
273
+ /** Repository file, relative to the checkout root, whose `setup` commands run in every new worktree. */
274
+ const WORKTREE_CONFIG_FILE = ".po/worktree.yaml";
275
+ /**
276
+ * po-runner's session command, so setup runs as a runner of the worktree session and its log
277
+ * shows in Runners. Mirrors po-runner's `runnerLaunchLine`; the command follows the separator.
278
+ */
279
+ const WORKTREE_SETUP_LAUNCH = "/runners start name=worktree-setup --";
280
+ /** Installs the rift CLI. Shown to the person to copy, and named in the error an agent reads. */
281
+ const RIFT_INSTALL_COMMAND = "npm install -g rift-snapshot";
282
+ //#endregion
263
283
  //#region src/services/errors/index.ts
264
284
  var PoGitExpectedError = class extends Error {
265
285
  code;
@@ -295,12 +315,12 @@ const OWNER_ONLY_FILE = 384;
295
315
  * record in it. Writing beside the target and renaming makes the swap atomic on
296
316
  * the same filesystem, so a reader sees either the old file or the new one.
297
317
  */
298
- function writeJsonAtomic(file, value) {
318
+ async function writeJsonAtomic(file, value) {
299
319
  const directory = path.dirname(file);
300
- fs.mkdirSync(directory, { recursive: true });
301
- const temporary = path.join(directory, `.${path.basename(file)}.${String(process.pid)}.tmp`);
302
- fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: OWNER_ONLY_FILE });
303
- fs.renameSync(temporary, file);
320
+ await fs.mkdir(directory, { recursive: true });
321
+ const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
322
+ await fs.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: OWNER_ONLY_FILE });
323
+ await fs.rename(temporary, file);
304
324
  }
305
325
  /**
306
326
  * Reads JSON, treating every failure as absent.
@@ -309,9 +329,9 @@ function writeJsonAtomic(file, value) {
309
329
  * here: there is nothing to act on. Distinguishing them would only offer a
310
330
  * choice no caller wants to make.
311
331
  */
312
- function readJson(file) {
332
+ async function readJson(file) {
313
333
  try {
314
- return JSON.parse(fs.readFileSync(file, "utf8"));
334
+ return JSON.parse(await fs.readFile(file, "utf8"));
315
335
  } catch {
316
336
  return;
317
337
  }
@@ -330,12 +350,12 @@ const MAX_LABEL_LENGTH = 32;
330
350
  * the registry is per repository and must be found from inside a worktree too.
331
351
  * A session may run in a subdirectory, so the nearest enclosing checkout is used.
332
352
  */
333
- function gitCommonDirectory(directory) {
353
+ async function gitCommonDirectory(directory) {
334
354
  let root = path.resolve(directory);
335
355
  let marker = path.join(root, ".git");
336
356
  let stat;
337
357
  for (;;) try {
338
- stat = fs.statSync(marker);
358
+ stat = await fs.stat(marker);
339
359
  break;
340
360
  } catch {
341
361
  const parent = path.dirname(root);
@@ -346,14 +366,14 @@ function gitCommonDirectory(directory) {
346
366
  if (stat.isDirectory()) return marker;
347
367
  let pointer;
348
368
  try {
349
- pointer = fs.readFileSync(marker, "utf8").trim();
369
+ pointer = (await fs.readFile(marker, "utf8")).trim();
350
370
  } catch {
351
371
  return;
352
372
  }
353
373
  if (!pointer.startsWith("gitdir:")) return void 0;
354
374
  const gitDir = path.resolve(root, pointer.slice(7).trim());
355
375
  try {
356
- const common = fs.readFileSync(path.join(gitDir, "commondir"), "utf8").trim();
376
+ const common = (await fs.readFile(path.join(gitDir, "commondir"), "utf8")).trim();
357
377
  return path.resolve(gitDir, common);
358
378
  } catch {
359
379
  return gitDir;
@@ -372,8 +392,8 @@ function repositoryLabel(root) {
372
392
  * registry. Falling back to the path keeps a non-repository directory usable
373
393
  * instead of failing, though nothing here should reach that case.
374
394
  */
375
- function repositoryId(root) {
376
- const common = gitCommonDirectory(root);
395
+ async function repositoryId(root) {
396
+ const common = await gitCommonDirectory(root);
377
397
  const token = common === void 0 ? `root:${path.resolve(root)}` : `git:${common}`;
378
398
  return createHash("sha256").update(token).digest("hex").slice(0, ID_LENGTH);
379
399
  }
@@ -409,22 +429,26 @@ function credentialsFile(homeDir = os.homedir()) {
409
429
  return path.join(poGitRoot(homeDir), "credentials.json");
410
430
  }
411
431
  /** One registry file per repository, keyed by the shared git directory. */
412
- function registryFile(repositoryRoot, homeDir = os.homedir()) {
413
- const id = repositoryId(repositoryRoot);
432
+ async function registryFile(repositoryRoot, homeDir = os.homedir()) {
433
+ const id = await repositoryId(repositoryRoot);
414
434
  const root = path.join(poGitRoot(homeDir), "registry");
415
435
  const canonical = path.join(root, id, "worktrees.json");
416
- if (fs.existsSync(canonical)) return canonical;
436
+ if (await fs.access(canonical).then(() => true, () => false)) return canonical;
417
437
  let names;
418
438
  try {
419
- names = fs.readdirSync(root);
439
+ names = await fs.readdir(root);
420
440
  } catch {
421
441
  return canonical;
422
442
  }
423
- const legacyFiles = names.filter((entry) => entry.endsWith(`--${id}`)).sort().map((entry) => path.join(root, entry, "worktrees.json")).filter((file) => fs.existsSync(file));
443
+ const legacyFiles = [];
444
+ for (const entry of names.filter((name) => name.endsWith(`--${id}`)).sort()) {
445
+ const file = path.join(root, entry, "worktrees.json");
446
+ if (await fs.access(file).then(() => true, () => false)) legacyFiles.push(file);
447
+ }
424
448
  if (legacyFiles.length === 0) return canonical;
425
449
  const entries = /* @__PURE__ */ new Map();
426
450
  for (const file of legacyFiles) {
427
- const legacy = readJson(file);
451
+ const legacy = await readJson(file);
428
452
  if (legacy?.version !== 1 || !Array.isArray(legacy.entries)) continue;
429
453
  for (const value of legacy.entries) {
430
454
  if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || value.id === "") continue;
@@ -435,20 +459,119 @@ function registryFile(repositoryRoot, homeDir = os.homedir()) {
435
459
  }
436
460
  }
437
461
  const temporary = `${canonical}.${randomUUID()}.migration`;
438
- writeJsonAtomic(temporary, {
462
+ await writeJsonAtomic(temporary, {
439
463
  version: 1,
440
464
  entries: [...entries.values()]
441
465
  });
442
466
  try {
443
- fs.linkSync(temporary, canonical);
467
+ await fs.link(temporary, canonical);
444
468
  } catch (error) {
445
469
  if (error.code !== "EEXIST") throw error;
446
470
  } finally {
447
- fs.rmSync(temporary);
471
+ await fs.rm(temporary);
448
472
  }
449
473
  return canonical;
450
474
  }
451
475
  //#endregion
476
+ //#region src/services/riftCli/index.ts
477
+ /** Present at a repository root, it makes worktrees Rift copy-on-write workspaces instead of git worktrees. */
478
+ const RIFT_CONFIG_FILE = ".rift.toml";
479
+ /** Rift's identity marker; a root without it was never `rift init`ed. */
480
+ const RIFT_MARKER = ".rift";
481
+ /** Cloning is instant, but `.rift.toml` hooks such as an install run inside create. */
482
+ const CREATE_TIMEOUT_MS = 6e5;
483
+ const REMOVE_TIMEOUT_MS = 3e5;
484
+ /**
485
+ * Runs the rift CLI. Stderr is dropped rather than piped: hooks inherit it and may keep it open
486
+ * after rift exits (a `docker compose up -d`), and raw output never reaches a tool result anyway.
487
+ */
488
+ function rift(cwd, args, timeout) {
489
+ return new Promise((resolve, reject) => {
490
+ const child = spawn("rift", [...args], {
491
+ cwd,
492
+ stdio: [
493
+ "ignore",
494
+ "pipe",
495
+ "ignore"
496
+ ],
497
+ timeout
498
+ });
499
+ let stdout = "";
500
+ child.stdout.on("data", (chunk) => stdout += String(chunk));
501
+ 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));
502
+ child.on("exit", (code) => resolve({
503
+ code: code ?? 1,
504
+ stdout
505
+ }));
506
+ });
507
+ }
508
+ const exists = (file) => fs.access(file).then(() => true, () => false);
509
+ function createWorktreeRift() {
510
+ const remove = async (target) => {
511
+ if (!await exists(target)) return;
512
+ const result = await rift(path.dirname(target), ["remove", target], REMOVE_TIMEOUT_MS);
513
+ if (result.code !== 0) throw new Error(`rift remove failed (exit ${String(result.code)})`);
514
+ };
515
+ return {
516
+ enabled: (repositoryRoot) => exists(path.join(repositoryRoot, RIFT_CONFIG_FILE)),
517
+ async create({ repositoryRoot, path: target, branch, baseRef, existing, remote }) {
518
+ if (!await exists(path.join(repositoryRoot, RIFT_MARKER))) {
519
+ await rift(repositoryRoot, ["--help"], READ_TIMEOUT_MS);
520
+ 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.");
521
+ }
522
+ if (await exists(target)) throw new Error(`The worktree path ${target} already exists.`);
523
+ await fs.mkdir(path.dirname(target), { recursive: true });
524
+ const created = await rift(repositoryRoot, [
525
+ "create",
526
+ repositoryRoot,
527
+ "--into",
528
+ path.dirname(target),
529
+ "--name",
530
+ path.basename(target),
531
+ "--copy-all"
532
+ ], CREATE_TIMEOUT_MS);
533
+ if (created.code !== 0) {
534
+ await remove(target).catch(() => void 0);
535
+ throw new Error(`rift create failed (exit ${String(created.code)})`);
536
+ }
537
+ const switchArgs = existing ? remote === void 0 ? [
538
+ "switch",
539
+ "--discard-changes",
540
+ branch
541
+ ] : [
542
+ "switch",
543
+ "--discard-changes",
544
+ "--track",
545
+ "-c",
546
+ branch,
547
+ `${remote}/${branch}`
548
+ ] : [
549
+ "switch",
550
+ "--discard-changes",
551
+ "-c",
552
+ branch,
553
+ baseRef
554
+ ];
555
+ for (const args of [switchArgs, ["clean", "-fd"]]) {
556
+ const result = await runGit(target, args, { timeout: READ_TIMEOUT_MS });
557
+ if (result.code !== 0) {
558
+ await remove(target).catch(() => void 0);
559
+ throw new Error(`git ${String(args[0])} in the rift failed (exit ${String(result.code)})`);
560
+ }
561
+ }
562
+ },
563
+ remove,
564
+ async fetchBranch({ repositoryRoot, path: source, branch }) {
565
+ const result = await runGit(repositoryRoot, [
566
+ "fetch",
567
+ source,
568
+ `refs/heads/${branch}:refs/heads/${branch}`
569
+ ], { timeout: READ_TIMEOUT_MS });
570
+ if (result.code !== 0) throw new Error(`git fetch from the rift failed (exit ${String(result.code)})`);
571
+ }
572
+ };
573
+ }
574
+ //#endregion
452
575
  //#region src/services/worktreeEvents/index.ts
453
576
  const GIT_WORKTREE_LIFECYCLE_EVENT = "git_worktree_lifecycle";
454
577
  const GIT_WORKTREE_MESSAGE_EVENT = "git_worktree_message";
@@ -563,18 +686,22 @@ function refuseClose(input) {
563
686
  * uncommitted work, so losing the record would lose the only pointer to it.
564
687
  * Deletion is always a separate, named decision.
565
688
  */
566
- function reconcile(records, probe) {
689
+ async function reconcile(records, probe) {
567
690
  const changed = [];
691
+ const next = [];
692
+ for (const record of records) {
693
+ if (record.status === "orphaned" || await probe.exists(record.path) && probe.alive(record)) {
694
+ next.push(record);
695
+ continue;
696
+ }
697
+ changed.push(record.id);
698
+ next.push({
699
+ ...record,
700
+ status: "orphaned"
701
+ });
702
+ }
568
703
  return {
569
- records: records.map((record) => {
570
- if (record.status === "orphaned") return record;
571
- if (!(!probe.exists(record.path) || !probe.alive(record))) return record;
572
- changed.push(record.id);
573
- return {
574
- ...record,
575
- status: "orphaned"
576
- };
577
- }),
704
+ records: next,
578
705
  changed
579
706
  };
580
707
  }
@@ -634,25 +761,25 @@ function isRecord(value) {
634
761
  */
635
762
  function createWorktreeRegistry(file) {
636
763
  return {
637
- list() {
638
- const parsed = readJson(file);
764
+ async list() {
765
+ const parsed = await readJson(file);
639
766
  if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) return [];
640
767
  return parsed.entries.filter(isRecord);
641
768
  },
642
- replace(entries) {
643
- writeJsonAtomic(file, {
769
+ async replace(entries) {
770
+ await writeJsonAtomic(file, {
644
771
  version: 1,
645
772
  entries
646
773
  });
647
774
  },
648
775
  async transaction(operation, signal) {
649
776
  const lock = `${file}.lock`;
650
- fs.mkdirSync(path.dirname(file), { recursive: true });
777
+ await fs.mkdir(path.dirname(file), { recursive: true });
651
778
  const deadline = Date.now() + LOCK_WAIT_MS;
652
779
  for (;;) {
653
780
  signal?.throwIfAborted();
654
781
  try {
655
- fs.mkdirSync(lock, { mode: 448 });
782
+ await fs.mkdir(lock, { mode: 448 });
656
783
  break;
657
784
  } catch (error) {
658
785
  if (error.code !== "EEXIST") throw error;
@@ -661,14 +788,14 @@ function createWorktreeRegistry(file) {
661
788
  }
662
789
  }
663
790
  try {
664
- fs.writeFileSync(path.join(lock, "owner.json"), JSON.stringify({
791
+ await fs.writeFile(path.join(lock, "owner.json"), JSON.stringify({
665
792
  pid: process.pid,
666
793
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
667
794
  }), { mode: 384 });
668
795
  signal?.throwIfAborted();
669
796
  return await operation();
670
797
  } finally {
671
- fs.rmSync(lock, { recursive: true });
798
+ await fs.rm(lock, { recursive: true });
672
799
  }
673
800
  }
674
801
  };
@@ -678,8 +805,26 @@ function createWorktreeRegistry(file) {
678
805
  const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
679
806
  const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
680
807
  const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
808
+ /**
809
+ * The repository's worktree setup as one shell command, or undefined when it asks for none.
810
+ * Read from the checkout the spawn starts in, so an uncommitted edit applies at once.
811
+ */
812
+ async function readWorktreeSetup(root) {
813
+ const file = path.join(root, WORKTREE_CONFIG_FILE);
814
+ let setup;
815
+ try {
816
+ setup = load(await fs.readFile(file, "utf8"))?.setup;
817
+ } catch (error) {
818
+ if (error.code === "ENOENT") return void 0;
819
+ throw new PoGitExpectedError("invalid_request", `${WORKTREE_CONFIG_FILE} is not valid YAML (${error.message}).`, false, "Fix the file and try again.");
820
+ }
821
+ const commands = [setup ?? []].flat();
822
+ 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.");
823
+ return commands.filter((command) => command.trim() !== "").join(" && ") || void 0;
824
+ }
681
825
  function createWorktreeOperations(deps) {
682
826
  const { git } = deps;
827
+ const rift = deps.rift ?? createWorktreeRift();
683
828
  const sessionService = deps.sessionService;
684
829
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
685
830
  const requireSessionService = () => {
@@ -698,8 +843,19 @@ function createWorktreeOperations(deps) {
698
843
  };
699
844
  const probe = {
700
845
  alive: (record) => requireSessionService().isLive(record.sessionId),
701
- exists: (path) => fs.existsSync(path)
846
+ exists: (path) => fs.access(path).then(() => true, () => false)
847
+ };
848
+ /** What git lists as worktrees, plus Rift workspaces, which git cannot see. */
849
+ const listedPaths = async (root, records) => {
850
+ const rifts = records.filter((record) => record.backend === "rift");
851
+ const present = await Promise.all(rifts.map((record) => probe.exists(record.path)));
852
+ return [...await git.listWorktreePaths(root), ...rifts.filter((_, index) => present[index]).map((r) => r.path)];
702
853
  };
854
+ const removeCheckout = (root, record, force) => record.backend === "rift" ? rift.remove(record.path) : git.removeWorktree({
855
+ repositoryRoot: root,
856
+ path: record.path,
857
+ force
858
+ });
703
859
  /**
704
860
  * Everything an action needs about the repository it is acting on.
705
861
  *
@@ -710,11 +866,11 @@ function createWorktreeOperations(deps) {
710
866
  const resolve = async (context) => {
711
867
  const root = await git.repositoryRoot(context.cwd);
712
868
  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.");
713
- const store = createWorktreeRegistry(registryFile(root, deps.homeDir));
869
+ const store = createWorktreeRegistry(await registryFile(root, deps.homeDir));
714
870
  return {
715
871
  root,
716
872
  store,
717
- records: store.list()
873
+ records: await store.list()
718
874
  };
719
875
  };
720
876
  const require = (records, id) => {
@@ -765,11 +921,11 @@ function createWorktreeOperations(deps) {
765
921
  const expectedPath = worktreeDirectory({
766
922
  worktreesRoot: worktreesRoot(deps.homeDir),
767
923
  repositoryLabel: repositoryLabel(root),
768
- repositoryId: repositoryId(root),
924
+ repositoryId: await repositoryId(root),
769
925
  branch: request.branch,
770
926
  shortId: shortId(reserved.sessionId)
771
927
  });
772
- 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 || !fs.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.");
928
+ 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.");
773
929
  if (!requireSessionService().isLive(existing.sessionId)) {
774
930
  if ((await requireSessionService().create({
775
931
  cwd: existing.path,
@@ -789,18 +945,20 @@ function createWorktreeOperations(deps) {
789
945
  });
790
946
  if (refusal !== void 0) throw new PoGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
791
947
  const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
948
+ const setup = await readWorktreeSetup(root);
949
+ const useRift = await rift.enabled(root);
792
950
  const existingBranch = request.checkout === "existing";
793
951
  const id = shortId(reserved?.sessionId ?? randomUUID());
794
952
  let path = worktreeDirectory({
795
953
  worktreesRoot: worktreesRoot(deps.homeDir),
796
954
  repositoryLabel: repositoryLabel(root),
797
- repositoryId: repositoryId(root),
955
+ repositoryId: await repositoryId(root),
798
956
  branch: request.branch,
799
957
  shortId: id
800
958
  });
801
959
  if (request.reservationId !== void 0) path = (await reservations.prepare(request.reservationId, parentSessionId, path)).cwd;
802
- const recovering = reserved?.cwd !== void 0 && fs.existsSync(path);
803
- 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.");
960
+ const recovering = reserved?.cwd !== void 0 && await probe.exists(path);
961
+ 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.");
804
962
  if (!recovering) {
805
963
  if (existingBranch) {
806
964
  const branches = await git.listBranches(root);
@@ -813,11 +971,11 @@ function createWorktreeOperations(deps) {
813
971
  const cancelled = () => options?.signal?.aborted === true;
814
972
  const rollback = async () => {
815
973
  if (reserved) return ` The reserved checkout at ${path} was retained for recovery.`;
816
- await git.removeWorktree({
817
- repositoryRoot: root,
974
+ await removeCheckout(root, {
818
975
  path,
819
- force: true
820
- }).catch(() => void 0);
976
+ ...useRift ? { backend: "rift" } : {}
977
+ }, true).catch(() => void 0);
978
+ if (useRift) return "";
821
979
  if (existingBranch && request.remote === void 0) return "";
822
980
  return await git.deleteBranch({
823
981
  repositoryRoot: root,
@@ -826,7 +984,15 @@ function createWorktreeOperations(deps) {
826
984
  };
827
985
  options?.onProgress?.(`creating branch ${request.branch}\u2026`);
828
986
  if (!recovering) {
829
- if (existingBranch) await git.addExistingWorktree({
987
+ if (useRift) await rift.create({
988
+ repositoryRoot: root,
989
+ path,
990
+ branch: request.branch,
991
+ baseRef,
992
+ existing: existingBranch,
993
+ ...request.remote === void 0 ? {} : { remote: request.remote }
994
+ });
995
+ else if (existingBranch) await git.addExistingWorktree({
830
996
  repositoryRoot: root,
831
997
  path,
832
998
  branch: request.branch,
@@ -868,20 +1034,30 @@ function createWorktreeOperations(deps) {
868
1034
  sessionId,
869
1035
  ...parented,
870
1036
  status: "running",
1037
+ ...useRift ? { backend: "rift" } : {},
871
1038
  createdAt: now().toISOString()
872
1039
  };
873
1040
  try {
874
- store.replace([...records, record]);
1041
+ await store.replace([...records, record]);
875
1042
  } catch (error) {
876
1043
  await requireSessionService().close(sessionId).catch(() => void 0);
877
1044
  const kept = await rollback();
878
1045
  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.");
879
1046
  }
1047
+ let setupStarted = false;
1048
+ if (setup !== void 0) try {
1049
+ const service = requireSessionService();
1050
+ if (service.prompt === void 0) throw new Error("this host cannot run session commands");
1051
+ await service.prompt(sessionId, `${WORKTREE_SETUP_LAUNCH} ${setup}`);
1052
+ setupStarted = true;
1053
+ } catch (error) {
1054
+ options?.onProgress?.(`worktree setup did not start (${String(error)})`);
1055
+ }
880
1056
  if (request.task !== void 0 && taskDelivery !== void 0) try {
881
1057
  const receipt = await taskDelivery.deliver({
882
1058
  recipientKey: sessionId,
883
1059
  kind: GIT_WORKTREE_DELEGATION_KIND,
884
- prompt: request.task,
1060
+ 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,
885
1061
  metadata: {
886
1062
  worktreeId: record.id,
887
1063
  fromRole: "parent",
@@ -904,23 +1080,19 @@ function createWorktreeOperations(deps) {
904
1080
  requireOwner(context, record, "close");
905
1081
  const refusal = refuseClose({
906
1082
  record,
907
- dirtyFiles: fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [],
1083
+ dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : [],
908
1084
  force
909
1085
  });
910
1086
  if (refusal !== void 0) throw new PoGitExpectedError("worktree_dirty", refusal, false, "Commit the work, or pass force: true.");
911
1087
  await requireSessionService().close(record.sessionId);
912
- await git.removeWorktree({
913
- repositoryRoot: root,
914
- path: record.path,
915
- force
916
- });
917
- store.replace(records.filter((entry) => entry.id !== id));
1088
+ await removeCheckout(root, record, force);
1089
+ await store.replace(records.filter((entry) => entry.id !== id));
918
1090
  return record;
919
1091
  },
920
1092
  async list(context) {
921
1093
  const { store, records } = await resolve(context);
922
- const outcome = reconcile(records, probe);
923
- if (outcome.changed.length > 0) store.replace(outcome.records);
1094
+ const outcome = await reconcile(records, probe);
1095
+ if (outcome.changed.length > 0) await store.replace(outcome.records);
924
1096
  return outcome.records;
925
1097
  },
926
1098
  async status(context, id) {
@@ -928,7 +1100,7 @@ function createWorktreeOperations(deps) {
928
1100
  const record = require(records, id);
929
1101
  return {
930
1102
  record,
931
- dirtyFiles: fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : []
1103
+ dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : []
932
1104
  };
933
1105
  },
934
1106
  async merge(context, id, message) {
@@ -937,6 +1109,11 @@ function createWorktreeOperations(deps) {
937
1109
  requireOwner(context, record, "merge");
938
1110
  const parentDirty = await git.dirtyFiles(root);
939
1111
  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.");
1112
+ if (record.backend === "rift") await rift.fetchBranch({
1113
+ repositoryRoot: root,
1114
+ path: record.path,
1115
+ branch: record.branch
1116
+ });
940
1117
  await git.mergeBranch({
941
1118
  repositoryRoot: root,
942
1119
  branch: record.branch,
@@ -1001,10 +1178,10 @@ function createWorktreeOperations(deps) {
1001
1178
  },
1002
1179
  async prune(context, dryRun) {
1003
1180
  const { root, store, records } = await resolve(context);
1004
- const reconciled = reconcile(records, probe);
1005
- const gitPaths = await git.listWorktreePaths(root);
1181
+ const reconciled = await reconcile(records, probe);
1182
+ const gitPaths = await listedPaths(root, reconciled.records);
1006
1183
  const dirtyByPath = /* @__PURE__ */ new Map();
1007
- for (const record of reconciled.records) if (record.status === "orphaned" && fs.existsSync(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
1184
+ for (const record of reconciled.records) if (record.status === "orphaned" && await probe.exists(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
1008
1185
  const plan = planPrune({
1009
1186
  records: reconciled.records,
1010
1187
  gitPaths,
@@ -1012,20 +1189,16 @@ function createWorktreeOperations(deps) {
1012
1189
  dirtyByPath
1013
1190
  });
1014
1191
  if (dryRun) {
1015
- store.replace(reconciled.records);
1192
+ await store.replace(reconciled.records);
1016
1193
  return plan;
1017
1194
  }
1018
- for (const record of plan.remove) await git.removeWorktree({
1019
- repositoryRoot: root,
1020
- path: record.path,
1021
- force: false
1022
- });
1195
+ for (const record of plan.remove) await removeCheckout(root, record, false);
1023
1196
  for (const record of [...plan.remove, ...plan.forget]) if (!await git.deleteBranch({
1024
1197
  repositoryRoot: root,
1025
1198
  branch: record.branch
1026
- }).catch(() => false)) plan.keptBranches.push(record.branch);
1199
+ }).catch(() => false) && record.backend !== "rift") plan.keptBranches.push(record.branch);
1027
1200
  const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));
1028
- store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
1201
+ await store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
1029
1202
  return plan;
1030
1203
  }
1031
1204
  };
@@ -1047,7 +1220,7 @@ function createWorktreeOperations(deps) {
1047
1220
  return {
1048
1221
  ...operations,
1049
1222
  spawn: (context, request, options) => {
1050
- 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."));
1223
+ 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."));
1051
1224
  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."));
1052
1225
  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."));
1053
1226
  return locked(context, () => operations.spawn(context, request, options), options?.signal);
@@ -1059,6 +1232,6 @@ function createWorktreeOperations(deps) {
1059
1232
  };
1060
1233
  }
1061
1234
  //#endregion
1062
- export { NETWORK_TIMEOUT_MS as _, GIT_WORKTREE_LIFECYCLE_EVENT as a, createWorktreeGit as b, isWorktreeLifecycleEvent as c, registryFile as d, readJson as f, invalidRequest as g, PoGitExpectedError as h, GIT_SESSION_CHANGED_EVENT as i, credentialsFile as l, HubUnavailableError as m, createWorktreeRegistry as n, createWorktreeMessageInbox as o, writeJsonAtomic as p, validRefName as r, isSessionChangedEvent as s, createWorktreeOperations as t, poGitRoot as u, READ_TIMEOUT_MS as v, runGit as x, REBASE_TIMEOUT_MS as y };
1235
+ export { READ_TIMEOUT_MS as C, runGit as E, NETWORK_TIMEOUT_MS as S, createWorktreeGit as T, GIT_AUTH_PATH as _, GIT_WORKTREE_LIFECYCLE_EVENT as a, GIT_REVIEW_PATH as b, isWorktreeLifecycleEvent as c, registryFile as d, readJson as f, invalidRequest as g, PoGitExpectedError as h, GIT_SESSION_CHANGED_EVENT as i, credentialsFile as l, HubUnavailableError as m, createWorktreeRegistry as n, createWorktreeMessageInbox as o, writeJsonAtomic as p, validRefName as r, isSessionChangedEvent as s, createWorktreeOperations as t, poGitRoot as u, GIT_BRANCHES_PATH as v, REBASE_TIMEOUT_MS as w, GIT_SESSIONS_PATH as x, GIT_REVIEW_FILE_PATH as y };
1063
1236
 
1064
- //# sourceMappingURL=worktreeOperations-BSeyxUai.mjs.map
1237
+ //# sourceMappingURL=worktreeOperations-B3E4JY4H.mjs.map