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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
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";
9
10
  //#region src/schemas/runWorktreeTool.ts
@@ -247,7 +248,8 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
247
248
  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
249
  details: {
249
250
  action: params.action,
250
- records: [record]
251
+ records: [record],
252
+ dirtyFiles
251
253
  }
252
254
  };
253
255
  }
@@ -271,7 +273,10 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
271
273
  const waiting = await operations.messages(context, params.id);
272
274
  return {
273
275
  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 }
276
+ details: {
277
+ action: params.action,
278
+ messages: waiting
279
+ }
275
280
  };
276
281
  }
277
282
  case WORKTREE_ACTIONS.prune: {
@@ -286,12 +291,172 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
286
291
  `Untracked directories, left alone: ${plan.untracked.length === 0 ? "none" : plan.untracked.join(", ")}`,
287
292
  ...plan.keptBranches.length === 0 ? [] : [`Branches kept, they hold commits: ${plan.keptBranches.join(", ")}`]
288
293
  ]),
289
- details: { action: params.action }
294
+ details: {
295
+ action: params.action,
296
+ plan
297
+ }
290
298
  };
291
299
  }
292
300
  }
293
301
  }
294
302
  //#endregion
303
+ //#region src/services/worktreeNamespace/index.ts
304
+ const DECLARATIONS = `type WorktreeStatus = 'spawning' | 'running' | 'closing' | 'orphaned';
305
+
306
+ interface Worktree {
307
+ id: string;
308
+ branch: string;
309
+ baseRef: string;
310
+ /** Absolute path of the checkout, under ~/.po/git/worktrees. */
311
+ path: string;
312
+ repositoryRoot: string;
313
+ /** The session running inside the worktree. */
314
+ sessionId: string;
315
+ parentSessionId?: string;
316
+ status: WorktreeStatus;
317
+ createdAt: string;
318
+ /** How your session relates to it: 'parent' spawned it, 'child' runs inside it, 'other' is neither. */
319
+ relationship: 'parent' | 'child' | 'other';
320
+ }
321
+
322
+ interface WorktreeMessage {
323
+ worktreeId: string;
324
+ fromSessionId: string;
325
+ from: 'parent' | 'child';
326
+ text: string;
327
+ sentAt: string;
328
+ }
329
+
330
+ interface PrunePlan {
331
+ remove: Worktree[];
332
+ forget: Worktree[];
333
+ /** Kept because they hold uncommitted work. */
334
+ keptDirty: Worktree[];
335
+ untracked: string[];
336
+ /** Branches kept because they hold commits. */
337
+ keptBranches: string[];
338
+ }
339
+
340
+ declare const worktrees: {
341
+ /** Every worktree for this repository. */
342
+ list(): Promise<Worktree[]>;
343
+ /** One worktree and its uncommitted files. */
344
+ status(id: string): Promise<{ worktree: Worktree; dirtyFiles: string[] }>;
345
+ /**
346
+ * Create a worktree on a new branch and start a Po session in it, nested under this one. baseRef defaults to
347
+ * the current branch's remote-tracking ref, then the remote default branch; name labels it in the rail; task is
348
+ * the new session's first message. Setup can be slow, so give exec a larger timeoutMs.
349
+ */
350
+ spawn(request: { branch: string; baseRef?: string; name?: string; task?: string }): Promise<Worktree>;
351
+ /** Stop the worktree's session and delete its checkout. Refused on a dirty tree unless force is true. */
352
+ close(id: string, options?: { force?: boolean }): Promise<Worktree>;
353
+ /** Merge the worktree's branch into this checkout. Refused when this checkout is dirty. */
354
+ merge(id: string, options?: { message?: string }): Promise<Worktree>;
355
+ /** Remove orphaned worktrees. With dryRun, only return the plan. */
356
+ prune(options?: { dryRun?: boolean }): Promise<PrunePlan>;
357
+ /**
358
+ * Message the other side of a worktree. Use the record with relationship 'child' to report to your parent,
359
+ * and 'parent' to reach your own children. 'other' records are not communication edges.
360
+ */
361
+ send(id: string, message: string): Promise<void>;
362
+ /** Messages waiting for you from a worktree. */
363
+ messages(id: string): Promise<WorktreeMessage[]>;
364
+ };`;
365
+ const json = (value) => JSON.parse(JSON.stringify(value));
366
+ /** Git worktrees for code mode: the actions of `run_worktree` as methods, returning records instead of text. */
367
+ function createWorktreeNamespace(options) {
368
+ return {
369
+ name: "worktrees",
370
+ summary: "Git worktrees, each with its own Po session nested under this one: worktrees.list, status, spawn, close, merge, prune, send and messages.",
371
+ declarations: DECLARATIONS,
372
+ bind(run) {
373
+ if (run.sessionId !== options.execution.sessionId) throw new Error("Worktrees namespace belongs to another session.");
374
+ let disposed = false;
375
+ run.onDispose(() => {
376
+ disposed = true;
377
+ });
378
+ const view = (record) => ({
379
+ ...record,
380
+ relationship: record.parentSessionId === options.execution.sessionId ? "parent" : record.sessionId === options.execution.sessionId ? "child" : "other"
381
+ });
382
+ /** One `run_worktree` action through the tool's own code, so the tool and the namespace cannot drift. */
383
+ const act = async (name, action, input) => {
384
+ run.signal.throwIfAborted();
385
+ if (disposed) throw new Error("Worktrees namespace run is disposed.");
386
+ options.assertAvailable();
387
+ const params = validateParams({
388
+ action,
389
+ ...typeof input === "object" && input !== null ? input : {}
390
+ });
391
+ const context = {
392
+ cwd: options.execution.cwd,
393
+ sessionId: options.execution.sessionId
394
+ };
395
+ const started = Date.now();
396
+ try {
397
+ const result = await executeRunWorktreeTool(params, options.operations, context, { signal: run.signal });
398
+ options.assertAvailable();
399
+ run.record?.({
400
+ name: `worktrees.${name}`,
401
+ tool: RUN_WORKTREE_TOOL_NAME,
402
+ args: params,
403
+ ok: true,
404
+ ms: Date.now() - started,
405
+ result
406
+ });
407
+ return result;
408
+ } catch (error) {
409
+ run.record?.({
410
+ name: `worktrees.${name}`,
411
+ tool: RUN_WORKTREE_TOOL_NAME,
412
+ args: params,
413
+ ok: false,
414
+ ms: Date.now() - started,
415
+ error: error instanceof Error ? error.message : String(error)
416
+ });
417
+ throw error;
418
+ }
419
+ };
420
+ const record = (result) => view(result.details.records[0]);
421
+ const members = Object.create(null);
422
+ members.list = async () => json((await act("list", "list", {})).details.records.map(view));
423
+ members.status = async (id) => {
424
+ const { details } = await act("status", "status", { id });
425
+ return json({
426
+ worktree: view(details.records[0]),
427
+ dirtyFiles: details.dirtyFiles ?? []
428
+ });
429
+ };
430
+ members.spawn = async (request) => json(record(await act("spawn", "spawn_worktree", request)));
431
+ members.close = async (id, settings) => json(record(await act("close", "close_worktree", {
432
+ ...settings,
433
+ id
434
+ })));
435
+ members.merge = async (id, settings) => json(record(await act("merge", "merge", {
436
+ ...settings,
437
+ id
438
+ })));
439
+ members.prune = async (settings) => {
440
+ const { plan } = (await act("prune", "prune", settings)).details;
441
+ return json({
442
+ ...plan,
443
+ remove: plan.remove.map(view),
444
+ forget: plan.forget.map(view),
445
+ keptDirty: plan.keptDirty.map(view)
446
+ });
447
+ };
448
+ members.send = async (id, message) => {
449
+ await act("send", "send", {
450
+ id,
451
+ message
452
+ });
453
+ };
454
+ members.messages = async (id) => json((await act("messages", "messages", { id })).details.messages);
455
+ return members;
456
+ }
457
+ };
458
+ }
459
+ //#endregion
295
460
  //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
296
461
  function progressResult(action, label) {
297
462
  return {
@@ -302,9 +467,9 @@ function progressResult(action, label) {
302
467
  details: { action }
303
468
  };
304
469
  }
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.");
470
+ function createGitSession({ context, host: serverHost, agent, signal }) {
471
+ if (serverHost.context.directEvents === void 0) throw new Error("Git server plugin requires the session direct event bus.");
472
+ if (serverHost.context.sessionId === void 0) throw new Error("Git server plugin requires a session identity.");
308
473
  const sessionId = serverHost.context.sessionId;
309
474
  const directEvents = serverHost.context.directEvents;
310
475
  const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);
@@ -315,20 +480,12 @@ function createGitSession({ context, host: serverHost }) {
315
480
  sessionDelivery: () => readPoSessionDelivery(context)
316
481
  });
317
482
  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",
483
+ name: "codemode-how-to-git",
484
+ 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
485
  kind: "skill",
331
- read: () => readPackageResource(import.meta.url, "src/prompts/po-use-git/SKILL.md")
486
+ pinned: true,
487
+ path: packageResourcePath(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md"),
488
+ read: () => readPackageResource(import.meta.url, "src/prompts/codemode-how-to-git/SKILL.md")
332
489
  }];
333
490
  const tool = {
334
491
  name: RUN_WORKTREE_TOOL_NAME,
@@ -348,17 +505,37 @@ function createGitSession({ context, host: serverHost }) {
348
505
  const changed = (_event, execution) => {
349
506
  directEvents.publish(GIT_SESSION_CHANGED_EVENT, execution.sessionId, { version: 1 });
350
507
  };
508
+ const hooks = [{
509
+ event: "tool_execution_end",
510
+ handle: changed
511
+ }, {
512
+ event: "agent_settled",
513
+ handle: changed
514
+ }];
515
+ if (agent) {
516
+ const namespace = createWorktreeNamespace({
517
+ operations,
518
+ execution: agent.context,
519
+ assertAvailable: () => {
520
+ if (signal?.aborted) throw new Error("Git session is unavailable.");
521
+ }
522
+ });
523
+ context.inject([PO_CODEMODE_SERVICE], (scoped) => {
524
+ const codemode = readPoCodemode(scoped);
525
+ if (!codemode) return void 0;
526
+ const registration = codemode.register("@agimon-ai/po-git", namespace);
527
+ const restriction = agent.registerToolRestriction({ excludedTools: [RUN_WORKTREE_TOOL_NAME] });
528
+ return () => {
529
+ restriction.dispose();
530
+ registration.dispose();
531
+ };
532
+ });
533
+ }
351
534
  return {
352
535
  value: {
353
536
  resources,
354
537
  tool,
355
- hooks: [{
356
- event: "tool_execution_end",
357
- handle: changed
358
- }, {
359
- event: "agent_settled",
360
- handle: changed
361
- }]
538
+ hooks
362
539
  },
363
540
  onDispose: () => messageInbox.close()
364
541
  };
@@ -469,11 +646,11 @@ const NAME_STATUS = {
469
646
  T: "modified"
470
647
  };
471
648
  /** Line count of a small regular file, or undefined when it is binary, large, or not a regular file. */
472
- function untrackedLines(root, relPath) {
649
+ async function untrackedLines(root, relPath) {
473
650
  const absolute = path.join(root, relPath);
474
651
  let stat;
475
652
  try {
476
- stat = fs.lstatSync(absolute);
653
+ stat = await fs.lstat(absolute);
477
654
  } catch {
478
655
  return;
479
656
  }
@@ -482,7 +659,7 @@ function untrackedLines(root, relPath) {
482
659
  binary: false
483
660
  };
484
661
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
485
- const content = fs.readFileSync(absolute);
662
+ const content = await fs.readFile(absolute);
486
663
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
487
664
  lines: 0,
488
665
  binary: true
@@ -605,8 +782,8 @@ function createBranchDiff() {
605
782
  "--exclude-standard",
606
783
  "-z"
607
784
  ]));
608
- untracked.forEach((relPath, index) => {
609
- const counted = index < MAX_UNTRACKED_COUNTED ? untrackedLines(root, relPath) : void 0;
785
+ for (const [index, relPath] of untracked.entries()) {
786
+ const counted = index < MAX_UNTRACKED_COUNTED ? await untrackedLines(root, relPath) : void 0;
610
787
  entries.push({
611
788
  path: relPath,
612
789
  status: "untracked",
@@ -614,18 +791,20 @@ function createBranchDiff() {
614
791
  removed: 0,
615
792
  ...counted?.binary ? { binary: true } : {}
616
793
  });
617
- });
794
+ }
618
795
  entries.sort((left, right) => left.path.localeCompare(right.path));
619
796
  const conflicts = [...unmerged];
620
- const rebasing = [await readLine$1(root, [
797
+ const rebaseMerge = await readLine$1(root, [
621
798
  "rev-parse",
622
799
  "--git-path",
623
800
  "rebase-merge"
624
- ]), await readLine$1(root, [
801
+ ]);
802
+ const rebaseApply = await readLine$1(root, [
625
803
  "rev-parse",
626
804
  "--git-path",
627
805
  "rebase-apply"
628
- ])].some((gitPath) => gitPath !== void 0 && fs.existsSync(path.resolve(root, gitPath)));
806
+ ]);
807
+ 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
808
  const upstreamRef = branch === void 0 ? void 0 : await upstreamOf(root, branch);
630
809
  let upstream;
631
810
  if (upstreamRef !== void 0) {
@@ -670,7 +849,7 @@ function createBranchDiff() {
670
849
  if (entry === void 0) throw new Error("That path is not in this review.");
671
850
  if (entry.status === "untracked") {
672
851
  const absolute = path.join(review.root, entry.path);
673
- const stat = fs.lstatSync(absolute);
852
+ const stat = await fs.lstat(absolute);
674
853
  if (stat.isSymbolicLink()) return {
675
854
  path: entry.path,
676
855
  hunks: [{
@@ -678,7 +857,7 @@ function createBranchDiff() {
678
857
  rows: [{
679
858
  marker: "+",
680
859
  line: 1,
681
- content: fs.readlinkSync(absolute)
860
+ content: await fs.readlink(absolute)
682
861
  }]
683
862
  }]
684
863
  };
@@ -687,7 +866,7 @@ function createBranchDiff() {
687
866
  hunks: [],
688
867
  tooLarge: true
689
868
  };
690
- const content = fs.readFileSync(absolute);
869
+ const content = await fs.readFile(absolute);
691
870
  if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
692
871
  path: entry.path,
693
872
  hunks: [],
@@ -804,9 +983,9 @@ function isRecord(value) {
804
983
  return typeof value === "object" && value !== null && !Array.isArray(value);
805
984
  }
806
985
  /** The workspace key: the canonical root, so a symlinked path and its target share one entry. */
807
- function workspaceKey(workspaceRoot) {
986
+ async function workspaceKey(workspaceRoot) {
808
987
  try {
809
- return fs.realpathSync(workspaceRoot);
988
+ return await fs.realpath(workspaceRoot);
810
989
  } catch {
811
990
  return path.resolve(workspaceRoot);
812
991
  }
@@ -855,7 +1034,7 @@ function publicView(config) {
855
1034
  * the saved one, and only for the same host: a token never follows a host it
856
1035
  * was not entered for.
857
1036
  */
858
- function parseGitAuthSave(request, current, homeDir) {
1037
+ async function parseGitAuthSave(request, current, homeDir) {
859
1038
  if (!isRecord(request)) throw invalidRequest("The git auth request is not an object.", "Send method none, ssh or https.");
860
1039
  if (request.method === "none") return { method: "none" };
861
1040
  if (request.method === "ssh") {
@@ -865,7 +1044,7 @@ function parseGitAuthSave(request, current, homeDir) {
865
1044
  if (!keyPath.startsWith("/") && !keyPath.startsWith("~/")) throw invalidRequest("The key path is not absolute.", "Give the key as an absolute path or one under ~/.");
866
1045
  let isFile = false;
867
1046
  try {
868
- isFile = fs.statSync(expandHome(keyPath, homeDir)).isFile();
1047
+ isFile = (await fs.stat(expandHome(keyPath, homeDir))).isFile();
869
1048
  } catch {
870
1049
  isFile = false;
871
1050
  }
@@ -900,16 +1079,16 @@ function parseGitAuthSave(request, current, homeDir) {
900
1079
  throw invalidRequest("The git auth method is unknown.", "Send method none, ssh or https.");
901
1080
  }
902
1081
  /** Tightens a file or folder something left readable by group or others. */
903
- function tighten(target, mode) {
1082
+ async function tighten(target, mode) {
904
1083
  try {
905
- if ((fs.statSync(target).mode & LOOSE_BITS) !== 0) fs.chmodSync(target, mode);
1084
+ if (((await fs.stat(target)).mode & LOOSE_BITS) !== 0) await fs.chmod(target, mode);
906
1085
  } catch {}
907
1086
  }
908
1087
  function createGitAuthStore(homeDir = os.homedir()) {
909
1088
  const file = credentialsFile(homeDir);
910
- const readFile = () => {
911
- tighten(file, OWNER_ONLY_FILE);
912
- const raw = readJson(file);
1089
+ const readFile = async () => {
1090
+ await tighten(file, OWNER_ONLY_FILE);
1091
+ const raw = await readJson(file);
913
1092
  const workspaces = {};
914
1093
  if (isRecord(raw) && raw.version === 1 && isRecord(raw.workspaces)) for (const [key, value] of Object.entries(raw.workspaces)) {
915
1094
  const config = parseConfig(value);
@@ -920,28 +1099,28 @@ function createGitAuthStore(homeDir = os.homedir()) {
920
1099
  workspaces
921
1100
  };
922
1101
  };
923
- const read = (workspaceRoot) => readFile().workspaces[workspaceKey(workspaceRoot)] ?? { method: "none" };
1102
+ const read = async (workspaceRoot) => (await readFile()).workspaces[await workspaceKey(workspaceRoot)] ?? { method: "none" };
924
1103
  return {
925
1104
  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);
1105
+ view: async (workspaceRoot) => publicView(await read(workspaceRoot)),
1106
+ async save(workspaceRoot, request) {
1107
+ const key = await workspaceKey(workspaceRoot);
1108
+ const current = await readFile();
1109
+ const next = await parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
931
1110
  const root = poGitRoot(homeDir);
932
- fs.mkdirSync(root, {
1111
+ await fs.mkdir(root, {
933
1112
  recursive: true,
934
1113
  mode: OWNER_ONLY_DIRECTORY
935
1114
  });
936
- fs.chmodSync(root, OWNER_ONLY_DIRECTORY);
1115
+ await fs.chmod(root, OWNER_ONLY_DIRECTORY);
937
1116
  const workspaces = { ...current.workspaces };
938
1117
  if (next.method === "none") delete workspaces[key];
939
1118
  else workspaces[key] = next;
940
- writeJsonAtomic(file, {
1119
+ await writeJsonAtomic(file, {
941
1120
  version: 1,
942
1121
  workspaces
943
1122
  });
944
- fs.chmodSync(file, OWNER_ONLY_FILE);
1123
+ await fs.chmod(file, OWNER_ONLY_FILE);
945
1124
  return publicView(next);
946
1125
  }
947
1126
  };
@@ -1187,9 +1366,9 @@ var route_server_default = defineRoute({
1187
1366
  if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1188
1367
  const root = rootOf(url);
1189
1368
  if (root instanceof Response) return root;
1190
- if (request.method === "GET") return Response.json(gitAuth.view(root));
1369
+ if (request.method === "GET") return Response.json(await gitAuth.view(root));
1191
1370
  try {
1192
- return Response.json(gitAuth.save(root, await request.json().catch(() => void 0)));
1371
+ return Response.json(await gitAuth.save(root, await request.json().catch(() => void 0)));
1193
1372
  } catch (error) {
1194
1373
  if (error instanceof PoGitExpectedError && error.code === "invalid_request") {
1195
1374
  const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
@@ -1213,7 +1392,7 @@ var route_server_default = defineRoute({
1213
1392
  if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1214
1393
  let recordedBaseRef;
1215
1394
  try {
1216
- recordedBaseRef = createWorktreeRegistry(registryFile(cwd, homeDir)).list().find((record) => record.sessionId === context.sessionId)?.baseRef;
1395
+ recordedBaseRef = (await createWorktreeRegistry(await registryFile(cwd, homeDir)).list()).find((record) => record.sessionId === context.sessionId)?.baseRef;
1217
1396
  } catch {
1218
1397
  recordedBaseRef = void 0;
1219
1398
  }
@@ -1413,7 +1592,7 @@ async function rebasing(root) {
1413
1592
  "--git-path",
1414
1593
  name
1415
1594
  ]);
1416
- if (gitPath !== void 0 && fs.existsSync(path.resolve(root, gitPath))) return true;
1595
+ if (gitPath !== void 0 && await fs.access(path.resolve(root, gitPath)).then(() => true, () => false)) return true;
1417
1596
  }
1418
1597
  return false;
1419
1598
  }
@@ -1583,9 +1762,9 @@ const GIT_CHANGES_TYPE = "git_changes";
1583
1762
  * DESIGN PATTERNS:
1584
1763
  * - Apart from the worktrees channel. Worktrees are repository management;
1585
1764
  * 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.
1765
+ * - Push, not poll. The session's server plugin publishes a "changed" event
1766
+ * after every tool call and settled turn; recounts are coalesced to one in
1767
+ * flight plus one rerun, so a burst of tool calls costs at most two.
1589
1768
  * - One sync command per session. A rebase and a push must never overlap.
1590
1769
  * - Auth is read fresh for each command from the session's workspace, and goes
1591
1770
  * only into that command's git child.
@@ -1646,9 +1825,9 @@ function createGitChangesChannel(options = {}) {
1646
1825
  const rerun = /* @__PURE__ */ new Set();
1647
1826
  const busy = /* @__PURE__ */ new Set();
1648
1827
  /** The base the session's own worktree was created from, when it has one. */
1649
- const recordedBaseRef = (scope) => {
1828
+ const recordedBaseRef = async (scope) => {
1650
1829
  try {
1651
- return createWorktreeRegistry(registryFile(scope.cwd, homeDir)).list().find((record) => record.sessionId === scope.sessionId)?.baseRef;
1830
+ return (await createWorktreeRegistry(await registryFile(scope.cwd, homeDir)).list()).find((record) => record.sessionId === scope.sessionId)?.baseRef;
1652
1831
  } catch {
1653
1832
  return;
1654
1833
  }
@@ -1681,8 +1860,7 @@ function createGitChangesChannel(options = {}) {
1681
1860
  return;
1682
1861
  }
1683
1862
  inFlight.add(sessionId);
1684
- const base = recordedBaseRef(scope);
1685
- diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base }).then((review) => {
1863
+ recordedBaseRef(scope).then((base) => diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base })).then((review) => {
1686
1864
  if (scopes.has(sessionId)) update(sessionId, { changes: review?.changes });
1687
1865
  }).catch((error) => {
1688
1866
  host.onNotice(`git diff refresh failed for ${sessionId} (${error instanceof Error ? error.name : "unknown"})`);
@@ -1691,12 +1869,12 @@ function createGitChangesChannel(options = {}) {
1691
1869
  if (rerun.delete(sessionId)) refresh(sessionId);
1692
1870
  });
1693
1871
  };
1694
- const run = (scope, command) => {
1872
+ const run = async (scope, command) => {
1695
1873
  const workspaceRoot = scope.sessionContext?.workspaceRoot ?? scope.cwd;
1696
- const base = recordedBaseRef(scope);
1874
+ const base = await recordedBaseRef(scope);
1697
1875
  const context = {
1698
1876
  cwd: scope.cwd,
1699
- auth: auth.read(workspaceRoot),
1877
+ auth: await auth.read(workspaceRoot),
1700
1878
  ...base === void 0 ? {} : { recordedBaseRef: base }
1701
1879
  };
1702
1880
  if (command.action === "pull") return sync.pull(context);
@@ -1794,9 +1972,9 @@ function payloadOf(state) {
1794
1972
  };
1795
1973
  }
1796
1974
  /** Worktrees visible to this session, using the hub's live session table for ownership. */
1797
- function viewsFor(scope, deps) {
1975
+ async function viewsFor(scope, deps) {
1798
1976
  try {
1799
- return createWorktreeRegistry(registryFile(scope.cwd, deps.homeDir)).list().flatMap((record) => {
1977
+ return (await createWorktreeRegistry(await registryFile(scope.cwd, deps.homeDir)).list()).flatMap((record) => {
1800
1978
  if (record.parentSessionId === void 0 && record.sessionId !== scope.sessionId) return [];
1801
1979
  const owned = record.parentSessionId === void 0 || record.parentSessionId === scope.sessionId;
1802
1980
  const unowned = !owned && !deps.sessionService.isLive(record.parentSessionId);
@@ -1821,11 +1999,15 @@ function sameList(left, right) {
1821
1999
  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
2000
  });
1823
2001
  }
1824
- function errorTargetFor(command) {
1825
- return command.action === "create" ? { action: "create" } : {
2002
+ function errorTargetFor(command, error) {
2003
+ if (command.action === "close") return {
1826
2004
  action: "close",
1827
2005
  id: command.id
1828
2006
  };
2007
+ return error instanceof PoGitExpectedError && error.code === "rift_missing" ? {
2008
+ action: "create",
2009
+ riftMissing: true
2010
+ } : { action: "create" };
1829
2011
  }
1830
2012
  /** A command the page sent, or undefined when the frame was not one. */
1831
2013
  function parseCommand(payload) {
@@ -1853,11 +2035,15 @@ function parseCommand(payload) {
1853
2035
  /**
1854
2036
  * What the reader is told when a command fails.
1855
2037
  *
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.
2038
+ * An expected failure explains itself, minus the `[code]` tag and recovery line
2039
+ * written for the agent's tool result. A dirty close is said in the person's
2040
+ * terms, because its message tells the agent to pass `force`. Anything else is
2041
+ * summarised, never echoed: a git error can carry a remote URL with a token in it.
1858
2042
  */
1859
2043
  function failureText(error) {
1860
- return error instanceof PoGitExpectedError ? error.message : "The worktree operation failed.";
2044
+ if (!(error instanceof PoGitExpectedError)) return "The worktree operation failed.";
2045
+ if (error.code === "worktree_dirty") return "The worktree has uncommitted changes.";
2046
+ return error.message.replace(/^\[[a-z_]+\] /u, "").split("\nRecovery:")[0] ?? error.message;
1861
2047
  }
1862
2048
  /** Starts one direct-event subscription per managed session. */
1863
2049
  function createWorktreesChannel(options = {}) {
@@ -1910,10 +2096,13 @@ function createWorktreesChannel(options = {}) {
1910
2096
  return generation;
1911
2097
  };
1912
2098
  /** Republishes a session, skipping a frame that would say nothing new. */
1913
- const publish = (scope, force) => {
2099
+ const publish = async (scope, force) => {
2100
+ const generation = generations.get(scope.sessionId);
2101
+ const worktreeViews = await viewsFor(scope, views);
2102
+ if (generation === void 0 || generations.get(scope.sessionId) !== generation) return;
1914
2103
  const previous = latest.get(scope.sessionId);
1915
2104
  const next = {
1916
- worktrees: viewsFor(scope, views),
2105
+ worktrees: worktreeViews,
1917
2106
  pending: previous?.pending,
1918
2107
  error: previous?.error,
1919
2108
  errorTarget: previous?.errorTarget
@@ -1925,7 +2114,7 @@ function createWorktreesChannel(options = {}) {
1925
2114
  const mark = (scope, pending, error, errorTarget) => {
1926
2115
  const previous = latest.get(scope.sessionId);
1927
2116
  latest.set(scope.sessionId, {
1928
- worktrees: previous?.worktrees ?? viewsFor(scope, views),
2117
+ worktrees: previous?.worktrees ?? [],
1929
2118
  pending,
1930
2119
  error,
1931
2120
  errorTarget
@@ -1965,15 +2154,15 @@ function createWorktreesChannel(options = {}) {
1965
2154
  sessionAdded(scope) {
1966
2155
  newGeneration(scope.sessionId);
1967
2156
  publish(scope, false);
1968
- const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, (payload) => {
2157
+ const unsubscribe = host.directEvents.subscribe(GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, async (payload) => {
1969
2158
  if (!isWorktreeLifecycleEvent(payload)) return;
1970
2159
  let sameRepository = false;
1971
2160
  try {
1972
- sameRepository = registryFile(scope.cwd, options.homeDir) === registryFile(payload.repositoryRoot, options.homeDir);
2161
+ sameRepository = await registryFile(scope.cwd, options.homeDir) === await registryFile(payload.repositoryRoot, options.homeDir);
1973
2162
  } catch {
1974
2163
  sameRepository = false;
1975
2164
  }
1976
- if (sameRepository) publish(scope, false);
2165
+ if (sameRepository) await publish(scope, false);
1977
2166
  });
1978
2167
  subscriptions.set(scope.sessionId, unsubscribe);
1979
2168
  },
@@ -2006,7 +2195,7 @@ function createWorktreesChannel(options = {}) {
2006
2195
  }).catch((error) => {
2007
2196
  if (generations.get(scope.sessionId) !== generation) return;
2008
2197
  host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);
2009
- mark(scope, void 0, failureText(error), errorTargetFor(command));
2198
+ mark(scope, void 0, failureText(error), errorTargetFor(command, error));
2010
2199
  }).finally(() => {
2011
2200
  if (generations.get(scope.sessionId) === generation) busy.delete(scope.sessionId);
2012
2201
  });
@@ -2028,20 +2217,8 @@ var agent_settled_server_default = defineHook((context) => context.root.hooks[1]
2028
2217
  //#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts
2029
2218
  var tool_execution_end_server_default = defineHook((context) => context.root.hooks[0]);
2030
2219
  //#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
2220
  //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poUseGit.server.ts
2044
- var poUseGit_server_default$1 = (context) => context.root.resources[1];
2221
+ var poUseGit_server_default$1 = (context) => context.root.resources[0];
2045
2222
  //#endregion
2046
2223
  //#region src/extensions/workspaces/sessions/(backend)/resource/poUseGit.server.ts
2047
2224
  var poUseGit_server_default = defineResource(poUseGit_server_default$1);
@@ -2051,19 +2228,22 @@ var runWorktree_server_default = defineServerTool((context) => context.root.tool
2051
2228
  //#endregion
2052
2229
  //#region generated/server.ts
2053
2230
  const at = (value, context) => typeof value === "function" ? value(context) : value;
2231
+ /* v8 ignore next -- generated normalization branch */
2232
+ const defined = (values) => values.filter((value) => value !== void 0);
2054
2233
  const via = (identity, value) => value === void 0 ? void 0 : {
2055
2234
  ...identity,
2056
2235
  ...value
2057
2236
  };
2058
- const facet = defineServerPlugin({
2237
+ const serverPlugin = defineServerPlugin({
2059
2238
  name: "@agimon-ai/po-git",
2060
2239
  providerOverrides: composeServerProviderOverrides(root_server_default),
2240
+ injects: composeRootInjects({ session: [root_server_default] }),
2061
2241
  global: (context) => ({
2062
2242
  get api() {
2063
2243
  return [at(route_server_default, context)];
2064
2244
  },
2065
2245
  get channels() {
2066
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2246
+ return [() => via({ frameType: "git_changes" }, at(git_changes_server_default, context)), () => via({ frameType: "git_worktrees" }, at(git_worktrees_server_default, context))];
2067
2247
  }
2068
2248
  }),
2069
2249
  workspace: (context) => ({
@@ -2071,11 +2251,11 @@ const facet = defineServerPlugin({
2071
2251
  return [at(route_server_default, context)];
2072
2252
  },
2073
2253
  get channels() {
2074
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2254
+ 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
2255
  }
2076
2256
  }),
2077
2257
  session: async (context) => {
2078
- const scopeSession = await root_server_default(context);
2258
+ const scopeSession = await context.scopeRoot("session", root_server_default, context);
2079
2259
  const contextSession = {
2080
2260
  ...context,
2081
2261
  root: scopeSession.value
@@ -2086,17 +2266,13 @@ const facet = defineServerPlugin({
2086
2266
  return [at(route_server_default, context)];
2087
2267
  },
2088
2268
  get channels() {
2089
- return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2269
+ 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
2270
  },
2091
2271
  get hooks() {
2092
2272
  return [via({ event: "agent_settled" }, at(agent_settled_server_default, contextSession)), via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession))];
2093
2273
  },
2094
2274
  get resources() {
2095
- return [
2096
- at(poGit_server_default, contextSession),
2097
- at(poGitReadme_server_default, contextSession),
2098
- at(poUseGit_server_default, contextSession)
2099
- ];
2275
+ return [at(poUseGit_server_default, contextSession)];
2100
2276
  },
2101
2277
  get tools() {
2102
2278
  return [via({ name: "run_worktree" }, at(runWorktree_server_default, contextSession))];
@@ -2109,6 +2285,6 @@ const facet = defineServerPlugin({
2109
2285
  }
2110
2286
  });
2111
2287
  //#endregion
2112
- export { facet as default, facet };
2288
+ export { serverPlugin as default, serverPlugin };
2113
2289
 
2114
2290
  //# sourceMappingURL=server.mjs.map