@agimon-ai/po-git 0.0.0-alpha.12 → 0.0.0-alpha.121

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.
@@ -1,11 +1,15 @@
1
- import { _ as NETWORK_TIMEOUT_MS, a as GIT_WORKTREE_LIFECYCLE_EVENT, b as createWorktreeGit, c as isWorktreeLifecycleEvent, d as registryFile, f as readJson, g as invalidRequest, h as PoGitExpectedError, i as GIT_SESSION_CHANGED_EVENT, l as credentialsFile, m as HubUnavailableError, n as createWorktreeRegistry, o as createWorktreeMessageInbox, p as writeJsonAtomic, r as validRefName, s as isSessionChangedEvent, t as createWorktreeOperations, u as poGitRoot, v as READ_TIMEOUT_MS, x as runGit, y as REBASE_TIMEOUT_MS } from "../worktreeOperations-BSeyxUai.mjs";
2
- import fs from "node:fs";
3
- import os from "node:os";
1
+ import { C as READ_TIMEOUT_MS, E as runGit, S as NETWORK_TIMEOUT_MS, T as createWorktreeGit, a as GIT_WORKTREE_LIFECYCLE_EVENT, c as isWorktreeLifecycleEvent, d as registryFile, f as readJson, g as invalidRequest, h as PoGitExpectedError, i as GIT_SESSION_CHANGED_EVENT, l as credentialsFile, m as HubUnavailableError, n as createWorktreeRegistry, o as createWorktreeMessageInbox, p as writeJsonAtomic, r as validRefName, s as isSessionChangedEvent, t as createWorktreeOperations, u as poGitRoot, w as REBASE_TIMEOUT_MS } from "../worktreeOperations-DA_dbDv8.mjs";
2
+ import fs from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { composeServerProviderOverrides, defineServerPlugin, readPackageResource } from "@agimon-ai/po-core/serverFacet";
6
- import { composeRootHooks, defineChannel, defineHook, defineResource, defineRoot, defineRoute, defineServerTool } from "@agimon-ai/po-core/extensionFile";
4
+ import os from "node:os";
5
+ import { composeServerProviderOverrides, defineServerPlugin, packageResourcePath, readPackageResource } from "@agimon-ai/po-core/serverPlugin";
6
+ import { composeRootHooks, composeRootInjects, defineChannel, defineHook, defineResource, defineRoot, defineRoute, defineServerTool } from "@agimon-ai/po-core/extensionFile";
7
+ import { PO_CODEMODE_SERVICE, readPoCodemode } from "@agimon-ai/po-core/codemode";
7
8
  import { readPoSessionDelivery } from "@agimon-ai/po-session";
8
9
  import { Type } from "typebox";
10
+ import { BACKGROUND_CONTEXT, PO_SHELL_SERVICE, requirePoShell, withAbortSignal } from "@agimon-ai/po-core/platform";
11
+ import { poApiCallerFrom } from "@agimon-ai/po-core/packageApi";
12
+ import { parseLaunchSelection } from "@agimon-ai/po-core/hubChannel";
9
13
  //#region src/schemas/runWorktreeTool.ts
10
14
  /**
11
15
  * The tool's action vocabulary.
@@ -247,7 +251,8 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
247
251
  content: text(describe(record, context.sessionId), `Session: ${record.sessionId}`, dirtyFiles.length === 0 ? "Clean." : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(", ")}`),
248
252
  details: {
249
253
  action: params.action,
250
- records: [record]
254
+ records: [record],
255
+ dirtyFiles
251
256
  }
252
257
  };
253
258
  }
@@ -271,7 +276,10 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
271
276
  const waiting = await operations.messages(context, params.id);
272
277
  return {
273
278
  content: waiting.length === 0 ? text("No messages.") : text(`${String(waiting.length)} message(s):`, ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`)),
274
- details: { action: params.action }
279
+ details: {
280
+ action: params.action,
281
+ messages: waiting
282
+ }
275
283
  };
276
284
  }
277
285
  case WORKTREE_ACTIONS.prune: {
@@ -286,12 +294,172 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
286
294
  `Untracked directories, left alone: ${plan.untracked.length === 0 ? "none" : plan.untracked.join(", ")}`,
287
295
  ...plan.keptBranches.length === 0 ? [] : [`Branches kept, they hold commits: ${plan.keptBranches.join(", ")}`]
288
296
  ]),
289
- details: { action: params.action }
297
+ details: {
298
+ action: params.action,
299
+ plan
300
+ }
290
301
  };
291
302
  }
292
303
  }
293
304
  }
294
305
  //#endregion
306
+ //#region src/services/worktreeNamespace/index.ts
307
+ const DECLARATIONS = `type WorktreeStatus = 'spawning' | 'running' | 'closing' | 'orphaned';
308
+
309
+ interface Worktree {
310
+ id: string;
311
+ branch: string;
312
+ baseRef: string;
313
+ /** Absolute path of the checkout, under ~/.po/git/worktrees. */
314
+ path: string;
315
+ repositoryRoot: string;
316
+ /** The session running inside the worktree. */
317
+ sessionId: string;
318
+ parentSessionId?: string;
319
+ status: WorktreeStatus;
320
+ createdAt: string;
321
+ /** How your session relates to it: 'parent' spawned it, 'child' runs inside it, 'other' is neither. */
322
+ relationship: 'parent' | 'child' | 'other';
323
+ }
324
+
325
+ interface WorktreeMessage {
326
+ worktreeId: string;
327
+ fromSessionId: string;
328
+ from: 'parent' | 'child';
329
+ text: string;
330
+ sentAt: string;
331
+ }
332
+
333
+ interface PrunePlan {
334
+ remove: Worktree[];
335
+ forget: Worktree[];
336
+ /** Kept because they hold uncommitted work. */
337
+ keptDirty: Worktree[];
338
+ untracked: string[];
339
+ /** Branches kept because they hold commits. */
340
+ keptBranches: string[];
341
+ }
342
+
343
+ declare const worktrees: {
344
+ /** Every worktree for this repository. */
345
+ list(): Promise<Worktree[]>;
346
+ /** One worktree and its uncommitted files. */
347
+ status(id: string): Promise<{ worktree: Worktree; dirtyFiles: string[] }>;
348
+ /**
349
+ * Create a worktree on a new branch and start a Po session in it, nested under this one. baseRef defaults to
350
+ * the current branch's remote-tracking ref, then the remote default branch; name labels it in the rail; task is
351
+ * the new session's first message. Setup can be slow, so give exec a larger timeoutMs.
352
+ */
353
+ spawn(request: { branch: string; baseRef?: string; name?: string; task?: string }): Promise<Worktree>;
354
+ /** Stop the worktree's session and delete its checkout. Refused on a dirty tree unless force is true. */
355
+ close(id: string, options?: { force?: boolean }): Promise<Worktree>;
356
+ /** Merge the worktree's branch into this checkout. Refused when this checkout is dirty. */
357
+ merge(id: string, options?: { message?: string }): Promise<Worktree>;
358
+ /** Remove orphaned worktrees. With dryRun, only return the plan. */
359
+ prune(options?: { dryRun?: boolean }): Promise<PrunePlan>;
360
+ /**
361
+ * Message the other side of a worktree. Use the record with relationship 'child' to report to your parent,
362
+ * and 'parent' to reach your own children. 'other' records are not communication edges.
363
+ */
364
+ send(id: string, message: string): Promise<void>;
365
+ /** Messages waiting for you from a worktree. */
366
+ messages(id: string): Promise<WorktreeMessage[]>;
367
+ };`;
368
+ const json = (value) => JSON.parse(JSON.stringify(value));
369
+ /** Git worktrees for code mode: the actions of `run_worktree` as methods, returning records instead of text. */
370
+ function createWorktreeNamespace(options) {
371
+ return {
372
+ name: "worktrees",
373
+ summary: "Git worktrees, each with its own Po session nested under this one: worktrees.list, status, spawn, close, merge, prune, send and messages.",
374
+ declarations: DECLARATIONS,
375
+ bind(run) {
376
+ if (run.sessionId !== options.execution.sessionId) throw new Error("Worktrees namespace belongs to another session.");
377
+ let disposed = false;
378
+ run.onDispose(() => {
379
+ disposed = true;
380
+ });
381
+ const view = (record) => ({
382
+ ...record,
383
+ relationship: record.parentSessionId === options.execution.sessionId ? "parent" : record.sessionId === options.execution.sessionId ? "child" : "other"
384
+ });
385
+ /** One `run_worktree` action through the tool's own code, so the tool and the namespace cannot drift. */
386
+ const act = async (name, action, input) => {
387
+ run.signal.throwIfAborted();
388
+ if (disposed) throw new Error("Worktrees namespace run is disposed.");
389
+ options.assertAvailable();
390
+ const params = validateParams({
391
+ action,
392
+ ...typeof input === "object" && input !== null ? input : {}
393
+ });
394
+ const context = {
395
+ cwd: options.execution.cwd,
396
+ sessionId: options.execution.sessionId
397
+ };
398
+ const started = Date.now();
399
+ try {
400
+ const result = await executeRunWorktreeTool(params, options.operations, context, { signal: run.signal });
401
+ options.assertAvailable();
402
+ run.record?.({
403
+ name: `worktrees.${name}`,
404
+ tool: RUN_WORKTREE_TOOL_NAME,
405
+ args: params,
406
+ ok: true,
407
+ ms: Date.now() - started,
408
+ result
409
+ });
410
+ return result;
411
+ } catch (error) {
412
+ run.record?.({
413
+ name: `worktrees.${name}`,
414
+ tool: RUN_WORKTREE_TOOL_NAME,
415
+ args: params,
416
+ ok: false,
417
+ ms: Date.now() - started,
418
+ error: error instanceof Error ? error.message : String(error)
419
+ });
420
+ throw error;
421
+ }
422
+ };
423
+ const record = (result) => view(result.details.records[0]);
424
+ const members = Object.create(null);
425
+ members.list = async () => json((await act("list", "list", {})).details.records.map(view));
426
+ members.status = async (id) => {
427
+ const { details } = await act("status", "status", { id });
428
+ return json({
429
+ worktree: view(details.records[0]),
430
+ dirtyFiles: details.dirtyFiles ?? []
431
+ });
432
+ };
433
+ members.spawn = async (request) => json(record(await act("spawn", "spawn_worktree", request)));
434
+ members.close = async (id, settings) => json(record(await act("close", "close_worktree", {
435
+ ...settings,
436
+ id
437
+ })));
438
+ members.merge = async (id, settings) => json(record(await act("merge", "merge", {
439
+ ...settings,
440
+ id
441
+ })));
442
+ members.prune = async (settings) => {
443
+ const { plan } = (await act("prune", "prune", settings)).details;
444
+ return json({
445
+ ...plan,
446
+ remove: plan.remove.map(view),
447
+ forget: plan.forget.map(view),
448
+ keptDirty: plan.keptDirty.map(view)
449
+ });
450
+ };
451
+ members.send = async (id, message) => {
452
+ await act("send", "send", {
453
+ id,
454
+ message
455
+ });
456
+ };
457
+ members.messages = async (id) => json((await act("messages", "messages", { id })).details.messages);
458
+ return members;
459
+ }
460
+ };
461
+ }
462
+ //#endregion
295
463
  //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
296
464
  function progressResult(action, label) {
297
465
  return {
@@ -302,9 +470,9 @@ function progressResult(action, label) {
302
470
  details: { action }
303
471
  };
304
472
  }
305
- function createGitSession({ context, host: serverHost }) {
306
- if (serverHost.context.directEvents === void 0) throw new Error("Git headless facet requires the session direct event bus.");
307
- if (serverHost.context.sessionId === void 0) throw new Error("Git headless facet requires a session identity.");
473
+ function createGitSession({ context, host: serverHost, agent, signal }) {
474
+ if (serverHost.context.directEvents === void 0) throw new Error("Git server plugin requires the session direct event bus.");
475
+ if (serverHost.context.sessionId === void 0) throw new Error("Git server plugin requires a session identity.");
308
476
  const sessionId = serverHost.context.sessionId;
309
477
  const directEvents = serverHost.context.directEvents;
310
478
  const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);
@@ -315,20 +483,12 @@ function createGitSession({ context, host: serverHost }) {
315
483
  sessionDelivery: () => readPoSessionDelivery(context)
316
484
  });
317
485
  const resources = [{
318
- when: {
319
- state: { "minor-mode": "help" },
320
- attribution: {
321
- kind: "minor",
322
- mode: "help"
323
- }
324
- },
325
- name: "po-git",
326
- kind: "context",
327
- read: () => readPackageResource(import.meta.url, "llms.txt")
328
- }, {
329
- name: "po-use-git",
486
+ name: "codemode-how-to-git",
487
+ description: "Hand a branch of work to a separate session in its own checkout, keeping the current checkout untouched. Use for a long refactor, a risky migration, or parallel features that touch the same files; not for parallel work on one branch.",
330
488
  kind: "skill",
331
- read: () => readPackageResource(import.meta.url, "src/prompts/po-use-git/SKILL.md")
489
+ pinned: true,
490
+ path: packageResourcePath(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md"),
491
+ read: () => readPackageResource(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md")
332
492
  }];
333
493
  const tool = {
334
494
  name: RUN_WORKTREE_TOOL_NAME,
@@ -348,17 +508,37 @@ function createGitSession({ context, host: serverHost }) {
348
508
  const changed = (_event, execution) => {
349
509
  directEvents.publish(GIT_SESSION_CHANGED_EVENT, execution.sessionId, { version: 1 });
350
510
  };
511
+ const hooks = [{
512
+ event: "tool_execution_end",
513
+ handle: changed
514
+ }, {
515
+ event: "agent_settled",
516
+ handle: changed
517
+ }];
518
+ if (agent) {
519
+ const namespace = createWorktreeNamespace({
520
+ operations,
521
+ execution: agent.context,
522
+ assertAvailable: () => {
523
+ if (signal?.aborted) throw new Error("Git session is unavailable.");
524
+ }
525
+ });
526
+ context.inject([PO_CODEMODE_SERVICE], (scoped) => {
527
+ const codemode = readPoCodemode(scoped);
528
+ if (!codemode) return void 0;
529
+ const registration = codemode.register("@agimon-ai/po-git", namespace);
530
+ const restriction = agent.registerToolRestriction({ excludedTools: [RUN_WORKTREE_TOOL_NAME] });
531
+ return () => {
532
+ restriction.dispose();
533
+ registration.dispose();
534
+ };
535
+ });
536
+ }
351
537
  return {
352
538
  value: {
353
539
  resources,
354
540
  tool,
355
- hooks: [{
356
- event: "tool_execution_end",
357
- handle: changed
358
- }, {
359
- event: "agent_settled",
360
- handle: changed
361
- }]
541
+ hooks
362
542
  },
363
543
  onDispose: () => messageInbox.close()
364
544
  };
@@ -469,11 +649,11 @@ const NAME_STATUS = {
469
649
  T: "modified"
470
650
  };
471
651
  /** Line count of a small regular file, or undefined when it is binary, large, or not a regular file. */
472
- function untrackedLines(root, relPath) {
652
+ async function untrackedLines(root, relPath) {
473
653
  const absolute = path.join(root, relPath);
474
654
  let stat;
475
655
  try {
476
- stat = fs.lstatSync(absolute);
656
+ stat = await fs.lstat(absolute);
477
657
  } catch {
478
658
  return;
479
659
  }
@@ -482,7 +662,7 @@ function untrackedLines(root, relPath) {
482
662
  binary: false
483
663
  };
484
664
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
485
- const content = fs.readFileSync(absolute);
665
+ const content = await fs.readFile(absolute);
486
666
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
487
667
  lines: 0,
488
668
  binary: true
@@ -605,8 +785,8 @@ function createBranchDiff() {
605
785
  "--exclude-standard",
606
786
  "-z"
607
787
  ]));
608
- untracked.forEach((relPath, index) => {
609
- const counted = index < MAX_UNTRACKED_COUNTED ? untrackedLines(root, relPath) : void 0;
788
+ for (const [index, relPath] of untracked.entries()) {
789
+ const counted = index < MAX_UNTRACKED_COUNTED ? await untrackedLines(root, relPath) : void 0;
610
790
  entries.push({
611
791
  path: relPath,
612
792
  status: "untracked",
@@ -614,18 +794,20 @@ function createBranchDiff() {
614
794
  removed: 0,
615
795
  ...counted?.binary ? { binary: true } : {}
616
796
  });
617
- });
797
+ }
618
798
  entries.sort((left, right) => left.path.localeCompare(right.path));
619
799
  const conflicts = [...unmerged];
620
- const rebasing = [await readLine$1(root, [
800
+ const rebaseMerge = await readLine$1(root, [
621
801
  "rev-parse",
622
802
  "--git-path",
623
803
  "rebase-merge"
624
- ]), await readLine$1(root, [
804
+ ]);
805
+ const rebaseApply = await readLine$1(root, [
625
806
  "rev-parse",
626
807
  "--git-path",
627
808
  "rebase-apply"
628
- ])].some((gitPath) => gitPath !== void 0 && fs.existsSync(path.resolve(root, gitPath)));
809
+ ]);
810
+ const rebasing = (await Promise.all([rebaseMerge, rebaseApply].map(async (gitPath) => gitPath === void 0 ? false : fs.access(path.resolve(root, gitPath)).then(() => true, () => false)))).some(Boolean);
629
811
  const upstreamRef = branch === void 0 ? void 0 : await upstreamOf(root, branch);
630
812
  let upstream;
631
813
  if (upstreamRef !== void 0) {
@@ -670,7 +852,7 @@ function createBranchDiff() {
670
852
  if (entry === void 0) throw new Error("That path is not in this review.");
671
853
  if (entry.status === "untracked") {
672
854
  const absolute = path.join(review.root, entry.path);
673
- const stat = fs.lstatSync(absolute);
855
+ const stat = await fs.lstat(absolute);
674
856
  if (stat.isSymbolicLink()) return {
675
857
  path: entry.path,
676
858
  hunks: [{
@@ -678,7 +860,7 @@ function createBranchDiff() {
678
860
  rows: [{
679
861
  marker: "+",
680
862
  line: 1,
681
- content: fs.readlinkSync(absolute)
863
+ content: await fs.readlink(absolute)
682
864
  }]
683
865
  }]
684
866
  };
@@ -687,7 +869,7 @@ function createBranchDiff() {
687
869
  hunks: [],
688
870
  tooLarge: true
689
871
  };
690
- const content = fs.readFileSync(absolute);
872
+ const content = await fs.readFile(absolute);
691
873
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
692
874
  path: entry.path,
693
875
  hunks: [],
@@ -804,9 +986,9 @@ function isRecord(value) {
804
986
  return typeof value === "object" && value !== null && !Array.isArray(value);
805
987
  }
806
988
  /** The workspace key: the canonical root, so a symlinked path and its target share one entry. */
807
- function workspaceKey(workspaceRoot) {
989
+ async function workspaceKey(workspaceRoot) {
808
990
  try {
809
- return fs.realpathSync(workspaceRoot);
991
+ return await fs.realpath(workspaceRoot);
810
992
  } catch {
811
993
  return path.resolve(workspaceRoot);
812
994
  }
@@ -855,7 +1037,7 @@ function publicView(config) {
855
1037
  * the saved one, and only for the same host: a token never follows a host it
856
1038
  * was not entered for.
857
1039
  */
858
- function parseGitAuthSave(request, current, homeDir) {
1040
+ async function parseGitAuthSave(request, current, homeDir) {
859
1041
  if (!isRecord(request)) throw invalidRequest("The git auth request is not an object.", "Send method none, ssh or https.");
860
1042
  if (request.method === "none") return { method: "none" };
861
1043
  if (request.method === "ssh") {
@@ -865,7 +1047,7 @@ function parseGitAuthSave(request, current, homeDir) {
865
1047
  if (!keyPath.startsWith("/") && !keyPath.startsWith("~/")) throw invalidRequest("The key path is not absolute.", "Give the key as an absolute path or one under ~/.");
866
1048
  let isFile = false;
867
1049
  try {
868
- isFile = fs.statSync(expandHome(keyPath, homeDir)).isFile();
1050
+ isFile = (await fs.stat(expandHome(keyPath, homeDir))).isFile();
869
1051
  } catch {
870
1052
  isFile = false;
871
1053
  }
@@ -900,16 +1082,16 @@ function parseGitAuthSave(request, current, homeDir) {
900
1082
  throw invalidRequest("The git auth method is unknown.", "Send method none, ssh or https.");
901
1083
  }
902
1084
  /** Tightens a file or folder something left readable by group or others. */
903
- function tighten(target, mode) {
1085
+ async function tighten(target, mode) {
904
1086
  try {
905
- if ((fs.statSync(target).mode & LOOSE_BITS) !== 0) fs.chmodSync(target, mode);
1087
+ if (((await fs.stat(target)).mode & LOOSE_BITS) !== 0) await fs.chmod(target, mode);
906
1088
  } catch {}
907
1089
  }
908
1090
  function createGitAuthStore(homeDir = os.homedir()) {
909
1091
  const file = credentialsFile(homeDir);
910
- const readFile = () => {
911
- tighten(file, OWNER_ONLY_FILE);
912
- const raw = readJson(file);
1092
+ const readFile = async () => {
1093
+ await tighten(file, OWNER_ONLY_FILE);
1094
+ const raw = await readJson(file);
913
1095
  const workspaces = {};
914
1096
  if (isRecord(raw) && raw.version === 1 && isRecord(raw.workspaces)) for (const [key, value] of Object.entries(raw.workspaces)) {
915
1097
  const config = parseConfig(value);
@@ -920,28 +1102,28 @@ function createGitAuthStore(homeDir = os.homedir()) {
920
1102
  workspaces
921
1103
  };
922
1104
  };
923
- const read = (workspaceRoot) => readFile().workspaces[workspaceKey(workspaceRoot)] ?? { method: "none" };
1105
+ const read = async (workspaceRoot) => (await readFile()).workspaces[await workspaceKey(workspaceRoot)] ?? { method: "none" };
924
1106
  return {
925
1107
  read,
926
- view: (workspaceRoot) => publicView(read(workspaceRoot)),
927
- save(workspaceRoot, request) {
928
- const key = workspaceKey(workspaceRoot);
929
- const current = readFile();
930
- const next = parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
1108
+ view: async (workspaceRoot) => publicView(await read(workspaceRoot)),
1109
+ async save(workspaceRoot, request) {
1110
+ const key = await workspaceKey(workspaceRoot);
1111
+ const current = await readFile();
1112
+ const next = await parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
931
1113
  const root = poGitRoot(homeDir);
932
- fs.mkdirSync(root, {
1114
+ await fs.mkdir(root, {
933
1115
  recursive: true,
934
1116
  mode: OWNER_ONLY_DIRECTORY
935
1117
  });
936
- fs.chmodSync(root, OWNER_ONLY_DIRECTORY);
1118
+ await fs.chmod(root, OWNER_ONLY_DIRECTORY);
937
1119
  const workspaces = { ...current.workspaces };
938
1120
  if (next.method === "none") delete workspaces[key];
939
1121
  else workspaces[key] = next;
940
- writeJsonAtomic(file, {
1122
+ await writeJsonAtomic(file, {
941
1123
  version: 1,
942
1124
  workspaces
943
1125
  });
944
- fs.chmodSync(file, OWNER_ONLY_FILE);
1126
+ await fs.chmod(file, OWNER_ONLY_FILE);
945
1127
  return publicView(next);
946
1128
  }
947
1129
  };
@@ -1060,7 +1242,13 @@ function newSessionRequest(body) {
1060
1242
  if (branch === void 0 || branch === false || !validRefName(branch)) return { error: "A valid branch name is required." };
1061
1243
  const name = optionalText(record.name);
1062
1244
  if (name === false || name !== void 0 && name.length > MAX_NAME_LENGTH) return { error: `The session name must be text of up to ${String(MAX_NAME_LENGTH)} characters.` };
1063
- const named = name === void 0 ? {} : { name };
1245
+ const chosen = record.selection === void 0 ? {} : parseLaunchSelection(record.selection);
1246
+ if ("error" in chosen) return chosen;
1247
+ const profile = chosen.selection?.profile;
1248
+ const named = {
1249
+ ...name === void 0 ? {} : { name },
1250
+ ...profile === void 0 ? {} : { selection: { profile } }
1251
+ };
1064
1252
  if (record.mode === "existing-branch") {
1065
1253
  const remote = optionalText(record.remote);
1066
1254
  if (remote === false || remote !== void 0 && !validRefName(remote)) return { error: "That remote name is not valid." };
@@ -1106,177 +1294,237 @@ function failed(error) {
1106
1294
  }, { status: error.retryable ? 503 : 409 });
1107
1295
  return Response.json({ error: "The worktree operation failed." }, { status: 500 });
1108
1296
  }
1109
- //#endregion
1110
- //#region src/extensions/(backend)/api/git/route.server.ts
1111
- var route_server_default = defineRoute({
1112
- basePath: "git",
1113
- start(context) {
1114
- if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1115
- const directEvents = context.directEvents;
1116
- const publishLifecycle = (sessionId, repositoryRoot) => {
1117
- directEvents.publish(GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1118
- version: 1,
1119
- repositoryRoot
1297
+ function createGitApi(openDirectory) {
1298
+ return {
1299
+ basePath: "git",
1300
+ start(context) {
1301
+ if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1302
+ const directEvents = context.directEvents;
1303
+ const publishLifecycle = (sessionId, repositoryRoot) => {
1304
+ directEvents.publish(GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1305
+ version: 1,
1306
+ repositoryRoot
1307
+ });
1308
+ };
1309
+ const git = createWorktreeGit();
1310
+ const operations = createWorktreeOperations({
1311
+ git,
1312
+ sessionService: context.sessionService,
1313
+ ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1120
1314
  });
1121
- };
1122
- const git = createWorktreeGit();
1123
- const operations = createWorktreeOperations({
1124
- git,
1125
- sessionService: context.sessionService,
1126
- ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1127
- });
1128
- /**
1129
- * The new-session dialog's routes: the workspace's branches, and a top-level
1130
- * worktree session on one. Workspace-mounted only, and always rooted at the
1131
- * mount's own workspace, so a request can never name another directory.
1132
- */
1133
- const newSession = async (request, url) => {
1134
- const isBranches = request.method === "GET" && url.pathname === "/branches";
1135
- const isCreate = request.method === "POST" && url.pathname === "/sessions";
1136
- if (!isBranches && !isCreate) return void 0;
1137
- const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1138
- if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1139
- if (isBranches) {
1140
- const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1141
- const branches = repositoryRoot === void 0 ? {
1142
- repository: false,
1143
- local: [],
1144
- remote: []
1145
- } : {
1146
- repository: true,
1147
- ...await git.listBranches(repositoryRoot)
1315
+ /**
1316
+ * The new-session dialog's routes: the workspace's branches, and a top-level
1317
+ * worktree session on one. Workspace-mounted only, and always rooted at the
1318
+ * mount's own workspace, so a request can never name another directory.
1319
+ */
1320
+ const newSession = async (request, url) => {
1321
+ const isBranches = request.method === "GET" && url.pathname === "/branches";
1322
+ const isCreate = request.method === "POST" && url.pathname === "/sessions";
1323
+ const isDelete = request.method === "DELETE" && url.pathname === "/branches";
1324
+ const isOpen = request.method === "POST" && url.pathname === `/branches/open`;
1325
+ if (!isBranches && !isCreate && !isDelete && !isOpen) return void 0;
1326
+ const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1327
+ if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1328
+ if (isBranches) {
1329
+ const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1330
+ const branches = repositoryRoot === void 0 ? {
1331
+ repository: false,
1332
+ local: [],
1333
+ remote: []
1334
+ } : {
1335
+ repository: true,
1336
+ ...await git.listBranches(repositoryRoot)
1337
+ };
1338
+ return Response.json(branches);
1339
+ }
1340
+ if (isDelete || isOpen) {
1341
+ if (isOpen && poApiCallerFrom(request.headers)?.locality !== "local") return Response.json({ error: "Opening Finder is only available on the host." }, { status: 403 });
1342
+ const body = await request.json().catch(() => void 0);
1343
+ const branch = typeof body === "object" && body !== null ? body.branch : void 0;
1344
+ if (typeof branch !== "string" || !validRefName(branch)) return badRequest("A valid branch name is required.");
1345
+ const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1346
+ if (repositoryRoot === void 0) return badRequest("This workspace is not a git checkout.");
1347
+ const branches = await git.listBranches(repositoryRoot);
1348
+ const local = branches.local.find((item) => item.name === branch);
1349
+ if (local === void 0) return Response.json({ error: "That local branch no longer exists." }, { status: 404 });
1350
+ if (isOpen) {
1351
+ if (local.checkedOutAt === void 0) return Response.json({ error: "This branch has no checked-out folder." }, { status: 409 });
1352
+ if (openDirectory === void 0) return Response.json({ error: "Finder is not available on this host." }, { status: 409 });
1353
+ try {
1354
+ await openDirectory(local.checkedOutAt);
1355
+ return Response.json({ opened: true });
1356
+ } catch {
1357
+ return Response.json({ error: "The branch folder could not be opened in Finder." }, { status: 409 });
1358
+ }
1359
+ }
1360
+ if (local.checkedOutAt !== void 0 || branches.current === branch) return Response.json({ error: "Close this branch’s worktree before deleting it." }, { status: 409 });
1361
+ if (!await git.deleteBranch({
1362
+ repositoryRoot,
1363
+ branch
1364
+ })) return Response.json({ error: "Git refused to delete this branch. It may contain unmerged commits or be checked out." }, { status: 409 });
1365
+ return Response.json({ deleted: true });
1366
+ }
1367
+ const parsed = newSessionRequest(await request.json().catch(() => void 0));
1368
+ if ("error" in parsed) return badRequest(parsed.error);
1369
+ const record = await operations.spawn({ cwd: workspaceRoot }, {
1370
+ branch: parsed.branch,
1371
+ checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1372
+ ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1373
+ ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1374
+ ...parsed.name === void 0 ? {} : { name: parsed.name },
1375
+ ...parsed.selection === void 0 ? {} : { selection: parsed.selection }
1376
+ });
1377
+ publishLifecycle(record.sessionId, record.repositoryRoot);
1378
+ const created = {
1379
+ sessionId: record.sessionId,
1380
+ worktreeId: record.id
1148
1381
  };
1149
- return Response.json(branches);
1150
- }
1151
- const parsed = newSessionRequest(await request.json().catch(() => void 0));
1152
- if ("error" in parsed) return badRequest(parsed.error);
1153
- const record = await operations.spawn({ cwd: workspaceRoot }, {
1154
- branch: parsed.branch,
1155
- checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1156
- ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1157
- ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1158
- ...parsed.name === void 0 ? {} : { name: parsed.name }
1159
- });
1160
- publishLifecycle(record.sessionId, record.repositoryRoot);
1161
- const created = {
1162
- sessionId: record.sessionId,
1163
- worktreeId: record.id
1382
+ return Response.json(created, { status: 201 });
1164
1383
  };
1165
- return Response.json(created, { status: 201 });
1166
- };
1167
- /** The admitted root for a request, or a response explaining why not. */
1168
- const rootOf = (url) => {
1169
- const repositoryId = url.searchParams.get("repositoryId");
1170
- if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1171
- const root = context.resolveRepository?.(repositoryId);
1172
- if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1173
- return root;
1174
- };
1175
- const homeDir = context.homeDirectory ?? os.homedir();
1176
- const gitAuth = createGitAuthStore(homeDir);
1177
- const branchDiff = createBranchDiff();
1178
- const pendingReviews = /* @__PURE__ */ new Map();
1179
- /**
1180
- * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1181
- * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1182
- * The view never carries the token.
1183
- */
1184
- const auth = async (request, url) => {
1185
- if (url.pathname !== "/auth") return void 0;
1186
- if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1187
- if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1188
- const root = rootOf(url);
1189
- if (root instanceof Response) return root;
1190
- if (request.method === "GET") return Response.json(gitAuth.view(root));
1191
- try {
1192
- return Response.json(gitAuth.save(root, await request.json().catch(() => void 0)));
1193
- } catch (error) {
1194
- if (error instanceof PoGitExpectedError && error.code === "invalid_request") {
1195
- const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1196
- return Response.json({
1197
- error: text,
1198
- code: error.code
1199
- }, { status: 400 });
1200
- }
1201
- throw error;
1202
- }
1203
- };
1204
- /**
1205
- * The session's change against its base. Session mounts only, rooted at the
1206
- * session's own checkout; a file is served only when it is in the change set
1207
- * computed for the request. Overlapping requests share an in-flight snapshot.
1208
- */
1209
- const review = async (request, url) => {
1210
- if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1211
- const cwd = context.scope === "session" ? context.cwd : void 0;
1212
- if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1213
- if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1214
- let recordedBaseRef;
1215
- try {
1216
- recordedBaseRef = createWorktreeRegistry(registryFile(cwd, homeDir)).list().find((record) => record.sessionId === context.sessionId)?.baseRef;
1217
- } catch {
1218
- recordedBaseRef = void 0;
1219
- }
1220
- const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1221
- let pending = pendingReviews.get(key);
1222
- if (pending === void 0) {
1223
- pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1224
- pendingReviews.set(key, pending);
1225
- }
1226
- const result = await pending;
1227
- if (url.pathname === "/review") return Response.json(result?.summary ?? {
1228
- repository: false,
1229
- files: []
1230
- });
1231
- const filePath = url.searchParams.get("path") ?? "";
1232
- if (result === void 0 || !result.summary.files.some((file) => file.path === filePath)) return Response.json({ error: "That file is not part of this review." }, { status: 404 });
1233
- return Response.json(await branchDiff.fileDiff(result, filePath));
1234
- };
1235
- return {
1236
- async fetch(request) {
1237
- const url = new URL(request.url);
1384
+ /** The admitted root for a request, or a response explaining why not. */
1385
+ const rootOf = (url) => {
1386
+ const repositoryId = url.searchParams.get("repositoryId");
1387
+ if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1388
+ const root = context.resolveRepository?.(repositoryId);
1389
+ if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1390
+ return root;
1391
+ };
1392
+ const homeDir = context.homeDirectory ?? context.environment?.HOME;
1393
+ const gitAuth = homeDir === void 0 ? void 0 : createGitAuthStore(homeDir);
1394
+ const branchDiff = createBranchDiff();
1395
+ const pendingReviews = /* @__PURE__ */ new Map();
1396
+ /**
1397
+ * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1398
+ * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1399
+ * The view never carries the token.
1400
+ */
1401
+ const auth = async (request, url) => {
1402
+ if (url.pathname !== "/auth") return void 0;
1403
+ if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1404
+ if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1405
+ if (gitAuth === void 0) throw new Error("Git auth requires the admitted host home directory.");
1406
+ const root = rootOf(url);
1407
+ if (root instanceof Response) return root;
1408
+ if (request.method === "GET") return Response.json(await gitAuth.view(root));
1238
1409
  try {
1239
- const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1240
- if (answered !== void 0) return answered;
1410
+ return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
1241
1411
  } catch (error) {
1242
- context.onNotice(`git request failed: ${error.name}`);
1243
- return failed(error);
1412
+ if (error instanceof PoGitExpectedError && error.code === "invalid_request") {
1413
+ const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1414
+ return Response.json({
1415
+ error: text,
1416
+ code: error.code
1417
+ }, { status: 400 });
1418
+ }
1419
+ throw error;
1244
1420
  }
1245
- const root = rootOf(url);
1246
- if (root instanceof Response) return root;
1247
- const callerContext = {
1248
- cwd: root,
1249
- sessionId: url.searchParams.get("sessionId") ?? ""
1250
- };
1251
- if ((request.method === "POST" && url.pathname === "/worktrees" || request.method === "DELETE" && /^\/worktrees\/[^/]+$/u.test(url.pathname)) && callerContext.sessionId === "") return badRequest("A sessionId is required for worktree mutations.");
1421
+ };
1422
+ /**
1423
+ * The session's change against its base. Session mounts only, rooted at the
1424
+ * session's own checkout; a file is served only when it is in the change set
1425
+ * computed for the request. Overlapping requests share an in-flight snapshot.
1426
+ */
1427
+ const review = async (request, url) => {
1428
+ if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1429
+ const cwd = context.scope === "session" ? context.cwd : void 0;
1430
+ if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1431
+ if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1432
+ if (homeDir === void 0) throw new Error("Git review requires the admitted host home directory.");
1433
+ let recordedBaseRef;
1252
1434
  try {
1253
- if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1254
- if (request.method === "POST" && url.pathname === "/worktrees") {
1255
- const body = await request.json();
1256
- if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1257
- const record = await operations.spawn(callerContext, {
1258
- branch: body.branch,
1259
- ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1260
- ...body.name === void 0 ? {} : { name: body.name }
1261
- });
1262
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1263
- return Response.json({ worktree: record }, { status: 201 });
1435
+ recordedBaseRef = (await createWorktreeRegistry(await registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
1436
+ } catch {
1437
+ recordedBaseRef = void 0;
1438
+ }
1439
+ const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1440
+ let pending = pendingReviews.get(key);
1441
+ if (pending === void 0) {
1442
+ pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1443
+ pendingReviews.set(key, pending);
1444
+ }
1445
+ const result = await pending;
1446
+ if (url.pathname === "/review") return Response.json(result?.summary ?? {
1447
+ repository: false,
1448
+ files: []
1449
+ });
1450
+ const filePath = url.searchParams.get("path") ?? "";
1451
+ if (result === void 0 || !result.summary.files.some((file) => file.path === filePath)) return Response.json({ error: "That file is not part of this review." }, { status: 404 });
1452
+ return Response.json(await branchDiff.fileDiff(result, filePath));
1453
+ };
1454
+ return {
1455
+ async fetch(request) {
1456
+ const url = new URL(request.url);
1457
+ try {
1458
+ const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1459
+ if (answered !== void 0) return answered;
1460
+ } catch (error) {
1461
+ context.onNotice(`git request failed: ${error.name}`);
1462
+ return failed(error);
1264
1463
  }
1265
- const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1266
- if (request.method === "DELETE" && closing) {
1267
- const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1268
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1269
- return Response.json({ worktree: record });
1464
+ const root = rootOf(url);
1465
+ if (root instanceof Response) return root;
1466
+ const callerContext = {
1467
+ cwd: root,
1468
+ sessionId: url.searchParams.get("sessionId") ?? ""
1469
+ };
1470
+ if ((request.method === "POST" && url.pathname === "/worktrees" || request.method === "DELETE" && /^\/worktrees\/[^/]+$/u.test(url.pathname)) && callerContext.sessionId === "") return badRequest("A sessionId is required for worktree mutations.");
1471
+ try {
1472
+ if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1473
+ if (request.method === "POST" && url.pathname === "/worktrees") {
1474
+ const body = await request.json();
1475
+ if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1476
+ const record = await operations.spawn(callerContext, {
1477
+ branch: body.branch,
1478
+ ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1479
+ ...body.name === void 0 ? {} : { name: body.name }
1480
+ });
1481
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1482
+ return Response.json({ worktree: record }, { status: 201 });
1483
+ }
1484
+ const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1485
+ if (request.method === "DELETE" && closing) {
1486
+ const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1487
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1488
+ return Response.json({ worktree: record });
1489
+ }
1490
+ return Response.json({ error: "No such worktree route." }, { status: 404 });
1491
+ } catch (error) {
1492
+ context.onNotice(`worktree request failed: ${error.name}`);
1493
+ return failed(error);
1270
1494
  }
1271
- return Response.json({ error: "No such worktree route." }, { status: 404 });
1272
- } catch (error) {
1273
- context.onNotice(`worktree request failed: ${error.name}`);
1274
- return failed(error);
1275
- }
1276
- },
1277
- close() {}
1495
+ },
1496
+ close() {}
1497
+ };
1498
+ }
1499
+ };
1500
+ }
1501
+ //#endregion
1502
+ //#region src/extensions/(backend)/api/git/route.server.ts
1503
+ var route_server_default = defineRoute(({ context, host, signal }) => {
1504
+ let openDirectory;
1505
+ context.inject([PO_SHELL_SERVICE], (child) => {
1506
+ const shell = requirePoShell(child);
1507
+ const env = Object.fromEntries(Object.entries(host.context.environment ?? {}).filter((entry) => entry[1] !== void 0));
1508
+ openDirectory = async (path) => {
1509
+ const result = await shell.exec([
1510
+ "/usr/bin/open",
1511
+ "-a",
1512
+ "Finder",
1513
+ path
1514
+ ], {
1515
+ env,
1516
+ inheritEnv: false
1517
+ }, withAbortSignal(AbortSignal.any([signal, AbortSignal.timeout(5e3)]), BACKGROUND_CONTEXT));
1518
+ if (!result.ok || result.value.exitCode !== 0) throw new Error("Finder could not be opened.");
1278
1519
  };
1279
- }
1520
+ return () => {
1521
+ openDirectory = void 0;
1522
+ };
1523
+ });
1524
+ return createGitApi(async (path) => {
1525
+ if (openDirectory === void 0) throw new Error("The host shell is unavailable.");
1526
+ await openDirectory(path);
1527
+ });
1280
1528
  });
1281
1529
  //#endregion
1282
1530
  //#region src/services/gitFailure/index.ts
@@ -1413,7 +1661,7 @@ async function rebasing(root) {
1413
1661
  "--git-path",
1414
1662
  name
1415
1663
  ]);
1416
- if (gitPath !== void 0 && fs.existsSync(path.resolve(root, gitPath))) return true;
1664
+ if (gitPath !== void 0 && await fs.access(path.resolve(root, gitPath)).then(() => true, () => false)) return true;
1417
1665
  }
1418
1666
  return false;
1419
1667
  }
@@ -1583,9 +1831,9 @@ const GIT_CHANGES_TYPE = "git_changes";
1583
1831
  * DESIGN PATTERNS:
1584
1832
  * - Apart from the worktrees channel. Worktrees are repository management;
1585
1833
  * this is the session's own code change.
1586
- * - Push, not poll. The session facet publishes a "changed" event after every
1587
- * tool call and settled turn; recounts are coalesced to one in flight plus
1588
- * one rerun, so a burst of tool calls costs at most two.
1834
+ * - Push, not poll. The session's server plugin publishes a "changed" event
1835
+ * after every tool call and settled turn; recounts are coalesced to one in
1836
+ * flight plus one rerun, so a burst of tool calls costs at most two.
1589
1837
  * - One sync command per session. A rebase and a push must never overlap.
1590
1838
  * - Auth is read fresh for each command from the session's workspace, and goes
1591
1839
  * only into that command's git child.
@@ -1646,9 +1894,9 @@ function createGitChangesChannel(options = {}) {
1646
1894
  const rerun = /* @__PURE__ */ new Set();
1647
1895
  const busy = /* @__PURE__ */ new Set();
1648
1896
  /** The base the session's own worktree was created from, when it has one. */
1649
- const recordedBaseRef = (scope) => {
1897
+ const recordedBaseRef = async (scope) => {
1650
1898
  try {
1651
- return createWorktreeRegistry(registryFile(scope.cwd, homeDir)).list().find((record) => record.sessionId === scope.sessionId)?.baseRef;
1899
+ return (await createWorktreeRegistry(await registryFile(scope.cwd, homeDir)).list()).find((record) => record.sessionId === scope.sessionId)?.baseRef;
1652
1900
  } catch {
1653
1901
  return;
1654
1902
  }
@@ -1681,8 +1929,7 @@ function createGitChangesChannel(options = {}) {
1681
1929
  return;
1682
1930
  }
1683
1931
  inFlight.add(sessionId);
1684
- const base = recordedBaseRef(scope);
1685
- diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base }).then((review) => {
1932
+ recordedBaseRef(scope).then((base) => diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base })).then((review) => {
1686
1933
  if (scopes.has(sessionId)) update(sessionId, { changes: review?.changes });
1687
1934
  }).catch((error) => {
1688
1935
  host.onNotice(`git diff refresh failed for ${sessionId} (${error instanceof Error ? error.name : "unknown"})`);
@@ -1691,12 +1938,12 @@ function createGitChangesChannel(options = {}) {
1691
1938
  if (rerun.delete(sessionId)) refresh(sessionId);
1692
1939
  });
1693
1940
  };
1694
- const run = (scope, command) => {
1941
+ const run = async (scope, command) => {
1695
1942
  const workspaceRoot = scope.sessionContext?.workspaceRoot ?? scope.cwd;
1696
- const base = recordedBaseRef(scope);
1943
+ const base = await recordedBaseRef(scope);
1697
1944
  const context = {
1698
1945
  cwd: scope.cwd,
1699
- auth: auth.read(workspaceRoot),
1946
+ auth: await auth.read(workspaceRoot),
1700
1947
  ...base === void 0 ? {} : { recordedBaseRef: base }
1701
1948
  };
1702
1949
  if (command.action === "pull") return sync.pull(context);
@@ -1794,9 +2041,9 @@ function payloadOf(state) {
1794
2041
  };
1795
2042
  }
1796
2043
  /** Worktrees visible to this session, using the hub's live session table for ownership. */
1797
- function viewsFor(scope, deps) {
2044
+ async function viewsFor(scope, deps) {
1798
2045
  try {
1799
- return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir)).list().flatMap((record) => {
2046
+ return (await createWorktreeRegistry(await registryFile(scope.cwd, deps.homeDir)).list()).flatMap((record) => {
1800
2047
  if (record.parentSessionId === void 0 && record.sessionId !== scope.sessionId) return [];
1801
2048
  const owned = record.parentSessionId === void 0 || record.parentSessionId === scope.sessionId;
1802
2049
  const unowned = !owned && !deps.sessionService.isLive(record.parentSessionId);
@@ -1821,11 +2068,15 @@ function sameList(left, right) {
1821
2068
  return other !== void 0 && entry.id === other.id && entry.branch === other.branch && entry.sessionId === other.sessionId && entry.orphaned === other.orphaned && entry.unowned === other.unowned;
1822
2069
  });
1823
2070
  }
1824
- function errorTargetFor(command) {
1825
- return command.action === "create" ? { action: "create" } : {
2071
+ function errorTargetFor(command, error) {
2072
+ if (command.action === "close") return {
1826
2073
  action: "close",
1827
2074
  id: command.id
1828
2075
  };
2076
+ return error instanceof PoGitExpectedError && error.code === "rift_missing" ? {
2077
+ action: "create",
2078
+ riftMissing: true
2079
+ } : { action: "create" };
1829
2080
  }
1830
2081
  /** A command the page sent, or undefined when the frame was not one. */
1831
2082
  function parseCommand(payload) {
@@ -1853,11 +2104,15 @@ function parseCommand(payload) {
1853
2104
  /**
1854
2105
  * What the reader is told when a command fails.
1855
2106
  *
1856
- * An expected failure explains itself. Anything else is summarised, never
1857
- * echoed: a git error can carry a remote URL with a token in it.
2107
+ * An expected failure explains itself, minus the `[code]` tag and recovery line
2108
+ * written for the agent's tool result. A dirty close is said in the person's
2109
+ * terms, because its message tells the agent to pass `force`. Anything else is
2110
+ * summarised, never echoed: a git error can carry a remote URL with a token in it.
1858
2111
  */
1859
2112
  function failureText(error) {
1860
- return error instanceof PoGitExpectedError ? error.message : "The worktree operation failed.";
2113
+ if (!(error instanceof PoGitExpectedError)) return "The worktree operation failed.";
2114
+ if (error.code === "worktree_dirty") return "The worktree has uncommitted changes.";
2115
+ return error.message.replace(/^\[[a-z_]+\] /u, "").split("\nRecovery:")[0] ?? error.message;
1861
2116
  }
1862
2117
  /** Starts one direct-event subscription per managed session. */
1863
2118
  function createWorktreesChannel(options = {}) {
@@ -1910,10 +2165,13 @@ function createWorktreesChannel(options = {}) {
1910
2165
  return generation;
1911
2166
  };
1912
2167
  /** Republishes a session, skipping a frame that would say nothing new. */
1913
- const publish = (scope, force) => {
2168
+ const publish = async (scope, force) => {
2169
+ const generation = generations.get(scope.sessionId);
2170
+ const worktreeViews = await viewsFor(scope, views);
2171
+ if (generation === void 0 || generations.get(scope.sessionId) !== generation) return;
1914
2172
  const previous = latest.get(scope.sessionId);
1915
2173
  const next = {
1916
- worktrees: viewsFor(scope, views),
2174
+ worktrees: worktreeViews,
1917
2175
  pending: previous?.pending,
1918
2176
  error: previous?.error,
1919
2177
  errorTarget: previous?.errorTarget
@@ -1925,7 +2183,7 @@ function createWorktreesChannel(options = {}) {
1925
2183
  const mark = (scope, pending, error, errorTarget) => {
1926
2184
  const previous = latest.get(scope.sessionId);
1927
2185
  latest.set(scope.sessionId, {
1928
- worktrees: previous?.worktrees ?? viewsFor(scope, views),
2186
+ worktrees: previous?.worktrees ?? [],
1929
2187
  pending,
1930
2188
  error,
1931
2189
  errorTarget
@@ -1965,15 +2223,15 @@ function createWorktreesChannel(options = {}) {
1965
2223
  sessionAdded(scope) {
1966
2224
  newGeneration(scope.sessionId);
1967
2225
  publish(scope, false);
1968
- const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, (payload) => {
2226
+ const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, async (payload) => {
1969
2227
  if (!isWorktreeLifecycleEvent(payload)) return;
1970
2228
  let sameRepository = false;
1971
2229
  try {
1972
- sameRepository = registryFile(scope.cwd, options.homeDir) === registryFile(payload.repositoryRoot, options.homeDir);
2230
+ sameRepository = await registryFile(scope.cwd, options.homeDir) === await registryFile(payload.repositoryRoot, options.homeDir);
1973
2231
  } catch {
1974
2232
  sameRepository = false;
1975
2233
  }
1976
- if (sameRepository) publish(scope, false);
2234
+ if (sameRepository) await publish(scope, false);
1977
2235
  });
1978
2236
  subscriptions.set(scope.sessionId, unsubscribe);
1979
2237
  },
@@ -2006,7 +2264,7 @@ function createWorktreesChannel(options = {}) {
2006
2264
  }).catch((error) => {
2007
2265
  if (generations.get(scope.sessionId) !== generation) return;
2008
2266
  host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);
2009
- mark(scope, void 0, failureText(error), errorTargetFor(command));
2267
+ mark(scope, void 0, failureText(error), errorTargetFor(command, error));
2010
2268
  }).finally(() => {
2011
2269
  if (generations.get(scope.sessionId) === generation) busy.delete(scope.sessionId);
2012
2270
  });
@@ -2028,20 +2286,8 @@ var agent_settled_server_default = defineHook((context) => context.root.hooks[1]
2028
2286
  //#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts
2029
2287
  var tool_execution_end_server_default = defineHook((context) => context.root.hooks[0]);
2030
2288
  //#endregion
2031
- //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGit.server.ts
2032
- var poGit_server_default$1 = (context) => context.root.resources[0];
2033
- //#endregion
2034
- //#region src/extensions/workspaces/sessions/(backend)/resource/poGit.server.ts
2035
- var poGit_server_default = defineResource(poGit_server_default$1);
2036
- //#endregion
2037
- //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGitReadme.server.ts
2038
- var poGitReadme_server_default$1 = (context) => context.root.resources[2];
2039
- //#endregion
2040
- //#region src/extensions/workspaces/sessions/(backend)/resource/poGitReadme.server.ts
2041
- var poGitReadme_server_default = defineResource(poGitReadme_server_default$1);
2042
- //#endregion
2043
2289
  //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poUseGit.server.ts
2044
- var poUseGit_server_default$1 = (context) => context.root.resources[1];
2290
+ var poUseGit_server_default$1 = (context) => context.root.resources[0];
2045
2291
  //#endregion
2046
2292
  //#region src/extensions/workspaces/sessions/(backend)/resource/poUseGit.server.ts
2047
2293
  var poUseGit_server_default = defineResource(poUseGit_server_default$1);
@@ -2051,19 +2297,22 @@ var runWorktree_server_default = defineServerTool((context) => context.root.tool
2051
2297
  //#endregion
2052
2298
  //#region generated/server.ts
2053
2299
  const at = (value, context) => typeof value === "function" ? value(context) : value;
2300
+ /* v8 ignore next -- generated normalization branch */
2301
+ const defined = (values) => values.filter((value) => value !== void 0);
2054
2302
  const via = (identity, value) => value === void 0 ? void 0 : {
2055
2303
  ...identity,
2056
2304
  ...value
2057
2305
  };
2058
- const facet = defineServerPlugin({
2306
+ const serverPlugin = defineServerPlugin({
2059
2307
  name: "@agimon-ai/po-git",
2060
2308
  providerOverrides: composeServerProviderOverrides(root_server_default),
2309
+ injects: composeRootInjects({ session: [root_server_default] }),
2061
2310
  global: (context) => ({
2062
2311
  get api() {
2063
2312
  return [at(route_server_default, context)];
2064
2313
  },
2065
2314
  get channels() {
2066
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2315
+ return [() => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))];
2067
2316
  }
2068
2317
  }),
2069
2318
  workspace: (context) => ({
@@ -2071,11 +2320,11 @@ const facet = defineServerPlugin({
2071
2320
  return [at(route_server_default, context)];
2072
2321
  },
2073
2322
  get channels() {
2074
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2323
+ return defined([context.hosted("global") ? void 0 : () => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), context.hosted("global") ? void 0 : () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))]);
2075
2324
  }
2076
2325
  }),
2077
2326
  session: async (context) => {
2078
- const scopeSession = await root_server_default(context);
2327
+ const scopeSession = await context.scopeRoot("session", root_server_default, context);
2079
2328
  const contextSession = {
2080
2329
  ...context,
2081
2330
  root: scopeSession.value
@@ -2086,17 +2335,13 @@ const facet = defineServerPlugin({
2086
2335
  return [at(route_server_default, context)];
2087
2336
  },
2088
2337
  get channels() {
2089
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2338
+ return defined([context.hosted("global") ? void 0 : () => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), context.hosted("global") ? void 0 : () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))]);
2090
2339
  },
2091
2340
  get hooks() {
2092
2341
  return [via({ event: "agent_settled" }, at(agent_settled_server_default, contextSession)), via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession))];
2093
2342
  },
2094
2343
  get resources() {
2095
- return [
2096
- at(poGit_server_default, contextSession),
2097
- at(poGitReadme_server_default, contextSession),
2098
- at(poUseGit_server_default, contextSession)
2099
- ];
2344
+ return [at(poUseGit_server_default, contextSession)];
2100
2345
  },
2101
2346
  get tools() {
2102
2347
  return [via({ name: "run_worktree" }, at(runWorktree_server_default, contextSession))];
@@ -2109,6 +2354,6 @@ const facet = defineServerPlugin({
2109
2354
  }
2110
2355
  });
2111
2356
  //#endregion
2112
- export { facet as default, facet };
2357
+ export { serverPlugin as default, serverPlugin };
2113
2358
 
2114
2359
  //# sourceMappingURL=server.mjs.map