github-proyect-tracking 0.4.1 → 0.6.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/README.md +67 -11
- package/bin/cli.js +0 -0
- package/package.json +1 -1
- package/skill/SKILL.md +14 -5
- package/skill/references/api.md +15 -1
- package/skill/scripts/tracking.py +12 -1
- package/skill/scripts/tracking_core/commands.py +68 -8
- package/skill/scripts/tracking_core/common.py +47 -2
- package/skill/scripts/tracking_core/events.py +30 -1
- package/skill/scripts/tracking_core/github.py +12 -3
package/README.md
CHANGED
|
@@ -6,11 +6,20 @@ de GitHub mediante Issues y comentarios**. Si el proceso se interrumpe (error,
|
|
|
6
6
|
caída de sesión, reinicio), el skill reanuda exactamente donde quedó leyendo el
|
|
7
7
|
último comentario del Issue.
|
|
8
8
|
|
|
9
|
-
> **v0.
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
9
|
+
> **v0.6.0** — el Issue documenta QUÉ y CON QUÉ: `--process-name` nombra
|
|
10
|
+
> el proceso (título `[Process Tracking][N] …`) y `--process-inputs
|
|
11
|
+
> '{"k":"v",…}` publica los parámetros de entrada del proceso en una
|
|
12
|
+
> sección del cuerpo (secretos redactados) — la única forma de saber
|
|
13
|
+
> exactamente qué tarea estaba realizando la ejecución.
|
|
14
|
+
>
|
|
15
|
+
> **v0.5.0** — el Issue ahora identifica el proceso rastreado:
|
|
16
|
+
> `init --process-name "Mi Skill"` produce el título
|
|
17
|
+
> `[Process Tracking][Mi Skill] Execution ID: …` y el cuerpo incluye
|
|
18
|
+
> `- **Proceso:** Mi Skill` (compatibilidad total con Issues anteriores).
|
|
19
|
+
> Conserva las correcciones de `Review_V3.md` (v0.4.x): resultados
|
|
20
|
+
> terminales derivados del Issue, recuperación explícita tras `ERROR`,
|
|
21
|
+
> reconciliación por UUID, heartbeat, esquema v2 con migración e
|
|
22
|
+
> instaladores transaccionales.
|
|
14
23
|
|
|
15
24
|
---
|
|
16
25
|
|
|
@@ -19,7 +28,14 @@ caída de sesión, reinicio), el skill reanuda exactamente donde quedó leyendo
|
|
|
19
28
|
`github-proyect-tracking` convierte cada ejecución de un proceso en un Issue de
|
|
20
29
|
GitHub que funciona como bitácora persistente y auditable:
|
|
21
30
|
|
|
22
|
-
- **Un Issue por ejecución**, titulado
|
|
31
|
+
- **Un Issue por ejecución**, titulado
|
|
32
|
+
`[Process Tracking][{proceso}] Execution ID: {id}` — el nombre del proceso
|
|
33
|
+
(skill padre) se declara con `--process-name` y deja claro **qué** se
|
|
34
|
+
rastrea; sin él se conserva el formato legado
|
|
35
|
+
`[Process Tracking] Execution ID: {id}`.
|
|
36
|
+
- **Parámetros de entrada del proceso** (`--process-inputs '{"k":"v",…}`): se
|
|
37
|
+
publican en la sección "Parámetros de entrada" del cuerpo del Issue — la
|
|
38
|
+
constancia de **con qué** se ejecutó la tarea (secretos redactados).
|
|
23
39
|
- **Cada transición del proceso es un comentario** con un contrato visible
|
|
24
40
|
cerrado y sin ambigüedades:
|
|
25
41
|
|
|
@@ -51,6 +67,7 @@ transición. No hay dependencias de terceros: solo Python 3.8+ (stdlib);
|
|
|
51
67
|
| | |
|
|
52
68
|
|---|---|
|
|
53
69
|
| Entrada obligatoria | `github_project_url` — `https://github.com/{owner}/{repo}` |
|
|
70
|
+
| Identificación del proceso | `--process-name N` (skill padre; título del Issue) y `--process-inputs JSON` (parámetros de entrada, sección del cuerpo) — ambos opcionales pero recomendados |
|
|
54
71
|
| Salida de cada comando | Un único objeto JSON + exit code documentado |
|
|
55
72
|
| Permisos requeridos | PAT classic con scope `repo`, o fine-grained con Issues → Read and write |
|
|
56
73
|
| Plataformas | Linux, macOS, Windows |
|
|
@@ -145,7 +162,7 @@ npm publish # prepack empaqueta skill/ + README + LICENSE automáticament
|
|
|
145
162
|
```
|
|
146
163
|
|
|
147
164
|
`npm pack` genera el tarball verificable de forma idéntica
|
|
148
|
-
(`npx --yes ./npm/github-proyect-tracking-0.
|
|
165
|
+
(`npx --yes ./npm/github-proyect-tracking-0.5.0.tgz install`).
|
|
149
166
|
|
|
150
167
|
### Desinstalación
|
|
151
168
|
|
|
@@ -168,7 +185,7 @@ cp -R github-proyect-tracking/ ~/.hermes/profiles/<perfil>/skills/software-devel
|
|
|
168
185
|
### Ejecutar las pruebas
|
|
169
186
|
|
|
170
187
|
```bash
|
|
171
|
-
python3 tests/test_tracking.py #
|
|
188
|
+
python3 tests/test_tracking.py # 172 pruebas: 140 offline + 1 skip opt-in
|
|
172
189
|
```
|
|
173
190
|
|
|
174
191
|
---
|
|
@@ -331,9 +348,48 @@ code documentado:
|
|
|
331
348
|
```json
|
|
332
349
|
{ "ok": true, "action": "INIT", "execution_id": "exec-20261028T101500Z-ab12cd34ef56",
|
|
333
350
|
"issue_number": 42, "issue_url": "https://github.com/o/r/issues/42",
|
|
351
|
+
"process_name": "Mi Skill", "issue_title": "[Process Tracking][Mi Skill] Execution ID: exec-20261028T101500Z-ab12cd34ef56",
|
|
352
|
+
"process_inputs": { "archivo": "datos.csv", "modo": "lint" },
|
|
334
353
|
"state_file": "~/.github-proyect-tracking/o__r.json", "first_step": "Paso 1" }
|
|
335
354
|
```
|
|
336
355
|
|
|
356
|
+
`process_name` y `process_inputs` son `null` cuando no se pasaron las
|
|
357
|
+
opciones correspondientes.
|
|
358
|
+
|
|
359
|
+
### Ejemplo del Issue generado
|
|
360
|
+
|
|
361
|
+
Con `init --url … --process-name "Mi Skill" --process-inputs
|
|
362
|
+
'{"archivo":"datos.csv","modo":"lint"}' --plan '["Paso 1","Paso 2"]'`:
|
|
363
|
+
|
|
364
|
+
```markdown
|
|
365
|
+
[Process Tracking][Mi Skill] Execution ID: exec-20261030T153448Z-9071da2dae76
|
|
366
|
+
```
|
|
367
|
+
*(título)* — y el cuerpo:
|
|
368
|
+
|
|
369
|
+
```markdown
|
|
370
|
+
Proceso rastreado por el skill `github-proyect-tracking`.
|
|
371
|
+
|
|
372
|
+
- **Execution ID:** `exec-20261030T153448Z-9071da2dae76`
|
|
373
|
+
- **Proceso:** Mi Skill
|
|
374
|
+
- **Proyecto:** https://github.com/o/r
|
|
375
|
+
- **Iniciado (UTC):** 2026-10-30T15:34:48+00:00
|
|
376
|
+
|
|
377
|
+
## Parámetros de entrada
|
|
378
|
+
|
|
379
|
+
- **archivo:** `datos.csv`
|
|
380
|
+
- **modo:** `lint`
|
|
381
|
+
|
|
382
|
+
## Plan de pasos
|
|
383
|
+
|
|
384
|
+
1. Paso 1
|
|
385
|
+
2. Paso 2
|
|
386
|
+
|
|
387
|
+
> El avance se registra como comentarios con el contrato `[STATUS: ...]`.
|
|
388
|
+
> El ÚLTIMO comentario es la fuente de verdad para reanudar.
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Ejemplo de error (JSON con `ok: false` + exit code documentado):
|
|
392
|
+
|
|
337
393
|
```json
|
|
338
394
|
{ "ok": false, "error": { "code": "RESUME_REQUIRED", "message": "...",
|
|
339
395
|
"state": { "...": "..." }, "next": ["resume"] } }
|
|
@@ -358,7 +414,7 @@ Excepciones documentadas al contrato "un JSON por comando": `--help`/
|
|
|
358
414
|
|
|
359
415
|
| Comando | Efecto | Efectos secundarios |
|
|
360
416
|
|---|---|---|
|
|
361
|
-
| `init --url URL [--plan JSON \| --plan-file F] [--token T] [--store-token]` | Crea el Issue y la memoria (plan sanitizado) | Issue nuevo en GitHub + archivo local |
|
|
417
|
+
| `init --url URL [--plan JSON \| --plan-file F] [--token T] [--store-token] [--process-name N] [--process-inputs JSON]` | Crea el Issue y la memoria (plan sanitizado; `N` = skill padre en título/cuerpo; JSON = parámetros de entrada del proceso, publicados en el Issue con secretos redactados) | Issue nuevo en GitHub + archivo local |
|
|
362
418
|
| `resume [--url URL]` | Veredicto de reanudación | Actualiza memoria local |
|
|
363
419
|
| `step-start --name N` | Comenta INICIANDO | Comentario + memoria |
|
|
364
420
|
| `step-done --name N` | Comenta COMPLETADO | Comentario + memoria |
|
|
@@ -411,7 +467,7 @@ rechazan.
|
|
|
411
467
|
|
|
412
468
|
### 4.6 Pruebas
|
|
413
469
|
|
|
414
|
-
La suite descubre **
|
|
470
|
+
La suite descubre **172 pruebas**: **140 offline** y una integración real
|
|
415
471
|
omitida por defecto. Las pruebas offline usan stdlib, `FakeGateway`, inyección
|
|
416
472
|
de fallos, subprocesos CLI e instaladores reales en directorios temporales.
|
|
417
473
|
Cubren los formatos visibles, máquina de estados, reconciliación con UUID y
|
|
@@ -420,7 +476,7 @@ contexto ambiguo, seguridad, rollback y contrato JSON.
|
|
|
420
476
|
|
|
421
477
|
```bash
|
|
422
478
|
python3 tests/test_tracking.py
|
|
423
|
-
# Ran
|
|
479
|
+
# Ran 172 tests ... OK (skipped=1)
|
|
424
480
|
python3 tools/check_docs.py
|
|
425
481
|
```
|
|
426
482
|
|
package/bin/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-proyect-tracking",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Instalador npx del skill github-proyect-tracking para Hermes Agent: rastrea el estado de procesos en GitHub Issues con reanudacion.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"github-proyect-tracking": "bin/cli.js"
|
package/skill/SKILL.md
CHANGED
|
@@ -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
|
+
version: 0.6.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).
|
|
99
|
-
|
|
100
|
-
|
|
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
|
package/skill/references/api.md
CHANGED
|
@@ -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`.
|
|
@@ -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",
|
|
@@ -153,6 +161,9 @@ def build_parser():
|
|
|
153
161
|
common(subparser, url_required=True)
|
|
154
162
|
subparser.add_argument("--execution-id", required=True, metavar="ID",
|
|
155
163
|
help="Execution ID EXACTO del título del Issue")
|
|
164
|
+
subparser.add_argument("--process-name", default=None, metavar="N",
|
|
165
|
+
help="nombre del proceso a adoptar (filtro adicional "
|
|
166
|
+
"opcional si hay ambigüedad)")
|
|
156
167
|
subparser.add_argument("--force", action="store_true",
|
|
157
168
|
help="reemplazar un proceso activo existente (conserva token "
|
|
158
169
|
"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,
|
|
18
|
-
|
|
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 =
|
|
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"]),
|
|
@@ -653,8 +703,9 @@ def cmd_adopt(args):
|
|
|
653
703
|
existing = load_state(path)
|
|
654
704
|
token, _ = need_token(args, existing)
|
|
655
705
|
|
|
656
|
-
|
|
657
|
-
|
|
706
|
+
# v0.5.0: el título puede incluir [Nombre]; se busca por el execution_id
|
|
707
|
+
# (único) y el filtrado canónico exacto lo hace canonical_title_match.
|
|
708
|
+
query = f'repo:{owner}/{repo} "{execution_id}" in:title is:issue'
|
|
658
709
|
status, data, _ = github.api(
|
|
659
710
|
"GET", "/search/issues?" + urllib.parse.urlencode({"q": query, "per_page": 30}),
|
|
660
711
|
token)
|
|
@@ -662,8 +713,10 @@ def cmd_adopt(args):
|
|
|
662
713
|
raise SkillError(EXIT_RUNTIME, "SEARCH_FAILED",
|
|
663
714
|
f"búsqueda de Issues devolvió {status}: "
|
|
664
715
|
f"{data.get('message', '')}")
|
|
716
|
+
process_name = normalize_process_name(getattr(args, "process_name", None))
|
|
665
717
|
matches = [issue for issue in (data.get("items") or [])
|
|
666
|
-
if github.canonical_title_match(issue, execution_id
|
|
718
|
+
if github.canonical_title_match(issue, execution_id,
|
|
719
|
+
process_name=process_name)]
|
|
667
720
|
if not matches:
|
|
668
721
|
raise SkillError(EXIT_NOT_FOUND, "ISSUE_NOT_FOUND",
|
|
669
722
|
f"no se encontró un Issue con título canónico para "
|
|
@@ -691,6 +744,10 @@ def cmd_adopt(args):
|
|
|
691
744
|
plan = parse_plan_from_body(issue.get("body") or "")
|
|
692
745
|
verdict = plan_resume(events, plan, issue.get("state", "open"))
|
|
693
746
|
action = verdict["action"]
|
|
747
|
+
title_match = TITLE_RE.match((issue.get("title") or "").strip())
|
|
748
|
+
adopted_name = (title_match.group("process_name")
|
|
749
|
+
if title_match and title_match.group("process_name")
|
|
750
|
+
else None)
|
|
694
751
|
if issue.get("state") == "open":
|
|
695
752
|
active = number
|
|
696
753
|
process_state = ("ERROR" if events and events[-1]["status"] == STATUS_ERROR
|
|
@@ -708,6 +765,7 @@ def cmd_adopt(args):
|
|
|
708
765
|
"owner": owner, "repo": repo,
|
|
709
766
|
"active_process_id": active,
|
|
710
767
|
"execution_id": execution_id,
|
|
768
|
+
"process_name": process_name or adopted_name,
|
|
711
769
|
"issue_url": issue.get("html_url"),
|
|
712
770
|
"process_state": process_state,
|
|
713
771
|
"current_step": verdict.get("resume_from_step"),
|
|
@@ -727,7 +785,9 @@ def cmd_adopt(args):
|
|
|
727
785
|
"ok": True, "action": "ADOPT", "execution_id": execution_id,
|
|
728
786
|
"issue_number": number, "issue_url": issue.get("html_url"),
|
|
729
787
|
"issue_state": issue.get("state"), "verdict": verdict,
|
|
730
|
-
"process_state": process_state,
|
|
788
|
+
"process_state": process_state,
|
|
789
|
+
"process_name": process_name or adopted_name,
|
|
790
|
+
"state_file": str(path),
|
|
731
791
|
})
|
|
732
792
|
return EXIT_OK
|
|
733
793
|
|
|
@@ -7,7 +7,7 @@ import re
|
|
|
7
7
|
import uuid
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
|
|
10
|
-
VERSION = "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\]
|
|
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
|
|
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
|
-
|
|
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
|