@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0

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.
Files changed (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -1023,18 +1023,18 @@
1023
1023
  }
1024
1024
 
1025
1025
  function renderTask(name, args, result, ctx) {
1026
- const agent = str(args.agent) || '?';
1027
- const tasks = Array.isArray(args.tasks) ? args.tasks : [];
1028
- const badges = ['agent=' + agent, tasks.length + ' subtask' + (tasks.length === 1 ? '' : 's')];
1026
+ const badges = [];
1027
+ if (args.resume) badges.push('resume=' + str(args.resume));
1028
+ else badges.push('agent=' + (str(args.agent) || '?'));
1029
+ if (args.id) badges.push('id=' + str(args.id));
1029
1030
  if (args.isolated) badges.push('isolated');
1030
1031
  let html = toolHead('task', '', badges);
1031
- if (tasks.length) {
1032
+ const description = str(args.description);
1033
+ const assignment = str(args.assignment);
1034
+ if (description || assignment) {
1032
1035
  html += '<div class="tool-args">';
1033
- for (const t of tasks) {
1034
- const id = t?.id ? escapeHtml(String(t.id)) : '?';
1035
- const desc = t?.description ? escapeHtml(String(t.description)) : '';
1036
- html += '<div class="tool-arg"><span class="tool-arg-key">' + id + '</span> ' + desc + '</div>';
1037
- }
1036
+ if (description) html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(description) + '</span></div>';
1037
+ if (assignment) html += '<div class="tool-arg">' + escapeHtml(assignment) + '</div>';
1038
1038
  html += '</div>';
1039
1039
  }
1040
1040
  if (result) {
@@ -1479,13 +1479,38 @@
1479
1479
  }
1480
1480
 
1481
1481
  function renderIrc(name, args, result, ctx) {
1482
- const op = str(args.op) || '?';
1482
+ const details = result && result.details ? result.details : null;
1483
+ const op = str(args.op) || (details && str(details.op)) || '?';
1483
1484
  const badges = [op];
1484
- if (args.to) badges.push('to=' + args.to);
1485
- if (args.awaitReply === false) badges.push('no-reply');
1485
+ if (args.to) badges.push('to=' + str(args.to));
1486
+ if (op === 'wait' && args.from) badges.push('from=' + str(args.from));
1487
+ if (args.await) badges.push('await');
1488
+ if (args.peek) badges.push('peek');
1486
1489
  let html = toolHead('irc', '', badges);
1487
1490
  if (args.message) html += '<div class="tool-output"><div>' + escapeHtml(String(args.message)) + '</div></div>';
1488
- if (result) {
1491
+ let renderedDetails = false;
1492
+ if (details && Array.isArray(details.receipts) && details.receipts.length) {
1493
+ html += '<div class="tool-args">';
1494
+ for (const receipt of details.receipts) {
1495
+ const outcome = escapeHtml(String(receipt.outcome)) + (receipt.error ? ' — ' + escapeHtml(String(receipt.error)) : '');
1496
+ html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(String(receipt.to)) + '</span> ' + outcome + '</div>';
1497
+ }
1498
+ html += '</div>';
1499
+ renderedDetails = true;
1500
+ }
1501
+ if (details && details.waited) {
1502
+ html += '<div class="tool-output"><div>' + escapeHtml(String(details.waited.from)) + ': ' + escapeHtml(String(details.waited.body)) + '</div></div>';
1503
+ renderedDetails = true;
1504
+ }
1505
+ if (details && Array.isArray(details.inbox) && details.inbox.length) {
1506
+ html += '<div class="tool-args">';
1507
+ for (const msg of details.inbox) {
1508
+ html += '<div class="tool-arg"><span class="tool-arg-key">' + escapeHtml(String(msg.from)) + '</span> ' + escapeHtml(String(msg.body)) + '</div>';
1509
+ }
1510
+ html += '</div>';
1511
+ renderedDetails = true;
1512
+ }
1513
+ if (!renderedDetails && result) {
1489
1514
  const output = ctx.getResultText();
1490
1515
  if (output) html += formatExpandableOutput(output, 8);
1491
1516
  }
@@ -103,11 +103,11 @@ export type CustomToolSessionEvent =
103
103
  | {
104
104
  reason: "auto_compaction_start";
105
105
  trigger: "threshold" | "overflow" | "idle" | "incomplete";
106
- action: "context-full" | "handoff" | "shake";
106
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
107
107
  }
108
108
  | {
109
109
  reason: "auto_compaction_end";
110
- action: "context-full" | "handoff" | "shake";
110
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
111
111
  result: CompactionResult | undefined;
112
112
  aborted: boolean;
113
113
  willRetry: boolean;
@@ -15,6 +15,7 @@
15
15
  * themselves. Each frontend (interactive-mode, ACP) prepends its own builtins.
16
16
  */
17
17
  import type { SkillsSettings } from "../../config/settings";
18
+ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../../slash-commands/builtin-registry";
18
19
  import type { CustomCommandSource, LoadedCustomCommand } from "../custom-commands";
19
20
  import { getSkillSlashCommandName, type Skill } from "../skills";
20
21
  import type { SlashCommandInfo, SlashCommandLocation } from "../slash-commands";
@@ -32,7 +33,7 @@ export function getSessionSlashCommands(session: CommandsCapableSession): SlashC
32
33
 
33
34
  const runner = session.extensionRunner;
34
35
  if (runner) {
35
- for (const cmd of runner.getRegisteredCommands()) {
36
+ for (const cmd of runner.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES)) {
36
37
  out.push({
37
38
  name: cmd.name,
38
39
  description: cmd.description,
@@ -6,6 +6,7 @@ import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMeta
6
6
  import type { KeyId } from "@oh-my-pi/pi-tui";
7
7
  import { logger } from "@oh-my-pi/pi-utils";
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
+ import type { MemoryRuntimeContext } from "../../memory-backend";
9
10
  import { type Theme, theme } from "../../modes/theme/theme";
10
11
  import type { SessionManager } from "../../session/session-manager";
11
12
  import type {
@@ -187,6 +188,7 @@ export class ExtensionRunner {
187
188
  #switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
188
189
  #reloadHandler: () => Promise<void> = async () => {};
189
190
  #shutdownHandler: ShutdownHandler = () => {};
191
+ #getMemoryFn?: () => MemoryRuntimeContext | undefined;
190
192
  #commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
191
193
  #initialized = false;
192
194
  /**
@@ -204,8 +206,10 @@ export class ExtensionRunner {
204
206
  private readonly cwd: string,
205
207
  private readonly sessionManager: SessionManager,
206
208
  private readonly modelRegistry: ModelRegistry,
209
+ getMemory?: () => MemoryRuntimeContext | undefined,
207
210
  ) {
208
211
  this.#uiContext = noOpUIContext;
212
+ this.#getMemoryFn = getMemory;
209
213
  }
210
214
 
211
215
  initialize(
@@ -427,7 +431,7 @@ export class ExtensionRunner {
427
431
  return this.extensions.flatMap(ext => ext.assistantThinkingRenderers);
428
432
  }
429
433
 
430
- getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
434
+ getRegisteredCommands(reserved?: ReadonlySet<string>): RegisteredCommand[] {
431
435
  this.#commandDiagnostics = [];
432
436
 
433
437
  const commands = new Map<string, RegisteredCommand>();
@@ -480,6 +484,7 @@ export class ExtensionRunner {
480
484
  hasPendingMessages: () => this.#hasPendingMessagesFn(),
481
485
  shutdown: () => this.#shutdownHandler(),
482
486
  getSystemPrompt: () => this.#getSystemPromptFn(),
487
+ memory: this.#getMemoryFn?.(),
483
488
  };
484
489
  }
485
490
 
@@ -40,6 +40,7 @@ import type { EditToolDetails } from "../../edit";
40
40
  import type { PythonResult } from "../../eval/py/executor";
41
41
  import type { BashResult } from "../../exec/bash-executor";
42
42
  import type { ExecOptions, ExecResult } from "../../exec/exec";
43
+ import type { MemoryRuntimeContext } from "../../memory-backend";
43
44
  import type { CustomEditor } from "../../modes/components/custom-editor";
44
45
  import type { Theme } from "../../modes/theme/theme";
45
46
  import type { CustomMessage } from "../../session/messages";
@@ -319,6 +320,8 @@ export interface ExtensionContext {
319
320
  shutdown(): void;
320
321
  /** Get the current effective system prompt. */
321
322
  getSystemPrompt(): string[];
323
+ /** Structured memory runtime for status/search/save across the configured backend. */
324
+ memory?: MemoryRuntimeContext;
322
325
  }
323
326
 
324
327
  /**
@@ -204,13 +204,13 @@ export interface TurnEndEvent {
204
204
  export interface AutoCompactionStartEvent {
205
205
  type: "auto_compaction_start";
206
206
  reason: "threshold" | "overflow" | "idle" | "incomplete";
207
- action: "context-full" | "handoff" | "shake";
207
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
208
208
  }
209
209
 
210
210
  /** Fired when auto-compaction ends */
211
211
  export interface AutoCompactionEndEvent {
212
212
  type: "auto_compaction_end";
213
- action: "context-full" | "handoff" | "shake";
213
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
214
214
  result: CompactionResult | undefined;
215
215
  aborted: boolean;
216
216
  willRetry: boolean;
@@ -22,6 +22,7 @@
22
22
 
23
23
  import * as path from "node:path";
24
24
  import { logger } from "@oh-my-pi/pi-utils";
25
+ import * as git from "../utils/git";
25
26
  import type { HindsightApi } from "./client";
26
27
  import type { HindsightConfig } from "./config";
27
28
 
@@ -53,10 +54,24 @@ function baseBankId(config: HindsightConfig): string {
53
54
  return prefix ? `${prefix}-${base}` : base;
54
55
  }
55
56
 
56
- /** Best-effort project label from a working-directory path. */
57
+ /**
58
+ * Best-effort project label from a working-directory path.
59
+ *
60
+ * When `directory` lives inside a git repository we resolve the primary
61
+ * checkout root (or the shared common dir for bare-repo worktrees) via
62
+ * {@link git.repo.primaryRootSync} and basename that, so every linked
63
+ * worktree of one repo shares the same `project:<name>` tag.
64
+ * Outside a repo (or when resolution fails), fall back to the cwd basename.
65
+ *
66
+ * Sync only: this runs on the hot path of `computeBankScope`, which is
67
+ * exposed as a sync API to callers like `backend.ts` and must stay sync.
68
+ * `git.repo.primaryRootSync` walks `.git`/`commondir` with sync file reads —
69
+ * no subprocess — so the cost is one or two `stat`s and a small `readFile`.
70
+ */
57
71
  function projectLabel(directory: string): string {
58
72
  if (!directory) return UNKNOWN_PROJECT;
59
- return path.basename(directory) || UNKNOWN_PROJECT;
73
+ const primary = git.repo.primaryRootSync(directory);
74
+ return path.basename(primary ?? directory) || UNKNOWN_PROJECT;
60
75
  }
61
76
 
62
77
  /**