@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.4

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 (62) hide show
  1. package/CHANGELOG.md +95 -2
  2. package/README.md +21 -0
  3. package/package.json +23 -7
  4. package/src/cli/grievances-cli.ts +89 -4
  5. package/src/commands/grievances.ts +33 -7
  6. package/src/config/prompt-templates.ts +14 -7
  7. package/src/config/settings-schema.ts +610 -100
  8. package/src/config/settings.ts +42 -0
  9. package/src/discovery/helpers.ts +13 -6
  10. package/src/edit/index.ts +3 -3
  11. package/src/edit/line-hash.ts +73 -25
  12. package/src/edit/modes/hashline.lark +10 -3
  13. package/src/edit/modes/hashline.ts +295 -40
  14. package/src/edit/renderer.ts +3 -3
  15. package/src/hindsight/backend.ts +205 -0
  16. package/src/hindsight/bank.ts +131 -0
  17. package/src/hindsight/client.ts +598 -0
  18. package/src/hindsight/config.ts +175 -0
  19. package/src/hindsight/content.ts +210 -0
  20. package/src/hindsight/index.ts +8 -0
  21. package/src/hindsight/mental-models.ts +382 -0
  22. package/src/hindsight/seeds.json +32 -0
  23. package/src/hindsight/state.ts +469 -0
  24. package/src/hindsight/transcript.ts +71 -0
  25. package/src/main.ts +7 -10
  26. package/src/memories/index.ts +1 -1
  27. package/src/memory-backend/index.ts +4 -0
  28. package/src/memory-backend/local-backend.ts +30 -0
  29. package/src/memory-backend/off-backend.ts +16 -0
  30. package/src/memory-backend/resolve.ts +24 -0
  31. package/src/memory-backend/types.ts +79 -0
  32. package/src/modes/components/settings-defs.ts +50 -451
  33. package/src/modes/components/settings-selector.ts +2 -2
  34. package/src/modes/components/status-line/presets.ts +1 -1
  35. package/src/modes/controllers/command-controller.ts +266 -6
  36. package/src/modes/controllers/event-controller.ts +12 -0
  37. package/src/modes/controllers/selector-controller.ts +3 -12
  38. package/src/modes/theme/theme.ts +4 -0
  39. package/src/prompts/tools/github.md +3 -0
  40. package/src/prompts/tools/hashline.md +21 -16
  41. package/src/prompts/tools/read.md +10 -6
  42. package/src/prompts/tools/recall.md +5 -0
  43. package/src/prompts/tools/reflect.md +5 -0
  44. package/src/prompts/tools/retain.md +5 -0
  45. package/src/prompts/tools/search.md +1 -1
  46. package/src/sdk.ts +21 -9
  47. package/src/session/agent-session.ts +118 -3
  48. package/src/slash-commands/builtin-registry.ts +12 -12
  49. package/src/task/executor.ts +3 -0
  50. package/src/task/index.ts +2 -0
  51. package/src/tools/ast-edit.ts +14 -5
  52. package/src/tools/ast-grep.ts +12 -3
  53. package/src/tools/find.ts +47 -7
  54. package/src/tools/gh-renderer.ts +10 -1
  55. package/src/tools/gh.ts +233 -5
  56. package/src/tools/hindsight-recall.ts +68 -0
  57. package/src/tools/hindsight-reflect.ts +55 -0
  58. package/src/tools/hindsight-retain.ts +60 -0
  59. package/src/tools/index.ts +20 -0
  60. package/src/tools/path-utils.ts +55 -0
  61. package/src/tools/read.ts +1 -1
  62. package/src/tools/search.ts +45 -8
@@ -102,6 +102,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
102
102
  import type { HookCommandContext } from "../extensibility/hooks/types";
103
103
  import type { Skill, SkillWarning } from "../extensibility/skills";
104
104
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
105
+ import type { HindsightSessionState } from "../hindsight/state";
105
106
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
106
107
  import {
107
108
  buildDiscoverableMCPSearchIndex,
@@ -111,6 +112,7 @@ import {
111
112
  isMCPToolName,
112
113
  selectDiscoverableMCPToolNamesByServer,
113
114
  } from "../mcp/discoverable-tool-metadata";
115
+ import { resolveMemoryBackend } from "../memory-backend";
114
116
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
115
117
  import type { PlanModeState } from "../plan-mode/state";
116
118
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
@@ -192,7 +194,8 @@ export type AgentSessionEvent =
192
194
  | { type: "ttsr_triggered"; rules: Rule[] }
193
195
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
194
196
  | { type: "todo_auto_clear" }
195
- | { type: "irc_message"; message: CustomMessage };
197
+ | { type: "irc_message"; message: CustomMessage }
198
+ | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string };
196
199
 
197
200
  /** Listener function for agent session events */
198
201
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
@@ -561,6 +564,7 @@ export class AgentSession {
561
564
  #lastSuccessfulYieldToolCallId: string | undefined = undefined;
562
565
  #promptGeneration = 0;
563
566
  #providerSessionState = new Map<string, ProviderSessionState>();
567
+ #hindsightSessionState: HindsightSessionState | undefined = undefined;
564
568
 
565
569
  #startPowerAssertion(): void {
566
570
  if (process.platform !== "darwin") {
@@ -700,6 +704,16 @@ export class AgentSession {
700
704
  return this.#providerSessionState;
701
705
  }
702
706
 
707
+ getHindsightSessionState(): HindsightSessionState | undefined {
708
+ return this.#hindsightSessionState;
709
+ }
710
+
711
+ setHindsightSessionState(state: HindsightSessionState | undefined): HindsightSessionState | undefined {
712
+ const previous = this.#hindsightSessionState;
713
+ this.#hindsightSessionState = state;
714
+ return previous;
715
+ }
716
+
703
717
  /** TTSR manager for time-traveling stream rules */
704
718
  get ttsrManager(): TtsrManager | undefined {
705
719
  return this.#ttsrManager;
@@ -742,6 +756,19 @@ export class AgentSession {
742
756
  }
743
757
  }
744
758
 
759
+ /**
760
+ * Emit a UI-only notice to the session. Surfaces in interactive mode as a
761
+ * `showWarning` / `showError` / `showStatus` line; non-interactive modes
762
+ * receive the event through the normal subscribe stream.
763
+ *
764
+ * Notices are NOT added to agent state and never reach the LLM — use this
765
+ * for out-of-band conditions the user should see but the model shouldn't
766
+ * react to (e.g. background queue flush failures).
767
+ */
768
+ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void {
769
+ this.#emit({ type: "notice", level, message, source });
770
+ }
771
+
745
772
  #queuedExtensionEvents: Promise<void> = Promise.resolve();
746
773
 
747
774
  #queueExtensionEvent(event: AgentSessionEvent): Promise<void> {
@@ -1949,6 +1976,22 @@ export class AgentSession {
1949
1976
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
1950
1977
  }
1951
1978
 
1979
+ /** Keep Hindsight metadata aligned when the underlying agent session id changes. */
1980
+ #rekeyHindsightMemoryForCurrentSessionId(): void {
1981
+ if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
1982
+ const sid = this.agent.sessionId;
1983
+ if (!sid) return;
1984
+ this.getHindsightSessionState()?.setSessionId(sid);
1985
+ }
1986
+
1987
+ /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
1988
+ #resetHindsightConversationTrackingIfHindsight(): void {
1989
+ if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
1990
+ const state = this.getHindsightSessionState();
1991
+ if (!state || state.aliasOf) return;
1992
+ state.resetConversationTracking();
1993
+ }
1994
+
1952
1995
  /**
1953
1996
  * Remove all listeners, flush pending writes, and disconnect from agent.
1954
1997
  * Call this when completely done with the session.
@@ -1979,6 +2022,9 @@ export class AgentSession {
1979
2022
  this.#stopPowerAssertion();
1980
2023
  await this.sessionManager.close();
1981
2024
  this.#closeAllProviderSessions("dispose");
2025
+ const hindsightState = this.setHindsightSessionState(undefined);
2026
+ await hindsightState?.flushRetainQueue();
2027
+ hindsightState?.dispose();
1982
2028
  this.#disconnectFromAgent();
1983
2029
  this.#eventListeners = [];
1984
2030
  }
@@ -2289,6 +2335,23 @@ export class AgentSession {
2289
2335
  this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
2290
2336
  }
2291
2337
 
2338
+ async #buildSystemPromptForAgentStart(promptText: string): Promise<string> {
2339
+ const backend = resolveMemoryBackend(this.settings);
2340
+ if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
2341
+
2342
+ try {
2343
+ const injected = await backend.beforeAgentStartPrompt(this, promptText);
2344
+ if (!injected) return this.#baseSystemPrompt;
2345
+ return `${this.#baseSystemPrompt}\n\n${injected}`;
2346
+ } catch (err) {
2347
+ logger.debug("Memory backend beforeAgentStartPrompt failed", {
2348
+ backend: backend.id,
2349
+ error: String(err),
2350
+ });
2351
+ return this.#baseSystemPrompt;
2352
+ }
2353
+ }
2354
+
2292
2355
  /**
2293
2356
  * Compose a stable signature for the inputs that `rebuildSystemPrompt` reads.
2294
2357
  * Two calls producing identical signatures are guaranteed to produce identical
@@ -2908,12 +2971,14 @@ export class AgentSession {
2908
2971
  messages.push(...fileMentionMessages);
2909
2972
  }
2910
2973
 
2974
+ const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
2975
+
2911
2976
  // Emit before_agent_start extension event
2912
2977
  if (this.#extensionRunner) {
2913
2978
  const result = await this.#extensionRunner.emitBeforeAgentStart(
2914
2979
  expandedText,
2915
2980
  options?.images,
2916
- this.#baseSystemPrompt,
2981
+ beforeAgentStartSystemPrompt,
2917
2982
  );
2918
2983
  if (result?.messages) {
2919
2984
  const promptAttribution: "user" | "agent" | undefined =
@@ -2934,8 +2999,10 @@ export class AgentSession {
2934
2999
  if (result?.systemPrompt !== undefined) {
2935
3000
  this.agent.setSystemPrompt(result.systemPrompt);
2936
3001
  } else {
2937
- this.agent.setSystemPrompt(this.#baseSystemPrompt);
3002
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
2938
3003
  }
3004
+ } else {
3005
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
2939
3006
  }
2940
3007
 
2941
3008
  // Bail out if a newer abort/prompt cycle has started since we began setup
@@ -3590,6 +3657,8 @@ export class AgentSession {
3590
3657
  await this.sessionManager.newSession(options);
3591
3658
  this.setTodoPhases([]);
3592
3659
  this.agent.sessionId = this.sessionManager.getSessionId();
3660
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3661
+ this.#resetHindsightConversationTrackingIfHindsight();
3593
3662
  this.#steeringMessages = [];
3594
3663
  this.#followUpMessages = [];
3595
3664
  this.#pendingNextTurnMessages = [];
@@ -3683,6 +3752,7 @@ export class AgentSession {
3683
3752
 
3684
3753
  // Update agent session ID
3685
3754
  this.agent.sessionId = this.sessionManager.getSessionId();
3755
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3686
3756
 
3687
3757
  // Emit session_switch event with reason "fork" to hooks
3688
3758
  if (this.#extensionRunner) {
@@ -4118,6 +4188,11 @@ export class AgentSession {
4118
4188
  preserveData = result?.preserveData;
4119
4189
  }
4120
4190
 
4191
+ const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
4192
+ if (memoryBackendContext) {
4193
+ hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
4194
+ }
4195
+
4121
4196
  let summary: string;
4122
4197
  let shortSummary: string | undefined;
4123
4198
  let firstKeptEntryId: string;
@@ -4204,6 +4279,32 @@ export class AgentSession {
4204
4279
  }
4205
4280
  }
4206
4281
 
4282
+ /**
4283
+ * Ask the active memory backend for an extra-context block to splice into
4284
+ * the compaction summary prompt. Both the manual and auto compaction paths
4285
+ * funnel through this helper so the behaviour stays identical.
4286
+ *
4287
+ * Failures are swallowed: a memory backend going sideways MUST NOT block
4288
+ * compaction (which is itself the recovery path for context overflow).
4289
+ */
4290
+ async #collectMemoryBackendContext(preparation: {
4291
+ messagesToSummarize: AgentMessage[];
4292
+ turnPrefixMessages: AgentMessage[];
4293
+ }): Promise<string | undefined> {
4294
+ const backend = resolveMemoryBackend(this.settings);
4295
+ if (!backend.preCompactionContext) return undefined;
4296
+ const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
4297
+ try {
4298
+ return await backend.preCompactionContext(messages, this.settings, this);
4299
+ } catch (err) {
4300
+ logger.debug("Memory backend preCompactionContext failed", {
4301
+ backend: backend.id,
4302
+ error: String(err),
4303
+ });
4304
+ return undefined;
4305
+ }
4306
+ }
4307
+
4207
4308
  /**
4208
4309
  * Cancel in-progress context maintenance (manual compaction, auto-compaction, or auto-handoff).
4209
4310
  */
@@ -4358,6 +4459,8 @@ export class AgentSession {
4358
4459
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
4359
4460
  this.agent.reset();
4360
4461
  this.agent.sessionId = this.sessionManager.getSessionId();
4462
+ this.#rekeyHindsightMemoryForCurrentSessionId();
4463
+ this.#resetHindsightConversationTrackingIfHindsight();
4361
4464
  this.#steeringMessages = [];
4362
4465
  this.#followUpMessages = [];
4363
4466
  this.#pendingNextTurnMessages = [];
@@ -5190,6 +5293,11 @@ export class AgentSession {
5190
5293
  preserveData = result?.preserveData;
5191
5294
  }
5192
5295
 
5296
+ const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
5297
+ if (memoryBackendContext) {
5298
+ hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
5299
+ }
5300
+
5193
5301
  let summary: string;
5194
5302
  let shortSummary: string | undefined;
5195
5303
  let firstKeptEntryId: string;
@@ -6499,6 +6607,7 @@ export class AgentSession {
6499
6607
  try {
6500
6608
  await this.sessionManager.setSessionFile(sessionPath);
6501
6609
  this.agent.sessionId = this.sessionManager.getSessionId();
6610
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6502
6611
 
6503
6612
  const sessionContext = this.buildDisplaySessionContext();
6504
6613
  const didReloadConversationChange =
@@ -6568,11 +6677,15 @@ export class AgentSession {
6568
6677
  ? undefined
6569
6678
  : configuredServiceTier;
6570
6679
 
6680
+ if (switchingToDifferentSession) {
6681
+ this.#resetHindsightConversationTrackingIfHindsight();
6682
+ }
6571
6683
  this.#reconnectToAgent();
6572
6684
  return true;
6573
6685
  } catch (error) {
6574
6686
  this.sessionManager.restoreState(previousSessionState);
6575
6687
  this.agent.sessionId = previousSessionState.sessionId;
6688
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6576
6689
  let restoreMcpError: unknown;
6577
6690
  try {
6578
6691
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -6664,6 +6777,8 @@ export class AgentSession {
6664
6777
  }
6665
6778
  this.#syncTodoPhasesFromBranch();
6666
6779
  this.agent.sessionId = this.sessionManager.getSessionId();
6780
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6781
+ this.#resetHindsightConversationTrackingIfHindsight();
6667
6782
 
6668
6783
  // Reload messages from entries (works for both file and in-memory mode)
6669
6784
  const sessionContext = this.buildDisplaySessionContext();
@@ -1,13 +1,7 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
3
-
4
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
5
- import { getConfigDirName } from "@oh-my-pi/pi-utils";
6
- import { invalidate as invalidateFsCache } from "../capability/fs";
7
2
  import type { SettingPath, SettingValue } from "../config/settings";
8
3
  import { settings } from "../config/settings";
9
4
  import {
10
- clearClaudePluginRootsCache,
11
5
  clearPluginRootsAndCaches,
12
6
  resolveActiveProjectRegistryPath,
13
7
  resolveOrDefaultProjectRegistryPath,
@@ -604,6 +598,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
604
598
  { name: "reset", description: "Alias for clear" },
605
599
  { name: "enqueue", description: "Enqueue memory consolidation maintenance" },
606
600
  { name: "rebuild", description: "Alias for enqueue" },
601
+ { name: "mm list", description: "List mental models on the active bank" },
602
+ { name: "mm show", description: "Show one mental model (id required)" },
603
+ {
604
+ name: "mm refresh",
605
+ description: "Refresh auto-refresh models bank-wide, or one model by id",
606
+ },
607
+ { name: "mm history", description: "Diff the change history of a mental model" },
608
+ { name: "mm seed", description: "Create any built-in mental models that are missing" },
609
+ { name: "mm delete", description: "Delete a mental model from the bank (id required)" },
610
+ { name: "mm reload", description: "Re-pull the cached <mental_models> block" },
607
611
  ],
608
612
  allowArgs: true,
609
613
  handle: async (command, runtime) => {
@@ -942,14 +946,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
942
946
  name: "reload-plugins",
943
947
  description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
944
948
  handle: async (_command, runtime) => {
945
- // Invalidate the fs content cache for all registry files so
949
+ // Invalidate registry fs caches and the plugin roots cache so
946
950
  // listClaudePluginRoots re-reads from disk on next access.
947
- const home = os.homedir();
948
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
949
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
950
951
  const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
951
- if (projectPath) invalidateFsCache(projectPath);
952
- clearClaudePluginRootsCache();
952
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
953
953
  await runtime.ctx.refreshSlashCommandState();
954
954
  runtime.ctx.showStatus("Plugins reloaded.");
955
955
  runtime.ctx.editor.setText("");
@@ -17,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
17
17
  import type { CustomTool } from "../extensibility/custom-tools/types";
18
18
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
19
19
  import type { Skill } from "../extensibility/skills";
20
+ import type { HindsightSessionState } from "../hindsight/state";
20
21
  import type { LocalProtocolOptions } from "../internal-urls";
21
22
  import { callTool } from "../mcp/client";
22
23
  import type { MCPManager } from "../mcp/manager";
@@ -163,6 +164,7 @@ export interface ExecutorOptions {
163
164
  settings?: Settings;
164
165
  /** Override local:// protocol options so subagent shares parent's local:// root */
165
166
  localProtocolOptions?: LocalProtocolOptions;
167
+ parentHindsightSessionState?: HindsightSessionState;
166
168
  }
167
169
 
168
170
  function parseStringifiedJson(value: unknown): unknown {
@@ -979,6 +981,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
979
981
  hasUI: false,
980
982
  spawns: spawnsEnv,
981
983
  taskDepth: childDepth,
984
+ parentHindsightSessionState: options.parentHindsightSessionState,
982
985
  parentTaskPrefix: id,
983
986
  agentId: id,
984
987
  agentDisplayName: agent.name,
package/src/task/index.ts CHANGED
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
864
864
  skills: availableSkills,
865
865
  promptTemplates,
866
866
  localProtocolOptions,
867
+ parentHindsightSessionState: this.session.getHindsightSessionState?.(),
867
868
  });
868
869
  }
869
870
 
@@ -918,6 +919,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
918
919
  skills: availableSkills,
919
920
  promptTemplates,
920
921
  localProtocolOptions,
922
+ parentHindsightSessionState: this.session.getHindsightSessionState?.(),
921
923
  });
922
924
  if (mergeMode === "branch" && result.exitCode === 0) {
923
925
  try {
@@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
- import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
8
+ import { computeLineHash, HL_BODY_SEP } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
@@ -20,6 +20,7 @@ import {
20
20
  hasGlobPathChars,
21
21
  normalizePathLikeInput,
22
22
  parseSearchPath,
23
+ partitionExistingPaths,
23
24
  resolveExplicitSearchPaths,
24
25
  resolveToCwd,
25
26
  } from "./path-utils";
@@ -226,13 +227,21 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
226
227
  }
227
228
  resolvedPathInputs.push(resource.sourcePath);
228
229
  }
229
- if (resolvedPathInputs.length === 1) {
230
- const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
230
+ let effectivePathInputs = resolvedPathInputs;
231
+ if (resolvedPathInputs.length > 1) {
232
+ const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
233
+ if (partition.valid.length === 0) {
234
+ throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
235
+ }
236
+ effectivePathInputs = partition.valid;
237
+ }
238
+ if (effectivePathInputs.length === 1) {
239
+ const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
231
240
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
232
241
  globFilter = parsedPath.glob;
233
242
  scopePath = formatScopePath(searchPath);
234
243
  } else {
235
- const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
244
+ const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
236
245
  if (!multiSearchPath) {
237
246
  throw new ToolError("`paths` must contain at least one path or glob");
238
247
  }
@@ -321,7 +330,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
321
330
  const afterRef = useHashLines
322
331
  ? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
323
332
  : `${change.startLine}:${change.startColumn}`;
324
- const lineSeparator = useHashLines ? HASHLINE_CONTENT_SEPARATOR : " ";
333
+ const lineSeparator = useHashLines ? HL_BODY_SEP : " ";
325
334
  modelOut.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
326
335
  modelOut.push(`+${afterRef}${lineSeparator}${afterLine}`);
327
336
  displayOut.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
@@ -20,6 +20,7 @@ import {
20
20
  hasGlobPathChars,
21
21
  normalizePathLikeInput,
22
22
  parseSearchPath,
23
+ partitionExistingPaths,
23
24
  resolveExplicitSearchPaths,
24
25
  resolveToCwd,
25
26
  } from "./path-utils";
@@ -171,13 +172,21 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
171
172
  }
172
173
  resolvedPathInputs.push(resource.sourcePath);
173
174
  }
174
- if (resolvedPathInputs.length === 1) {
175
- const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
175
+ let effectivePathInputs = resolvedPathInputs;
176
+ if (resolvedPathInputs.length > 1) {
177
+ const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
178
+ if (partition.valid.length === 0) {
179
+ throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
180
+ }
181
+ effectivePathInputs = partition.valid;
182
+ }
183
+ if (effectivePathInputs.length === 1) {
184
+ const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
176
185
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
177
186
  globFilter = parsedPath.glob;
178
187
  scopePath = formatScopePath(searchPath);
179
188
  } else {
180
- const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
189
+ const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
181
190
  if (!multiSearchPath) {
182
191
  throw new ToolError("`paths` must contain at least one path or glob");
183
192
  }
package/src/tools/find.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  formatPathRelativeToCwd,
28
28
  normalizePathLikeInput,
29
29
  parseFindPattern,
30
+ partitionExistingPaths,
30
31
  resolveExplicitFindPatterns,
31
32
  resolveToCwd,
32
33
  } from "./path-utils";
@@ -59,6 +60,10 @@ export interface FindToolDetails {
59
60
  files?: string[];
60
61
  truncated?: boolean;
61
62
  error?: string;
63
+ /** User-supplied paths whose base directory was missing on disk. The tool
64
+ * skipped these and continued with the surviving entries; surfaced as a
65
+ * non-fatal warning in the renderer and in the model-facing text. */
66
+ missingPaths?: string[];
62
67
  }
63
68
 
64
69
  /**
@@ -114,8 +119,23 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
114
119
  throw new ToolError("`paths` must contain non-empty globs or paths");
115
120
  }
116
121
 
117
- const multiPattern = await resolveExplicitFindPatterns(normalizedPatterns, this.session.cwd);
118
- const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPatterns[0] ?? ".");
122
+ // Tolerate missing entries in a multi-path call: skip ones whose base
123
+ // directory is gone, and only error if every entry is missing. Single
124
+ // missing path keeps the original ENOENT semantics — the user explicitly
125
+ // asked about that one path, so silent empty results would be misleading.
126
+ let missingPaths: string[] = [];
127
+ let effectivePatterns = normalizedPatterns;
128
+ if (normalizedPatterns.length > 1 && !this.#customOps) {
129
+ const partition = await partitionExistingPaths(normalizedPatterns, this.session.cwd, parseFindPattern);
130
+ if (partition.valid.length === 0) {
131
+ throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
132
+ }
133
+ effectivePatterns = partition.valid;
134
+ missingPaths = partition.missing;
135
+ }
136
+
137
+ const multiPattern = await resolveExplicitFindPatterns(effectivePatterns, this.session.cwd);
138
+ const parsedPattern = multiPattern ? null : parseFindPattern(effectivePatterns[0] ?? ".");
119
139
  const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
120
140
  const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
121
141
  const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
@@ -124,7 +144,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
124
144
  if (searchPath === "/") {
125
145
  throw new ToolError("Searching from root directory '/' is not allowed");
126
146
  }
127
-
128
147
  const rawLimit = limit ?? DEFAULT_LIMIT;
129
148
  const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
130
149
  if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
@@ -141,16 +160,29 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
141
160
  });
142
161
  };
143
162
 
163
+ const missingPathsNote =
164
+ missingPaths.length > 0 ? `Skipped missing paths: ${missingPaths.join(", ")}` : undefined;
165
+
144
166
  const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
145
167
  if (files.length === 0) {
146
- const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
147
- return toolResult(details).text("No files found matching pattern").done();
168
+ const details: FindToolDetails = {
169
+ scopePath,
170
+ fileCount: 0,
171
+ files: [],
172
+ truncated: false,
173
+ missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
174
+ };
175
+ const text = missingPathsNote
176
+ ? `No files found matching pattern\n${missingPathsNote}`
177
+ : "No files found matching pattern";
178
+ return toolResult(details).text(text).done();
148
179
  }
149
180
 
150
181
  const listLimit = applyListLimit(files, { limit: effectiveLimit });
151
182
  const limited = listLimit.items;
152
183
  const limitMeta = listLimit.meta;
153
- const rawOutput = limited.join("\n");
184
+ const baseOutput = limited.join("\n");
185
+ const rawOutput = missingPathsNote ? `${baseOutput}\n\n${missingPathsNote}` : baseOutput;
154
186
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
155
187
 
156
188
  const details: FindToolDetails = {
@@ -160,6 +192,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
160
192
  truncated: Boolean(limitMeta.resultLimit || truncation.truncated),
161
193
  resultLimitReached: limitMeta.resultLimit?.reached,
162
194
  truncation: truncation.truncated ? truncation : undefined,
195
+ missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
163
196
  };
164
197
 
165
198
  const resultBuilder = toolResult(details)
@@ -380,12 +413,18 @@ export const findToolRenderer = {
380
413
  const truncated = Boolean(details?.truncated || truncation || details?.resultLimitReached || limits?.resultLimit);
381
414
  const files = details?.files ?? [];
382
415
 
416
+ const missingPaths = details?.missingPaths ?? [];
417
+ const missingNote =
418
+ missingPaths.length > 0 ? uiTheme.fg("warning", `skipped missing: ${missingPaths.join(", ")}`) : undefined;
419
+
383
420
  if (fileCount === 0) {
384
421
  const header = renderStatusLine(
385
422
  { icon: "warning", title: "Find", description: args?.paths?.join(", "), meta: ["0 files"] },
386
423
  uiTheme,
387
424
  );
388
- return new Text([header, formatEmptyMessage("No files found", uiTheme)].join("\n"), 0, 0);
425
+ const lines = [header, formatEmptyMessage("No files found", uiTheme)];
426
+ if (missingNote) lines.push(missingNote);
427
+ return new Text(lines.join("\n"), 0, 0);
389
428
  }
390
429
  const meta: string[] = [formatCount("file", fileCount)];
391
430
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
@@ -406,6 +445,7 @@ export const findToolRenderer = {
406
445
  if (truncationReasons.length > 0) {
407
446
  extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
408
447
  }
448
+ if (missingNote) extraLines.push(missingNote);
409
449
 
410
450
  let cached: RenderCache | undefined;
411
451
  return {
@@ -47,6 +47,9 @@ const OP_TITLES: Record<string, string> = {
47
47
  pr_push: "GitHub PR Push",
48
48
  search_issues: "GitHub Search Issues",
49
49
  search_prs: "GitHub Search PRs",
50
+ search_code: "GitHub Search Code",
51
+ search_commits: "GitHub Search Commits",
52
+ search_repos: "GitHub Search Repos",
50
53
  run_watch: "GitHub Run Watch",
51
54
  };
52
55
 
@@ -99,11 +102,17 @@ function buildOpMeta(args: GithubToolRenderArgs): string[] {
99
102
  break;
100
103
  }
101
104
  case "search_issues":
102
- case "search_prs": {
105
+ case "search_prs":
106
+ case "search_code":
107
+ case "search_commits": {
103
108
  if (args.query) meta.push(truncateVisualWidth(args.query, TRUNCATE_LENGTHS.CONTENT));
104
109
  if (args.repo) meta.push(args.repo);
105
110
  break;
106
111
  }
112
+ case "search_repos": {
113
+ if (args.query) meta.push(truncateVisualWidth(args.query, TRUNCATE_LENGTHS.CONTENT));
114
+ break;
115
+ }
107
116
  case "repo_view": {
108
117
  if (args.repo) meta.push(args.repo);
109
118
  if (args.branch) meta.push(args.branch);