@@ -125,6 +125,7 @@ resolve_source
125125
126126exec python3 - " $SOURCE_DIR " " $@ " << 'PYEOF '
127127import argparse
128+ import copy
128129import json
129130import os
130131import re
@@ -259,13 +260,25 @@ def remove_file(path):
259260
260261# ── JSON config helpers ────────────────────────────────────────────────────────
261262
263+ def merge_json_value(existing, desired):
264+ """Recursively merge JSON-like values, preserving unknown keys from existing objects."""
265+ if isinstance(existing, dict) and isinstance(desired, dict):
266+ merged = copy.deepcopy(existing)
267+ for key, desired_value in desired.items():
268+ merged[key] = merge_json_value(merged.get(key), desired_value)
269+ return merged
270+ return copy.deepcopy(desired)
271+
272+
262273def upsert_json_key(path, key_path: list, value):
263274 """Upsert a nested key into a JSON file. key_path = ['a', 'b', 'c'] → data['a']['b']['c'] = value."""
264275 data = read_json(path)
265276 cursor = data
266277 for key in key_path[:-1]:
267- cursor = cursor.setdefault(key, {})
268- cursor[key_path[-1]] = value
278+ if not isinstance(cursor.get(key), dict):
279+ cursor[key] = {}
280+ cursor = cursor[key]
281+ cursor[key_path[-1]] = merge_json_value(cursor.get(key_path[-1]), value)
269282 atomic_write_json(path, data)
270283
271284
@@ -289,10 +302,10 @@ def upsert_json_array_item(path, array_key: str, item: dict, id_key: str):
289302 arr = data.setdefault(array_key, [])
290303 for i, existing in enumerate(arr):
291304 if existing.get(id_key) == item.get(id_key):
292- arr[i] = item
305+ arr[i] = merge_json_value(existing, item)
293306 break
294307 else:
295- arr.append(item)
308+ arr.append(copy.deepcopy( item) )
296309 atomic_write_json(path, data)
297310
298311
@@ -337,10 +350,10 @@ def upsert_codex_marketplace_entry(path, item):
337350
338351 for index, existing in enumerate(plugins):
339352 if isinstance(existing, dict) and existing.get("name") == item.get("name"):
340- plugins[index] = item
353+ plugins[index] = merge_json_value(existing, item)
341354 break
342355 else:
343- plugins.append(item)
356+ plugins.append(copy.deepcopy( item) )
344357
345358 atomic_write_json(path, data)
346359
@@ -363,22 +376,82 @@ def _is_codex_recall_command(command):
363376 return isinstance(command, str) and "plugins/evolve-lite/skills/recall/scripts/retrieve_entities.py" in command
364377
365378
379+ def _codex_recall_hook():
380+ return {
381+ "type": "command",
382+ "command": _codex_recall_hook_command(),
383+ "statusMessage": "Loading Evolve guidance",
384+ }
385+
386+
366387def _codex_recall_hook_group():
367388 return {
368389 "matcher": "",
369- "hooks": [
370- {
371- "type": "command",
372- "command": _codex_recall_hook_command(),
373- "statusMessage": "Loading Evolve guidance",
374- }
375- ]
390+ "hooks": [_codex_recall_hook()],
376391 }
377392
378393
379- def _group_contains_codex_recall_command (group):
394+ def _iter_group_hooks (group):
380395 hooks = group.get("hooks", [])
381- return any(isinstance(hook, dict) and _is_codex_recall_command(hook.get("command")) for hook in hooks)
396+ if isinstance(hooks, list):
397+ return hooks
398+ if isinstance(hooks, dict):
399+ return hooks.values()
400+ return []
401+
402+
403+ def _group_contains_codex_recall_command(group):
404+ return any(isinstance(hook, dict) and _is_codex_recall_command(hook.get("command")) for hook in _iter_group_hooks(group))
405+
406+
407+ def _upsert_codex_recall_hook_into_group(group):
408+ updated_group = copy.deepcopy(group)
409+ recall_hook = _codex_recall_hook()
410+ hooks = updated_group.get("hooks")
411+
412+ if isinstance(hooks, list):
413+ for index, existing_hook in enumerate(hooks):
414+ if isinstance(existing_hook, dict) and _is_codex_recall_command(existing_hook.get("command")):
415+ hooks[index] = merge_json_value(existing_hook, recall_hook)
416+ break
417+ else:
418+ hooks.append(copy.deepcopy(recall_hook))
419+ return updated_group
420+
421+ if isinstance(hooks, dict):
422+ for key, existing_hook in hooks.items():
423+ if isinstance(existing_hook, dict) and _is_codex_recall_command(existing_hook.get("command")):
424+ hooks[key] = merge_json_value(existing_hook, recall_hook)
425+ break
426+ else:
427+ hooks["evolve-lite"] = copy.deepcopy(recall_hook)
428+ return updated_group
429+
430+ updated_group["hooks"] = [copy.deepcopy(recall_hook)]
431+ return updated_group
432+
433+
434+ def _remove_codex_recall_hook_from_group(group):
435+ updated_group = copy.deepcopy(group)
436+ hooks = updated_group.get("hooks")
437+
438+ if isinstance(hooks, list):
439+ updated_group["hooks"] = [
440+ hook
441+ for hook in hooks
442+ if not (isinstance(hook, dict) and _is_codex_recall_command(hook.get("command")))
443+ ]
444+ return updated_group
445+
446+ if isinstance(hooks, dict):
447+ updated_group["hooks"] = {
448+ key: hook
449+ for key, hook in hooks.items()
450+ if not (isinstance(hook, dict) and _is_codex_recall_command(hook.get("command")))
451+ }
452+ return updated_group
453+
454+ return updated_group
382455
383456
384457def upsert_codex_user_prompt_hook(path, group):
@@ -401,10 +474,10 @@ def upsert_codex_user_prompt_hook(path, group):
401474
402475 for index, existing in enumerate(groups):
403476 if isinstance(existing, dict) and _group_contains_codex_recall_command(existing):
404- groups[index] = group
477+ groups[index] = _upsert_codex_recall_hook_into_group(existing)
405478 break
406479 else:
407- groups.append(group)
480+ groups.append(copy.deepcopy( group) )
408481
409482 atomic_write_json(path, data)
410483
@@ -424,7 +497,10 @@ def remove_codex_user_prompt_hook(path):
424497 return
425498
426499 hooks["UserPromptSubmit"] = [
427- group for group in groups if not (isinstance(group, dict) and _group_contains_codex_recall_command(group))
500+ _remove_codex_recall_hook_from_group(group)
501+ if isinstance(group, dict) and _group_contains_codex_recall_command(group)
502+ else group
503+ for group in groups
428504 ]
429505 if not hooks["UserPromptSubmit"]:
430506 hooks.pop("UserPromptSubmit", None)
0 commit comments