pum-agent 0.2.27-beta.2 → 0.2.29-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **A compact coding agent for the terminal.**
6
6
 
7
- Plan, edit, run commands, review Markdown, and coordinate parallel Git worktrees without leaving the TUI.
7
+ Plan, edit, run commands, review Markdown, and coordinate parallel subagents without leaving the TUI.
8
8
 
9
9
  [![CI](https://github.com/eugen1763/Pum/actions/workflows/ci.yml/badge.svg)](https://github.com/eugen1763/Pum/actions/workflows/ci.yml)
10
10
  [![npm beta](https://img.shields.io/npm/v/pum-agent/beta?label=npm%20beta)](https://www.npmjs.com/package/pum-agent)
@@ -51,8 +51,9 @@ drives the actual TUI and converts the captured cells to SVG.
51
51
  ## What it does
52
52
 
53
53
  - **A full coding loop** — `read`, `write`, `edit`, and `bash`, with streaming Markdown, syntax highlighting, usage, cost, and Git status.
54
- - **Parallel subagents** — persistent agents in isolated Git worktrees that message each other durably and report to their spawner. See [Subagents](docs/subagents.md).
54
+ - **Parallel subagents** — persistent agents that share the project by default, with optional isolated Git worktrees. They message each other durably and report to their spawner. See [Subagents](docs/subagents.md).
55
55
  - **Goals that outlive a turn** — `/goal` keeps working, reviewed after each turn by a judge that reads but never writes. See [Goals](docs/goals.md).
56
+ - **Project memory across sessions** — the main agent maintains private Markdown facts that follow linked Git worktrees. See [Tools](docs/tools.md#project-memory).
56
57
  - **Supervised processes** — background shells and external triggers such as `gh run watch`, which wake the exact agent that was waiting. See [Tools](docs/tools.md).
57
58
  - **Layered safeguards** — a path guard on the file tools, deterministic Check mode, and a native OS sandbox for the processes the model starts. See [Safety](docs/security.md).
58
59
  - **A terminal-first look** — nine themes, semantic colour overrides, and optional animation. See [Appearance](docs/appearance.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.27-beta.2",
3
+ "version": "0.2.29-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,7 @@ export type AgentTreeRow = {
18
18
  name: string;
19
19
  status?: SubagentSnapshot["status"];
20
20
  readonly?: boolean;
21
+ shared?: boolean;
21
22
  depth: number;
22
23
  metadata?: StatusMetadataValues;
23
24
  };
@@ -38,7 +39,7 @@ export function agentSelectorRowLayout(
38
39
  // One column remains clear for the pinned scrollbar.
39
40
  const contentColumns = Math.max(1, popupColumns - indent - 1);
40
41
  const label = row.status
41
- ? `${row.name}${row.readonly ? " · readonly" : ""} · ${row.status}`
42
+ ? `${row.name}${row.readonly ? " · readonly" : ""}${row.shared ? " · shared" : ""} · ${row.status}`
42
43
  : row.name;
43
44
  const minimumLabelWidth = Math.min(
44
45
  label.length,
@@ -77,9 +78,10 @@ export function buildAgentTree(agents: readonly SubagentSnapshot[]): AgentTreeRo
77
78
  name: agent.name,
78
79
  status: agent.status,
79
80
  readonly: agent.readonly === true,
81
+ shared: agent.usesWorktree === false,
80
82
  depth,
81
83
  metadata: {
82
- branch: agent.worktree.branch ?? null,
84
+ branch: agent.usesWorktree === false ? null : agent.worktree.branch ?? null,
83
85
  outgoingTokens: usage.outgoing,
84
86
  incomingTokens: usage.incoming,
85
87
  cacheReadTokens: usage.cacheRead,
@@ -100,9 +102,10 @@ export function buildAgentTree(agents: readonly SubagentSnapshot[]): AgentTreeRo
100
102
  name: agent.name,
101
103
  status: agent.status,
102
104
  readonly: agent.readonly === true,
105
+ shared: agent.usesWorktree === false,
103
106
  depth: 1,
104
107
  metadata: {
105
- branch: agent.worktree.branch ?? null,
108
+ branch: agent.usesWorktree === false ? null : agent.worktree.branch ?? null,
106
109
  outgoingTokens: usage.outgoing,
107
110
  incomingTokens: usage.incoming,
108
111
  cacheReadTokens: usage.cacheRead,
package/src/animation.tsx CHANGED
@@ -143,7 +143,12 @@ const RULE_CHARS_PER_MS = 0.04;
143
143
  const RULE_HIGHLIGHT_WIDTH = 10;
144
144
  const COMET_CHARS_PER_MS = 0.035;
145
145
  const ELECTRIC_FRAME_MS = 140;
146
- const CONSTELLATION_SPACING = 13;
146
+ /** Average columns per star. Each slot places its own star inside this span. */
147
+ const CONSTELLATION_SPACING = 11;
148
+ /** Base period of one star. A hashed factor spreads the real periods around it. */
149
+ const CONSTELLATION_TWINKLE_MS = 2600;
150
+ /** How much of its period a star burns for. It is dark and moves in the rest. */
151
+ const CONSTELLATION_LIFETIME = 0.72;
147
152
  const RANDOM_CONSTELLATION_CYCLE_MS = 2000;
148
153
  /** How much of one cycle a single sparkle burns for, start to finish. */
149
154
  const RANDOM_CONSTELLATION_LIFETIME = 0.5;
@@ -658,6 +663,44 @@ const hashPosition = (frame: number, seed: number, width: number) => {
658
663
  return Math.abs(value) % width;
659
664
  };
660
665
 
666
+ export type ConstellationStar = { column: number; strength: number };
667
+
668
+ /**
669
+ * One star of the constellation field, for the slot of columns it belongs to.
670
+ *
671
+ * The field used to be a fixed grid: a star every `CONSTELLATION_SPACING`
672
+ * columns, in the same place for the whole run. Each slot now holds a star with
673
+ * its own period, its own start, its own peak brightness, and a column that is
674
+ * re-drawn for every life. A star is dark between two lives, so it never moves
675
+ * while it is visible. Returns `null` while the slot is empty.
676
+ */
677
+ export function constellationStar(
678
+ slot: number,
679
+ elapsedMs: number,
680
+ role: WorkingRuleRole,
681
+ ): ConstellationStar | null {
682
+ if (slot < 0) return null;
683
+ const seed = roleSeed(role);
684
+ const period = CONSTELLATION_TWINKLE_MS *
685
+ (0.55 + (hashPosition(slot * 13 + 7, seed + 3, 100) / 100) * 1.05);
686
+ const offset = (hashPosition(slot * 29 + 5, seed + 11, 1000) / 1000) * period;
687
+ const time = elapsedMs + offset;
688
+ const life = (time % period) / period;
689
+ if (life >= CONSTELLATION_LIFETIME) return null;
690
+
691
+ const epoch = Math.floor(time / period);
692
+ // Two columns of margin on each side keep the halo of one star clear of the
693
+ // next slot, so no two stars ever touch.
694
+ const span = CONSTELLATION_SPACING - 4;
695
+ const column = slot * CONSTELLATION_SPACING + 2 +
696
+ hashPosition(epoch * 31 + slot, seed * 7 + slot, span);
697
+ const amplitude = 0.5 + (hashPosition(epoch * 17 + slot, seed + 23, 100) / 100) * 0.5;
698
+ const strength = clampStrength(
699
+ Math.sin((life / CONSTELLATION_LIFETIME) * Math.PI) ** 1.2 * amplitude,
700
+ );
701
+ return { column, strength };
702
+ }
703
+
661
704
  /** Choose stable, random-looking sparkle centers for one two-second cycle. */
662
705
  export function randomConstellationCenters(
663
706
  width: number,
@@ -754,20 +797,24 @@ export function workingRuleCell(
754
797
  }
755
798
 
756
799
  if (mode === "constellation") {
757
- const seed = roleSeed(role);
758
- const spacing = (offset: number) => (column + offset + seed * 3) % CONSTELLATION_SPACING === 0;
759
- // A star ends at its own cell unless it carries a halo, which is what
760
- // makes it bloom on the rule rather than switch on and off in place.
761
- const halo = spacing(1) || spacing(-1);
762
- if (!spacing(0) && !halo) return { strength: 0, glyph: "─" };
763
- const star = spacing(0) ? column : spacing(1) ? column + 1 : column - 1;
764
- // Stars sit thirteen columns apart, so any fixed phase step per column put
765
- // them all within a fifth of a radian of each other and they blinked in
766
- // unison. A hashed offset gives each one its own place in the cycle.
767
- const phase = elapsedMs / 620 + hashPosition(star, seed, 628) / 100;
768
- const peak = 0.12 + ((Math.sin(phase) + 1) / 2) * 0.88;
769
- if (!spacing(0)) return { strength: peak * CONSTELLATION_HALO, glyph: "·" };
770
- return { strength: peak, glyph: peak > 0.82 ? "✦" : peak > 0.48 ? "✧" : "·" };
800
+ const slot = Math.floor(column / CONSTELLATION_SPACING);
801
+ let strength = 0;
802
+ let glyph = "─";
803
+ // A star can sit in the neighbouring slot and still reach this column with
804
+ // its halo, which is what makes it bloom instead of switching on in place.
805
+ for (let index = slot - 1; index <= slot + 1; index++) {
806
+ const star = constellationStar(index, elapsedMs, role);
807
+ if (!star || star.column >= width) continue;
808
+ const distance = Math.abs(column - star.column);
809
+ if (distance > 1) continue;
810
+ const lit = distance === 0 ? star.strength : star.strength * CONSTELLATION_HALO;
811
+ if (lit <= strength) continue;
812
+ strength = lit;
813
+ glyph = distance > 0 ? "·"
814
+ : star.strength > 0.82 ? "✦"
815
+ : star.strength > 0.48 ? "✧" : "·";
816
+ }
817
+ return { strength, glyph };
771
818
  }
772
819
 
773
820
  if (mode === "random-constellation") {
package/src/app.tsx CHANGED
@@ -232,6 +232,7 @@ import {
232
232
  type ShellManagerLike,
233
233
  } from "./processes-popup";
234
234
  import type { TerminalTitleController } from "./terminal-title";
235
+ import { resolveOrcaStatusState, type OrcaStatusController } from "./orca-status";
235
236
  import { readClipboardText } from "./text-paste";
236
237
  import { copyTextToClipboard } from "./clipboard";
237
238
  import { NewsPopup } from "./news-popup";
@@ -831,6 +832,7 @@ export function App({
831
832
  shellManager,
832
833
  messageCacheController,
833
834
  terminalTitle,
835
+ orcaStatus,
834
836
  startupWarnings = [],
835
837
  onSandboxModeChange,
836
838
  sandboxWarningSource,
@@ -870,6 +872,7 @@ export function App({
870
872
  shellManager?: ShellManagerLike;
871
873
  messageCacheController?: MessageCacheController;
872
874
  terminalTitle?: TerminalTitleController;
875
+ orcaStatus?: OrcaStatusController;
873
876
  /** Visible process-local warnings. These lines never enter pi session context. */
874
877
  startupWarnings?: readonly string[];
875
878
  onSandboxModeChange?: (mode: NonNullable<PumSettings["sandboxMode"]>) => void;
@@ -1242,7 +1245,9 @@ export function App({
1242
1245
  });
1243
1246
  const visibleModelId = activeAgent?.modelId.split("/").slice(1).join("/") || modelId;
1244
1247
  const visibleThinkingLevel = activeAgent?.thinkingLevel ?? thinkingLevel;
1245
- const visibleBranch = activeAgent?.worktree.branch ?? branch;
1248
+ const visibleBranch = activeAgent?.usesWorktree === false
1249
+ ? branch
1250
+ : activeAgent?.worktree.branch ?? branch;
1246
1251
  const visibleElapsedSec = activeAgent ? agentElapsedSec : elapsedSec;
1247
1252
  const visibleUsage = activeAgent?.usage ?? usage;
1248
1253
  const agentTreeRows = buildAgentTree(agents);
@@ -1949,11 +1954,28 @@ export function App({
1949
1954
  useEffect(() => {
1950
1955
  // A delegate answering counts as work even though it is not a managed
1951
1956
  // agent, or the terminal reads as idle while AFK decides something.
1957
+ orcaStatus?.update({
1958
+ state: resolveOrcaStatusState({
1959
+ working: busy || activeSubagentCount > 0 || afkAnswering,
1960
+ waitingForUser: Boolean(visibleQuestionnaire),
1961
+ }),
1962
+ model: session.agent.state.model?.id,
1963
+ sessionKey: session.sessionId,
1964
+ });
1952
1965
  terminalTitle?.update({
1953
1966
  working: busy || activeSubagentCount > 0 || afkAnswering,
1954
1967
  activeSubagentCount,
1955
1968
  });
1956
- }, [terminalTitle, busy, activeSubagentCount, afkAnswering]);
1969
+ }, [
1970
+ terminalTitle,
1971
+ orcaStatus,
1972
+ busy,
1973
+ activeSubagentCount,
1974
+ afkAnswering,
1975
+ visibleQuestionnaire,
1976
+ session.sessionId,
1977
+ session.agent.state.model?.id,
1978
+ ]);
1957
1979
 
1958
1980
  useEffect(() => spawnPreviewManager?.subscribe(() => {
1959
1981
  setSpawnPreviewRevision((revision) => revision + 1);
@@ -4180,7 +4202,10 @@ export function App({
4180
4202
  appendRequesterLine(requesterAgentId, {
4181
4203
  kind: "text",
4182
4204
  role: "system",
4183
- text: `background agent started: ${spawned.name} (${spawned.id})\n${spawned.worktree.branch}\n${spawned.worktree.path}`,
4205
+ text: `background agent started: ${spawned.name} (${spawned.id})\n`
4206
+ + (spawned.usesWorktree
4207
+ ? `${spawned.worktree.branch}\n${spawned.worktree.path}`
4208
+ : `shared directory\n${spawned.worktree.path}`),
4184
4209
  });
4185
4210
  })().catch((error) => {
4186
4211
  appendRequesterLine(requesterAgentId, {
@@ -4403,7 +4428,7 @@ export function App({
4403
4428
  const call: ToolCall = {
4404
4429
  id,
4405
4430
  name: "bash",
4406
- args: [command.split("\n")[0]!.trim()],
4431
+ args: toolArgs("bash", { command }, cwd),
4407
4432
  state: "running",
4408
4433
  startedAt: Date.now(),
4409
4434
  input: { command },
@@ -4442,7 +4467,7 @@ export function App({
4442
4467
  };
4443
4468
 
4444
4469
  const cachedBatchDisplay = (prompts: readonly string[]): string => [
4445
- `Run ${prompts.length} cached tasks with worktree subagents:`,
4470
+ `Run ${prompts.length} cached tasks with subagents:`,
4446
4471
  ...prompts.map((prompt, index) => `${index + 1}. ${prompt}`),
4447
4472
  ].join("\n");
4448
4473
 
package/src/commands.ts CHANGED
@@ -31,7 +31,7 @@ export const COMMANDS: Command[] = [
31
31
  },
32
32
  {
33
33
  name: "/background",
34
- description: "Start a managed worktree agent for the selected transcript",
34
+ description: "Start a shared-project agent for the selected transcript",
35
35
  },
36
36
  {
37
37
  name: "/history",
package/src/headless.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { mkdirSync } from "node:fs";
9
9
  import { AGENT_DIR, AUTH_PATH, MODELS_PATH, sessionDir } from "./config";
10
+ import { createMemoryExtension, MEMORY_EDIT_TOOL_NAME, MEMORY_READ_TOOL_NAME } from "./memory";
10
11
  import { checkPathsForProject, loadSettings } from "./settings";
11
12
  import { identityExtension } from "./identity";
12
13
  import { setWritingStyle, writingStyleExtension } from "./writing-style";
@@ -34,7 +35,14 @@ import { prepareHeadlessStatsOutput, type HeadlessStatsOutput } from "./headless
34
35
  * are not constructed here, and subagent, trigger, and message-cache tools
35
36
  * need the running TUI for routing and notifications.
36
37
  */
37
- const HEADLESS_TOOL_NAMES = ["read", "write", "edit", "bash"];
38
+ const HEADLESS_TOOL_NAMES = [
39
+ "read",
40
+ "write",
41
+ "edit",
42
+ "bash",
43
+ MEMORY_READ_TOOL_NAME,
44
+ MEMORY_EDIT_TOOL_NAME,
45
+ ];
38
46
 
39
47
  /**
40
48
  * Handle one hosted web-search call from a headless run.
@@ -188,6 +196,7 @@ async function runPromptSession(
188
196
  checkModePromptExtension,
189
197
  checkModeExtension,
190
198
  sandboxController.extension(),
199
+ createMemoryExtension({ agentDir: AGENT_DIR, audience: "main" }),
191
200
  ],
192
201
  },
193
202
  });
@@ -4,14 +4,14 @@ import { PopupFrame } from "./popup-frame";
4
4
  type HelpGroup = { title: string; controls: [string, string][] };
5
5
 
6
6
  export const HELP_SUMMARY_WIDE = [
7
- "PUM workflow — prompt or steer · cache prompts · attach images · run managed worktree agents in parallel",
8
- "switch transcripts · merge successful agents · persist sessions · use Settings and safety checks",
7
+ "PUM workflow — prompt or steer · cache prompts · attach images · run managed agents in parallel",
8
+ "switch transcripts · close successful agents · optional worktrees · persistent sessions · safety settings",
9
9
  ] as const;
10
10
 
11
11
  export const HELP_SUMMARY = [
12
12
  "Prompt or steer. Cache prompts. Attach images.",
13
- "Run managed worktree agents in parallel.",
14
- "Switch transcripts and merge successful agents.",
13
+ "Run agents in parallel. Use worktrees for isolation.",
14
+ "Switch transcripts and close successful agents.",
15
15
  "Sessions persist. Settings include safety checks.",
16
16
  ] as const;
17
17
 
@@ -59,7 +59,7 @@ export const HELP_GROUPS: HelpGroup[] = [
59
59
  ["/clear", "Start a fresh session"],
60
60
  ["/goal", "Set or control a goal"],
61
61
  ["/goalf", "Work out a goal, then start it"],
62
- ["/background", "Start a managed agent for the selected transcript"],
62
+ ["/background", "Start a shared-project agent for the selected transcript"],
63
63
  ["/history", "Browse saved sessions"],
64
64
  ["/providers", "Add, edit, or delete a provider; /login adds"],
65
65
  ["/news", "Open recent answers (News)"],
package/src/main.tsx CHANGED
@@ -7,10 +7,11 @@ import {
7
7
  ModelRuntime,
8
8
  SessionManager,
9
9
  } from "@earendil-works/pi-coding-agent";
10
- import { mkdirSync } from "node:fs";
10
+ import { mkdirSync, writeSync } from "node:fs";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { App } from "./app";
13
13
  import { AGENT_DIR, AUTH_PATH, MODELS_PATH, sessionDir } from "./config";
14
+ import { createMemoryExtension } from "./memory";
14
15
  import { checkPathsForProject, loadSettings } from "./settings";
15
16
  import { setBashOutputSettingsIfPresent } from "./bash-output";
16
17
  import { installWebSearch, webSearch } from "./web-search";
@@ -49,6 +50,7 @@ import type { StartupOptions } from "./cli";
49
50
  import { TodoToolsController } from "./todo-tools";
50
51
  import { installSelectionClipboard } from "./clipboard";
51
52
  import { TerminalTitleController } from "./terminal-title";
53
+ import { isOrcaTerminal, OrcaStatusController } from "./orca-status";
52
54
  import { SandboxController } from "./sandbox";
53
55
  import {
54
56
  createFilesystemSandboxExtension,
@@ -260,6 +262,9 @@ export async function start(
260
262
  (_agentId, isReadonly) => sandboxController.extension({ readonly: isReadonly }),
261
263
  (_agentId, isReadonly) => createFilesystemSandboxExtension({ readonly: isReadonly }),
262
264
  ],
265
+ childWorkerExtensionFactories: [
266
+ () => createMemoryExtension({ agentDir: AGENT_DIR, audience: "subagent" }),
267
+ ],
263
268
  sandboxModeSource: () => sandboxController.mode,
264
269
  });
265
270
  const subagentExtension = subagentManager.mainExtension();
@@ -295,6 +300,7 @@ export async function start(
295
300
  filesystemSandboxExtension,
296
301
  mainCheckModeExtension,
297
302
  sandboxExtension,
303
+ createMemoryExtension({ agentDir: AGENT_DIR, audience: "main" }),
298
304
  questionnaireManager.extension({ id: "main", name: "main" }),
299
305
  mainToolGroups.extension(),
300
306
  mainTodoTools.extension(),
@@ -342,6 +348,11 @@ export async function start(
342
348
 
343
349
  const renderer = await createCliRenderer({ exitOnCtrlC: false });
344
350
  const terminalTitle = new TerminalTitleController((title) => renderer.setTerminalTitle(title));
351
+ const orcaStatus = new OrcaStatusController(
352
+ isOrcaTerminal(process.env),
353
+ // Keep the control message outside OpenTUI's external-output path.
354
+ (sequence) => { writeSync(process.stdout.fd, sequence); },
355
+ );
345
356
  const selectionClipboard = installSelectionClipboard(renderer);
346
357
  const root = createRoot(renderer);
347
358
  // Reported on the restored terminal by the exit action below, so the message
@@ -445,6 +456,7 @@ export async function start(
445
456
  spawnPreviewManager={spawnPreviewManager}
446
457
  messageCacheController={messageCacheController}
447
458
  terminalTitle={terminalTitle}
459
+ orcaStatus={orcaStatus}
448
460
  startupWarnings={[
449
461
  ...(sandboxWarning ? [sandboxWarning] : []),
450
462
  ...(outerSandbox ? [
@@ -0,0 +1,76 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { posix, win32 } from "node:path";
4
+ import {
5
+ canonicalRealpathSync,
6
+ projectStorageKey,
7
+ type RuntimePlatform,
8
+ } from "./platform";
9
+
10
+ export type MemoryIdentity = {
11
+ key: string;
12
+ digest: string;
13
+ kind: "git" | "directory";
14
+ };
15
+
16
+ type MemoryIdentityOptions = {
17
+ platform?: RuntimePlatform;
18
+ gitPath?: (cwd: string, args: string[]) => string;
19
+ realpath?: (path: string, platform: RuntimePlatform) => string;
20
+ };
21
+
22
+ const resolvedIdentities = new Map<string, MemoryIdentity>();
23
+
24
+ function defaultGitPath(cwd: string, args: string[]): string {
25
+ return execFileSync("git", args, {
26
+ cwd,
27
+ encoding: "utf8",
28
+ maxBuffer: 4 * 1024 * 1024,
29
+ windowsHide: true,
30
+ stdio: ["ignore", "pipe", "ignore"],
31
+ }).replace(/\r?\n$/, "");
32
+ }
33
+
34
+ function digest(key: string): string {
35
+ return createHash("sha256").update(key).digest("hex");
36
+ }
37
+
38
+ /**
39
+ * Use Git's shared administrative directory as the repository identity.
40
+ * Every linked worktree reports the same directory, even when its checkout
41
+ * path is unrelated to the primary checkout path.
42
+ */
43
+ export function resolveMemoryIdentity(
44
+ cwd: string,
45
+ options: MemoryIdentityOptions = {},
46
+ ): MemoryIdentity {
47
+ const platform = options.platform ?? process.platform;
48
+ const paths = platform === "win32" ? win32 : posix;
49
+ const realpath = options.realpath ?? canonicalRealpathSync;
50
+ const gitPath = options.gitPath ?? defaultGitPath;
51
+ const cacheKey = `${platform}\0${projectStorageKey(cwd, platform)}`;
52
+ const cacheable = options.gitPath === undefined && options.realpath === undefined;
53
+ if (cacheable) {
54
+ const cached = resolvedIdentities.get(cacheKey);
55
+ if (cached) return cached;
56
+ }
57
+
58
+ try {
59
+ const raw = gitPath(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
60
+ const absolute = paths.isAbsolute(raw) ? raw : paths.resolve(cwd, raw);
61
+ const canonical = realpath(absolute, platform);
62
+ const key = `git:${projectStorageKey(canonical, platform)}`;
63
+ const identity: MemoryIdentity = { key, digest: digest(key), kind: "git" };
64
+ if (cacheable) resolvedIdentities.set(cacheKey, identity);
65
+ return identity;
66
+ } catch {
67
+ let canonical = paths.resolve(cwd);
68
+ try {
69
+ canonical = realpath(canonical, platform);
70
+ } catch { /* the session startup owns directory validation */ }
71
+ const key = `directory:${projectStorageKey(canonical, platform)}`;
72
+ const identity: MemoryIdentity = { key, digest: digest(key), kind: "directory" };
73
+ if (cacheable) resolvedIdentities.set(cacheKey, identity);
74
+ return identity;
75
+ }
76
+ }