Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/vigia.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Tests del vigia de vencimientos
# -----------------------------------------
# A diferencia de los smoke tests de SAIJ, estos no dependen de ninguna API
# externa: arman expedientes sinteticos en una carpeta temporal y comprueban
# que el escaneo separe bien lo que vence, lo que nadie computo y lo que no se
# puede computar por falta de jurisdiccion.
#
# Sin red y sin datos reales, asi que no hay motivo para que sean tolerantes a
# fallos: si fallan, fallan.

name: Vigia de vencimientos

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Tests del vigia
run: node test/vigia-test.mjs
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ computar plazos o citar procedimiento. Leé la ficha de jurisdicción correspond
área en SAIJ (fuente primaria, en vivo).
4. Confirmá jurisdicción/fuero antes de cualquier cómputo de plazos.

## Al empezar la semana

Corré la skill **`argentina-vigia`**: revisa todos los expedientes de una pasada
y separa lo que vence pronto, lo que nadie computó todavía, y lo que no se puede
computar porque a la ficha le falta la jurisdicción.

El plazo que se pierde casi nunca es el que estabas mirando. Es la fila que
quedó en `🔲 a computar` en uno de los otros casos.

## Reglas de operación (no negociables)

- **Plazos por jurisdicción:** días hábiles judiciales ≠ hábiles administrativos ≠
Expand All @@ -63,7 +72,8 @@ computar plazos o citar procedimiento. Leé la ficha de jurisdicción correspond
`saij_buscar_doctrina`, `saij_documento(uuid)`. Cubre Nacional/Federal y legislación
Local de Jujuy y Salta.
- **Skills** — `abogacia-argentina` (router y ejes normativos por área),
`argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`.
`argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`,
`argentina-vigia`.

## Privacidad

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ y se computa el plazo aplicable.

| Capa | Qué hace |
|------|----------|
| **Skills** | `abogacia-argentina` (router generalista por área: laboral, civil, penal, familia, consumidor, societario, administrativo, previsional, tributario, tránsito, protección de datos), `argentina-formatos` (anatomía de cada tipo de escrito: demanda ≠ descargo ≠ recurso ≠ nota), `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`. Todas propias, MIT. |
| **Skills** | `abogacia-argentina` (router generalista por área: laboral, civil, penal, familia, consumidor, societario, administrativo, previsional, tributario, tránsito, protección de datos), `argentina-formatos` (anatomía de cada tipo de escrito: demanda ≠ descargo ≠ recurso ≠ nota), `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`. Todas propias, MIT., `argentina-vigia` (repaso de vencimientos de todos los expedientes) |
| **MCP SAIJ** | Búsqueda en vivo de jurisprudencia, legislación y doctrina + texto completo por uuid. Código propio, cero dependencias. |
| **Multi-jurisdicción** | CABA/Nacional (CPCCN, CPPF), Jujuy y Salta — extensible con una ficha por provincia. Pregunta el fuero al abrir cada caso. |
| **Workspace de casos** | Cada caso es una subcarpeta en `casos/` con ficha, escritos, documentación, jurisprudencia y plazos. |
Expand Down
221 changes: 221 additions & 0 deletions componentes/vigia/escanear.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#!/usr/bin/env node
/**
* Escanea `casos/` y reporta el estado de los plazos de cada expediente.
*
* Este script NO computa ningún plazo, y eso es deliberado. El cómputo depende
* de la jurisdicción, del tipo de plazo, de la feria y de las acordadas del
* tribunal, y la regla del proyecto es que eso lo resuelve `argentina-plazos`
* después de confirmar el fuero. Un script que se pusiera a restar días
* hábiles estaría adivinando exactamente lo que la regla prohíbe adivinar.
*
* Lo que sí hace es lo mecánico, que es donde se pierden los plazos de verdad:
* recorrer veinte carpetas, leer veinte tablas, y decir cuáles tienen filas sin
* computar, cuáles vencen pronto y cuáles no se pueden computar todavía porque
* a la ficha le falta la jurisdicción.
*
* Uso:
* node componentes/vigia/escanear.mjs [--casos <dir>] [--json] [--dias <n>]
*
* --casos dónde están los expedientes (default: ./casos)
* --json salida en JSON, para encadenar
* --dias ventana de "vence pronto" en días corridos (default: 15)
*/

import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';

const args = process.argv.slice(2);
const opt = (nombre, porDefecto) => {
const i = args.indexOf(`--${nombre}`);
return i >= 0 && args[i + 1] ? args[i + 1] : porDefecto;
};
const DIR_CASOS = opt('casos', 'casos');
const VENTANA = Number(opt('dias', '15'));
const JSON_OUT = args.includes('--json');

/** Marcas del proyecto: 🔲 es pendiente, 🔴 es fatal. */
const PENDIENTE = /🔲/u;
const FATAL = /🔴/u;

/** Una fila de la tabla de plazos, tal como está escrita. */
function parsearFilas(md) {
const filas = [];
for (const linea of md.split('\n')) {
const l = linea.trim();
if (!l.startsWith('|') || !l.endsWith('|')) continue;
const celdas = l.slice(1, -1).split('|').map((c) => c.trim());
if (celdas.length < 5) continue;
// Cabecera y separador de la tabla markdown.
if (/^-+$/.test(celdas[0].replace(/[\s:]/g, ''))) continue;
if (/^acto\b/i.test(celdas[0])) continue;
// Fila vacía de la plantilla.
if (celdas.every((c) => c === '')) continue;
// La fila de ejemplo que trae la plantilla, entre guiones bajos.
if (/^_.*_$/.test(celdas[0])) continue;

filas.push({
acto: celdas[0],
tipo: celdas[1],
inicio: celdas[2],
vencimiento: celdas[3],
estado: celdas[4],
});
}
return filas;
}

/** Una fecha suelta escrita como la escribe una persona. */
function leerFecha(texto) {
if (!texto) return null;
const t = texto.trim();
let m = t.match(/(\d{4})-(\d{2})-(\d{2})/);
if (m) return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
m = t.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
if (m) return new Date(Date.UTC(+m[3], +m[2] - 1, +m[1]));
return null;
}

/** La jurisdicción y el fuero declarados en la ficha, o null si faltan. */
function leerFicha(md) {
const campo = (etiqueta) => {
const re = new RegExp(`\\*\\*${etiqueta}:?\\*\\*\\s*(.*)`, 'i');
const m = md.match(re);
if (!m) return null;
// Se descarta el paréntesis de ayuda que trae la plantilla sin completar.
const v = m[1].replace(/\(.*?\)/g, '').trim();
return v === '' ? null : v;
};
return {
caratula: campo('Carátula'),
expediente: campo('N° de expediente'),
jurisdiccion: campo('Jurisdicción'),
fuero: campo('Fuero'),
estado: campo('Estado actual'),
};
}

const hoy = new Date();
hoy.setUTCHours(0, 0, 0, 0);
const diasHasta = (f) => Math.round((f - hoy) / 86400000);
Comment on lines +97 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: UTC-based 'hoy' can shift day count for evening runs

hoy is derived from new Date() then floored with setUTCHours(0,0,0,0), and deadlines are parsed to UTC midnight. For a user in Argentina (UTC-3) running the scanner in the evening, the UTC date is already the next calendar day, so a deadline due 'tomorrow' local time is reported as en 0d (and an overdue-by-today boundary can flip). Since the tool's audience is Argentine courts, anchor 'today' to the local (or an explicit America/Argentina) calendar date rather than UTC to avoid off-by-one urgency counts.

Was this helpful? React with 👍 / 👎


async function escanear() {
let entradas;
try {
entradas = await readdir(DIR_CASOS, { withFileTypes: true });
} catch {
return { error: `no pude leer ${DIR_CASOS}`, casos: [] };
}

const casos = [];
for (const e of entradas) {
if (!e.isDirectory() || e.name.startsWith('_') || e.name.startsWith('.')) continue;
const base = path.join(DIR_CASOS, e.name);

let ficha = {};
try {
ficha = leerFicha(await readFile(path.join(base, 'FICHA.md'), 'utf8'));
} catch {
ficha = { falta_ficha: true };
}

let filas = [];
let sinArchivo = false;
try {
filas = parsearFilas(await readFile(path.join(base, 'plazos.md'), 'utf8'));
} catch {
sinArchivo = true;
}

const plazos = filas.map((f) => {
const venc = leerFecha(f.vencimiento);
return {
...f,
fatal: FATAL.test(f.estado) || FATAL.test(f.acto),
sinComputar: venc === null || PENDIENTE.test(f.estado),
dias: venc ? diasHasta(venc) : null,
};
});

casos.push({
caso: e.name,
...ficha,
sinArchivoDePlazos: sinArchivo,
// La regla dura del proyecto: sin jurisdicción no se computa nada.
puedeComputarse: Boolean(ficha.jurisdiccion && ficha.fuero),
plazos,
});
}
return { casos };
}

function informar({ casos, error }) {
if (error) {
console.error(` ${error}`);
process.exitCode = 2;
return;
}
if (casos.length === 0) {
console.log(' No hay expedientes en ' + DIR_CASOS);
return;
}

const bloqueados = casos.filter((c) => !c.puedeComputarse);
const sinComputar = casos.flatMap((c) =>
c.plazos.filter((p) => p.sinComputar).map((p) => ({ ...p, caso: c.caso })),
);
const proximos = casos
.flatMap((c) => c.plazos.filter((p) => p.dias !== null).map((p) => ({ ...p, caso: c.caso })))
.filter((p) => p.dias <= VENTANA)
.sort((a, b) => a.dias - b.dias);

console.log(`\n ${casos.length} expedientes revisados el ${hoy.toISOString().slice(0, 10)}\n`);

// Lo vencido y lo inminente primero: es lo único que puede costar un derecho hoy.
if (proximos.length) {
console.log(' ── Vencen dentro de ' + VENTANA + ' días ──');
for (const p of proximos) {
const marca = p.dias < 0 ? '⛔ VENCIDO' : p.fatal ? '🔴 fatal ' : ' ';
const cuando = p.dias < 0 ? `hace ${-p.dias}d` : `en ${p.dias}d`;
console.log(` ${marca} ${cuando.padEnd(9)} ${p.caso} · ${p.acto}`);
}
console.log('');
}

// El modo de falla real: nadie lo computó todavía.
if (sinComputar.length) {
console.log(' ── Sin computar ──');
console.log(' (una fila sin vencimiento no es un plazo holgado: es un plazo que nadie miró)');
for (const p of sinComputar) {
console.log(` 🔲 ${p.caso} · ${p.acto || '(sin acto)'} · ${p.tipo || 'tipo sin declarar'}`);
}
console.log('');
}

// Y lo que ni siquiera se puede computar todavía.
if (bloqueados.length) {
console.log(' ── No se pueden computar: falta jurisdicción o fuero en la FICHA ──');
for (const c of bloqueados) {
const falta = [
c.jurisdiccion ? null : 'jurisdicción',
c.fuero ? null : 'fuero',
].filter(Boolean).join(' y ');
console.log(` ⚠️ ${c.caso} · falta ${falta || 'la ficha entera'}`);
}
console.log('');
}

const limpios = casos.length - new Set([
...proximos.map((p) => p.caso),
...sinComputar.map((p) => p.caso),
...bloqueados.map((c) => c.caso),
]).size;
console.log(` ${limpios} expediente(s) sin nada pendiente en esta ventana.`);

if (proximos.some((p) => p.dias < 0) || sinComputar.length || bloqueados.length) {
process.exitCode = 1;
}
Comment on lines +214 to +216

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Imminent (non-overdue) deadlines don't trigger exit 1

The exit-code condition is proximos.some((p) => p.dias < 0) || sinComputar.length || bloqueados.length. A workspace whose only issue is a deadline that is close but not yet past (e.g. a 🔴 fatal en 3d, exactly the case shown in the PR's example output) yields exit 0 — reported as "limpio". This defeats the stated purpose ("exit 1 cuando hay algo que atender") for the most time-sensitive scenario and would let a chained check pass while a fatal deadline looms. Treat any entry in proximos as attention-worthy.

Any deadline within the window, not only overdue ones, marks the run as needing attention.:

if (proximos.length || sinComputar.length || bloqueados.length) {
  process.exitCode = 1;
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

}

const resultado = await escanear();
if (JSON_OUT) console.log(JSON.stringify(resultado, null, 2));
else informar(resultado);
Comment on lines +214 to +221

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: --json mode never sets a non-zero exit code

In JSON mode the entry point does if (JSON_OUT) console.log(...); else informar(resultado);, so informar() — the only place that sets process.exitCode = 1 — is never called. The scanner therefore always exits 0 with --json, contradicting the documented behavior ("Devuelve exit 1 cuando hay algo que atender... así se puede encadenar. Con --json sale estructurado"). Any CI/pipeline that chains on the JSON output's exit status will treat every run as clean. Compute the exit code from the result regardless of output format (e.g. factor the proximos/sinComputar/bloqueados check into a helper called in both branches).

Set the exit code from the result in both output modes.:

function estadoSalida({ casos, error }) {
  if (error) return 2;
  const hayVencido = casos.some((c) => c.plazos.some((p) => p.dias !== null && p.dias < 0));
  const haySinComputar = casos.some((c) => c.plazos.some((p) => p.sinComputar));
  const hayBloqueado = casos.some((c) => !c.puedeComputarse);
  return hayVencido || haySinComputar || hayBloqueado ? 1 : 0;
}

const resultado = await escanear();
if (JSON_OUT) console.log(JSON.stringify(resultado, null, 2));
else informar(resultado);
process.exitCode = estadoSalida(resultado);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

90 changes: 90 additions & 0 deletions skills/argentina-vigia/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
name: argentina-vigia
description: "Revisa todos los expedientes abiertos de una sola pasada y reporta qué vence pronto, qué plazo nadie computó todavía, y qué caso no se puede computar porque a la ficha le falta la jurisdicción o el fuero. Usar al empezar el día o la semana, al volver de la feria, antes de irse de licencia, o cuando el usuario diga «qué tengo pendiente», «cómo vienen los plazos», «revisá los casos» o «qué se me vence»."
license: MIT
---

# Vigía de vencimientos · Todos los expedientes de una pasada

Skill original de Agente Smith (MIT).

Las otras skills trabajan **un** caso. Esta mira **todos** a la vez, porque el
plazo que se pierde en una Defensoría casi nunca es el que estabas mirando.

## El modo de falla que esta skill persigue

No es el plazo que computaste y se te pasó. Ese lo tenés en la cabeza.

Es la fila que quedó así:

```
| contestar demanda | hábiles judiciales | | | 🔲 a computar |
```

Alguien abrió el caso, cargó el acto, y no volvió. Con tres expedientes eso se
nota. Con veinte no se nota nunca, y desde afuera se ve idéntico a un caso al
día: una tabla prolija, sin fechas rojas, porque **no hay fechas**.

Por eso el reporte separa tres cosas distintas y no las mezcla:

| | qué es | por qué duele |
|---|---|---|
| **vence pronto** | tiene fecha y está cerca | lo que ya sabés |
| **sin computar** | no tiene fecha porque nadie la calculó | lo que no sabés que no sabés |
| **bloqueado** | no se puede calcular: falta jurisdicción o fuero en la `FICHA.md` | el que parece un problema administrativo y es un plazo escondido |

## Procedimiento

**1. Escaneá lo mecánico.**

```bash
node componentes/vigia/escanear.mjs
```

Recorre `casos/`, lee cada `FICHA.md` y cada `plazos.md`, y devuelve el estado.
Con `--json` sale estructurado; con `--dias N` cambiás la ventana de "vence
pronto", que por defecto son 15 días corridos.

**El script no computa ningún plazo, a propósito.** Sólo lee fechas ya escritas
y cuenta días corridos hasta ellas para ordenar la urgencia. Restar días hábiles
sin saber la jurisdicción, la feria y las acordadas del tribunal sería adivinar
justo lo que la regla del proyecto prohíbe adivinar.

**2. Los bloqueados van primero.**

Un caso sin jurisdicción o sin fuero en la ficha **no tiene un problema de
formulario: tiene todos sus plazos sin computar y no lo parece.** Antes de
seguir, pedile al defensor esos datos. No los deduzcas del nombre del juzgado ni
del tipo de carátula.

**3. Recién ahí, computá lo que falta.**

Para cada fila `🔲`, activá **`argentina-plazos`** con la jurisdicción y el fuero
de la ficha. Esa skill pregunta lo que le falte; dejala preguntar. Escribí el
resultado en la `plazos.md` del caso, con la norma fuente en la columna de tipo.

**4. Marcá lo que quede sin verificar.**

Si computaste un vencimiento pero no pudiste confirmar la feria o una acordada
del tribunal en SAIJ, la fila queda `🔲` con la nota de qué falta verificar. Un
plazo computado a medias que figura como cerrado es peor que uno sin computar,
porque deja de aparecer en este reporte.

## Cuándo correrla

Al empezar la semana, al volver de la feria, antes de tomarte licencia, y cuando
recibís un expediente de otro defensor. Los cuatro son momentos donde el estado
real de los plazos y el que uno recuerda se separan.

## Lo que esta skill no hace

**No manda avisos ni recordatorios.** Reporta cuando la corrés. Un sistema que
avisa solo hace falta y es la evolución natural, pero necesita un lugar donde
correr programado; mientras tanto, esto se corre y se lee.

**No abre el expediente electrónico.** Lee lo que está escrito en `casos/`. Si
el juzgado notificó algo que nadie cargó, el vigía no lo sabe. Sincronizar con
el sistema del Poder Judicial es otro trabajo.

**No decide qué es urgente.** Ordena por fecha y marca lo fatal con 🔴 si vos lo
marcaste. La prioridad entre dos plazos del mismo día la pone el defensor.
Loading
Loading