@ingeniomaps/cauce 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/README.md +6 -6
- package/automatization/AGENTS.md +1 -1
- package/automatization/runners/antigravity/rules/cauce.md +1 -1
- package/automatization/runners/claude/CLAUDE.md +1 -1
- package/automatization/runners/codex/AGENTS.md +1 -1
- package/automatization/runners/gemini/GEMINI.md +1 -1
- package/automatization/shared/skills/autobuild/SKILL.md +1 -1
- package/automatization/workflows/autobuild.js +44 -8
- package/engine/cli/archive.js +87 -0
- package/engine/cli/args.js +6 -2
- package/engine/cli/catalog.js +9 -1
- package/engine/cli/claims.js +125 -0
- package/engine/cli/ops.js +15 -4
- package/engine/cli/planning.js +110 -107
- package/engine/cli/worktree.js +89 -0
- package/engine/core/ownership.js +13 -5
- package/engine/core/repos.js +67 -0
- package/engine/hooks/files.js +3 -2
- package/engine/planning/adoption.js +1 -1
- package/engine/planning/claims.js +153 -0
- package/engine/planning/contracts.js +43 -202
- package/engine/planning/parser.js +51 -12
- package/engine/planning/state.js +39 -6
- package/engine/planning/structure.js +220 -0
- package/package.json +1 -1
- package/template/.gitattributes +19 -0
- package/template/AGENTS.md +47 -5
- package/template/automatization/AGENTS.md +1 -1
- package/template/gitignore +9 -0
- package/template/planning/BACKLOG.md +5 -0
- package/template/planning/FLOW.md +1 -1
- package/template/planning/PROTOCOL.md +20 -8
- package/template/planning/README.md +3 -3
- package/template/planning/RECURRING.md +1 -1
- package/template/planning/adr/system/OPS-001-planificacion-como-fuente-de-verdad.md +3 -2
- package/template/planning/business-rules/system/BR-OPS-001-una-sola-tarea-activa.md +7 -4
- package/template/planning/business-rules/system/BR-OPS-005-una-tarea-un-runner.md +44 -0
- package/template/planning/claims/README.md +70 -0
- package/template/planning/delivery/README.md +1 -0
- package/template/planning/delivery/multi-repo.md +11 -0
- package/template/planning/delivery/teamwork.md +162 -0
- package/template/planning/done/README.md +41 -0
- package/template/planning/wip/README.md +49 -0
- package/template/planning/DONE.md +0 -13
- package/template/planning/WIP.md +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,145 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.70.0] - 2026-09-08
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **`planning/claims/`: quién tomó qué, para que dos runners no construyan lo mismo.** Un archivo por tarea
|
|
22
|
+
tomada, con el slug de la tarea como nombre: `ops claim planning <tarea>` lo crea y `ops release` lo borra.
|
|
23
|
+
`ops context` deja de ofrecer una tarea con reclamo ajeno —antes le entregaba la misma a los dos y ninguno
|
|
24
|
+
se enteraba—, nombra quién la tiene y devuelve antes lo que vos reclamaste que lo que está libre.
|
|
25
|
+
|
|
26
|
+
Es un archivo por tarea y no uno por persona a propósito: así dos personas en tareas distintas no tocan
|
|
27
|
+
nunca el mismo archivo, y dos que toman la misma chocan en git, que es donde el choque significa algo.
|
|
28
|
+
`check` rechaza el reclamo que nombra una tarea que no existe, avisa a los tres días de tomada y avisa
|
|
29
|
+
cuando hay dos reclamos sobre el mismo `service:` — avisa y no frena, porque frenar serializaría a un
|
|
30
|
+
equipo entero sobre un servicio.
|
|
31
|
+
|
|
32
|
+
El reclamo distingue `owner` —la persona, a quién preguntarle— de `runner` —el agente que la hace—, y
|
|
33
|
+
lo segundo es lo que decide de quién es una tarea. Con varios agentes en una máquina la persona es la
|
|
34
|
+
misma y el árbol de trabajo no: sin esa distinción, el segundo agente tomaría por propia la tarea del
|
|
35
|
+
primero. Se crea con exclusión —el archivo se abre en modo exclusivo, así que dos reclamos simultáneos
|
|
36
|
+
no se pisan— y un runner lleva una tarea a la vez.
|
|
37
|
+
|
|
38
|
+
**Lo que te pide algo**: el reclamo hay que commitearlo y empujarlo — sin eso, el otro runner lee lo
|
|
39
|
+
que hay en su copia y la reserva no existe para nadie más—. Y si corrés varios agentes en la misma
|
|
40
|
+
máquina, cada uno exporta `CAUCE_RUNNER` con un valor propio.
|
|
41
|
+
|
|
42
|
+
- **`autobuild` reserva la tarea antes de construirla y la suelta al cerrarla.** Es lo que hace que dos
|
|
43
|
+
corridas en paralelo dejen de trabajar lo mismo: entre preguntar qué toca y reservarlo hay una ventana, y
|
|
44
|
+
perder esa carrera no frena la corrida — relee y sigue con la que quedó libre.
|
|
45
|
+
|
|
46
|
+
- **El aviso de reclamo viejo mira si la rama avanzó, no cuánto hace que se tomó.** El tiempo transcurrido
|
|
47
|
+
no distingue una tarea larga de una abandonada, y equivocarse cuesta en los dos sentidos: apurar a quien
|
|
48
|
+
está trabajando, o dejar bloqueada para siempre la tarea de quien se fue. Ahora `check` mira el último
|
|
49
|
+
commit **propio** de `task/<tarea>` —los que no están en el tronco, porque una rama recién creada hereda
|
|
50
|
+
su historia entera y sin esa distinción toda rama parecería haber avanzado el día que se creó—: tres días
|
|
51
|
+
sin ninguno avisan, y una tarea que recibe commits no se apura nunca
|
|
52
|
+
aunque lleve semanas tomada. Sin repositorio resoluble el aviso vuelve a mirar sólo la fecha — degrada a
|
|
53
|
+
lo que había, no rompe.
|
|
54
|
+
|
|
55
|
+
- **`ops context --hito <slug>` acota la cola a un hito.** Es la forma más barata de que dos personas o dos
|
|
56
|
+
agentes no se crucen: en hitos distintos casi nunca dependen entre sí ni tocan los mismos archivos. Lo
|
|
57
|
+
que se acota es qué se ofrece, no qué se sabe — una dependencia que vive en otro hito se sigue juzgando
|
|
58
|
+
igual—, y un hito mal escrito lo dice en vez de contestar «sin tarea disponible», que es indistinguible
|
|
59
|
+
de un hito terminado.
|
|
60
|
+
|
|
61
|
+
- **El plan en vuelo es uno por runner: `planning/wip/<runner>.md`.** Con `mode: sidecar` hay un solo
|
|
62
|
+
`planning/` por máquina, así que un plan compartido lo escribían todos los agentes que corren ahí: el
|
|
63
|
+
segundo pisaba el del primero, y `ops context` le entregaba la tarea que el primero estaba construyendo
|
|
64
|
+
—con el plan ajeno adentro y diciéndole que estaba libre—. `context` honra sólo el tuyo y `check` los
|
|
65
|
+
recorre todos.
|
|
66
|
+
|
|
67
|
+
**Lo que te pide algo**: `planning/WIP.md` se retiró. Mové tu plan a `planning/wip/<runner>.md` —el
|
|
68
|
+
nombre sale de tu `CAUCE_RUNNER`, aplanado; `ops context --json` lo dice en `wipFile`— y borrá el
|
|
69
|
+
archivo viejo, que mientras esté `ops check` lo nombra. El `.gitignore` nuevo excluye `planning/wip/*.md`
|
|
70
|
+
y conserva su README; si venías con la línea de `planning/WIP.md`, cambiala.
|
|
71
|
+
|
|
72
|
+
- **Un `service:` ambiguo entre varias raíces se nombra en vez de elegirse.** Con más de un
|
|
73
|
+
`workspaceRoots`, un servicio que existe en dos —`.` existe en todas— resolvía al primero: el árbol de
|
|
74
|
+
trabajo terminaba en el repositorio que no era, y el aviso de avance miraba las ramas de otro. Ahora
|
|
75
|
+
`ops worktree` nombra los candidatos y se niega, y el aviso degrada a mirar sólo la fecha.
|
|
76
|
+
|
|
77
|
+
- **`ops worktree` avisa cuando la instancia está embebida.** Con `mode: embedded` cada árbol se lleva su
|
|
78
|
+
propia copia de `planning/`, así que los reclamos de un agente no los ve el otro hasta mergear y la
|
|
79
|
+
coordinación entre varios deja de existir sin que nada falle. No lo frena: un árbol por rama con un solo
|
|
80
|
+
agente es un uso legítimo.
|
|
81
|
+
|
|
82
|
+
- **`ops runners <planning>`: qué runners tienen trabajo abierto, para que un agente pueda preguntar.**
|
|
83
|
+
Elegir con qué runner se arranca es lo primero de una sesión y `ops context` no lo contesta: responde
|
|
84
|
+
«qué hago» para un runner ya elegido. Sin esa lista, un agente se inventa un id y deja huérfano el
|
|
85
|
+
trabajo de ayer, o se lo pisa a otro que sigue corriendo.
|
|
86
|
+
|
|
87
|
+
**Lo que te pide algo**: nada, y es el punto. `AGENTS.md` le dice al runner que mire esa lista al abrir
|
|
88
|
+
la sesión, que **pregunte** cuál se retoma o si arranca uno nuevo, y que **exporte el id él mismo**. A
|
|
89
|
+
una persona no se le pide que escriba una variable de entorno.
|
|
90
|
+
|
|
91
|
+
- **Tu propio reclamo desde otro runner se reconoce en vez de resolverse solo.** Volver al día siguiente
|
|
92
|
+
sin reponer `CAUCE_RUNNER` y correr un segundo agente tuyo se ven idénticos desde el archivo, y las dos
|
|
93
|
+
salidas automáticas rompen trabajo: retomar sola le saca la tarea al otro agente, y crear un runner
|
|
94
|
+
nuevo deja dos construyendo lo mismo. `ops claim` dice cuál es cuál y con qué id se retoma; `ops context`
|
|
95
|
+
marca esas tareas como «vos, desde otro runner».
|
|
96
|
+
|
|
97
|
+
- **La evidencia de una tarea cerrada vive en su propio archivo: `planning/done/<slug>.md`.** Cerrar es lo
|
|
98
|
+
que más se hace, y mientras la evidencia se acumulaba en un `DONE.md` compartido, cerrar era agregarle
|
|
99
|
+
una entrada a algo que otro también estaba tocando. Ahora dos personas —o dos agentes— que cierran a la
|
|
100
|
+
vez escriben archivos distintos: no hay conflicto que resolver ni regla de merge que aplicar.
|
|
101
|
+
|
|
102
|
+
La entrada declara `fecha:`, la del cierre. Mientras vivían en un archivo, «la última» era la última del
|
|
103
|
+
archivo; con archivos sueltos el orden lo daría el listado del directorio, que es alfabético, y la
|
|
104
|
+
respuesta equivocada se leería igual de bien que la correcta. `ops evidence` sin `--task` ordena por ese
|
|
105
|
+
campo, y el contrato de una entrada está en `planning/done/README.md`.
|
|
106
|
+
|
|
107
|
+
**Lo que te pide algo**: `planning/DONE.md` se retiró. Pasá cada entrada a su propio
|
|
108
|
+
`planning/done/<slug>.md` con su `fecha:` y borrá el archivo — mientras esté, `ops check` lo dice en vez
|
|
109
|
+
de ignorarlo, porque un `DONE.md` que ya nadie lee deja a sus épicas sin poder cerrar y a sus historias
|
|
110
|
+
figurando sin evidencia.
|
|
111
|
+
|
|
112
|
+
- **`ops archive <NNN>` se retiró; `ops archive human-actions` se queda.** Archivar una épica existía para
|
|
113
|
+
descongestionar un `DONE.md` que se hinchaba con una entrada por tarea; con un archivo por tarea no hay
|
|
114
|
+
nada que descongestionar, y mover esos archivos a una carpeta por épica sería reintroducir el movimiento
|
|
115
|
+
que esto vino a sacar. El comando lo dice, en vez de contestar «la épica debe ser NNN».
|
|
116
|
+
|
|
117
|
+
- **`(depende: slug)` en una línea de tarea: lo que sigue no se le ofrece a otro.** El orden del BACKLOG
|
|
118
|
+
era la dependencia y alcanzaba mientras hubiera un runner; con dos, el segundo toma la que sigue mientras
|
|
119
|
+
el primero construye aquella de la que depende, y las dos ramas se pisan al integrar. Una tarea con
|
|
120
|
+
dependencias sin cerrar no se ofrece ni se puede tomar, y `context` la muestra con una línea `WAIT` que
|
|
121
|
+
nombra la dependencia y quién la tiene — una cola trabada no se lee como una cola vacía. `check` rechaza
|
|
122
|
+
la dependencia que no existe y nombra el ciclo entero cuando lo hay.
|
|
123
|
+
|
|
124
|
+
- **`ops worktree <planning> <tarea>`: un árbol de trabajo por agente, sin clonar el repositorio.** Resuelve
|
|
125
|
+
en qué raíz de `workspaceRoots` vive el `service:` de la tarea, crea la rama `task/<slug>` y el árbol al
|
|
126
|
+
lado, y devuelve la ruta con el `export CAUCE_RUNNER` ya escrito. `git worktree` comparte el mismo `.git`
|
|
127
|
+
y el mismo historial, así que no hay una segunda copia del repositorio: lo que hay es un segundo
|
|
128
|
+
directorio de archivos fijado a su rama, y por eso **ningún agente hace `checkout`** sobre el trabajo de
|
|
129
|
+
otro. Repetirlo devuelve el árbol que ya existe.
|
|
130
|
+
|
|
131
|
+
- **`.gitattributes`: `DONE.md` y `HUMAN_ACTIONS.md` se concatenan en vez de conflictuar.** Dos personas
|
|
132
|
+
cerrando trabajo el mismo día chocaban siempre, y ese conflicto no significaba nada: las dos entradas son
|
|
133
|
+
buenas y van las dos. Lo que `union` no hace es deduplicar, y esa falla ya la atrapa `DONE duplicado`.
|
|
134
|
+
|
|
135
|
+
- **`planning/delivery/teamwork.md`**: qué comparte el equipo y qué no, por qué dos agentes necesitan un
|
|
136
|
+
`git worktree` cada uno, cómo repartir trabajo, y qué se rompe primero según el tamaño del equipo.
|
|
137
|
+
|
|
138
|
+
- **BR-OPS-005 — una tarea, un runner.** La contracara de BR-OPS-001: aquélla impide que un runner lleve dos
|
|
139
|
+
tareas, ésta que dos runners lleven la misma.
|
|
140
|
+
|
|
141
|
+
### Cambiado
|
|
142
|
+
|
|
143
|
+
- **`planning/WIP.md` pasa a ser local y deja de viajar por git.** Existe para recuperar la sesión de quien
|
|
144
|
+
lo escribió —nadie más puede retomarla— y cambia en cada paso, así que compartirlo era un conflicto por
|
|
145
|
+
commit a cambio de nada. `check` deja de exigir que exista: ausente se lee como IDLE, que es lo que
|
|
146
|
+
significa, y un clon nuevo ya no falla por no traerlo.
|
|
147
|
+
|
|
148
|
+
**Lo que te pide algo**: el molde nuevo lo gitignorea, pero tu `.gitignore` es tuyo y `upgrade` no lo toca.
|
|
149
|
+
Para aprovecharlo, agregale `planning/WIP.md` y sacalo del índice con `git rm --cached planning/WIP.md`.
|
|
150
|
+
Sin hacer nada, todo sigue funcionando como antes.
|
|
151
|
+
|
|
152
|
+
- **BR-OPS-001 se acota al runner.** Decía que WIP es el mutex sin decir de quién, y con equipo eso se leía
|
|
153
|
+
como «trabaja uno por vez». Ahora dice que un runner no toma dos tareas; que dos runners no tomen la misma
|
|
154
|
+
es BR-OPS-005.
|
|
155
|
+
|
|
17
156
|
## [0.69.0] - 2026-09-07
|
|
18
157
|
|
|
19
158
|
### Agregado
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ el contexto de cada empresa vive en su propia instancia.
|
|
|
7
7
|
## Qué resuelve
|
|
8
8
|
|
|
9
9
|
- Una tarea tiene una sola fuente de verdad durante todo su ciclo de vida.
|
|
10
|
-
- Una sesión interrumpida se recupera desde `
|
|
10
|
+
- Una sesión interrumpida se recupera desde el plan del runner en `planning/wip/`, sin reconstruir la intención.
|
|
11
11
|
- Las ideas del agente no entran solas a la cola: quedan en `INBOX.md` hasta promoción humana.
|
|
12
12
|
- El trabajo que vuelve cada tanto se declara una vez en `RECURRING.md`; el CLI dice cuándo venció y
|
|
13
13
|
nadie lo encola solo.
|
|
@@ -164,8 +164,8 @@ cualquiera de esas formas.
|
|
|
164
164
|
## Flujo
|
|
165
165
|
|
|
166
166
|
```text
|
|
167
|
-
idea → INBOX → roadmap → BACKLOG →
|
|
168
|
-
aprobación
|
|
167
|
+
idea → INBOX → roadmap → BACKLOG → claim → WIP → done/<tarea>.md
|
|
168
|
+
aprobación reserva ejecución evidencia
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
1. Captura ideas, deuda o lecciones en `INBOX.md`.
|
|
@@ -173,9 +173,9 @@ idea → INBOX → roadmap → BACKLOG → WIP → DONE → done/epic-NNN.md
|
|
|
173
173
|
un recorrido —una etapa por dueño de decisión, con su exit gate— y dejar la épica candidata escrita;
|
|
174
174
|
si falta evidencia o autoridad, para y registra la acción humana en vez de suponer.
|
|
175
175
|
3. Promueve historias listas a un `## Hito` de `BACKLOG.md`.
|
|
176
|
-
4. Un runner
|
|
177
|
-
5. Tras Build, Review, Verify y QA,
|
|
178
|
-
6. Al cerrar la
|
|
176
|
+
4. Un runner reclama la tarea con `ops claim`, para que otro no la tome, y persiste su plan en `wip/<runner>.md`.
|
|
177
|
+
5. Tras Build, Review, Verify y QA, escribe la evidencia en `done/<slug>.md` y suelta el reclamo.
|
|
178
|
+
6. Al cerrar la última historia, la épica pasa a `closed`.
|
|
179
179
|
|
|
180
180
|
Lo que vuelve cada tanto —actualizar dependencias, revisar accesos, mirar el gasto del mes— entra por
|
|
181
181
|
un costado: se declara una vez en `RECURRING.md` con su cadencia, y `ops recurring planning` dice qué
|
package/automatization/AGENTS.md
CHANGED
|
@@ -65,6 +65,6 @@ cambia instalación o materialización, valida `ops init` en un directorio tempo
|
|
|
65
65
|
|
|
66
66
|
## Límites
|
|
67
67
|
|
|
68
|
-
No edites `planning/BACKLOG.md`, `
|
|
68
|
+
No edites `planning/BACKLOG.md`, `planning/wip/` ni `planning/done/` desde hooks o instaladores. No hagas que `ops init` active
|
|
69
69
|
un runner silenciosamente. No agregues lógica de negocio, nombres de servicios de un proyecto, tokens, rutas
|
|
70
70
|
personales ni modelos concretos a esta capa reusable.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Cauce
|
|
2
2
|
|
|
3
3
|
Lee y cumple `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/rules/system/` antes
|
|
4
|
-
de ejecutar trabajo. `{{OPS_DIR}}planning/
|
|
4
|
+
de ejecutar trabajo. `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex de
|
|
5
5
|
ejecución y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
|
|
6
6
|
inventes aprobaciones o credenciales y no hagas push ni deploy. Cierra cada tarea con verificación real y
|
|
7
7
|
evidencia en DONE.
|
|
@@ -20,5 +20,5 @@ misma pregunta más caro. El arranque busca entender qué es el proyecto y dejar
|
|
|
20
20
|
intención es viable y propone una épica, y `/autobuild` ejecuta trabajo ya promovido; `/integration-sync` e
|
|
21
21
|
`/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
|
|
22
22
|
|
|
23
|
-
Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/
|
|
23
|
+
Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/wip/<runner>.md`. Si el protocolo y
|
|
24
24
|
un workflow difieren, manda el protocolo y la diferencia se registra en `{{OPS_DIR}}planning/INBOX.md`.
|
|
@@ -15,7 +15,7 @@ Los cinco recorridos llegan como skills en `.agents/skills/` y se invocan con `$
|
|
|
15
15
|
acá —son referencia, no un runtime compatible—, así que cada recorrido se ejecuta fase por fase
|
|
16
16
|
siguiendo el protocolo.
|
|
17
17
|
|
|
18
|
-
`{{OPS_DIR}}planning/
|
|
18
|
+
`{{OPS_DIR}}planning/wip/<runner>.md` es el mutex: una sola tarea activa. `{{OPS_DIR}}planning/AWAITING_REVIEW.md`
|
|
19
19
|
bloquea una corrida nueva hasta que un humano revise. Nada se promueve desde `INBOX.md` sin aprobación.
|
|
20
20
|
|
|
21
21
|
Antes de cerrar, corré `node {{OPS_DIR}}tools/ops.js check {{OPS_DIR}}planning`.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
@{{OPS_DIR}}planning/rules/system/conduct.md
|
|
9
9
|
|
|
10
10
|
`{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/cauce:autobuild` fase por fase; los
|
|
11
|
-
workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/
|
|
11
|
+
workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex
|
|
12
12
|
y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
|
|
13
13
|
|
|
14
14
|
Los hooks de `.gemini/settings.json` son obligatorios y bloquean por su cuenta. **Requieren que la
|
|
@@ -3,7 +3,7 @@ name: autobuild
|
|
|
3
3
|
description: Ejecuta el siguiente hito aprobado siguiendo el protocolo Cauce.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Lee completos `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/
|
|
6
|
+
Lee completos `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/wip/`. Si existe
|
|
7
7
|
`{{OPS_DIR}}planning/AWAITING_REVIEW.md`, detente y explica la acción humana pendiente. Toma solamente la primera tarea
|
|
8
8
|
aprobada, persiste el estado en WIP y ejecuta Build, Review, Verify y QA en orden. Usa los comandos reales del
|
|
9
9
|
servicio, registra evidencia verificable en DONE y detente en el checkpoint entre hitos. No hagas push ni
|
|
@@ -21,7 +21,7 @@ export const meta = {
|
|
|
21
21
|
{ title: 'Verify', detail: 'Los gates del servicio y la aceptación que ninguna prueba codifica' },
|
|
22
22
|
{ title: 'QA', detail: 'El comportamiento ejercitado como lo ve quien lo usa' },
|
|
23
23
|
{ title: 'Commit', detail: 'Conventional Commits, uno por naturaleza del diff, sin push' },
|
|
24
|
-
{ title: 'Done', detail: 'Cierre atómico
|
|
24
|
+
{ title: 'Done', detail: 'Cierre atómico: evidencia escrita, cola y plan limpios, reserva suelta' },
|
|
25
25
|
{ title: 'Closing', detail: 'Check de planning y checkpoint humano del hito' },
|
|
26
26
|
],
|
|
27
27
|
}
|
|
@@ -31,8 +31,8 @@ const CONFIG = `${ROOT}/ops.config.json`
|
|
|
31
31
|
const P = `${ROOT}/planning`
|
|
32
32
|
const ORG = `${ROOT}/organization`
|
|
33
33
|
const BACKLOG = `${P}/BACKLOG.md`
|
|
34
|
-
|
|
35
|
-
const
|
|
34
|
+
// Una tarea cerrada escribe su propio archivo, así que dos corridas en paralelo no comparten ninguno.
|
|
35
|
+
const doneFile = (slug) => `${P}/done/${slug}.md`
|
|
36
36
|
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
37
37
|
const GATE = `${P}/AWAITING_REVIEW.md`
|
|
38
38
|
const ROADMAP = `${P}/roadmap`
|
|
@@ -58,8 +58,21 @@ const CONTEXT = {
|
|
|
58
58
|
properties: { build: { type: 'string' }, review: { type: 'array', items: { type: 'string' } } },
|
|
59
59
|
},
|
|
60
60
|
blockedTasks: { type: 'array', items: { type: 'string' } },
|
|
61
|
+
// Si la tarea que `context` devolvió ya está reservada a nombre de este runner. Libre no significa
|
|
62
|
+
// que sea nuestra: significa que todavía la puede tomar cualquiera, y dos corridas en paralelo la
|
|
63
|
+
// reciben las dos.
|
|
64
|
+
claimed: { type: 'boolean' },
|
|
65
|
+
// La fecha de hoy según el motor. Este recorrido no tiene reloj propio a propósito.
|
|
66
|
+
today: { type: 'string' },
|
|
67
|
+
// Dónde va el plan de este runner. El nombre sale de su id y el recorrido no lo deriva: lo
|
|
68
|
+
// pregunta, igual que la fecha.
|
|
69
|
+
wipFile: { type: 'string' },
|
|
61
70
|
},
|
|
62
71
|
}
|
|
72
|
+
const CLAIM = {
|
|
73
|
+
type: 'object', additionalProperties: false, required: ['claimed'],
|
|
74
|
+
properties: { claimed: { type: 'boolean' }, details: { type: 'string' } },
|
|
75
|
+
}
|
|
63
76
|
const EXPANSION = {
|
|
64
77
|
type: 'object', additionalProperties: false, required: ['expanded'],
|
|
65
78
|
properties: { expanded: { type: 'boolean' }, hito: { type: 'string' }, reason: { type: 'string' } },
|
|
@@ -311,7 +324,10 @@ const write = (prompt, options = {}) => agent(`${LEDGER}\n\n${prompt}`, options)
|
|
|
311
324
|
// WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
|
|
312
325
|
const readContext = () => read(
|
|
313
326
|
`Corré "node tools/ops.js context ${P} --json" desde ${ROOT} y reportá sólo lo que imprimió. Derivá hasTask ` +
|
|
314
|
-
`de si task es null, wipActive de si wip es null
|
|
327
|
+
`de si task es null, wipActive de si wip es null, claimed del campo claimed, today y wipFile de sus ` +
|
|
328
|
+
`campos, y lane ` +
|
|
329
|
+
`de task.tier; copiá slug, ` +
|
|
330
|
+
`hito, service, acceptance, ` +
|
|
315
331
|
`epic y cast de task, y epicContext de epic.context —vacío si no hay épica—. El comando es la fuente de ` +
|
|
316
332
|
`verdad: no abras archivos de planning para completarlo.`,
|
|
317
333
|
{ schema: CONTEXT, label: 'planning-context' },
|
|
@@ -351,6 +367,24 @@ while (rounds++ < MAX_TASKS) {
|
|
|
351
367
|
id: planning.slug, hito: planning.hito, service: planning.service,
|
|
352
368
|
acceptance: planning.acceptance, epic: planning.epic, epicContext: planning.epicContext || '',
|
|
353
369
|
}
|
|
370
|
+
// Reservar antes de construir, y antes de fijar el hito de la corrida. Sin esto dos corridas en
|
|
371
|
+
// paralelo trabajan la misma tarea: `context` sólo puede saltear lo que alguien ya reclamó, y el
|
|
372
|
+
// primero en preguntar todavía no reclamó nada. La ventana entre preguntar y reservar existe igual, y
|
|
373
|
+
// por eso perder la carrera no es un error: se relee y se sigue con la que quedó libre.
|
|
374
|
+
if (!planning.claimed && !planning.wipActive) {
|
|
375
|
+
phase('Claim')
|
|
376
|
+
const reserva = await write(
|
|
377
|
+
`Corré "node tools/ops.js claim ${P} ${task.id}" desde ${ROOT}. No escribas ningún archivo vos: lo ` +
|
|
378
|
+
`escribe el comando. claimed=true sólo con exit 0; si falla porque la tomó otro, claimed=false y ` +
|
|
379
|
+
`copiá el mensaje en details.`,
|
|
380
|
+
{ schema: CLAIM, label: `claim:${task.id}` },
|
|
381
|
+
)
|
|
382
|
+
if (!reserva || !reserva.claimed) {
|
|
383
|
+
planning = await readContext()
|
|
384
|
+
if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
}
|
|
354
388
|
currentMilestone = task.hito
|
|
355
389
|
|
|
356
390
|
// Ver la tarea y, si no está clasificada, clasificarla antes de ejecutarla. Se hace una vez por
|
|
@@ -535,7 +569,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
535
569
|
`dio; recién después implementá. Un test que pasa antes de que exista el código no asercia lo que dice ` +
|
|
536
570
|
`aserciar: endurecelo y volvé a correr hasta verlo fallar. Corré las pruebas que necesites para ver ese ` +
|
|
537
571
|
`rojo y ese verde, y nada más: los gates completos, el QA, el commit y el cierre son fases posteriores, ` +
|
|
538
|
-
`así que no toques ${
|
|
572
|
+
`así que no toques ${P}/done/ ni ${BACKLOG} ni el status del WIP. Lo que el plan no previó va en discovered y ` +
|
|
539
573
|
`no en el código a secas: kind=edge si esta tarea lo puede fijar —y entonces entra con su prueba, que ` +
|
|
540
574
|
`nombrás en test y anotás en redFirst—, kind=open si lo notaste y no impide entregar la aceptación: se ` +
|
|
541
575
|
`registra para que lo decida quien corresponde y el recorrido sigue. Si de verdad no podés entregar sin ` +
|
|
@@ -693,9 +727,11 @@ while (rounds++ < MAX_TASKS) {
|
|
|
693
727
|
|
|
694
728
|
phase('Done')
|
|
695
729
|
await write(
|
|
696
|
-
`Cerrá ${task.id} de forma atómica:
|
|
697
|
-
`tests y commit
|
|
698
|
-
`
|
|
730
|
+
`Cerrá ${task.id} de forma atómica: escribí ${doneFile(task.id)} con su evidencia —acept, ` +
|
|
731
|
+
`fecha: ${planning.today}, done, qa, tests y commit, en el formato de entrada que trae este preámbulo—; ` +
|
|
732
|
+
`sacala junto con sus notas indentadas de ${BACKLOG}; cerrá su épica sólo si no queda ` +
|
|
733
|
+
`ninguna tarea etiquetada; dejá ${P}/${planning.wipFile} en status IDLE; y soltá la reserva corriendo ` +
|
|
734
|
+
`"node tools/ops.js release ${P} ${task.id}". En decisions no nombres una fase ni un cargo ` +
|
|
699
735
|
`que no figure en estos hechos. Hechos: lane=${planning.lane || 'sin clasificar'}; ` +
|
|
700
736
|
`review=${reviewFact}; fases=${ran.join(' → ')}; build=${build.summary}; ` +
|
|
701
737
|
`verify=${JSON.stringify(verified.commands)}; qa=${qa.evidence}; commit=${commit.hash || commit.reason}.`,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Los comandos que mueven estado ya decidido: archivar lo que cerró y declarar de una vez qué historia
|
|
4
|
+
// llegó con el proyecto. Viven aparte de `planning.js` porque son la otra mitad de una costura: aquéllos
|
|
5
|
+
// leen y juzgan sin mutar nada, éstos escriben. Se separaron cuando el archivo cruzó las 500 líneas, y
|
|
6
|
+
// lo que decidió el corte fue eso y no el número.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
const P = require('../planning/parser')
|
|
11
|
+
const PC = require('../planning/contracts')
|
|
12
|
+
const AD = require('../planning/adoption')
|
|
13
|
+
const F = require('../core/files')
|
|
14
|
+
const { fail } = require('./io')
|
|
15
|
+
|
|
16
|
+
// La fecha de hoy, la misma que usan los comandos que leen.
|
|
17
|
+
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
18
|
+
|
|
19
|
+
// El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
|
|
20
|
+
// a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
|
|
21
|
+
// cerró ninguna —que es justo cuando el archivo se vuelve ilegible—.
|
|
22
|
+
// Adoptar es declarar de una vez qué historia llegó con el proyecto. Se genera con lo que hoy no cumple
|
|
23
|
+
// y no se vuelve a correr: un baseline que se regenera perdona de nuevo lo que alguien ya se tomó el
|
|
24
|
+
// trabajo de arreglar, y uno que crece a mano deja de ser una lista de perdones para ser una amnistía.
|
|
25
|
+
// Achicarlo sí es a mano, borrando el renglón que `check` señala.
|
|
26
|
+
function adopt(dir) {
|
|
27
|
+
const root = path.resolve(dir || '.')
|
|
28
|
+
const target = path.join(root, AD.BASELINE)
|
|
29
|
+
if (fs.existsSync(target)) {
|
|
30
|
+
// Un baseline que ya trae huella no se toca: regenerarlo es exactamente lo que la huella impide.
|
|
31
|
+
// Uno sin huella lo generó una versión anterior, y sellarlo no es regenerar nada — se calcula sobre
|
|
32
|
+
// lo que ya está—, así que es la única salida de un aviso que si no no tendría ninguna.
|
|
33
|
+
const existing = fs.readFileSync(target, 'utf8')
|
|
34
|
+
if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
|
|
35
|
+
fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
|
|
36
|
+
+ 'delante; `check` marca los que ya cumplen.')
|
|
37
|
+
}
|
|
38
|
+
const slugs = AD.declared(existing)
|
|
39
|
+
F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
|
|
40
|
+
+ `sha256:${AD.digest(slugs)}\n`))
|
|
41
|
+
return console.log(`✓ ${AD.BASELINE} sellado con ${slugs.length} entrada(s); la lista no cambió`)
|
|
42
|
+
}
|
|
43
|
+
const epics = P.readEpics(root)
|
|
44
|
+
const pending = P.readDone(root).entries.filter((entry) => PC.doneEntryErrors(entry, epics).length)
|
|
45
|
+
if (!pending.length) {
|
|
46
|
+
return console.log('= no hay nada que exentar: todas las entradas de DONE cumplen el contrato')
|
|
47
|
+
}
|
|
48
|
+
const today = TODAY()
|
|
49
|
+
const slugs = pending.map((entry) => entry.slug)
|
|
50
|
+
F.atomicWrite(target, `# Entradas anteriores a la adopción de Cauce (${today}). No se agregan nuevas:\n`
|
|
51
|
+
+ '# desde esa fecha rige el contrato completo, y `check` avisa cuando una de éstas pasa a\n'
|
|
52
|
+
+ '# cumplirlo para que le pongas `#~` delante y quede retirada.\n'
|
|
53
|
+
+ `# huella: ${slugs.length} entradas · sha256:${AD.digest(slugs)}\n`
|
|
54
|
+
+ `${slugs.join('\n')}\n`)
|
|
55
|
+
console.log(`✓ ${pending.length} entrada(s) exentas en ${AD.BASELINE}`)
|
|
56
|
+
return console.log(' revisá la lista: lo que sí cumple el contrato no tiene por qué estar ahí')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function archiveHumanActions(root) {
|
|
60
|
+
const source = path.join(root, 'HUMAN_ACTIONS.md')
|
|
61
|
+
const rows = P.readHumanActions(root).filter((row) => row.resolved)
|
|
62
|
+
if (!rows.length) return console.log('= no hay filas resueltas')
|
|
63
|
+
const target = path.join(root, 'done', 'human-actions.md')
|
|
64
|
+
const header = '| Tarea | Estado | Origen | Acción concreta y condición de desbloqueo |\n|---|---|---|---|'
|
|
65
|
+
const previous = P.read(target).trimEnd()
|
|
66
|
+
const head = previous || `---\nstatus: archived\n---\n\n# Acciones humanas resueltas\n\n${header}`
|
|
67
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
68
|
+
F.atomicWrite(target, `${head}\n${rows.map((row) => row.raw).join('\n')}\n`)
|
|
69
|
+
const drop = new Set(rows.map((row) => row.raw))
|
|
70
|
+
const kept = P.read(source).split('\n').filter((line) => !drop.has(line))
|
|
71
|
+
F.atomicWrite(source, `${kept.join('\n').trimEnd()}\n`)
|
|
72
|
+
return console.log(`✓ ${rows.length} fila(s) archivadas`)
|
|
73
|
+
}
|
|
74
|
+
// Archivar una épica se retiró en 0.71.0. Existía para descongestionar un `DONE.md` que se hinchaba con
|
|
75
|
+
// una entrada por tarea; con un archivo por tarea no hay nada que descongestionar, y mover esos archivos
|
|
76
|
+
// a una carpeta por épica sería reintroducir el movimiento que la mudanza vino a sacar.
|
|
77
|
+
//
|
|
78
|
+
// El comando se queda para lo que sí sigue archivándose, y para decirle a quien escriba el número que ya
|
|
79
|
+
// no hace falta: sin esto contestaría «La épica debe ser NNN», que manda a corregir la forma de algo que
|
|
80
|
+
// no existe.
|
|
81
|
+
function archive(dir, rawNum) {
|
|
82
|
+
if (String(rawNum || '') === 'human-actions') return archiveHumanActions(path.resolve(dir || '.'))
|
|
83
|
+
return fail('Sólo se archiva `human-actions`. La evidencia de una tarea ya vive en su propio archivo '
|
|
84
|
+
+ 'de `done/`, así que archivar una épica dejó de tener sentido.', 2)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { archive, adopt }
|
package/engine/cli/args.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Banderas que consumen el argumento siguiente: su valor no es un posicional.
|
|
8
8
|
const VALUED_FLAGS = new Set([
|
|
9
9
|
'--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
|
|
10
|
-
'--task', '--promote',
|
|
10
|
+
'--task', '--promote', '--hito',
|
|
11
11
|
])
|
|
12
12
|
|
|
13
13
|
// Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
|
|
@@ -19,8 +19,12 @@ const FLAGS = {
|
|
|
19
19
|
onboard: ['--json'],
|
|
20
20
|
check: ['--json'],
|
|
21
21
|
tree: ['--json', '--no-color'],
|
|
22
|
-
context: ['--json'],
|
|
22
|
+
context: ['--json', '--hito'],
|
|
23
23
|
recurring: ['--json', '--promote'],
|
|
24
|
+
claim: ['--json'],
|
|
25
|
+
runners: ['--json'],
|
|
26
|
+
worktree: ['--json'],
|
|
27
|
+
release: [],
|
|
24
28
|
evidence: ['--json', '--task'],
|
|
25
29
|
upgrade: ['--check', '--force'],
|
|
26
30
|
destroy: ['--force'],
|
package/engine/cli/catalog.js
CHANGED
|
@@ -115,7 +115,15 @@ function evaluationBench(root, agent, caso, force, kind) {
|
|
|
115
115
|
// sin pagar un `npm install` por corrida. El cargo llega a un banco donde el CLI funciona.
|
|
116
116
|
const scope = path.join(dir, 'node_modules', '@ingeniomaps')
|
|
117
117
|
fs.mkdirSync(scope, { recursive: true })
|
|
118
|
-
|
|
118
|
+
// El enlace se pisa por lo mismo que el andamiaje de arriba: a veces sobrevive al borrado del banco, y
|
|
119
|
+
// entonces crearlo corta la corrida con `EEXIST` en vez de rehacerlo. Es el único paso que no seguía esa
|
|
120
|
+
// regla, y el que falló en CI rehaciendo el mismo `11-otro` que ya tiene reintentos por esto.
|
|
121
|
+
//
|
|
122
|
+
// `rmSync` sobre un enlace lo quita a él y no a lo que apunta —que acá es la raíz del toolkit—, así que
|
|
123
|
+
// esto no puede llevarse por delante el repositorio.
|
|
124
|
+
const link = path.join(scope, 'cauce')
|
|
125
|
+
fs.rmSync(link, { force: true })
|
|
126
|
+
fs.symlinkSync(IN.PROJECT_ROOT, link, 'dir')
|
|
119
127
|
|
|
120
128
|
// El artefacto del caso, si lo tiene: la guía del proveedor que el pedido manda implementar, el CSV
|
|
121
129
|
// con instrucciones adentro. Entra antes del commit limpio a propósito — si entrara después, `status`
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Tomar y soltar una tarea, y ver quién tiene trabajo abierto. Los dos primeros son los únicos comandos
|
|
4
|
+
// del motor que escriben un archivo de coordinación, y escriben **el propio**: `BACKLOG.md` no lo toca
|
|
5
|
+
// ninguno, porque la cola es de lo aprobado y la escribe una persona.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const CL = require('../planning/claims')
|
|
10
|
+
const R = require('../core/repos')
|
|
11
|
+
const ST = require('../planning/state')
|
|
12
|
+
const { fail } = require('./io')
|
|
13
|
+
|
|
14
|
+
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
15
|
+
|
|
16
|
+
function claim(dir, slug, cli) {
|
|
17
|
+
const root = path.resolve(dir || '.')
|
|
18
|
+
if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`', 2)
|
|
19
|
+
const state = ST.snapshot(root)
|
|
20
|
+
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
21
|
+
if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`, 2)
|
|
22
|
+
|
|
23
|
+
// No se reserva lo que todavía no se puede empezar: una tarea tomada con su dependencia en vuelo
|
|
24
|
+
// bloquea la cola sin que nadie pueda avanzarla, y el runner que la tomó se queda sin poder tomar otra.
|
|
25
|
+
const falta = task.depends.find((dep) => !state.done.set.has(dep))
|
|
26
|
+
if (falta) return fail(`${slug} depende de ${falta}, que todavía no está en DONE.`)
|
|
27
|
+
|
|
28
|
+
const me = CL.owner(root)
|
|
29
|
+
const from = CL.runner()
|
|
30
|
+
if (!me) {
|
|
31
|
+
return fail('No sé quién sos. Configurá `git config user.email` o exportá CAUCE_OWNER: '
|
|
32
|
+
+ 'un reclamo anónimo no dice a quién preguntarle.', 2)
|
|
33
|
+
}
|
|
34
|
+
// Un runner lleva una tarea a la vez (BR-OPS-001), y exigirlo acá hace ruidosa la única forma en que
|
|
35
|
+
// este contrato falla en silencio: dos agentes de la misma máquina compartiendo id porque nadie puso
|
|
36
|
+
// `CAUCE_RUNNER`. Sin esto, el segundo se llevaría la tarea del primero creyéndola suya.
|
|
37
|
+
//
|
|
38
|
+
// La que se está pidiendo queda afuera del conteo: volver a pedir la propia es reintentar, no llevar dos.
|
|
39
|
+
const ocupado = state.claims
|
|
40
|
+
.find((one) => one.runner === from && one.slug !== slug && !state.done.set.has(one.slug))
|
|
41
|
+
if (ocupado) {
|
|
42
|
+
return fail(`este runner ya tiene ${ocupado.slug}. Cerrala o soltala primero; y si sos otro agente `
|
|
43
|
+
+ 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const target = CL.file(root, slug)
|
|
47
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
48
|
+
const cuerpo = CL.content({ task: slug, owner: me, runner: from, started: TODAY(), service: task.service })
|
|
49
|
+
try {
|
|
50
|
+
// Reservar **es** crear el archivo, así que el único juez de quién la tiene es el archivo. `wx` falla
|
|
51
|
+
// si ya está, y de ahí sale la respuesta entera: propia, ajena o perdida en la carrera.
|
|
52
|
+
//
|
|
53
|
+
// Sin `wx` no habría reserva: `atomicWrite` renombra encima, y dos agentes que arrancan con segundos
|
|
54
|
+
// de diferencia ganarían los dos sin que ninguno se entere. Y una comprobación previa tampoco
|
|
55
|
+
// alcanzaría —entre mirar y escribir queda la misma ventana—, así que sería un segundo juez que
|
|
56
|
+
// adelanta un veredicto que este bloque tiene que volver a dar igual.
|
|
57
|
+
fs.writeFileSync(target, cuerpo, { flag: 'wx' })
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code !== 'EEXIST') throw error
|
|
60
|
+
const dueño = CL.read(root).find((one) => one.slug === slug)
|
|
61
|
+
// Existía al crear y ya no está: alguien la soltó entre las dos operaciones. Es una ventana de
|
|
62
|
+
// microsegundos y aun así tiene respuesta, porque la alternativa es reventar con un TypeError.
|
|
63
|
+
if (!dueño) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`)
|
|
64
|
+
if (dueño.runner === from) return console.log(`= ${slug} ya era tuya desde ${dueño.started}`)
|
|
65
|
+
// Mismo dueño y otro runner son dos situaciones que se ven idénticas desde acá —vos retomando la
|
|
66
|
+
// sesión de ayer, o un segundo agente tuyo— y ninguna se puede distinguir mirando el archivo.
|
|
67
|
+
// Retomarla sola le sacaría la tarea al otro agente; crear un runner nuevo dejaría dos trabajando lo
|
|
68
|
+
// mismo. Las dos rompen trabajo, así que decide una persona y acá sólo se dice cuál es cuál.
|
|
69
|
+
if (dueño.owner === me) {
|
|
70
|
+
return fail(`${slug} la tenés vos, tomada el ${dueño.started} desde el runner ${dueño.runner}. `
|
|
71
|
+
+ 'Preguntá si se retoma esa sesión —y entonces corré con ese id— o si es otro agente en '
|
|
72
|
+
+ `paralelo, que toma otra tarea. \`ops runners ${path.relative(process.cwd(), root) || '.'}\` `
|
|
73
|
+
+ 'lista lo que hay abierto.')
|
|
74
|
+
}
|
|
75
|
+
return fail(`${slug} la tomó ${dueño.owner} el ${dueño.started}. Si se abandonó, borrá `
|
|
76
|
+
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
|
|
77
|
+
}
|
|
78
|
+
console.log(`✓ ${slug} tomada por ${me}`)
|
|
79
|
+
// Un reclamo sin empujar no protege de nada: el otro runner lee lo que hay en su copia. Decirlo acá
|
|
80
|
+
// es lo único que separa «tomé la tarea» de «creí que la había tomado».
|
|
81
|
+
if (!cli.has('--json')) console.log(` commiteá y empujá ${CL.DIR}/${slug}.md para que el equipo lo vea`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function release(dir, slug) {
|
|
85
|
+
const root = path.resolve(dir || '.')
|
|
86
|
+
if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`', 2)
|
|
87
|
+
const from = CL.runner()
|
|
88
|
+
const taken = CL.read(root).find((one) => one.slug === slug)
|
|
89
|
+
if (!taken) return fail(`${slug} no está tomada por nadie.`, 2)
|
|
90
|
+
if (taken.runner !== from) {
|
|
91
|
+
return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano.`)
|
|
92
|
+
}
|
|
93
|
+
fs.rmSync(CL.file(root, slug))
|
|
94
|
+
console.log(`✓ ${slug} soltada; volvió a la cola`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Qué runners tienen trabajo abierto, para que un agente pueda preguntar antes de inventarse un id.
|
|
98
|
+
//
|
|
99
|
+
// Es lo primero de una sesión y no lo contesta `context`: `context` responde «qué hago» para un runner ya
|
|
100
|
+
// elegido, y elegirlo viene antes. Sin esta lista, un agente que arranca sin `CAUCE_RUNNER` se crea uno
|
|
101
|
+
// nuevo y deja el trabajo de ayer huérfano, o peor, se lo pisa a otro agente que sigue corriendo.
|
|
102
|
+
//
|
|
103
|
+
// La persona elige; el agente exporta. Pedirle a una persona que escriba una variable de entorno para
|
|
104
|
+
// retomar su propio trabajo es hacerle hacer de intérprete.
|
|
105
|
+
function runners(dir, cli) {
|
|
106
|
+
const root = path.resolve(dir || '.')
|
|
107
|
+
const done = ST.snapshot(root).done
|
|
108
|
+
const abiertos = CL.read(root).filter((one) => !done.set.has(one.slug))
|
|
109
|
+
const hoy = TODAY()
|
|
110
|
+
const filas = abiertos.map((one) => {
|
|
111
|
+
const commit = R.lastCommit(R.repoOf(path.join(root, '..'), one.service), CL.branchOf(one.slug))
|
|
112
|
+
return { runner: one.runner, task: one.slug, owner: one.owner, started: one.started, lastCommit: commit }
|
|
113
|
+
})
|
|
114
|
+
if (cli.has('--json')) return console.log(JSON.stringify(filas))
|
|
115
|
+
if (!filas.length) return console.log('= ningún runner tiene trabajo abierto: arrancá con un id propio')
|
|
116
|
+
const ancho = Math.max(...filas.map((one) => one.runner.length))
|
|
117
|
+
for (const una of filas) {
|
|
118
|
+
const avance = una.lastCommit ? `último commit ${una.lastCommit}` : 'sin commits en su rama'
|
|
119
|
+
console.log(`${una.runner.padEnd(ancho)} ${una.task} (${una.owner}, desde ${una.started}; ${avance})`)
|
|
120
|
+
}
|
|
121
|
+
console.log(`\n${filas.length} runner(s) con trabajo abierto al ${hoy}. `
|
|
122
|
+
+ 'Retomá uno usando su id, o arrancá con uno propio.')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { claim, release, runners }
|