@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.
@@ -2,17 +2,21 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_worktreeOperations = require("../worktreeOperations-DGUbUnYU.cjs");
6
- let node_fs = require("node:fs");
7
- node_fs = require_worktreeOperations.__toESM(node_fs, 1);
8
- let node_os = require("node:os");
9
- node_os = require_worktreeOperations.__toESM(node_os, 1);
5
+ const require_worktreeOperations = require("../worktreeOperations-0Jrjcbg7.cjs");
6
+ let node_fs_promises = require("node:fs/promises");
7
+ node_fs_promises = require_worktreeOperations.__toESM(node_fs_promises, 1);
10
8
  let node_path = require("node:path");
11
9
  node_path = require_worktreeOperations.__toESM(node_path, 1);
12
- let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
10
+ let node_os = require("node:os");
11
+ node_os = require_worktreeOperations.__toESM(node_os, 1);
12
+ let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
13
13
  let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
14
+ let _agimon_ai_po_core_codemode = require("@agimon-ai/po-core/codemode");
14
15
  let _agimon_ai_po_session = require("@agimon-ai/po-session");
15
16
  let typebox = require("typebox");
17
+ let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
18
+ let _agimon_ai_po_core_packageApi = require("@agimon-ai/po-core/packageApi");
19
+ let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
16
20
  //#region src/schemas/runWorktreeTool.ts
17
21
  /**
18
22
  * The tool's action vocabulary.
@@ -254,7 +258,8 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
254
258
  content: text(describe(record, context.sessionId), `Session: ${record.sessionId}`, dirtyFiles.length === 0 ? "Clean." : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(", ")}`),
255
259
  details: {
256
260
  action: params.action,
257
- records: [record]
261
+ records: [record],
262
+ dirtyFiles
258
263
  }
259
264
  };
260
265
  }
@@ -278,7 +283,10 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
278
283
  const waiting = await operations.messages(context, params.id);
279
284
  return {
280
285
  content: waiting.length === 0 ? text("No messages.") : text(`${String(waiting.length)} message(s):`, ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`)),
281
- details: { action: params.action }
286
+ details: {
287
+ action: params.action,
288
+ messages: waiting
289
+ }
282
290
  };
283
291
  }
284
292
  case WORKTREE_ACTIONS.prune: {
@@ -293,12 +301,172 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
293
301
  `Untracked directories, left alone: ${plan.untracked.length === 0 ? "none" : plan.untracked.join(", ")}`,
294
302
  ...plan.keptBranches.length === 0 ? [] : [`Branches kept, they hold commits: ${plan.keptBranches.join(", ")}`]
295
303
  ]),
296
- details: { action: params.action }
304
+ details: {
305
+ action: params.action,
306
+ plan
307
+ }
297
308
  };
298
309
  }
299
310
  }
300
311
  }
301
312
  //#endregion
313
+ //#region src/services/worktreeNamespace/index.ts
314
+ const DECLARATIONS = `type WorktreeStatus = 'spawning' | 'running' | 'closing' | 'orphaned';
315
+
316
+ interface Worktree {
317
+ id: string;
318
+ branch: string;
319
+ baseRef: string;
320
+ /** Absolute path of the checkout, under ~/.po/git/worktrees. */
321
+ path: string;
322
+ repositoryRoot: string;
323
+ /** The session running inside the worktree. */
324
+ sessionId: string;
325
+ parentSessionId?: string;
326
+ status: WorktreeStatus;
327
+ createdAt: string;
328
+ /** How your session relates to it: 'parent' spawned it, 'child' runs inside it, 'other' is neither. */
329
+ relationship: 'parent' | 'child' | 'other';
330
+ }
331
+
332
+ interface WorktreeMessage {
333
+ worktreeId: string;
334
+ fromSessionId: string;
335
+ from: 'parent' | 'child';
336
+ text: string;
337
+ sentAt: string;
338
+ }
339
+
340
+ interface PrunePlan {
341
+ remove: Worktree[];
342
+ forget: Worktree[];
343
+ /** Kept because they hold uncommitted work. */
344
+ keptDirty: Worktree[];
345
+ untracked: string[];
346
+ /** Branches kept because they hold commits. */
347
+ keptBranches: string[];
348
+ }
349
+
350
+ declare const worktrees: {
351
+ /** Every worktree for this repository. */
352
+ list(): Promise<Worktree[]>;
353
+ /** One worktree and its uncommitted files. */
354
+ status(id: string): Promise<{ worktree: Worktree; dirtyFiles: string[] }>;
355
+ /**
356
+ * Create a worktree on a new branch and start a Po session in it, nested under this one. baseRef defaults to
357
+ * the current branch's remote-tracking ref, then the remote default branch; name labels it in the rail; task is
358
+ * the new session's first message. Setup can be slow, so give exec a larger timeoutMs.
359
+ */
360
+ spawn(request: { branch: string; baseRef?: string; name?: string; task?: string }): Promise<Worktree>;
361
+ /** Stop the worktree's session and delete its checkout. Refused on a dirty tree unless force is true. */
362
+ close(id: string, options?: { force?: boolean }): Promise<Worktree>;
363
+ /** Merge the worktree's branch into this checkout. Refused when this checkout is dirty. */
364
+ merge(id: string, options?: { message?: string }): Promise<Worktree>;
365
+ /** Remove orphaned worktrees. With dryRun, only return the plan. */
366
+ prune(options?: { dryRun?: boolean }): Promise<PrunePlan>;
367
+ /**
368
+ * Message the other side of a worktree. Use the record with relationship 'child' to report to your parent,
369
+ * and 'parent' to reach your own children. 'other' records are not communication edges.
370
+ */
371
+ send(id: string, message: string): Promise<void>;
372
+ /** Messages waiting for you from a worktree. */
373
+ messages(id: string): Promise<WorktreeMessage[]>;
374
+ };`;
375
+ const json = (value) => JSON.parse(JSON.stringify(value));
376
+ /** Git worktrees for code mode: the actions of `run_worktree` as methods, returning records instead of text. */
377
+ function createWorktreeNamespace(options) {
378
+ return {
379
+ name: "worktrees",
380
+ summary: "Git worktrees, each with its own Po session nested under this one: worktrees.list, status, spawn, close, merge, prune, send and messages.",
381
+ declarations: DECLARATIONS,
382
+ bind(run) {
383
+ if (run.sessionId !== options.execution.sessionId) throw new Error("Worktrees namespace belongs to another session.");
384
+ let disposed = false;
385
+ run.onDispose(() => {
386
+ disposed = true;
387
+ });
388
+ const view = (record) => ({
389
+ ...record,
390
+ relationship: record.parentSessionId === options.execution.sessionId ? "parent" : record.sessionId === options.execution.sessionId ? "child" : "other"
391
+ });
392
+ /** One `run_worktree` action through the tool's own code, so the tool and the namespace cannot drift. */
393
+ const act = async (name, action, input) => {
394
+ run.signal.throwIfAborted();
395
+ if (disposed) throw new Error("Worktrees namespace run is disposed.");
396
+ options.assertAvailable();
397
+ const params = validateParams({
398
+ action,
399
+ ...typeof input === "object" && input !== null ? input : {}
400
+ });
401
+ const context = {
402
+ cwd: options.execution.cwd,
403
+ sessionId: options.execution.sessionId
404
+ };
405
+ const started = Date.now();
406
+ try {
407
+ const result = await executeRunWorktreeTool(params, options.operations, context, { signal: run.signal });
408
+ options.assertAvailable();
409
+ run.record?.({
410
+ name: `worktrees.${name}`,
411
+ tool: RUN_WORKTREE_TOOL_NAME,
412
+ args: params,
413
+ ok: true,
414
+ ms: Date.now() - started,
415
+ result
416
+ });
417
+ return result;
418
+ } catch (error) {
419
+ run.record?.({
420
+ name: `worktrees.${name}`,
421
+ tool: RUN_WORKTREE_TOOL_NAME,
422
+ args: params,
423
+ ok: false,
424
+ ms: Date.now() - started,
425
+ error: error instanceof Error ? error.message : String(error)
426
+ });
427
+ throw error;
428
+ }
429
+ };
430
+ const record = (result) => view(result.details.records[0]);
431
+ const members = Object.create(null);
432
+ members.list = async () => json((await act("list", "list", {})).details.records.map(view));
433
+ members.status = async (id) => {
434
+ const { details } = await act("status", "status", { id });
435
+ return json({
436
+ worktree: view(details.records[0]),
437
+ dirtyFiles: details.dirtyFiles ?? []
438
+ });
439
+ };
440
+ members.spawn = async (request) => json(record(await act("spawn", "spawn_worktree", request)));
441
+ members.close = async (id, settings) => json(record(await act("close", "close_worktree", {
442
+ ...settings,
443
+ id
444
+ })));
445
+ members.merge = async (id, settings) => json(record(await act("merge", "merge", {
446
+ ...settings,
447
+ id
448
+ })));
449
+ members.prune = async (settings) => {
450
+ const { plan } = (await act("prune", "prune", settings)).details;
451
+ return json({
452
+ ...plan,
453
+ remove: plan.remove.map(view),
454
+ forget: plan.forget.map(view),
455
+ keptDirty: plan.keptDirty.map(view)
456
+ });
457
+ };
458
+ members.send = async (id, message) => {
459
+ await act("send", "send", {
460
+ id,
461
+ message
462
+ });
463
+ };
464
+ members.messages = async (id) => json((await act("messages", "messages", { id })).details.messages);
465
+ return members;
466
+ }
467
+ };
468
+ }
469
+ //#endregion
302
470
  //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
303
471
  function progressResult(action, label) {
304
472
  return {
@@ -309,9 +477,9 @@ function progressResult(action, label) {
309
477
  details: { action }
310
478
  };
311
479
  }
312
- function createGitSession({ context, host: serverHost }) {
313
- if (serverHost.context.directEvents === void 0) throw new Error("Git headless facet requires the session direct event bus.");
314
- if (serverHost.context.sessionId === void 0) throw new Error("Git headless facet requires a session identity.");
480
+ function createGitSession({ context, host: serverHost, agent, signal }) {
481
+ if (serverHost.context.directEvents === void 0) throw new Error("Git server plugin requires the session direct event bus.");
482
+ if (serverHost.context.sessionId === void 0) throw new Error("Git server plugin requires a session identity.");
315
483
  const sessionId = serverHost.context.sessionId;
316
484
  const directEvents = serverHost.context.directEvents;
317
485
  const messageInbox = require_worktreeOperations.createWorktreeMessageInbox(directEvents, sessionId);
@@ -322,20 +490,12 @@ function createGitSession({ context, host: serverHost }) {
322
490
  sessionDelivery: () => (0, _agimon_ai_po_session.readPoSessionDelivery)(context)
323
491
  });
324
492
  const resources = [{
325
- when: {
326
- state: { "minor-mode": "help" },
327
- attribution: {
328
- kind: "minor",
329
- mode: "help"
330
- }
331
- },
332
- name: "po-git",
333
- kind: "context",
334
- read: () => (0, _agimon_ai_po_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "llms.txt")
335
- }, {
336
- name: "po-use-git",
493
+ name: "codemode-how-to-git",
494
+ 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.",
337
495
  kind: "skill",
338
- read: () => (0, _agimon_ai_po_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/po-use-git/SKILL.md")
496
+ pinned: true,
497
+ path: (0, _agimon_ai_po_core_serverPlugin.packageResourcePath)(require("url").pathToFileURL(__filename).href, "src/prompts/codemode-how-to-git/SKILL.md"),
498
+ read: () => (0, _agimon_ai_po_core_serverPlugin.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/codemode-how-to-git/SKILL.md")
339
499
  }];
340
500
  const tool = {
341
501
  name: RUN_WORKTREE_TOOL_NAME,
@@ -355,17 +515,37 @@ function createGitSession({ context, host: serverHost }) {
355
515
  const changed = (_event, execution) => {
356
516
  directEvents.publish(require_worktreeOperations.GIT_SESSION_CHANGED_EVENT, execution.sessionId, { version: 1 });
357
517
  };
518
+ const hooks = [{
519
+ event: "tool_execution_end",
520
+ handle: changed
521
+ }, {
522
+ event: "agent_settled",
523
+ handle: changed
524
+ }];
525
+ if (agent) {
526
+ const namespace = createWorktreeNamespace({
527
+ operations,
528
+ execution: agent.context,
529
+ assertAvailable: () => {
530
+ if (signal?.aborted) throw new Error("Git session is unavailable.");
531
+ }
532
+ });
533
+ context.inject([_agimon_ai_po_core_codemode.PO_CODEMODE_SERVICE], (scoped) => {
534
+ const codemode = (0, _agimon_ai_po_core_codemode.readPoCodemode)(scoped);
535
+ if (!codemode) return void 0;
536
+ const registration = codemode.register("@agimon-ai/po-git", namespace);
537
+ const restriction = agent.registerToolRestriction({ excludedTools: [RUN_WORKTREE_TOOL_NAME] });
538
+ return () => {
539
+ restriction.dispose();
540
+ registration.dispose();
541
+ };
542
+ });
543
+ }
358
544
  return {
359
545
  value: {
360
546
  resources,
361
547
  tool,
362
- hooks: [{
363
- event: "tool_execution_end",
364
- handle: changed
365
- }, {
366
- event: "agent_settled",
367
- handle: changed
368
- }]
548
+ hooks
369
549
  },
370
550
  onDispose: () => messageInbox.close()
371
551
  };
@@ -476,11 +656,11 @@ const NAME_STATUS = {
476
656
  T: "modified"
477
657
  };
478
658
  /** Line count of a small regular file, or undefined when it is binary, large, or not a regular file. */
479
- function untrackedLines(root, relPath) {
659
+ async function untrackedLines(root, relPath) {
480
660
  const absolute = node_path.default.join(root, relPath);
481
661
  let stat;
482
662
  try {
483
- stat = node_fs.default.lstatSync(absolute);
663
+ stat = await node_fs_promises.default.lstat(absolute);
484
664
  } catch {
485
665
  return;
486
666
  }
@@ -489,7 +669,7 @@ function untrackedLines(root, relPath) {
489
669
  binary: false
490
670
  };
491
671
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
492
- const content = node_fs.default.readFileSync(absolute);
672
+ const content = await node_fs_promises.default.readFile(absolute);
493
673
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
494
674
  lines: 0,
495
675
  binary: true
@@ -612,8 +792,8 @@ function createBranchDiff() {
612
792
  "--exclude-standard",
613
793
  "-z"
614
794
  ]));
615
- untracked.forEach((relPath, index) => {
616
- const counted = index < MAX_UNTRACKED_COUNTED ? untrackedLines(root, relPath) : void 0;
795
+ for (const [index, relPath] of untracked.entries()) {
796
+ const counted = index < MAX_UNTRACKED_COUNTED ? await untrackedLines(root, relPath) : void 0;
617
797
  entries.push({
618
798
  path: relPath,
619
799
  status: "untracked",
@@ -621,18 +801,20 @@ function createBranchDiff() {
621
801
  removed: 0,
622
802
  ...counted?.binary ? { binary: true } : {}
623
803
  });
624
- });
804
+ }
625
805
  entries.sort((left, right) => left.path.localeCompare(right.path));
626
806
  const conflicts = [...unmerged];
627
- const rebasing = [await readLine$1(root, [
807
+ const rebaseMerge = await readLine$1(root, [
628
808
  "rev-parse",
629
809
  "--git-path",
630
810
  "rebase-merge"
631
- ]), await readLine$1(root, [
811
+ ]);
812
+ const rebaseApply = await readLine$1(root, [
632
813
  "rev-parse",
633
814
  "--git-path",
634
815
  "rebase-apply"
635
- ])].some((gitPath) => gitPath !== void 0 && node_fs.default.existsSync(node_path.default.resolve(root, gitPath)));
816
+ ]);
817
+ const rebasing = (await Promise.all([rebaseMerge, rebaseApply].map(async (gitPath) => gitPath === void 0 ? false : node_fs_promises.default.access(node_path.default.resolve(root, gitPath)).then(() => true, () => false)))).some(Boolean);
636
818
  const upstreamRef = branch === void 0 ? void 0 : await upstreamOf(root, branch);
637
819
  let upstream;
638
820
  if (upstreamRef !== void 0) {
@@ -677,7 +859,7 @@ function createBranchDiff() {
677
859
  if (entry === void 0) throw new Error("That path is not in this review.");
678
860
  if (entry.status === "untracked") {
679
861
  const absolute = node_path.default.join(review.root, entry.path);
680
- const stat = node_fs.default.lstatSync(absolute);
862
+ const stat = await node_fs_promises.default.lstat(absolute);
681
863
  if (stat.isSymbolicLink()) return {
682
864
  path: entry.path,
683
865
  hunks: [{
@@ -685,7 +867,7 @@ function createBranchDiff() {
685
867
  rows: [{
686
868
  marker: "+",
687
869
  line: 1,
688
- content: node_fs.default.readlinkSync(absolute)
870
+ content: await node_fs_promises.default.readlink(absolute)
689
871
  }]
690
872
  }]
691
873
  };
@@ -694,7 +876,7 @@ function createBranchDiff() {
694
876
  hunks: [],
695
877
  tooLarge: true
696
878
  };
697
- const content = node_fs.default.readFileSync(absolute);
879
+ const content = await node_fs_promises.default.readFile(absolute);
698
880
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
699
881
  path: entry.path,
700
882
  hunks: [],
@@ -811,9 +993,9 @@ function isRecord(value) {
811
993
  return typeof value === "object" && value !== null && !Array.isArray(value);
812
994
  }
813
995
  /** The workspace key: the canonical root, so a symlinked path and its target share one entry. */
814
- function workspaceKey(workspaceRoot) {
996
+ async function workspaceKey(workspaceRoot) {
815
997
  try {
816
- return node_fs.default.realpathSync(workspaceRoot);
998
+ return await node_fs_promises.default.realpath(workspaceRoot);
817
999
  } catch {
818
1000
  return node_path.default.resolve(workspaceRoot);
819
1001
  }
@@ -862,7 +1044,7 @@ function publicView(config) {
862
1044
  * the saved one, and only for the same host: a token never follows a host it
863
1045
  * was not entered for.
864
1046
  */
865
- function parseGitAuthSave(request, current, homeDir) {
1047
+ async function parseGitAuthSave(request, current, homeDir) {
866
1048
  if (!isRecord(request)) throw require_worktreeOperations.invalidRequest("The git auth request is not an object.", "Send method none, ssh or https.");
867
1049
  if (request.method === "none") return { method: "none" };
868
1050
  if (request.method === "ssh") {
@@ -872,7 +1054,7 @@ function parseGitAuthSave(request, current, homeDir) {
872
1054
  if (!keyPath.startsWith("/") && !keyPath.startsWith("~/")) throw require_worktreeOperations.invalidRequest("The key path is not absolute.", "Give the key as an absolute path or one under ~/.");
873
1055
  let isFile = false;
874
1056
  try {
875
- isFile = node_fs.default.statSync(expandHome(keyPath, homeDir)).isFile();
1057
+ isFile = (await node_fs_promises.default.stat(expandHome(keyPath, homeDir))).isFile();
876
1058
  } catch {
877
1059
  isFile = false;
878
1060
  }
@@ -907,16 +1089,16 @@ function parseGitAuthSave(request, current, homeDir) {
907
1089
  throw require_worktreeOperations.invalidRequest("The git auth method is unknown.", "Send method none, ssh or https.");
908
1090
  }
909
1091
  /** Tightens a file or folder something left readable by group or others. */
910
- function tighten(target, mode) {
1092
+ async function tighten(target, mode) {
911
1093
  try {
912
- if ((node_fs.default.statSync(target).mode & LOOSE_BITS) !== 0) node_fs.default.chmodSync(target, mode);
1094
+ if (((await node_fs_promises.default.stat(target)).mode & LOOSE_BITS) !== 0) await node_fs_promises.default.chmod(target, mode);
913
1095
  } catch {}
914
1096
  }
915
1097
  function createGitAuthStore(homeDir = node_os.default.homedir()) {
916
1098
  const file = require_worktreeOperations.credentialsFile(homeDir);
917
- const readFile = () => {
918
- tighten(file, OWNER_ONLY_FILE);
919
- const raw = require_worktreeOperations.readJson(file);
1099
+ const readFile = async () => {
1100
+ await tighten(file, OWNER_ONLY_FILE);
1101
+ const raw = await require_worktreeOperations.readJson(file);
920
1102
  const workspaces = {};
921
1103
  if (isRecord(raw) && raw.version === 1 && isRecord(raw.workspaces)) for (const [key, value] of Object.entries(raw.workspaces)) {
922
1104
  const config = parseConfig(value);
@@ -927,28 +1109,28 @@ function createGitAuthStore(homeDir = node_os.default.homedir()) {
927
1109
  workspaces
928
1110
  };
929
1111
  };
930
- const read = (workspaceRoot) => readFile().workspaces[workspaceKey(workspaceRoot)] ?? { method: "none" };
1112
+ const read = async (workspaceRoot) => (await readFile()).workspaces[await workspaceKey(workspaceRoot)] ?? { method: "none" };
931
1113
  return {
932
1114
  read,
933
- view: (workspaceRoot) => publicView(read(workspaceRoot)),
934
- save(workspaceRoot, request) {
935
- const key = workspaceKey(workspaceRoot);
936
- const current = readFile();
937
- const next = parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
1115
+ view: async (workspaceRoot) => publicView(await read(workspaceRoot)),
1116
+ async save(workspaceRoot, request) {
1117
+ const key = await workspaceKey(workspaceRoot);
1118
+ const current = await readFile();
1119
+ const next = await parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
938
1120
  const root = require_worktreeOperations.poGitRoot(homeDir);
939
- node_fs.default.mkdirSync(root, {
1121
+ await node_fs_promises.default.mkdir(root, {
940
1122
  recursive: true,
941
1123
  mode: OWNER_ONLY_DIRECTORY
942
1124
  });
943
- node_fs.default.chmodSync(root, OWNER_ONLY_DIRECTORY);
1125
+ await node_fs_promises.default.chmod(root, OWNER_ONLY_DIRECTORY);
944
1126
  const workspaces = { ...current.workspaces };
945
1127
  if (next.method === "none") delete workspaces[key];
946
1128
  else workspaces[key] = next;
947
- require_worktreeOperations.writeJsonAtomic(file, {
1129
+ await require_worktreeOperations.writeJsonAtomic(file, {
948
1130
  version: 1,
949
1131
  workspaces
950
1132
  });
951
- node_fs.default.chmodSync(file, OWNER_ONLY_FILE);
1133
+ await node_fs_promises.default.chmod(file, OWNER_ONLY_FILE);
952
1134
  return publicView(next);
953
1135
  }
954
1136
  };
@@ -1067,7 +1249,13 @@ function newSessionRequest(body) {
1067
1249
  if (branch === void 0 || branch === false || !require_worktreeOperations.validRefName(branch)) return { error: "A valid branch name is required." };
1068
1250
  const name = optionalText(record.name);
1069
1251
  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.` };
1070
- const named = name === void 0 ? {} : { name };
1252
+ const chosen = record.selection === void 0 ? {} : (0, _agimon_ai_po_core_hubChannel.parseLaunchSelection)(record.selection);
1253
+ if ("error" in chosen) return chosen;
1254
+ const profile = chosen.selection?.profile;
1255
+ const named = {
1256
+ ...name === void 0 ? {} : { name },
1257
+ ...profile === void 0 ? {} : { selection: { profile } }
1258
+ };
1071
1259
  if (record.mode === "existing-branch") {
1072
1260
  const remote = optionalText(record.remote);
1073
1261
  if (remote === false || remote !== void 0 && !require_worktreeOperations.validRefName(remote)) return { error: "That remote name is not valid." };
@@ -1113,177 +1301,237 @@ function failed(error) {
1113
1301
  }, { status: error.retryable ? 503 : 409 });
1114
1302
  return Response.json({ error: "The worktree operation failed." }, { status: 500 });
1115
1303
  }
1116
- //#endregion
1117
- //#region src/extensions/(backend)/api/git/route.server.ts
1118
- var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)({
1119
- basePath: "git",
1120
- start(context) {
1121
- if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1122
- const directEvents = context.directEvents;
1123
- const publishLifecycle = (sessionId, repositoryRoot) => {
1124
- directEvents.publish(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1125
- version: 1,
1126
- repositoryRoot
1304
+ function createGitApi(openDirectory) {
1305
+ return {
1306
+ basePath: "git",
1307
+ start(context) {
1308
+ if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1309
+ const directEvents = context.directEvents;
1310
+ const publishLifecycle = (sessionId, repositoryRoot) => {
1311
+ directEvents.publish(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1312
+ version: 1,
1313
+ repositoryRoot
1314
+ });
1315
+ };
1316
+ const git = require_worktreeOperations.createWorktreeGit();
1317
+ const operations = require_worktreeOperations.createWorktreeOperations({
1318
+ git,
1319
+ sessionService: context.sessionService,
1320
+ ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1127
1321
  });
1128
- };
1129
- const git = require_worktreeOperations.createWorktreeGit();
1130
- const operations = require_worktreeOperations.createWorktreeOperations({
1131
- git,
1132
- sessionService: context.sessionService,
1133
- ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1134
- });
1135
- /**
1136
- * The new-session dialog's routes: the workspace's branches, and a top-level
1137
- * worktree session on one. Workspace-mounted only, and always rooted at the
1138
- * mount's own workspace, so a request can never name another directory.
1139
- */
1140
- const newSession = async (request, url) => {
1141
- const isBranches = request.method === "GET" && url.pathname === "/branches";
1142
- const isCreate = request.method === "POST" && url.pathname === "/sessions";
1143
- if (!isBranches && !isCreate) return void 0;
1144
- const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1145
- if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1146
- if (isBranches) {
1147
- const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1148
- const branches = repositoryRoot === void 0 ? {
1149
- repository: false,
1150
- local: [],
1151
- remote: []
1152
- } : {
1153
- repository: true,
1154
- ...await git.listBranches(repositoryRoot)
1322
+ /**
1323
+ * The new-session dialog's routes: the workspace's branches, and a top-level
1324
+ * worktree session on one. Workspace-mounted only, and always rooted at the
1325
+ * mount's own workspace, so a request can never name another directory.
1326
+ */
1327
+ const newSession = async (request, url) => {
1328
+ const isBranches = request.method === "GET" && url.pathname === "/branches";
1329
+ const isCreate = request.method === "POST" && url.pathname === "/sessions";
1330
+ const isDelete = request.method === "DELETE" && url.pathname === "/branches";
1331
+ const isOpen = request.method === "POST" && url.pathname === `/branches/open`;
1332
+ if (!isBranches && !isCreate && !isDelete && !isOpen) return void 0;
1333
+ const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1334
+ if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1335
+ if (isBranches) {
1336
+ const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1337
+ const branches = repositoryRoot === void 0 ? {
1338
+ repository: false,
1339
+ local: [],
1340
+ remote: []
1341
+ } : {
1342
+ repository: true,
1343
+ ...await git.listBranches(repositoryRoot)
1344
+ };
1345
+ return Response.json(branches);
1346
+ }
1347
+ if (isDelete || isOpen) {
1348
+ if (isOpen && (0, _agimon_ai_po_core_packageApi.poApiCallerFrom)(request.headers)?.locality !== "local") return Response.json({ error: "Opening Finder is only available on the host." }, { status: 403 });
1349
+ const body = await request.json().catch(() => void 0);
1350
+ const branch = typeof body === "object" && body !== null ? body.branch : void 0;
1351
+ if (typeof branch !== "string" || !require_worktreeOperations.validRefName(branch)) return badRequest("A valid branch name is required.");
1352
+ const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1353
+ if (repositoryRoot === void 0) return badRequest("This workspace is not a git checkout.");
1354
+ const branches = await git.listBranches(repositoryRoot);
1355
+ const local = branches.local.find((item) => item.name === branch);
1356
+ if (local === void 0) return Response.json({ error: "That local branch no longer exists." }, { status: 404 });
1357
+ if (isOpen) {
1358
+ if (local.checkedOutAt === void 0) return Response.json({ error: "This branch has no checked-out folder." }, { status: 409 });
1359
+ if (openDirectory === void 0) return Response.json({ error: "Finder is not available on this host." }, { status: 409 });
1360
+ try {
1361
+ await openDirectory(local.checkedOutAt);
1362
+ return Response.json({ opened: true });
1363
+ } catch {
1364
+ return Response.json({ error: "The branch folder could not be opened in Finder." }, { status: 409 });
1365
+ }
1366
+ }
1367
+ if (local.checkedOutAt !== void 0 || branches.current === branch) return Response.json({ error: "Close this branch’s worktree before deleting it." }, { status: 409 });
1368
+ if (!await git.deleteBranch({
1369
+ repositoryRoot,
1370
+ branch
1371
+ })) return Response.json({ error: "Git refused to delete this branch. It may contain unmerged commits or be checked out." }, { status: 409 });
1372
+ return Response.json({ deleted: true });
1373
+ }
1374
+ const parsed = newSessionRequest(await request.json().catch(() => void 0));
1375
+ if ("error" in parsed) return badRequest(parsed.error);
1376
+ const record = await operations.spawn({ cwd: workspaceRoot }, {
1377
+ branch: parsed.branch,
1378
+ checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1379
+ ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1380
+ ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1381
+ ...parsed.name === void 0 ? {} : { name: parsed.name },
1382
+ ...parsed.selection === void 0 ? {} : { selection: parsed.selection }
1383
+ });
1384
+ publishLifecycle(record.sessionId, record.repositoryRoot);
1385
+ const created = {
1386
+ sessionId: record.sessionId,
1387
+ worktreeId: record.id
1155
1388
  };
1156
- return Response.json(branches);
1157
- }
1158
- const parsed = newSessionRequest(await request.json().catch(() => void 0));
1159
- if ("error" in parsed) return badRequest(parsed.error);
1160
- const record = await operations.spawn({ cwd: workspaceRoot }, {
1161
- branch: parsed.branch,
1162
- checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1163
- ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1164
- ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1165
- ...parsed.name === void 0 ? {} : { name: parsed.name }
1166
- });
1167
- publishLifecycle(record.sessionId, record.repositoryRoot);
1168
- const created = {
1169
- sessionId: record.sessionId,
1170
- worktreeId: record.id
1389
+ return Response.json(created, { status: 201 });
1171
1390
  };
1172
- return Response.json(created, { status: 201 });
1173
- };
1174
- /** The admitted root for a request, or a response explaining why not. */
1175
- const rootOf = (url) => {
1176
- const repositoryId = url.searchParams.get("repositoryId");
1177
- if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1178
- const root = context.resolveRepository?.(repositoryId);
1179
- if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1180
- return root;
1181
- };
1182
- const homeDir = context.homeDirectory ?? node_os.default.homedir();
1183
- const gitAuth = createGitAuthStore(homeDir);
1184
- const branchDiff = createBranchDiff();
1185
- const pendingReviews = /* @__PURE__ */ new Map();
1186
- /**
1187
- * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1188
- * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1189
- * The view never carries the token.
1190
- */
1191
- const auth = async (request, url) => {
1192
- if (url.pathname !== "/auth") return void 0;
1193
- if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1194
- if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1195
- const root = rootOf(url);
1196
- if (root instanceof Response) return root;
1197
- if (request.method === "GET") return Response.json(gitAuth.view(root));
1198
- try {
1199
- return Response.json(gitAuth.save(root, await request.json().catch(() => void 0)));
1200
- } catch (error) {
1201
- if (error instanceof require_worktreeOperations.PoGitExpectedError && error.code === "invalid_request") {
1202
- const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1203
- return Response.json({
1204
- error: text,
1205
- code: error.code
1206
- }, { status: 400 });
1207
- }
1208
- throw error;
1209
- }
1210
- };
1211
- /**
1212
- * The session's change against its base. Session mounts only, rooted at the
1213
- * session's own checkout; a file is served only when it is in the change set
1214
- * computed for the request. Overlapping requests share an in-flight snapshot.
1215
- */
1216
- const review = async (request, url) => {
1217
- if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1218
- const cwd = context.scope === "session" ? context.cwd : void 0;
1219
- if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1220
- if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1221
- let recordedBaseRef;
1222
- try {
1223
- recordedBaseRef = require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(cwd, homeDir)).list().find((record) => record.sessionId === context.sessionId)?.baseRef;
1224
- } catch {
1225
- recordedBaseRef = void 0;
1226
- }
1227
- const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1228
- let pending = pendingReviews.get(key);
1229
- if (pending === void 0) {
1230
- pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1231
- pendingReviews.set(key, pending);
1232
- }
1233
- const result = await pending;
1234
- if (url.pathname === "/review") return Response.json(result?.summary ?? {
1235
- repository: false,
1236
- files: []
1237
- });
1238
- const filePath = url.searchParams.get("path") ?? "";
1239
- 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 });
1240
- return Response.json(await branchDiff.fileDiff(result, filePath));
1241
- };
1242
- return {
1243
- async fetch(request) {
1244
- const url = new URL(request.url);
1391
+ /** The admitted root for a request, or a response explaining why not. */
1392
+ const rootOf = (url) => {
1393
+ const repositoryId = url.searchParams.get("repositoryId");
1394
+ if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1395
+ const root = context.resolveRepository?.(repositoryId);
1396
+ if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1397
+ return root;
1398
+ };
1399
+ const homeDir = context.homeDirectory ?? context.environment?.HOME;
1400
+ const gitAuth = homeDir === void 0 ? void 0 : createGitAuthStore(homeDir);
1401
+ const branchDiff = createBranchDiff();
1402
+ const pendingReviews = /* @__PURE__ */ new Map();
1403
+ /**
1404
+ * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1405
+ * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1406
+ * The view never carries the token.
1407
+ */
1408
+ const auth = async (request, url) => {
1409
+ if (url.pathname !== "/auth") return void 0;
1410
+ if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1411
+ if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1412
+ if (gitAuth === void 0) throw new Error("Git auth requires the admitted host home directory.");
1413
+ const root = rootOf(url);
1414
+ if (root instanceof Response) return root;
1415
+ if (request.method === "GET") return Response.json(await gitAuth.view(root));
1245
1416
  try {
1246
- const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1247
- if (answered !== void 0) return answered;
1417
+ return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
1248
1418
  } catch (error) {
1249
- context.onNotice(`git request failed: ${error.name}`);
1250
- return failed(error);
1419
+ if (error instanceof require_worktreeOperations.PoGitExpectedError && error.code === "invalid_request") {
1420
+ const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1421
+ return Response.json({
1422
+ error: text,
1423
+ code: error.code
1424
+ }, { status: 400 });
1425
+ }
1426
+ throw error;
1251
1427
  }
1252
- const root = rootOf(url);
1253
- if (root instanceof Response) return root;
1254
- const callerContext = {
1255
- cwd: root,
1256
- sessionId: url.searchParams.get("sessionId") ?? ""
1257
- };
1258
- 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.");
1428
+ };
1429
+ /**
1430
+ * The session's change against its base. Session mounts only, rooted at the
1431
+ * session's own checkout; a file is served only when it is in the change set
1432
+ * computed for the request. Overlapping requests share an in-flight snapshot.
1433
+ */
1434
+ const review = async (request, url) => {
1435
+ if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1436
+ const cwd = context.scope === "session" ? context.cwd : void 0;
1437
+ if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1438
+ if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1439
+ if (homeDir === void 0) throw new Error("Git review requires the admitted host home directory.");
1440
+ let recordedBaseRef;
1259
1441
  try {
1260
- if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1261
- if (request.method === "POST" && url.pathname === "/worktrees") {
1262
- const body = await request.json();
1263
- if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1264
- const record = await operations.spawn(callerContext, {
1265
- branch: body.branch,
1266
- ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1267
- ...body.name === void 0 ? {} : { name: body.name }
1268
- });
1269
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1270
- return Response.json({ worktree: record }, { status: 201 });
1442
+ recordedBaseRef = (await require_worktreeOperations.createWorktreeRegistry(await require_worktreeOperations.registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
1443
+ } catch {
1444
+ recordedBaseRef = void 0;
1445
+ }
1446
+ const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1447
+ let pending = pendingReviews.get(key);
1448
+ if (pending === void 0) {
1449
+ pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1450
+ pendingReviews.set(key, pending);
1451
+ }
1452
+ const result = await pending;
1453
+ if (url.pathname === "/review") return Response.json(result?.summary ?? {
1454
+ repository: false,
1455
+ files: []
1456
+ });
1457
+ const filePath = url.searchParams.get("path") ?? "";
1458
+ 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 });
1459
+ return Response.json(await branchDiff.fileDiff(result, filePath));
1460
+ };
1461
+ return {
1462
+ async fetch(request) {
1463
+ const url = new URL(request.url);
1464
+ try {
1465
+ const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1466
+ if (answered !== void 0) return answered;
1467
+ } catch (error) {
1468
+ context.onNotice(`git request failed: ${error.name}`);
1469
+ return failed(error);
1271
1470
  }
1272
- const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1273
- if (request.method === "DELETE" && closing) {
1274
- const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1275
- publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1276
- return Response.json({ worktree: record });
1471
+ const root = rootOf(url);
1472
+ if (root instanceof Response) return root;
1473
+ const callerContext = {
1474
+ cwd: root,
1475
+ sessionId: url.searchParams.get("sessionId") ?? ""
1476
+ };
1477
+ 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.");
1478
+ try {
1479
+ if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1480
+ if (request.method === "POST" && url.pathname === "/worktrees") {
1481
+ const body = await request.json();
1482
+ if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1483
+ const record = await operations.spawn(callerContext, {
1484
+ branch: body.branch,
1485
+ ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1486
+ ...body.name === void 0 ? {} : { name: body.name }
1487
+ });
1488
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1489
+ return Response.json({ worktree: record }, { status: 201 });
1490
+ }
1491
+ const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1492
+ if (request.method === "DELETE" && closing) {
1493
+ const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1494
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1495
+ return Response.json({ worktree: record });
1496
+ }
1497
+ return Response.json({ error: "No such worktree route." }, { status: 404 });
1498
+ } catch (error) {
1499
+ context.onNotice(`worktree request failed: ${error.name}`);
1500
+ return failed(error);
1277
1501
  }
1278
- return Response.json({ error: "No such worktree route." }, { status: 404 });
1279
- } catch (error) {
1280
- context.onNotice(`worktree request failed: ${error.name}`);
1281
- return failed(error);
1282
- }
1283
- },
1284
- close() {}
1502
+ },
1503
+ close() {}
1504
+ };
1505
+ }
1506
+ };
1507
+ }
1508
+ //#endregion
1509
+ //#region src/extensions/(backend)/api/git/route.server.ts
1510
+ var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)(({ context, host, signal }) => {
1511
+ let openDirectory;
1512
+ context.inject([_agimon_ai_po_core_platform.PO_SHELL_SERVICE], (child) => {
1513
+ const shell = (0, _agimon_ai_po_core_platform.requirePoShell)(child);
1514
+ const env = Object.fromEntries(Object.entries(host.context.environment ?? {}).filter((entry) => entry[1] !== void 0));
1515
+ openDirectory = async (path) => {
1516
+ const result = await shell.exec([
1517
+ "/usr/bin/open",
1518
+ "-a",
1519
+ "Finder",
1520
+ path
1521
+ ], {
1522
+ env,
1523
+ inheritEnv: false
1524
+ }, (0, _agimon_ai_po_core_platform.withAbortSignal)(AbortSignal.any([signal, AbortSignal.timeout(5e3)]), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT));
1525
+ if (!result.ok || result.value.exitCode !== 0) throw new Error("Finder could not be opened.");
1285
1526
  };
1286
- }
1527
+ return () => {
1528
+ openDirectory = void 0;
1529
+ };
1530
+ });
1531
+ return createGitApi(async (path) => {
1532
+ if (openDirectory === void 0) throw new Error("The host shell is unavailable.");
1533
+ await openDirectory(path);
1534
+ });
1287
1535
  });
1288
1536
  //#endregion
1289
1537
  //#region src/services/gitFailure/index.ts
@@ -1420,7 +1668,7 @@ async function rebasing(root) {
1420
1668
  "--git-path",
1421
1669
  name
1422
1670
  ]);
1423
- if (gitPath !== void 0 && node_fs.default.existsSync(node_path.default.resolve(root, gitPath))) return true;
1671
+ if (gitPath !== void 0 && await node_fs_promises.default.access(node_path.default.resolve(root, gitPath)).then(() => true, () => false)) return true;
1424
1672
  }
1425
1673
  return false;
1426
1674
  }
@@ -1590,9 +1838,9 @@ const GIT_CHANGES_TYPE = "git_changes";
1590
1838
  * DESIGN PATTERNS:
1591
1839
  * - Apart from the worktrees channel. Worktrees are repository management;
1592
1840
  * this is the session's own code change.
1593
- * - Push, not poll. The session facet publishes a "changed" event after every
1594
- * tool call and settled turn; recounts are coalesced to one in flight plus
1595
- * one rerun, so a burst of tool calls costs at most two.
1841
+ * - Push, not poll. The session's server plugin publishes a "changed" event
1842
+ * after every tool call and settled turn; recounts are coalesced to one in
1843
+ * flight plus one rerun, so a burst of tool calls costs at most two.
1596
1844
  * - One sync command per session. A rebase and a push must never overlap.
1597
1845
  * - Auth is read fresh for each command from the session's workspace, and goes
1598
1846
  * only into that command's git child.
@@ -1653,9 +1901,9 @@ function createGitChangesChannel(options = {}) {
1653
1901
  const rerun = /* @__PURE__ */ new Set();
1654
1902
  const busy = /* @__PURE__ */ new Set();
1655
1903
  /** The base the session's own worktree was created from, when it has one. */
1656
- const recordedBaseRef = (scope) => {
1904
+ const recordedBaseRef = async (scope) => {
1657
1905
  try {
1658
- return require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(scope.cwd, homeDir)).list().find((record) => record.sessionId === scope.sessionId)?.baseRef;
1906
+ return (await require_worktreeOperations.createWorktreeRegistry(await require_worktreeOperations.registryFile(scope.cwd, homeDir)).list()).find((record) => record.sessionId === scope.sessionId)?.baseRef;
1659
1907
  } catch {
1660
1908
  return;
1661
1909
  }
@@ -1688,8 +1936,7 @@ function createGitChangesChannel(options = {}) {
1688
1936
  return;
1689
1937
  }
1690
1938
  inFlight.add(sessionId);
1691
- const base = recordedBaseRef(scope);
1692
- diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base }).then((review) => {
1939
+ recordedBaseRef(scope).then((base) => diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base })).then((review) => {
1693
1940
  if (scopes.has(sessionId)) update(sessionId, { changes: review?.changes });
1694
1941
  }).catch((error) => {
1695
1942
  host.onNotice(`git diff refresh failed for ${sessionId} (${error instanceof Error ? error.name : "unknown"})`);
@@ -1698,12 +1945,12 @@ function createGitChangesChannel(options = {}) {
1698
1945
  if (rerun.delete(sessionId)) refresh(sessionId);
1699
1946
  });
1700
1947
  };
1701
- const run = (scope, command) => {
1948
+ const run = async (scope, command) => {
1702
1949
  const workspaceRoot = scope.sessionContext?.workspaceRoot ?? scope.cwd;
1703
- const base = recordedBaseRef(scope);
1950
+ const base = await recordedBaseRef(scope);
1704
1951
  const context = {
1705
1952
  cwd: scope.cwd,
1706
- auth: auth.read(workspaceRoot),
1953
+ auth: await auth.read(workspaceRoot),
1707
1954
  ...base === void 0 ? {} : { recordedBaseRef: base }
1708
1955
  };
1709
1956
  if (command.action === "pull") return sync.pull(context);
@@ -1801,9 +2048,9 @@ function payloadOf(state) {
1801
2048
  };
1802
2049
  }
1803
2050
  /** Worktrees visible to this session, using the hub's live session table for ownership. */
1804
- function viewsFor(scope, deps) {
2051
+ async function viewsFor(scope, deps) {
1805
2052
  try {
1806
- return require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(scope.cwd, deps.homeDir)).list().flatMap((record) => {
2053
+ return (await require_worktreeOperations.createWorktreeRegistry(await require_worktreeOperations.registryFile(scope.cwd, deps.homeDir)).list()).flatMap((record) => {
1807
2054
  if (record.parentSessionId === void 0 && record.sessionId !== scope.sessionId) return [];
1808
2055
  const owned = record.parentSessionId === void 0 || record.parentSessionId === scope.sessionId;
1809
2056
  const unowned = !owned && !deps.sessionService.isLive(record.parentSessionId);
@@ -1828,11 +2075,15 @@ function sameList(left, right) {
1828
2075
  return other !== void 0 && entry.id === other.id && entry.branch === other.branch && entry.sessionId === other.sessionId && entry.orphaned === other.orphaned && entry.unowned === other.unowned;
1829
2076
  });
1830
2077
  }
1831
- function errorTargetFor(command) {
1832
- return command.action === "create" ? { action: "create" } : {
2078
+ function errorTargetFor(command, error) {
2079
+ if (command.action === "close") return {
1833
2080
  action: "close",
1834
2081
  id: command.id
1835
2082
  };
2083
+ return error instanceof require_worktreeOperations.PoGitExpectedError && error.code === "rift_missing" ? {
2084
+ action: "create",
2085
+ riftMissing: true
2086
+ } : { action: "create" };
1836
2087
  }
1837
2088
  /** A command the page sent, or undefined when the frame was not one. */
1838
2089
  function parseCommand(payload) {
@@ -1860,11 +2111,15 @@ function parseCommand(payload) {
1860
2111
  /**
1861
2112
  * What the reader is told when a command fails.
1862
2113
  *
1863
- * An expected failure explains itself. Anything else is summarised, never
1864
- * echoed: a git error can carry a remote URL with a token in it.
2114
+ * An expected failure explains itself, minus the `[code]` tag and recovery line
2115
+ * written for the agent's tool result. A dirty close is said in the person's
2116
+ * terms, because its message tells the agent to pass `force`. Anything else is
2117
+ * summarised, never echoed: a git error can carry a remote URL with a token in it.
1865
2118
  */
1866
2119
  function failureText(error) {
1867
- return error instanceof require_worktreeOperations.PoGitExpectedError ? error.message : "The worktree operation failed.";
2120
+ if (!(error instanceof require_worktreeOperations.PoGitExpectedError)) return "The worktree operation failed.";
2121
+ if (error.code === "worktree_dirty") return "The worktree has uncommitted changes.";
2122
+ return error.message.replace(/^\[[a-z_]+\] /u, "").split("\nRecovery:")[0] ?? error.message;
1868
2123
  }
1869
2124
  /** Starts one direct-event subscription per managed session. */
1870
2125
  function createWorktreesChannel(options = {}) {
@@ -1917,10 +2172,13 @@ function createWorktreesChannel(options = {}) {
1917
2172
  return generation;
1918
2173
  };
1919
2174
  /** Republishes a session, skipping a frame that would say nothing new. */
1920
- const publish = (scope, force) => {
2175
+ const publish = async (scope, force) => {
2176
+ const generation = generations.get(scope.sessionId);
2177
+ const worktreeViews = await viewsFor(scope, views);
2178
+ if (generation === void 0 || generations.get(scope.sessionId) !== generation) return;
1921
2179
  const previous = latest.get(scope.sessionId);
1922
2180
  const next = {
1923
- worktrees: viewsFor(scope, views),
2181
+ worktrees: worktreeViews,
1924
2182
  pending: previous?.pending,
1925
2183
  error: previous?.error,
1926
2184
  errorTarget: previous?.errorTarget
@@ -1932,7 +2190,7 @@ function createWorktreesChannel(options = {}) {
1932
2190
  const mark = (scope, pending, error, errorTarget) => {
1933
2191
  const previous = latest.get(scope.sessionId);
1934
2192
  latest.set(scope.sessionId, {
1935
- worktrees: previous?.worktrees ?? viewsFor(scope, views),
2193
+ worktrees: previous?.worktrees ?? [],
1936
2194
  pending,
1937
2195
  error,
1938
2196
  errorTarget
@@ -1972,15 +2230,15 @@ function createWorktreesChannel(options = {}) {
1972
2230
  sessionAdded(scope) {
1973
2231
  newGeneration(scope.sessionId);
1974
2232
  publish(scope, false);
1975
- const unsubscribe = host.directEvents.subscribe(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, (payload) => {
2233
+ const unsubscribe = host.directEvents.subscribe(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, async (payload) => {
1976
2234
  if (!require_worktreeOperations.isWorktreeLifecycleEvent(payload)) return;
1977
2235
  let sameRepository = false;
1978
2236
  try {
1979
- sameRepository = require_worktreeOperations.registryFile(scope.cwd, options.homeDir) === require_worktreeOperations.registryFile(payload.repositoryRoot, options.homeDir);
2237
+ sameRepository = await require_worktreeOperations.registryFile(scope.cwd, options.homeDir) === await require_worktreeOperations.registryFile(payload.repositoryRoot, options.homeDir);
1980
2238
  } catch {
1981
2239
  sameRepository = false;
1982
2240
  }
1983
- if (sameRepository) publish(scope, false);
2241
+ if (sameRepository) await publish(scope, false);
1984
2242
  });
1985
2243
  subscriptions.set(scope.sessionId, unsubscribe);
1986
2244
  },
@@ -2013,7 +2271,7 @@ function createWorktreesChannel(options = {}) {
2013
2271
  }).catch((error) => {
2014
2272
  if (generations.get(scope.sessionId) !== generation) return;
2015
2273
  host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);
2016
- mark(scope, void 0, failureText(error), errorTargetFor(command));
2274
+ mark(scope, void 0, failureText(error), errorTargetFor(command, error));
2017
2275
  }).finally(() => {
2018
2276
  if (generations.get(scope.sessionId) === generation) busy.delete(scope.sessionId);
2019
2277
  });
@@ -2035,20 +2293,8 @@ var agent_settled_server_default = (0, _agimon_ai_po_core_extensionFile.defineHo
2035
2293
  //#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts
2036
2294
  var tool_execution_end_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)((context) => context.root.hooks[0]);
2037
2295
  //#endregion
2038
- //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGit.server.ts
2039
- var poGit_server_default$1 = (context) => context.root.resources[0];
2040
- //#endregion
2041
- //#region src/extensions/workspaces/sessions/(backend)/resource/poGit.server.ts
2042
- var poGit_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poGit_server_default$1);
2043
- //#endregion
2044
- //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGitReadme.server.ts
2045
- var poGitReadme_server_default$1 = (context) => context.root.resources[2];
2046
- //#endregion
2047
- //#region src/extensions/workspaces/sessions/(backend)/resource/poGitReadme.server.ts
2048
- var poGitReadme_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poGitReadme_server_default$1);
2049
- //#endregion
2050
2296
  //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poUseGit.server.ts
2051
- var poUseGit_server_default$1 = (context) => context.root.resources[1];
2297
+ var poUseGit_server_default$1 = (context) => context.root.resources[0];
2052
2298
  //#endregion
2053
2299
  //#region src/extensions/workspaces/sessions/(backend)/resource/poUseGit.server.ts
2054
2300
  var poUseGit_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poUseGit_server_default$1);
@@ -2058,19 +2304,22 @@ var runWorktree_server_default = (0, _agimon_ai_po_core_extensionFile.defineServ
2058
2304
  //#endregion
2059
2305
  //#region generated/server.ts
2060
2306
  const at = (value, context) => typeof value === "function" ? value(context) : value;
2307
+ /* v8 ignore next -- generated normalization branch */
2308
+ const defined = (values) => values.filter((value) => value !== void 0);
2061
2309
  const via = (identity, value) => value === void 0 ? void 0 : {
2062
2310
  ...identity,
2063
2311
  ...value
2064
2312
  };
2065
- const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2313
+ const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
2066
2314
  name: "@agimon-ai/po-git",
2067
- providerOverrides: (0, _agimon_ai_po_core_serverFacet.composeServerProviderOverrides)(root_server_default),
2315
+ providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default),
2316
+ injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({ session: [root_server_default] }),
2068
2317
  global: (context) => ({
2069
2318
  get api() {
2070
2319
  return [at(route_server_default, context)];
2071
2320
  },
2072
2321
  get channels() {
2073
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2322
+ return [() => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))];
2074
2323
  }
2075
2324
  }),
2076
2325
  workspace: (context) => ({
@@ -2078,11 +2327,11 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2078
2327
  return [at(route_server_default, context)];
2079
2328
  },
2080
2329
  get channels() {
2081
- 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))]);
2082
2331
  }
2083
2332
  }),
2084
2333
  session: async (context) => {
2085
- const scopeSession = await root_server_default(context);
2334
+ const scopeSession = await context.scopeRoot("session", root_server_default, context);
2086
2335
  const contextSession = {
2087
2336
  ...context,
2088
2337
  root: scopeSession.value
@@ -2093,17 +2342,13 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2093
2342
  return [at(route_server_default, context)];
2094
2343
  },
2095
2344
  get channels() {
2096
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2345
+ 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))]);
2097
2346
  },
2098
2347
  get hooks() {
2099
2348
  return [via({ event: "agent_settled" }, at(agent_settled_server_default, contextSession)), via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession))];
2100
2349
  },
2101
2350
  get resources() {
2102
- return [
2103
- at(poGit_server_default, contextSession),
2104
- at(poGitReadme_server_default, contextSession),
2105
- at(poUseGit_server_default, contextSession)
2106
- ];
2351
+ return [at(poUseGit_server_default, contextSession)];
2107
2352
  },
2108
2353
  get tools() {
2109
2354
  return [via({ name: "run_worktree" }, at(runWorktree_server_default, contextSession))];
@@ -2116,7 +2361,7 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2116
2361
  }
2117
2362
  });
2118
2363
  //#endregion
2119
- exports.default = facet;
2120
- exports.facet = facet;
2364
+ exports.default = serverPlugin;
2365
+ exports.serverPlugin = serverPlugin;
2121
2366
 
2122
2367
  //# sourceMappingURL=server.cjs.map