SLOPSHOPPER

tablero-despacho

Tablero de PRs abiertos, checks, reviews y worktrees, con atajos para pedirle cosas a Claude. Los atajos de despacho aparecen solo si el proyecto declara…

newpanebandcommandprocesstimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tablero-despacho
│ ┃ Tablero de despacho ✕ › fix the failing auth test and add an audit log call │ ┃ Actualizado recién [ Actualizar ] │ ┃ ⏺ Read(src/auth.ts) │ ┃ No pude correr gh: ¿está instalado y en el ⎿ Read 6 lines │ ┃ PATH? ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /tablero │ ⎿ tablero-despacho: Tablero de despacho abierto. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Tablero de despacho
Actualizado recién [ Actualizar ] No pude correr gh: ¿está instalado y en el PATH?
README

orca-stack

Instala de una vez el stack de agentes que uso para trabajar con Orca: la política del stack, las skills propias, los wrappers de proveedores, Pi con Gentle Shell y los mods de Claude Code. Todo en un comando, con respaldo de lo que toca y con vuelta atrás.

¿Cómo se usa día a día? Leé el manual de uso: comandos, mods, marchas, cuándo conviene usarlo y cuándo no, casos borde y manejo. Marca lo que está solo en main y todavía no salió en un release.

npx github:Gagaismo/orca-stack#v0.12.0 global     # instalación a nivel usuario
npx github:Gagaismo/orca-stack#v0.12.0 init       # cablea el proyecto actual
npx github:Gagaismo/orca-stack#v0.12.0 doctor     # verifica que todo funcione de verdad
npx github:Gagaismo/orca-stack#v0.12.0 remove     # deshace lo que instaló global
npx github:Gagaismo/orca-stack#v0.12.0 sesiones   # lista las sesiones recientes de Claude, Pi y agy (para `/retro`)
npx github:Gagaismo/orca-stack#v0.12.0 despachar 12 agy   # desde el checkout principal: despacha el ticket #12 al agente
npx github:Gagaismo/orca-stack#v0.12.0 revisar 15 pi      # despacha el review del PR #15
npx github:Gagaismo/orca-stack#v0.12.0 brief-revision 15 --modelo <modelo>   # modo un-agente: worktree y brief de review para un subagente
npx github:Gagaismo/orca-stack#v0.12.0 consultas 42 57    # filtra y avisa consultas nuevas en issues y PRs en curso
npx github:Gagaismo/orca-stack#v0.12.0 avisar 42 <url>    # Proactive: le toca el timbre al worker de #42 con la URL de la respuesta
npx github:Gagaismo/orca-stack#v0.12.0 vigilar 42 57      # el Monitor de la tanda: solo eventos de sus PRs, hasta tanda-cerrada

¿Primera vez, o un proyecto nuevo o ya empezado? Seguí la guía de setup: tiene un cuestionario que dice qué configurar en cada caso.

Cero dependencias, Node 20 o más nuevo. Funciona en Windows, macOS y Linux.

Qué instala

global (idempotente; todo archivo que modifica se respalda antes en ~/.orca-stack/backups/<fecha>/ (nunca al lado del archivo, para que ningún agente cargue el respaldo)):

QuéDónde
Política del stack: ORCA-STACK.md y ROLE-BRIEFS.md~/orca/stack/
Wrappers claude-orca-stack, codex-orca-stack, agy-orca-stack (.cmd en Windows, .sh en el resto), cada uno con --orca-stack-self-test~/orca/stack/scripts/
Skills propias: orca-stack-policy, orca-setup, ponytail, ponytail-review, ponytail-audit, ponytail-debt, ponytail-help~/.claude/skills/ y ~/.agents/skills/
Ocho mods de Claude Code (ver abajo) y sus rutas en env.CLAUDE_CODE_PLUGIN_DIRS~/.claude/mods/ y ~/.claude/settings.json
Nada de metodología en ~/.claude/CLAUDE.md: si un global anterior dejó ahí el bloque <!-- orca-stack:begin --> … <!-- orca-stack:end -->, lo saca (con respaldo). También achica el bloque que gentle-ai escribe ahí (ver abajo)~/.claude/CLAUDE.md
Pi: gentle-pi@4.2.0 y pi-claude-bridge@0.9.2 con pi install (pi install npm:gentle-pi@4.2.0), más npm i -g gentle-pi@4.2.0 (comando gentle-shell). Necesita Pi 1.2.0 o más nuevo: con un Pi anterior no instala gentle-pi y falla~/.pi/agent/ y npm global
Comando orca-stack, para usar desde cualquier repo sin npx ni redWindows: prefijo global de npm si esta en PATH, con shims .cmd y sin extension para PowerShell y Git Bash. Otros sistemas: primer directorio escribible del PATH. La copia queda en ~/.orca-stack/cli/; si falta el destino compartido, se usa ~/.orca-stack/bin/ y global imprime como agregarlo al PATH
Registro de lo instalado, para remove~/.orca-stack/state.json
Capa personal de configuración~/.orca-stack/config.json

Gentle Shell se actualiza volviendo a correr global con la versión nueva de orca-stack. Desde gentle-pi 4.2.0, gentle-shell update reinstala solo el paquete global dueño del Pi que corre, y si no puede probar qué paquete global es dueño de Pi, no toca nada.

Los wrappers se llaman distinto del binario real a propósito: así nunca se llaman a sí mismos. Cada uno fija los marcadores ORCA_STACK_*, busca el binario real en el PATH, pasa los argumentos tal cual y devuelve su código de salida.

global instala herramientas, skills, mods y wrappers, pero ningún texto de metodología: una sesión de Claude en un proyecto sin .orca-stack.json no recibe nada del stack por esta vía. La metodología llega por el proyecto, con init (el contrato y las líneas de la política en AGENTS.md, y @AGENTS.md en CLAUDE.md). Del CLAUDE.md global global solo saca el bloque entre orca-stack:begin y orca-stack:end (o orca-stack:auto, el viejo) que dejó una versión anterior, y respeta todo lo demás. Con settings.json hace algo parecido: solo suma sus carpetas a CLAUDE_CODE_PLUGIN_DIRS (con el separador de tu sistema), conserva las que ya había y no toca ninguna otra clave.

El bloque gentle-ai:sdd-orchestrator (v2/v3) o gentle-ai:orchestrator (v4) de ~/.claude/CLAUDE.md (unos 16 KB: coordinación, delegación, tabla de modelos y el flujo de cada versión) se carga en cada sesión. Por eso global lo achica según gentle.orchestrator en ~/.orca-stack/config.json:

  • "lazy" (por defecto): guarda el texto completo, tal cual lo escribió gentle-ai, en ~/.orca-stack/gentle/orchestrator.md y deja entre los marcadores un puntero corto (empieza con <!-- orca-stack:gentle-lazy v1 -->). El puntero dice que ODD es el flujo predeterminado y que la sesión que orquesta (main) debe leer la copia completa; también exige presentar completos los menús y pedidos de consentimiento, sin responder por el humano. Hace respaldo, es idempotente, respeta --dry-run e imprime cuántos KB ahorró. Con marcadores incompletos, repetidos o desordenados no escribe nada y dice por qué. Se omite mientras el RDD de gentle-ai esté prendido (o no se pueda leer): el bloque trae su contrato de review y el puntero lo apagaría en silencio; si ya estaba en lazy, global lo restaura desde la copia.
  • "full": deja el bloque como lo escribe gentle-ai (y, si estaba en lazy, lo restaura desde la copia, con respaldo; si la copia falta, avisa que corras gentle-ai sync). Con el RDD de gentle-ai apagado (gentle-ai review mode status dice off), además recorta las secciones de review, que no hacen nada y se cargan igual (unos 26 KB); si RDD está prendido o gentle-ai no está instalado, ese recorte se omite y dice por qué.

gentle-ai sync reescribe el bloque entre los marcadores (reemplaza el contenido, no duplica ni valida un hash), así que después de cada sync hay que correr orca-stack global de nuevo, que además refresca la copia y el puntero. doctor avisa si la sync lo reescribió, si falta la copia o si todavía está en la ruta antigua. Para volver al bloque completo: poné "gentle": { "orchestrator": "full" } y corré orca-stack global, o corré gentle-ai sync. orca-stack remove también lo restaura desde la copia. La clave antigua gentle.sddOrchestrator sigue funcionando durante la migración; doctor sugiere renombrarla y, si están las dos, gentle.orchestrator tiene prioridad. global mueve la copia antigua sdd-orchestrator.md a orchestrator.md; si existen ambas, conserva la más nueva por fecha de modificación y respalda la otra.

gentle-ai v4. v4 retiró SDD y OpenSpec: ODD es su único flujo y gentle-ai sync renombra el bloque en el lugar a gentle-ai:orchestrator, con el orquestador ODD adentro y las mismas secciones de review. global, doctor y remove reconocen los dos nombres: el modo lazy, el recorte y la restauración funcionan igual; el puntero indica que ODD es el flujo predeterminado y que la sesión main lee la copia completa. La copia queda en ~/.orca-stack/gentle/orchestrator.md. Para pasar a v4 en Windows (necesita Go 1.25.10 o más nuevo en el PATH):

go install github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1
gentle-ai sync
npx github:Gagaismo/orca-stack global

La sync deja el bloque completo y la copia vieja (de v2) queda desactualizada: doctor avisa que corras orca-stack global, que guarda el texto de v4 en la copia y vuelve a dejar el puntero.

Los mods

Son plugins de Claude Code que se recargan en caliente (.claude-plugin/plugin.json + hooks/). Se activan con la variable CLAUDE_CODE_PLUGIN_DIRS, que global completa por vos; se cargan en las sesiones que abras después.

ModQué haceConfig
tablero-despachoTablero de PRs abiertos con checks, review y worktrees (/tablero). Usa gh.Los atajos de despacho aparecen solo si hay despacho
guardia-despachoBloquea o corrige las llamadas a tus scripts de despacho y revisión según reglas del proyecto. Siempre activo, con o sin despacho: pregunta qué perfil de gentle-pi usa un despacho a pi (--perfil-pi); rechaza orca worktree rm y git worktree remove si la rama de ese worktree tiene commits que no están en ningún remoto, porque orca worktree rm borra también la rama local. Si no puede resolver la rama, rechazaLas reglas de despacho están apagadas salvo que haya despacho
puente-gentleConsulta a Gentle Shell (Pi) como otro orquestador par (/pi, /gentle). La instrucción de consultar y la herramienta consultar_gentle_shell aparecen solo en un repo con .orca-stack.json. Frena el merge de un PR grande sin consulta y el de uno que toca riesgo sin sus reviews limpios (de pi, o de dos familias de modelo que no sean la de pi si el PR lo escribió pi)humano, riesgo, agentes.pi.claudeConfigDir
bandeja-mensajesCaptura los bloques `mensaje <destinatario> que redacta Claude y deja copiarlos o guardarlosmensajes.destinatarios
grilling-okResponde con un clic (o con un ok) a las preguntas Q1…Qn de una sesión de grilling—
modo-autonomoInstrucción permanente de trabajo autónomo hasta la meta, con recordatorio anti-parada acotado—
marchas/marcha muestra o cambia la marcha de main: Lazy, Medium o Proactive; cada sesión nueva empieza en Medium; avisa en una línea cuando el modo autónomo coincide con Lazy, Proactive o ultracode—
reloj-cacheMuestra cuánto falta para que venza el caché del prompt y ofrece /compactar-ya—

El mod marchas actúa solo en el checkout principal de un repo con .orca-stack.json o .orca-stack.local.json cuyo contrato incluye la sección Marchas. Para mostrar la marcha actual, usá /marcha; para cambiarla, /marcha lazy, /marcha medium o /marcha proactive. Para apagarlo, corré claude plugin disable marchas@inline --scope user; usá --scope local para este repo en esta máquina o --scope project para el equipo.

Prerrequisitos

  • Node.js 20 o más nuevo (Pi pide Node 22.19 o más nuevo).
  • Git y GitHub CLI (gh), para el tablero y para el puente.
  • Claude Code, para los mods.
  • En Windows, el postinstall de gentle-pi compila el binario de gentle-ai con Go (1.25.10 o más nuevo): si pi install de gentle-pi falla, revisá que go esté en el PATH.

global detecta estas herramientas, no las instala; si faltan, imprime el comando oficial de cada una: Orca, engram, gentle-ai y Pi.

Uso

orca-stack global [--dry-run] [--force] [--skip-pi] [--limpiar-sdd] [--humano <nombre>]
orca-stack init   [--dry-run]
orca-stack doctor [--offline] [--skip-pi]
orca-stack remove [--dry-run] [--with-pi]
orca-stack sesiones [--desde <AAAA-MM-DD>] [--limite <n>]
orca-stack despachar <issue> <agente> [--nota "<texto>"] [--confirmado] [--perfil-pi <nombre>] [--dry-run]
orca-stack revisar <pr> <agente> [--nota "<texto>"] [--confirmado] [--perfil-pi <nombre>] [--dry-run]
orca-stack consultas <n...>
orca-stack avisar <issue|pr> <url-del-comentario> [--dry-run]
orca-stack reanudar <issue> <url-del-comentario> [--confirmado] [--dry-run]
orca-stack vigilar <n...> [--cada <segundos>] [--una-vez] [--dry-run] [--consultas]
orca-stack vigilar --merge-de-main <pr>
  • --dry-run cuenta cada acción sin escribir nada.
  • reanudar retoma una TUI detenida o relanza al worker en el mismo worktree con su brief original y la URL de respuesta.
  • --force reemplaza archivos que ya existían y no son de orca-stack (por ejemplo, una skill tuya llamada igual). Sin --force se saltean con un aviso ⚠ y quedan como estaban; con --force se respaldan antes.
  • global --limpiar-sdd respalda en ~/.orca-stack/backups/sdd-<fecha>/ y mueve solo restos conocidos; doctor lista los que encuentra y muestra el comando exacto. Los nombres desconocidos nunca se mueven y global sin esta opción no limpia nada. Si algún movimiento falla, informa qué quedó pendiente; podés volver a ejecutar el mismo comando.
  • init escribe en el AGENTS.md del proyecto el bloque <!-- orca-stack:begin --> … <!-- orca-stack:end --> con el contrato de trabajo completo (assets/stack/CONTRATO.md, igual en todos los proyectos) y las líneas de la política; a CLAUDE.md le pone un bloque corto con @AGENTS.md. Además crea .orca-stack.json y .orca-stack.local.json si faltan y agrega este último a .gitignore. Es idempotente y respalda lo que modifica. Los valores propios de cada proyecto no van en el bloque: viven en .orca-stack.json.
  • orca-setup es la skill con la que la sesión de main entrevista al humano, en una sola ronda, para completar .orca-stack.json ("setup": "completo"). La ofrece sola cuando falta y doctor avisa si quedó pendiente.
  • sesiones es de solo lectura: lista las sesiones recientes de Claude Code (~/.claude/projects y, si está, la carpeta de agentes.pi.claudeConfigDir), de Pi (~/.pi/agent/sessions) y de agy (~/.gemini/antigravity-cli, solo con Node 22.13 o más nuevo). Por sesión muestra la herramienta, la carpeta, inicio y fin, las llamadas, tus mensajes, las llamadas con error y los resultados de más de 15k caracteres. Es la entrada para quien corre /retro de Matt: --desde fija el día de inicio (por defecto, los últimos 2 días) y --limite cuántas sesiones (por defecto, 10).
  • consultas <n...> filtra y avisa consultas nuevas en issues o PRs de una tanda. Consulta gh api user en ejecución, busca comentarios de la cuenta del stack que empiecen con Consulta: e imprime consulta nueva en #<n> una sola vez por comentario, guardando los IDs en .git/orca-stack/consultas-reportadas.json.
  • vigilar <n...> es el Monitor de una tanda (<n...>, los issues de la tanda): stdout lleva solo eventos, uno por línea, y todo diagnóstico va a stderr. Cada --cada segundos (por defecto 60) lee gh pr list --state all (sin --state no aparecen los PRs mergeados ni cerrados) y emite pr-abierto, ci-verde, ci-roja (con los checks que fallaron), pr-mergeado, pr-cerrado, vigilar-ciego (tres vueltas seguidas sin poder leer GitHub, una vez por corte; nunca dice que no hay PRs) y, cuando terminaron todos los items, tanda-cerrada, y sale con 0. Un PR es del issue por su referencia de cierre, por la rama del worktree de este repo enlazado al issue (buscado como en avisar) o por una línea Closes #<n> en su cuerpo, nunca por el nombre de la rama; un issue cerrado sin PR cuenta como terminado. De dos corridas de un mismo check en un commit cuenta la última, y cada corrida que termina en rojo es un ci-roja, también un gh run rerun --failed que vuelve a fallar en el mismo commit; la misma corrida leída de nuevo no se repite. Lo emitido queda en .git/orca-stack/vigilar.json: rearmado con los mismos argumentos, retoma sin repetir nada. --una-vez consulta una vez y sale, --dry-run además no escribe nada, y --consultas corre en cada vuelta el chequeo de consultas para los mismos items, así Proactive sigue con un solo Monitor. Como todas las sesiones comparten la cuenta de gh, un merge de main se ve igual que uno del humano: main corre vigilar --merge-de-main <pr> justo antes de cada intento de gh pr merge <pr> (y de nuevo después de gh pr update-branch), que registra el head del PR en .git/orca-stack/vigilar-merges.json; un merge de ese head hecho dentro de los 30 minutos siguientes al registro (según el mergedAt del PR) no emite pr-mergeado, aunque el watcher lo vea más tarde. Con un PR ya mergeado o cerrado, --merge-de-main sale con 1 y no registra nada. El watcher solo lee ese archivo.
  • avisar <issue|pr> <url-del-comentario> es el timbre de Proactive: cuando main responde la pregunta de un worker, le escribe en su terminal una sola línea fija, Message from main: <url>, con la URL del comentario y nada de su texto. Encuentra la terminal por el worktree de este repo enlazado a <n> en orca worktree ps (con un PR, el de la rama del PR), buscado solo entre los de git worktree list, porque los selectores issue:<n> y branch: de Orca abarcan todos los repos; lista sus terminales con orca terminal list --worktree path:<ruta> y se niega si alguna no trae ese worktreePath. Si un rebote dejó dos, elige la del agente vivo según orca worktree ps. Manda solo mientras el turno del worker está en curso (estado working en Orca y el indicador de turno de Claude en pantalla): se niega si el turno ya terminó, si la terminal muestra un shell o un diálogo, o si hay más de un agente vivo. Por ahora solo avisa a claude: se niega con pi, que corre sin TUI y relee los comentarios antes de su PR, y con codex y agy hasta que pasen su prueba de Enter a mitad de turno. Se niega si el comentario no es de la cuenta del stack (gh api user), si no está en <issue|pr> o si no empieza con Respuesta de main: o Respuesta del humano: (la misma cuenta publica las Consulta: de los workers). Envía una sola vez con --wait-submit: sin recibo de entrega informa la falla y no reenvía. --dry-run muestra la línea y la terminal elegida sin enviar nada. main nunca arma el timbre con llamadas sueltas a orca.
  • despachar y revisar (agentes: claude, agy, codex, codex-cc, pi) corren desde el checkout principal de un repo con el setup completo. Se niegan si el agente no está en agentes.usa, si está en agentes.preguntarAntes (repetí con --confirmado cuando el humano dijo que sí), si el issue está cerrado o asignado, si el revisor es el implementador del PR (línea Implementador:) o corre un modelo de su misma familia (sale de agentes.modelos; pi y claude sí se revisan entre sí), si el agente tiene una regla guardia.bloquear vigente, o si ya hay un worktree o un agente vivo para ese ticket. Antes de tocar nada hacen git fetch origin y comprueban que cada ruta que cita el brief (armado desde assets/stack/brief-ticket.md o brief-revision.md) exista en origin/<ramaBase> (en revisar, en la rama del PR): una ruta que falta o que git ignora aborta sin asignar ni crear nada. Si pasa todo, despachar asigna el issue (el assignee es el cerrojo y lo maneja el comando: no se asigna antes), y ambos crean el worktree con orca worktree create (así lo ve Orca), escriben el brief en ~/orca/briefs/, lanzan el agente en una terminal de Orca y le mandan el brief. despachar imprime Rama: <rama> con el nombre devuelto por Orca; en --dry-run aclara que Orca define la rama al crear el worktree. Después de confirmar el inicio, despachar, revisar y rebote leen tres veces la pantalla de cada terminal salvo pi, con pausas de 5 segundos: fallan si ven un mensaje de cuota o si el último renglón no vacío volvió al prompt de PowerShell (PS <unidad>:\...>). No detectan prompts de bash ni cmd.exe. Si algo falla después de crear el worktree, el mensaje lista lo que quedó (assignee, worktree, brief) y cómo recuperarlo, porque el cerrojo rechaza un reintento. Pi corre sin TUI (pi -p), así que sí puede ejecutar git, gh y la suite. --nota agrega texto al brief y --dry-run muestra el plan y el brief sin escribir nada. Los modelos salen de agentes.modelos de tu config personal: un texto, { "modelo", "esfuerzo" } o { "implementar": {…}, "revisar": {…} } por agente.
  • --perfil-pi <nombre> (solo con pi) fija qué perfil de modelos de gentle-pi usa ese despacho, sin tocar el perfil global activo. Los perfiles viven en ~/.pi/gentle-ai/profiles.json; el comando se niega si el nombre no existe o si el clon ya fija otro perfil (<git-common-dir>/gentle-ai/profile-pin.json, que gana en silencio sobre el del worktree). Después de crear el worktree escribe el pin .pi/gentle-ai/profile.json ahí (gentle-pi lo lee en cada lanzamiento de subagente) y, si git no lo ignora, agrega /.pi/gentle-ai/profile.json a <git-common-dir>/info/exclude, así que nunca se commitea. Si el pin ya está versionado en el repo, lo deja como está cuando declara el mismo perfil y se niega (sin escribir) si declara otro; si git sigue sin ignorarlo después de la entrada (por ejemplo un .gitignore con !.pi/gentle-ai/profile.json), también se niega. Un thinking inválido en el perfil se avisa y se ignora. Como un pin no mueve al orquestador, lanza pi con --model <orchestrator.model del perfil> --thinking <orchestrator.thinking>. Con --dry-run cuenta todo esto sin escribir. Sin el flag y sin agentes.pi.perfil (o con "preguntar") rige el perfil activo global, como siempre. agentes.pi.perfil: "<nombre>" lo fija para todos los despachos a pi y el flag gana.
  • guardia-despacho pregunta ese perfil por vos: cuando Claude corre orca-stack despachar|revisar <n> pi sin --perfil-pi y agentes.pi.perfil no es un nombre, abre el diálogo "Perfil Pi" con los perfiles de profiles.json (opus-haiku y samu-balanceado primero; máximo cuatro opciones) y agrega --perfil-pi <elegido> justo detrás del agente. Si cierran el diálogo, o no hay nadie que conteste (-p), el comando corre sin cambios. Las consultas (/pi, consultar_gentle_shell) no preguntan, y un --dry-run tampoco.
  • Perfil opus-haiku: el orquestador es Opus 5.5 (claude-bridge/claude-opus-5-5, thinking high) y los diez subagentes de gentle-pi (gentle-ai-explore, gentle-ai-verify, gentle-ai-worker, jd-fix-agent, jd-judge-a, jd-judge-b, review-readability, review-reliability, review-resilience, review-risk) corren en Haiku 5.5 (claude-bridge/claude-haiku-5-5). Un perfil tiene que nombrar a todos los agentes: el que omite uno lo deja con el modelo de su frontmatter. Para cambiarlo después: /gentle:profiles en Pi, o editar ~/.pi/gentle-ai/profiles.json (hacé una copia antes).
  • --skip-pi no toca Pi (útil si todavía no lo instalaste).
  • --humano fija cómo te nombran los mods; si no lo pasás, sale de git config user.name.
  • doctor hace sondeos reales y sale con código 1 si hay una falla (✖). Los avisos (⚠) no cambian el código.
  • remove borra lo que global creó, restaura lo que había reemplazado, saca sus entradas de CLAUDE_CODE_PLUGIN_DIRS y, si quedó, el bloque de CLAUDE.md de una instalación vieja. Los respaldos (~/.orca-stack/backups/<fecha>/) se quedan. Los paquetes de Pi solo se sacan con --with-pi, y solo los que no tenías antes de instalar. Si editaste ~/.orca-stack/config.json, se conserva.
  • Para probar sin tocar tu carpeta personal: ORCA_STACK_HOME=/ruta/temporal orca-stack global.

doctor

No se queda en "el archivo existe":

  • corre el --orca-stack-self-test de cada wrapper y comprueba que la ruta de la política exista;
  • compara la versión de Pi y de sus paquetes con las fijadas (Pi 1.2.0, gentle-pi 4.2.0, pi-claude-bridge 0.9.2) y avisa si no coinciden; un Pi anterior a 1.2.0 es una falla, porque gentle-pi 4.2.0 no arranca con él;
  • sondea con un pi -p corto (con tiempo máximo) si falta el /login (se omite con
Source 4 files
hooks/register.tsx 190 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import type { TableroBoard } from '../types'
4import { loadConfig } from './orca-config'
5import type { Host } from './orca-config'
6import {
7  FIELDS,
8  GITHUB_REMOTE,
9  bandText,
10  checksText,
11  explainGhFailure,
12  parsePrs,
13  parseWorktrees,
14  prompts,
15  relativeTime,
16  reviewText,
17} from './tablero'
18
19const PANE = 'tablero'
20const EVERY_MS = 3 * 60 * 1000
21const board = { plugin: 'tablero-despacho', key: 'board' } as const
22
23const empty: TableroBoard = { status: 'unknown', message: '', prs: [], at: 0, hasDespacho: false }
24
25// The engine's `$` cannot cross an import: the config module gets these closures instead.
26const hostOf = ($: EngineInterface): Host => ({
27  home: async () => (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')),
28  exists: path => $.fs.exists(path),
29  read: path => $.fs.read(path),
30  git: argv => $.process.run(argv, { timeoutMs: 10_000 }),
31  now: () => $.clock.now(),
32})
33
34const refresh = async ($: EngineInterface): Promise<void> => {
35  let hasDespacho = false
36  const put = (next: Omit<TableroBoard, 'at' | 'hasDespacho'>) =>
37    $.clock.now().then(at => $.state.set(board, { ...next, at, hasDespacho }))
38
39  // Nothing runs outside a git repository with a GitHub remote.
40  const repo = await $.session.repo()
41  if (repo === null || repo.remote === null || !GITHUB_REMOTE.test(repo.remote)) {
42    await put({ status: 'norepo', message: '', prs: [] })
43    return
44  }
45  // The dispatch buttons exist only when the project config declares `despacho`.
46  const script = (await loadConfig(hostOf($))).despacho?.script
47  hasDespacho = typeof script === 'string' && script.trim() !== ''
48  try {
49    const gh = await $.process.run(
50      ['gh', 'pr', 'list', '--state', 'open', '--limit', '50', '--json', FIELDS],
51      { timeoutMs: 20_000 },
52    )
53    if (gh.exitCode !== 0) {
54      const failure = explainGhFailure(gh.stderr)
55      await put(
56        failure.kind === 'norepo'
57          ? { status: 'norepo', message: '', prs: [] }
58          : { status: 'error', message: failure.message, prs: [] },
59      )
60      return
61    }
62    const git = await $.process.run(['git', 'worktree', 'list', '--porcelain']).catch(() => undefined)
63    const worktrees = parseWorktrees(git?.exitCode === 0 ? git.stdout : '')
64    await put({ status: 'ok', message: '', prs: parsePrs(gh.stdout, worktrees) })
65  } catch {
66    await put({
67      status: 'error',
68      message: 'No pude correr gh: ¿está instalado y en el PATH?',
69      prs: [],
70    })
71  }
72}
73
74export const register: Register = on => {
75  on('session.start', async ($, e, next) => {
76    await $.command.register({
77      name: 'tablero',
78      description: 'Abre el tablero de PRs, checks, reviews y worktrees',
79    })
80    // Off the start-up path: the first read must not hold the session back.
81    $.clock.after(0, () => void refresh($))
82    $.clock.every(EVERY_MS, () => void refresh($))
83
84    return next(e)
85  })
86
87  on('command.run', { command: 'tablero' }, async $ => {
88    await $.ui.open({ id: PANE, title: 'Tablero de despacho' })
89    await refresh($)
90
91    return { text: 'Tablero de despacho abierto.' }
92  })
93
94  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
95    const { value = empty } = await $.state.get(board)
96    if (e.props.hasSurvey || value.status !== 'ok' || value.prs.length === 0) {
97      return next(e)
98    }
99    const { Box, Button, Text } = $.ui.resolve(e)
100
101    return (
102      <Box>
103        <Text dimColor>{bandText(value.prs)} </Text>
104        <Button
105          key="open"
106          label="Tablero"
107          onPress={async () => {
108            await $.ui.open({ id: PANE, title: 'Tablero de despacho' })
109            await refresh($)
110          }}
111        />
112      </Box>
113    )
114  })
115
116  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
117    const { Box, Button, Text } = $.ui.resolve(e)
118    const { value = empty } = await $.state.get(board)
119    const now = await $.clock.now()
120    const send = (text: string) => $.prompt.submit({ text, asUser: true })
121
122    const body =
123      value.status === 'norepo' ? (
124        <Text dimColor>Este directorio no es un repo de git con remoto en GitHub.</Text>
125      ) : value.status === 'error' ? (
126        <Text color="red">{value.message}</Text>
127      ) : value.status !== 'ok' ? (
128        <Text dimColor>Cargando…</Text>
129      ) : value.prs.length === 0 ? (
130        <Text dimColor>No hay PRs abiertos.</Text>
131      ) : (
132        <Box flexDirection="column" gap={1}>
133          {value.prs.map(pr => (
134            <Box key={`pr:${pr.number}`} flexDirection="column">
135              <Text bold>
136                #{pr.number} {pr.title}
137                {pr.isDraft ? ' (borrador)' : ''}
138              </Text>
139              <Text dimColor>
140                {pr.branch} · {pr.author} · {relativeTime(pr.updatedAt, now)}
141              </Text>
142              <Text
143                color={pr.checks.state === 'fail' ? 'red' : pr.checks.state === 'ok' ? 'green' : undefined}
144              >
145                checks {checksText(pr.checks)} · {reviewText(pr.review)}
146                {pr.worktree === undefined ? '' : ` · worktree ${pr.worktree}`}
147              </Text>
148              <Box gap={2}>
149                {value.hasDespacho ? (
150                  <Button
151                    key={`review:${pr.number}`}
152                    label="Revisar con agy"
153                    onPress={() => send(prompts.review(pr.number))}
154                  />
155                ) : null}
156                <Button
157                  key={`status:${pr.number}`}
158                  label="Ver estado"
159                  onPress={() => send(prompts.status(pr.number))}
160                />
161                <Button
162                  key={`gentle:${pr.number}`}
163                  label="Consultar a Gentle Shell"
164                  onPress={() => send(prompts.gentle(pr.number))}
165                />
166                <Button
167                  key={`merge:${pr.number}`}
168                  label="Mergear"
169                  onPress={() => send(prompts.merge(pr.number))}
170                />
171              </Box>
172            </Box>
173          ))}
174        </Box>
175      )
176
177    return (
178      <Box flexDirection="column" gap={1}>
179        <Box gap={2}>
180          <Text dimColor>
181            {value.at === 0 ? 'Sin datos todavía' : `Actualizado ${relativeTime(value.at, now)}`}
182          </Text>
183          <Button key="refresh" label="Actualizar" onPress={() => refresh($)} />
184        </Box>
185        {body}
186      </Box>
187    )
188  })
189}
190
hooks/orca-config.ts 279 lines
1// Config resolution shared by the orca-stack mods. A plugin can only import files of its own folder,
2// so this file is copied verbatim into each mod that needs it (hooks/orca-config.ts); the repo's
3// tests fail if a copy drifts from this one (edit shared/orca-config.ts, then run `npm run sync-shared`).
4//
5// Three layers, later ones override earlier ones:
6//   1. ~/.orca-stack/config.json        (personal)
7//   2. <repo>/.orca-stack.json          (committed project rules)
8//   3. <repo>/.orca-stack.local.json    (gitignored, local/sensitive)
9// <repo> is the main checkout, even when the session runs inside a git worktree.
10
11export type BlockRule = { agente: string; hasta: string; motivo?: string }
12
13export type StackConfig = {
14  humano?: string
15  mensajes?: { destinatarios?: string[] }
16  despacho?: { script?: string; revisar?: string }
17  riesgo?: string[] // path globs whose change needs a second review before merging (pi, or two other model families when pi wrote it)
18  agentes?: {
19    usa?: string[]
20    preguntarAntes?: string[]
21    modelos?: Record<string, unknown>
22    pi?: { perfil?: string; claudeConfigDir?: string } // perfil: gentle-pi profile name, or "preguntar" (the default)
23  }
24  guardia?: {
25    bloquear?: BlockRule[]
26    modeloClaude?: string
27  }
28}
29
30// Model families. Two agents whose models share a family share blind spots: one does not review the other, and
31// they do not count as two independent reviews. The family comes from the model in `agentes.modelos` (the one the
32// launcher passes with --model): its name, without the provider prefix, against `patterns`, in order. An agent
33// with no model takes `defaults`; pi is `fixed`, because its launcher never passes a model. A model that matches
34// no pattern has no family: nothing is proved about it.
35// The literal between the markers is JSON: src/despacho.mjs keeps the same one, and test/units.test.mjs compares them.
36// BEGIN FAMILIES
37export const FAMILIES = {
38  "agents": ["claude", "agy", "codex", "codex-cc", "pi"],
39  "fixed": { "pi": "claude" },
40  "defaults": { "claude": "claude", "codex": "gpt", "agy": "gemini" },
41  "patterns": [
42    ["claude", "^(claude|opus|sonnet|haiku|fable)"],
43    ["gpt", "^(gpt|codex|o\\d)"],
44    ["gemini", "^gemini"],
45    ["glm", "^glm"]
46  ]
47} as const
48// END FAMILIES
49
50const isPlain = (value: unknown): value is Record<string, unknown> =>
51  value !== null && typeof value === 'object' && !Array.isArray(value)
52
53export const familyOfModel = (model: string): string | undefined => {
54  const name = model.trim().toLowerCase().split('/').pop() ?? ''
55
56  return FAMILIES.patterns.find(([, pattern]) => new RegExp(pattern).test(name))?.[0]
57}
58
59// agentes.modelos.<agent>: "model", { modelo, esfuerzo } or { implementar: {...}, revisar: {...} } (as src/despacho.mjs).
60export const modelFor = (config: StackConfig, agent: string, mode: 'ticket' | 'review'): string | undefined => {
61  let entry = config.agentes?.modelos?.[agent]
62  if (isPlain(entry) && (entry['implementar'] !== undefined || entry['revisar'] !== undefined)) {
63    entry = entry[mode === 'review' ? 'revisar' : 'implementar']
64  }
65  const model = typeof entry === 'string' ? entry : isPlain(entry) ? entry['modelo'] : undefined
66
67  return typeof model === 'string' && model.trim() !== '' ? model.trim() : undefined
68}
69
70// The family `agent` runs on to implement ('ticket') or to review; `model` is the configured one, for messages.
71export const familyOf = (
72  config: StackConfig,
73  agent: string,
74  mode: 'ticket' | 'review',
75): { family: string | undefined; model: string | undefined } => {
76  const fixed = (FAMILIES.fixed as Record<string, string>)[agent]
77  if (fixed !== undefined) {
78    return { family: fixed, model: undefined }
79  }
80  const model = modelFor(config, agent, mode)
81
82  return { family: model === undefined ? (FAMILIES.defaults as Record<string, string>)[agent] : familyOfModel(model), model }
83}
84
85// "2026-10-29" -> local midnight of that day; an agent is blocked while now < that instant.
86export const parseUntil = (date: string): number | undefined => {
87  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date.trim())
88  if (match === null) {
89    return undefined
90  }
91  const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])]
92  const at = new Date(year, month - 1, day)
93  // Round trip: 2026-02-30 would silently become 2026-03-02.
94  if (at.getFullYear() !== year || at.getMonth() !== month - 1 || at.getDate() !== day) {
95    return undefined
96  }
97
98  return at.getTime()
99}
100
101// Why `agent` cannot be dispatched now, or undefined. A rule with a bad date stops it too: a block must not
102// vanish because of a typo.
103export const blockOf = (config: StackConfig, agent: string, now: number): string | undefined => {
104  for (const rule of config.guardia?.bloquear ?? []) {
105    if (typeof rule?.agente !== 'string' || rule.agente.toLowerCase() !== agent.toLowerCase()) {
106      continue
107    }
108    const until = typeof rule.hasta === 'string' ? parseUntil(rule.hasta) : undefined
109    if (until === undefined) {
110      return `La regla guardia.bloquear de ${agent} tiene una fecha "hasta" que no es AAAA-MM-DD (${String(rule.hasta)}): corregila antes de despacharlo.`
111    }
112    if (now < until) {
113      return typeof rule.motivo === 'string' && rule.motivo.trim() !== '' ? rule.motivo : `${agent} está bloqueado hasta el ${rule.hasta}.`
114    }
115  }
116
117  return undefined
118}
119
120// Plain objects merge key by key; anything else (arrays included) is replaced by the later layer.
121const mergePlain = (...layers: unknown[]): Record<string, unknown> => {
122  const out: Record<string, unknown> = {}
123  for (const layer of layers) {
124    if (!isPlain(layer)) {
125      continue
126    }
127    for (const [key, value] of Object.entries(layer)) {
128      const before = out[key]
129      out[key] = isPlain(value) && isPlain(before) ? mergePlain(before, value) : value
130    }
131  }
132
133  return out
134}
135
136// One exception: `guardia.bloquear` adds up across the layers, so a block in one layer cannot be dropped by another.
137export const mergeConfig = (...layers: unknown[]): StackConfig => {
138  const out = mergePlain(...layers) as StackConfig
139  const rules = layers.flatMap(layer => {
140    const guardia = isPlain(layer) ? layer['guardia'] : undefined
141    const list = isPlain(guardia) ? guardia['bloquear'] : undefined
142
143    return Array.isArray(list) ? (list as BlockRule[]) : []
144  })
145  if (rules.length > 0) {
146    out.guardia = { ...out.guardia, bloquear: rules }
147  }
148
149  return out
150}
151
152export const parseJson = (text: string): unknown => {
153  try {
154    return JSON.parse(text.replace(/^/, ''))
155  } catch {
156    return undefined
157  }
158}
159
160const slashed = (path: string): string => {
161  let out = path.split('\\').join('/')
162  while (out.length > 1 && out.endsWith('/')) {
163    out = out.slice(0, -1)
164  }
165
166  return out
167}
168
169// Output of `git rev-parse --path-format=absolute --git-common-dir --show-toplevel`:
170// the main checkout is the parent of the common `.git` dir; a bare repo falls back to the toplevel.
171export const repoRootFrom = (stdout: string): string | undefined => {
172  const [common, top] = stdout.split(/\r?\n/).map(line => line.trim())
173  if (common !== undefined && common !== '') {
174    const dir = slashed(common)
175    if (dir.endsWith('/.git')) {
176      return dir.slice(0, -'/.git'.length)
177    }
178  }
179
180  return top === undefined || top === '' ? undefined : slashed(top)
181}
182
183// The engine's `$` cannot cross an import, so the host passes the few operations this module needs
184// as closures: { home: async () => (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')), exists: p => $.fs.exists(p), read: p => $.fs.read(p),
185// git: argv => $.process.run(argv, { timeoutMs: 10_000 }), now: () => $.clock.now() }.
186export type Host = {
187  home: () => Promise<string | undefined>
188  exists: (path: string) => Promise<boolean>
189  read: (path: string) => Promise<string>
190  git: (argv: string[]) => Promise<{ exitCode: number; stdout: string }>
191  now: () => Promise<number>
192}
193
194export const homeOf = async (host: Host): Promise<string | undefined> => {
195  let home: string | undefined
196  try {
197    home = await host.home()
198  } catch {
199    home = undefined
200  }
201
202  return home === undefined || home === '' ? undefined : slashed(home)
203}
204
205const readLayer = async (host: Host, path: string): Promise<unknown> => {
206  try {
207    if (!(await host.exists(path))) {
208      return undefined
209    }
210
211    return parseJson(await host.read(path))
212  } catch {
213    return undefined
214  }
215}
216
217export const repoRoot = async (host: Host): Promise<string | undefined> => {
218  try {
219    const git = await host.git(['git', 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel'])
220
221    return git.exitCode === 0 ? repoRootFrom(git.stdout) : undefined
222  } catch {
223    return undefined
224  }
225}
226
227// Whether the repo opted into orca-stack, with `.orca-stack.json` or `.orca-stack.local.json`: mods that inject text
228// into the session, or that gate on it, only do so in such a project.
229export const hasProjectConfig = async (host: Host): Promise<boolean> => {
230  const root = await repoRoot(host)
231  if (root === undefined) {
232    return false
233  }
234  for (const name of ['.orca-stack.json', '.orca-stack.local.json']) {
235    try {
236      if (await host.exists(`${root}/${name}`)) {
237        return true
238      }
239    } catch {
240      // An unreadable layer counts as missing.
241    }
242  }
243
244  return false
245}
246
247const loadFor = async (host: Host, home: string | undefined, root: string | undefined): Promise<StackConfig> =>
248  mergeConfig(
249    home === undefined ? undefined : await readLayer(host, `${home}/.orca-stack/config.json`),
250    root === undefined ? undefined : await readLayer(host, `${root}/.orca-stack.json`),
251    root === undefined ? undefined : await readLayer(host, `${root}/.orca-stack.local.json`),
252  )
253
254// Never throws: a missing or broken layer is skipped.
255export const loadConfig = async (host: Host): Promise<StackConfig> =>
256  loadFor(host, await homeOf(host), await repoRoot(host))
257
258// loadConfig with a short memory, for hooks that run on every tool call. The memory is keyed by the
259// resolved home and repo root, so switching to another checkout never serves the previous one's rules.
260let remembered: { key: string; at: number; config: StackConfig } | undefined
261
262export const currentConfig = async (host: Host, maxAgeMs = 30_000): Promise<StackConfig> => {
263  const home = await homeOf(host)
264  const root = await repoRoot(host)
265  const key = `${home ?? ''}|${root ?? ''}`
266  const now = await host.now()
267  if (remembered !== undefined && remembered.key === key && now - remembered.at < maxAgeMs) {
268    return remembered.config
269  }
270  const config = await loadFor(host, home, root)
271  remembered = { key, at: now, config }
272
273  return config
274}
275
276export const forgetConfig = (): void => {
277  remembered = undefined
278}
279
hooks/tablero.ts 161 lines
1// Pure helpers for the dispatch board: parse gh/git output and word it in Spanish.
2
3import type { TableroChecks, TableroPr } from '../types'
4
5export const FIELDS =
6  'number,title,headRefName,author,isDraft,reviewDecision,statusCheckRollup,updatedAt'
7
8export const GITHUB_REMOTE = /github\.com[:/]/i
9
10type Check = {
11  status?: string
12  conclusion?: string
13  state?: string
14}
15
16const FAILED = new Set(['FAILURE', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE', 'ERROR'])
17const PENDING = new Set(['PENDING', 'EXPECTED', 'QUEUED', 'IN_PROGRESS', 'WAITING', 'REQUESTED'])
18
19export const summarizeChecks = (rollup: readonly Check[]): TableroChecks => {
20  let failed = 0
21  let pending = 0
22  for (const check of rollup) {
23    const verdict = (check.conclusion || check.state || check.status || '').toUpperCase()
24    if (FAILED.has(verdict)) {
25      failed += 1
26    } else if (PENDING.has(verdict) || (check.status !== undefined && check.status !== 'COMPLETED')) {
27      pending += 1
28    }
29  }
30  const total = rollup.length
31  if (total === 0) {
32    return { state: 'none', done: 0, total: 0 }
33  }
34  if (failed > 0) {
35    return { state: 'fail', done: failed, total }
36  }
37  if (pending > 0) {
38    return { state: 'pending', done: total - pending, total }
39  }
40
41  return { state: 'ok', done: total, total }
42}
43
44export const checksText = (checks: TableroChecks): string =>
45  ({
46    none: 'sin checks',
47    ok: `✓ ${checks.done}/${checks.total}`,
48    fail: `✗ ${checks.done} rojo${checks.done === 1 ? '' : 's'} de ${checks.total}`,
49    pending: `… ${checks.done}/${checks.total}`,
50  })[checks.state]
51
52export const reviewText = (review: TableroPr['review']): string =>
53  ({
54    approved: 'review aprobada',
55    changes: 'cambios pedidos',
56    required: 'review pendiente',
57    none: 'sin review',
58  })[review]
59
60const reviewOf = (decision: string | undefined): TableroPr['review'] =>
61  decision === 'APPROVED'
62    ? 'approved'
63    : decision === 'CHANGES_REQUESTED'
64      ? 'changes'
65      : decision === 'REVIEW_REQUIRED'
66        ? 'required'
67        : 'none'
68
69type RawPr = {
70  number: number
71  title: string
72  headRefName: string
73  author?: { login?: string }
74  isDraft?: boolean
75  reviewDecision?: string
76  statusCheckRollup?: Check[] | null
77  updatedAt?: string
78}
79
80// branch -> folder name of the worktree that has it checked out
81export const parseWorktrees = (porcelain: string): Map<string, string> => {
82  const byBranch = new Map<string, string>()
83  let path = ''
84  for (const line of porcelain.split(/\r?\n/)) {
85    if (line.startsWith('worktree ')) {
86      path = line.slice('worktree '.length).trim()
87    } else if (line.startsWith('branch refs/heads/')) {
88      byBranch.set(line.slice('branch refs/heads/'.length).trim(), path.split(/[\\/]/).pop() ?? path)
89    }
90  }
91
92  return byBranch
93}
94
95export const parsePrs = (json: string, worktrees: Map<string, string>): TableroPr[] => {
96  const raw = JSON.parse(json) as RawPr[]
97
98  return raw.map(pr => ({
99    number: pr.number,
100    title: pr.title,
101    branch: pr.headRefName,
102    author: pr.author?.login ?? '?',
103    isDraft: pr.isDraft === true,
104    review: reviewOf(pr.reviewDecision),
105    checks: summarizeChecks(pr.statusCheckRollup ?? []),
106    updatedAt: Date.parse(pr.updatedAt ?? '') || 0,
107    worktree: worktrees.get(pr.headRefName),
108  }))
109}
110
111export const relativeTime = (at: number, now: number): string => {
112  const minutes = Math.max(0, Math.round((now - at) / 60_000))
113  if (minutes < 1) {
114    return 'recién'
115  }
116  if (minutes < 60) {
117    return `hace ${minutes} min`
118  }
119  if (minutes < 60 * 24) {
120    return `hace ${Math.round(minutes / 60)} h`
121  }
122
123  return `hace ${Math.round(minutes / (60 * 24))} d`
124}
125
126export const bandText = (prs: readonly TableroPr[]): string => {
127  const ready = prs.filter(pr => pr.review === 'approved').length
128  const red = prs.filter(pr => pr.checks.state === 'fail').length
129  const parts = [`${prs.length} abierto${prs.length === 1 ? '' : 's'}`]
130  if (ready > 0) {
131    parts.push(`${ready} con review lista`)
132  }
133  if (red > 0) {
134    parts.push(`${red} con checks rojos`)
135  }
136
137  return `PRs: ${parts.join(' · ')}`
138}
139
140export type GhFailure = { kind: 'norepo' } | { kind: 'error'; message: string }
141
142// Short Spanish message for a failed gh call.
143export const explainGhFailure = (stderr: string): GhFailure => {
144  if (/not a git repository|no git remotes|none of the git remotes|could not determine base repo/i.test(stderr)) {
145    return { kind: 'norepo' }
146  }
147  if (/gh auth login|not logged in|authentication|GH_TOKEN|HTTP 401/i.test(stderr)) {
148    return { kind: 'error', message: 'gh no está autenticado: corré gh auth login.' }
149  }
150  const first = stderr.trim().split(/\r?\n/)[0] ?? ''
151
152  return { kind: 'error', message: `No pude leer los PRs${first === '' ? '.' : `: ${first.slice(0, 120)}`}` }
153}
154
155export const prompts = {
156  review: (n: number): string => `Lanzá el review del PR #${n} con agy`,
157  status: (n: number): string => `Fijate el PR #${n} y decime cómo está`,
158  merge: (n: number): string => `Si no hay bloqueos, mergeá el PR #${n}`,
159  gentle: (n: number): string => `Consultá a Gentle Shell sobre el PR #${n}`,
160}
161
types/index.d.ts 32 lines
1export type TableroChecks = {
2  state: 'ok' | 'fail' | 'pending' | 'none'
3  done: number
4  total: number
5}
6
7export type TableroPr = {
8  number: number
9  title: string
10  branch: string
11  author: string
12  isDraft: boolean
13  review: 'approved' | 'changes' | 'required' | 'none'
14  checks: TableroChecks
15  updatedAt: number
16  worktree?: string
17}
18
19export type TableroBoard = {
20  status: 'unknown' | 'loading' | 'ok' | 'norepo' | 'error'
21  message: string
22  prs: TableroPr[]
23  at: number
24  hasDespacho: boolean
25}
26
27declare module 'claude-code' {
28  interface PluginState {
29    'tablero-despacho': { board: TableroBoard }
30  }
31}
32