Coverage for astrocyte/harness/choices.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-10-29 22:10 +0000

1"""Agents the user switched off, remembered across ``astrocyte setup`` runs. 

2 

3Plain ``astrocyte setup`` wires every agent it detects. Without a record of 

4what the user turned off, the next plain run (after an upgrade, say) turns it 

5back on. ``astrocyte uninstall --<agent>`` and ``astrocyte setup --no-hooks`` 

6record the choice here; naming the agent again (``astrocyte setup --claude``) 

7clears it. 

8 

9Kept beside the config file, as ``harnesses.json``: it is a preference, and a 

10different config (``--config`` / ``$ASTROCYTE_CONFIG``) is a different install. 

11""" 

12 

13from __future__ import annotations 

14 

15import json 

16import os 

17from dataclasses import dataclass, field 

18from pathlib import Path 

19 

20FILE_NAME = "harnesses.json" 

21 

22 

23@dataclass 

24class Choices: 

25 off: set[str] = field(default_factory=set) # not wired at all 

26 hooks_off: set[str] = field(default_factory=set) # wired, without automatic memory 

27 file_recall: set[str] = field(default_factory=set) # opted in to recall on file reads/edits 

28 

29 

30def choices_path(config: Path) -> Path: 

31 return config.parent / FILE_NAME 

32 

33 

34def load_choices(config: Path) -> Choices: 

35 """The recorded choices; none when the file is missing or unreadable.""" 

36 try: 

37 data = json.loads(choices_path(config).read_text(encoding="utf-8")) 

38 except (OSError, ValueError): 

39 return Choices() 

40 if not isinstance(data, dict): 

41 return Choices() 

42 

43 def keys(name: str) -> set[str]: 

44 value = data.get(name) 

45 return {k for k in value if isinstance(k, str)} if isinstance(value, list) else set() 

46 

47 return Choices(off=keys("off"), hooks_off=keys("hooks_off"), file_recall=keys("file_recall")) 

48 

49 

50def save_choices(config: Path, choices: Choices) -> None: 

51 path = choices_path(config) 

52 if not (choices.off or choices.hooks_off or choices.file_recall or path.exists()): 

53 return # no choice made: no file to leave behind 

54 path.parent.mkdir(parents=True, exist_ok=True) 

55 tmp = path.with_name(path.name + ".tmp") 

56 data = {"off": sorted(choices.off), "hooks_off": sorted(choices.hooks_off)} 

57 if choices.file_recall: 

58 data["file_recall"] = sorted(choices.file_recall) 

59 tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") 

60 os.replace(tmp, path)