@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/src/autoresearch/helpers.ts +17 -0
  6. package/src/autoresearch/tools/log-experiment.ts +9 -17
  7. package/src/autoresearch/tools/run-experiment.ts +2 -17
  8. package/src/capability/skill.ts +7 -0
  9. package/src/cli/list-models.ts +1 -1
  10. package/src/cli/shell-cli.ts +3 -13
  11. package/src/cli/update-cli.ts +1 -1
  12. package/src/cli.ts +10 -29
  13. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  14. package/src/commit/analysis/conventional.ts +8 -66
  15. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  16. package/src/commit/pipeline.ts +2 -2
  17. package/src/commit/shared-llm.ts +89 -0
  18. package/src/config/config-file.ts +210 -0
  19. package/src/config/model-equivalence.ts +8 -11
  20. package/src/config/model-registry.ts +13 -2
  21. package/src/config/model-resolver.ts +1 -4
  22. package/src/config/settings-schema.ts +71 -1
  23. package/src/config/settings.ts +1 -1
  24. package/src/config.ts +3 -219
  25. package/src/edit/renderer.ts +7 -1
  26. package/src/eval/js/executor.ts +3 -0
  27. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  28. package/src/eval/py/executor.ts +5 -0
  29. package/src/exa/factory.ts +2 -2
  30. package/src/exa/mcp-client.ts +74 -1
  31. package/src/exec/bash-executor.ts +5 -1
  32. package/src/export/html/template.generated.ts +1 -1
  33. package/src/export/html/template.js +0 -11
  34. package/src/extensibility/extensions/runner.ts +1 -1
  35. package/src/extensibility/extensions/types.ts +89 -223
  36. package/src/extensibility/hooks/types.ts +89 -314
  37. package/src/extensibility/shared-events.ts +343 -0
  38. package/src/extensibility/skills.ts +9 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/runtime.ts +500 -0
  41. package/src/goals/state.ts +37 -0
  42. package/src/goals/tools/goal-tool.ts +237 -0
  43. package/src/hashline/anchors.ts +2 -2
  44. package/src/hindsight/mental-models.ts +1 -1
  45. package/src/internal-urls/agent-protocol.ts +1 -20
  46. package/src/internal-urls/artifact-protocol.ts +1 -19
  47. package/src/internal-urls/docs-index.generated.ts +5 -6
  48. package/src/internal-urls/registry-helpers.ts +25 -0
  49. package/src/main.ts +11 -2
  50. package/src/mcp/oauth-flow.ts +20 -0
  51. package/src/modes/acp/acp-agent.ts +79 -45
  52. package/src/modes/components/assistant-message.ts +14 -8
  53. package/src/modes/components/bash-execution.ts +24 -63
  54. package/src/modes/components/custom-message.ts +14 -40
  55. package/src/modes/components/eval-execution.ts +27 -57
  56. package/src/modes/components/execution-shared.ts +102 -0
  57. package/src/modes/components/hook-message.ts +17 -49
  58. package/src/modes/components/mcp-add-wizard.ts +26 -5
  59. package/src/modes/components/message-frame.ts +88 -0
  60. package/src/modes/components/model-selector.ts +1 -1
  61. package/src/modes/components/session-observer-overlay.ts +6 -2
  62. package/src/modes/components/session-selector.ts +1 -1
  63. package/src/modes/components/status-line/segments.ts +55 -4
  64. package/src/modes/components/status-line/types.ts +4 -0
  65. package/src/modes/components/status-line.ts +28 -10
  66. package/src/modes/components/tool-execution.ts +7 -8
  67. package/src/modes/controllers/command-controller-shared.ts +108 -0
  68. package/src/modes/controllers/command-controller.ts +13 -4
  69. package/src/modes/controllers/event-controller.ts +36 -7
  70. package/src/modes/controllers/input-controller.ts +13 -0
  71. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  72. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  73. package/src/modes/interactive-mode.ts +624 -52
  74. package/src/modes/print-mode.ts +16 -86
  75. package/src/modes/rpc/rpc-mode.ts +14 -87
  76. package/src/modes/runtime-init.ts +115 -0
  77. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  78. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  79. package/src/modes/theme/theme.ts +18 -6
  80. package/src/modes/types.ts +14 -3
  81. package/src/modes/utils/context-usage.ts +13 -13
  82. package/src/modes/utils/ui-helpers.ts +10 -3
  83. package/src/plan-mode/approved-plan.ts +35 -1
  84. package/src/prompts/goals/goal-budget-limit.md +16 -0
  85. package/src/prompts/goals/goal-continuation.md +28 -0
  86. package/src/prompts/goals/goal-mode-active.md +23 -0
  87. package/src/prompts/system/plan-mode-active.md +5 -5
  88. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  89. package/src/prompts/tools/bash.md +6 -0
  90. package/src/prompts/tools/goal.md +13 -0
  91. package/src/prompts/tools/hashline.md +102 -114
  92. package/src/prompts/tools/read.md +1 -0
  93. package/src/prompts/tools/resolve.md +6 -5
  94. package/src/sdk.ts +12 -5
  95. package/src/session/agent-session.ts +428 -106
  96. package/src/session/blob-store.ts +36 -3
  97. package/src/session/messages.ts +67 -2
  98. package/src/session/session-manager.ts +131 -12
  99. package/src/session/session-storage.ts +33 -15
  100. package/src/session/streaming-output.ts +309 -13
  101. package/src/slash-commands/builtin-registry.ts +18 -0
  102. package/src/ssh/ssh-executor.ts +5 -0
  103. package/src/system-prompt.ts +4 -2
  104. package/src/task/executor.ts +17 -7
  105. package/src/task/index.ts +3 -0
  106. package/src/task/render.ts +21 -15
  107. package/src/task/types.ts +4 -0
  108. package/src/tools/ast-edit.ts +21 -120
  109. package/src/tools/ast-grep.ts +21 -119
  110. package/src/tools/bash-interactive.ts +9 -1
  111. package/src/tools/bash.ts +27 -4
  112. package/src/tools/browser/attach.ts +3 -3
  113. package/src/tools/browser/launch.ts +81 -18
  114. package/src/tools/browser/registry.ts +1 -5
  115. package/src/tools/browser/tab-supervisor.ts +51 -14
  116. package/src/tools/conflict-detect.ts +15 -4
  117. package/src/tools/eval.ts +3 -1
  118. package/src/tools/find.ts +20 -38
  119. package/src/tools/gh.ts +7 -6
  120. package/src/tools/index.ts +22 -11
  121. package/src/tools/inspect-image.ts +3 -10
  122. package/src/tools/output-meta.ts +176 -37
  123. package/src/tools/path-utils.ts +125 -2
  124. package/src/tools/read.ts +516 -233
  125. package/src/tools/render-utils.ts +92 -0
  126. package/src/tools/renderers.ts +2 -0
  127. package/src/tools/resolve.ts +72 -44
  128. package/src/tools/search.ts +120 -186
  129. package/src/tools/write.ts +44 -9
  130. package/src/utils/file-mentions.ts +1 -1
  131. package/src/utils/image-loading.ts +7 -3
  132. package/src/utils/image-resize.ts +32 -43
  133. package/src/vim/parser.ts +0 -17
  134. package/src/vim/render.ts +1 -1
  135. package/src/vim/types.ts +1 -1
  136. package/src/web/search/providers/gemini.ts +35 -95
  137. package/src/prompts/tools/exit-plan-mode.md +0 -6
  138. package/src/tools/exit-plan-mode.ts +0 -97
  139. package/src/utils/fuzzy.ts +0 -108
  140. package/src/utils/image-convert.ts +0 -27
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.0.1] - 2026-05-14
6
+ ### Breaking Changes
7
+
8
+ - Removed the dedicated `exit_plan_mode` tool and its prompt, requiring plan-mode completion to use the existing `resolve` tool path instead
9
+
10
+ ### Added
11
+
12
+ - Added optional `extra` metadata object to the `resolve` tool so callers can pass context-specific payloads, including plan approval titles
13
+ - Added `hide: true` frontmatter option for skill `SKILL.md` files. Hidden skills are still loaded and remain reachable via `skill://<name>` URLs and (when enabled) `/skill:<name>` slash commands, but are omitted from the rendered system prompt's `<skills>` listing so the model won't auto-discover them. Use for skills the user opts into explicitly rather than ones the model should pick up from descriptions.
14
+ - Added middle elision for streaming tool outputs (bash, ssh, python, js eval) and post-execution tool result spill. When `tools.artifactHeadBytes` is set (default 20 KB), large outputs now keep both the first N KB and the last N KB with an inline `[… N lines elided (M KB) …]` marker between them, instead of dropping everything before the trailing tail. Setting `tools.artifactHeadBytes = 0` reverts to the previous tail-only behavior. The full output is still mirrored to the session artifact (`artifact://<id>`) regardless of elision mode. Exposes `truncateMiddle` and `formatMiddleElisionMarker` from `@oh-my-pi/pi-coding-agent/session/streaming-output`, extends `OutputSinkOptions` with `headBytes`, and adds `direction: "middle"` plus `headRange` / `tailRange` / `elidedLines` / `elidedBytes` to `TruncationMeta`.
15
+ - Added per-line column cap shared across streaming tool outputs (`bash`, `ssh`, `python`, `js eval`) and the `read` tool. Lines wider than `tools.outputMaxColumns` bytes (default **768**) are ellipsis-truncated at write time and remaining bytes up to the next `\n` are dropped — bounded memory even on multi-MB single-line outputs (e.g. `cat /dev/urandom`). The cap lives on `OutputSink` as the new `maxColumns` option, persists state across chunk boundaries so split-mid-line writes still respect the budget, and exposes `columnDroppedBytes` / `columnTruncatedLines` on `OutputSummary`. Middle-elision byte math subtracts column drops so the "elided from middle" count stays honest. `read` reuses the same setting but trims its already-collected lines via `truncateLine`. Skipped when the read selector is `:raw`. The artifact file (`artifact://<id>`) keeps the full uncapped stream. Set `tools.outputMaxColumns = 0` to disable.
16
+ - Added Bun HTTP/2 fetch opt-in. Dev scripts (`bun run dev`, `bun run stats`) now pass `bun --experimental-http2-fetch` so every `fetch()` advertises `h2` in the TLS ALPN list and falls back to HTTP/1.1 when the server doesn't select it. Multiplexing collapses parallel requests to the same origin onto one TLS connection. For the installed `omp` binary, export `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` in your shell to enable the same behavior (the flag has to be set before Bun starts; `process.env` from inside JS is too late). Requires Bun **1.3.14**.
17
+ - Added per-subagent cost display (`$X.XX` in the task progress tree and the session-observer stats line). Cost is accumulated incrementally from `message_end` events and shown only when non-zero, using the `statusLineCost` theme color. Providers that do not report per-turn cost data (e.g. subscription/OAuth usage) continue to show nothing.
18
+
19
+ ### Changed
20
+
21
+ - Changed plan-mode completion to use `resolve { action: "apply", reason, extra: { title } }` to request plan approval rather than calling `exit_plan_mode`
22
+ - Changed resolve pending-action previews to trim and truncate long `reason` text for cleaner status-line rendering
23
+ - Raised the image downscaling default JPEG quality from 75 to 80 in `resizeImage` output generation
24
+ - Changed image resize metadata notes from coordinate-scale hints to a simple `Image resized from <original> to <displayed>` message and hide the note when the resized dimensions are unchanged
25
+ - Removed `utils/image-convert.ts` and its `convertToPng` helper; callers now inline `new Bun.Image(bytes).png().toBase64()` from [`Bun.Image`](https://bun.com/docs/runtime/image) (Bun 1.3.14+).
26
+ - Changed image decode/resize/encode in `utils/image-resize.ts` from the native `PhotonImage` binding to [`Bun.Image`](https://bun.com/docs/runtime/image). Same PNG/JPEG/WebP quality+dimension ladder, but pipelines run off-thread on Bun's statically-linked codecs with no native-addon round-trip. Bumped the minimum Bun runtime requirement to **1.3.14**.
27
+ - Changed `search` pagination in multi-file scopes so `skip` now skips entire files and pages results in groups of up to 20 files, with output guiding the next `skip` value via `Showing files X-Y of N`
28
+ - Changed multi-file search result selection to cap each file at 20 matches and round-robin across files, so one noisy file no longer suppresses visibility of hits in other files and truncation now reports per-file limits
29
+ - Changed search truncation metadata/renderer output from match/result-based limits to file-based limits (`fileLimitReached`, `perFileLimitReached`) and updated truncation labels accordingly
30
+ - Lowered `read.defaultLimit` default from `500` to `300` lines, and split the per-range context padding into asymmetric `RANGE_LEADING_CONTEXT_LINES = 1` / `RANGE_TRAILING_CONTEXT_LINES = 3` (was symmetric `RANGE_CONTEXT_LINES = 3`). Replay analysis over post-summarizer sessions (`scripts/session-stats/optimize_read_config.py`) showed that bare-path reads are over-provisioned at the median (file p50 = 220 lines) and that most follow-up reads are disjoint hops rather than adjacent extensions — so a smaller default plus narrower leading context reclaims tokens without measurably changing first-cover rate. Trailing context stays at 3 lines to keep anchor-stale recovery on narrow reads. Explicit `read.defaultLimit` overrides in settings are honoured unchanged.
31
+
32
+ ### Fixed
33
+
34
+ - Fixed abrupt process termination data loss during session persistence by moving steady-state session writes to a synchronous path that writes each entry to the kernel page cache before returning
35
+ - Fixed `--help` startup to avoid a config/model-registry load cycle so the root CLI help command now exits successfully in a clean environment
36
+ - Queued `/skill:<name> [args]` invocations now show as compact `Steer: /skill:<name> [args]` / `Follow-up: /skill:<name> [args]` chips in the pending-messages bar and disappear when the agent consumes the queued message (parity with plain-text steer/follow-up). Previously the queued skill was invisible while queued and rendered as a full skill block at consumption with no chip ever appearing.
37
+ - Plan-mode "Approve and compact context" no longer surfaces a red "Operation aborted" line on the plan-mode assistant message; the silent transition into compaction now renders cleanly on both live and replay paths. Real user-cancel aborts on unrelated turns and the existing "Compaction cancelled" path are unchanged.
38
+ - Auto-recover conflict-resolution `write`/`read` paths that the agent malformed as `<file>:conflict://<N>` (or `<file>:conflict://*`) by mixing the `:conflicts` read selector with the `conflict://` scheme. The stripped `<file>:` prefix is stored on `ParsedConflictUri.recoveredPrefix` and, for writes, surfaces as a trailing note in the result text so the agent learns the correct shape. Clean `conflict://…` URIs are unchanged.
39
+ - Fixed hashline edit renderer leaving a stray `@` in the displayed file path when the agent emitted a canonical `@@ PATH` header (or any `@`-run longer than one). Titles like `Edit: @ packages/foo.ts` now render as `Edit: packages/foo.ts`, matching the actual parser in `hashline/input.ts` which already strips every leading `@` before resolving the path. Purely cosmetic — the edit itself was always routed to the correct file.
40
+ - Fixed model contextWindow and maxTokens defaulting to `UNK_CONTEXT_WINDOW` (222222) / `UNK_MAX_TOKENS` (8888) when cached or freshly-discovered provider models replace bundled models through `ModelRegistry.#mergeResolvedModels`. The merge now preserves the bundled model's values when the replacement only has sentinel fallbacks.
41
+ - Fixed headless `browser.open` tab startup on slow Chromium target enumeration by making worker-side stealth user-agent target setup selective, bounded, and best-effort for non-active targets. Worker startup errors are now surfaced directly instead of degrading into the generic tab worker initialization timeout.
42
+ - Fixed token display for sessions and subagents inflating far beyond the context window. `token_total` status-line segment and the subagent overlay token counter now show `input + output + cacheWrite` instead of `input + output + cacheRead + cacheWrite`. With prompt caching, `cacheRead` per turn equals the full cached context — summing it across all turns produces a cumulative total that is N×context_size (e.g. a 5-turn session with a 1 M-token context reported ~5 M tokens). Cache activity is still visible via the dedicated `cache_read`/`cache_write` status-line segments; billing cost is unaffected.
43
+ - Fixed ACP clients missing `config_option_update` notifications when the thinking level changed via any path other than the client's own `session/set_session_config_option` call (slash commands, model auto-adjust, extension UI). `AgentSession` now emits a `thinking_level_changed` event from `setThinkingLevel`, and `AcpAgent` subscribes to each managed session for the session's lifetime and pushes a fresh `config_option_update` whenever the effective level changes — independent of any active prompt turn. The subscription is installed inside `#scheduleBootstrapUpdates`'s 50 ms timer so it shares the same race guard that prevents Zed's `Received session notification for unknown session` drop when notifications fire before `session/new` (or fork) returns; the pre-bootstrap thinking level is reported in the response's `configOptions`. The `session/set_session_config_option` handler keeps its own push only when the subscription has not yet been installed, so client-driven thinking changes still notify pre-bootstrap, post-bootstrap they flow through the subscription exactly once. Subscriptions are released in `#disposeSessionRecord`.
44
+ - Fixed MCP OAuth refresh failing with `HTTP 401 invalid_client` for servers that require Dynamic Client Registration (RFC 7591) and have no `oauth.clientId` configured (e.g. `mcp.linear.app`). `MCPOAuthFlow` registered a fresh public PKCE client on each authorize and discarded the issued `client_id` once the flow object went out of scope; refresh then called the provider's `/token` endpoint without a `client_id`. The flow now exposes `resolvedClientId` / `registeredClientSecret` getters, `MCPCommandController#handleOAuthFlow` returns them alongside `credentialId`, and both the initial-connect and `/mcp reauth` paths persist them into `auth.{clientId,clientSecret}` (used at refresh) and `oauth.{clientId,clientSecret}` (used by subsequent `/mcp reauth` to skip re-registration). The `MCPAddWizard` `onOAuth` callback type is now `Promise<MCPAddWizardOAuthResult>` and `#launchOAuthFlow` folds the registered credentials into wizard state. Servers with a statically-configured `oauth.clientId` (Notion, Slack, Datadog) are unaffected — `#tryRegisterClient` short-circuits and the write-back is a no-op. ([#1061](https://github.com/can1357/oh-my-pi/pull/1061) by [@ldx](https://github.com/ldx)).
45
+
5
46
  ## [15.0.0] - 2026-05-13
6
47
  ### Breaking Changes
7
48
 
@@ -334,7 +334,6 @@ export default function planModeExtension(pi: ExtensionAPI) {
334
334
  }
335
335
 
336
336
  // Remove any previous plan-mode-context messages
337
- const _beforeCount = event.messages.length;
338
337
  const filtered = event.messages.filter(m => {
339
338
  if (m.role === "user" && Array.isArray(m.content)) {
340
339
  const hasOldContext = m.content.some(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.0.0",
4
+ "version": "15.0.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "15.0.0",
51
- "@oh-my-pi/pi-agent-core": "15.0.0",
52
- "@oh-my-pi/pi-ai": "15.0.0",
53
- "@oh-my-pi/pi-natives": "15.0.0",
54
- "@oh-my-pi/pi-tui": "15.0.0",
55
- "@oh-my-pi/pi-utils": "15.0.0",
50
+ "@oh-my-pi/omp-stats": "15.0.1",
51
+ "@oh-my-pi/pi-agent-core": "15.0.1",
52
+ "@oh-my-pi/pi-ai": "15.0.1",
53
+ "@oh-my-pi/pi-natives": "15.0.1",
54
+ "@oh-my-pi/pi-tui": "15.0.1",
55
+ "@oh-my-pi/pi-utils": "15.0.1",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@sinclair/typebox": "^0.34.49",
58
58
  "@types/turndown": "5.0.6",
@@ -71,10 +71,10 @@
71
71
  "zod": "4.4.3"
72
72
  },
73
73
  "devDependencies": {
74
- "@types/bun": "^1.3.13"
74
+ "@types/bun": "^1.3.14"
75
75
  },
76
76
  "engines": {
77
- "bun": ">=1.3.7"
77
+ "bun": ">=1.3.14"
78
78
  },
79
79
  "files": [
80
80
  "src",
@@ -33,6 +33,11 @@ async function main(): Promise<void> {
33
33
  "bun",
34
34
  "build",
35
35
  "--compile",
36
+ "--no-compile-autoload-bunfig",
37
+ "--no-compile-autoload-dotenv",
38
+ "--no-compile-autoload-tsconfig",
39
+ "--no-compile-autoload-package-json",
40
+ "--keep-names",
36
41
  "--define",
37
42
  'process.env.PI_COMPILED="true"',
38
43
  "--external",
@@ -1,3 +1,4 @@
1
+ import * as git from "../utils/git";
1
2
  import type { ASIData, ASIValue, MetricDirection, NumericMetricMap } from "./types";
2
3
 
3
4
  export const METRIC_LINE_PREFIX = "METRIC";
@@ -199,3 +200,19 @@ function sanitizeAsiValue(value: unknown): ASIValue | undefined {
199
200
  }
200
201
  return undefined;
201
202
  }
203
+
204
+ export async function tryGitStatus(cwd: string): Promise<string> {
205
+ try {
206
+ return await git.status(cwd, { porcelainV1: true, untrackedFiles: "all", z: true });
207
+ } catch {
208
+ return "";
209
+ }
210
+ }
211
+
212
+ export async function tryGitPrefix(cwd: string): Promise<string> {
213
+ try {
214
+ return await git.show.prefix(cwd);
215
+ } catch {
216
+ return "";
217
+ }
218
+ }
@@ -8,7 +8,15 @@ import type { Theme } from "../../modes/theme/theme";
8
8
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
9
9
  import * as git from "../../utils/git";
10
10
  import { computeRunModifiedPaths, getCurrentAutoresearchBranch, parseWorkDirDirtyPaths } from "../git";
11
- import { ensureNumericMetricMap, formatNum, mergeAsi, pathMatchesSpec, sanitizeAsi } from "../helpers";
11
+ import {
12
+ ensureNumericMetricMap,
13
+ formatNum,
14
+ mergeAsi,
15
+ pathMatchesSpec,
16
+ sanitizeAsi,
17
+ tryGitPrefix,
18
+ tryGitStatus,
19
+ } from "../helpers";
12
20
  import {
13
21
  buildExperimentState,
14
22
  computeConfidence,
@@ -445,22 +453,6 @@ async function tryReadHeadSha(cwd: string): Promise<string | null> {
445
453
  }
446
454
  }
447
455
 
448
- async function tryGitStatus(cwd: string): Promise<string> {
449
- try {
450
- return await git.status(cwd, { porcelainV1: true, untrackedFiles: "all", z: true });
451
- } catch {
452
- return "";
453
- }
454
- }
455
-
456
- async function tryGitPrefix(cwd: string): Promise<string> {
457
- try {
458
- return await git.show.prefix(cwd);
459
- } catch {
460
- return "";
461
- }
462
- }
463
-
464
456
  function buildLogText(
465
457
  state: ExperimentState,
466
458
  experiment: ExperimentResult,
@@ -18,6 +18,8 @@ import {
18
18
  killTree,
19
19
  parseAsiLines,
20
20
  parseMetricLines,
21
+ tryGitPrefix,
22
+ tryGitStatus,
21
23
  } from "../helpers";
22
24
  import { buildExperimentState } from "../state";
23
25
  import { openAutoresearchStorageIfExists } from "../storage";
@@ -265,23 +267,6 @@ export function createRunExperimentTool(
265
267
  },
266
268
  };
267
269
  }
268
-
269
- async function tryGitStatus(cwd: string): Promise<string> {
270
- try {
271
- return await git.status(cwd, { porcelainV1: true, untrackedFiles: "all", z: true });
272
- } catch {
273
- return "";
274
- }
275
- }
276
-
277
- async function tryGitPrefix(cwd: string): Promise<string> {
278
- try {
279
- return await git.show.prefix(cwd);
280
- } catch {
281
- return "";
282
- }
283
- }
284
-
285
270
  async function executeProcess(opts: {
286
271
  command: string[];
287
272
  cwd: string;
@@ -14,6 +14,13 @@ export interface SkillFrontmatter {
14
14
  description?: string;
15
15
  globs?: string[];
16
16
  alwaysApply?: boolean;
17
+ /**
18
+ * When `true`, the skill is loaded and accessible via `skill://<name>` (and
19
+ * `/skill:<name>` slash commands), but is omitted from the rendered system
20
+ * prompt's skill listing. Use for skills the user opts into explicitly
21
+ * rather than ones the model should auto-discover.
22
+ */
23
+ hide?: boolean;
17
24
  [key: string]: unknown;
18
25
  }
19
26
 
@@ -2,11 +2,11 @@
2
2
  * List available models with optional fuzzy search
3
3
  */
4
4
  import { type Api, getSupportedEfforts, type Model } from "@oh-my-pi/pi-ai";
5
+ import { fuzzyFilter } from "@oh-my-pi/pi-tui";
5
6
  import { formatNumber } from "@oh-my-pi/pi-utils";
6
7
  import type { ModelRegistry } from "../config/model-registry";
7
8
  import { discoverAndLoadExtensions, loadExtensions } from "../extensibility/extensions";
8
9
  import { EventBus } from "../utils/event-bus";
9
- import { fuzzyFilter } from "../utils/fuzzy";
10
10
 
11
11
  interface ProviderRow {
12
12
  provider: string;
@@ -5,10 +5,11 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
- import { type MinimizerOptions, Shell } from "@oh-my-pi/pi-natives";
8
+ import { Shell } from "@oh-my-pi/pi-natives";
9
9
  import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils";
10
10
  import chalk from "chalk";
11
- import { Settings, type ShellMinimizerSettings } from "../config/settings";
11
+ import { Settings } from "../config/settings";
12
+ import { buildMinimizerOptions } from "../exec/bash-executor";
12
13
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
13
14
 
14
15
  export interface ShellCommandArgs {
@@ -41,17 +42,6 @@ export function parseShellArgs(args: string[]): ShellCommandArgs | undefined {
41
42
  return result;
42
43
  }
43
44
 
44
- function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
45
- if (!group.enabled) return undefined;
46
- return {
47
- enabled: true,
48
- settingsPath: group.settingsPath || undefined,
49
- only: group.only.length > 0 ? group.only : undefined,
50
- except: group.except.length > 0 ? group.except : undefined,
51
- maxCaptureBytes: group.maxCaptureBytes,
52
- };
53
- }
54
-
55
45
  export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
56
46
  if (!process.stdin.isTTY) {
57
47
  process.stderr.write("Error: shell console requires an interactive TTY.\n");
@@ -91,7 +91,7 @@ function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "b
91
91
  return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
92
92
  }
93
93
 
94
- export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
94
+ export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
95
95
  return resolveUpdateMethod(ompPath, bunBinDir);
96
96
  }
97
97
  async function resolveUpdateTarget(): Promise<UpdateTarget> {
package/src/cli.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env bun
2
+ import { installH2Fetch } from "@oh-my-pi/pi-ai";
2
3
  import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils";
3
4
 
5
+ // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
6
+ // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
7
+ // can't toggle from JS, so we patch globalThis.fetch to pass
8
+ // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
9
+ // `HTTP2Unsupported`. See @oh-my-pi/pi-ai/utils/h2-fetch for details.
10
+ installH2Fetch();
11
+
4
12
  // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
5
13
  // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
6
14
  // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
@@ -12,40 +20,13 @@ procmgr.scrubProcessEnv();
12
20
  */
13
21
  import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
14
22
 
15
- function parseSemver(version: string): [number, number, number] {
16
- function toint(value: string): number {
17
- const int = Number.parseInt(value, 10);
18
- if (Number.isNaN(int) || !Number.isFinite(int)) return 0;
19
- return int;
20
- }
21
- const [majorRaw, minorRaw, patchRaw] = version.split(".").map(toint);
22
- return [majorRaw, minorRaw, patchRaw];
23
- }
24
-
25
- function isAtLeastBunVersion(minimum: string): boolean {
26
- const ver = parseSemver(Bun.version);
27
- const min = parseSemver(minimum);
28
- for (let i = 0; i < 3; i++) {
29
- if (ver[i] !== min[i]) {
30
- return ver[i] > min[i];
31
- }
32
- }
33
- return true;
34
- }
35
-
36
- if (typeof Bun.JSONL?.parseChunk !== "function" || !isAtLeastBunVersion(MIN_BUN_VERSION)) {
23
+ if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
37
24
  process.stderr.write(
38
- `error: Bun runtime must be >= ${MIN_BUN_VERSION} (found v${Bun.version}). Please update Bun: bun upgrade\n`,
25
+ `error: Bun runtime must be >= ${MIN_BUN_VERSION} (found v${Bun.version}). Please upgrade: bun upgrade\n`,
39
26
  );
40
27
  process.exit(1);
41
28
  }
42
29
 
43
- // Detect known Bun errata that cause TUI crashes (e.g. Bun.stringWidth mishandling OSC sequences).
44
- if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) {
45
- process.stderr.write(`error: Bun runtime errata detected (v${Bun.version}). Please update Bun: bun upgrade\n`);
46
- process.exit(1);
47
- }
48
-
49
30
  process.title = APP_NAME;
50
31
 
51
32
  const commands: CommandEntry[] = [
@@ -130,8 +130,15 @@ export function createProposeChangelogTool(
130
130
  state.changelogProposal = { entries: normalized };
131
131
  }
132
132
 
133
+ let text = response.valid ? "Changelog entries accepted." : "Changelog validation failed.";
134
+ if (response.errors.length > 0) {
135
+ text += `\n\nErrors:\n${response.errors.map(e => `- ${e}`).join("\n")}`;
136
+ }
137
+ if (response.warnings.length > 0) {
138
+ text += `\n\nWarnings:\n${response.warnings.map(w => `- ${w}`).join("\n")}`;
139
+ }
133
140
  return {
134
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
141
+ content: [{ type: "text", text }],
135
142
  details: response,
136
143
  };
137
144
  },
@@ -1,52 +1,16 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
- import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
2
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
6
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
7
6
  import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
8
- import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
7
+ import type { ConventionalAnalysis } from "../../commit/types";
9
8
  import { toReasoningEffort } from "../../thinking";
10
- import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
+ import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
11
10
 
12
- const ConventionalAnalysisTool = {
13
- name: "create_conventional_analysis",
14
- description: "Analyze a diff and return conventional commit classification.",
15
- parameters: Type.Object({
16
- type: Type.Union([
17
- Type.Literal("feat"),
18
- Type.Literal("fix"),
19
- Type.Literal("refactor"),
20
- Type.Literal("docs"),
21
- Type.Literal("test"),
22
- Type.Literal("chore"),
23
- Type.Literal("style"),
24
- Type.Literal("perf"),
25
- Type.Literal("build"),
26
- Type.Literal("ci"),
27
- Type.Literal("revert"),
28
- ]),
29
- scope: Type.Union([Type.String(), Type.Null()]),
30
- details: Type.Array(
31
- Type.Object({
32
- text: Type.String(),
33
- changelog_category: Type.Optional(
34
- Type.Union([
35
- Type.Literal("Added"),
36
- Type.Literal("Changed"),
37
- Type.Literal("Fixed"),
38
- Type.Literal("Deprecated"),
39
- Type.Literal("Removed"),
40
- Type.Literal("Security"),
41
- Type.Literal("Breaking Changes"),
42
- ]),
43
- ),
44
- user_visible: Type.Optional(Type.Boolean()),
45
- }),
46
- ),
47
- issue_refs: Type.Array(Type.String()),
48
- }),
49
- };
11
+ const ConventionalAnalysisTool = createConventionalAnalysisTool(
12
+ "Analyze a diff and return conventional commit classification.",
13
+ );
50
14
 
51
15
  export interface ConventionalAnalysisInput {
52
16
  model: Model<Api>;
@@ -96,27 +60,5 @@ export async function generateConventionalAnalysis({
96
60
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
97
61
  );
98
62
 
99
- return parseAnalysisFromResponse(response);
100
- }
101
-
102
- function parseAnalysisFromResponse(message: AssistantMessage): ConventionalAnalysis {
103
- const toolCall = extractToolCall(message, "create_conventional_analysis");
104
- if (toolCall) {
105
- const parsed = validateToolCall([ConventionalAnalysisTool], toolCall) as {
106
- type: ConventionalAnalysis["type"];
107
- scope: string | null;
108
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
109
- issue_refs: string[];
110
- };
111
- return normalizeAnalysis(parsed);
112
- }
113
-
114
- const text = extractTextContent(message);
115
- const parsed = parseJsonPayload(text) as {
116
- type: ConventionalAnalysis["type"];
117
- scope: string | null;
118
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
119
- issue_refs: string[];
120
- };
121
- return normalizeAnalysis(parsed);
63
+ return parseConventionalAnalysisResponse(response, ConventionalAnalysisTool);
122
64
  }
@@ -1,52 +1,14 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
- import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
2
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
6
5
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
7
6
  import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
8
- import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
7
+ import type { ConventionalAnalysis, FileObservation } from "../../commit/types";
9
8
  import { toReasoningEffort } from "../../thinking";
10
- import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
+ import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
11
10
 
12
- const ReduceTool = {
13
- name: "create_conventional_analysis",
14
- description: "Synthesize file observations into a conventional commit analysis.",
15
- parameters: Type.Object({
16
- type: Type.Union([
17
- Type.Literal("feat"),
18
- Type.Literal("fix"),
19
- Type.Literal("refactor"),
20
- Type.Literal("docs"),
21
- Type.Literal("test"),
22
- Type.Literal("chore"),
23
- Type.Literal("style"),
24
- Type.Literal("perf"),
25
- Type.Literal("build"),
26
- Type.Literal("ci"),
27
- Type.Literal("revert"),
28
- ]),
29
- scope: Type.Union([Type.String(), Type.Null()]),
30
- details: Type.Array(
31
- Type.Object({
32
- text: Type.String(),
33
- changelog_category: Type.Optional(
34
- Type.Union([
35
- Type.Literal("Added"),
36
- Type.Literal("Changed"),
37
- Type.Literal("Fixed"),
38
- Type.Literal("Deprecated"),
39
- Type.Literal("Removed"),
40
- Type.Literal("Security"),
41
- Type.Literal("Breaking Changes"),
42
- ]),
43
- ),
44
- user_visible: Type.Optional(Type.Boolean()),
45
- }),
46
- ),
47
- issue_refs: Type.Array(Type.String()),
48
- }),
49
- };
11
+ const ReduceTool = createConventionalAnalysisTool("Synthesize file observations into a conventional commit analysis.");
50
12
 
51
13
  export interface ReducePhaseInput {
52
14
  model: Model<Api>;
@@ -83,26 +45,5 @@ export async function runReducePhase({
83
45
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
84
46
  );
85
47
 
86
- return parseAnalysisResponse(response);
87
- }
88
-
89
- function parseAnalysisResponse(message: AssistantMessage): ConventionalAnalysis {
90
- const toolCall = extractToolCall(message, "create_conventional_analysis");
91
- if (toolCall) {
92
- const parsed = validateToolCall([ReduceTool], toolCall) as {
93
- type: ConventionalAnalysis["type"];
94
- scope: string | null;
95
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
96
- issue_refs: string[];
97
- };
98
- return normalizeAnalysis(parsed);
99
- }
100
- const text = extractTextContent(message);
101
- const parsed = parseJsonPayload(text) as {
102
- type: ConventionalAnalysis["type"];
103
- scope: string | null;
104
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
105
- issue_refs: string[];
106
- };
107
- return normalizeAnalysis(parsed);
48
+ return parseConventionalAnalysisResponse(response, ReduceTool);
108
49
  }
@@ -25,8 +25,8 @@ import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
25
25
 
26
26
  const SUMMARY_MAX_CHARS = 72;
27
27
  const RECENT_COMMITS_COUNT = 8;
28
- let _typesDescription: string | undefined;
29
- const TYPES_DESCRIPTION = (): string => (_typesDescription ??= prompt.render(typesDescriptionPrompt));
28
+ let typesDescription: string | undefined;
29
+ const TYPES_DESCRIPTION = (): string => (typesDescription ??= prompt.render(typesDescriptionPrompt));
30
30
 
31
31
  /**
32
32
  * Execute the omp commit pipeline for staged changes.
@@ -0,0 +1,89 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import { validateToolCall } from "@oh-my-pi/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import type { ChangelogCategory, ConventionalAnalysis } from "./types";
5
+ import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "./utils";
6
+
7
+ /**
8
+ * Shared TypeBox schema for the `create_conventional_analysis` tool used by
9
+ * both the single-pass analysis call and the map-reduce reduce phase. Schemas
10
+ * are identical across phases — only the surrounding tool `description`
11
+ * differs to reflect the input the phase is summarizing.
12
+ */
13
+ export const conventionalAnalysisParameters = Type.Object({
14
+ type: Type.Union([
15
+ Type.Literal("feat"),
16
+ Type.Literal("fix"),
17
+ Type.Literal("refactor"),
18
+ Type.Literal("docs"),
19
+ Type.Literal("test"),
20
+ Type.Literal("chore"),
21
+ Type.Literal("style"),
22
+ Type.Literal("perf"),
23
+ Type.Literal("build"),
24
+ Type.Literal("ci"),
25
+ Type.Literal("revert"),
26
+ ]),
27
+ scope: Type.Union([Type.String(), Type.Null()]),
28
+ details: Type.Array(
29
+ Type.Object({
30
+ text: Type.String(),
31
+ changelog_category: Type.Optional(
32
+ Type.Union([
33
+ Type.Literal("Added"),
34
+ Type.Literal("Changed"),
35
+ Type.Literal("Fixed"),
36
+ Type.Literal("Deprecated"),
37
+ Type.Literal("Removed"),
38
+ Type.Literal("Security"),
39
+ Type.Literal("Breaking Changes"),
40
+ ]),
41
+ ),
42
+ user_visible: Type.Optional(Type.Boolean()),
43
+ }),
44
+ ),
45
+ issue_refs: Type.Array(Type.String()),
46
+ });
47
+
48
+ export interface ConventionalAnalysisTool {
49
+ name: "create_conventional_analysis";
50
+ description: string;
51
+ parameters: typeof conventionalAnalysisParameters;
52
+ }
53
+
54
+ /**
55
+ * Build a `create_conventional_analysis` tool descriptor. Phase-specific
56
+ * `description` text is the only thing that varies between callers.
57
+ */
58
+ export function createConventionalAnalysisTool(description: string): ConventionalAnalysisTool {
59
+ return {
60
+ name: "create_conventional_analysis",
61
+ description,
62
+ parameters: conventionalAnalysisParameters,
63
+ };
64
+ }
65
+
66
+ interface ParsedConventionalAnalysis {
67
+ type: ConventionalAnalysis["type"];
68
+ scope: string | null;
69
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
70
+ issue_refs: string[];
71
+ }
72
+
73
+ /**
74
+ * Extract a {@link ConventionalAnalysis} from an assistant response, preferring
75
+ * a structured tool call and falling back to JSON embedded in text content.
76
+ */
77
+ export function parseConventionalAnalysisResponse(
78
+ message: AssistantMessage,
79
+ tool: ConventionalAnalysisTool,
80
+ ): ConventionalAnalysis {
81
+ const toolCall = extractToolCall(message, tool.name);
82
+ if (toolCall) {
83
+ const parsed = validateToolCall([tool], toolCall) as ParsedConventionalAnalysis;
84
+ return normalizeAnalysis(parsed);
85
+ }
86
+ const text = extractTextContent(message);
87
+ const parsed = parseJsonPayload(text) as ParsedConventionalAnalysis;
88
+ return normalizeAnalysis(parsed);
89
+ }