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
« 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.
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.
9Kept beside the config file, as ``harnesses.json``: it is a preference, and a
10different config (``--config`` / ``$ASTROCYTE_CONFIG``) is a different install.
11"""
13from __future__ import annotations
15import json
16import os
17from dataclasses import dataclass, field
18from pathlib import Path
20FILE_NAME = "harnesses.json"
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
30def choices_path(config: Path) -> Path:
31 return config.parent / FILE_NAME
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()
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()
47 return Choices(off=keys("off"), hooks_off=keys("hooks_off"), file_recall=keys("file_recall"))
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)