@@ -167,6 +167,89 @@ def _install_evolution_helpers(repo_root: Path) -> list[str]:
167167 return installed
168168
169169
170+ # Labels required by the evolution pipeline. Kept in one place so every skill
171+ # stage (issues, introspection, integration, implementation) can rely on them
172+ # existing. Creation is idempotent; failures are warnings, not fatal.
173+ _EVOLUTION_LABELS : list [tuple [str , str , str ]] = [
174+ ("capability" , "5319e7" , "Missing ability users needed" ),
175+ ("introspection" , "0e8a16" , "Found by session introspection" ),
176+ ("ux" , "fbca04" , "Interaction friction" ),
177+ ("proposal" , "0e8a16" , "Evolution-generated improvement proposal" ),
178+ ("research-generated" , "1d76db" , "Created by the evolution research cycle" ),
179+ ("needs-work" , "d93f0b" , "Blocked by code-review (dead code / not integrated)" ),
180+ ("next-increment" , "1d76db" , "Roadmap increment merged; more deferred — re-queued" ),
181+ ("accepted" , "0e8a16" , "Accepted by evolution — sent to a PR / implemented" ),
182+ ("rejected" , "b60205" , "Not accepted by evolution — see closing comment" ),
183+ ("needs-split" , "d4c5f9" , "Wanted, but exceeds one cycle — needs decomposition" ),
184+ ("blocked" , "e11d21" , "Needs human/infrastructure action — see comment" ),
185+ ("fix" , "1d76db" , "Bug or fix" ),
186+ ("improvement" , "a2eeef" , "An improvement to existing functionality" ),
187+ (
188+ "implemented-on-main" ,
189+ "0e8a16" ,
190+ "Capability already exists on main — no code change needed" ,
191+ ),
192+ ]
193+
194+
195+ def _ensure_evolution_labels (repo_root : Path , dry_run : bool = False ) -> list [str ]:
196+ """Idempotently create the GitHub labels used by the evolution pipeline.
197+
198+ Several evolution skills call ``gh label create`` with the expectation that
199+ the label exists; on a fresh fork the labels are missing and every label
200+ operation fails silently (wasting API calls and leaving issues
201+ uncategorized — #468). This bootstrap step runs once per registration pass.
202+
203+ Returns the list of label names that were created or confirmed present.
204+ Warnings are printed for any failure, but registration continues.
205+ """
206+ import subprocess
207+
208+ created : list [str ] = []
209+ for name , color , description in _EVOLUTION_LABELS :
210+ cmd = [
211+ "gh" ,
212+ "label" ,
213+ "create" ,
214+ name ,
215+ "--repo" ,
216+ "Lexus2016/hermes-agent-evolution" ,
217+ "--color" ,
218+ color ,
219+ "--description" ,
220+ description ,
221+ ]
222+ if dry_run :
223+ print (f"[evolution-cron] dry-run label: { name } " )
224+ created .append (name )
225+ continue
226+ try :
227+ result = subprocess .run (
228+ cmd ,
229+ cwd = repo_root ,
230+ capture_output = True ,
231+ text = True ,
232+ check = False ,
233+ timeout = 30 ,
234+ )
235+ if result .returncode == 0 :
236+ created .append (name )
237+ elif "already exists" in (result .stderr or "" ).lower ():
238+ created .append (name )
239+ else :
240+ print (
241+ f"[evolution-cron] warning: could not create label { name } : "
242+ f"{ result .stderr or result .stdout } " ,
243+ file = sys .stderr ,
244+ )
245+ except Exception as exc : # pragma: no cover - gh may be missing
246+ print (
247+ f"[evolution-cron] warning: could not create label { name } : { exc } " ,
248+ file = sys .stderr ,
249+ )
250+ return created
251+
252+
170253def main (argv : list [str ]) -> int :
171254 dry_run = "--dry-run" in argv
172255 positional = [a for a in argv [1 :] if not a .startswith ("--" )]
@@ -178,6 +261,10 @@ def main(argv: list[str]) -> int:
178261 # the process when needed, so nobody has to launch us with the right python.
179262 _ensure_venv_python (repo_root , argv )
180263
264+ # Bootstrap the GitHub labels used by every evolution skill. Missing labels
265+ # make issue/PR operations fail silently on fresh forks (#468).
266+ label_ensured = [] if dry_run else _ensure_evolution_labels (repo_root )
267+
181268 src_dir = Path (positional [0 ]) if positional else repo_root / "cron" / "evolution"
182269 if not src_dir .is_dir ():
183270 print (f"[evolution-cron] no evolution cron dir at { src_dir } " , file = sys .stderr )
@@ -221,7 +308,11 @@ def main(argv: list[str]) -> int:
221308 # executes the copy in HERMES_HOME/scripts; without this refresh the
222309 # installed script stays frozen at whatever version existed when the
223310 # job was first registered.
224- if spec .get ("no_agent" ) and str (spec .get ("script" ) or "" ).strip () and not dry_run :
311+ if (
312+ spec .get ("no_agent" )
313+ and str (spec .get ("script" ) or "" ).strip ()
314+ and not dry_run
315+ ):
225316 _install_script (repo_root , str (spec ["script" ]).strip ())
226317
227318 schedule = str (spec .get ("schedule" ) or "" ).strip ()
@@ -252,7 +343,9 @@ def main(argv: list[str]) -> int:
252343 continue
253344 changes : dict = {}
254345 want_sched = parse_schedule (schedule ).get ("display" , schedule )
255- cur_sched = (cur .get ("schedule" ) or {}).get ("display" ) or cur .get ("schedule_display" )
346+ cur_sched = (cur .get ("schedule" ) or {}).get ("display" ) or cur .get (
347+ "schedule_display"
348+ )
256349 if want_sched != cur_sched :
257350 changes ["schedule" ] = schedule
258351 if not no_agent :
@@ -325,7 +418,7 @@ def main(argv: list[str]) -> int:
325418 print (
326419 f"[evolution-cron] { verb } ={ len (created )} reconciled={ len (updated )} "
327420 f"skipped(unchanged)={ len (skipped )} failed={ len (failed )} "
328- f"helper_scripts_installed={ len (helper_scripts )} "
421+ f"helper_scripts_installed={ len (helper_scripts )} labels_ensured= { len ( label_ensured ) } "
329422 )
330423 for name , jid in created :
331424 print (f" + { name } ({ jid } )" )
0 commit comments