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 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.4.0** — correcciones de `Review_V3.md`: resultados terminales derivados
10
- > del Issue, recuperación explícita tras `ERROR`, reconciliación por UUID ante
11
- > respuestas perdidas o 5xx, heartbeat durante toda la operación, esquema local
12
- > v2 con migración e instaladores transaccionales. El CLI se separó en módulos
13
- > sin cambiar su punto de entrada ni sus formatos visibles.
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 `[Process Tracking] Execution ID: {id}`.
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.4.0.tgz install`).
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 # 141 pruebas: 140 offline + 1 skip opt-in
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 **141 pruebas**: **140 offline** y una integración real
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 141 tests ... OK (skipped=1)
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.4.1",
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.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). 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`.
@@ -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, 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"]),
@@ -653,8 +703,9 @@ def cmd_adopt(args):
653
703
  existing = load_state(path)
654
704
  token, _ = need_token(args, existing)
655
705
 
656
- query = (f'repo:{owner}/{repo} "[Process Tracking] Execution ID: {execution_id}" '
657
- f'in:title is:issue')
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, "state_file": str(path),
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.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