Fecha: 2026-07-12
Una extensión de pi es un módulo TypeScript que agrega comandos, tools o manejadores de eventos al agente. Esta guía explica cómo escribir una extensión de este repo y probarla sin romper la sesión activa. Usala cuando quieras crear una extensión nueva, modificar una existente o decidir entre un comando, un tool o un event handler.
El loop recomendado usa un checkout sibling de pi-cante. Desde este repo:
# feedback primario
node --test extensions/pandi-<ext>/tests/integration/<algo>.test.mjs
# inspección y smokes aislados, sin instalar la suite globalmente
npm run dev:picante -- status
npm run smoke:picante
npm run smoke:picante:tui
# TUI interactiva contra los edits actuales
npm run dev:picanteEl wrapper registra este checkout como package user-scope dentro del agent descartable pi-cante/.pandi-dev/ y abre la
TUI en este repo como workspace real. Los smokes fuerzan un proyecto scratch separado. Deshabilita las copias bundled de
Pandi, no usa ni modifica perfiles reales y no carga sus packages; /doctor sí puede inspeccionarlos en modo read-only.
Si los repos no son siblings, definí PI_CANTE_ROOT=/ruta/a/pi-cante.
Una extensión exporta una función default que recibe pi: ExtensionAPI:
// extensions/pandi-hello/index.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
if (ctx.hasUI) ctx.ui.notify("pandi-hello cargada", "info");
});
pi.registerCommand("hello", {
description: "Saluda",
handler: async (args, ctx) => ctx.ui.notify(`Hola ${args || "mundo"}!`, "info"),
});
}Cada extensión de este repo vive en su propio extensions/pandi-<nombre>/index.ts (o un único .ts) y es
self-contained: nada de imports runtime cruzados a otra extensión (../shared/ solo existe para el harness de
tests, nunca para código que corre en producción — ver AGENTS.md). Si dos extensiones necesitan la misma utilidad
chica (un notify.ts, un parser de flags), se duplica a propósito: así cada una se puede instalar sola vía
pi install.
Los tres primitivos conviven en la misma extensión; la pregunta es quién dispara el código:
| Primitivo | Quién lo dispara | Usalo cuando | Ejemplo en este repo |
|---|---|---|---|
Comando — pi.registerCommand("nombre", {...}) |
El usuario, tipeando /nombre |
La acción es explícita y el usuario decide el momento | /plan, /loop, /goal |
Tool — pi.registerTool({...}) |
El LLM, cuando decide que la necesita dentro de un turno | Es una capacidad que el modelo debe poder invocar por su cuenta | dynamic_workflow (pandi-dynamic-workflows) |
Event handler — pi.on("evento", (event, ctx) => ...) |
El runtime de pi, en cada punto del lifecycle | Necesitás reaccionar o interceptar sin que nadie lo pida (bloquear un tool, inyectar contexto, persistir estado) | pi.on("tool_call") bloqueando mutaciones en pandi-plan/pandi-loop; pi.on("session_before_compact") en pandi-auto-compact |
Ver la referencia completa de eventos y ExtensionAPI en el
extensions.md de pi
upstream (o docs/extensions.md de tu instalación local del paquete).
Este repo sigue siendo auto-hospedado — Picante ejecuta el TypeScript actual desde disco — pero el proceso de prueba ya no comparte perfil con la sesión de autoría:
flowchart LR
E["pandi-extensions checkout"] --> W["wrapper npm"]
W --> C["pi-cante desde source"]
C --> D[".pandi-dev"]
D --> A["agent + sessions"]
A -->|"package user-scope aislado"| E
W -->|"TUI: workspace real"| E
D --> P["scratch project solo para smokes"]
Al ejecutar npm run dev:picante, el wrapper:
- encuentra
../pi-canteo la ruta indicada porPI_CANTE_ROOT; - crea o reutiliza
pi-cante/.pandi-dev/; - instala este checkout por ruta local con alcance de usuario dentro del agent descartable, nunca en un home real;
- abre la TUI interactiva en este repo, donde el estado Picante vive en
.picante/(gitignored); - reserva el proyecto scratch para los smokes RPC y TUI;
- fuerza los directorios de agent y sesiones de todas las distros y deshabilita el bundle duplicado;
- hace que Dynamic Workflows, Goal y Rename ejecuten el
pi-test.shde ese mismo checkout.
Por eso un edit aparece en el siguiente arranque sin publicar ni hacer pi install ./ global. También evita que una
copia instalada en ~/.pi se mezcle con la local. El aislamiento cubre configuración, estado y selección de packages;
no es un sandbox del sistema operativo: las extensiones conservan acceso al filesystem, procesos, red y credenciales
del usuario que lanzó Picante.
Para razonar sobre las pruebas conviene separar tres ejes ortogonales. Cada uno tiene una herramienta distinta.
Loop primario. Sin riesgo para tu sesión. Es TDD (la vía Farley).
Los tests de integración no cargan la extensión en tu sesión: el harness compartido
(extensions/shared/test/harness.mjs) hace un esbuild-bundle de la extensión a un directorio temporal y la importa
dinámicamente con un ctx mockeado. Es aislado y rápido.
# una suite puntual (segundos) — el loop de desarrollo
node --test extensions/pandi-<ext>/tests/integration/<algo>.test.mjs
# todo el gate antes de commitear (typecheck + biome + markdownlint + suites)
npm testscripts/test/run-all.mjsauto-descubre toda suiteextensions/<ext>/tests/integration/*.test.mjs(no hay manifest hardcodeado); para excluir una suite todavía-no-verde se usa su denylistignoredDraftSuites.- Escribí el test antes que la implementación (Red → Green → Refactor → Commit). Si un comportamiento "solo se puede ver en vivo", suele faltar cubrirlo con un test aislado.
Este eje debería ser ~90% de tu dev-test. Los ejes 2 y 3 son complementos, no sustitutos.
Picante convierte la segunda instancia aislada en un comando repetible. Usá cada entrada según el riesgo que querés cubrir:
| Comando | Qué comprueba | Modelo |
|---|---|---|
npm run dev:picante -- status |
Rutas efectivas, perfil aislado, bundle deshabilitado y única fuente local. | No |
npm run smoke:picante |
Carga RPC, cero errores de extensión e inventario exacto del manifiesto resuelto en este checkout. | No |
npm run smoke:picante:tui |
Startup real en tmux, dashboard /workflows y reporte /doctor; siempre elimina la sesión temporal. |
No |
npm run dev:picante |
TUI interactiva para una exploración manual después de los checks. | Solo si enviás un prompt |
El mecanismo in-place sigue siendo /reload (comando) o ctx.reload() desde un handler: recarga extensions, skills,
prompts y themes leyendo el source actual. Dentro del perfil .pandi-dev es útil; en una sesión estable vuelve a tener
el riesgo de cargar edits sin commitear. Para el patrón de reload seguro, ver el ejemplo upstream
packages/coding-agent/examples/extensions/reload-runtime.ts.
Regla relacionada: no hagas busy-poll de un run en background — el harness lo trackea y avisa al terminar. (Ver la guía del tool
dynamic_workflowy el skillultracode.)
Solo cuando necesites validar el host vanilla, podés cargar un entry point explícito sin instalarlo:
pi --no-extensions -e ./extensions/pandi-<ext>/index.tsEse comando requiere el Pi CLI global y queda fuera del loop normal de contribución. Para probar específicamente la
semántica de instalación, usá pi install -l ./ dentro de un proyecto scratch; no apuntes el perfil global al checkout
de trabajo.
Eje aparte, opt-in. Nada que ver con /reload: acá aislás la ejecución de tools/!-commands, no el ciclo de
recarga.
- Gondolin (micro-VM Linux): ver
gondolin-isolation.md. Aísla los tools built-in y!en una micro-VM; no aísla los subagentes de dynamic-workflows (spawneanpi/codexen el host). - Contenedor / Docker: para aislar el orquestador entero, correr todo
pidentro de un contenedor, o usar la extensiónpandi-containerpara correr comandos puntuales en micro-VMs de Applecontainer.
No uses el eje 3 para resolver el eje 2: un edit roto no es un problema de seguridad de ejecución, es de cuándo recargás.
- Escribí/actualizá el test aislado primero (eje 1, Red).
- Implementá hasta verde:
node --test <suite>; refactor con la red de tests. - Corré
npm run dev:picante -- statusynpm run smoke:picantepara validar la topología y el runtime local. - Si tocaste startup o TUI, corré
npm run smoke:picante:tui. - Corré
npm testcompleto antes de commitear (gate del repo). - Hacé un commit atómico con Conventional Commits + scope (ej.
feat(pandi-goal): …).
README.md— consumo estable y desarrollo aislado con Picante.setup.md— requisitos y topología de los dos caminos.- Skill
init-pandi-extensions— onboarding desde un clon fresco. README.md#verificación— cómo corrernpm test(harness de tests, lint, typecheck).gondolin-isolation.md— aislamiento por micro-VM (eje 3).