kcreate-scaffolding-gcp-script 0.0.0-stage → 0.2.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 +429 -2
- package/bin/build.js +70 -0
- package/bin/cli.js +441 -0
- package/package.json +36 -4
- package/skill/SKILL.md +262 -0
- package/skill/references/lessons-learned.md +706 -0
- package/skill/scripts/01_init_config.sh +151 -0
- package/skill/scripts/02_print_config.sh +60 -0
- package/skill/scripts/10_build_components.sh +596 -0
- package/skill/scripts/20_validate_file_parity.sh +107 -0
- package/skill/scripts/21_validate_project_pinning.sh +105 -0
- package/skill/scripts/22_validate_no_app_name_leftover.sh +62 -0
- package/skill/scripts/lib/common.sh +788 -0
- package/skill/scripts/run_tracking.sh +93 -0
- package/skill/templates/scaffolding.config.template +81 -0
- package/vendor/github-proyect-tracking/SKILL.md +180 -0
- package/vendor/github-proyect-tracking/references/api.md +185 -0
- package/vendor/github-proyect-tracking/references/architecture.md +171 -0
- package/vendor/github-proyect-tracking/scripts/tracking.py +214 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/__init__.py +1 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/commands.py +940 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/common.py +208 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/events.py +233 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/github.py +324 -0
- package/vendor/github-proyect-tracking/scripts/tracking_core/state.py +379 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Constantes, errores y utilidades compartidas por el CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
VERSION = "0.6.0"
|
|
11
|
+
API_BASE = "https://api.github.com"
|
|
12
|
+
USER_AGENT = f"github-proyect-tracking/{VERSION}"
|
|
13
|
+
|
|
14
|
+
STATE_DIR_ENV = "GITHUB_TRACKING_STATE"
|
|
15
|
+
SCHEMA_VERSION = 2
|
|
16
|
+
SUPPORTED_SCHEMA_VERSIONS = (1, SCHEMA_VERSION)
|
|
17
|
+
PROCESS_STATES = ("RUNNING", "ERROR", "FINALIZED", "CANCELADO",
|
|
18
|
+
"CLEARED", "ABORTED")
|
|
19
|
+
|
|
20
|
+
STATUS_INICIANDO = "INICIANDO"
|
|
21
|
+
STATUS_ERROR = "ERROR"
|
|
22
|
+
STATUS_COMPLETADO = "COMPLETADO"
|
|
23
|
+
STATUS_FINALIZADO = "FINALIZADO"
|
|
24
|
+
STATUS_CANCELADO = "CANCELADO"
|
|
25
|
+
|
|
26
|
+
EXIT_OK = 0
|
|
27
|
+
EXIT_RUNTIME = 1
|
|
28
|
+
EXIT_VALIDATION = 2
|
|
29
|
+
EXIT_TOKEN_INSUFFICIENT = 3
|
|
30
|
+
EXIT_NO_TOKEN = 4
|
|
31
|
+
EXIT_NO_PROCESS = 5
|
|
32
|
+
EXIT_NOT_FOUND = 6
|
|
33
|
+
EXIT_RESUME_REQUIRED = 7
|
|
34
|
+
EXIT_CONFLICT = 8
|
|
35
|
+
|
|
36
|
+
MAX_STEP_LEN = 200
|
|
37
|
+
MAX_DETAILS_LEN = 60000
|
|
38
|
+
MAX_PLAN_STEPS = 100
|
|
39
|
+
MAX_COMMENT_PAGES = 100
|
|
40
|
+
LOCK_STALE_SECONDS = 120
|
|
41
|
+
LOCK_WAIT_SECONDS = 10
|
|
42
|
+
AMBIGUOUS_HTTP_STATUSES = {500, 502, 503, 504}
|
|
43
|
+
|
|
44
|
+
REPO_URL_RE = re.compile(
|
|
45
|
+
r"^https://(?:www\.)?github\.com/"
|
|
46
|
+
r"(?P<owner>[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)/"
|
|
47
|
+
r"(?P<repo>[A-Za-z0-9._-]+?)(?:\.git)?/?$",
|
|
48
|
+
re.IGNORECASE,
|
|
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)
|
|
53
|
+
TITLE_RE = re.compile(
|
|
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*$",
|
|
59
|
+
re.IGNORECASE,
|
|
60
|
+
)
|
|
61
|
+
MAX_PROCESS_NAME_LEN = 100
|
|
62
|
+
|
|
63
|
+
SECRET_REDACTIONS = [
|
|
64
|
+
(re.compile(r"(?i)\b(authorization|proxy-authorization)\s*[:=]\s*"
|
|
65
|
+
r"(?:bearer|token|basic)?\s*[A-Za-z0-9._\-+/=~]{8,}"),
|
|
66
|
+
"[REDactado:authorization]"),
|
|
67
|
+
(re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._\-+/=]{8,}"), "[REDactado:bearer]"),
|
|
68
|
+
(re.compile(r"github_pat_[A-Za-z0-9_]{22,}"), "[REDACTADO:github-pat]"),
|
|
69
|
+
(re.compile(r"\bgh[pousr]_[A-Za-z0-9]{16,}\b"), "[REDactado:github-token]"),
|
|
70
|
+
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[REDactado:aws-key]"),
|
|
71
|
+
(re.compile(r"(?i)\b(password|passwd|pwd|secret|api[_-]?key|apikey|"
|
|
72
|
+
r"client[_-]?secret)\b\s*[=:]\s*['\"]?[A-Za-z0-9._\-+/=~]{6,}"),
|
|
73
|
+
"[REDactado:secret]"),
|
|
74
|
+
(re.compile(r"(?i)([a-z][a-z0-9+.\-]*://)[^/@\s]+@"),
|
|
75
|
+
r"\1[REDactado]@"),
|
|
76
|
+
]
|
|
77
|
+
|
|
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
|
+
|
|
116
|
+
class SkillError(Exception):
|
|
117
|
+
"""Error operativo que se serializa como un único objeto JSON."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, exit_code, code, message, extra=None):
|
|
120
|
+
super().__init__(message)
|
|
121
|
+
self.exit_code = exit_code
|
|
122
|
+
self.code = code
|
|
123
|
+
self.message = message
|
|
124
|
+
self.extra = extra or {}
|
|
125
|
+
|
|
126
|
+
def to_json(self):
|
|
127
|
+
error = {"code": self.code, "message": self.message}
|
|
128
|
+
error.update(self.extra)
|
|
129
|
+
return {"ok": False, "error": error}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def emit(obj):
|
|
133
|
+
print(json.dumps(obj, indent=2, ensure_ascii=False), flush=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def now_iso():
|
|
137
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def new_execution_id():
|
|
141
|
+
return (f"exec-{datetime.now(timezone.utc):%Y%m%dT%H%M%SZ}-"
|
|
142
|
+
f"{uuid.uuid4().hex[:12]}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def ordered_unique(items):
|
|
146
|
+
seen, output = set(), []
|
|
147
|
+
for item in items:
|
|
148
|
+
if item and item not in seen:
|
|
149
|
+
seen.add(item)
|
|
150
|
+
output.append(item)
|
|
151
|
+
return output
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def validate_repo_url(url):
|
|
155
|
+
if not isinstance(url, str):
|
|
156
|
+
return None
|
|
157
|
+
match = REPO_URL_RE.match(url.strip())
|
|
158
|
+
if not match:
|
|
159
|
+
return None
|
|
160
|
+
owner, repo = match.group("owner").lower(), match.group("repo").lower()
|
|
161
|
+
if ".." in repo or repo.startswith(".") or repo.endswith("."):
|
|
162
|
+
return None
|
|
163
|
+
return owner, repo
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def redact_url(url):
|
|
167
|
+
if not isinstance(url, str):
|
|
168
|
+
return repr(url)
|
|
169
|
+
output = re.sub(r"(?i)([a-z][a-z0-9+.\-]*://)[^/@\s]+@",
|
|
170
|
+
r"\1[REDactado]@", url)
|
|
171
|
+
output = re.sub(r"#.*$", "#[REDactado]", output)
|
|
172
|
+
return re.sub(r"\?.*$", "?[REDactado]", output)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sanitize_text(text):
|
|
176
|
+
if not text:
|
|
177
|
+
return (text or ""), 0
|
|
178
|
+
output, total = text, 0
|
|
179
|
+
for regex, replacement in SECRET_REDACTIONS:
|
|
180
|
+
output, count = regex.subn(replacement, output)
|
|
181
|
+
total += count
|
|
182
|
+
return output, total
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _step_name_unsafe(name):
|
|
186
|
+
if not isinstance(name, str) or not name.strip():
|
|
187
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
188
|
+
"nombre de paso requerido (no vacío)")
|
|
189
|
+
if len(name) > MAX_STEP_LEN:
|
|
190
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
191
|
+
f"nombre de paso demasiado largo (>{MAX_STEP_LEN} caracteres)")
|
|
192
|
+
if "\n" in name or "\r" in name:
|
|
193
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
194
|
+
"el nombre del paso no puede contener saltos de línea")
|
|
195
|
+
return name
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def normalize_step_name(name):
|
|
199
|
+
normalized = _step_name_unsafe(name).strip()
|
|
200
|
+
if re.search(r"(?i)\bdetalles\b\s*:", normalized):
|
|
201
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
202
|
+
"el nombre del paso no puede contener 'Detalles:' en "
|
|
203
|
+
"ninguna capitalización (rompe el parser de reanudación)")
|
|
204
|
+
clean, _ = sanitize_text(normalized)
|
|
205
|
+
if not clean.strip():
|
|
206
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
207
|
+
"el nombre del paso quedó vacío tras sanitizar")
|
|
208
|
+
return clean.strip()
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Contrato visible de comentarios, metadatos y planner de reanudación."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .common import (
|
|
9
|
+
EXIT_VALIDATION, STATUS_CANCELADO, STATUS_COMPLETADO, STATUS_ERROR,
|
|
10
|
+
STATUS_FINALIZADO, STATUS_INICIANDO, SkillError, now_iso, ordered_unique,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
COMMENT_RE = re.compile(
|
|
14
|
+
r"\[STATUS:\s*(?P<status>INICIANDO|ERROR|COMPLETADO|FINALIZADO|CANCELADO)\]\s*-\s*"
|
|
15
|
+
r"(?:Paso:\s*(?P<step>[^\r\n]*?)\s*(?:-\s*Detalles:\s*(?P<details>.*))?[\r\n]*$"
|
|
16
|
+
r"|(?P<final>El\s+proceso\s+concluy[oó]\s+exitosamente\.?)"
|
|
17
|
+
r"|(?P<canceled>El\s+proceso\s+fue\s+cerrado\s+anticipadamente.*))",
|
|
18
|
+
re.IGNORECASE | re.DOTALL | re.MULTILINE,
|
|
19
|
+
)
|
|
20
|
+
PLAN_LINE_RE = re.compile(r"^\s*\d+\.\s+(.+?)\s*$", re.MULTILINE)
|
|
21
|
+
TRANSITION_RE = re.compile(
|
|
22
|
+
r"\n<!--\s*github-proyect-tracking:\s*(?P<meta>\{.*\})\s*-->\s*$",
|
|
23
|
+
re.DOTALL,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fmt_iniciando(step):
|
|
28
|
+
return f"[STATUS: INICIANDO] - Paso: {step}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fmt_error(step, details):
|
|
32
|
+
return f"[STATUS: ERROR] - Paso: {step} - Detalles: {details}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fmt_completado(step):
|
|
36
|
+
return f"[STATUS: COMPLETADO] - Paso: {step}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fmt_finalizado():
|
|
40
|
+
return "[STATUS: FINALIZADO] - El proceso concluyó exitosamente."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fmt_cancelado(pending):
|
|
44
|
+
if pending:
|
|
45
|
+
items = ", ".join(pending[:10]) + ("…" if len(pending) > 10 else "")
|
|
46
|
+
return ("[STATUS: CANCELADO] - El proceso fue cerrado anticipadamente "
|
|
47
|
+
f"(--force). Pasos pendientes: {items}.")
|
|
48
|
+
return "[STATUS: CANCELADO] - El proceso fue cerrado anticipadamente (--force)."
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def transition_metadata(body):
|
|
52
|
+
match = TRANSITION_RE.search(body or "")
|
|
53
|
+
if not match:
|
|
54
|
+
return {}
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(match.group("meta"))
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return {}
|
|
59
|
+
return data if isinstance(data, dict) else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def with_transition_metadata(body, execution_id, sequence, transition_id):
|
|
63
|
+
metadata = json.dumps({
|
|
64
|
+
"execution_id": execution_id,
|
|
65
|
+
"sequence": sequence,
|
|
66
|
+
"transition_id": transition_id,
|
|
67
|
+
}, ensure_ascii=False, separators=(",", ":"))
|
|
68
|
+
return f"{body}\n<!-- github-proyect-tracking: {metadata} -->"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_status_comment(body):
|
|
72
|
+
events = []
|
|
73
|
+
visible_body = TRANSITION_RE.sub("", body or "")
|
|
74
|
+
for match in COMMENT_RE.finditer(visible_body):
|
|
75
|
+
events.append({
|
|
76
|
+
"status": (match.group("status") or "").upper(),
|
|
77
|
+
"step": match.group("step").strip() if match.group("step") else None,
|
|
78
|
+
"details": (match.group("details") or "").strip()
|
|
79
|
+
if match.group("details") is not None else None,
|
|
80
|
+
})
|
|
81
|
+
return events
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def events_from_comments(comments):
|
|
85
|
+
events = []
|
|
86
|
+
for comment in comments or []:
|
|
87
|
+
body = comment.get("body") or ""
|
|
88
|
+
metadata = transition_metadata(body)
|
|
89
|
+
for event in parse_status_comment(body):
|
|
90
|
+
event["comment_id"] = comment.get("id")
|
|
91
|
+
event["created_at"] = comment.get("created_at")
|
|
92
|
+
event["transition_id"] = metadata.get("transition_id")
|
|
93
|
+
event["execution_id"] = metadata.get("execution_id")
|
|
94
|
+
event["sequence"] = metadata.get("sequence")
|
|
95
|
+
events.append(event)
|
|
96
|
+
events.sort(key=lambda event: (
|
|
97
|
+
event.get("created_at") or "", event.get("comment_id") or 0))
|
|
98
|
+
return events
|
|
99
|
+
|
|
100
|
+
|
|
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):
|
|
117
|
+
lines = [
|
|
118
|
+
"Proceso rastreado por el skill `github-proyect-tracking`.",
|
|
119
|
+
"",
|
|
120
|
+
f"- **Execution ID:** `{execution_id}`",
|
|
121
|
+
]
|
|
122
|
+
if process_name:
|
|
123
|
+
lines.append(f"- **Proceso:** {process_name}")
|
|
124
|
+
lines += [
|
|
125
|
+
f"- **Proyecto:** {url}",
|
|
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 += [
|
|
137
|
+
"",
|
|
138
|
+
"## Plan de pasos",
|
|
139
|
+
]
|
|
140
|
+
if plan:
|
|
141
|
+
lines += [f"{index}. {step}" for index, step in enumerate(plan, 1)]
|
|
142
|
+
else:
|
|
143
|
+
lines.append("_(sin plan explícito — los pasos los define el agente "
|
|
144
|
+
"durante la ejecución)_")
|
|
145
|
+
lines += [
|
|
146
|
+
"",
|
|
147
|
+
"> El avance se registra como comentarios con el contrato `[STATUS: ...]`.",
|
|
148
|
+
"> El ÚLTIMO comentario es la fuente de verdad para reanudar.",
|
|
149
|
+
]
|
|
150
|
+
return "\n".join(lines) + "\n"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_plan_from_body(body):
|
|
154
|
+
match = re.search(r"##\s*Plan de pasos(.*?)(?=\n##\s|\Z)", body or "",
|
|
155
|
+
re.DOTALL | re.IGNORECASE)
|
|
156
|
+
if not match:
|
|
157
|
+
return []
|
|
158
|
+
return PLAN_LINE_RE.findall(match.group(1))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _validate_resume_plan(plan):
|
|
162
|
+
plan = list(plan or [])
|
|
163
|
+
if len(plan) != len(set(plan)):
|
|
164
|
+
raise SkillError(
|
|
165
|
+
EXIT_VALIDATION, "PLAN_AMBIGUOUS",
|
|
166
|
+
"el plan contiene pasos duplicados y no permite una reanudación "
|
|
167
|
+
"determinista",
|
|
168
|
+
{"hint": "edita el cuerpo del Issue para asignar un nombre único a "
|
|
169
|
+
"cada paso; no se modificó ningún evento"})
|
|
170
|
+
return plan
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def plan_resume(events, plan, issue_state="open"):
|
|
174
|
+
plan = _validate_resume_plan(plan)
|
|
175
|
+
completed = ordered_unique(
|
|
176
|
+
event["step"] for event in events
|
|
177
|
+
if event["status"] == STATUS_COMPLETADO and event.get("step"))
|
|
178
|
+
output = {
|
|
179
|
+
"completed_steps": completed,
|
|
180
|
+
"issue_state": issue_state,
|
|
181
|
+
"plan": plan,
|
|
182
|
+
"event_count": len(events),
|
|
183
|
+
}
|
|
184
|
+
if issue_state == "closed":
|
|
185
|
+
if events and events[-1]["status"] == STATUS_FINALIZADO:
|
|
186
|
+
return {**output, "action": "NONE", "outcome": "FINALIZADO",
|
|
187
|
+
"reason": "El proceso ya finalizó y el Issue está cerrado."}
|
|
188
|
+
if events and events[-1]["status"] == STATUS_CANCELADO:
|
|
189
|
+
return {**output, "action": "NONE", "outcome": "CANCELADO",
|
|
190
|
+
"reason": "El proceso fue CANCELADO (--force) y el Issue "
|
|
191
|
+
"está cerrado."}
|
|
192
|
+
return {**output, "action": "ABORTED_EXTERNALLY", "resume_from_step": None,
|
|
193
|
+
"reason": "El Issue fue cerrado sin comentario de cierre; "
|
|
194
|
+
"consultar al usuario antes de reabrir o iniciar uno nuevo."}
|
|
195
|
+
if not events:
|
|
196
|
+
return {**output, "action": "START_FIRST",
|
|
197
|
+
"resume_from_step": plan[0] if plan else None,
|
|
198
|
+
"reason": "El Issue no tiene comentarios: comenzar desde el primer paso."}
|
|
199
|
+
last = events[-1]
|
|
200
|
+
output["last_status"] = last["status"]
|
|
201
|
+
if last["status"] == STATUS_FINALIZADO:
|
|
202
|
+
return {**output, "action": "FINISH_CLEANUP", "resume_from_step": None,
|
|
203
|
+
"outcome": "FINALIZADO",
|
|
204
|
+
"reason": "Último comentario FINALIZADO pero el Issue sigue abierto: "
|
|
205
|
+
"ejecutar finalize (idempotente) para cerrarlo y limpiar memoria."}
|
|
206
|
+
if last["status"] == STATUS_CANCELADO:
|
|
207
|
+
return {**output, "action": "FINISH_CLEANUP", "outcome": "CANCELADO",
|
|
208
|
+
"reason": "El proceso fue CANCELADO (--force); el Issue sigue "
|
|
209
|
+
"abierto: ejecutar finalize (idempotente) para "
|
|
210
|
+
"cerrarlo y limpiar memoria."}
|
|
211
|
+
if last["status"] in (STATUS_ERROR, STATUS_INICIANDO):
|
|
212
|
+
reason = ("El último paso registró ERROR: re-ejecutar ese paso (o el que "
|
|
213
|
+
"generó el error) y continuar con los siguientes."
|
|
214
|
+
if last["status"] == STATUS_ERROR else
|
|
215
|
+
"El último paso quedó en INICIANDO sin COMPLETADO: re-ejecutar ese paso.")
|
|
216
|
+
result = {**output, "action": "RERUN_STEP",
|
|
217
|
+
"resume_from_step": last.get("step"), "reason": reason}
|
|
218
|
+
if last["status"] == STATUS_ERROR:
|
|
219
|
+
result["failed_step"] = last.get("step")
|
|
220
|
+
result["error_details"] = (last.get("details") or "")[:2000]
|
|
221
|
+
return result
|
|
222
|
+
done = set(completed)
|
|
223
|
+
remaining = [step for step in plan if step not in done]
|
|
224
|
+
if remaining:
|
|
225
|
+
return {**output, "action": "NEXT_STEP", "resume_from_step": remaining[0],
|
|
226
|
+
"remaining_steps": remaining,
|
|
227
|
+
"reason": "Paso completado: continuar con el siguiente paso del plan."}
|
|
228
|
+
if plan:
|
|
229
|
+
return {**output, "action": "ALL_STEPS_DONE", "resume_from_step": None,
|
|
230
|
+
"reason": "Todos los pasos del plan están COMPLETADO: ejecutar finalize."}
|
|
231
|
+
return {**output, "action": "NEXT_STEP_AGENT_DEFINED", "resume_from_step": None,
|
|
232
|
+
"reason": "Sin plan explícito: el agente define el siguiente paso "
|
|
233
|
+
"o ejecuta finalize."}
|