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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Referencia de API (CLI) — github-proyect-tracking
|
|
2
|
+
|
|
3
|
+
`scripts/tracking.py` — stdlib only, Python 3.8+. Cada comando imprime
|
|
4
|
+
EXACTAMENTE un objeto JSON a stdout y usa un exit code documentado. Los
|
|
5
|
+
ejemplos suponen `PY="python3 <ruta-del-skill>/scripts/tracking.py"`.
|
|
6
|
+
|
|
7
|
+
## Contrato de respuesta
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{ "ok": true, "action": "INIT", "execution_id": "exec-...", "...": "..." }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Error:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "ok": false, "error": { "code": "RESUME_REQUIRED", "message": "...",
|
|
17
|
+
"state": {...}, "next": ["resume"] } }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Exit | code típico | Significado / acción del agente |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 0 | — | Éxito |
|
|
23
|
+
| 1 | `NETWORK_ERROR`, `RATE_LIMITED`, `STATE_CORRUPT`, `COMMENTS_TOO_MANY`, `STATE_LOCKED`, `STATE_LOCK_LOST`… | Reintentar / diagnosticar / `resume` |
|
|
24
|
+
| 2 | `VALIDATION_ERROR`, `NO_STEP_IN_FLIGHT`, `STEP_OUT_OF_ORDER`, `STEP_NOT_IN_PLAN`, `STEP_ALREADY_DONE`, `STEP_IN_FLIGHT`, `CONTEXT_AMBIGUOUS` | Corregir argumentos o transición |
|
|
25
|
+
| 3 | `INVALID_TOKEN`, `INSUFFICIENT_SCOPE`, `NO_REPO_ACCESS`, `REPO_NOT_FOUND`, `ISSUES_WRITE_FAILED` | Token sin `issues:write` |
|
|
26
|
+
| 4 | `NO_TOKEN` | Pedir el token al usuario |
|
|
27
|
+
| 5 | `NO_STATE`, `NO_ACTIVE_PROCESS` | `init` primero |
|
|
28
|
+
| 6 | `ISSUE_NOT_FOUND`, `ISSUE_AMBIGUOUS` | Verificar número / `adopt` (ID EXACTO) |
|
|
29
|
+
| 7 | `RESUME_REQUIRED` (proceso inconcluso, estado `ERROR`, `adopt` sobre activo) | `resume` (o `adopt --force`) |
|
|
30
|
+
| 8 | `FINALIZE_BLOCKED`, `ABORTED_EXTERNALLY`, `ISSUE_CLOSED_EXTERNALLY`, `ALREADY_FINALIZED` | Conflicto con estado remoto |
|
|
31
|
+
|
|
32
|
+
`finalize --force` publica `[STATUS: CANCELADO] - El proceso fue cerrado
|
|
33
|
+
anticipadamente (--force).` con los pasos pendientes — distinguible del
|
|
34
|
+
éxito (`FINALIZADO`). `NONE` y `FINISH_CLEANUP` incluyen `outcome:
|
|
35
|
+
FINALIZADO|CANCELADO`; reintentar, reanudar o adoptar nunca reclasifica una
|
|
36
|
+
cancelación como éxito.
|
|
37
|
+
|
|
38
|
+
Los comentarios nuevos conservan exactamente esos formatos visibles y añaden
|
|
39
|
+
una línea HTML `<!-- github-proyect-tracking: {...} -->` con `execution_id`,
|
|
40
|
+
`sequence` y `transition_id` UUID. El parser ignora esos metadatos al leer el
|
|
41
|
+
texto y los usa para reconciliar únicamente el intento que originó el POST.
|
|
42
|
+
|
|
43
|
+
## Comandos
|
|
44
|
+
|
|
45
|
+
### `init --url URL [--plan JSON | --plan-file F] [--token T] [--prompt-token] [--store-token]`
|
|
46
|
+
|
|
47
|
+
- Valida la URL (regex, redactada en errores), resuelve el token, valida
|
|
48
|
+
`issues:write` ANTES de crear nada.
|
|
49
|
+
- Crea el Issue `[Process Tracking] Execution ID: {exec_id}` con el cuerpo
|
|
50
|
+
estándar (metadatos + plan numerado SANITIZADO) y guarda
|
|
51
|
+
`active_process_id`.
|
|
52
|
+
- `--plan` recomendado: lista JSON de cadenas (nombres únicos tras
|
|
53
|
+
normalizar: strip+sanitización; duplicados y colisiones se rechazan) →
|
|
54
|
+
`resume` determinista.
|
|
55
|
+
- `--store-token`: persistencia del token OPT-IN (por defecto NO se
|
|
56
|
+
guarda; `--no-store-token` quedó obsoleto).
|
|
57
|
+
|
|
58
|
+
Salida: `execution_id`, `issue_number`, `issue_url`, `state_file`, `plan`,
|
|
59
|
+
`first_step`, `token_source`.
|
|
60
|
+
|
|
61
|
+
### `resume [--url URL] [--token T]`
|
|
62
|
+
|
|
63
|
+
- Trae Issue + TODOS los comentarios (paginación por `Link`), parsea
|
|
64
|
+
eventos, emite `verdict` (ver tabla en `references/architecture.md`).
|
|
65
|
+
- `--url` opcional si hay exactamente UN proceso activo; con varios
|
|
66
|
+
activos → `CONTEXT_AMBIGUOUS` (el error lista los repos activos).
|
|
67
|
+
- Registra `last_resume`: después de ERROR autoriza únicamente el reintento
|
|
68
|
+
del paso fallido. Un plan legado duplicado produce `PLAN_AMBIGUOUS` con
|
|
69
|
+
instrucciones para asignar nombres únicos en el cuerpo del Issue.
|
|
70
|
+
|
|
71
|
+
Salida: `issue_state`, `comment_count`, `verdict{action, outcome?,
|
|
72
|
+
resume_from_step, remaining_steps?, failed_step?, error_details?, reason}`.
|
|
73
|
+
|
|
74
|
+
### `step-start --name N` / `step-done --name N`
|
|
75
|
+
|
|
76
|
+
Publican exactamente:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
[STATUS: INICIANDO] - Paso: {N}
|
|
80
|
+
[STATUS: COMPLETADO] - Paso: {N}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `step-error --name N (--details D | --details-file F)`
|
|
84
|
+
|
|
85
|
+
Publica `[STATUS: ERROR] - Paso: {N} - Detalles: {D}` y fija
|
|
86
|
+
`process_state: "ERROR"` — **el proceso se detiene aquí** (por diseño;
|
|
87
|
+
reanudar exige `resume`). `--details-file` ideal para stacktraces largos
|
|
88
|
+
(trunca a 60000 chars).
|
|
89
|
+
|
|
90
|
+
### `finalize [--url URL] [--token T] [--force]`
|
|
91
|
+
|
|
92
|
+
1. Verifica el veredicto remoto: rechaza procesos con pasos pendientes
|
|
93
|
+
(`FINALIZE_BLOCKED`, exit 8) y abortes externos. Sin plan permite éxito
|
|
94
|
+
después de un COMPLETADO; sin trabajo exige `--force`.
|
|
95
|
+
2. Sin `--force`: comenta `[STATUS: FINALIZADO] - El proceso concluyó
|
|
96
|
+
exitosamente.` (idempotente). Con `--force`: `[STATUS: CANCELADO] -
|
|
97
|
+
El proceso fue cerrado anticipadamente (--force).` con los pasos
|
|
98
|
+
pendientes (cierre anticipado distinguible, nunca éxito falso).
|
|
99
|
+
3. Cierra el Issue (PATCH state=closed) y **verifica re-leyendo**.
|
|
100
|
+
4. Limpia `active_process_id` (estado `FINALIZED`/`CANCELADO`) + historial
|
|
101
|
+
(con `outcome`). Un CANCELADO existente conserva ese resultado aunque el
|
|
102
|
+
reintento no lleve `--force`.
|
|
103
|
+
|
|
104
|
+
### `status [--url URL] [--remote]`
|
|
105
|
+
|
|
106
|
+
Estado local enmascarado (nunca imprime el token) y, con `--remote`,
|
|
107
|
+
veredicto fresco del Issue.
|
|
108
|
+
|
|
109
|
+
### `adopt --url URL --execution-id ID [--token T] [--force]`
|
|
110
|
+
|
|
111
|
+
Recuperación: busca el Issue por título canónico EXACTO (el ID completo;
|
|
112
|
+
parciales rechazados, ambigüedad → `ISSUE_AMBIGUOUS`), reconstruye el
|
|
113
|
+
archivo de estado local. Carga el estado candidato antes de resolver el
|
|
114
|
+
token, por lo que puede usar el guardado con `--store-token`. No sustituye
|
|
115
|
+
un proceso activo sin `--force` (que conserva token e historial). Si adopta
|
|
116
|
+
un ERROR, guarda `process_state=ERROR` y exige `resume`; conserva CANCELADO
|
|
117
|
+
y ABORTED según el Issue. Útil tras perder la máquina local o tras un
|
|
118
|
+
`clear --hard` accidental.
|
|
119
|
+
|
|
120
|
+
### `clear [--url URL] [--hard]`
|
|
121
|
+
|
|
122
|
+
Soft: `active_process_id := null`, `process_state := "CLEARED"` (conserva
|
|
123
|
+
token/historial). Hard: elimina el archivo. Si el proceso era inconcluso,
|
|
124
|
+
avisa: el Issue queda abierto en GitHub (recuperable con `adopt`). Sin URL,
|
|
125
|
+
varios estados locales —activos o inactivos— producen `CONTEXT_AMBIGUOUS`;
|
|
126
|
+
nunca se elige un archivo destructivamente por fecha.
|
|
127
|
+
|
|
128
|
+
### `doctor [--url URL] [--token T]`
|
|
129
|
+
|
|
130
|
+
Checks: python ≥ 3.8, directorio de estado escribible (crea el directorio
|
|
131
|
+
y un archivo de prueba — efecto de escritura declarado), token resuelto
|
|
132
|
+
(incluye el guardado con `--store-token` del repo de `--url`), GitHub API
|
|
133
|
+
+ rate limit, `gh` CLI (opcional), archivos de estado, URL válida y
|
|
134
|
+
`issues:write` (con `--url`). Siempre exit 0 — es un reporte.
|
|
135
|
+
|
|
136
|
+
## Variables de entorno
|
|
137
|
+
|
|
138
|
+
| Variable | Uso |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `GITHUB_TRACKING_TOKEN` | Token (prioridad máxima de las fuentes automáticas) |
|
|
141
|
+
| `GITHUB_TOKEN` / `GH_TOKEN` | Fallbacks estándar |
|
|
142
|
+
| `GITHUB_TRACKING_STATE` | Directorio alternativo de archivos de estado |
|
|
143
|
+
|
|
144
|
+
## Detalles de la API REST usada
|
|
145
|
+
|
|
146
|
+
| Operación | Endpoint |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Validación de repo/permisos | `GET /repos/{owner}/{repo}` |
|
|
149
|
+
| Crear Issue | `POST /repos/{owner}/{repo}/issues` |
|
|
150
|
+
| Leer Issue / cerrar Issue | `GET` / `PATCH /repos/{owner}/{repo}/issues/{n}` |
|
|
151
|
+
| Comentarios (paginado 100/pág) | `GET /repos/{owner}/{repo}/issues/{n}/comments` |
|
|
152
|
+
| Publicar comentario | `POST /repos/{owner}/{repo}/issues/{n}/comments` |
|
|
153
|
+
| Buscar Issue (adopt) | `GET /search/issues?q=...` |
|
|
154
|
+
| Rate limit (doctor) | `GET /rate_limit` |
|
|
155
|
+
|
|
156
|
+
Cabeceras: `Authorization: Bearer`, `Accept: application/vnd.github+json`,
|
|
157
|
+
`X-GitHub-Api-Version: 2022-11-28`, `User-Agent: github-proyect-tracking/{v}`.
|
|
158
|
+
Reintentos: GET/PATCH/5xx con backoff. Ante respuesta perdida o
|
|
159
|
+
500/502/503/504 en POST, se busca el `transition_id` UUID fuera de la línea
|
|
160
|
+
base y solo entonces se reconcilia; si no aparece, hay un único reintento con
|
|
161
|
+
el mismo UUID y una segunda reconciliación. La creación del Issue usa búsqueda
|
|
162
|
+
paginada por título exacto. Rate limit 403/429 con `X-RateLimit-Remaining: 0`
|
|
163
|
+
espera controlada (máx 90 s).
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Arquitectura — github-proyect-tracking
|
|
2
|
+
|
|
3
|
+
## Visión general
|
|
4
|
+
|
|
5
|
+
El skill instrumenta procesos (no los ejecuta): convierte cada transición en
|
|
6
|
+
un evento persistido en un Issue de GitHub y mantiene una memoria local para
|
|
7
|
+
saber si hay un proceso inconcluso y en qué Issue vive.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌──────────────┐ instrucción ┌─────────────────────────────────────┐
|
|
11
|
+
│ Agente (tú) │ ───────────────▶ │ scripts/tracking.py (CLI, stdlib) │
|
|
12
|
+
│ │ ◀─────────────── │ · 1 JSON por comando + exit code │
|
|
13
|
+
└──────────────┘ JSON + exit └───────────────┬─────────────────────┘
|
|
14
|
+
│ trabajo real del proceso │
|
|
15
|
+
▼ ▼
|
|
16
|
+
┌──────────────────┐ comentarios [STATUS:…] ┌──────────────────────────┐
|
|
17
|
+
│ Memoria interna │ ◀──────────────────────── │ GitHub REST API v3 │
|
|
18
|
+
│ ~/.github- │ Issue #N (fuente de │ /repos/{o}/{r}/issues/… │
|
|
19
|
+
│ proyect-tracking/│ verdad al reanudar) │ Bearer token │
|
|
20
|
+
│ {owner}__{repo}. │ └──────────────────────────┘
|
|
21
|
+
│ json │
|
|
22
|
+
└──────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Doble libro:
|
|
26
|
+
|
|
27
|
+
1. **Remoto (autoritativo):** un Issue con título
|
|
28
|
+
`[Process Tracking] Execution ID: {id}`; cada transición es un comentario
|
|
29
|
+
con contrato visible exacto y el último evento decide la reanudación.
|
|
30
|
+
2. **Local (rápido):** `~/.github-proyect-tracking/{owner}__{repo}.json` con
|
|
31
|
+
esquema v2, `active_process_id`, `github_token` opcional, plan y metadatos.
|
|
32
|
+
La carga migra v1 a v2 en memoria y el siguiente guardado la persiste.
|
|
33
|
+
Escritura atómica (tmp único + fsync + rename), permisos 0600.
|
|
34
|
+
|
|
35
|
+
## Contrato de eventos y reconciliación
|
|
36
|
+
|
|
37
|
+
Formatos visibles:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
[STATUS: INICIANDO] - Paso: {nombre}
|
|
41
|
+
[STATUS: ERROR] - Paso: {nombre} - Detalles: {detalles}
|
|
42
|
+
[STATUS: COMPLETADO] - Paso: {nombre}
|
|
43
|
+
[STATUS: FINALIZADO] - El proceso concluyó exitosamente.
|
|
44
|
+
[STATUS: CANCELADO] - El proceso fue cerrado anticipadamente (--force).
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Los eventos nuevos añaden una línea HTML invisible:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
<!-- github-proyect-tracking: {"execution_id":"...","sequence":1,"transition_id":"..."} -->
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`transition_id` es un UUID por intento y se conserva en el reintento del mismo
|
|
54
|
+
comando. Ante respuesta de red perdida o HTTP 500/502/503/504, el cliente lee
|
|
55
|
+
los comentarios y solo reconcilia si encuentra ese UUID fuera de la línea base.
|
|
56
|
+
Un texto idéntico antiguo o nuevo de otro emisor no basta. Si no lo encuentra,
|
|
57
|
+
reintenta una vez y vuelve a reconciliar. `init` recupera una creación ambigua
|
|
58
|
+
buscando el título exacto con paginación; si no lo encuentra, el error incluye
|
|
59
|
+
Execution ID, título y comando `adopt` sugerido.
|
|
60
|
+
|
|
61
|
+
El parser elimina los metadatos antes de aplicar `COMMENT_RE`, por lo que los
|
|
62
|
+
formatos visibles y los comentarios antiguos siguen siendo compatibles.
|
|
63
|
+
`Detalles` admite stacktraces multilínea. `events_from_comments()` ordena por
|
|
64
|
+
`(created_at, comment_id)`, sin confiar en el orden de llegada.
|
|
65
|
+
|
|
66
|
+
## Máquina de estados del proceso
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
init (URL válida + token issues:write)
|
|
70
|
+
(sin proceso) ───────────────────────────────────────▶ RUNNING
|
|
71
|
+
│
|
|
72
|
+
┌─────────── step-start / step-done ───────────────┤
|
|
73
|
+
▼ │
|
|
74
|
+
RUNNING ── step-error ──▶ ERROR ──(resume: RERUN_STEP)─┘
|
|
75
|
+
│
|
|
76
|
+
├──── finalize: FINALIZADO + Issue cerrado ────────▶ FINALIZED
|
|
77
|
+
└──── finalize --force: CANCELADO + Issue cerrado ─▶ CANCELADO
|
|
78
|
+
(active_process_id := null)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Estado en memoria | Significado | `active_process_id` |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `RUNNING` | Issue abierto, último evento INICIANDO/COMPLETADO | número |
|
|
84
|
+
| `ERROR` | Último evento ERROR; proceso detenido | número |
|
|
85
|
+
| `FINALIZED` | Issue cerrado con FINALIZADO | `null` |
|
|
86
|
+
| `CANCELADO` | Issue cerrado anticipadamente con CANCELADO | `null` |
|
|
87
|
+
| `ABORTED` | Issue cerrado sin FINALIZADO/CANCELADO | `null` |
|
|
88
|
+
| `CLEARED` | Memoria descartada localmente | `null` |
|
|
89
|
+
|
|
90
|
+
`is_unfinished()` exige ID activo y estado RUNNING o ERROR. `init` sobre un
|
|
91
|
+
proceso inconcluso falla con exit 7 (`RESUME_REQUIRED`). El esquema valida
|
|
92
|
+
tipos y estas invariantes cruzadas; un estado terminal no puede conservar ID
|
|
93
|
+
activo.
|
|
94
|
+
|
|
95
|
+
Después de ERROR, `resume` registra `last_resume` con Issue y comentario de
|
|
96
|
+
error. `step-start` solo acepta esa autorización para reintentar el mismo paso;
|
|
97
|
+
`step-done` y `step-error` también se detienen mientras el último evento remoto
|
|
98
|
+
sea ERROR. `adopt` reconstruye ERROR, nunca lo sustituye por RUNNING.
|
|
99
|
+
|
|
100
|
+
## Ciclo de vida de una ejecución
|
|
101
|
+
|
|
102
|
+
1. **init** — valida URL y permisos, genera `exec-{timestamp}-{12 hex}`, crea
|
|
103
|
+
el Issue y guarda memoria. Un plan duplicado nuevo se rechaza.
|
|
104
|
+
2. **resume** — lee Issue y todos los comentarios, deriva el veredicto y
|
|
105
|
+
sincroniza el estado. Los planes legados duplicados producen
|
|
106
|
+
`PLAN_AMBIGUOUS` con instrucciones de reparación; no se deduplican.
|
|
107
|
+
3. **step-start / step-error / step-done** — una transición por comentario,
|
|
108
|
+
validada contra la misma instantánea remota.
|
|
109
|
+
4. **finalize** — publica el evento terminal, cierra y verifica el Issue,
|
|
110
|
+
limpia memoria y conserva las últimas 20 entradas de historial.
|
|
111
|
+
|
|
112
|
+
Con plan, el éxito exige todos los pasos completados. Sin plan, el último
|
|
113
|
+
evento COMPLETADO permite FINALIZADO; sin trabajo, `finalize` se bloquea y
|
|
114
|
+
`--force` registra CANCELADO. El `outcome` siempre procede del evento remoto:
|
|
115
|
+
un reintento sin `--force` no convierte CANCELADO en FINALIZADO.
|
|
116
|
+
|
|
117
|
+
## Veredictos de `resume`
|
|
118
|
+
|
|
119
|
+
| Último evento / situación | `verdict.action` | `outcome` | Acción |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| FINALIZADO + Issue abierto | `FINISH_CLEANUP` | `FINALIZADO` | Ejecutar `finalize` |
|
|
122
|
+
| CANCELADO + Issue abierto | `FINISH_CLEANUP` | `CANCELADO` | Ejecutar `finalize` |
|
|
123
|
+
| FINALIZADO + Issue cerrado | `NONE` | `FINALIZADO` | Nada; memoria auto-limpia |
|
|
124
|
+
| CANCELADO + Issue cerrado | `NONE` | `CANCELADO` | Nada; conservar cancelación |
|
|
125
|
+
| ERROR | `RERUN_STEP` | — | Re-ejecutar ese paso tras esta autorización |
|
|
126
|
+
| INICIANDO | `RERUN_STEP` | — | Re-ejecutar el paso en vuelo |
|
|
127
|
+
| COMPLETADO + plan pendiente | `NEXT_STEP` | — | Ejecutar `resume_from_step` |
|
|
128
|
+
| COMPLETADO + plan agotado | `ALL_STEPS_DONE` | — | Ejecutar `finalize` |
|
|
129
|
+
| Sin comentarios | `START_FIRST` | — | Empezar por el paso 1 |
|
|
130
|
+
| Issue cerrado sin evento terminal | `ABORTED_EXTERNALLY` | — | Preguntar al usuario |
|
|
131
|
+
| Sin plan explícito | `NEXT_STEP_AGENT_DEFINED` | — | Elegir paso o finalizar tras COMPLETADO |
|
|
132
|
+
|
|
133
|
+
## Módulos
|
|
134
|
+
|
|
135
|
+
| Archivo | Responsabilidad |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `scripts/tracking.py` | Parser CLI, punto de entrada y exportaciones compatibles |
|
|
138
|
+
| `tracking_core/common.py` | Versión, constantes, errores y validación/redacción |
|
|
139
|
+
| `tracking_core/events.py` | Contrato visible, metadatos, parser y planner |
|
|
140
|
+
| `tracking_core/state.py` | Esquema v2, migración, persistencia, selección y locks |
|
|
141
|
+
| `tracking_core/github.py` | Token, preflight, transporte, paginación y reconciliación |
|
|
142
|
+
| `tracking_core/commands.py` | Comandos y guardas sobre instantáneas remotas |
|
|
143
|
+
|
|
144
|
+
## Locks y robustez
|
|
145
|
+
|
|
146
|
+
`StateLock` usa un directorio por repositorio, propietario con token y un hilo
|
|
147
|
+
daemon que refresca heartbeat durante toda la operación, incluidas las llamadas
|
|
148
|
+
HTTP. `ensure_owner()` se exige antes de efectos remotos y antes de guardar o
|
|
149
|
+
borrar. Perder el lock produce `STATE_LOCK_LOST` y exige `resume`.
|
|
150
|
+
|
|
151
|
+
- Escritura atómica, estado 0600, tokens enmascarados y persistencia opt-in.
|
|
152
|
+
- Reintentos de GET/PATCH y rate limit con espera acotada.
|
|
153
|
+
- Paginación completa por `Link`; más de 10 000 comentarios →
|
|
154
|
+
`COMMENTS_TOO_MANY`.
|
|
155
|
+
- `adopt` usa el token almacenado antes de buscar el Issue.
|
|
156
|
+
- `clear` sin URL exige contexto único también cuando solo hay estados
|
|
157
|
+
inactivos; varios candidatos → `CONTEXT_AMBIGUOUS`.
|
|
158
|
+
- Preflight: un rol demostrado de solo lectura falla; `triage`, `push`,
|
|
159
|
+
`maintain` o `admin` habilitan la gestión de Issues.
|
|
160
|
+
- Redacción de todo userinfo, query y fragment en URLs reflejadas.
|
|
161
|
+
|
|
162
|
+
## Límites y garantías
|
|
163
|
+
|
|
164
|
+
- Un proceso activo por repositorio (la memoria es por `{owner}/{repo}`).
|
|
165
|
+
- Los eventos son append-only; una persona con acceso puede imitar el contrato
|
|
166
|
+
visible. Usa repositorios controlados y revisa la cronología ante anomalías.
|
|
167
|
+
- No hay transacciones distribuidas: el lock protege el estado local y el
|
|
168
|
+
heartbeat evita su expiración durante una operación viva, pero dos agentes
|
|
169
|
+
independientes pueden intervenir manualmente en el mismo Issue.
|
|
170
|
+
- La integración real es opt-in y requiere autorización y un repositorio
|
|
171
|
+
desechable; no se ejecuta en la suite offline por defecto.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""CLI de github-proyect-tracking; un JSON por comando, stdlib únicamente."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from tracking_core import github as _github
|
|
12
|
+
from tracking_core.common import * # noqa: F401,F403
|
|
13
|
+
from tracking_core.events import * # noqa: F401,F403
|
|
14
|
+
from tracking_core.state import * # noqa: F401,F403
|
|
15
|
+
from tracking_core.commands import * # noqa: F401,F403
|
|
16
|
+
|
|
17
|
+
_raw_api = _github._raw_api
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _sync_runtime():
|
|
21
|
+
_github._raw_api = _raw_api
|
|
22
|
+
_github.shutil = shutil
|
|
23
|
+
_github.MAX_COMMENT_PAGES = MAX_COMMENT_PAGES
|
|
24
|
+
StateLock.STALE_SECONDS = LOCK_STALE_SECONDS
|
|
25
|
+
StateLock.WAIT_SECONDS = LOCK_WAIT_SECONDS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def api(method, path, token, body=None, retries=3):
|
|
29
|
+
_sync_runtime()
|
|
30
|
+
return _github.api(method, path, token, body, retries)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_issues_write(owner, repo, token):
|
|
34
|
+
_sync_runtime()
|
|
35
|
+
return _github.check_issues_write(owner, repo, token)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_token(cli_token=None, state=None):
|
|
39
|
+
_sync_runtime()
|
|
40
|
+
return _github.resolve_token(cli_token, state)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def post_comment(owner, repo, number, body, token,
|
|
44
|
+
baseline_ids=None, execution_id=None, sequence=None):
|
|
45
|
+
_sync_runtime()
|
|
46
|
+
return _github.post_comment(owner, repo, number, body, token,
|
|
47
|
+
baseline_ids, execution_id, sequence)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fetch_issue(owner, repo, number, token):
|
|
51
|
+
_sync_runtime()
|
|
52
|
+
return _github.fetch_issue(owner, repo, number, token)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def fetch_all_comments(owner, repo, number, token):
|
|
56
|
+
_sync_runtime()
|
|
57
|
+
return _github.fetch_all_comments(owner, repo, number, token)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def close_issue(owner, repo, number, token):
|
|
61
|
+
_sync_runtime()
|
|
62
|
+
return _github.close_issue(owner, repo, number, token)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_issue_by_title(owner, repo, title, token):
|
|
66
|
+
_sync_runtime()
|
|
67
|
+
return _github.find_issue_by_title(owner, repo, title, token)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class JsonArgumentParser(argparse.ArgumentParser):
|
|
71
|
+
def error(self, message):
|
|
72
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
73
|
+
f"argumentos inválidos: {message}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_parser():
|
|
77
|
+
parser = JsonArgumentParser(
|
|
78
|
+
prog="tracking.py",
|
|
79
|
+
description="github-proyect-tracking: persiste y reanuda el estado de un "
|
|
80
|
+
"proceso en GitHub Issues. Cada comando imprime un único JSON.")
|
|
81
|
+
parser.add_argument("--version", action="version", version=USER_AGENT)
|
|
82
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
83
|
+
|
|
84
|
+
def common(subparser, with_url=True, url_required=False):
|
|
85
|
+
if with_url:
|
|
86
|
+
subparser.add_argument(
|
|
87
|
+
"--url", required=url_required, metavar="URL",
|
|
88
|
+
help="github_project_url (https://github.com/{owner}/{repo}); "
|
|
89
|
+
"obligatorio si hay varios procesos activos")
|
|
90
|
+
subparser.add_argument("--token", default=None, metavar="T",
|
|
91
|
+
help="GitHub PAT con issues:write (si se omite: "
|
|
92
|
+
"env/gh/estado)")
|
|
93
|
+
|
|
94
|
+
subparser = subparsers.add_parser(
|
|
95
|
+
"init", help="nueva ejecución: valida, crea el Issue y guarda memoria")
|
|
96
|
+
common(subparser, url_required=True)
|
|
97
|
+
subparser.add_argument("--plan", default=None, metavar="JSON",
|
|
98
|
+
help='plan de pasos con nombres ÚNICOS, ej. \'["Paso 1","Paso 2"]\'')
|
|
99
|
+
subparser.add_argument("--plan-file", default=None, metavar="F",
|
|
100
|
+
help="archivo JSON con la lista de pasos")
|
|
101
|
+
subparser.add_argument("--prompt-token", action="store_true",
|
|
102
|
+
help="pedir el token interactivamente (TTY)")
|
|
103
|
+
subparser.add_argument("--store-token", action="store_true",
|
|
104
|
+
help="persistir el token provisto (opt-in; por defecto NO se guarda)")
|
|
105
|
+
subparser.add_argument("--no-store-token", action="store_true",
|
|
106
|
+
help="obsoleto (compatibilidad): ahora NO guardar es el default")
|
|
107
|
+
subparser.set_defaults(func=cmd_init)
|
|
108
|
+
|
|
109
|
+
subparser = subparsers.add_parser(
|
|
110
|
+
"resume", help="lee el último comentario del Issue y planea la reanudación")
|
|
111
|
+
common(subparser)
|
|
112
|
+
subparser.set_defaults(func=cmd_resume)
|
|
113
|
+
|
|
114
|
+
subparser = subparsers.add_parser(
|
|
115
|
+
"step-start", help="comenta [STATUS: INICIANDO] - Paso: N")
|
|
116
|
+
common(subparser)
|
|
117
|
+
subparser.add_argument("--name", required=True, metavar="N", help="nombre del paso")
|
|
118
|
+
subparser.set_defaults(func=cmd_step_start)
|
|
119
|
+
|
|
120
|
+
subparser = subparsers.add_parser(
|
|
121
|
+
"step-done", help="comenta [STATUS: COMPLETADO] - Paso: N")
|
|
122
|
+
common(subparser)
|
|
123
|
+
subparser.add_argument("--name", required=True, metavar="N", help="nombre del paso")
|
|
124
|
+
subparser.set_defaults(func=cmd_step_done)
|
|
125
|
+
|
|
126
|
+
subparser = subparsers.add_parser(
|
|
127
|
+
"step-error", help="comenta [STATUS: ERROR] y detiene el proceso")
|
|
128
|
+
common(subparser)
|
|
129
|
+
subparser.add_argument("--name", required=True, metavar="N", help="nombre del paso")
|
|
130
|
+
subparser.add_argument("--details", default=None, metavar="D",
|
|
131
|
+
help="mensaje de error o stacktrace (se sanitiza antes de publicar)")
|
|
132
|
+
subparser.add_argument("--details-file", default=None, metavar="F",
|
|
133
|
+
help="archivo con el error/stacktrace")
|
|
134
|
+
subparser.set_defaults(func=cmd_step_error)
|
|
135
|
+
|
|
136
|
+
subparser = subparsers.add_parser(
|
|
137
|
+
"finalize", help="cierra el Issue con resultado FINALIZADO o CANCELADO")
|
|
138
|
+
common(subparser)
|
|
139
|
+
subparser.add_argument("--force", action="store_true",
|
|
140
|
+
help="cancelar aunque queden pasos pendientes o no haya trabajo "
|
|
141
|
+
"(no aplica si el Issue fue cerrado externamente)")
|
|
142
|
+
subparser.set_defaults(func=cmd_finalize)
|
|
143
|
+
|
|
144
|
+
subparser = subparsers.add_parser(
|
|
145
|
+
"status", help="muestra el estado local (y remoto con --remote)")
|
|
146
|
+
common(subparser)
|
|
147
|
+
subparser.add_argument("--remote", action="store_true",
|
|
148
|
+
help="consultar también el Issue en GitHub")
|
|
149
|
+
subparser.set_defaults(func=cmd_status)
|
|
150
|
+
|
|
151
|
+
subparser = subparsers.add_parser(
|
|
152
|
+
"adopt", help="recupera el estado local desde un Issue remoto")
|
|
153
|
+
common(subparser, url_required=True)
|
|
154
|
+
subparser.add_argument("--execution-id", required=True, metavar="ID",
|
|
155
|
+
help="Execution ID EXACTO del título del Issue")
|
|
156
|
+
subparser.add_argument("--force", action="store_true",
|
|
157
|
+
help="reemplazar un proceso activo existente (conserva token "
|
|
158
|
+
"e historial)")
|
|
159
|
+
subparser.set_defaults(func=cmd_adopt)
|
|
160
|
+
|
|
161
|
+
subparser = subparsers.add_parser(
|
|
162
|
+
"clear", help="limpia la memoria interna (soft por defecto)")
|
|
163
|
+
common(subparser)
|
|
164
|
+
subparser.add_argument("--hard", action="store_true",
|
|
165
|
+
help="eliminar el archivo de estado por completo")
|
|
166
|
+
subparser.set_defaults(func=cmd_clear)
|
|
167
|
+
|
|
168
|
+
subparser = subparsers.add_parser(
|
|
169
|
+
"doctor", help="diagnóstico del entorno (siempre exit 0)")
|
|
170
|
+
common(subparser)
|
|
171
|
+
subparser.set_defaults(func=cmd_doctor)
|
|
172
|
+
return parser
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main(argv=None):
|
|
176
|
+
try:
|
|
177
|
+
args = build_parser().parse_args(argv)
|
|
178
|
+
_sync_runtime()
|
|
179
|
+
return args.func(args)
|
|
180
|
+
except SkillError as error:
|
|
181
|
+
emit(error.to_json())
|
|
182
|
+
return error.exit_code
|
|
183
|
+
except KeyboardInterrupt:
|
|
184
|
+
return 130
|
|
185
|
+
except SystemExit:
|
|
186
|
+
raise
|
|
187
|
+
except Exception as error:
|
|
188
|
+
emit({"ok": False, "error": {
|
|
189
|
+
"code": "UNEXPECTED",
|
|
190
|
+
"message": f"{type(error).__name__}: {error}",
|
|
191
|
+
"hint": "error no previsto; reporta el comando exacto y este JSON"}})
|
|
192
|
+
return EXIT_RUNTIME
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Componentes internos de github-proyect-tracking."""
|