github-proyect-tracking 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +456 -2
- package/bin/build.js +42 -0
- package/bin/cli.js +359 -0
- package/package.json +35 -4
- package/skill/SKILL.md +171 -0
- package/skill/references/api.md +163 -0
- package/skill/references/architecture.md +171 -0
- package/skill/scripts/tracking.py +196 -0
- package/skill/scripts/tracking_core/__init__.py +1 -0
- package/skill/scripts/tracking_core/commands.py +846 -0
- package/skill/scripts/tracking_core/common.py +163 -0
- package/skill/scripts/tracking_core/events.py +204 -0
- package/skill/scripts/tracking_core/github.py +315 -0
- package/skill/scripts/tracking_core/state.py +379 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
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 mostRecentProfile(hermesHome) {
|
|
72
|
+
// último recurso: el perfil con actividad más reciente (Hermes no
|
|
73
|
+
// persiste "perfil activo" global; en su terminal no hay HERMES_*).
|
|
74
|
+
const dir = path.join(hermesHome, 'profiles');
|
|
75
|
+
try {
|
|
76
|
+
let best = null, bestT = -1;
|
|
77
|
+
for (const name of fs.readdirSync(dir)) {
|
|
78
|
+
const p = path.join(dir, name);
|
|
79
|
+
if (!fs.statSync(p).isDirectory()) continue;
|
|
80
|
+
const t = fs.statSync(p).mtimeMs;
|
|
81
|
+
if (t > bestT) { bestT = t; best = name; }
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function profileFromAgentMd(cwd) {
|
|
88
|
+
for (const f of ['AGENT.md', 'AGENTS.md']) {
|
|
89
|
+
const p = path.join(cwd, f);
|
|
90
|
+
if (fs.existsSync(p)) {
|
|
91
|
+
const m = fs.readFileSync(p, 'utf8').match(/^profile\s*[:=]\s*(\S+)/m);
|
|
92
|
+
if (m) return m[1];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveDest(flags) {
|
|
99
|
+
if (flags.dest) return flags.dest;
|
|
100
|
+
const hermesHome = process.env.HERMES_HOME || path.join(os.homedir(), '.hermes');
|
|
101
|
+
if (flags.user || !fs.existsSync(path.join(hermesHome, 'profiles'))) {
|
|
102
|
+
return path.join(hermesHome, 'skills');
|
|
103
|
+
}
|
|
104
|
+
const explicit = Boolean(flags.profile);
|
|
105
|
+
let profile = flags.profile || process.env.HERMES_PROFILE
|
|
106
|
+
|| profileFromAgentMd(process.cwd());
|
|
107
|
+
const exists = (p) => fs.existsSync(path.join(hermesHome, 'profiles', p));
|
|
108
|
+
const available = () => fs.readdirSync(path.join(hermesHome, 'profiles'))
|
|
109
|
+
.filter((n) => fs.statSync(path.join(hermesHome, 'profiles', n)).isDirectory());
|
|
110
|
+
// --profile EXPLÍCITO inexistente → error claro (nunca fallback silencioso)
|
|
111
|
+
if (explicit && !exists(profile)) {
|
|
112
|
+
die(`el perfil '${profile}' no existe en ${hermesHome}/profiles/ ` +
|
|
113
|
+
`(disponibles: ${available().join(', ') || 'ninguno'})`, 2);
|
|
114
|
+
}
|
|
115
|
+
// Automático inexistente → elegir uno sensato en vez de abortar:
|
|
116
|
+
// 1) 'default' si existe, 2) el ÚNICO perfil si solo hay uno.
|
|
117
|
+
if (!profile || !exists(profile)) {
|
|
118
|
+
const sugerido = profile;
|
|
119
|
+
const perfs = available();
|
|
120
|
+
if (exists('default')) {
|
|
121
|
+
if (sugerido) {
|
|
122
|
+
console.error(`ADVERTENCIA: el perfil automático '${sugerido}' no existe; ` +
|
|
123
|
+
"usando 'default'");
|
|
124
|
+
}
|
|
125
|
+
profile = 'default';
|
|
126
|
+
} else if (perfs.length === 1) {
|
|
127
|
+
profile = perfs[0];
|
|
128
|
+
console.error(`ADVERTENCIA: perfil automático '${sugerido || "(no detectado)"}' ` +
|
|
129
|
+
`no disponible; usando el único perfil existente '${profile}'`);
|
|
130
|
+
} else if (perfs.length > 1) {
|
|
131
|
+
const recent = mostRecentProfile(hermesHome);
|
|
132
|
+
if (recent) {
|
|
133
|
+
profile = recent;
|
|
134
|
+
console.error(`ADVERTENCIA: no se detectó perfil ` +
|
|
135
|
+
`(${sugerido ? `'${sugerido}' no existe` : 'sin HERMES_PROFILE ni AGENT.md'}); ` +
|
|
136
|
+
`usando el de actividad más reciente '${profile}'. ` +
|
|
137
|
+
'Usa --profile PERFIL para elegirlo explícitamente');
|
|
138
|
+
} else {
|
|
139
|
+
die(`no se pudo determinar el perfil: ${sugerido
|
|
140
|
+
? `'${sugerido}' no existe` : 'no se detectó (ejecuta desde el repo o define HERMES_PROFILE)'}. ` +
|
|
141
|
+
`Perfiles disponibles: ${perfs.join(', ')}. ` +
|
|
142
|
+
'Usa --profile PERFIL o --dest RUTA', 2);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
die(`no hay perfiles de Hermes en ${hermesHome}/profiles/; ` +
|
|
146
|
+
'usa --dest RUTA para indicar el destino', 2);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return path.join(hermesHome, 'profiles', profile, 'skills');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cleanCaches(dir) {
|
|
153
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
154
|
+
const p = path.join(dir, e.name);
|
|
155
|
+
if (e.isDirectory()) {
|
|
156
|
+
if (e.name === '__pycache__') fs.rmSync(p, { recursive: true, force: true });
|
|
157
|
+
else cleanCaches(p);
|
|
158
|
+
} else if (/\.py[co]$/.test(e.name)) {
|
|
159
|
+
fs.rmSync(p, { force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateFrontmatter(skillDir) {
|
|
165
|
+
const md = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
166
|
+
if (!md.startsWith('---')) throw new Error('SKILL.md: falta "---" inicial');
|
|
167
|
+
const rel = md.slice(3).search(/\n---\s*\n/);
|
|
168
|
+
if (rel < 0) throw new Error('SKILL.md: falta cierre "---"');
|
|
169
|
+
const fm = md.slice(3, 3 + rel);
|
|
170
|
+
const name = (fm.match(/^name:\s*(.+?)\s*$/m) || [])[1];
|
|
171
|
+
let desc = (fm.match(/^description:\s*(.+?)\s*$/m) || [])[1] || '';
|
|
172
|
+
if (desc.startsWith('"') && desc.endsWith('"')) desc = desc.slice(1, -1);
|
|
173
|
+
if (name !== SKILL_NAME) throw new Error(`name=${name} (esperado ${SKILL_NAME})`);
|
|
174
|
+
if (!desc || desc.length > 60) {
|
|
175
|
+
throw new Error(`description (${desc.length} chars) vacía o > 60`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function verifyTrackingPy(target) {
|
|
180
|
+
// E-23: la verificación del script es OBLIGATORIA (exit != 0 si falla)
|
|
181
|
+
const py = pythonCmd();
|
|
182
|
+
if (!py) throw new Error('python3 no está en PATH; el skill lo requiere');
|
|
183
|
+
const r = spawnSync(py, [path.join(target, 'scripts', 'tracking.py'), '--version'],
|
|
184
|
+
{ encoding: 'utf8' });
|
|
185
|
+
if (r.status !== 0) {
|
|
186
|
+
throw new Error(`tracking.py --version falló (exit ${r.status}); ` +
|
|
187
|
+
'instalación abortada');
|
|
188
|
+
}
|
|
189
|
+
return r.stdout.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// comandos
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function install(flags) {
|
|
197
|
+
if (!fs.existsSync(path.join(SKILL_SRC, 'SKILL.md'))) {
|
|
198
|
+
die(`paquete corrupto: falta ${SKILL_SRC}/SKILL.md`, 2);
|
|
199
|
+
}
|
|
200
|
+
const dest = resolveDest(flags);
|
|
201
|
+
const target = path.join(dest, CATEGORY, SKILL_NAME);
|
|
202
|
+
log(`Origen : ${SKILL_SRC}`);
|
|
203
|
+
log(`Destino: ${target}`);
|
|
204
|
+
|
|
205
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'gpt-npx-'));
|
|
206
|
+
const stageSkill = path.join(stage, SKILL_NAME);
|
|
207
|
+
const unique = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
208
|
+
const staged = `${target}.new.${unique}`;
|
|
209
|
+
const bak = `${target}.bak.${unique}`;
|
|
210
|
+
let swapStarted = false;
|
|
211
|
+
try {
|
|
212
|
+
fs.cpSync(SKILL_SRC, stageSkill, { recursive: true });
|
|
213
|
+
cleanCaches(stageSkill);
|
|
214
|
+
validateFrontmatter(stageSkill);
|
|
215
|
+
log('frontmatter: OK (name + description <= 60)');
|
|
216
|
+
const preVer = verifyTrackingPy(stageSkill);
|
|
217
|
+
if (preVer) log(preVer);
|
|
218
|
+
|
|
219
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
220
|
+
fs.cpSync(stageSkill, staged, { recursive: true });
|
|
221
|
+
if (fs.existsSync(target)) {
|
|
222
|
+
fs.cpSync(target, bak, { recursive: true });
|
|
223
|
+
log(`Copia previa respaldada en: ${bak}`);
|
|
224
|
+
}
|
|
225
|
+
swapStarted = true;
|
|
226
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
227
|
+
fs.renameSync(staged, target);
|
|
228
|
+
const ver = verifyTrackingPy(target);
|
|
229
|
+
if (ver) log(ver);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (swapStarted) {
|
|
232
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
233
|
+
if (fs.existsSync(bak)) {
|
|
234
|
+
fs.cpSync(bak, target, { recursive: true });
|
|
235
|
+
log(`Rollback: restaurado ${bak}`);
|
|
236
|
+
} else {
|
|
237
|
+
log('Sin respaldo previo: la copia no usable fue eliminada');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
} finally {
|
|
242
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
243
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const py = pythonCmd();
|
|
247
|
+
if (!py) {
|
|
248
|
+
log('ADVERTENCIA: python3 no está en PATH; el skill lo requiere.');
|
|
249
|
+
} else {
|
|
250
|
+
const r = spawnSync(py, [path.join(target, 'scripts', 'tracking.py'), '--version'],
|
|
251
|
+
{ encoding: 'utf8' });
|
|
252
|
+
if (r.status === 0 && r.stdout) log(r.stdout.trim());
|
|
253
|
+
else log(`ADVERTENCIA: verificación de tracking.py falló (exit ${r.status})`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
log();
|
|
257
|
+
log('Instalado. Prueba rápida:');
|
|
258
|
+
log(` ${py || 'python3'} "${path.join(target, 'scripts', 'tracking.py')}" doctor --url https://github.com/{owner}/{repo}`);
|
|
259
|
+
log();
|
|
260
|
+
log('Nota: el skill estará visible para sesiones NUEVAS de Hermes (el catálogo');
|
|
261
|
+
log('se carga al iniciar sesión). La memoria interna vive en');
|
|
262
|
+
log('~/.github-proyect-tracking/ (override: $GITHUB_TRACKING_STATE).');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function uninstall(flags) {
|
|
266
|
+
const dest = resolveDest(flags);
|
|
267
|
+
const target = path.join(dest, CATEGORY, SKILL_NAME);
|
|
268
|
+
if (fs.existsSync(target)) {
|
|
269
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
270
|
+
log(`Eliminado: ${target}`);
|
|
271
|
+
try { fs.rmdirSync(path.dirname(target)); } catch { /* categoría no vacía */ }
|
|
272
|
+
} else {
|
|
273
|
+
log(`No hay instalación en: ${target} (nada que desinstalar)`);
|
|
274
|
+
}
|
|
275
|
+
if (flags.purgeState) {
|
|
276
|
+
const stateDir = process.env.GITHUB_TRACKING_STATE
|
|
277
|
+
|| path.join(os.homedir(), '.github-proyect-tracking');
|
|
278
|
+
if (fs.existsSync(stateDir)) {
|
|
279
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
280
|
+
log(`Memoria interna eliminada: ${stateDir}`);
|
|
281
|
+
}
|
|
282
|
+
log('Nota: los Issues creados en GitHub NO se eliminan (permanecen en el repo).');
|
|
283
|
+
}
|
|
284
|
+
log('Listo.');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function doctor(flags, rest) {
|
|
288
|
+
const py = pythonCmd();
|
|
289
|
+
if (!py) die('python3 no está en PATH (requerido por el skill)');
|
|
290
|
+
const candidates = [
|
|
291
|
+
path.join(resolveDest(flags), CATEGORY, SKILL_NAME, 'scripts', 'tracking.py'),
|
|
292
|
+
path.join(SKILL_SRC, 'scripts', 'tracking.py'),
|
|
293
|
+
];
|
|
294
|
+
const script = candidates.find((p) => fs.existsSync(p));
|
|
295
|
+
if (!script) die('no se encontró tracking.py (¿paquete corrupto?)', 2);
|
|
296
|
+
const r = spawnSync(py, [script, 'doctor', ...rest], { stdio: 'inherit' });
|
|
297
|
+
process.exit(r.status === null ? 1 : r.status);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function run(flags, rest) {
|
|
301
|
+
const py = pythonCmd();
|
|
302
|
+
if (!py) die('python3 no está en PATH (requerido por el skill)');
|
|
303
|
+
const candidates = [
|
|
304
|
+
path.join(resolveDest(flags), CATEGORY, SKILL_NAME, 'scripts', 'tracking.py'),
|
|
305
|
+
path.join(SKILL_SRC, 'scripts', 'tracking.py'),
|
|
306
|
+
];
|
|
307
|
+
const script = candidates.find((p) => fs.existsSync(p));
|
|
308
|
+
if (!script) die('no se encontró tracking.py (¿paquete corrupto?)', 2);
|
|
309
|
+
const r = spawnSync(py, [script, ...rest], { stdio: 'inherit' });
|
|
310
|
+
process.exit(r.status === null ? 1 : r.status);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// ayuda y main
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function help() {
|
|
318
|
+
log(`github-proyect-tracking v${VERSION} — instalador npx del skill para Hermes Agent`);
|
|
319
|
+
log();
|
|
320
|
+
log('Uso:');
|
|
321
|
+
log(' npx --yes github-proyect-tracking [install] [flags] instalar el skill');
|
|
322
|
+
log(' npx --yes github-proyect-tracking doctor [args…] diagnóstico (sin instalar)');
|
|
323
|
+
log(' npx --yes github-proyect-tracking run <cmd> [args…] ejecutar un comando del skill');
|
|
324
|
+
log(' npx --yes github-proyect-tracking uninstall [flags] desinstalar');
|
|
325
|
+
log();
|
|
326
|
+
log('Flags:');
|
|
327
|
+
log(' --dest RUTA destino explícito (directorio padre de skills)');
|
|
328
|
+
log(' --profile PERFIL perfil de Hermes (~/.hermes/profiles/<p>/skills)');
|
|
329
|
+
log(' --user ~/.hermes/skills del perfil por defecto');
|
|
330
|
+
log(' --purge-state (uninstall) borra también ~/.github-proyect-tracking/');
|
|
331
|
+
log(' -h, --help esta ayuda');
|
|
332
|
+
log(' -v, --version versión del paquete');
|
|
333
|
+
log();
|
|
334
|
+
log('El destino por defecto se resuelve igual que install.sh: HERMES_HOME,');
|
|
335
|
+
log('perfil activo (HERMES_PROFILE o la línea "profile:" del AGENT.md del');
|
|
336
|
+
log('directorio actual) y, si no, el perfil default. Requisitos: Node >= 18');
|
|
337
|
+
log('para este instalador; Python 3.8+ (stdlib) para el skill.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function main() {
|
|
341
|
+
const { command, flags, rest } = parseArgs(process.argv.slice(2));
|
|
342
|
+
if (flags.version) { log(`github-proyect-tracking/${VERSION} (instalador npx)`); return; }
|
|
343
|
+
if (flags.help) { help(); return; }
|
|
344
|
+
if (!['doctor', 'run'].includes(command) && rest.length) {
|
|
345
|
+
die(`argumentos no reconocidos para ${command}: ${rest.join(' ')}`, 2);
|
|
346
|
+
}
|
|
347
|
+
if (command === 'install' && flags.purgeState) {
|
|
348
|
+
die('--purge-state solo es válido con uninstall', 2);
|
|
349
|
+
}
|
|
350
|
+
switch (command) {
|
|
351
|
+
case 'install': install(flags); break;
|
|
352
|
+
case 'uninstall': uninstall(flags); break;
|
|
353
|
+
case 'doctor': doctor(flags, rest); break;
|
|
354
|
+
case 'run': run(flags, rest); break;
|
|
355
|
+
default: die(`comando desconocido: ${command} (usa --help)`, 2);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-proyect-tracking",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.1",
|
|
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).
|