github-proyect-tracking 0.0.0-stage → 0.4.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/bin/cli.js ADDED
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * github-proyect-tracking — instalador tipo npx del skill para Hermes Agent.
4
+ *
5
+ * npx --yes github-proyect-tracking (install)
6
+ * npx --yes github-proyect-tracking doctor --url https://github.com/{owner}/{repo}
7
+ * npx --yes github-proyect-tracking uninstall [--purge-state]
8
+ *
9
+ * Comandos: install (por defecto) · doctor · uninstall · run (paso a tracking.py)
10
+ * Flags: --dest RUTA · --profile PERFIL · --user · --purge-state · -h/-v
11
+ *
12
+ * Sin dependencias npm (Node >= 18). El skill en sí es Python 3.8+ stdlib.
13
+ * La resolución de destino replica install.sh: HERMES_HOME → perfil activo
14
+ * (HERMES_PROFILE o la línea "profile:" del AGENT.md del directorio actual)
15
+ * → perfil default. install.sh y este instalador son intercambiables.
16
+ */
17
+ 'use strict';
18
+
19
+ const { spawnSync } = require('node:child_process');
20
+ const fs = require('node:fs');
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+
24
+ const SKILL_NAME = 'github-proyect-tracking';
25
+ const CATEGORY = 'software-development';
26
+ const VERSION = require('../package.json').version;
27
+ const SKILL_SRC = path.join(__dirname, '..', 'skill'); // copia empaquetada
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // utilidades
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const log = (m = '') => console.log(m);
34
+ const die = (msg, code = 1) => { console.error(`ERROR: ${msg}`); process.exit(code); };
35
+
36
+ function pythonCmd() {
37
+ for (const c of ['python3', 'python']) {
38
+ const r = spawnSync(c, ['-c', ''], { stdio: 'ignore' });
39
+ if (!r.error) return c;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function parseArgs(argv) {
45
+ const out = { command: null, flags: {}, rest: [] };
46
+ const valueFlags = new Set(['--dest', '--profile']); // V2-E-12
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a === '-h' || a === '--help') out.flags.help = true;
50
+ else if (a === '-v' || a === '--version') out.flags.version = true;
51
+ else if (valueFlags.has(a)) {
52
+ // V2-E-12: flag que exige valor — sin valor → error de uso (exit 2)
53
+ const v = argv[i + 1];
54
+ if (v === undefined || !v.trim() || v.startsWith('-')) {
55
+ die(`el flag ${a} requiere un valor no vacío (ej. ${a} RUTA)`, 2);
56
+ }
57
+ out.flags[a === '--dest' ? 'dest' : 'profile'] = v;
58
+ i++;
59
+ }
60
+ else if (a === '--user') out.flags.user = true;
61
+ else if (a === '--purge-state') out.flags.purgeState = true;
62
+ else if (a === '--') { out.rest.push(...argv.slice(i + 1)); break; }
63
+ else if (a.startsWith('-')) out.rest.push(a); // p.ej. --url (doctor/run)
64
+ else if (out.command === null) out.command = a;
65
+ else out.rest.push(a);
66
+ }
67
+ if (out.command === null) out.command = 'install';
68
+ return out;
69
+ }
70
+
71
+ function profileFromAgentMd(cwd) {
72
+ for (const f of ['AGENT.md', 'AGENTS.md']) {
73
+ const p = path.join(cwd, f);
74
+ if (fs.existsSync(p)) {
75
+ const m = fs.readFileSync(p, 'utf8').match(/^profile\s*[:=]\s*(\S+)/m);
76
+ if (m) return m[1];
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function resolveDest(flags) {
83
+ if (flags.dest) return flags.dest;
84
+ const hermesHome = process.env.HERMES_HOME || path.join(os.homedir(), '.hermes');
85
+ if (flags.user || !fs.existsSync(path.join(hermesHome, 'profiles'))) {
86
+ return path.join(hermesHome, 'skills');
87
+ }
88
+ const explicit = Boolean(flags.profile);
89
+ let profile = flags.profile || process.env.HERMES_PROFILE
90
+ || profileFromAgentMd(process.cwd());
91
+ const exists = (p) => fs.existsSync(path.join(hermesHome, 'profiles', p));
92
+ // E-24: --profile EXPLÍCITO inexistente → error claro, no default
93
+ if (explicit && !exists(profile)) {
94
+ die(`el perfil '${profile}' no existe en ${hermesHome}/profiles/ ` +
95
+ '(usa --profile con un perfil válido o --user)', 2);
96
+ }
97
+ // V2-E-11: AUTOMÁTICO inexistente → fallback a default CON aviso
98
+ // (paridad con lib/common.sh)
99
+ if (!profile || !exists(profile)) {
100
+ console.error(`ADVERTENCIA: el perfil automático '${profile || "(ninguno)"}' ` +
101
+ 'no existe; usando default (pasa --profile para elegir uno)');
102
+ profile = 'default';
103
+ if (!exists(profile)) {
104
+ die(`tampoco existe el perfil 'default' en ${hermesHome}/profiles/`, 2);
105
+ }
106
+ }
107
+ return path.join(hermesHome, 'profiles', profile, 'skills');
108
+ }
109
+
110
+ function cleanCaches(dir) {
111
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
112
+ const p = path.join(dir, e.name);
113
+ if (e.isDirectory()) {
114
+ if (e.name === '__pycache__') fs.rmSync(p, { recursive: true, force: true });
115
+ else cleanCaches(p);
116
+ } else if (/\.py[co]$/.test(e.name)) {
117
+ fs.rmSync(p, { force: true });
118
+ }
119
+ }
120
+ }
121
+
122
+ function validateFrontmatter(skillDir) {
123
+ const md = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
124
+ if (!md.startsWith('---')) throw new Error('SKILL.md: falta "---" inicial');
125
+ const rel = md.slice(3).search(/\n---\s*\n/);
126
+ if (rel < 0) throw new Error('SKILL.md: falta cierre "---"');
127
+ const fm = md.slice(3, 3 + rel);
128
+ const name = (fm.match(/^name:\s*(.+?)\s*$/m) || [])[1];
129
+ let desc = (fm.match(/^description:\s*(.+?)\s*$/m) || [])[1] || '';
130
+ if (desc.startsWith('"') && desc.endsWith('"')) desc = desc.slice(1, -1);
131
+ if (name !== SKILL_NAME) throw new Error(`name=${name} (esperado ${SKILL_NAME})`);
132
+ if (!desc || desc.length > 60) {
133
+ throw new Error(`description (${desc.length} chars) vacía o > 60`);
134
+ }
135
+ }
136
+
137
+ function verifyTrackingPy(target) {
138
+ // E-23: la verificación del script es OBLIGATORIA (exit != 0 si falla)
139
+ const py = pythonCmd();
140
+ if (!py) throw new Error('python3 no está en PATH; el skill lo requiere');
141
+ const r = spawnSync(py, [path.join(target, 'scripts', 'tracking.py'), '--version'],
142
+ { encoding: 'utf8' });
143
+ if (r.status !== 0) {
144
+ throw new Error(`tracking.py --version falló (exit ${r.status}); ` +
145
+ 'instalación abortada');
146
+ }
147
+ return r.stdout.trim();
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // comandos
152
+ // ---------------------------------------------------------------------------
153
+
154
+ function install(flags) {
155
+ if (!fs.existsSync(path.join(SKILL_SRC, 'SKILL.md'))) {
156
+ die(`paquete corrupto: falta ${SKILL_SRC}/SKILL.md`, 2);
157
+ }
158
+ const dest = resolveDest(flags);
159
+ const target = path.join(dest, CATEGORY, SKILL_NAME);
160
+ log(`Origen : ${SKILL_SRC}`);
161
+ log(`Destino: ${target}`);
162
+
163
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'gpt-npx-'));
164
+ const stageSkill = path.join(stage, SKILL_NAME);
165
+ const unique = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
166
+ const staged = `${target}.new.${unique}`;
167
+ const bak = `${target}.bak.${unique}`;
168
+ let swapStarted = false;
169
+ try {
170
+ fs.cpSync(SKILL_SRC, stageSkill, { recursive: true });
171
+ cleanCaches(stageSkill);
172
+ validateFrontmatter(stageSkill);
173
+ log('frontmatter: OK (name + description <= 60)');
174
+ const preVer = verifyTrackingPy(stageSkill);
175
+ if (preVer) log(preVer);
176
+
177
+ fs.mkdirSync(path.dirname(target), { recursive: true });
178
+ fs.cpSync(stageSkill, staged, { recursive: true });
179
+ if (fs.existsSync(target)) {
180
+ fs.cpSync(target, bak, { recursive: true });
181
+ log(`Copia previa respaldada en: ${bak}`);
182
+ }
183
+ swapStarted = true;
184
+ fs.rmSync(target, { recursive: true, force: true });
185
+ fs.renameSync(staged, target);
186
+ const ver = verifyTrackingPy(target);
187
+ if (ver) log(ver);
188
+ } catch (error) {
189
+ if (swapStarted) {
190
+ fs.rmSync(target, { recursive: true, force: true });
191
+ if (fs.existsSync(bak)) {
192
+ fs.cpSync(bak, target, { recursive: true });
193
+ log(`Rollback: restaurado ${bak}`);
194
+ } else {
195
+ log('Sin respaldo previo: la copia no usable fue eliminada');
196
+ }
197
+ }
198
+ throw error;
199
+ } finally {
200
+ fs.rmSync(stage, { recursive: true, force: true });
201
+ fs.rmSync(staged, { recursive: true, force: true });
202
+ }
203
+
204
+ const py = pythonCmd();
205
+ if (!py) {
206
+ log('ADVERTENCIA: python3 no está en PATH; el skill lo requiere.');
207
+ } else {
208
+ const r = spawnSync(py, [path.join(target, 'scripts', 'tracking.py'), '--version'],
209
+ { encoding: 'utf8' });
210
+ if (r.status === 0 && r.stdout) log(r.stdout.trim());
211
+ else log(`ADVERTENCIA: verificación de tracking.py falló (exit ${r.status})`);
212
+ }
213
+
214
+ log();
215
+ log('Instalado. Prueba rápida:');
216
+ log(` ${py || 'python3'} "${path.join(target, 'scripts', 'tracking.py')}" doctor --url https://github.com/{owner}/{repo}`);
217
+ log();
218
+ log('Nota: el skill estará visible para sesiones NUEVAS de Hermes (el catálogo');
219
+ log('se carga al iniciar sesión). La memoria interna vive en');
220
+ log('~/.github-proyect-tracking/ (override: $GITHUB_TRACKING_STATE).');
221
+ }
222
+
223
+ function uninstall(flags) {
224
+ const dest = resolveDest(flags);
225
+ const target = path.join(dest, CATEGORY, SKILL_NAME);
226
+ if (fs.existsSync(target)) {
227
+ fs.rmSync(target, { recursive: true, force: true });
228
+ log(`Eliminado: ${target}`);
229
+ try { fs.rmdirSync(path.dirname(target)); } catch { /* categoría no vacía */ }
230
+ } else {
231
+ log(`No hay instalación en: ${target} (nada que desinstalar)`);
232
+ }
233
+ if (flags.purgeState) {
234
+ const stateDir = process.env.GITHUB_TRACKING_STATE
235
+ || path.join(os.homedir(), '.github-proyect-tracking');
236
+ if (fs.existsSync(stateDir)) {
237
+ fs.rmSync(stateDir, { recursive: true, force: true });
238
+ log(`Memoria interna eliminada: ${stateDir}`);
239
+ }
240
+ log('Nota: los Issues creados en GitHub NO se eliminan (permanecen en el repo).');
241
+ }
242
+ log('Listo.');
243
+ }
244
+
245
+ function doctor(flags, rest) {
246
+ const py = pythonCmd();
247
+ if (!py) die('python3 no está en PATH (requerido por el skill)');
248
+ const candidates = [
249
+ path.join(resolveDest(flags), CATEGORY, SKILL_NAME, 'scripts', 'tracking.py'),
250
+ path.join(SKILL_SRC, 'scripts', 'tracking.py'),
251
+ ];
252
+ const script = candidates.find((p) => fs.existsSync(p));
253
+ if (!script) die('no se encontró tracking.py (¿paquete corrupto?)', 2);
254
+ const r = spawnSync(py, [script, 'doctor', ...rest], { stdio: 'inherit' });
255
+ process.exit(r.status === null ? 1 : r.status);
256
+ }
257
+
258
+ function run(flags, rest) {
259
+ const py = pythonCmd();
260
+ if (!py) die('python3 no está en PATH (requerido por el skill)');
261
+ const candidates = [
262
+ path.join(resolveDest(flags), CATEGORY, SKILL_NAME, 'scripts', 'tracking.py'),
263
+ path.join(SKILL_SRC, 'scripts', 'tracking.py'),
264
+ ];
265
+ const script = candidates.find((p) => fs.existsSync(p));
266
+ if (!script) die('no se encontró tracking.py (¿paquete corrupto?)', 2);
267
+ const r = spawnSync(py, [script, ...rest], { stdio: 'inherit' });
268
+ process.exit(r.status === null ? 1 : r.status);
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // ayuda y main
273
+ // ---------------------------------------------------------------------------
274
+
275
+ function help() {
276
+ log(`github-proyect-tracking v${VERSION} — instalador npx del skill para Hermes Agent`);
277
+ log();
278
+ log('Uso:');
279
+ log(' npx --yes github-proyect-tracking [install] [flags] instalar el skill');
280
+ log(' npx --yes github-proyect-tracking doctor [args…] diagnóstico (sin instalar)');
281
+ log(' npx --yes github-proyect-tracking run <cmd> [args…] ejecutar un comando del skill');
282
+ log(' npx --yes github-proyect-tracking uninstall [flags] desinstalar');
283
+ log();
284
+ log('Flags:');
285
+ log(' --dest RUTA destino explícito (directorio padre de skills)');
286
+ log(' --profile PERFIL perfil de Hermes (~/.hermes/profiles/<p>/skills)');
287
+ log(' --user ~/.hermes/skills del perfil por defecto');
288
+ log(' --purge-state (uninstall) borra también ~/.github-proyect-tracking/');
289
+ log(' -h, --help esta ayuda');
290
+ log(' -v, --version versión del paquete');
291
+ log();
292
+ log('El destino por defecto se resuelve igual que install.sh: HERMES_HOME,');
293
+ log('perfil activo (HERMES_PROFILE o la línea "profile:" del AGENT.md del');
294
+ log('directorio actual) y, si no, el perfil default. Requisitos: Node >= 18');
295
+ log('para este instalador; Python 3.8+ (stdlib) para el skill.');
296
+ }
297
+
298
+ function main() {
299
+ const { command, flags, rest } = parseArgs(process.argv.slice(2));
300
+ if (flags.version) { log(`github-proyect-tracking/${VERSION} (instalador npx)`); return; }
301
+ if (flags.help) { help(); return; }
302
+ if (!['doctor', 'run'].includes(command) && rest.length) {
303
+ die(`argumentos no reconocidos para ${command}: ${rest.join(' ')}`, 2);
304
+ }
305
+ if (command === 'install' && flags.purgeState) {
306
+ die('--purge-state solo es válido con uninstall', 2);
307
+ }
308
+ switch (command) {
309
+ case 'install': install(flags); break;
310
+ case 'uninstall': uninstall(flags); break;
311
+ case 'doctor': doctor(flags, rest); break;
312
+ case 'run': run(flags, rest); break;
313
+ default: die(`comando desconocido: ${command} (usa --help)`, 2);
314
+ }
315
+ }
316
+
317
+ main();
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "github-proyect-tracking",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Instalador npx del skill github-proyect-tracking para Hermes Agent: rastrea el estado de procesos en GitHub Issues con reanudacion.",
5
+ "bin": {
6
+ "github-proyect-tracking": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "skill/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "node bin/build.js",
19
+ "prepack": "node bin/build.js"
20
+ },
21
+ "keywords": [
22
+ "hermes-agent",
23
+ "skill",
24
+ "github",
25
+ "issues",
26
+ "tracking",
27
+ "state-machine",
28
+ "resume"
29
+ ],
30
+ "author": "Rommel (https://github.com/rommelmg)",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/rommelmg/KGithubTracking.git"
35
+ },
36
+ "private": false
37
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: github-proyect-tracking
3
+ description: Track process execution state in GitHub Issues with resume.
4
+ version: 0.4.0
5
+ author: Rommel (rommelmg), Hermes Agent
6
+ license: MIT
7
+ platforms: [linux, macos, windows]
8
+ metadata:
9
+ hermes:
10
+ tags: [github, tracking, state-machine, resume, issues]
11
+ category: software-development
12
+ related_skills: [github]
13
+ ---
14
+
15
+ # github-proyect-tracking Skill
16
+
17
+ Persiste el estado de ejecución de un proceso estructurado en un Issue de
18
+ GitHub (uno por ejecución) y lo reanuda tras interrupciones leyendo el
19
+ último comentario. No ejecuta los pasos del proceso: los instrumenta —
20
+ el agente hace el trabajo real y reporta cada transición con este skill.
21
+ Depende solo de Python 3.8+ (stdlib); `gh` es opcional (fuente de token).
22
+
23
+ **v0.4.0**: cada transición nueva lleva metadatos HTML con `transition_id`
24
+ UUID para reconciliar respuestas perdidas y 500/502/503/504 sin confundir
25
+ otro emisor.
26
+ El resultado CANCELADO se conserva en reintentos, `resume` y `adopt`; tras
27
+ ERROR se exige una reanudación explícita. El lock tiene heartbeat automático,
28
+ el estado usa esquema v2 con migración desde v1 y los instaladores restauran
29
+ el respaldo exacto ante fallos.
30
+
31
+ ## When to Use
32
+
33
+ - El usuario pide rastrear/persistir la ejecución de un proceso con estados
34
+ verificables en GitHub (Issues como bitácora) y reanudar tras fallos.
35
+ - Un proceso se interrumpió (error, caída de sesión) y hay que continuar
36
+ exactamente donde quedó, usando el Issue como fuente de verdad.
37
+ - Contador: "trackea este proceso en GitHub", "reanuda donde quedó",
38
+ "registrando el avance del proceso".
39
+
40
+ Don't use for:
41
+ - Crear issues/PRs de mantenimiento sin seguimiento de proceso
42
+ (usa el skill `github`).
43
+ - Persistir secretos en el cuerpo de un Issue (los `Detalles` se
44
+ sanitizan por patrones, pero no guarantees absolutas: no metas
45
+ credenciales).
46
+
47
+ ## Prerequisites
48
+
49
+ - `github_project_url` exacta: `https://github.com/{owner}/{repo}`
50
+ (obligatoria en `init`; owner/repo se normalizan a minúsculas).
51
+ - Token de GitHub con permisos de escritura sobre Issues (classic PAT
52
+ scope `repo`; fine-grained: Issues → Read and write). Resolución
53
+ automática: `--token` → `GITHUB_TRACKING_TOKEN` → `GITHUB_TOKEN` →
54
+ `GH_TOKEN` → `gh auth token` → archivo de estado (solo con
55
+ `--store-token`). Sin token → pídelo al usuario; nunca lo inventes.
56
+ - Python 3.8+ en PATH (`python3`).
57
+
58
+ ## How to Run
59
+
60
+ Todo corre por `terminal` con un único JSON de respuesta por comando.
61
+ Ruta del script relativa a este skill: `scripts/tracking.py`:
62
+
63
+ ```bash
64
+ SKILL="$HOME/.hermes/profiles/<profile>/skills/software-development/github-proyect-tracking/scripts/tracking.py"
65
+ python3 "$SKILL" doctor --url https://github.com/OWNER/REPO --token "$GITHUB_TOKEN"
66
+ ```
67
+
68
+ ## Quick Reference
69
+
70
+ | Comando | Efecto |
71
+ |---|---|
72
+ | `init --url URL [--plan JSON] [--store-token]` | Valida URL+token, crea el Issue, guarda `active_process_id` |
73
+ | `resume [--url URL]` | Lee el ÚLTIMO comentario y dicta cómo continuar |
74
+ | `step-start --name N` | Comenta `[STATUS: INICIANDO] - Paso: N` (valida orden del plan) |
75
+ | `step-done --name N` | Comenta `[STATUS: COMPLETADO] - Paso: N` (exige paso en vuelo) |
76
+ | `step-error --name N --details D` | Comenta `[STATUS: ERROR]` sanitizado y detiene |
77
+ | `finalize [--force]` | FINALIZADO, o CANCELADO con `--force`; cierra y limpia memoria |
78
+ | `status [--remote]` | Estado local (+ veredicto remoto) |
79
+ | `adopt --url URL --execution-id ID [--force]` | Recupera memoria desde un Issue remoto |
80
+ | `clear [--hard]` | Limpia `active_process_id` (o borra el archivo) |
81
+ | `doctor [--url URL]` | Diagnóstico de entorno y permisos (exit 0) |
82
+
83
+ Exit codes: `0` ok · `2` validación/transición inválida · `3` token sin
84
+ permiso · `4` sin token · `5` sin proceso activo · `6` Issue no
85
+ encontrado/ambiguo · `7` resume requerido · `8` conflicto con el estado
86
+ remoto (Issue cerrado externamente o finalize bloqueado). Excepciones
87
+ documentadas al contrato JSON: `--help`/`--version` (salida humana,
88
+ exit 0) y `KeyboardInterrupt` (130).
89
+
90
+ ## Procedure
91
+
92
+ 1. **Preparar.** `doctor --url URL`. Si `token` o `issues-write` falla,
93
+ pide el PAT al usuario. Criterio: JSON con `"ok": true` en checks
94
+ requeridos (gh es opcional y no rompe `ok`). Un rol demostrado de solo
95
+ lectura hace fallar `issues-write`; `UNVERIFIED` se reserva para permisos
96
+ granulares que la API no puede confirmar.
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.
101
+ 3. **Reanudar (si aplica).** `resume` y sigue el `verdict.action`
102
+ (`RERUN_STEP`, `NEXT_STEP`, `START_FIRST`, `ALL_STEPS_DONE`/
103
+ `FINISH_CLEANUP` → finalize, `ABORTED_EXTERNALLY` → pregunta al
104
+ usuario, `NEXT_STEP_AGENT_DEFINED`). `NONE` y `FINISH_CLEANUP` llevan
105
+ `outcome=FINALIZADO|CANCELADO`; conserva ese resultado.
106
+ 4. **Ejecutar pasos.** Por cada paso, en orden estricto: `step-start`
107
+ ANTES de trabajar; si el trabajo falla, `step-error` (el proceso se
108
+ detiene; reanudar exige `resume` y solo permite reintentar ese paso);
109
+ si triunfa, `step-done`. Con plan, el CLI rechaza pasos fuera de orden,
110
+ desconocidos, duplicados o sin `step-start` previo (exit 2). Criterio:
111
+ `comment_url` por transición.
112
+ 5. **Finalizar.** Con todos los pasos COMPLETADO: `finalize` — rechaza
113
+ cerrar procesos incompletos (exit 8 `FINALIZE_BLOCKED`) salvo
114
+ `--force` (que registra CANCELADO, no éxito), y nunca un Issue cerrado
115
+ externamente. Sin plan exige al menos un COMPLETADO para éxito; sin
116
+ trabajo, `--force` cancela. Criterio: `issue_state: "closed"`,
117
+ `active_process_id: null` y `outcome` coherente.
118
+ 6. **Fallos transitorios de red.** GET/PATCH reintentan solos. Si un POST
119
+ pierde la respuesta o recibe 500/502/503/504, se reconcilia por el UUID
120
+ del intento antes de reintentar UNA vez; otro emisor con el mismo texto
121
+ no lo confirma. `init` también busca el Issue por título con paginación.
122
+
123
+ Memoria interna: `~/.github-proyect-tracking/{owner}__{repo}.json`
124
+ (owner/repo en minúsculas; esquema v2, migración automática desde v1 al
125
+ guardar, escritura atómica con tmp único + fsync y permisos 0600). El candado
126
+ por repositorio mantiene heartbeat durante toda la operación y comprueba su
127
+ propiedad antes de efectos remotos y escrituras. `active_process_id` (número
128
+ de Issue; null si no hay proceso) y `github_token` (SOLO con `--store-token`;
129
+ enmascarado en salidas). Overridable con `GITHUB_TRACKING_STATE`.
130
+
131
+ ## Pitfalls
132
+
133
+ - **Título exacto del Issue:** `[Process Tracking] Execution ID: {ID}` —
134
+ no lo edites; `adopt` exige igualdad canónica (IDs parciales se
135
+ rechazan).
136
+ - **Contrato de comentarios:** los formatos visibles `[STATUS: ...]` son
137
+ exactos. La línea HTML de metadatos no cambia ese contrato. Un comentario
138
+ manual que imite el formato se interpretará como evento (el modelo de
139
+ confianza es "cualquiera con acceso al repo puede comentar").
140
+ - **Sanitización de secretos:** `Detalles` y nombres de paso se redactan
141
+ por patrones (tokens GitHub/AWS, Authorization/Bearer, passwords,
142
+ URLs con credenciales) ANTES de publicar — no es una garantía
143
+ absoluta: revisa lo que envías.
144
+ - **`init` con proceso inconcluso** falla con exit 7 a propósito:
145
+ reanuda con `resume` o descarta con `clear`.
146
+ - **`step-error` detiene el proceso** (estado `ERROR`). Continuar exige
147
+ `resume` → `RERUN_STEP` (no se puede hacer `step-done` directo).
148
+ - **`finalize` exige plan completo** (o `--force`); sin plan requiere un
149
+ COMPLETADO para éxito. Un Issue cerrado por un humano nunca se convierte
150
+ en éxito (`ABORTED_EXTERNALLY`, exit 8).
151
+ - **Planes legados duplicados** producen `PLAN_AMBIGUOUS`; asigna nombres
152
+ únicos en el cuerpo del Issue, sin alterar los eventos.
153
+ - **Contexto ambiguo:** varios procesos activos exigen `--url`; `clear`
154
+ también lo exige con varios estados inactivos.
155
+ - **Token en el archivo de estado** solo con `--store-token` (opt-in);
156
+ prefiere variables de entorno o `gh auth token`. `adopt` sí puede usarlo.
157
+ - **Límite de comentarios:** un Issue con >10 000 comentarios produce
158
+ `COMMENTS_TOO_MANY` (error explícito, no veredicto truncado).
159
+
160
+ ## Verification
161
+
162
+ - `doctor` → `"ok": true` (gh opcional no rompe el global).
163
+ - Tras `init`: `status` muestra `active_process_id` = número del Issue.
164
+ - Tras `finalize`: `status` → `process_state: "FINALIZED"` o `"CANCELADO"`
165
+ según `outcome`, `active_process_id: null`; `resume` responde `NONE` con el
166
+ mismo `outcome`.
167
+ - Reanudación determinista: tras `step-error`, `resume` →
168
+ `verdict.action == "RERUN_STEP"` con el mismo paso; `step-done` sin
169
+ `step-start` previo falla con `NO_STEP_IN_FLIGHT`.
170
+ - 141 pruebas descubiertas: `python3 tests/test_tracking.py` (140 offline;
171
+ la integración real opt-in se omite por defecto).