@oh-my-pi/pi-coding-agent 13.2.0 → 13.3.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 (243) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/scripts/generate-docs-index.ts +2 -2
  5. package/src/capability/index.ts +1 -2
  6. package/src/cli/args.ts +3 -3
  7. package/src/cli/config-cli.ts +1 -1
  8. package/src/cli/file-processor.ts +1 -2
  9. package/src/cli/grep-cli.ts +1 -1
  10. package/src/cli/jupyter-cli.ts +1 -1
  11. package/src/cli/plugin-cli.ts +1 -1
  12. package/src/cli/setup-cli.ts +1 -1
  13. package/src/cli/shell-cli.ts +1 -1
  14. package/src/cli/ssh-cli.ts +1 -1
  15. package/src/cli/stats-cli.ts +1 -2
  16. package/src/cli/update-cli.ts +1 -2
  17. package/src/cli/web-search-cli.ts +1 -1
  18. package/src/cli.ts +1 -1
  19. package/src/commands/launch.ts +2 -1
  20. package/src/commit/agentic/agent.ts +2 -1
  21. package/src/commit/agentic/index.ts +1 -2
  22. package/src/commit/agentic/prompts/system.md +3 -3
  23. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  24. package/src/commit/changelog/generate.ts +16 -6
  25. package/src/commit/changelog/index.ts +2 -1
  26. package/src/commit/pipeline.ts +1 -2
  27. package/src/commit/prompts/reduce-system.md +1 -1
  28. package/src/commit/types.ts +10 -1
  29. package/src/config/keybindings.ts +1 -2
  30. package/src/config/model-registry.ts +1 -1
  31. package/src/config/prompt-templates.ts +14 -2
  32. package/src/config/settings-schema.ts +36 -4
  33. package/src/config/settings.ts +19 -2
  34. package/src/config.ts +1 -2
  35. package/src/debug/index.ts +1 -1
  36. package/src/debug/report-bundle.ts +1 -2
  37. package/src/debug/system-info.ts +1 -2
  38. package/src/discovery/agents.ts +2 -2
  39. package/src/discovery/builtin.ts +8 -9
  40. package/src/discovery/claude-plugins.ts +2 -2
  41. package/src/discovery/claude.ts +30 -12
  42. package/src/discovery/codex.ts +3 -3
  43. package/src/discovery/cursor.ts +5 -4
  44. package/src/discovery/gemini.ts +5 -5
  45. package/src/discovery/helpers.ts +47 -69
  46. package/src/discovery/mcp-json.ts +3 -3
  47. package/src/discovery/opencode.ts +7 -8
  48. package/src/discovery/ssh.ts +3 -3
  49. package/src/discovery/vscode.ts +3 -2
  50. package/src/discovery/windsurf.ts +3 -2
  51. package/src/exa/company.ts +1 -1
  52. package/src/exa/factory.ts +1 -6
  53. package/src/exa/linkedin.ts +1 -1
  54. package/src/exa/mcp-client.ts +19 -8
  55. package/src/exa/search.ts +2 -2
  56. package/src/exa/types.ts +3 -3
  57. package/src/exec/bash-executor.ts +2 -1
  58. package/src/exec/non-interactive-env.ts +43 -0
  59. package/src/export/custom-share.ts +1 -1
  60. package/src/export/html/index.ts +1 -2
  61. package/src/extensibility/custom-commands/loader.ts +1 -2
  62. package/src/extensibility/plugins/installer.ts +1 -2
  63. package/src/extensibility/plugins/loader.ts +1 -2
  64. package/src/extensibility/plugins/manager.ts +3 -2
  65. package/src/extensibility/skills.ts +59 -115
  66. package/src/index.ts +1 -3
  67. package/src/internal-urls/docs-index.generated.ts +1 -1
  68. package/src/ipy/executor.ts +1 -2
  69. package/src/ipy/gateway-coordinator.ts +1 -2
  70. package/src/ipy/modules.ts +1 -1
  71. package/src/ipy/runtime.ts +2 -3
  72. package/src/main.ts +1 -2
  73. package/src/mcp/config.ts +2 -2
  74. package/src/mcp/transports/stdio.ts +1 -2
  75. package/src/memories/index.ts +1 -2
  76. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  77. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  78. package/src/modes/components/footer.ts +1 -2
  79. package/src/modes/components/settings-defs.ts +17 -1
  80. package/src/modes/components/status-line/segments.ts +1 -2
  81. package/src/modes/components/status-line.ts +7 -5
  82. package/src/modes/components/tool-execution.ts +3 -10
  83. package/src/modes/components/welcome.ts +1 -1
  84. package/src/modes/controllers/command-controller.ts +1 -2
  85. package/src/modes/controllers/mcp-command-controller.ts +5 -4
  86. package/src/modes/controllers/selector-controller.ts +22 -1
  87. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  88. package/src/modes/interactive-mode.ts +11 -3
  89. package/src/modes/oauth-manual-input.ts +42 -0
  90. package/src/modes/shared.ts +1 -2
  91. package/src/modes/theme/theme.ts +1 -2
  92. package/src/modes/types.ts +2 -0
  93. package/src/patch/hashline.ts +19 -1
  94. package/src/patch/index.ts +1 -25
  95. package/src/prompts/agents/designer.md +7 -10
  96. package/src/prompts/agents/explore.md +15 -23
  97. package/src/prompts/agents/init.md +23 -23
  98. package/src/prompts/agents/plan.md +14 -77
  99. package/src/prompts/agents/reviewer.md +6 -5
  100. package/src/prompts/agents/task.md +13 -11
  101. package/src/prompts/compaction/branch-summary.md +3 -3
  102. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  103. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  104. package/src/prompts/compaction/compaction-summary.md +5 -5
  105. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  106. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  107. package/src/prompts/memories/consolidation.md +5 -5
  108. package/src/prompts/memories/read-path.md +6 -6
  109. package/src/prompts/memories/stage_one_input.md +1 -1
  110. package/src/prompts/memories/stage_one_system.md +5 -5
  111. package/src/prompts/review-request.md +4 -4
  112. package/src/prompts/system/agent-creation-architect.md +17 -17
  113. package/src/prompts/system/agent-creation-user.md +2 -2
  114. package/src/prompts/system/commit-message-system.md +2 -0
  115. package/src/prompts/system/custom-system-prompt.md +4 -4
  116. package/src/prompts/system/plan-mode-active.md +20 -20
  117. package/src/prompts/system/plan-mode-approved.md +7 -7
  118. package/src/prompts/system/plan-mode-reference.md +2 -2
  119. package/src/prompts/system/plan-mode-subagent.md +8 -8
  120. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  121. package/src/prompts/system/subagent-system-prompt.md +29 -22
  122. package/src/prompts/system/subagent-user-prompt.md +7 -3
  123. package/src/prompts/system/summarization-system.md +1 -1
  124. package/src/prompts/system/system-prompt.md +214 -226
  125. package/src/prompts/system/title-system.md +2 -2
  126. package/src/prompts/system/ttsr-interrupt.md +1 -1
  127. package/src/prompts/system/web-search.md +16 -16
  128. package/src/prompts/tools/ask.md +1 -3
  129. package/src/prompts/tools/await.md +2 -4
  130. package/src/prompts/tools/bash.md +5 -7
  131. package/src/prompts/tools/browser.md +4 -6
  132. package/src/prompts/tools/calculator.md +1 -3
  133. package/src/prompts/tools/cancel-job.md +2 -4
  134. package/src/prompts/tools/exit-plan-mode.md +7 -7
  135. package/src/prompts/tools/fetch.md +0 -2
  136. package/src/prompts/tools/find.md +3 -5
  137. package/src/prompts/tools/gemini-image.md +6 -22
  138. package/src/prompts/tools/grep.md +4 -6
  139. package/src/prompts/tools/hashline.md +56 -15
  140. package/src/prompts/tools/lsp.md +1 -3
  141. package/src/prompts/tools/patch.md +7 -9
  142. package/src/prompts/tools/python.md +10 -14
  143. package/src/prompts/tools/read.md +0 -2
  144. package/src/prompts/tools/replace.md +5 -7
  145. package/src/prompts/tools/ssh.md +3 -5
  146. package/src/prompts/tools/task-summary.md +4 -4
  147. package/src/prompts/tools/task.md +7 -9
  148. package/src/prompts/tools/todo-write.md +7 -9
  149. package/src/prompts/tools/web-search.md +3 -5
  150. package/src/prompts/tools/write.md +3 -5
  151. package/src/sdk.ts +4 -2
  152. package/src/session/agent-session.ts +10 -26
  153. package/src/session/agent-storage.ts +1 -2
  154. package/src/session/history-storage.ts +1 -2
  155. package/src/session/session-manager.ts +10 -2
  156. package/src/slash-commands/builtin-registry.ts +26 -1
  157. package/src/ssh/connection-manager.ts +11 -2
  158. package/src/ssh/sshfs-mount.ts +7 -1
  159. package/src/system-prompt.ts +29 -103
  160. package/src/task/agents.ts +1 -1
  161. package/src/task/index.ts +211 -70
  162. package/src/task/render.ts +24 -8
  163. package/src/task/types.ts +6 -1
  164. package/src/task/worktree.ts +394 -32
  165. package/src/tools/ask.ts +0 -1
  166. package/src/tools/bash-interactive.ts +2 -45
  167. package/src/tools/bash.ts +5 -5
  168. package/src/tools/browser.ts +1 -2
  169. package/src/tools/gemini-image.ts +8 -28
  170. package/src/tools/json-tree.ts +2 -1
  171. package/src/tools/python.ts +1 -1
  172. package/src/tools/read.ts +1 -2
  173. package/src/tools/submit-result.ts +22 -23
  174. package/src/utils/commit-message-generator.ts +132 -0
  175. package/src/utils/tools-manager.ts +1 -2
  176. package/src/web/scrapers/artifacthub.ts +2 -1
  177. package/src/web/scrapers/aur.ts +2 -1
  178. package/src/web/scrapers/biorxiv.ts +2 -1
  179. package/src/web/scrapers/bluesky.ts +2 -1
  180. package/src/web/scrapers/chocolatey.ts +2 -1
  181. package/src/web/scrapers/cisa-kev.ts +2 -1
  182. package/src/web/scrapers/clojars.ts +2 -1
  183. package/src/web/scrapers/coingecko.ts +2 -1
  184. package/src/web/scrapers/crates-io.ts +2 -1
  185. package/src/web/scrapers/crossref.ts +2 -1
  186. package/src/web/scrapers/discogs.ts +3 -1
  187. package/src/web/scrapers/discourse.ts +2 -1
  188. package/src/web/scrapers/dockerhub.ts +2 -1
  189. package/src/web/scrapers/fdroid.ts +2 -1
  190. package/src/web/scrapers/firefox-addons.ts +2 -1
  191. package/src/web/scrapers/flathub.ts +2 -1
  192. package/src/web/scrapers/gitlab.ts +1 -1
  193. package/src/web/scrapers/go-pkg.ts +2 -1
  194. package/src/web/scrapers/hackage.ts +2 -1
  195. package/src/web/scrapers/hackernews.ts +2 -1
  196. package/src/web/scrapers/hex.ts +2 -1
  197. package/src/web/scrapers/huggingface.ts +2 -1
  198. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  199. package/src/web/scrapers/lemmy.ts +2 -1
  200. package/src/web/scrapers/lobsters.ts +2 -1
  201. package/src/web/scrapers/mastodon.ts +2 -1
  202. package/src/web/scrapers/maven.ts +2 -1
  203. package/src/web/scrapers/mdn.ts +2 -1
  204. package/src/web/scrapers/metacpan.ts +2 -1
  205. package/src/web/scrapers/musicbrainz.ts +3 -1
  206. package/src/web/scrapers/npm.ts +2 -1
  207. package/src/web/scrapers/nuget.ts +2 -1
  208. package/src/web/scrapers/nvd.ts +2 -1
  209. package/src/web/scrapers/ollama.ts +2 -1
  210. package/src/web/scrapers/open-vsx.ts +2 -1
  211. package/src/web/scrapers/opencorporates.ts +2 -1
  212. package/src/web/scrapers/openlibrary.ts +2 -1
  213. package/src/web/scrapers/orcid.ts +3 -1
  214. package/src/web/scrapers/osv.ts +2 -1
  215. package/src/web/scrapers/packagist.ts +2 -1
  216. package/src/web/scrapers/pub-dev.ts +2 -1
  217. package/src/web/scrapers/pubmed.ts +2 -1
  218. package/src/web/scrapers/pypi.ts +2 -1
  219. package/src/web/scrapers/rawg.ts +2 -8
  220. package/src/web/scrapers/reddit.ts +2 -1
  221. package/src/web/scrapers/repology.ts +2 -1
  222. package/src/web/scrapers/rfc.ts +2 -1
  223. package/src/web/scrapers/rubygems.ts +2 -1
  224. package/src/web/scrapers/searchcode.ts +2 -1
  225. package/src/web/scrapers/sec-edgar.ts +2 -1
  226. package/src/web/scrapers/semantic-scholar.ts +2 -1
  227. package/src/web/scrapers/snapcraft.ts +2 -1
  228. package/src/web/scrapers/sourcegraph.ts +2 -1
  229. package/src/web/scrapers/spdx.ts +2 -1
  230. package/src/web/scrapers/stackoverflow.ts +2 -1
  231. package/src/web/scrapers/terraform.ts +2 -1
  232. package/src/web/scrapers/types.ts +0 -11
  233. package/src/web/scrapers/vimeo.ts +2 -1
  234. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  235. package/src/web/scrapers/w3c.ts +2 -1
  236. package/src/web/scrapers/wikidata.ts +2 -1
  237. package/src/web/search/index.ts +10 -14
  238. package/src/web/search/provider.ts +2 -2
  239. package/src/web/search/providers/codex.ts +1 -2
  240. package/src/web/search/providers/exa.ts +42 -10
  241. package/src/web/search/providers/gemini.ts +1 -1
  242. package/src/web/search/providers/perplexity.ts +20 -9
  243. package/src/web/search/providers/utils.ts +1 -1
@@ -23,6 +23,7 @@ import {
23
23
  type AgentMessage,
24
24
  type AgentState,
25
25
  type AgentTool,
26
+ INTENT_FIELD,
26
27
  type ThinkingLevel,
27
28
  } from "@oh-my-pi/pi-agent-core";
28
29
  import type {
@@ -38,8 +39,7 @@ import type {
38
39
  UsageReport,
39
40
  } from "@oh-my-pi/pi-ai";
40
41
  import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
41
- import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
42
- import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
42
+ import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
43
43
  import type { AsyncJob, AsyncJobManager } from "../async";
44
44
  import type { Rule } from "../capability/rule";
45
45
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
@@ -84,8 +84,6 @@ import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with {
84
84
  import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
85
85
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
86
86
  import type { SecretObfuscator } from "../secrets/obfuscator";
87
- import { closeAllConnections } from "../ssh/connection-manager";
88
- import { unmountAll } from "../ssh/sshfs-mount";
89
87
  import { outputMeta } from "../tools/output-meta";
90
88
  import { resolveToCwd } from "../tools/path-utils";
91
89
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
@@ -273,15 +271,6 @@ const noOpUIContext: ExtensionUIContext = {
273
271
  setToolsExpanded: () => {},
274
272
  };
275
273
 
276
- async function cleanupSshResources(): Promise<void> {
277
- const results = await Promise.allSettled([closeAllConnections(), unmountAll()]);
278
- for (const result of results) {
279
- if (result.status === "rejected") {
280
- logger.warn("SSH cleanup failed", { error: String(result.reason) });
281
- }
282
- }
283
- }
284
-
285
274
  // ============================================================================
286
275
  // AgentSession Class
287
276
  // ============================================================================
@@ -1275,13 +1264,19 @@ export class AgentSession {
1275
1264
  * Call this when completely done with the session.
1276
1265
  */
1277
1266
  async dispose(): Promise<void> {
1267
+ try {
1268
+ if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
1269
+ await this.#extensionRunner.emit({ type: "session_shutdown" });
1270
+ }
1271
+ } catch (error) {
1272
+ logger.warn("Failed to emit session_shutdown event", { error: String(error) });
1273
+ }
1278
1274
  const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
1279
1275
  const deliveryState = this.#asyncJobManager?.getDeliveryState();
1280
1276
  if (drained === false && deliveryState) {
1281
1277
  logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
1282
1278
  }
1283
1279
  await this.sessionManager.flush();
1284
- await cleanupSshResources();
1285
1280
  for (const state of this.#providerSessionState.values()) {
1286
1281
  state.close();
1287
1282
  }
@@ -4615,7 +4610,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
4615
4610
  function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
4616
4611
  const parts: string[] = [];
4617
4612
  for (const [key, value] of Object.entries(args)) {
4618
- if (key === "agent__intent") continue;
4613
+ if (key === INTENT_FIELD) continue;
4619
4614
  const text = typeof value === "string" ? value : JSON.stringify(value);
4620
4615
  parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
4621
4616
  }
@@ -4861,15 +4856,4 @@ Be thorough - include exact file paths, function names, error messages, and tech
4861
4856
  get extensionRunner(): ExtensionRunner | undefined {
4862
4857
  return this.#extensionRunner;
4863
4858
  }
4864
-
4865
- /**
4866
- * Emit a custom tool session event (backwards compatibility for older callers).
4867
- */
4868
- async emitCustomToolSessionEvent(reason: "start" | "switch" | "branch" | "tree" | "shutdown"): Promise<void> {
4869
- if (reason !== "shutdown") return;
4870
- if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
4871
- await this.#extensionRunner.emit({ type: "session_shutdown" });
4872
- }
4873
- await cleanupSshResources();
4874
- }
4875
4859
  }
@@ -2,8 +2,7 @@ import { Database, type Statement } from "bun:sqlite";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { type AuthCredential, AuthCredentialStore, type StoredAuthCredential } from "@oh-my-pi/pi-ai";
5
- import { isRecord, logger } from "@oh-my-pi/pi-utils";
6
- import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
5
+ import { getAgentDbPath, isRecord, logger } from "@oh-my-pi/pi-utils";
7
6
  import type { RawSettings as Settings } from "../config/settings";
8
7
 
9
8
  /** Row shape for settings table queries */
@@ -1,8 +1,7 @@
1
1
  import { Database, type Statement } from "bun:sqlite";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { logger } from "@oh-my-pi/pi-utils";
5
- import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
4
+ import { getAgentDir, logger } from "@oh-my-pi/pi-utils";
6
5
 
7
6
  export interface HistoryEntry {
8
7
  id: number;
@@ -4,8 +4,16 @@ import * as path from "node:path";
4
4
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
5
5
  import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
6
6
  import { getTerminalId } from "@oh-my-pi/pi-tui";
7
- import { isEnoent, logger, parseJsonlLenient, Snowflake, toError } from "@oh-my-pi/pi-utils";
8
- import { getBlobsDir, getAgentDir as getDefaultAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
+ import {
8
+ getBlobsDir,
9
+ getAgentDir as getDefaultAgentDir,
10
+ getProjectDir,
11
+ isEnoent,
12
+ logger,
13
+ parseJsonlLenient,
14
+ Snowflake,
15
+ toError,
16
+ } from "@oh-my-pi/pi-utils";
9
17
  import { ArtifactManager } from "./artifacts";
10
18
  import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
11
19
  import {
@@ -259,7 +259,32 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
259
259
  {
260
260
  name: "login",
261
261
  description: "Login with OAuth provider",
262
- handle: (_command, runtime) => {
262
+ inlineHint: "[redirect URL]",
263
+ allowArgs: true,
264
+ handle: (command, runtime) => {
265
+ const manualInput = runtime.ctx.oauthManualInput;
266
+ const args = command.args.trim();
267
+ if (args.length > 0) {
268
+ const submitted = manualInput.submit(args);
269
+ if (submitted) {
270
+ runtime.ctx.showStatus("OAuth callback received; completing login…");
271
+ } else {
272
+ runtime.ctx.showWarning("No OAuth login is waiting for a manual callback.");
273
+ }
274
+ runtime.ctx.editor.setText("");
275
+ return;
276
+ }
277
+
278
+ if (manualInput.hasPending()) {
279
+ const provider = manualInput.pendingProviderId;
280
+ const message = provider
281
+ ? `OAuth login already in progress for ${provider}. Paste the redirect URL with /login <url>.`
282
+ : "OAuth login already in progress. Paste the redirect URL with /login <url>.";
283
+ runtime.ctx.showWarning(message);
284
+ runtime.ctx.editor.setText("");
285
+ return;
286
+ }
287
+
263
288
  void runtime.ctx.showOAuthSelector("login");
264
289
  runtime.ctx.editor.setText("");
265
290
  },
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
4
- import { getRemoteHostDir, getSshControlDir } from "@oh-my-pi/pi-utils/dirs";
3
+ import { getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
5
4
  import { $ } from "bun";
6
5
  import { buildSshTarget, sanitizeHostName } from "./utils";
7
6
 
@@ -69,6 +68,7 @@ async function validateKeyPermissions(keyPath?: string): Promise<void> {
69
68
 
70
69
  function buildCommonArgs(host: SSHConnectionTarget): string[] {
71
70
  const args = [
71
+ "-n",
72
72
  "-o",
73
73
  "ControlMaster=auto",
74
74
  "-o",
@@ -362,6 +362,8 @@ export async function buildRemoteCommand(host: SSHConnectionTarget, command: str
362
362
  return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command];
363
363
  }
364
364
 
365
+ let registered = false;
366
+
365
367
  export async function ensureConnection(host: SSHConnectionTarget): Promise<void> {
366
368
  const key = host.name;
367
369
  const pending = pendingConnections.get(key);
@@ -375,6 +377,13 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
375
377
  ensureControlDir();
376
378
  await validateKeyPermissions(host.keyPath);
377
379
 
380
+ if (!registered) {
381
+ registered = true;
382
+ postmortem.register("ssh-cleanup", async () => {
383
+ await closeAllConnections();
384
+ });
385
+ }
386
+
378
387
  const target = buildSshTarget(host.username, host.host);
379
388
  const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
380
389
  if (check.exitCode === 0) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs";
3
+ import { getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { $ } from "bun";
5
5
  import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
6
6
  import { buildSshTarget, sanitizeHostName } from "./utils";
@@ -83,6 +83,8 @@ export async function isMounted(path: string): Promise<boolean> {
83
83
  return result.exitCode === 0;
84
84
  }
85
85
 
86
+ let registered = false;
87
+
86
88
  export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): Promise<string | undefined> {
87
89
  if (!hasSshfs()) return undefined;
88
90
 
@@ -90,6 +92,10 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"):
90
92
  await Promise.all([ensureDir(REMOTE_DIR), ensureDir(CONTROL_DIR), ensureDir(mountPath)]);
91
93
 
92
94
  if (await isMounted(mountPath)) {
95
+ if (!registered) {
96
+ registered = true;
97
+ postmortem.register("sshfs-cleanup", unmountAll);
98
+ }
93
99
  mountedPaths.add(mountPath);
94
100
  return mountPath;
95
101
  }
@@ -5,8 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
- import { $env, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
9
- import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
8
+ import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
10
9
  import { $ } from "bun";
11
10
  import { contextFileCapability } from "./capability/context-file";
12
11
  import { systemPromptCapability } from "./capability/system-prompt";
@@ -16,7 +15,6 @@ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile
16
15
  import { loadSkills, type Skill } from "./extensibility/skills";
17
16
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
18
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
19
- import type { ToolName } from "./tools";
20
18
 
21
19
  type PreloadedSkill = { name: string; content: string };
22
20
 
@@ -206,65 +204,6 @@ function getTerminalName(): string | undefined {
206
204
  return term ?? undefined;
207
205
  }
208
206
 
209
- function normalizeDesktopValue(value: string): string | undefined {
210
- const trimmed = value.trim();
211
- if (!trimmed) return undefined;
212
- const parts = trimmed
213
- .split(":")
214
- .map(part => part.trim())
215
- .filter(Boolean);
216
- return parts[0] ?? trimmed;
217
- }
218
-
219
- function getDesktopEnvironment(): string | undefined {
220
- if (Bun.env.KDE_FULL_SESSION === "true") return "KDE";
221
- const raw = firstNonEmpty(
222
- Bun.env.XDG_CURRENT_DESKTOP,
223
- Bun.env.DESKTOP_SESSION,
224
- Bun.env.XDG_SESSION_DESKTOP,
225
- Bun.env.GDMSESSION,
226
- );
227
- return raw ? normalizeDesktopValue(raw) : undefined;
228
- }
229
-
230
- function matchKnownWindowManager(value: string): string | null {
231
- const normalized = value.toLowerCase();
232
- const candidates = [
233
- "sway",
234
- "i3",
235
- "i3wm",
236
- "bspwm",
237
- "openbox",
238
- "awesome",
239
- "herbstluftwm",
240
- "fluxbox",
241
- "icewm",
242
- "dwm",
243
- "hyprland",
244
- "wayfire",
245
- "river",
246
- "labwc",
247
- "qtile",
248
- ];
249
- for (const candidate of candidates) {
250
- if (normalized.includes(candidate)) return candidate;
251
- }
252
- return null;
253
- }
254
-
255
- function getWindowManager(): string | undefined {
256
- const explicit = firstNonEmpty(Bun.env.WINDOWMANAGER);
257
- if (explicit) return explicit;
258
-
259
- const desktop = firstNonEmpty(Bun.env.XDG_CURRENT_DESKTOP, Bun.env.DESKTOP_SESSION);
260
- if (desktop) {
261
- const matched = matchKnownWindowManager(desktop);
262
- if (matched) return matched;
263
- }
264
-
265
- return undefined;
266
- }
267
-
268
207
  /** Cached system info structure */
269
208
  interface GpuCache {
270
209
  gpu: string;
@@ -308,13 +247,11 @@ async function getEnvironmentInfo(): Promise<Array<{ label: string; value: strin
308
247
  { label: "Distro", value: os.type() },
309
248
  { label: "Kernel", value: os.version() },
310
249
  { label: "Arch", value: os.arch() },
311
- { label: "CPU", value: `${cpus.length}x ${cpus[0]?.model}` },
250
+ { label: "CPU", value: `${cpus[0]?.model}` },
312
251
  { label: "GPU", value: gpu },
313
252
  { label: "Terminal", value: getTerminalName() },
314
- { label: "DE", value: getDesktopEnvironment() },
315
- { label: "WM", value: getWindowManager() },
316
253
  ];
317
- return entries.filter((e): e is { label: string; value: string } => e.value != null && e.value !== "unknown");
254
+ return entries.filter((e): e is { label: string; value: string } => !!e.value);
318
255
  }
319
256
 
320
257
  /** Resolve input as file path or literal string */
@@ -421,6 +358,8 @@ export interface BuildSystemPromptOptions {
421
358
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
422
359
  /** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
423
360
  intentField?: string;
361
+ /** Encourage the agent to delegate via tasks unless changes are trivial. */
362
+ eagerTasks?: boolean;
424
363
  }
425
364
 
426
365
  /** Build the system prompt with tools, guidelines, and context */
@@ -435,13 +374,14 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
435
374
  appendSystemPrompt,
436
375
  repeatToolDescriptions = false,
437
376
  skillsSettings,
438
- toolNames,
377
+ toolNames: providedToolNames,
439
378
  cwd,
440
379
  contextFiles: providedContextFiles,
441
380
  skills: providedSkills,
442
381
  preloadedSkills: providedPreloadedSkills,
443
382
  rules,
444
383
  intentField,
384
+ eagerTasks = false,
445
385
  } = options;
446
386
  const resolvedCwd = cwd ?? getProjectDir();
447
387
  const preloadedSkills = providedPreloadedSkills;
@@ -554,25 +494,24 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
554
494
  timeZoneName: "short",
555
495
  });
556
496
 
557
- // Build tool descriptions array
558
- // Priority: toolNames (explicit list) > tools (Map) > defaults
497
+ // Build tool metadata for system prompt rendering
498
+ // Priority: explicit list > tools map > defaults
559
499
  // Default includes both bash and python; actual availability determined by settings in createTools
560
- const defaultToolNames: ToolName[] = ["read", "bash", "python", "edit", "write"];
561
- let toolNamesArray: string[];
562
- if (toolNames !== undefined) {
563
- // Explicit toolNames list provided (could be empty)
564
- toolNamesArray = toolNames;
565
- } else if (tools !== undefined) {
566
- // Tools map provided
567
- toolNamesArray = Array.from(tools.keys());
568
- } else {
569
- // Use defaults
570
- toolNamesArray = defaultToolNames;
500
+ let toolNames = providedToolNames;
501
+ if (!toolNames) {
502
+ if (tools) {
503
+ // Tools map provided
504
+ toolNames = Array.from(tools.keys());
505
+ } else {
506
+ // Use defaults
507
+ toolNames = ["read", "bash", "python", "edit", "write"]; // TODO: Why?
508
+ }
571
509
  }
572
510
 
573
511
  // Build tool descriptions for system prompt rendering
574
- const toolDescriptions = toolNamesArray.map(name => ({
512
+ const toolInfo = toolNames.map(name => ({
575
513
  name,
514
+ label: tools?.get(name)?.label ?? "",
576
515
  description: tools?.get(name)?.description ?? "",
577
516
  }));
578
517
 
@@ -580,29 +519,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
580
519
  const hasRead = tools?.has("read");
581
520
  const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
582
521
 
583
- if (resolvedCustomPrompt) {
584
- return renderPromptTemplate(customSystemPromptTemplate, {
585
- systemPromptCustomization: systemPromptCustomization ?? "",
586
- customPrompt: resolvedCustomPrompt,
587
- appendPrompt: resolvedAppendPrompt ?? "",
588
- contextFiles,
589
- agentsMdSearch,
590
- skills: filteredSkills,
591
- preloadedSkills: preloadedSkillContents,
592
- rules: rules ?? [],
593
- date,
594
- dateTime,
595
- cwd: resolvedCwd,
596
- });
597
- }
598
-
599
522
  const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
600
- return renderPromptTemplate(systemPromptTemplate, {
601
- tools: toolNamesArray,
602
- toolDescriptions,
523
+ const data = {
524
+ systemPromptCustomization: systemPromptCustomization ?? "",
525
+ customPrompt: resolvedCustomPrompt,
526
+ appendPrompt: resolvedAppendPrompt ?? "",
527
+ tools: toolNames,
528
+ toolInfo,
603
529
  repeatToolDescriptions,
604
530
  environment,
605
- systemPromptCustomization: systemPromptCustomization ?? "",
606
531
  contextFiles,
607
532
  agentsMdSearch,
608
533
  skills: filteredSkills,
@@ -611,8 +536,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
611
536
  date,
612
537
  dateTime,
613
538
  cwd: resolvedCwd,
614
- appendSystemPrompt: resolvedAppendPrompt ?? "",
615
539
  intentTracing: !!intentField,
616
540
  intentField: intentField ?? "",
617
- });
541
+ eagerTasks,
542
+ };
543
+ return renderPromptTemplate(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
618
544
  }
@@ -108,7 +108,7 @@ export function parseAgent(
108
108
  });
109
109
  const fields = parseAgentFields(frontmatter);
110
110
  if (!fields) {
111
- throw new AgentParsingError(new Error("Invalid agent fields"), filePath);
111
+ throw new AgentParsingError(new Error(`Invalid agent field: ${filePath}\n${content}`), filePath);
112
112
  }
113
113
  return {
114
114
  ...fields,