agent-bios 0.3.0 → 0.4.0
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/DEPENDENCIES.md +1 -0
- package/README.md +2 -1
- package/claude/CLAUDE.md +3 -1
- package/claude/guides/cli-multi-model-workflow.md +6 -0
- package/claude/guides/{session-learning-workflow.md → session-distill-workflow.md} +13 -13
- package/claude/hooks/tooling-gotchas-hook.py +1 -1
- package/codex/AGENTS.md +3 -1
- package/codex/guides/cli-multi-model-workflow.md +6 -0
- package/codex/guides/{session-learning-workflow.md → session-distill-workflow.md} +13 -13
- package/config/agent-launch.toml +34 -8
- package/package.json +1 -1
- package/scripts/agent-launch.py +248 -159
- package/scripts/check-parity.sh +99 -14
- package/scripts/install.sh +116 -15
package/scripts/agent-launch.py
CHANGED
|
@@ -44,6 +44,17 @@ EFFORT_DESCRIPTIONS = {
|
|
|
44
44
|
}
|
|
45
45
|
CUSTOM_PRESET = "__custom__"
|
|
46
46
|
OTHER_MODEL = "__other_model__"
|
|
47
|
+
# Root-menu grouping for presets: "builder" (tunable, includes Custom), "general"
|
|
48
|
+
# (plain CLI presets like Vanilla), "distill" (opens the Session Distill hub).
|
|
49
|
+
# A preset with a missing/unknown mode defaults to "builder" so presets written
|
|
50
|
+
# before this field existed (including saved/local user presets) keep working.
|
|
51
|
+
GENERAL_MODE = "general"
|
|
52
|
+
DEFAULT_PRESET_MODE = "builder"
|
|
53
|
+
# The mode value that opens the Session Distill hub instead of a preset
|
|
54
|
+
# submenu; doubles as the option value on the root Mode menu so selecting it
|
|
55
|
+
# needs no translation.
|
|
56
|
+
DISTILL_MODE = "distill"
|
|
57
|
+
PRESET_MODES = (GENERAL_MODE, DEFAULT_PRESET_MODE, DISTILL_MODE)
|
|
47
58
|
# User-saved presets live beside the deployed config, in a file the installer
|
|
48
59
|
# neither deploys nor verifies, so they survive `agent-bios install`.
|
|
49
60
|
USER_PRESETS_NAME = "presets.local.toml"
|
|
@@ -62,8 +73,9 @@ class MenuOption:
|
|
|
62
73
|
description: str
|
|
63
74
|
enabled: bool = True
|
|
64
75
|
unavailable_reason: str = ""
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
STANDARD_POLICY = "standard"
|
|
77
|
+
CODEX_POLICIES = {"bypass", "workspace-write", "read-only", STANDARD_POLICY}
|
|
78
|
+
CLAUDE_POLICIES = {"acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan", STANDARD_POLICY}
|
|
67
79
|
REVIEW_SETUPS = {
|
|
68
80
|
"none": {
|
|
69
81
|
"label": "None",
|
|
@@ -543,7 +555,6 @@ _UI_CANCEL = "\x00cancel"
|
|
|
543
555
|
def _build_app_class():
|
|
544
556
|
"""Import textual lazily and build the App/Screen classes, so importing this
|
|
545
557
|
module and every non-interactive path stays free of the textual dependency."""
|
|
546
|
-
from rich.text import Text
|
|
547
558
|
from textual import work
|
|
548
559
|
from textual.app import App
|
|
549
560
|
from textual.binding import Binding
|
|
@@ -583,8 +594,8 @@ def _build_app_class():
|
|
|
583
594
|
border: round $secondary; border-title-color: $secondary;
|
|
584
595
|
border-title-style: bold; padding: 0 1; height: 5;
|
|
585
596
|
}
|
|
586
|
-
#al-
|
|
587
|
-
#al-
|
|
597
|
+
#al-corpus-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
|
|
598
|
+
#al-corpus {
|
|
588
599
|
border: round $warning; padding: 0 1; height: auto;
|
|
589
600
|
}
|
|
590
601
|
#al-hdr { color: $text-muted; text-style: bold; padding: 0 1; }
|
|
@@ -606,7 +617,7 @@ def _build_app_class():
|
|
|
606
617
|
]
|
|
607
618
|
|
|
608
619
|
def __init__(
|
|
609
|
-
self, title, options, default, allow_back, plan, preview=None,
|
|
620
|
+
self, title, options, default, allow_back, plan, preview=None, corpus=None
|
|
610
621
|
):
|
|
611
622
|
super().__init__()
|
|
612
623
|
self._title = title
|
|
@@ -615,14 +626,14 @@ def _build_app_class():
|
|
|
615
626
|
self._allow_back = allow_back
|
|
616
627
|
self._plan = plan
|
|
617
628
|
self._preview = preview
|
|
618
|
-
self.
|
|
629
|
+
self._corpus = corpus
|
|
619
630
|
|
|
620
631
|
def compose(self):
|
|
621
632
|
yield Static(self._title, id="al-title")
|
|
622
633
|
yield setup_panel(self._plan)
|
|
623
|
-
if self.
|
|
624
|
-
yield Static("
|
|
625
|
-
yield Static("\n".join(self.
|
|
634
|
+
if self._corpus:
|
|
635
|
+
yield Static("Corpus status", id="al-corpus-title")
|
|
636
|
+
yield Static("\n".join(self._corpus), id="al-corpus")
|
|
626
637
|
detail = Static("", id="al-detail")
|
|
627
638
|
detail.border_title = "About highlighted option"
|
|
628
639
|
yield detail
|
|
@@ -632,19 +643,6 @@ def _build_app_class():
|
|
|
632
643
|
option_list = OptionList()
|
|
633
644
|
for option in self._options:
|
|
634
645
|
label = option.label + ("" if option.enabled else " [unavailable]")
|
|
635
|
-
if option.value == LEARNING_HUB:
|
|
636
|
-
# The learning area is a different kind of destination than
|
|
637
|
-
# the launch presets: separated, marked, and warning-toned
|
|
638
|
-
# to match its status panel.
|
|
639
|
-
option_list.add_option(None)
|
|
640
|
-
option_list.add_option(
|
|
641
|
-
Option(
|
|
642
|
-
Text(f"◆ {label}", style="bold yellow"),
|
|
643
|
-
id=option.value,
|
|
644
|
-
disabled=not option.enabled,
|
|
645
|
-
)
|
|
646
|
-
)
|
|
647
|
-
continue
|
|
648
646
|
option_list.add_option(
|
|
649
647
|
Option(label, id=option.value, disabled=not option.enabled)
|
|
650
648
|
)
|
|
@@ -803,12 +801,12 @@ class TextualUI:
|
|
|
803
801
|
default: str,
|
|
804
802
|
allow_back: bool,
|
|
805
803
|
preview=None,
|
|
806
|
-
|
|
804
|
+
corpus_lines: list[str] | None = None,
|
|
807
805
|
) -> str:
|
|
808
806
|
result = self.app.call_from_thread(
|
|
809
807
|
self.app.push_screen_wait,
|
|
810
808
|
self._menu_screen(
|
|
811
|
-
title, options, default, allow_back, self.plan, preview,
|
|
809
|
+
title, options, default, allow_back, self.plan, preview, corpus_lines
|
|
812
810
|
),
|
|
813
811
|
)
|
|
814
812
|
if result == _UI_BACK:
|
|
@@ -849,21 +847,18 @@ def choose_lines(
|
|
|
849
847
|
options: list[MenuOption],
|
|
850
848
|
default: str,
|
|
851
849
|
allow_back: bool,
|
|
852
|
-
|
|
850
|
+
corpus_lines: list[str] | None = None,
|
|
853
851
|
) -> str:
|
|
854
852
|
print(f"\n{title}")
|
|
855
|
-
if
|
|
856
|
-
print(" --
|
|
857
|
-
for line in
|
|
853
|
+
if corpus_lines:
|
|
854
|
+
print(" -- Corpus status --")
|
|
855
|
+
for line in corpus_lines:
|
|
858
856
|
print(f" {line}")
|
|
859
857
|
print(" --")
|
|
860
858
|
for index, option in enumerate(options, 1):
|
|
861
859
|
marker = "" if option.enabled else " [unavailable]"
|
|
862
860
|
selected = " *" if option.value == default and option.enabled else ""
|
|
863
861
|
label = option.label
|
|
864
|
-
if option.value == LEARNING_HUB:
|
|
865
|
-
print(" " + "-" * 30)
|
|
866
|
-
label = f"◆ {label}"
|
|
867
862
|
print(f" {index}. {label}{marker}{selected} - {option.description}")
|
|
868
863
|
if not option.enabled:
|
|
869
864
|
print(f" Unavailable: {option.unavailable_reason}")
|
|
@@ -898,13 +893,13 @@ def choose(
|
|
|
898
893
|
ui: TextualUI | None = None,
|
|
899
894
|
allow_back: bool = False,
|
|
900
895
|
preview=None,
|
|
901
|
-
|
|
896
|
+
corpus_lines: list[str] | None = None,
|
|
902
897
|
) -> str:
|
|
903
898
|
if not any(option.enabled for option in options):
|
|
904
899
|
raise LaunchError(f"no available options for {title}")
|
|
905
900
|
if ui is not None:
|
|
906
|
-
return ui.choose(title, options, default, allow_back, preview,
|
|
907
|
-
return choose_lines(title, options, default, allow_back,
|
|
901
|
+
return ui.choose(title, options, default, allow_back, preview, corpus_lines)
|
|
902
|
+
return choose_lines(title, options, default, allow_back, corpus_lines)
|
|
908
903
|
|
|
909
904
|
|
|
910
905
|
def read_input(prompt: str) -> str:
|
|
@@ -995,6 +990,11 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
995
990
|
label = preset.get("label", preset_name)
|
|
996
991
|
if not isinstance(label, str) or not label:
|
|
997
992
|
raise LaunchError(f"invalid label in preset {preset_name}")
|
|
993
|
+
mode = preset.get("mode")
|
|
994
|
+
if not isinstance(mode, str) or mode not in PRESET_MODES:
|
|
995
|
+
# Missing/unknown mode (older or user/local presets) defaults to builder
|
|
996
|
+
# rather than failing closed, so existing presets keep working unchanged.
|
|
997
|
+
mode = DEFAULT_PRESET_MODE
|
|
998
998
|
mission = preset.get("mission")
|
|
999
999
|
if mission is not None and (not isinstance(mission, str) or not mission):
|
|
1000
1000
|
raise LaunchError(f"presets.{preset_name}.mission must be a non-empty string")
|
|
@@ -1024,6 +1024,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
1024
1024
|
"preset": preset_name,
|
|
1025
1025
|
"label": label,
|
|
1026
1026
|
"description": preset.get("description", f"Launch the {label} preset."),
|
|
1027
|
+
"mode": mode,
|
|
1027
1028
|
"main_tier": main_tier,
|
|
1028
1029
|
"frontier_effort": frontier_effort,
|
|
1029
1030
|
"review_setup": review_setup,
|
|
@@ -1226,6 +1227,11 @@ def customize(
|
|
|
1226
1227
|
"Read-only sandbox",
|
|
1227
1228
|
"Allow inspection while blocking file writes through the Codex sandbox.",
|
|
1228
1229
|
),
|
|
1230
|
+
MenuOption(
|
|
1231
|
+
STANDARD_POLICY,
|
|
1232
|
+
"Standard",
|
|
1233
|
+
"Pass no policy flag; use Codex's own default approval and sandbox behavior.",
|
|
1234
|
+
),
|
|
1229
1235
|
]
|
|
1230
1236
|
policy_field = "codex_execution_policy"
|
|
1231
1237
|
policy_title = "Codex execution policy"
|
|
@@ -1261,6 +1267,11 @@ def customize(
|
|
|
1261
1267
|
"Plan mode",
|
|
1262
1268
|
"Start Claude in planning mode without direct implementation.",
|
|
1263
1269
|
),
|
|
1270
|
+
MenuOption(
|
|
1271
|
+
STANDARD_POLICY,
|
|
1272
|
+
"Standard",
|
|
1273
|
+
"Pass no policy flag; use Claude's own default permission prompts.",
|
|
1274
|
+
),
|
|
1264
1275
|
]
|
|
1265
1276
|
policy_field = "claude_permission_mode"
|
|
1266
1277
|
policy_title = "Claude permission mode (not an OS sandbox)"
|
|
@@ -1439,28 +1450,27 @@ def customize(
|
|
|
1439
1450
|
raise KeyboardInterrupt
|
|
1440
1451
|
|
|
1441
1452
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
LEARNING_STATUS_PATH = pathlib.Path(
|
|
1453
|
+
DISTILL_PRESET = "session-distill"
|
|
1454
|
+
CORPUS_STATUS_PATH = pathlib.Path(
|
|
1445
1455
|
os.environ.get(
|
|
1446
|
-
"
|
|
1447
|
-
str(pathlib.Path.home() / ".local/share/agent-bios/
|
|
1456
|
+
"AGENT_BIOS_CORPUS_STATUS",
|
|
1457
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/corpus-status.json"),
|
|
1448
1458
|
)
|
|
1449
1459
|
)
|
|
1450
1460
|
|
|
1451
1461
|
|
|
1452
|
-
def
|
|
1462
|
+
def load_corpus_status() -> dict[str, Any] | None:
|
|
1453
1463
|
try:
|
|
1454
|
-
return json.loads(
|
|
1464
|
+
return json.loads(CORPUS_STATUS_PATH.read_text())
|
|
1455
1465
|
except (OSError, ValueError):
|
|
1456
1466
|
return None
|
|
1457
1467
|
|
|
1458
1468
|
|
|
1459
|
-
def
|
|
1460
|
-
"""Panel body for the Session
|
|
1469
|
+
def corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
|
|
1470
|
+
"""Panel body for the Session Distill area: which corpus content is
|
|
1461
1471
|
live, through which mechanisms, and whether the corpus is rolled back."""
|
|
1462
1472
|
if status is None:
|
|
1463
|
-
return ["
|
|
1473
|
+
return ["corpus status not projected yet — run: agent-bios install"]
|
|
1464
1474
|
current = status.get("current_version", "?")
|
|
1465
1475
|
latest = status.get("latest_version", "?")
|
|
1466
1476
|
head = f"Applied version {current}"
|
|
@@ -1481,26 +1491,26 @@ def learning_summary_lines(status: dict[str, Any] | None) -> list[str]:
|
|
|
1481
1491
|
]
|
|
1482
1492
|
|
|
1483
1493
|
|
|
1484
|
-
def
|
|
1494
|
+
def _corpus_info(ui: TextualUI | None, title: str, lines: list[str]) -> None:
|
|
1485
1495
|
"""Info screen in both UIs: options carry the content; only exit is back."""
|
|
1486
|
-
options = [MenuOption("back", "Back", "Return to Session
|
|
1496
|
+
options = [MenuOption("back", "Back", "Return to Session Distill.")]
|
|
1487
1497
|
try:
|
|
1488
|
-
choose(title, options, "back", ui, allow_back=True,
|
|
1498
|
+
choose(title, options, "back", ui, allow_back=True, corpus_lines=lines)
|
|
1489
1499
|
except BackRequested:
|
|
1490
1500
|
pass
|
|
1491
1501
|
|
|
1492
1502
|
|
|
1493
|
-
def
|
|
1503
|
+
def _corpus_rollback(status: dict[str, Any], version: str, ui: TextualUI | None) -> None:
|
|
1494
1504
|
confirm = choose(
|
|
1495
|
-
f"Roll back
|
|
1505
|
+
f"Roll back corpus to {version}?",
|
|
1496
1506
|
[
|
|
1497
1507
|
MenuOption(
|
|
1498
|
-
"cancel", "Cancel", "Keep the currently deployed
|
|
1508
|
+
"cancel", "Cancel", "Keep the currently deployed corpus content."
|
|
1499
1509
|
),
|
|
1500
1510
|
MenuOption(
|
|
1501
1511
|
"rollback",
|
|
1502
1512
|
f"Roll back to {version}",
|
|
1503
|
-
"Re-deploys globals/guides/hooks as of that
|
|
1513
|
+
"Re-deploys globals/guides/hooks as of that corpus version. "
|
|
1504
1514
|
"System deployment (launcher, wrappers) stays current. Roll "
|
|
1505
1515
|
"forward again by selecting the latest version.",
|
|
1506
1516
|
),
|
|
@@ -1511,7 +1521,7 @@ def _learning_rollback(status: dict[str, Any], version: str, ui: TextualUI | Non
|
|
|
1511
1521
|
)
|
|
1512
1522
|
if confirm != "rollback":
|
|
1513
1523
|
return
|
|
1514
|
-
script = pathlib.Path(status["repo"]) / "scripts/session-
|
|
1524
|
+
script = pathlib.Path(status["repo"]) / "scripts/session-distill/corpus-state.py"
|
|
1515
1525
|
result = subprocess.run(
|
|
1516
1526
|
[sys.executable, str(script), "rollback", "--version", version],
|
|
1517
1527
|
capture_output=True,
|
|
@@ -1520,14 +1530,14 @@ def _learning_rollback(status: dict[str, Any], version: str, ui: TextualUI | Non
|
|
|
1520
1530
|
output = (result.stdout + result.stderr).strip().splitlines()
|
|
1521
1531
|
tail = output[-1] if output else ""
|
|
1522
1532
|
verdict = "Rollback complete." if result.returncode == 0 else "Rollback FAILED."
|
|
1523
|
-
|
|
1533
|
+
_corpus_info(ui, verdict, [tail] if tail else [])
|
|
1524
1534
|
|
|
1525
1535
|
|
|
1526
|
-
def
|
|
1536
|
+
def _corpus_versions(ui: TextualUI | None) -> None:
|
|
1527
1537
|
while True:
|
|
1528
|
-
status =
|
|
1538
|
+
status = load_corpus_status()
|
|
1529
1539
|
if status is None:
|
|
1530
|
-
|
|
1540
|
+
_corpus_info(ui, "Versions & rollback", corpus_summary_lines(None))
|
|
1531
1541
|
return
|
|
1532
1542
|
current = status.get("current_version")
|
|
1533
1543
|
options = []
|
|
@@ -1542,7 +1552,7 @@ def _learning_versions(ui: TextualUI | None) -> None:
|
|
|
1542
1552
|
f"{v.get('summary', '')}",
|
|
1543
1553
|
)
|
|
1544
1554
|
)
|
|
1545
|
-
options.append(MenuOption("back", "Back", "Return to Session
|
|
1555
|
+
options.append(MenuOption("back", "Back", "Return to Session Distill."))
|
|
1546
1556
|
try:
|
|
1547
1557
|
selected = choose(
|
|
1548
1558
|
"Versions & rollback",
|
|
@@ -1550,91 +1560,91 @@ def _learning_versions(ui: TextualUI | None) -> None:
|
|
|
1550
1560
|
options[0].value,
|
|
1551
1561
|
ui,
|
|
1552
1562
|
allow_back=True,
|
|
1553
|
-
|
|
1563
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1554
1564
|
)
|
|
1555
1565
|
except BackRequested:
|
|
1556
1566
|
return
|
|
1557
1567
|
if selected == "back":
|
|
1558
1568
|
return
|
|
1559
1569
|
if selected == current:
|
|
1560
|
-
|
|
1570
|
+
_corpus_info(
|
|
1561
1571
|
ui,
|
|
1562
1572
|
f"{selected} is the currently applied version",
|
|
1563
1573
|
["Select a different version to roll back or forward."],
|
|
1564
1574
|
)
|
|
1565
1575
|
continue
|
|
1566
|
-
|
|
1576
|
+
_corpus_rollback(status, selected, ui)
|
|
1567
1577
|
|
|
1568
1578
|
|
|
1569
|
-
def
|
|
1579
|
+
def _corpus_packages(ui: TextualUI | None) -> None:
|
|
1570
1580
|
"""v1: the corpus ships as a single core package; the list shape is ready
|
|
1571
1581
|
for the domain-packaging backlog to populate with real packages."""
|
|
1572
|
-
status =
|
|
1582
|
+
status = load_corpus_status()
|
|
1573
1583
|
if status is None:
|
|
1574
|
-
|
|
1584
|
+
_corpus_info(ui, "Corpus packages", corpus_summary_lines(None))
|
|
1575
1585
|
return
|
|
1576
1586
|
layers = status.get("summary", {}).get("placed_by_layer", {})
|
|
1577
1587
|
options = [
|
|
1578
1588
|
MenuOption(
|
|
1579
1589
|
"core",
|
|
1580
1590
|
f"core corpus @ {status.get('current_version', '?')}",
|
|
1581
|
-
"All
|
|
1591
|
+
"All corpus domains in one package until domain packaging "
|
|
1582
1592
|
f"lands (backlog). Layers: {json.dumps(layers, separators=(', ', ' '))}. "
|
|
1583
1593
|
"Per-domain selection, versions, and rollback will appear here.",
|
|
1584
1594
|
),
|
|
1585
|
-
MenuOption("back", "Back", "Return to Session
|
|
1595
|
+
MenuOption("back", "Back", "Return to Session Distill."),
|
|
1586
1596
|
]
|
|
1587
1597
|
try:
|
|
1588
1598
|
choose(
|
|
1589
|
-
"
|
|
1599
|
+
"Corpus packages",
|
|
1590
1600
|
options,
|
|
1591
1601
|
"back",
|
|
1592
1602
|
ui,
|
|
1593
1603
|
allow_back=True,
|
|
1594
|
-
|
|
1604
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1595
1605
|
)
|
|
1596
1606
|
except BackRequested:
|
|
1597
1607
|
pass
|
|
1598
1608
|
|
|
1599
1609
|
|
|
1600
|
-
def
|
|
1601
|
-
"""Session
|
|
1610
|
+
def distill_hub(config: dict[str, Any], ui: TextualUI | None) -> str:
|
|
1611
|
+
"""Session Distill area: status, packages, versions/rollback, session start.
|
|
1602
1612
|
|
|
1603
|
-
Returns "start" to launch the
|
|
1613
|
+
Returns "start" to launch the session-distill preset, "back" otherwise.
|
|
1604
1614
|
"""
|
|
1605
1615
|
while True:
|
|
1606
|
-
status =
|
|
1616
|
+
status = load_corpus_status()
|
|
1607
1617
|
options = [
|
|
1608
1618
|
MenuOption(
|
|
1609
1619
|
"start",
|
|
1610
|
-
"Start a
|
|
1611
|
-
"Launch with the Session
|
|
1620
|
+
"Start a session distill run",
|
|
1621
|
+
"Launch with the Session distill preset: the mission line "
|
|
1612
1622
|
"directs the session to the workflow guide and the SSOT.",
|
|
1613
|
-
enabled=
|
|
1614
|
-
unavailable_reason=f"preset {
|
|
1623
|
+
enabled=DISTILL_PRESET in config["presets"],
|
|
1624
|
+
unavailable_reason=f"preset {DISTILL_PRESET!r} not configured",
|
|
1615
1625
|
),
|
|
1616
1626
|
MenuOption(
|
|
1617
1627
|
"packages",
|
|
1618
|
-
"
|
|
1619
|
-
"What
|
|
1628
|
+
"Corpus packages",
|
|
1629
|
+
"What corpus content is applied, as installable packages "
|
|
1620
1630
|
"(v1: single core corpus).",
|
|
1621
1631
|
),
|
|
1622
1632
|
MenuOption(
|
|
1623
1633
|
"versions",
|
|
1624
1634
|
"Versions & rollback",
|
|
1625
|
-
"
|
|
1635
|
+
"Corpus content versions (distinct from system deployment); "
|
|
1626
1636
|
"roll the corpus back or forward.",
|
|
1627
1637
|
),
|
|
1628
1638
|
MenuOption("back", "Back", "Return to the launch menu."),
|
|
1629
1639
|
]
|
|
1630
1640
|
try:
|
|
1631
1641
|
selected = choose(
|
|
1632
|
-
"Session
|
|
1642
|
+
"Session Distill",
|
|
1633
1643
|
options,
|
|
1634
1644
|
"start",
|
|
1635
1645
|
ui,
|
|
1636
1646
|
allow_back=True,
|
|
1637
|
-
|
|
1647
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1638
1648
|
)
|
|
1639
1649
|
except BackRequested:
|
|
1640
1650
|
return "back"
|
|
@@ -1643,34 +1653,106 @@ def learning_hub(config: dict[str, Any], ui: TextualUI | None) -> str:
|
|
|
1643
1653
|
if selected == "start":
|
|
1644
1654
|
return "start"
|
|
1645
1655
|
if selected == "packages":
|
|
1646
|
-
|
|
1656
|
+
_corpus_packages(ui)
|
|
1647
1657
|
elif selected == "versions":
|
|
1648
|
-
|
|
1658
|
+
_corpus_versions(ui)
|
|
1649
1659
|
|
|
1650
1660
|
|
|
1651
|
-
def
|
|
1661
|
+
def preset_mode(data: dict[str, Any]) -> str:
|
|
1662
|
+
"""A preset's root-menu group. Missing/unknown falls back to builder — the
|
|
1663
|
+
same default build_plan applies — so listing presets for a menu never
|
|
1664
|
+
raises on a not-yet-migrated user preset."""
|
|
1665
|
+
mode = data.get("mode")
|
|
1666
|
+
return mode if isinstance(mode, str) and mode in PRESET_MODES else DEFAULT_PRESET_MODE
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def mode_default_preset(presets: dict[str, Any], mode: str) -> str | None:
|
|
1670
|
+
"""First configured preset in a mode; builder prefers 'balanced' so the
|
|
1671
|
+
long-standing everyday default stays the highlighted choice. None means
|
|
1672
|
+
the mode has no presets configured (only reachable via a stripped-down
|
|
1673
|
+
user config; Custom still covers builder in that case)."""
|
|
1674
|
+
names = [name for name, data in presets.items() if preset_mode(data) == mode]
|
|
1675
|
+
if mode == DEFAULT_PRESET_MODE and "balanced" in names:
|
|
1676
|
+
return "balanced"
|
|
1677
|
+
return names[0] if names else None
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def pick_mode_and_preset(
|
|
1652
1681
|
config: dict[str, Any],
|
|
1653
1682
|
host: str,
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
)
|
|
1683
|
+
ui: TextualUI | None,
|
|
1684
|
+
resume_mode: str | None,
|
|
1685
|
+
) -> tuple[str, bool, str]:
|
|
1686
|
+
"""Root menu: a 3-way mode picker (General user / Builder / Session
|
|
1687
|
+
distill), then that mode's preset submenu (Builder also lists Custom).
|
|
1688
|
+
|
|
1689
|
+
Esc in the submenu returns to the mode picker; Esc at the mode picker
|
|
1690
|
+
cancels the launcher, matching the picker's prior root Esc/q semantics.
|
|
1691
|
+
Returns (preset_name, custom_requested, mode) — the mode is handed back so
|
|
1692
|
+
a later Esc out of the Custom hub resumes this same submenu instead of
|
|
1693
|
+
dropping all the way back to the top mode picker.
|
|
1694
|
+
"""
|
|
1659
1695
|
presets = config["presets"]
|
|
1660
|
-
|
|
1661
|
-
show_picker = explicit_preset is None
|
|
1696
|
+
mode = resume_mode
|
|
1662
1697
|
while True:
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
if show_picker:
|
|
1666
|
-
options = [
|
|
1698
|
+
if mode is None:
|
|
1699
|
+
mode_options = [
|
|
1667
1700
|
MenuOption(
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1701
|
+
GENERAL_MODE,
|
|
1702
|
+
"General user",
|
|
1703
|
+
"Plain presets with nothing launch-specific applied, like "
|
|
1704
|
+
"Vanilla: no tier bindings or launch contract.",
|
|
1705
|
+
),
|
|
1706
|
+
MenuOption(
|
|
1707
|
+
DEFAULT_PRESET_MODE,
|
|
1708
|
+
"Builder",
|
|
1709
|
+
"Tune tiers, review routes, and permissions across fixed "
|
|
1710
|
+
"presets; includes Custom.",
|
|
1711
|
+
),
|
|
1712
|
+
MenuOption(
|
|
1713
|
+
DISTILL_MODE,
|
|
1714
|
+
"Session distill",
|
|
1715
|
+
"Enter the dedicated session-distill hub: status, "
|
|
1716
|
+
"packages, versions, and session start.",
|
|
1717
|
+
),
|
|
1673
1718
|
]
|
|
1719
|
+
|
|
1720
|
+
def preview_mode(value: str) -> dict[str, Any] | None:
|
|
1721
|
+
target = (
|
|
1722
|
+
DISTILL_PRESET
|
|
1723
|
+
if value == DISTILL_MODE
|
|
1724
|
+
else mode_default_preset(presets, value)
|
|
1725
|
+
)
|
|
1726
|
+
return build_plan(config, host, target) if target else None
|
|
1727
|
+
|
|
1728
|
+
initial = preview_mode(DEFAULT_PRESET_MODE)
|
|
1729
|
+
if ui is not None and initial is not None:
|
|
1730
|
+
ui.set_plan(initial)
|
|
1731
|
+
mode = choose(
|
|
1732
|
+
"Mode",
|
|
1733
|
+
mode_options,
|
|
1734
|
+
DEFAULT_PRESET_MODE,
|
|
1735
|
+
ui,
|
|
1736
|
+
preview=preview_mode,
|
|
1737
|
+
corpus_lines=corpus_summary_lines(load_corpus_status()),
|
|
1738
|
+
)
|
|
1739
|
+
if mode == DISTILL_MODE:
|
|
1740
|
+
if distill_hub(config, ui) != "start":
|
|
1741
|
+
mode = None
|
|
1742
|
+
continue
|
|
1743
|
+
if DISTILL_PRESET not in presets:
|
|
1744
|
+
raise LaunchError(f"preset {DISTILL_PRESET!r} missing from config")
|
|
1745
|
+
return DISTILL_PRESET, False, mode
|
|
1746
|
+
options = [
|
|
1747
|
+
MenuOption(
|
|
1748
|
+
name,
|
|
1749
|
+
data["label"],
|
|
1750
|
+
data.get("description", f"Launch the {data['label']} preset."),
|
|
1751
|
+
)
|
|
1752
|
+
for name, data in presets.items()
|
|
1753
|
+
if preset_mode(data) == mode
|
|
1754
|
+
]
|
|
1755
|
+
if mode == DEFAULT_PRESET_MODE:
|
|
1674
1756
|
options.append(
|
|
1675
1757
|
MenuOption(
|
|
1676
1758
|
CUSTOM_PRESET,
|
|
@@ -1678,46 +1760,45 @@ def select_plan(
|
|
|
1678
1760
|
"Open a settings hub for tiers, review setup, policy, and final confirmation.",
|
|
1679
1761
|
)
|
|
1680
1762
|
)
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
"Enter Session Learning Mode",
|
|
1685
|
-
"Learning-refinement mode: applied versions and mechanisms, "
|
|
1686
|
-
"packages, rollback — and start a dedicated learning session "
|
|
1687
|
-
"(preselected on entry).",
|
|
1688
|
-
)
|
|
1689
|
-
)
|
|
1690
|
-
default = "balanced" if "balanced" in presets else next(iter(presets))
|
|
1691
|
-
if ui is not None:
|
|
1692
|
-
ui.set_plan(build_plan(config, host, default))
|
|
1763
|
+
default = mode_default_preset(presets, mode) or CUSTOM_PRESET
|
|
1764
|
+
if ui is not None and default != CUSTOM_PRESET:
|
|
1765
|
+
ui.set_plan(build_plan(config, host, default))
|
|
1693
1766
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
if value == LEARNING_HUB:
|
|
1698
|
-
return LEARNING_PRESET if LEARNING_PRESET in presets else default
|
|
1699
|
-
return value
|
|
1767
|
+
def preview_preset(value: str) -> dict[str, Any]:
|
|
1768
|
+
target = default if value == CUSTOM_PRESET else value
|
|
1769
|
+
return build_plan(config, host, target)
|
|
1700
1770
|
|
|
1771
|
+
try:
|
|
1701
1772
|
selected = choose(
|
|
1702
|
-
"Preset",
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1773
|
+
"Preset", options, default, ui, allow_back=True, preview=preview_preset
|
|
1774
|
+
)
|
|
1775
|
+
except BackRequested:
|
|
1776
|
+
mode = None
|
|
1777
|
+
continue
|
|
1778
|
+
if selected == CUSTOM_PRESET:
|
|
1779
|
+
return default, True, mode
|
|
1780
|
+
return selected, False, mode
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def select_plan(
|
|
1784
|
+
config: dict[str, Any],
|
|
1785
|
+
host: str,
|
|
1786
|
+
preset_name: str | None,
|
|
1787
|
+
custom_requested: bool,
|
|
1788
|
+
ui: TextualUI | None = None,
|
|
1789
|
+
config_path: pathlib.Path | None = None,
|
|
1790
|
+
) -> dict[str, Any]:
|
|
1791
|
+
presets = config["presets"]
|
|
1792
|
+
explicit_preset = preset_name
|
|
1793
|
+
show_picker = explicit_preset is None
|
|
1794
|
+
resume_mode: str | None = None
|
|
1795
|
+
while True:
|
|
1796
|
+
selected_name = explicit_preset
|
|
1797
|
+
selected_custom = custom_requested
|
|
1798
|
+
if show_picker:
|
|
1799
|
+
selected_name, selected_custom, resume_mode = pick_mode_and_preset(
|
|
1800
|
+
config, host, ui, resume_mode
|
|
1708
1801
|
)
|
|
1709
|
-
if selected == CUSTOM_PRESET:
|
|
1710
|
-
selected_name = default
|
|
1711
|
-
selected_custom = True
|
|
1712
|
-
elif selected == LEARNING_HUB:
|
|
1713
|
-
if learning_hub(config, ui) != "start":
|
|
1714
|
-
continue
|
|
1715
|
-
if LEARNING_PRESET not in presets:
|
|
1716
|
-
raise LaunchError(f"preset {LEARNING_PRESET!r} missing from config")
|
|
1717
|
-
selected_name = LEARNING_PRESET
|
|
1718
|
-
selected_custom = False
|
|
1719
|
-
else:
|
|
1720
|
-
selected_name = selected
|
|
1721
1802
|
if selected_name not in presets:
|
|
1722
1803
|
raise LaunchError(f"unknown preset: {selected_name}")
|
|
1723
1804
|
plan = build_plan(config, host, selected_name)
|
|
@@ -1949,6 +2030,12 @@ def claude_agents(plan: dict[str, Any]) -> str:
|
|
|
1949
2030
|
|
|
1950
2031
|
def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[str]:
|
|
1951
2032
|
host = plan["host"]
|
|
2033
|
+
if plan.get("mode") == GENERAL_MODE:
|
|
2034
|
+
# General mode is defined as the plain backend with nothing applied: no
|
|
2035
|
+
# launch contract, no tier pinning, no agents, no permission/sandbox
|
|
2036
|
+
# flag. Short-circuit here so that invariant holds structurally instead
|
|
2037
|
+
# of depending on every general-mode preset being configured just so.
|
|
2038
|
+
return []
|
|
1952
2039
|
main = plan["tiers"][plan["main_tier"]]
|
|
1953
2040
|
main_effort = tier_effort(plan, plan["main_tier"])
|
|
1954
2041
|
contract = run_contract(plan)
|
|
@@ -1961,11 +2048,12 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
1961
2048
|
"-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
|
|
1962
2049
|
]
|
|
1963
2050
|
policy = plan["codex_execution_policy"]
|
|
1964
|
-
|
|
1965
|
-
["--dangerously-bypass-approvals-and-sandbox"]
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2051
|
+
if policy == "bypass":
|
|
2052
|
+
policy_args = ["--dangerously-bypass-approvals-and-sandbox"]
|
|
2053
|
+
elif policy == STANDARD_POLICY:
|
|
2054
|
+
policy_args = []
|
|
2055
|
+
else:
|
|
2056
|
+
policy_args = ["--sandbox", policy]
|
|
1969
2057
|
args += policy_args
|
|
1970
2058
|
if plan["delegation"]:
|
|
1971
2059
|
for tier, (path, description) in codex_agent_configs(plan, materialize_agents).items():
|
|
@@ -1989,11 +2077,12 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
1989
2077
|
if plan["delegation"]:
|
|
1990
2078
|
args += ["--agents", claude_agents(plan)]
|
|
1991
2079
|
policy = plan["claude_permission_mode"]
|
|
1992
|
-
|
|
1993
|
-
["--dangerously-skip-permissions"]
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2080
|
+
if policy == "bypassPermissions":
|
|
2081
|
+
policy_args = ["--dangerously-skip-permissions"]
|
|
2082
|
+
elif policy == STANDARD_POLICY:
|
|
2083
|
+
policy_args = []
|
|
2084
|
+
else:
|
|
2085
|
+
policy_args = ["--permission-mode", policy]
|
|
1997
2086
|
args += policy_args
|
|
1998
2087
|
if "onto" in effective_routes:
|
|
1999
2088
|
onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
|
|
@@ -2072,10 +2161,10 @@ def print_summary(
|
|
|
2072
2161
|
print(" Argv " + json.dumps([command, *args]), file=stream)
|
|
2073
2162
|
|
|
2074
2163
|
|
|
2075
|
-
|
|
2164
|
+
SESSION_DISTILL_STATE = pathlib.Path(
|
|
2076
2165
|
os.environ.get(
|
|
2077
|
-
"
|
|
2078
|
-
str(pathlib.Path.home() / ".local/share/agent-bios/session-
|
|
2166
|
+
"AGENT_BIOS_SESSION_DISTILL_STATE",
|
|
2167
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/session-distill-state.json"),
|
|
2079
2168
|
)
|
|
2080
2169
|
)
|
|
2081
2170
|
|
|
@@ -2088,19 +2177,19 @@ def _line_count(path: pathlib.Path) -> int:
|
|
|
2088
2177
|
return 0
|
|
2089
2178
|
|
|
2090
2179
|
|
|
2091
|
-
def
|
|
2180
|
+
def session_distill_nudge(config: dict[str, Any]) -> str | None:
|
|
2092
2181
|
"""Nudge when enough sessions accumulated since the last mining window.
|
|
2093
2182
|
|
|
2094
|
-
The baseline is written by scripts/session-
|
|
2183
|
+
The baseline is written by scripts/session-distill/update-state.py at
|
|
2095
2184
|
window close; provider history line counts are a cheap proxy for new
|
|
2096
2185
|
sessions. No state file means no nudge.
|
|
2097
2186
|
"""
|
|
2098
2187
|
try:
|
|
2099
|
-
state = json.loads(
|
|
2188
|
+
state = json.loads(SESSION_DISTILL_STATE.read_text())
|
|
2100
2189
|
baseline = int(state["history_lines_total"])
|
|
2101
2190
|
except (OSError, ValueError, KeyError, TypeError):
|
|
2102
2191
|
return None
|
|
2103
|
-
settings = config.get("
|
|
2192
|
+
settings = config.get("session_distill", {})
|
|
2104
2193
|
threshold = settings.get("nudge_after", 250) if isinstance(settings, dict) else 250
|
|
2105
2194
|
current = _line_count(pathlib.Path.home() / ".claude/history.jsonl") + _line_count(
|
|
2106
2195
|
pathlib.Path.home() / ".codex/history.jsonl"
|
|
@@ -2109,9 +2198,9 @@ def session_learning_nudge(config: dict[str, Any]) -> str | None:
|
|
|
2109
2198
|
if delta < threshold:
|
|
2110
2199
|
return None
|
|
2111
2200
|
return (
|
|
2112
|
-
f"session-
|
|
2201
|
+
f"session-distill due: ~{delta} new session entries since "
|
|
2113
2202
|
f"{state.get('window_end', '?')} (threshold {threshold}) — launch the "
|
|
2114
|
-
"Session
|
|
2203
|
+
"Session distill preset to run the next mining window"
|
|
2115
2204
|
)
|
|
2116
2205
|
|
|
2117
2206
|
|
|
@@ -2144,10 +2233,10 @@ def main(argv: list[str]) -> int:
|
|
|
2144
2233
|
config_path = args.config.expanduser()
|
|
2145
2234
|
config = load_config(config_path)
|
|
2146
2235
|
command, passthrough = resolve_backend(config, args.host)
|
|
2147
|
-
nudge =
|
|
2236
|
+
nudge = session_distill_nudge(config)
|
|
2148
2237
|
if nudge:
|
|
2149
2238
|
print(f"agent-launch: {nudge}", file=sys.stderr)
|
|
2150
|
-
nudged = config["presets"].get("session-
|
|
2239
|
+
nudged = config["presets"].get("session-distill")
|
|
2151
2240
|
if isinstance(nudged, dict):
|
|
2152
2241
|
nudged["description"] = f"{nudged.get('description', '')} ⚠ {nudge}".strip()
|
|
2153
2242
|
tty = sys.stdin.isatty() and sys.stdout.isatty()
|
|
@@ -2189,9 +2278,9 @@ def main(argv: list[str]) -> int:
|
|
|
2189
2278
|
print_summary(plan, command, projected, summary_stream, bool(args.forward))
|
|
2190
2279
|
trigger = plan.get("trigger")
|
|
2191
2280
|
if trigger:
|
|
2192
|
-
# Black-on-yellow to match the Session
|
|
2281
|
+
# Black-on-yellow to match the Session Distill identity; the session
|
|
2193
2282
|
# waits for this exact phrase before starting the workflow.
|
|
2194
|
-
line = f' Once the session is up, type "{trigger}" to begin the
|
|
2283
|
+
line = f' Once the session is up, type "{trigger}" to begin the session-distill workflow. '
|
|
2195
2284
|
print(f"\n\x1b[1;30;43m{line}\x1b[0m", file=summary_stream)
|
|
2196
2285
|
if args.dry_run:
|
|
2197
2286
|
print(json.dumps([command, *projected], ensure_ascii=False))
|