MusePi

Porting From pi-mono: A Practical Merge Guide

This guide is a repeatable checklist for porting changes from pi-mono into this repo. Use it for any merge: single file, feature branch, or full release sync.

Last Sync Point (historical upstream marker)

Commit: b21b42d032919de2f2e6920a76fa9a37c3920c0a Date: 2026-03-22

Update this section after each sync; do not reuse the previous range. This commit is an upstream pi-mono marker and may not exist in this repo’s local object database.

When starting a new sync, generate patches from this commit forward in a pi-mono checkout or remote that contains the commit:

git format-patch b21b42d032919de2f2e6920a76fa9a37c3920c0a..HEAD --stdout > changes.patch

0) Define the scope

1) Bring code over safely

2) Match import extension conventions

Most runtime TypeScript sources omit .js in internal imports, but several current entrypoints and tool modules keep .js for ESM/runtime compatibility. Follow the surrounding file and package export style; do not blanket-strip or blanket-add extensions.

3) Replace import scopes

Upstream uses different package scopes. Replace them consistently.

4) Use Bun APIs where they improve on Node

We run on Bun, but the current source intentionally mixes Bun APIs with small Node standard-library APIs. Replace Node APIs only when Bun provides a clearer, safer, or simpler implementation; do not mechanically rewrite every Node import.

Prefer replacing when porting new code:

DO NOT replace (these work fine in Bun):

Import style: Use the node: prefix for Node standard-library imports. Namespace imports are common, but named imports are acceptable where the surrounding code already uses them.

Additional Bun conventions:

Wrong:

// BROKEN: env vars may be undefined, "~" is not expanded
const home = Bun.env.HOME || "~";
const tmp = Bun.env.TMPDIR || "/tmp";

Correct:

import * as os from "node:os";
import * as fs from "node:fs";
import * as path from "node:path";

const configDir = path.join(os.homedir(), ".config", "myapp");
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "myapp-"));

5) Prefer Bun embeds (no copying)

Do not add new runtime asset copy steps. Keep assets in repo and prefer Bun embeds/imports; preserve existing explicit generation workflows such as packages/coding-agent/src/export/html/tool-views.generated.js (built from desktop-web sources via bun run gen:tool-views).

6) Port package.json carefully

Treat package.json as a contract. Merge intentionally.

7) Align code style and tooling

8) Remove old compatibility layers

Unless requested, remove upstream compatibility shims.

9) Update docs and references

10) Validate the port

Run the standard checks after changes:

If the repo already has failing checks unrelated to your changes, call that out. Tests use Bun’s runner (not Vitest), but only run bun test when explicitly requested.

11) Protect improved features (regression trap list)

If you already improved behavior locally, treat those as non‑negotiable. Before porting, write down the improvements and add explicit checks so they don’t get lost in the merge.

12) Detect and handle reworked code

Before porting a file, check if upstream significantly refactored it:

# Compare the file you're about to port against what you have locally
git diff HEAD upstream/main -- path/to/file.ts

If the diff shows the file was reworked (not just patched):

Then you must read the new implementation thoroughly before porting. Blind merging of reworked code loses functionality because:

Note: interactive mode was recently split into controllers/utils/types. When backporting related changes, port updates into the individual files we created and ensure interactive-mode.ts wiring stays in sync.

  1. Defaults change silently - A new variable defaultFoo = [a, b] may replace an old getAllFoo() that returned [a, b, c, d, e].

  2. API options get dropped - When systems merge (e.g., hooks + customToolsextensions), old options may not wire through to the new implementation.

  3. Code paths go stale - A renamed concept (e.g., hookMessagecustom) needs updates in every switch statement, type guard, and handler—not just the definition.

  4. Context/capabilities shrink - Old APIs may have exposed { logger, typebox, pi } that new APIs forgot to include.

Semantic porting process

When upstream reworked a module:

  1. Read the old implementation - Understand what it did, what options it accepted, what it exposed.

  2. Read the new implementation - Understand the new abstractions and how they map to old behavior.

  3. Verify feature parity - For each capability in the old code, confirm the new code preserves it or explicitly removes it.

  4. Grep for stragglers - Search for old names/concepts that may have been missed in switch statements, handlers, UI components.

  5. Test the boundaries - CLI flags, SDK options, event handlers, default values—these are where regressions hide.

Quick checks

# Find all uses of an old concept that may need updating
rg "oldConceptName" --type ts

# Compare default values between versions
git show upstream/main:path/to/file.ts | rg "default|DEFAULT"

# Check if all enum/union values have handlers
rg "case \"" path/to/file.ts

13) Quick audit checklist

Use this as a final pass before you finish:

14) Commit message format

When committing a backport, follow the repo format <type>(scope): <past-tense description> and keep the commit range in the title.

fix(coding-agent): backported pi-mono changes (<from>..<to>)

packages/<package>:
- <type>: <description>
- <type>: <description> (#<issue> by @<contributor>)

packages/<other-package>:
- <type>: <description>

Example:

fix(coding-agent): backported pi-mono changes (9f3eef65f..52532c7c0)

packages/ai:
- fix: handle "sensitive" stop reason from Anthropic API
- fix: normalize tool call IDs with special characters for Responses API
- fix: add overflow detection for Bedrock, MiniMax, Kimi providers
- fix: 429 status is rate limiting, not context overflow

packages/tui:
- fix: refactored autocomplete state tracking
- fix: file autocomplete should not trigger on empty text
- fix: configurable autocomplete max visible items
- fix: improved table column width calculation with word-aware wrapping

packages/coding-agent:
- fix: preserve external config.yml edits on save (#1046 by @nicobailonMD)
- fix: resolve macOS NFD and curly quote variants in file paths

Rules:

15) Intentional Divergences

Our fork has architectural decisions that differ from upstream. Do not port these upstream patterns:

UI Architecture

Upstream Our Fork Reason
FooterDataProvider class StatusLineComponent Simpler, integrated status line
ctx.ui.setHeader() / ctx.ui.setFooter() No-op stubs in current extension contexts Not currently wired to replace the TUI status/header UI
ctx.ui.setEditorComponent() Wired in interactive mode; no-op stubs in ACP/RPC/headless contexts Custom editor replacement works in the interactive TUI; non-TUI runtimes keep stubs
ctx.ui.addAutocompleteProvider() Wired in interactive mode; no-op stubs in ACP/RPC/headless contexts Factory wrapping matches upstream; musepi’s editor has no custom triggerCharacters, so wrapped providers surface at the built-in trigger points
InteractiveModeOptions options object Positional constructor args (options type still exported) Keep constructor signature; update the type when upstream adds fields

Component Naming

Upstream Our Fork
extension-input.ts hook-input.ts
extension-selector.ts hook-selector.ts
ExtensionInputComponent HookInputComponent
ExtensionSelectorComponent HookSelectorComponent

API Naming

Upstream Our Fork Notes
sessionManager.appendSessionInfo(name) sessionManager.setSessionName(name) We use sessionName throughout
sessionManager.getSessionName() sessionManager.getSessionName() Same (we unified to match upstream’s RPC)
agent.sessionName / setSessionName() agent.sessionName / setSessionName() Same

File Consolidation

Upstream Our Fork Reason
clipboard.ts + clipboard-image.ts (tool files) src/utils/clipboard.ts backed by @musepi/pi-natives Native implementation with a small TS wrapper

Test Framework

Upstream Our Fork
vitest with vi.mock() bun:test with vi from bun
node:test assertions expect() matchers

Tool Architecture

Upstream Our Fork Notes
createTool(cwd: string, options?) createTools(session: ToolSession) via BUILTIN_TOOLS registry Tool factories accept ToolSession and can return null
Per-tool *Operations interfaces Only current per-tool override interfaces remain (for example FindOperations) Used for SSH/remote overrides where present
Node.js fs/promises everywhere Bun file APIs for simple file writes/reads, node:fs/promises for dirs, selected sync node:fs where needed Prefer Bun APIs when they simplify

Auth Storage

Upstream Our Fork Notes
proper-lockfile + auth.json agent.db (bun:sqlite) Credentials stored exclusively in agent.db
Single credential per provider Multi-credential with round-robin selection Session affinity and backoff logic preserved

Extensions

Upstream Our Fork
jiti for TypeScript loading Native Bun import()
pkg.pi manifest field pkg.musepi preferred; fallback to pkg.pi remains
StringEnum from pi-ai Type.Enum from pi.typebox, or pi.arktype.enumerated(...); pi-ai no longer exports StringEnum
formatSize from pi-coding-agent formatBytes from @musepi/pi-utils
Upstream resource/package/settings managers as the native architecture Capability-based discovery (loadCapability(...)), the Settings singleton, and EventBus; legacy extension imports of DefaultResourceLoader, DefaultPackageManager, and SettingsManager are compatibility shims in legacy-pi-coding-agent-shim.ts, not the native implementation

Skip These Upstream Features

When porting, skip these files/features entirely:

Features We Added (Preserve These)

These exist in our fork but not upstream. Never overwrite: