@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2

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 (165) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +10 -10
  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/commands/commit.ts +10 -0
  14. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  15. package/src/commit/analysis/conventional.ts +8 -66
  16. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  17. package/src/commit/pipeline.ts +2 -2
  18. package/src/commit/shared-llm.ts +89 -0
  19. package/src/config/config-file.ts +210 -0
  20. package/src/config/model-equivalence.ts +8 -11
  21. package/src/config/model-registry.ts +44 -3
  22. package/src/config/model-resolver.ts +1 -4
  23. package/src/config/settings-schema.ts +82 -1
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -219
  26. package/src/discovery/claude-plugins.ts +19 -7
  27. package/src/edit/renderer.ts +7 -1
  28. package/src/eval/js/executor.ts +3 -0
  29. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  30. package/src/eval/py/executor.ts +5 -0
  31. package/src/eval/py/runner.py +42 -11
  32. package/src/eval/py/runtime.ts +1 -0
  33. package/src/exa/factory.ts +2 -2
  34. package/src/exa/mcp-client.ts +74 -1
  35. package/src/exec/bash-executor.ts +5 -1
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +0 -11
  38. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  39. package/src/extensibility/extensions/runner.ts +1 -1
  40. package/src/extensibility/extensions/types.ts +89 -223
  41. package/src/extensibility/hooks/types.ts +89 -314
  42. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  43. package/src/extensibility/shared-events.ts +343 -0
  44. package/src/extensibility/skills.ts +9 -0
  45. package/src/goals/index.ts +3 -0
  46. package/src/goals/runtime.ts +500 -0
  47. package/src/goals/state.ts +37 -0
  48. package/src/goals/tools/goal-tool.ts +237 -0
  49. package/src/hashline/anchors.ts +2 -2
  50. package/src/hashline/input.ts +2 -1
  51. package/src/hashline/parser.ts +27 -3
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/agent-protocol.ts +1 -20
  54. package/src/internal-urls/artifact-protocol.ts +1 -19
  55. package/src/internal-urls/docs-index.generated.ts +11 -12
  56. package/src/internal-urls/registry-helpers.ts +25 -0
  57. package/src/internal-urls/router.ts +8 -0
  58. package/src/internal-urls/types.ts +21 -0
  59. package/src/lsp/config.ts +15 -6
  60. package/src/lsp/defaults.json +6 -2
  61. package/src/main.ts +11 -2
  62. package/src/mcp/oauth-flow.ts +20 -0
  63. package/src/modes/acp/acp-agent.ts +327 -95
  64. package/src/modes/components/assistant-message.ts +14 -8
  65. package/src/modes/components/bash-execution.ts +24 -63
  66. package/src/modes/components/custom-message.ts +14 -40
  67. package/src/modes/components/eval-execution.ts +27 -57
  68. package/src/modes/components/execution-shared.ts +102 -0
  69. package/src/modes/components/hook-message.ts +17 -49
  70. package/src/modes/components/mcp-add-wizard.ts +26 -5
  71. package/src/modes/components/message-frame.ts +88 -0
  72. package/src/modes/components/model-selector.ts +1 -1
  73. package/src/modes/components/session-observer-overlay.ts +6 -2
  74. package/src/modes/components/session-selector.ts +1 -1
  75. package/src/modes/components/status-line/segments.ts +93 -8
  76. package/src/modes/components/status-line/types.ts +4 -0
  77. package/src/modes/components/status-line.ts +28 -10
  78. package/src/modes/components/tool-execution.ts +7 -8
  79. package/src/modes/controllers/command-controller-shared.ts +108 -0
  80. package/src/modes/controllers/command-controller.ts +13 -4
  81. package/src/modes/controllers/event-controller.ts +36 -7
  82. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  83. package/src/modes/controllers/input-controller.ts +13 -0
  84. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  85. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  86. package/src/modes/interactive-mode.ts +624 -52
  87. package/src/modes/print-mode.ts +16 -86
  88. package/src/modes/rpc/host-uris.ts +235 -0
  89. package/src/modes/rpc/rpc-mode.ts +41 -88
  90. package/src/modes/rpc/rpc-types.ts +57 -0
  91. package/src/modes/runtime-init.ts +116 -0
  92. package/src/modes/theme/defaults/dark-poimandres.json +3 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +3 -0
  94. package/src/modes/theme/theme.ts +24 -6
  95. package/src/modes/types.ts +14 -3
  96. package/src/modes/utils/context-usage.ts +13 -13
  97. package/src/modes/utils/ui-helpers.ts +10 -3
  98. package/src/plan-mode/approved-plan.ts +35 -1
  99. package/src/prompts/goals/goal-budget-limit.md +16 -0
  100. package/src/prompts/goals/goal-continuation.md +28 -0
  101. package/src/prompts/goals/goal-mode-active.md +23 -0
  102. package/src/prompts/system/plan-mode-active.md +5 -5
  103. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  104. package/src/prompts/tools/bash.md +6 -0
  105. package/src/prompts/tools/github.md +4 -4
  106. package/src/prompts/tools/goal.md +13 -0
  107. package/src/prompts/tools/hashline.md +101 -117
  108. package/src/prompts/tools/read.md +55 -36
  109. package/src/prompts/tools/resolve.md +6 -5
  110. package/src/sdk.ts +12 -5
  111. package/src/session/agent-session.ts +428 -106
  112. package/src/session/blob-store.ts +36 -3
  113. package/src/session/messages.ts +67 -2
  114. package/src/session/session-manager.ts +131 -12
  115. package/src/session/session-storage.ts +33 -15
  116. package/src/session/streaming-output.ts +309 -13
  117. package/src/slash-commands/builtin-registry.ts +18 -0
  118. package/src/ssh/ssh-executor.ts +5 -0
  119. package/src/system-prompt.ts +4 -2
  120. package/src/task/discovery.ts +5 -2
  121. package/src/task/executor.ts +19 -8
  122. package/src/task/index.ts +3 -0
  123. package/src/task/render.ts +21 -15
  124. package/src/task/types.ts +4 -0
  125. package/src/tools/ast-edit.ts +21 -120
  126. package/src/tools/ast-grep.ts +21 -119
  127. package/src/tools/bash-command-fixup.ts +47 -0
  128. package/src/tools/bash-interactive.ts +9 -1
  129. package/src/tools/bash.ts +66 -19
  130. package/src/tools/browser/attach.ts +3 -3
  131. package/src/tools/browser/launch.ts +81 -18
  132. package/src/tools/browser/registry.ts +1 -5
  133. package/src/tools/browser/render.ts +2 -2
  134. package/src/tools/browser/tab-supervisor.ts +51 -14
  135. package/src/tools/conflict-detect.ts +15 -4
  136. package/src/tools/eval.ts +12 -2
  137. package/src/tools/find.ts +20 -38
  138. package/src/tools/gh.ts +44 -10
  139. package/src/tools/index.ts +22 -11
  140. package/src/tools/inspect-image.ts +3 -10
  141. package/src/tools/job.ts +16 -7
  142. package/src/tools/output-meta.ts +202 -37
  143. package/src/tools/path-utils.ts +125 -2
  144. package/src/tools/read.ts +548 -237
  145. package/src/tools/render-utils.ts +92 -0
  146. package/src/tools/renderers.ts +2 -0
  147. package/src/tools/resolve.ts +72 -44
  148. package/src/tools/search.ts +120 -186
  149. package/src/tools/ssh.ts +3 -2
  150. package/src/tools/write.ts +64 -9
  151. package/src/utils/file-mentions.ts +1 -1
  152. package/src/utils/image-loading.ts +7 -3
  153. package/src/utils/image-resize.ts +32 -43
  154. package/src/vim/parser.ts +0 -17
  155. package/src/vim/render.ts +1 -1
  156. package/src/vim/types.ts +1 -1
  157. package/src/web/search/providers/anthropic.ts +5 -0
  158. package/src/web/search/providers/exa.ts +3 -0
  159. package/src/web/search/providers/gemini.ts +40 -95
  160. package/src/web/search/providers/jina.ts +5 -2
  161. package/src/web/search/providers/zai.ts +5 -2
  162. package/src/prompts/tools/exit-plan-mode.md +0 -6
  163. package/src/tools/exit-plan-mode.ts +0 -97
  164. package/src/utils/fuzzy.ts +0 -108
  165. package/src/utils/image-convert.ts +0 -27
package/CHANGELOG.md CHANGED
@@ -2,6 +2,85 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.0.2] - 2026-05-15
6
+
7
+ ### Added
8
+
9
+ - Added the `set_host_uri_schemes` RPC command so hosts can register and replace writable/read-only internal URI schemes with scheme metadata (`writable`, `immutable`) at runtime
10
+ - Enabled the `write` tool to dispatch `write(url, content)` to registered internal URL handlers, allowing edits to non-filesystem resources via host-managed URI schemes
11
+ - Added host-owned internal URI read/write over RPC, including abort support, so URI operations are resolved by the host transport for `read` and `write` requests
12
+ - Added handling of host URI request results in RPC mode so host services can stream completion frames for internal URI operations
13
+ - Added scratch-directory awareness to the status-line `path` segment. When the project directory is inside an OS-level scratch root (the platform `os.tmpdir()`, `/tmp` and `/var/tmp` plus their macOS `/private/...` aliases, `~/tmp`, or — on Windows — `%TEMP%` / `%TMP%` / `%SystemRoot%\Temp`), the segment now (1) renders the new `icon.scratchFolder` symbol instead of `icon.folder`, and (2) strips the scratch root from the displayed path so only the trailing folder (and any subpath beneath it) is shown — mirroring how `/work` and `~/Projects` are already abbreviated. Both behaviors honor the existing `stripWorkPrefix` option. Icon defaults: 🗑 (emoji), `` (nf-fa-trash) for Nerd Font, `[T]` for ASCII, `◌` in the poimandres themes; themes can override `icon.scratchFolder` independently of `icon.folder`.
14
+
15
+ ### Changed
16
+
17
+ - Changed the `github` tool's search ops (`search_issues`, `search_prs`, `search_code`, `search_commits`) to default the `repo` scope to the current checkout's `owner/repo` when `repo` is omitted. The auto-scope is skipped when the query already carries an explicit `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` cannot resolve a github remote (in which case the search proceeds across all of GitHub as before). `search_repos` is unchanged — repository-scoping there must live in the query.
18
+
19
+ - Changed bash command preprocessing to strip trailing `| head` and `| tail` pipelines (including `|&`) from each top-level segment in command chains separated by `;`, `&&`, `||`, or `&`
20
+ - Changed bash fixup notices to state that stderr is already merged into stdout and to reflect that fixes were applied for multiple stripped segments when several transforms fire
21
+ - Changed shell-minimizer per-line truncation marker from a bare `…` to `…[+N]`, where `N` is the count of dropped Unicode scalars. The bracketed tally disambiguates minimizer-driven cuts from genuine `…` characters in the source (paths, JSON, stack traces, etc.) and gives the agent an exact count so it can decide whether the missing tail is recoverable inline or warrants reading the `[raw output: artifact://<id>]` footer the bash wrapper already emits when the minimizer rewrites output. Affects pipeline Stage 5 (`truncate_lines_at` in `defs/*.toml`) and the internal callers in `filters/git.rs`, `filters/listing.rs`, and `filters/lint.rs`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046))
22
+ - Changed bash command preprocessing to use the real `brush-parser` AST via `pi-natives` `applyBashFixups` instead of a hand-rolled top-level mask scanner. The previous regex/character-walking implementation reimplemented quote/heredoc/`$(...)` tracking with conservative bail-outs (notably refusing to fixup commands containing here-strings); the AST-driven version inherits the full shell parser, so semantics-preserving rewrites like stripping `| head -5` off `cat <<<'content' | head -5` now succeed instead of being skipped. No public API change — `applyBashFixups(command)` returns the same `{ command, stripped }` shape.
23
+
24
+ ### Fixed
25
+
26
+ - Fixed bash command fixups to remove a redundant standalone trailing `2>&1` redirect when no other pipe or redirection remains
27
+ - Fixed command-fixup notices to list all stripped segments instead of reporting only one
28
+ - Fixed summarized `read` output stalling agents on elided regions by appending an explicit footer like `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`. The footer fires whenever the structural summarizer elided at least one span, so the model gets a concrete recovery selector instead of having to guess from a bare `...` / `{ .. }` marker. Surfaces `elidedLines` on `ReadToolDetails.summary` alongside the existing `elidedSpans`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046))
29
+ - Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing.
30
+ - Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare.
31
+ - Fixed `omp commit` hanging after a successful commit instead of returning to the shell. The command now mirrors the `runPrintMode` exit pattern and calls `postmortem.quit(0)` once the pipeline resolves so lingering HTTP/2 keep-alive sockets, the Settings autosave timer, and other AgentSession background handles don't keep the event loop pinned. ([#1041](https://github.com/can1357/oh-my-pi/issues/1041))
32
+ - Fixed hashline payload parsing to silently treat truly-blank lines as empty `~`-prefixed payload lines when more payload follows in the same run. The previous behavior broke at the blank ("payload line has no preceding +, <, or = operation.") even though the intent is obvious — the only ambiguity is between in-payload blanks and end-of-section blanks, and a one-line lookahead resolves it: blanks that precede a non-payload op still end the run cleanly as section separators. Recovers the common case of forgetting the leading separator on a blank inserted line without changing how trailing blanks between ops behave.
33
+ - Rewrote the hashline edit prompt examples to use an ASCII-only `TITLE = "Mr"` → `"Mrs"` / `"Dr"` motif instead of the previous `" • "` and `"·"` separators. Some agents had been copying the middle-dot literal characters into real edits as if they were format scaffolding (e.g. emitting payload lines like `~ ·`), since the demo inserts were near-twins of the existing string. The new example keeps every original op shape (single-line replace, multiline replace, insert AFTER/BEFORE, append, delete, blank, plus both anti-patterns) but uses content that is obviously domain-specific and clearly distinct from any payload separator. Pure prompt change; no parser, schema, or runtime behavior is affected.
34
+ - Fixed startup fallback-chain validation to recognize cached runtime-discovered standard provider models, including Ollama Cloud models listed by `--list-models`, so `retry.fallbackChains` no longer warns that valid `ollama-cloud/<model>` selectors are unknown. ([#1052](https://github.com/can1357/oh-my-pi/issues/1052))
35
+ - Fixed `discoverAgents()` ignoring `disabledProviders` for the `claude-plugins` provider. Plugin roots from `~/.claude/plugins/` were scanned unconditionally, so agents from Claude Code marketplace plugins continued to appear in `/agents` and the Agent Control Center even when `disabledProviders: [claude-plugins]` was set. The discovery path now checks `isProviderEnabled("claude-plugins")` before calling `listClaudePluginRoots()`, matching how every other capability respects the disabled-providers set. ([#1075](https://github.com/can1357/oh-my-pi/issues/1075))
36
+
37
+ ### Fixed
38
+
39
+ - Fixed `$env:VAR` PowerShell variables being mangled on Windows when commands invoked PowerShell as a subprocess (e.g. `powershell -Command "Write-Host $env:SystemRoot"`). Brush-core applied POSIX parameter expansion to `$env` before spawning the child, leaving a dangling `:NAME`. The fix lives in `pi-shell` at env-var application time: every brush session now defines `env=$env` as an internal shell variable so `$env:NAME` expands to the literal `$env:NAME` token that PowerShell expects. The fallback is not exported, only influences brush's own expansion, and is shadowed by any user assignment to `env` (e.g. `env=prod; echo "$env:8080"` still prints `prod:8080`), so the POSIX bash contract is preserved. ([#1079](https://github.com/can1357/oh-my-pi/issues/1079))
40
+
41
+
42
+ ## [15.0.1] - 2026-05-14
43
+ ### Breaking Changes
44
+
45
+ - Removed the dedicated `exit_plan_mode` tool and its prompt, requiring plan-mode completion to use the existing `resolve` tool path instead
46
+
47
+ ### Added
48
+
49
+ - Added optional `extra` metadata object to the `resolve` tool so callers can pass context-specific payloads, including plan approval titles
50
+ - 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.
51
+ - 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`.
52
+ - 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.
53
+ - 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**.
54
+ - 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.
55
+ - Added ACP elicitation bridge so skills/extensions calling `select`, `confirm`, or `input` on the extension UI context now produce real `unstable_createElicitation` form requests to the ACP client (rather than always resolving to `undefined` / `false`). The `acpExtensionUiContext` constant is promoted to `createAcpExtensionUiContext(connection, getSessionId, clientCapabilities)` — invoked once per session inside `#configureExtensions`, with `getSessionId: () => string` so the live `record.session.sessionId` is read on every elicitation (the underlying id mutates when an extension command calls `ctx.newSession` / `ctx.switchSession`). Each method maps to a single-property `value` schema: `select` → `{type: "string", enum}`, `confirm` → `{type: "boolean"}` (joined `title` + `message` when the trimmed message is non-empty; otherwise just `title`), `input` → `{type: "string", description: placeholder?}` (ACP has no `placeholder` field on `StringPropertySchema`; empty / whitespace-only placeholders are treated as absent). `accept` responses narrow the returned `ElicitationContentValue` back to the method's declared type with a runtime `typeof` guard; `decline` / `cancel` / transport failures fall back to the prior stub return values. `dialogOptions.signal` is honored: an already-aborted signal short-circuits before any SDK round-trip, and an abort mid-flight races against the elicitation so the caller's promise resolves to the stub fallback (the ACP request itself keeps running on the client side — the SDK exposes no form-mode cancel surface; `unstable_completeElicitation` is URL-mode only — matching the in-flight pattern used by `requestRpcEditor`). `dialogOptions.timeout` is honored on parity with `RpcExtensionUIContext`: when the timer fires before the client responds, `onTimeout` is invoked and the caller resolves to the stub fallback. A throwing `onTimeout` is caught and logged (`logger.warn`) so the elicitation promise still settles. Late SDK rejections that arrive after abort/timeout are dropped silently to keep operator logs clean; transport failures still emit `logger.warn` with `{ sessionId, method, error }`. Calls are skipped when the client did not advertise `clientCapabilities.elicitation.form` during `initialize`, so non-elicitation clients are unaffected. `createAcpExtensionUiContext` is exported for tests.
56
+
57
+ ### Changed
58
+
59
+ - Changed plan-mode completion to use `resolve { action: "apply", reason, extra: { title } }` to request plan approval rather than calling `exit_plan_mode`
60
+ - Changed resolve pending-action previews to trim and truncate long `reason` text for cleaner status-line rendering
61
+ - Raised the image downscaling default JPEG quality from 75 to 80 in `resizeImage` output generation
62
+ - 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
63
+ - 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+).
64
+ - 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**.
65
+ - 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`
66
+ - 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
67
+ - Changed search truncation metadata/renderer output from match/result-based limits to file-based limits (`fileLimitReached`, `perFileLimitReached`) and updated truncation labels accordingly
68
+ - 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.
69
+
70
+ ### Fixed
71
+
72
+ - 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
73
+ - Fixed `--help` startup to avoid a config/model-registry load cycle so the root CLI help command now exits successfully in a clean environment
74
+ - 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.
75
+ - 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.
76
+ - 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.
77
+ - 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.
78
+ - 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.
79
+ - 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.
80
+ - 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.
81
+ - 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`.
82
+ - 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)).
83
+
5
84
  ## [15.0.0] - 2026-05-13
6
85
  ### Breaking Changes
7
86
 
@@ -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,9 +1,9 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.0.0",
4
+ "version": "15.0.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
- "homepage": "https://github.com/can1357/oh-my-pi",
6
+ "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
8
8
  "contributors": [
9
9
  "Mario Zechner"
@@ -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.2",
51
+ "@oh-my-pi/pi-agent-core": "15.0.2",
52
+ "@oh-my-pi/pi-ai": "15.0.2",
53
+ "@oh-my-pi/pi-natives": "15.0.2",
54
+ "@oh-my-pi/pi-tui": "15.0.2",
55
+ "@oh-my-pi/pi-utils": "15.0.2",
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[] = [
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Generate and optionally push a commit with changelog updates.
3
3
  */
4
+ import { postmortem } from "@oh-my-pi/pi-utils";
4
5
  import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
6
  import { runCommitCommand } from "../commit";
6
7
  import type { CommitCommandArgs } from "../commit/types";
@@ -31,6 +32,15 @@ export default class Commit extends Command {
31
32
  };
32
33
 
33
34
  await initTheme();
35
+ // The agentic commit flow opens HTTP/2 keep-alive sockets to the model
36
+ // provider (via `installH2Fetch`) and spins up an AgentSession with
37
+ // background async-job + extension machinery. `session.dispose()` releases
38
+ // what it can, but Bun's fetch keeps idle connections warm and a few
39
+ // timers (Settings autosave, OAuth refresh) stay armed long enough to
40
+ // pin the event loop after the commit is already written. Mirror the
41
+ // `runPrintMode` exit pattern from `main.ts` so the CLI returns to the
42
+ // shell instead of stranding the user on Ctrl+C (issue #1041).
34
43
  await runCommitCommand(cmd);
44
+ await postmortem.quit(0);
35
45
  }
36
46
  }
@@ -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.