kcreate-scaffolding-gcp-script 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -24,7 +24,6 @@ El despliegue es un proceso aparte, posterior y manual.
24
24
  | `README.md` (este) | Manual de usuario: qué hace, cómo se instala y cómo se usa. |
25
25
  | [`AGENT.md`](AGENT.md) | Guía operativa para agentes de IA: ciclo completo, contrato de exit codes, fallas comunes, receta de validación. |
26
26
  | [`CONTRIBUTING.md`](CONTRIBUTING.md) | Flujo de contribución, convenciones e invariantes del código. |
27
- | [`DeployNPM.md`](DeployNPM.md) | Manual operativo para publicar el paquete npm (comandos exactos, autenticación con OTP, verificación y errores comunes). |
28
27
  | [`CHANGELOG.md`](CHANGELOG.md) | Historia de versiones (Keep a Changelog). |
29
28
  | [`IDEA.md`](IDEA.md) | Especificación de origen del skill. |
30
29
  | [`kcreate-scaffolding-gcp-script/SKILL.md`](kcreate-scaffolding-gcp-script/SKILL.md) | Instrucciones del agente, etapa por etapa (lo que ejecuta el skill instalado). |
@@ -44,42 +43,89 @@ diseño evita está en
44
43
 
45
44
  ## Instalación
46
45
 
47
- Hay dos instaladores **intercambiables**: dejan exactamente el mismo
48
- resultado en el repo destino (mismos archivos, mismos permisos, mismos exit
49
- codes). Elige el que te acomode.
46
+ Hay dos instaladores **intercambiables** (`npx` y `install.sh`): dejan
47
+ exactamente el mismo resultado en el repo destino (mismos archivos, mismos
48
+ permisos, mismos exit codes). Elige el que te acomode.
50
49
 
51
50
  En ambos casos se instala el skill (+ su dependencia
52
51
  [`github-proyect-tracking`](https://github.com/kronops/KGithubTracking),
53
52
  vendorizada en `vendor/`) en el repo donde se va a usar, como skill de
54
53
  proyecto de Claude Code.
55
54
 
56
- ### Opción A — `npx` (sin instalar el skill a mano)
55
+ | Vía | Cuándo usarla | Requisitos |
56
+ |---|---|---|
57
+ | **A — `npx`** (paquete publicado en npm) | La normal: no hay que clonar este repo. | Node ≥ 18 |
58
+ | **A-bis — `npx` sobre un tarball** | Sin acceso al registro, o para probar una versión local antes de publicarla. | Node ≥ 18 + copia de este repo |
59
+ | **B — `install.sh`** | Prefieres bash puro, o trabajas con el repo ya clonado. | bash + git |
57
60
 
58
- Si el paquete está en el registro de npm (o tienes el tarball):
61
+ ### Opción A — `npx` (recomendada, sin clonar el repo)
62
+
63
+ El paquete está publicado en el registro público de npm como
64
+ [`kcreate-scaffolding-gcp-script`](https://www.npmjs.com/package/kcreate-scaffolding-gcp-script)
65
+ (versión actual: `0.2.1`). Único requisito: **Node ≥ 18** (`npx` viene con
66
+ npm). El instalador no tiene dependencias npm; el skill en sí no usa Node:
67
+ es bash + Python 3.8+ stdlib.
59
68
 
60
69
  ```bash
70
+ # 0) (opcional) diagnóstico: no modifica nada, solo reporta el estado del destino
71
+ npx --yes kcreate-scaffolding-gcp-script doctor --target-repo /ruta/a/tu/repo-de-infra
72
+
73
+ # 1) instalar el skill en el repo destino
61
74
  npx --yes kcreate-scaffolding-gcp-script --target-repo /ruta/a/tu/repo-de-infra
62
- npx --yes ./kcreate-scaffolding-gcp-script-0.2.0.tgz --target-repo /ruta/a/tu/repo-de-infra
63
75
  ```
64
76
 
65
- Generar el tarball desde una copia del repo:
77
+ Salida esperada del paso 1:
78
+
79
+ ```
80
+ [OK] kcreate-scaffolding-gcp-script instalado en <repo>/.claude/skills/kcreate-scaffolding-gcp-script
81
+ [OK] github-proyect-tracking instalado en <repo>/.claude/skills/github-proyect-tracking
82
+ [OK] Instalación completa en <repo>
83
+ ```
84
+
85
+ Notas de uso:
86
+
87
+ - `--yes` evita el prompt de npx (`Need to install the following packages`)
88
+ la primera vez. `npx` cachea el paquete, así que las siguientes corridas son
89
+ instantáneas.
90
+ - Para instalaciones reproducibles fija la versión:
91
+ `npx --yes kcreate-scaffolding-gcp-script@0.2.1 --target-repo <repo>`. Sin
92
+ sufijo toma `latest`.
93
+ - **Actualizar** = volver a correr el mismo comando con `--force`. Sin
94
+ `--force`, una instalación existente nunca se toca: termina con
95
+ `[ERROR] No se instaló nada…` y **exit 1**.
96
+ - También sirve instalarlo global: `npm install -g kcreate-scaffolding-gcp-script`
97
+ y después `kcreate-scaffolding-gcp-script --target-repo <repo>`.
98
+ - Ayuda y versión del instalador: `npx --yes kcreate-scaffolding-gcp-script --help`
99
+ y `--version` (hoy responde `kcreate-scaffolding-gcp-script/0.2.1 (instalador npx)`).
100
+
101
+ > `npx ... github:kronops/KCreateScaffoldingGCPScripts` **no** sirve: npm
102
+ exige el `package.json` en la raíz del repo y aquí vive en `npm/` (npm no
103
+ soporta subdirectorios en dependencias git). Por eso la opción `npx` usa el
104
+ paquete del registro o un tarball local.
105
+
106
+ ### Opción A-bis — `npx` sobre un tarball local
107
+
108
+ Para probar una versión que aún no está publicada (o sin red):
66
109
 
67
110
  ```bash
68
111
  git clone git@github-work:kronops/KCreateScaffoldingGCPScripts.git
69
112
  cd KCreateScaffoldingGCPScripts/npm
70
113
  npm pack # prepack empaqueta skill/ + vendor/ + README + LICENSE
71
- npx --yes ./kcreate-scaffolding-gcp-script-0.2.0.tgz --target-repo /ruta/a/tu/repo
114
+ npx --yes ./kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/a/tu/repo-de-infra
72
115
  ```
73
116
 
74
- Requisitos del instalador npx: **Node ≥ 18** (cero dependencias npm). El skill
75
- en sí no usa Node: es bash + Python 3.8+ stdlib.
117
+ Detalle que muerde: `npx` solo acepta el tarball con ruta **relativa con
118
+ `./`** o como URL `file:`. Una ruta absoluta "pelada"
119
+ (`npx /tmp/x.tgz`) falla con `sh: Permission denied` y **exit 126**:
76
120
 
77
- > `npx ... github:kronops/KCreateScaffoldingGCPScripts` **no** sirve: npm
78
- exige el `package.json` en la raíz del repo y aquí vive en `npm/` (npm no
79
- soporta subdirectorios en dependencias git). Por eso la opción `npx`
80
- requiere el paquete publicado o un tarball local.
121
+ ```bash
122
+ # mal
123
+ npx --yes /tmp/kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/al/repo
124
+ # bien
125
+ npx --yes "file:/tmp/kcreate-scaffolding-gcp-script-0.2.1.tgz" --target-repo /ruta/al/repo
126
+ ```
81
127
 
82
- ### Opción B — clonando el repo
128
+ ### Opción B — clonando el repo (`install.sh`)
83
129
 
84
130
  ```bash
85
131
  git clone git@github-work:kronops/KCreateScaffoldingGCPScripts.git
@@ -87,7 +133,7 @@ cd KCreateScaffoldingGCPScripts
87
133
  ./install.sh --target-repo /ruta/a/tu/repo-de-infra
88
134
  ```
89
135
 
90
- ### Resultado (idéntico en ambas opciones)
136
+ ### Resultado (idéntico en las tres vías)
91
137
 
92
138
  ```
93
139
  <tu-repo>/.claude/skills/
@@ -100,6 +146,14 @@ skill asume la convención `live/<entorno>/<componente>/terragrunt.hcl`) pero
100
146
  igual instala: la herramienta sirve para cualquier repo, aunque falle al
101
147
  usarse.
102
148
 
149
+ ### Exit codes del instalador npx
150
+
151
+ | Exit | Significado |
152
+ |---|---|
153
+ | `0` | El skill quedó instalado/actualizado en el destino. |
154
+ | `1` | No se instaló nada: el skill ya existía sin `--force`, el destino no existe o no es la raíz de un repo git. |
155
+ | `2` | Argumentos inválidos (falta `--target-repo`, flag desconocido, comando desconocido). |
156
+
103
157
  ### Flags
104
158
 
105
159
  Los mismos nombres y semántica en `install.sh` y en el instalador npx:
@@ -141,15 +195,15 @@ listando los requisitos faltantes.
141
195
 
142
196
  ### Publicar el paquete npm (mantenedores)
143
197
 
144
- El procedimiento completo — y los comandos exactos, uno por uno — está en
145
- [`DeployNPM.md`](DeployNPM.md). Resumen:
198
+ El paquete está publicado y su mantenedor es `kronops`. Procedimiento:
146
199
 
147
200
  ```bash
148
201
  cd npm
149
- npm whoami # ¿hay sesión? (hoy: E401, hay que loguearse)
202
+ npm whoami # ¿hay sesión? (si da E401, hay que loguearse)
150
203
  npm login --auth-type=legacy # usuario, password, email y, si la cuenta tiene 2FA, OTP
151
204
  npm publish --dry-run # ensayo obligatorio: construye el tarball sin subir nada
152
205
  npm publish # publicar (con 2FA: npm publish --otp=123456)
206
+ npm view kcreate-scaffolding-gcp-script version # confirmar la versión en el registro
153
207
  ```
154
208
 
155
209
  Ojo: si la cuenta tiene 2FA, el login por navegador **no** sirve sin GUI; hay
@@ -163,7 +217,7 @@ nunca se comitean. Prueba local sin publicar:
163
217
 
164
218
  ```bash
165
219
  cd npm && npm pack
166
- npx --yes ./kcreate-scaffolding-gcp-script-0.2.0.tgz --target-repo /ruta/al/repo
220
+ npx --yes ./kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/al/repo
167
221
  ```
168
222
 
169
223
  Para subir una versión nueva: `node -p "require('./package.json').version"`,
@@ -273,8 +327,9 @@ $S/02_print_config.sh xoloshop-prod # revisar antes de construir
273
327
 
274
328
  $S/10_build_components.sh xoloshop-prod # construcción
275
329
 
276
- $S/20_validate_file_parity.sh boletera-qa xoloshop-prod
277
- $S/21_validate_project_pinning.sh xoloshop-prod
330
+ $S/20_validate_file_parity.sh boletera-qa xoloshop-prod
331
+ $S/21_validate_project_pinning.sh xoloshop-prod
332
+ $S/22_validate_no_app_name_leftover.sh xoloshop-prod
278
333
  ```
279
334
 
280
335
  Cada script termina con un mensaje de una sola línea y un código de salida
@@ -292,6 +347,7 @@ acorde — pensado para scripting, no solo para lectura humana:
292
347
  | `10_build_components.sh` | `BUILD_OK: <ruta>` | — | `BUILD_FAILED: <paso> - <detalle>` | — |
293
348
  | `20_validate_file_parity.sh` | `FILE_PARITY_OK: <n>` | — | `FILE_PARITY_MISMATCH` | — |
294
349
  | `21_validate_project_pinning.sh` | `PROJECT_PINNING_OK: <n>` | — | `PROJECT_PINNING_FAILED` | — |
350
+ | `22_validate_no_app_name_leftover.sh` | `APP_NAME_LEFTOVER_OK` | — | `APP_NAME_LEFTOVER_FOUND: <n>` | — |
295
351
  | `run_tracking.sh` | comando OK | — | error de tracking | tracking no instalado |
296
352
 
297
353
  `2` no es un error: es la señal de "faltan datos, llénalos y vuelve a correr
@@ -302,7 +358,6 @@ el mismo comando".
302
358
  ```
303
359
  KCreateScaffoldingGCPScripts/
304
360
  ├── README.md / AGENT.md / CONTRIBUTING.md / CHANGELOG.md
305
- ├── DeployNPM.md # manual para publicar el paquete en npm
306
361
  ├── IDEA.md # especificación de origen del skill
307
362
  ├── CreateScriptsNewGCPProjectInstructions.md # guía manual de referencia (base de experiencia)
308
363
  ├── LICENSE # MIT
@@ -324,8 +379,9 @@ KCreateScaffoldingGCPScripts/
324
379
  ├── 01_init_config.sh # Pre requisitos, paso 1
325
380
  ├── 02_print_config.sh # Pre requisitos, paso 2
326
381
  ├── 10_build_components.sh # Construcción
327
- ├── 20_validate_file_parity.sh # Validación 1
328
- ├── 21_validate_project_pinning.sh # Validación 2
382
+ ├── 20_validate_file_parity.sh # Validación 1
383
+ ├── 21_validate_project_pinning.sh # Validación 2
384
+ ├── 22_validate_no_app_name_leftover.sh # Validación 3
329
385
  └── run_tracking.sh # envoltura sobre github-proyect-tracking
330
386
  ```
331
387
 
@@ -341,10 +397,13 @@ KCreateScaffoldingGCPScripts/
341
397
  - El instalador npx **no** puede consumirse como
342
398
  `npx github:kronops/KCreateScaffoldingGCPScripts`: npm exige el
343
399
  `package.json` en la raíz del repo y aquí vive en `npm/` (no soporta
344
- subdirectorios en dependencias git). Requiere publicación en el registro
345
- de npm o un tarball local (`npm pack`).
346
- - `npm publish` está preparado pero el paquete aún no está publicado en el
347
- registro; hoy la vía sin clonar el repo es el tarball.
400
+ subdirectorios en dependencias git). Por eso se publica en el registro y
401
+ se instala como `npx kcreate-scaffolding-gcp-script`, o vía tarball local
402
+ (`npm pack`).
403
+ - El paquete del registro se actualiza solo cuando alguien corre
404
+ `npm publish` a mano: un cambio en este repo no llega a los usuarios de
405
+ `npx` hasta que sube la versión (y una versión publicada no se reutiliza,
406
+ `E409`).
348
407
  - El nombre del proyecto (`PROJECT_NAME`, ej. `Xoloshop-Prod`) se infiere de
349
408
  `NEW_ENV` con la convención `<App>-<QA|Prod>`; si la app tiene siglas o una
350
409
  capitalización no obvia, corrígelo a mano en el config antes de construir.
package/bin/cli.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcreate-scaffolding-gcp-script",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Instalador npx del skill kcreate-scaffolding-gcp-script: genera el scaffolding de Terragrunt para un entorno GCP nuevo a partir de uno existente.",
5
5
  "bin": {
6
6
  "kcreate-scaffolding-gcp-script": "bin/cli.js"
package/skill/SKILL.md CHANGED
@@ -182,12 +182,13 @@ el resultado final; no las descartes en silencio.
182
182
 
183
183
  ## Etapa 3 — Validación
184
184
 
185
- Dos scripts, cualquier orden, ambos obligatorios antes de dar por bueno
186
- el resultado:
185
+ Tres scripts, cualquier orden, los tres obligatorios antes de dar por
186
+ bueno el resultado:
187
187
 
188
188
  ```bash
189
189
  .claude/skills/kcreate-scaffolding-gcp-script/scripts/20_validate_file_parity.sh <SOURCE_ENV> <NEW_ENV>
190
190
  .claude/skills/kcreate-scaffolding-gcp-script/scripts/21_validate_project_pinning.sh <NEW_ENV>
191
+ .claude/skills/kcreate-scaffolding-gcp-script/scripts/22_validate_no_app_name_leftover.sh <NEW_ENV>
191
192
  ```
192
193
 
193
194
  - `20_validate_file_parity.sh` → `FILE_PARITY_OK` (exit 0) o
@@ -199,17 +200,32 @@ el resultado:
199
200
  problema). Confirma que, sin importar qué proyecto tenga `gcloud` como
200
201
  default en ese momento, cada componente de `NEW_ENV` resuelve siempre al
201
202
  proyecto correcto.
202
-
203
- SI cualquiera de los dos falla → `DETENTE`. No es un error de Terragrunt
203
+ - `22_validate_no_app_name_leftover.sh` → `APP_NAME_LEFTOVER_OK` (exit 0) o
204
+ `APP_NAME_LEFTOVER_FOUND` (exit 1, con archivo y línea exactos). Busca el
205
+ nombre corto de la app de `SOURCE_ENV` (sin el sufijo `-qa`/`-prod`/etc.),
206
+ en cualquier capitalización, en cualquier archivo de `live/<NEW_ENV>/`.
207
+ Complementa a `21`: un nombre de marca estilizado (ej. "XoloShop", con
208
+ una mayúscula interna) no es substring de "xoloshop-qa" ni de
209
+ "Xoloshop-QA", así que `21` no lo detecta — pero sí es un leftover real
210
+ del entorno fuente que no debe quedar en el nuevo.
211
+
212
+ SI cualquiera de los tres falla → `DETENTE`. No es un error de Terragrunt
204
213
  ni de GCP — es un defecto de generación real, y el mensaje ya dice
205
214
  exactamente qué componente/archivo revisar.
206
215
 
207
- SI ambos pasan y el tracking SÍ se inicializó en la Etapa 2 (no viste el
216
+ SI los tres pasan y el tracking SÍ se inicializó en la Etapa 2 (no viste el
208
217
  `[WARN]` de "no se pudo inicializar github-proyect-tracking") → marca el
209
- proceso como terminado:
218
+ proceso como terminado. Antes de llamar a `finalize`, construye el mismo
219
+ resumen de ejecución que vas a mostrarle al humano (paso 1 de la lista de
220
+ abajo) y escríbelo en un archivo de texto plano — ese archivo se publica
221
+ como un comentario aparte en el Issue, después del de FINALIZADO y antes
222
+ de cerrarlo, para que quien lea el Issue no tenga que pedírtelo por fuera:
210
223
 
211
224
  ```bash
212
- .claude/skills/kcreate-scaffolding-gcp-script/scripts/run_tracking.sh finalize "$(ls -d .kcreate-scaffolding/<NEW_ENV>.config)"
225
+ cat > /tmp/kcreate-resumen-<NEW_ENV>.txt <<'EOF'
226
+ <el mismo resumen de ejecución del paso 1 de abajo>
227
+ EOF
228
+ .claude/skills/kcreate-scaffolding-gcp-script/scripts/run_tracking.sh finalize "$(ls -d .kcreate-scaffolding/<NEW_ENV>.config)" /tmp/kcreate-resumen-<NEW_ENV>.txt
213
229
  ```
214
230
 
215
231
  SI el tracking nunca se inicializó (modo best-effort sin éxito, ver
@@ -1,6 +1,6 @@
1
1
  # Lecciones reales incorporadas en este skill
2
2
 
3
- Cinco fuentes:
3
+ Seis fuentes:
4
4
 
5
5
  - **A — descubiertas desplegando `xoloshop-qa` a mano** (sesión previa,
6
6
  documentadas también en `docs/CreateScriptsNewGCPProjectInstructions.md`
@@ -25,6 +25,10 @@ Cinco fuentes:
25
25
  propio fix de `artifact_registry` (una función nueva de esa misma
26
26
  ronda) se había quedado con la misma contaminación que D1/D2 ya habían
27
27
  corregido en las demás. Ver `Review_V3.md`.
28
+ - **F — encontradas ejecutando el skill de punta a punta contra el repo
29
+ real `club-tijuana/infra-cloud`** (no una copia descartable), generando
30
+ `coreservices-qa` desde `xoloshop-qa`, con Issue de tracking real
31
+ creado y cerrado.
28
32
 
29
33
  Cada lección dice **qué pasaba**, **por qué**, y **en qué script/función
30
34
  de este skill queda resuelta**.
@@ -611,6 +615,85 @@ excluye" pase sin ninguna pista de la causa.
611
615
 
612
616
  ---
613
617
 
618
+ ## F — encontradas ejecutando el skill de punta a punta contra `infra-cloud` real
619
+
620
+ A diferencia de B-E (copias descartables), esta corrida fue real: generó
621
+ `coreservices-qa` desde `xoloshop-qa` en `club-tijuana/infra-cloud`, con
622
+ Issue de tracking real (#332) creado y cerrado. El usuario revisó el
623
+ resultado campo por campo y reportó F1 directamente.
624
+
625
+ ### F1 — "XoloShop" (marca estilizada) sobrevivía la sustitución, sin ser detectado
626
+
627
+ **Qué pasaba:** `service_accounts/terragrunt.hcl`, generado desde
628
+ `xoloshop-qa`, dejó `display_name`/`description` de dos service accounts
629
+ con "XoloShop" (S interna mayúscula) sin tocar — debían decir
630
+ "Coreservices QA ...". `ksc_copy_component_files` solo hacía dos pasadas
631
+ de sustitución EXACTAS: `ksc_substr_replace` para `"xoloshop"` (minúsculas)
632
+ y una para `ksc_capitalize("xoloshop")` = `"Xoloshop"` (solo la primera
633
+ letra). Ninguna de las dos es substring de `"XoloShop"` (mayúscula
634
+ *interna*), así que esa capitalización en particular pasaba intacta.
635
+ Tampoco `21_validate_project_pinning.sh` lo atrapaba: busca el token
636
+ completo `"<app>-<entorno>"` exacto, y `"XoloShop"` no es substring de
637
+ `"xoloshop-qa"` ni de `"Xoloshop-QA"`.
638
+ **Resuelto en:**
639
+ - `lib/common.sh:ksc_substr_replace_app` — sustitución case-*insensitive*
640
+ (vía `re.IGNORECASE` en Python) con reemplazo case-*aware*: si el match
641
+ original era todo mayúsculas usa `APP_NEW` en mayúsculas, si era todo
642
+ minúsculas usa minúsculas, cualquier otro caso (incluida la "XoloShop"
643
+ original) usa capitalización simple (`Coreservices`). Así cualquier
644
+ variante de capitalización del nombre de marca queda cubierta, no solo
645
+ las dos que `ksc_capitalize` sabía producir.
646
+ - `10_build_components.sh:ksc_copy_component_files` — reemplaza las dos
647
+ pasadas exactas por una llamada a `ksc_substr_replace_app` (solo si
648
+ `APP_SOURCE != APP_NEW`, evitando trabajo innecesario cuando el nombre
649
+ de app no cambia entre entornos).
650
+ - `build_component_artifact_registry`'s detección de "ya es genérico" —
651
+ cambiada a `grep -qi` por la misma razón (consistencia: no tiene sentido
652
+ que la detección sea case-sensitive si la sustitución ya no lo es).
653
+ **Decisión explícita de NO tocar:** el check de `repository_id` en
654
+ `ksc_learn_artifact_registry_template` sigue siendo exacto/minúsculas —
655
+ GCP exige minúsculas en ese campo, así que ahí la sensibilidad a mayúsculas
656
+ es correcta, no un bug.
657
+ - **Nueva validación:** `22_validate_no_app_name_leftover.sh` — un script
658
+ de Etapa 3 dedicado, que busca `APP_SOURCE` (cualquier capitalización)
659
+ en TODO archivo de `live/<NEW_ENV>/`, fuera de comentarios. Complementa
660
+ (no reemplaza) a `21`: ese sigue validando el token completo exacto,
661
+ este cubre justo el caso que `21` no puede ver por diseño. Probado
662
+ contra una copia descartable: pasa limpio en un build correcto, y
663
+ detecta correctamente un leftover reintroducido a mano, reportando
664
+ archivo y línea exactos.
665
+
666
+ ### F2 — El tracking no decía qué skill lo estaba usando ni con qué parámetros
667
+
668
+ **Qué pasaba:** el Issue de GitHub que abre `github-proyect-tracking` no
669
+ tenía forma de decir, por sí solo, que lo estaba usando
670
+ `/kcreate-scaffolding-gcp-script`, ni con qué `SOURCE_ENV`/`NEW_ENV` —
671
+ alguien leyendo el Issue sin contexto previo no podía saber qué se estaba
672
+ construyendo sin preguntar aparte.
673
+ **Resuelto:** `github-proyect-tracking` 0.7.0+ agrega `--process-name`/
674
+ `--process-inputs` a `init` (ver `vendor/VENDORED_FROM.md`); este skill
675
+ los manda siempre, automáticamente, desde `run_tracking.sh init-plan`
676
+ (`--process-name "/kcreate-scaffolding-gcp-script"` y
677
+ `--process-inputs '{"SOURCE_ENV":...,"NEW_ENV":...}'`), sin que el agente
678
+ tenga que acordarse de pasarlos.
679
+
680
+ ### F3 — El resumen final solo se veía en pantalla, no quedaba en el Issue
681
+
682
+ **Qué pasaba:** al terminar, el agente comentaba "FINALIZADO" en el Issue
683
+ y lo cerraba, pero el resumen de ejecución (lo que sí le mostraba al
684
+ humano en pantalla) nunca quedaba escrito en el Issue — quien lo leyera
685
+ después no tenía ese resumen, solo el estado final.
686
+ **Resuelto:** `github-proyect-tracking` 0.7.0 agrega `finalize
687
+ --summary`/`--summary-file`, que publica un comentario aparte con el
688
+ resumen DESPUÉS del de FINALIZADO y ANTES de cerrar el Issue (orden
689
+ verificado con pruebas unitarias dedicadas, incluyendo que la ruta
690
+ `--force`/CANCELADO nunca publica resumen). `run_tracking.sh finalize`
691
+ acepta ahora un tercer argumento opcional (ruta a un archivo con el
692
+ resumen); `SKILL.md` (Etapa 3) ya le indica al agente que construya ese
693
+ resumen ANTES de llamar a `finalize` y lo pase como archivo.
694
+
695
+ ---
696
+
614
697
  ## Qué significa esta lista para quien edite este skill después
615
698
 
616
699
  Cada vez que se agregue un `build_component_<algo>` nuevo o se modifique
@@ -49,7 +49,6 @@ NEW_DIR="$REPO_ROOT/live/$NEW_ENV"
49
49
 
50
50
  read -r APP_SOURCE ENTORNO_SOURCE < <(ksc_split_env "$SOURCE_ENV")
51
51
  read -r APP_NEW ENTORNO_NEW < <(ksc_split_env "$NEW_ENV")
52
- APP_SOURCE_CAP="$(ksc_capitalize "$APP_SOURCE")"
53
52
  APP_NEW_CAP="$(ksc_capitalize "$APP_NEW")"
54
53
 
55
54
  CIDR_FILE="$(ksc_cidr_file_path "$REPO_ROOT" "$NEW_ENV")"
@@ -169,17 +168,25 @@ ksc_copy_component_files() {
169
168
  [[ -f "$SOURCE_DIR/$comp/$f" ]] && cp "$SOURCE_DIR/$comp/$f" "$NEW_DIR/$comp/$f"
170
169
  done
171
170
  # Sustitución estándar aplicada a TODO componente, conocido o no: el
172
- # token completo del entorno, y el nombre corto de la app (en minúscula
173
- # y capitalizado). Antes esto solo se hacía a mano en algunos
174
- # componentes — Review V1 (E-5) encontró texto real de SOURCE_ENV
175
- # filtrado en service_accounts (display_name) y project_iam (namespace
176
- # de un principal) porque esos dos nunca aplicaban la sustitución de
177
- # nombre de app, solo la del token completo.
171
+ # token completo del entorno, y el nombre corto de la app en CUALQUIER
172
+ # capitalización (ver ksc_substr_replace_app). Antes esto solo se hacía
173
+ # a mano en algunos componentes — Review V1 (E-5) encontró texto real
174
+ # de SOURCE_ENV filtrado en service_accounts (display_name) y
175
+ # project_iam (namespace de un principal) porque esos dos nunca
176
+ # aplicaban la sustitución de nombre de app, solo la del token
177
+ # completo. Corregido ahí; después, un bug real encontrado usando el
178
+ # skill (no en revisión) mostró que la sustitución de nombre de app
179
+ # EXISTÍA pero solo cubría dos capitalizaciones exactas (minúscula y
180
+ # "Primera-mayúscula") — un nombre de marca estilizado como "XoloShop"
181
+ # (S interna mayúscula) no hacía match con ninguna de las dos y se
182
+ # colaba completo a service_accounts.display_name/description. Ver
183
+ # lessons-learned.md, sección F.
178
184
  f="$NEW_DIR/$comp/terragrunt.hcl"
179
185
  [[ -f "$f" ]] || return 0
180
186
  ksc_substr_replace "$f" "$SOURCE_ENV" "$NEW_ENV"
181
- [[ "$APP_SOURCE" != "$APP_NEW" ]] && ksc_substr_replace "$f" "$APP_SOURCE" "$APP_NEW"
182
- [[ "$APP_SOURCE_CAP" != "$APP_NEW_CAP" ]] && ksc_substr_replace "$f" "$APP_SOURCE_CAP" "$APP_NEW_CAP"
187
+ if [[ "$APP_SOURCE" != "$APP_NEW" ]]; then
188
+ ksc_substr_replace_app "$f" "$APP_SOURCE" "$APP_NEW"
189
+ fi
183
190
  }
184
191
 
185
192
  # ---------------------------------------------------------------------------
@@ -429,7 +436,12 @@ build_component_artifact_registry() {
429
436
  # era genérico (sin el nombre de la app), la sustitución automática no
430
437
  # tiene nada que reemplazar y el resultado seguiría siendo genérico.
431
438
  local was_generic=1
432
- grep -q "$APP_SOURCE" "$SOURCE_DIR/artifact_registry/terragrunt.hcl" && was_generic=0
439
+ # -i: la app puede aparecer con cualquier capitalización (ver
440
+ # ksc_substr_replace_app) — un chequeo exacto-minúscula aquí marcaría
441
+ # "genérico" un archivo que en realidad sí tiene el nombre de la app
442
+ # (solo que estilizado, ej. "XoloShop"), y la sustitución automática de
443
+ # ksc_copy_component_files ya lo habría corregido de todas formas.
444
+ grep -qi "$APP_SOURCE" "$SOURCE_DIR/artifact_registry/terragrunt.hcl" && was_generic=0
433
445
 
434
446
  ksc_copy_component_files artifact_registry
435
447
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # Etapa: Validación — script 3.
3
+ # Busca, en TODO archivo de texto de live/<NEW_ENV>/, el nombre corto de
4
+ # la app de SOURCE_ENV (sin el sufijo de entorno -qa/-prod/-dev/etc.),
5
+ # en CUALQUIER capitalización. Complementa a 21_validate_project_pinning.sh
6
+ # (que solo busca el token completo "<app>-<entorno>", exacto): un nombre
7
+ # de marca estilizado como "XoloShop" (con una S interna mayúscula) no es
8
+ # "xoloshop-qa" ni "Xoloshop-QA" como substring, así que 21 nunca lo
9
+ # atrapa — y la sustitución de ksc_copy_component_files, si tiene un bug,
10
+ # tampoco. Caso real: service_accounts generado desde xoloshop-qa dejó
11
+ # "XoloShop" sin tocar en dos display_name/description, porque la
12
+ # sustitución de entonces solo cubría "xoloshop" y "Xoloshop" exactos.
13
+ #
14
+ # Uso: 22_validate_no_app_name_leftover.sh <NEW_ENV>
15
+ #
16
+ # Termina con exactamente uno de estos mensajes:
17
+ # "APP_NAME_LEFTOVER_OK: sin referencias a '<APP_SOURCE>'" (exit 0)
18
+ # "APP_NAME_LEFTOVER_FOUND: <n> archivo(s) (ver detalle arriba)" (exit 1)
19
+
20
+ set -uo pipefail
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ # shellcheck source=lib/common.sh
24
+ source "$SCRIPT_DIR/lib/common.sh"
25
+
26
+ NEW_ENV="${1:-}"
27
+ [[ -n "$NEW_ENV" ]] || ksc_die "uso: 22_validate_no_app_name_leftover.sh <NEW_ENV>"
28
+
29
+ REPO_ROOT="$(ksc_repo_root)"
30
+ NEW_DIR="$REPO_ROOT/live/$NEW_ENV"
31
+ [[ -d "$NEW_DIR" ]] || ksc_die "no existe $NEW_DIR"
32
+
33
+ CONFIG_FILE="$(ksc_config_path "$REPO_ROOT" "$NEW_ENV")"
34
+ SOURCE_ENV=""
35
+ [[ -f "$CONFIG_FILE" ]] && SOURCE_ENV="$(ksc_hcl_get "$CONFIG_FILE" SOURCE_ENV 2>/dev/null || true)"
36
+ [[ -n "$SOURCE_ENV" ]] || SOURCE_ENV="$(grep -m1 '^SOURCE_ENV=' "$CONFIG_FILE" 2>/dev/null | sed -E 's/^SOURCE_ENV="(.*)"$/\1/')"
37
+ [[ -n "$SOURCE_ENV" ]] || ksc_die "no pude determinar SOURCE_ENV (no está en $CONFIG_FILE) — no se puede validar sin saber qué nombre de app buscar"
38
+
39
+ read -r APP_SOURCE _ < <(ksc_split_env "$SOURCE_ENV")
40
+
41
+ FAILURES=()
42
+
43
+ # Mismo criterio que 21 (comentarios excluidos: la NOTA de project_iam
44
+ # menciona SOURCE_ENV completo a propósito, lo que incluye APP_SOURCE
45
+ # como substring) y misma cobertura de archivos (find, no una lista fija
46
+ # de nombres — Review V2, N-3).
47
+ while IFS= read -r -d '' file; do
48
+ hit="$(grep -v '^[[:space:]]*#' "$file" 2>/dev/null | grep -i -m1 -- "$APP_SOURCE" | sed -E 's/^[[:space:]]+//')"
49
+ if [[ -n "$hit" ]]; then
50
+ FAILURES+=("$file: \"$hit\"")
51
+ fi
52
+ done < <(find "$NEW_DIR" -maxdepth 2 -type f -print0)
53
+
54
+ if [[ "${#FAILURES[@]}" -eq 0 ]]; then
55
+ echo "APP_NAME_LEFTOVER_OK: sin referencias a '$APP_SOURCE' (ninguna capitalización) en $NEW_DIR"
56
+ exit 0
57
+ fi
58
+
59
+ echo "Encontrado el nombre de la app fuente ('$APP_SOURCE', cualquier capitalización) fuera de comentarios:"
60
+ printf ' - %s\n' "${FAILURES[@]}"
61
+ echo "APP_NAME_LEFTOVER_FOUND: ${#FAILURES[@]} archivo(s) (ver detalle arriba) — corrige el shell script que genera ese componente para que sustituya '$APP_SOURCE' por el nombre de la app nueva, en cualquier capitalización (ver ksc_substr_replace_app en lib/common.sh)."
62
+ exit 1
@@ -745,3 +745,44 @@ ksc_substr_replace() {
745
745
  local file="$1" old="$2" new="$3"
746
746
  sed -i "s/${old}/${new}/g" "$file"
747
747
  }
748
+
749
+ # ksc_substr_replace_app <file> <app_source_lower> <app_new_lower>
750
+ #
751
+ # Reemplaza TODAS las variantes de capitalización del nombre corto de la
752
+ # app fuente (no solo minúscula exacta ni "Primera-mayúscula" exacta) por
753
+ # la variante equivalente del nombre nuevo. Caso real encontrado
754
+ # construyendo `coreservices-qa` desde `xoloshop-qa`: `service_accounts`
755
+ # usa el nombre de marca estilizado `"XoloShop"` (con S interna
756
+ # mayúscula) en `display_name`/`description` — ni
757
+ # `ksc_substr_replace "$APP_SOURCE"` (minúscula exacta) ni
758
+ # `ksc_substr_replace "$(ksc_capitalize "$APP_SOURCE")"` ("Xoloshop",
759
+ # solo la primera letra) hacen match contra "XoloShop", así que ese
760
+ # leftover pasaba completo a `coreservices-qa` en silencio. Resuelto
761
+ # haciendo el match insensible a mayúsculas/minúsculas y eligiendo la
762
+ # capitalización del REEMPLAZO según cómo estaba escrita la coincidencia
763
+ # encontrada (todo minúsculas -> nuevo en minúsculas; todo mayúsculas ->
764
+ # nuevo en mayúsculas; cualquier otra mezcla, incluidos estilos de marca
765
+ # como "XoloShop" -> primera letra mayúscula del nuevo, mejor esfuerzo).
766
+ ksc_substr_replace_app() {
767
+ local file="$1" app_source="$2" app_new="$3"
768
+ [[ -f "$file" ]] || return 0
769
+ python3 - "$file" "$app_source" "$app_new" <<'PYEOF'
770
+ import re, sys
771
+ file, app_source, app_new = sys.argv[1], sys.argv[2], sys.argv[3]
772
+ with open(file) as f:
773
+ text = f.read()
774
+ pattern = re.compile(re.escape(app_source), re.IGNORECASE)
775
+
776
+ def repl(m):
777
+ s = m.group(0)
778
+ if s.isupper() and len(s) > 1:
779
+ return app_new.upper()
780
+ if s.islower():
781
+ return app_new.lower()
782
+ return app_new[:1].upper() + app_new[1:].lower()
783
+
784
+ new_text = pattern.sub(repl, text)
785
+ with open(file, "w") as f:
786
+ f.write(new_text)
787
+ PYEOF
788
+ }
@@ -10,7 +10,19 @@
10
10
  # run_tracking.sh step-done <config_file> "<nombre del paso>"
11
11
  # run_tracking.sh step-error <config_file> "<nombre del paso>" "<detalle>"
12
12
  # run_tracking.sh status <config_file>
13
- # run_tracking.sh finalize <config_file>
13
+ # run_tracking.sh finalize <config_file> [archivo_de_resumen]
14
+ #
15
+ # `init-plan` manda siempre --process-name "/kcreate-scaffolding-gcp-script"
16
+ # y --process-inputs con SOURCE_ENV/NEW_ENV (requiere github-proyect-tracking
17
+ # >= 0.5.0/0.6.0 — ver vendor/VENDORED_FROM.md) — así el Issue de tracking
18
+ # dice de entrada QUÉ skill lo está usando y CON QUÉ parámetros, sin que el
19
+ # humano tenga que adivinarlo leyendo los comentarios de los pasos.
20
+ #
21
+ # `finalize` acepta opcionalmente la ruta a un archivo de texto con el
22
+ # resumen final de la ejecución (requiere github-proyect-tracking >= 0.7.0):
23
+ # si se da, ese resumen queda como un comentario APARTE en el Issue,
24
+ # después del de FINALIZADO y antes de cerrarlo — el mismo resumen que el
25
+ # agente ya le muestra al humano en pantalla, también en el Issue.
14
26
  #
15
27
  # Si github-proyect-tracking no está instalado junto a este skill, todos los
16
28
  # subcomandos salen con error claro (código 3) en vez de fallar a medias.
@@ -42,7 +54,10 @@ case "$SUBCOMMAND" in
42
54
  init-plan)
43
55
  PLAN_FILE="${3:-}"
44
56
  [[ -n "$PLAN_FILE" && -f "$PLAN_FILE" ]] || ksc_die "init-plan requiere un archivo plan.json existente"
45
- TP init --plan-file "$PLAN_FILE"
57
+ PROCESS_INPUTS="$(python3 -c 'import json,sys; print(json.dumps({"SOURCE_ENV": sys.argv[1], "NEW_ENV": sys.argv[2]}))' "$SOURCE_ENV" "$NEW_ENV")"
58
+ TP init --plan-file "$PLAN_FILE" \
59
+ --process-name "/kcreate-scaffolding-gcp-script" \
60
+ --process-inputs "$PROCESS_INPUTS"
46
61
  ;;
47
62
  resume)
48
63
  TP resume
@@ -64,7 +79,13 @@ case "$SUBCOMMAND" in
64
79
  TP status --remote
65
80
  ;;
66
81
  finalize)
67
- TP finalize
82
+ SUMMARY_FILE="${3:-}"
83
+ if [[ -n "$SUMMARY_FILE" ]]; then
84
+ [[ -f "$SUMMARY_FILE" ]] || ksc_die "no existe el archivo de resumen: $SUMMARY_FILE"
85
+ TP finalize --summary-file "$SUMMARY_FILE"
86
+ else
87
+ TP finalize
88
+ fi
68
89
  ;;
69
90
  *)
70
91
  ksc_die "subcomando desconocido: $SUBCOMMAND"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: github-proyect-tracking
3
3
  description: Track process execution state in GitHub Issues with resume.
4
- version: 0.4.0
4
+ version: 0.7.0
5
5
  author: Rommel (rommelmg), Hermes Agent
6
6
  license: MIT
7
7
  platforms: [linux, macos, windows]
@@ -69,7 +69,7 @@ python3 "$SKILL" doctor --url https://github.com/OWNER/REPO --token "$GITHUB_TOK
69
69
 
70
70
  | Comando | Efecto |
71
71
  |---|---|
72
- | `init --url URL [--plan JSON] [--store-token]` | Valida URL+token, crea el Issue, guarda `active_process_id` |
72
+ | `init --url URL [--plan JSON] [--store-token] [--process-name N]` | Valida URL+token, crea el Issue (título `[Process Tracking][N] Execution ID: …` si se da N), guarda `active_process_id` y `process_name` |
73
73
  | `resume [--url URL]` | Lee el ÚLTIMO comentario y dicta cómo continuar |
74
74
  | `step-start --name N` | Comenta `[STATUS: INICIANDO] - Paso: N` (valida orden del plan) |
75
75
  | `step-done --name N` | Comenta `[STATUS: COMPLETADO] - Paso: N` (exige paso en vuelo) |
@@ -95,9 +95,18 @@ exit 0) y `KeyboardInterrupt` (130).
95
95
  lectura hace fallar `issues-write`; `UNVERIFIED` se reserva para permisos
96
96
  granulares que la API no puede confirmar.
97
97
  2. **Init.** `init --url URL --plan '["Paso 1: ...", ...]'` (plan
98
- recomendado: nombres ÚNICOS; hace la reanudación determinista). Si
99
- devuelve `RESUME_REQUIRED` (exit 7), NO crees otro Issue: ve al paso 3.
100
- Criterio: `execution_id` e `issue_url` en la salida.
98
+ recomendado: nombres ÚNICOS; hace la reanudación determinista).
99
+ Si el proceso pertenece a un skill padre, añade
100
+ `--process-name "Mi Skill"`: el título del Issue pasa a
101
+ `[Process Tracking][Mi Skill] Execution ID: …` y el cuerpo incluye
102
+ `- **Proceso:** Mi Skill` — así el Issue identifica QUÉ se rastrea.
103
+ Para dejar constancia de CON QUÉ se ejecutó, añade `--process-inputs
104
+ '{"archivo":"x.csv","modo":"lint"}'` (objeto clave→valor; se publica
105
+ en la sección "Parámetros de entrada" del Issue, con secretos
106
+ redactados).
107
+ Si devuelve `RESUME_REQUIRED` (exit 7), NO crees otro Issue: ve al
108
+ paso 3. Criterio: `execution_id`, `process_name` e `issue_url` en la
109
+ salida.
101
110
  3. **Reanudar (si aplica).** `resume` y sigue el `verdict.action`
102
111
  (`RERUN_STEP`, `NEXT_STEP`, `START_FIRST`, `ALL_STEPS_DONE`/
103
112
  `FINISH_CLEANUP` → finalize, `ABORTED_EXTERNALLY` → pregunta al
@@ -42,7 +42,7 @@ texto y los usa para reconciliar únicamente el intento que originó el POST.
42
42
 
43
43
  ## Comandos
44
44
 
45
- ### `init --url URL [--plan JSON | --plan-file F] [--token T] [--prompt-token] [--store-token]`
45
+ ### `init --url URL [--plan JSON | --plan-file F] [--token T] [--prompt-token] [--store-token] [--process-name N] [--process-inputs JSON]`
46
46
 
47
47
  - Valida la URL (regex, redactada en errores), resuelve el token, valida
48
48
  `issues:write` ANTES de crear nada.
@@ -54,6 +54,20 @@ texto y los usa para reconciliar únicamente el intento que originó el POST.
54
54
  `resume` determinista.
55
55
  - `--store-token`: persistencia del token OPT-IN (por defecto NO se
56
56
  guarda; `--no-store-token` quedó obsoleto).
57
+ - `--process-name N`: nombre del proceso/skill padre. Título resultante:
58
+ `[Process Tracking][N] Execution ID: {exec_id}`; el cuerpo incluye
59
+ `- **Proceso:** N`; estado y JSON llevan `process_name`. Sin `N`, se
60
+ conserva el formato legado `[Process Tracking] Execution ID: …`.
61
+ Un secreto en `N` se redacta; `[`/`]`/saltos → `VALIDATION_ERROR`.
62
+ - `--process-inputs JSON`: objeto clave→valor con los parámetros de
63
+ entrada del proceso. Se publican en la sección "Parámetros de entrada"
64
+ del cuerpo del Issue (ordenados por clave, valores entre backticks) y
65
+ se guardan en el estado (`process_inputs`) y en la salida de `init`
66
+ (`process_inputs`). Secretos en claves/valores se REDACTAN; límites:
67
+ 50 pares, valores ≤ 2000 caracteres; claves vacías, JSON inválido o no
68
+ objeto → `VALIDATION_ERROR`. Sin la opción, el estado queda limpio
69
+ (`process_inputs: null`) y el cuerpo no tiene la sección.
70
+ - Salida de `init`: añade `process_name`, `process_inputs` e `issue_title`.
57
71
 
58
72
  Salida: `execution_id`, `issue_number`, `issue_url`, `state_file`, `plan`,
59
73
  `first_step`, `token_source`.
@@ -87,7 +101,7 @@ Publica `[STATUS: ERROR] - Paso: {N} - Detalles: {D}` y fija
87
101
  reanudar exige `resume`). `--details-file` ideal para stacktraces largos
88
102
  (trunca a 60000 chars).
89
103
 
90
- ### `finalize [--url URL] [--token T] [--force]`
104
+ ### `finalize [--url URL] [--token T] [--force] [--summary TEXT | --summary-file F]`
91
105
 
92
106
  1. Verifica el veredicto remoto: rechaza procesos con pasos pendientes
93
107
  (`FINALIZE_BLOCKED`, exit 8) y abortes externos. Sin plan permite éxito
@@ -96,10 +110,18 @@ reanudar exige `resume`). `--details-file` ideal para stacktraces largos
96
110
  exitosamente.` (idempotente). Con `--force`: `[STATUS: CANCELADO] -
97
111
  El proceso fue cerrado anticipadamente (--force).` con los pasos
98
112
  pendientes (cierre anticipado distinguible, nunca éxito falso).
99
- 3. Cierra el Issue (PATCH state=closed) y **verifica re-leyendo**.
100
- 4. Limpia `active_process_id` (estado `FINALIZED`/`CANCELADO`) + historial
113
+ 3. Si se dio `--summary`/`--summary-file` **y** la ruta es de éxito (no
114
+ `--force`): publica un comentario APARTE con ese texto (sanitizado y
115
+ truncado a 60000 chars, igual que `--details`), inmediatamente después
116
+ del de FINALIZADO. Sin marcador `[STATUS: ...]`, así que `COMMENT_RE`
117
+ lo ignora en lecturas futuras — no es parte de la máquina de estados,
118
+ solo queda como constancia legible en el Issue. `--summary`/
119
+ `--summary-file` vacío o en blanco no publica nada.
120
+ 4. Cierra el Issue (PATCH state=closed) y **verifica re-leyendo**.
121
+ 5. Limpia `active_process_id` (estado `FINALIZED`/`CANCELADO`) + historial
101
122
  (con `outcome`). Un CANCELADO existente conserva ese resultado aunque el
102
- reintento no lleve `--force`.
123
+ reintento no lleve `--force`. La salida incluye
124
+ `summary_comment_posted`/`summary_comment_url`.
103
125
 
104
126
  ### `status [--url URL] [--remote]`
105
127
 
@@ -95,9 +95,17 @@ def build_parser():
95
95
  "init", help="nueva ejecución: valida, crea el Issue y guarda memoria")
96
96
  common(subparser, url_required=True)
97
97
  subparser.add_argument("--plan", default=None, metavar="JSON",
98
- help='plan de pasos con nombres ÚNICOS, ej. \'["Paso 1","Paso 2"]\'')
98
+ help='plan de pasos con nombres ÚNICOS, ej. \'["Paso 1", "Paso 2"]\'')
99
99
  subparser.add_argument("--plan-file", default=None, metavar="F",
100
100
  help="archivo JSON con la lista de pasos")
101
+ subparser.add_argument("--process-name", default=None, metavar="N",
102
+ help="nombre del proceso/skill padre que se rastrea "
103
+ "(aparece en el título y cuerpo del Issue)")
104
+ subparser.add_argument("--process-inputs", default=None, metavar="JSON",
105
+ help='parámetros de entrada del proceso (objeto '
106
+ "clave→valor, se publican en el Issue; los "
107
+ "secretos se redactan), ej. \'{\"archivo\": "
108
+ '\"datos.csv\"}\'')
101
109
  subparser.add_argument("--prompt-token", action="store_true",
102
110
  help="pedir el token interactivamente (TTY)")
103
111
  subparser.add_argument("--store-token", action="store_true",
@@ -139,6 +147,13 @@ def build_parser():
139
147
  subparser.add_argument("--force", action="store_true",
140
148
  help="cancelar aunque queden pasos pendientes o no haya trabajo "
141
149
  "(no aplica si el Issue fue cerrado externamente)")
150
+ subparser.add_argument("--summary", default=None, metavar="TEXT",
151
+ help="resumen final del proceso (se publica como comentario "
152
+ "aparte, después del de FINALIZADO y antes de cerrar el "
153
+ "Issue; se sanitiza igual que --details). Solo aplica en "
154
+ "la ruta de éxito (no con --force)")
155
+ subparser.add_argument("--summary-file", default=None, metavar="F",
156
+ help="archivo con el resumen final (alternativa a --summary)")
142
157
  subparser.set_defaults(func=cmd_finalize)
143
158
 
144
159
  subparser = subparsers.add_parser(
@@ -153,6 +168,9 @@ def build_parser():
153
168
  common(subparser, url_required=True)
154
169
  subparser.add_argument("--execution-id", required=True, metavar="ID",
155
170
  help="Execution ID EXACTO del título del Issue")
171
+ subparser.add_argument("--process-name", default=None, metavar="N",
172
+ help="nombre del proceso a adoptar (filtro adicional "
173
+ "opcional si hay ambigüedad)")
156
174
  subparser.add_argument("--force", action="store_true",
157
175
  help="reemplazar un proceso activo existente (conserva token "
158
176
  "e historial)")
@@ -14,8 +14,9 @@ from .common import (
14
14
  EXIT_RESUME_REQUIRED, EXIT_RUNTIME, EXIT_TOKEN_INSUFFICIENT,
15
15
  EXIT_VALIDATION, MAX_DETAILS_LEN, MAX_PLAN_STEPS, SCHEMA_VERSION,
16
16
  STATUS_CANCELADO, STATUS_COMPLETADO, STATUS_ERROR, STATUS_FINALIZADO,
17
- STATUS_INICIANDO, SkillError, emit, new_execution_id, normalize_step_name,
18
- now_iso, ordered_unique, redact_url, sanitize_text, validate_repo_url,
17
+ STATUS_INICIANDO, TITLE_RE, SkillError, emit, new_execution_id,
18
+ normalize_process_name, normalize_step_name, now_iso, ordered_unique,
19
+ issue_title, redact_url, sanitize_text, validate_repo_url,
19
20
  )
20
21
  from .events import (
21
22
  build_issue_body, events_from_comments, fmt_cancelado, fmt_completado,
@@ -96,6 +97,47 @@ def load_plan(args):
96
97
  return plan
97
98
 
98
99
 
100
+ MAX_INPUTS_COUNT = 50
101
+ MAX_INPUT_VALUE_LEN = 2000
102
+
103
+
104
+ def load_process_inputs(args):
105
+ """Valida y normaliza --process-inputs (objeto JSON clave→valor).
106
+
107
+ - claves y valores se convierten a cadena; claves vacías se rechazan
108
+ - secretos en valores (y claves) se REDACTAN antes de guardar/publicar
109
+ - valores se truncan a MAX_INPUT_VALUE_LEN; máx MAX_INPUTS_COUNT pares
110
+ - devuelve None cuando no se pasó la opción (mantiene el estado limpio)
111
+ """
112
+ raw = getattr(args, "process_inputs", None)
113
+ if raw is None:
114
+ return None
115
+ try:
116
+ data = json.loads(raw)
117
+ except json.JSONDecodeError as error:
118
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
119
+ f"--process-inputs no es JSON válido: {error}")
120
+ if not isinstance(data, dict):
121
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
122
+ "--process-inputs debe ser un objeto JSON "
123
+ "clave→valor, ej. '{\"archivo\": \"x.csv\", "
124
+ "\"modo\": \"lint\"}'")
125
+ if len(data) > MAX_INPUTS_COUNT:
126
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
127
+ f"--process-inputs no puede exceder "
128
+ f"{MAX_INPUTS_COUNT} pares")
129
+ inputs = {}
130
+ for key, value in data.items():
131
+ clean_key, _ = sanitize_text(str(key).strip())
132
+ clean_key = clean_key.strip()
133
+ if not clean_key:
134
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
135
+ "--process-inputs: las claves no pueden ser vacías")
136
+ clean_value, _ = sanitize_text(str(value))
137
+ inputs[clean_key] = clean_value[:MAX_INPUT_VALUE_LEN]
138
+ return inputs or None
139
+
140
+
99
141
  def _remote_snapshot(owner, repo, number, token):
100
142
  issue = github.fetch_issue(owner, repo, number, token)
101
143
  comments = github.fetch_all_comments(owner, repo, number, token)
@@ -125,6 +167,8 @@ def cmd_init(args):
125
167
  owner, repo = target
126
168
  path = state_file_for(owner, repo)
127
169
  plan = load_plan(args)
170
+ process_name = normalize_process_name(getattr(args, "process_name", None))
171
+ process_inputs = load_process_inputs(args)
128
172
 
129
173
  lock = StateLock(path)
130
174
  with lock:
@@ -154,8 +198,10 @@ def cmd_init(args):
154
198
  store = bool(args.store_token) and source in ("cli", "prompt")
155
199
 
156
200
  execution_id = new_execution_id()
157
- title = f"[Process Tracking] Execution ID: {execution_id}"
158
- body = build_issue_body(args.url.strip(), execution_id, plan)
201
+ title = issue_title(execution_id, process_name)
202
+ body = build_issue_body(args.url.strip(), execution_id, plan,
203
+ process_name=process_name,
204
+ process_inputs=process_inputs)
159
205
  recovered = False
160
206
  create_error = None
161
207
  lock.ensure_owner()
@@ -202,6 +248,8 @@ def cmd_init(args):
202
248
  "owner": owner, "repo": repo,
203
249
  "active_process_id": number,
204
250
  "execution_id": execution_id,
251
+ "process_name": process_name,
252
+ "process_inputs": process_inputs or None,
205
253
  "issue_url": data.get("html_url"),
206
254
  "process_state": "RUNNING",
207
255
  "current_step": None,
@@ -224,6 +272,8 @@ def cmd_init(args):
224
272
  "ok": True, "action": "INIT",
225
273
  "execution_id": execution_id, "issue_number": number,
226
274
  "issue_url": data.get("html_url"), "state_file": str(path),
275
+ "process_name": process_name, "issue_title": title,
276
+ "process_inputs": process_inputs or None,
227
277
  "plan": plan, "first_step": plan[0] if plan else None,
228
278
  "recovered": recovered,
229
279
  "token_source": source, "token_stored": bool(state["github_token"]),
@@ -493,6 +543,25 @@ _PENDING_VERDICTS = ("RERUN_STEP", "NEXT_STEP", "START_FIRST",
493
543
 
494
544
 
495
545
  def cmd_finalize(args):
546
+ summary = getattr(args, "summary", None)
547
+ summary_file = getattr(args, "summary_file", None)
548
+ if summary and summary_file:
549
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
550
+ "usa --summary o --summary-file, no ambos")
551
+ if summary_file:
552
+ try:
553
+ summary = Path(summary_file).read_text(encoding="utf-8", errors="replace")
554
+ except OSError as error:
555
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
556
+ f"no se pudo leer --summary-file: {error}")
557
+ if summary is not None:
558
+ summary = summary.strip()
559
+ if summary:
560
+ summary, _ = sanitize_text(summary)
561
+ summary = summary[:MAX_DETAILS_LEN]
562
+ else:
563
+ summary = None
564
+
496
565
  owner, repo, path = resolve_target(args)
497
566
  lock = StateLock(path)
498
567
  with lock:
@@ -547,6 +616,19 @@ def cmd_finalize(args):
547
616
  owner, repo, number, fmt_finalizado(), token,
548
617
  baseline_ids=frozenset(item.get("id") for item in comments),
549
618
  execution_id=state.get("execution_id"), sequence=len(events) + 1)
619
+
620
+ summary_comment = None
621
+ if summary and not forced and not already_cancel:
622
+ # Comentario aparte del resumen final, SOLO en la ruta de éxito
623
+ # (no al cancelar): va después del de FINALIZADO y antes de
624
+ # cerrar el Issue. No lleva el marcador [STATUS: ...], así que
625
+ # COMMENT_RE lo ignora en futuras lecturas de eventos — no
626
+ # interfiere con resume/plan_resume.
627
+ summary_comment = github.post_comment(
628
+ owner, repo, number, summary, token,
629
+ baseline_ids=frozenset(item.get("id") for item in comments),
630
+ execution_id=state.get("execution_id"), sequence=len(events) + 2)
631
+
550
632
  github.close_issue(owner, repo, number, token)
551
633
  issue = github.fetch_issue(owner, repo, number, token)
552
634
  if issue.get("state") != "closed":
@@ -586,6 +668,8 @@ def cmd_finalize(args):
586
668
  "forced": outcome == "CANCELADO",
587
669
  "comment_posted": bool(comment),
588
670
  "comment_url": comment["comment_url"] if comment else None,
671
+ "summary_comment_posted": bool(summary_comment),
672
+ "summary_comment_url": summary_comment["comment_url"] if summary_comment else None,
589
673
  "completed_steps": completed,
590
674
  "active_process_id": None,
591
675
  "state_file": str(path),
@@ -653,8 +737,9 @@ def cmd_adopt(args):
653
737
  existing = load_state(path)
654
738
  token, _ = need_token(args, existing)
655
739
 
656
- query = (f'repo:{owner}/{repo} "[Process Tracking] Execution ID: {execution_id}" '
657
- f'in:title is:issue')
740
+ # v0.5.0: el título puede incluir [Nombre]; se busca por el execution_id
741
+ # (único) y el filtrado canónico exacto lo hace canonical_title_match.
742
+ query = f'repo:{owner}/{repo} "{execution_id}" in:title is:issue'
658
743
  status, data, _ = github.api(
659
744
  "GET", "/search/issues?" + urllib.parse.urlencode({"q": query, "per_page": 30}),
660
745
  token)
@@ -662,8 +747,10 @@ def cmd_adopt(args):
662
747
  raise SkillError(EXIT_RUNTIME, "SEARCH_FAILED",
663
748
  f"búsqueda de Issues devolvió {status}: "
664
749
  f"{data.get('message', '')}")
750
+ process_name = normalize_process_name(getattr(args, "process_name", None))
665
751
  matches = [issue for issue in (data.get("items") or [])
666
- if github.canonical_title_match(issue, execution_id)]
752
+ if github.canonical_title_match(issue, execution_id,
753
+ process_name=process_name)]
667
754
  if not matches:
668
755
  raise SkillError(EXIT_NOT_FOUND, "ISSUE_NOT_FOUND",
669
756
  f"no se encontró un Issue con título canónico para "
@@ -691,6 +778,10 @@ def cmd_adopt(args):
691
778
  plan = parse_plan_from_body(issue.get("body") or "")
692
779
  verdict = plan_resume(events, plan, issue.get("state", "open"))
693
780
  action = verdict["action"]
781
+ title_match = TITLE_RE.match((issue.get("title") or "").strip())
782
+ adopted_name = (title_match.group("process_name")
783
+ if title_match and title_match.group("process_name")
784
+ else None)
694
785
  if issue.get("state") == "open":
695
786
  active = number
696
787
  process_state = ("ERROR" if events and events[-1]["status"] == STATUS_ERROR
@@ -708,6 +799,7 @@ def cmd_adopt(args):
708
799
  "owner": owner, "repo": repo,
709
800
  "active_process_id": active,
710
801
  "execution_id": execution_id,
802
+ "process_name": process_name or adopted_name,
711
803
  "issue_url": issue.get("html_url"),
712
804
  "process_state": process_state,
713
805
  "current_step": verdict.get("resume_from_step"),
@@ -727,7 +819,9 @@ def cmd_adopt(args):
727
819
  "ok": True, "action": "ADOPT", "execution_id": execution_id,
728
820
  "issue_number": number, "issue_url": issue.get("html_url"),
729
821
  "issue_state": issue.get("state"), "verdict": verdict,
730
- "process_state": process_state, "state_file": str(path),
822
+ "process_state": process_state,
823
+ "process_name": process_name or adopted_name,
824
+ "state_file": str(path),
731
825
  })
732
826
  return EXIT_OK
733
827
 
@@ -7,7 +7,7 @@ import re
7
7
  import uuid
8
8
  from datetime import datetime, timezone
9
9
 
10
- VERSION = "0.4.0"
10
+ VERSION = "0.6.0"
11
11
  API_BASE = "https://api.github.com"
12
12
  USER_AGENT = f"github-proyect-tracking/{VERSION}"
13
13
 
@@ -47,10 +47,18 @@ REPO_URL_RE = re.compile(
47
47
  r"(?P<repo>[A-Za-z0-9._-]+?)(?:\.git)?/?$",
48
48
  re.IGNORECASE,
49
49
  )
50
+ # Título dual (v0.5.0): con o sin nombre de proceso.
51
+ # [Process Tracking][Mi Skill] Execution ID: exec-...
52
+ # [Process Tracking] Execution ID: exec-... (formato <=0.4.x)
50
53
  TITLE_RE = re.compile(
51
- r"^\[Process Tracking\]\s*(?:-\s*)?Execution ID:\s*(?P<exec_id>\S+?)\s*$",
54
+ r"^\[Process Tracking\]"
55
+ r"(?:\[(?P<process_name>"
56
+ r"(?:[^\[\]\r\n]|\[RED[Aa]ctado:[^\[\]]*\]){1,200}"
57
+ r")\])?"
58
+ r"\s*(?:-\s*)?Execution ID:\s*(?P<exec_id>\S+?)\s*$",
52
59
  re.IGNORECASE,
53
60
  )
61
+ MAX_PROCESS_NAME_LEN = 100
54
62
 
55
63
  SECRET_REDACTIONS = [
56
64
  (re.compile(r"(?i)\b(authorization|proxy-authorization)\s*[:=]\s*"
@@ -68,6 +76,43 @@ SECRET_REDACTIONS = [
68
76
  ]
69
77
 
70
78
 
79
+ def normalize_process_name(name):
80
+ """Normaliza el nombre del proceso (skill padre) para título y cuerpo
81
+ del Issue. Orden V2-aprendido: validar la entrada CRUDA (longitud y
82
+ corchetes) ANTES de sanitizar — los marcadores [REDactado:...] que
83
+ introduce la sanitización son legítimos y TITLE_RE los acepta."""
84
+ if name is None:
85
+ return None
86
+ text = str(name).strip()
87
+ if not text:
88
+ return None
89
+ if len(text) > MAX_PROCESS_NAME_LEN:
90
+ raise SkillError(
91
+ EXIT_VALIDATION, "VALIDATION_ERROR",
92
+ f"el nombre del proceso no puede exceder {MAX_PROCESS_NAME_LEN} "
93
+ f"caracteres (recibido: {len(text)})")
94
+ if any(ch in text for ch in "[]\r\n"):
95
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
96
+ "el nombre del proceso no puede contener '[', ']' "
97
+ "ni saltos de línea (rompe el título canónico)")
98
+ clean, _ = sanitize_text(text)
99
+ clean = clean.strip()
100
+ if not clean:
101
+ raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
102
+ "el nombre del proceso quedó vacío tras sanitizar")
103
+ return clean
104
+
105
+
106
+ def issue_title(execution_id, process_name=None):
107
+ """Título canónico del Issue con el nombre del proceso (v0.5.0).
108
+ El nombre debe venir YA normalizado (normalize_process_name); aquí no
109
+ se re-valida porque los marcadores [REDactado:...] son legítimos."""
110
+ if process_name:
111
+ return f"[Process Tracking][{process_name}] Execution ID: {execution_id}"
112
+ return f"[Process Tracking] Execution ID: {execution_id}"
113
+
114
+
115
+
71
116
  class SkillError(Exception):
72
117
  """Error operativo que se serializa como un único objeto JSON."""
73
118
 
@@ -98,13 +98,42 @@ def events_from_comments(comments):
98
98
  return events
99
99
 
100
100
 
101
- def build_issue_body(url, execution_id, plan):
101
+ def _normalized_inputs(process_inputs):
102
+ """Normaliza los parámetros de entrada a pares (clave, valor) ordenados,
103
+ con ambos convertidos a cadena. Devuelve [] si no hay inputs."""
104
+ if not process_inputs:
105
+ return []
106
+ if not isinstance(process_inputs, dict):
107
+ return []
108
+ entries = []
109
+ for key in sorted(process_inputs, key=str):
110
+ value = process_inputs[key]
111
+ entries.append((str(key).strip(), str(value)))
112
+ return [(k, v) for k, v in entries if k]
113
+
114
+
115
+ def build_issue_body(url, execution_id, plan, process_name=None,
116
+ process_inputs=None):
102
117
  lines = [
103
118
  "Proceso rastreado por el skill `github-proyect-tracking`.",
104
119
  "",
105
120
  f"- **Execution ID:** `{execution_id}`",
121
+ ]
122
+ if process_name:
123
+ lines.append(f"- **Proceso:** {process_name}")
124
+ lines += [
106
125
  f"- **Proyecto:** {url}",
107
126
  f"- **Iniciado (UTC):** {now_iso()}",
127
+ ]
128
+
129
+ # v0.6.0: parámetros de entrada del proceso — la única forma de saber
130
+ # exactamente qué tarea estaba realizando esta ejecución.
131
+ entries = _normalized_inputs(process_inputs)
132
+ if entries:
133
+ lines += ["", "## Parámetros de entrada", ""]
134
+ lines += [f"- **{key}:** `{value}`" for key, value in entries]
135
+
136
+ lines += [
108
137
  "",
109
138
  "## Plan de pasos",
110
139
  ]
@@ -14,7 +14,7 @@ import uuid
14
14
  from .common import (
15
15
  AMBIGUOUS_HTTP_STATUSES, API_BASE, EXIT_NOT_FOUND, EXIT_NO_TOKEN,
16
16
  EXIT_RUNTIME, EXIT_TOKEN_INSUFFICIENT, MAX_COMMENT_PAGES, TITLE_RE,
17
- USER_AGENT, SkillError,
17
+ USER_AGENT, SkillError, issue_title,
18
18
  )
19
19
  from .events import events_from_comments, transition_metadata, with_transition_metadata
20
20
 
@@ -295,6 +295,7 @@ def close_issue(owner, repo, number, token):
295
295
 
296
296
 
297
297
  def find_issue_by_title(owner, repo, title, token):
298
+ # v0.5.0: title ya viene construido con issue_title()
298
299
  url = (f"{API_BASE}/repos/{owner}/{repo}/issues?state=all&sort=created"
299
300
  "&direction=desc&per_page=100")
300
301
  seen = set()
@@ -310,6 +311,14 @@ def find_issue_by_title(owner, repo, title, token):
310
311
  return None
311
312
 
312
313
 
313
- def canonical_title_match(issue, execution_id):
314
+ def canonical_title_match(issue, execution_id, process_name=None):
315
+ """v0.5.0: casa el título canónico con o sin nombre de proceso; el
316
+ exec_id debe coincidir EXACTO y, si se indica process_name, también."""
314
317
  match = TITLE_RE.match((issue.get("title") or "").strip())
315
- return bool(match and match.group("exec_id").lower() == execution_id.lower())
318
+ if not match or match.group("exec_id").lower() != execution_id.lower():
319
+ return False
320
+ if process_name:
321
+ found = match.group("process_name")
322
+ if (found or "").strip().lower() != str(process_name).strip().lower():
323
+ return False
324
+ return True