@agimon-ai/po-git 0.0.0-alpha.9 → 0.0.0-alpha.91

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