@alexleekt/pi-patch-manager

Safe inspection and management of Git patches for Pi packages.

Packages

Package details

extensionskill

Install @alexleekt/pi-patch-manager from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@alexleekt/pi-patch-manager
Package
@alexleekt/pi-patch-manager
Version
0.4.0
Published
Sep 6, 2026
Downloads
300/mo · 173/wk
Author
alexleekt
License
MIT
Types
extension, skill
Size
85.8 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ],
  "skills": [
    "./skills"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@alexleekt/pi-patch-manager

Install with pi install npm:@alexleekt/pi-patch-manager. The manager reads patches from ~/.pi/agent/patches/<id>/ and supports npm-style packages found under the agent, agent extensions, and current project node_modules roots.

Each directory contains a strict manifest.json. Required fields: id, package, baseVersion, baseHash (sha256: plus 64 lowercase hex characters), patch (a relative path under patch/), intent, reason, and enabled. Optional fields capture the plan's full context: target (file, symbol, changefile is validated as a safe relative path), validation (safe relative path to a checks script), upstream (status kebab-case, url http(s)), and createdWith (provider, model provenance). Unknown fields and unsafe paths are rejected.

/patch list, /patch status, and /patch explain <id> inspect the registry. /patch disable <id> atomically changes only enabled to false, writing through a random temp directory inside the patch directory to defeat symlink pre-planting. /patch apply [id] applies one or all enabled patches: the patch bytes are snapshotted to a private temp file, guarded by a dry-run (git apply --check), applied all-or-nothing (never --reject or --3way), then reverse-verified. Git refuses any patch path escaping the package root (no --unsafe-paths; git subprocesses run at the package root with inherited GIT_* environment stripped). Drifted packages are refused — a version change normally requires a rebase. If the old patch still reverse-applies after the version change, status is possibly-unneeded: the patch may no longer belong in the new package and /patch status offers Disable rather than Rebase. A failed validation script yields a distinct validation-failed outcome: the patch is present but unhealthy. Validation scripts are user-authored code that run with full user privileges; their output is only ever displayed, never fed to an agent. Apply-all is atomic per patch, not across the batch, and there is no cross-process lock. The patch_status agent tool is read-only.

Status is computed from package identity, deterministic SHA-256 hashes over a sorted file walk (symlinks rejected at every level, including the package root), and reverse git apply --check classification. A different package version is drift; it is classified as possibly-unneeded when the old patch still reverse-applies, or drifted when it does not. applied proves the patch itself is present, not that the whole tree matches a recorded post-patch state. /patch status groups entries as NEEDS REBASE, POSSIBLY NO LONGER NEEDED, FAILED OR MISSING, READY TO APPLY, HEALTHY, and DISABLED. Its action menu offers Rebase for drifted patches and Disable for possibly-unneeded patches. Patch files are resolved through realpath and must stay inside their patch directory. Git subprocesses run at the package root with --no-index and all inherited GIT_* environment stripped, so path resolution never depends on where pi was started. No reject or three-way modes are used.

Dependencies

Zero runtime dependencies. Peer dependencies declare the Pi integration surface: @earendil-works/pi-coding-agent, @earendil-works/pi-ai (used only by the optional /patch rebase LLM flow), and typebox.

Crafting patches

The package ships a patch-creator skill (available in pi as /skill:patch-creator after install). It walks through snapshotting the pristine package, editing it, generating a registry-compatible diff, and writing manifest.json. Its create-patch.mjs helper imports this package's own hashing so generated hashes always match the registry's algorithm, and self-verifies the generated patch (forward-apply to the pristine snapshot, reverse-apply to the edited snapshot) before reporting success. Crafted patches report applied in /patch status once the edit is present and identity matches. /patch apply applies them; /patch rebase rebuilds a drifted patch.

Rebase

/patch rebase <id> handles drift: the old and new package versions and hashes, the original patch, the manifest's intent, reason, target, and validation are collected into a context, together with bounded source excerpts from the newly installed package around each hunk, and the first available model is asked for a JSON candidate patch. Source and patch text are sent to the model as untrusted data. Oversized input is rejected before any of it is read: the original patch must fit 64 KB, prompt fields and excerpts are capped individually, and the total prompt must fit a 100 KB byte budget before the model is called. The candidate is then validated without touching the installed package: shape checks (JSON {patch} only, size caps, diff --git present, no code fences), safe-path checks on every ---/+++ target, a dry-run forward apply plus reverse verification inside a private temp copy of the package (with the old patch first reverse-applied in the copy, and control patches kept outside the copied tree), and a checks.sh run there. The candidate diff and validation result are shown, and an explicit confirmation dialog gates every mutation.

On approval, the patch registry is re-read and compared (manifest bytes, patch bytes, resolved patch path) before any history write or package mutation; the package hash and identity are re-verified as well. The old patch and manifest are archived under history/<timestamp>-<hash>/ preserving the manifest's own patch path; the old patch is reverse-applied if it was present; the candidate is applied to the live package; validation and a full-tree hash check run again; and only then does the rebase commit, by writing the candidate to a new immutable patch file and atomically renaming the manifest as the commit point — before the switch, readers see the old manifest and old patch; after it, the new manifest and completed candidate. The manifest records the new pristine base version and hash (with the old patch removed) plus rebasedFrom provenance (model, date, previousBase). A validation failure, package identity change, or patch-registry change mid-flight routes through rollback: the candidate is reversed, the old patch restored, and the restoration verified against the recorded pre-rebase tree hash — with primary and rollback errors both reported if restoration fails. The model is always the first available; there is no model choice yet, and an LLM-generated patch is never applied without explicit user approval.