agent-bios 0.2.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 +14 -5
- package/claude/agents/frontier.md +9 -0
- package/claude/agents/sweep.md +9 -0
- package/claude/agents/workhorse.md +8 -0
- package/claude/guides/cli-multi-model-workflow.md +17 -4
- package/claude/guides/coding-staged-workflow.md +8 -0
- package/claude/guides/llm-capability-boundary-patterns.md +5 -0
- package/claude/guides/mock-realization-boundary.md +9 -0
- package/claude/guides/review-request.md +8 -0
- package/claude/guides/session-distill-workflow.md +100 -0
- package/claude/guides/tooling-gotchas.md +155 -0
- package/claude/hooks/tooling-gotchas-hook.py +79 -0
- package/codex/AGENTS.md +14 -5
- package/codex/config-additions.toml +22 -0
- package/codex/guides/cli-multi-model-workflow.md +17 -4
- package/codex/guides/coding-staged-workflow.md +8 -0
- package/codex/guides/llm-capability-boundary-patterns.md +5 -0
- package/codex/guides/mock-realization-boundary.md +9 -0
- package/codex/guides/review-request.md +8 -0
- package/codex/guides/session-distill-workflow.md +100 -0
- package/codex/guides/tooling-gotchas.md +155 -0
- package/config/agent-launch.toml +43 -0
- package/package.json +4 -1
- package/scripts/agent-launch.py +486 -52
- package/scripts/check-parity.sh +154 -7
- package/scripts/codex-helm.sh +19 -1
- package/scripts/codex-run.sh +19 -0
- package/scripts/install.sh +271 -10
package/scripts/agent-launch.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import pathlib
|
|
12
12
|
import shutil
|
|
13
|
+
import subprocess
|
|
13
14
|
import sys
|
|
14
15
|
import textwrap
|
|
15
16
|
import tomllib
|
|
@@ -18,6 +19,9 @@ from typing import Any, NoReturn
|
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
TIER_ORDER = ("frontier", "helm", "workhorse", "sweep")
|
|
22
|
+
# Tiers projected as spawnable subagents. HELM is the main role, never a
|
|
23
|
+
# spawnable worker — registering it invited helm-as-subagent misuse.
|
|
24
|
+
SPAWNABLE_TIERS = ("frontier", "workhorse", "sweep")
|
|
21
25
|
EFFORT_ORDER = ("low", "medium", "high", "xhigh", "max", "ultra")
|
|
22
26
|
HOST_EFFORTS = {
|
|
23
27
|
"codex": {"low", "medium", "high", "xhigh", "max", "ultra"},
|
|
@@ -40,6 +44,17 @@ EFFORT_DESCRIPTIONS = {
|
|
|
40
44
|
}
|
|
41
45
|
CUSTOM_PRESET = "__custom__"
|
|
42
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)
|
|
43
58
|
# User-saved presets live beside the deployed config, in a file the installer
|
|
44
59
|
# neither deploys nor verifies, so they survive `agent-bios install`.
|
|
45
60
|
USER_PRESETS_NAME = "presets.local.toml"
|
|
@@ -58,8 +73,9 @@ class MenuOption:
|
|
|
58
73
|
description: str
|
|
59
74
|
enabled: bool = True
|
|
60
75
|
unavailable_reason: str = ""
|
|
61
|
-
|
|
62
|
-
|
|
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}
|
|
63
79
|
REVIEW_SETUPS = {
|
|
64
80
|
"none": {
|
|
65
81
|
"label": "None",
|
|
@@ -569,7 +585,7 @@ def _build_app_class():
|
|
|
569
585
|
|
|
570
586
|
app_css = """
|
|
571
587
|
Screen { background: $surface; }
|
|
572
|
-
#al-title {
|
|
588
|
+
#al-title { background: $primary; color: black; text-style: bold; padding: 0 1; }
|
|
573
589
|
#al-setup {
|
|
574
590
|
border: round $primary; border-title-color: $primary;
|
|
575
591
|
border-title-style: bold; padding: 0 1; height: auto;
|
|
@@ -578,6 +594,10 @@ def _build_app_class():
|
|
|
578
594
|
border: round $secondary; border-title-color: $secondary;
|
|
579
595
|
border-title-style: bold; padding: 0 1; height: 5;
|
|
580
596
|
}
|
|
597
|
+
#al-corpus-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
|
|
598
|
+
#al-corpus {
|
|
599
|
+
border: round $warning; padding: 0 1; height: auto;
|
|
600
|
+
}
|
|
581
601
|
#al-hdr { color: $text-muted; text-style: bold; padding: 0 1; }
|
|
582
602
|
OptionList { height: 1fr; border: none; padding: 0 1; }
|
|
583
603
|
#al-footer { color: $text-muted; dock: bottom; padding: 0 1; background: $panel; }
|
|
@@ -596,7 +616,9 @@ def _build_app_class():
|
|
|
596
616
|
Binding("ctrl+c", "cancel", "cancel", priority=True),
|
|
597
617
|
]
|
|
598
618
|
|
|
599
|
-
def __init__(
|
|
619
|
+
def __init__(
|
|
620
|
+
self, title, options, default, allow_back, plan, preview=None, corpus=None
|
|
621
|
+
):
|
|
600
622
|
super().__init__()
|
|
601
623
|
self._title = title
|
|
602
624
|
self._options = options
|
|
@@ -604,10 +626,14 @@ def _build_app_class():
|
|
|
604
626
|
self._allow_back = allow_back
|
|
605
627
|
self._plan = plan
|
|
606
628
|
self._preview = preview
|
|
629
|
+
self._corpus = corpus
|
|
607
630
|
|
|
608
631
|
def compose(self):
|
|
609
632
|
yield Static(self._title, id="al-title")
|
|
610
633
|
yield setup_panel(self._plan)
|
|
634
|
+
if self._corpus:
|
|
635
|
+
yield Static("Corpus status", id="al-corpus-title")
|
|
636
|
+
yield Static("\n".join(self._corpus), id="al-corpus")
|
|
611
637
|
detail = Static("", id="al-detail")
|
|
612
638
|
detail.border_title = "About highlighted option"
|
|
613
639
|
yield detail
|
|
@@ -775,10 +801,13 @@ class TextualUI:
|
|
|
775
801
|
default: str,
|
|
776
802
|
allow_back: bool,
|
|
777
803
|
preview=None,
|
|
804
|
+
corpus_lines: list[str] | None = None,
|
|
778
805
|
) -> str:
|
|
779
806
|
result = self.app.call_from_thread(
|
|
780
807
|
self.app.push_screen_wait,
|
|
781
|
-
self._menu_screen(
|
|
808
|
+
self._menu_screen(
|
|
809
|
+
title, options, default, allow_back, self.plan, preview, corpus_lines
|
|
810
|
+
),
|
|
782
811
|
)
|
|
783
812
|
if result == _UI_BACK:
|
|
784
813
|
raise BackRequested
|
|
@@ -814,13 +843,23 @@ def run_textual_flow(
|
|
|
814
843
|
|
|
815
844
|
|
|
816
845
|
def choose_lines(
|
|
817
|
-
title: str,
|
|
846
|
+
title: str,
|
|
847
|
+
options: list[MenuOption],
|
|
848
|
+
default: str,
|
|
849
|
+
allow_back: bool,
|
|
850
|
+
corpus_lines: list[str] | None = None,
|
|
818
851
|
) -> str:
|
|
819
852
|
print(f"\n{title}")
|
|
853
|
+
if corpus_lines:
|
|
854
|
+
print(" -- Corpus status --")
|
|
855
|
+
for line in corpus_lines:
|
|
856
|
+
print(f" {line}")
|
|
857
|
+
print(" --")
|
|
820
858
|
for index, option in enumerate(options, 1):
|
|
821
859
|
marker = "" if option.enabled else " [unavailable]"
|
|
822
860
|
selected = " *" if option.value == default and option.enabled else ""
|
|
823
|
-
|
|
861
|
+
label = option.label
|
|
862
|
+
print(f" {index}. {label}{marker}{selected} - {option.description}")
|
|
824
863
|
if not option.enabled:
|
|
825
864
|
print(f" Unavailable: {option.unavailable_reason}")
|
|
826
865
|
while True:
|
|
@@ -854,12 +893,13 @@ def choose(
|
|
|
854
893
|
ui: TextualUI | None = None,
|
|
855
894
|
allow_back: bool = False,
|
|
856
895
|
preview=None,
|
|
896
|
+
corpus_lines: list[str] | None = None,
|
|
857
897
|
) -> str:
|
|
858
898
|
if not any(option.enabled for option in options):
|
|
859
899
|
raise LaunchError(f"no available options for {title}")
|
|
860
900
|
if ui is not None:
|
|
861
|
-
return ui.choose(title, options, default, allow_back, preview)
|
|
862
|
-
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)
|
|
863
903
|
|
|
864
904
|
|
|
865
905
|
def read_input(prompt: str) -> str:
|
|
@@ -950,6 +990,17 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
950
990
|
label = preset.get("label", preset_name)
|
|
951
991
|
if not isinstance(label, str) or not label:
|
|
952
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
|
+
mission = preset.get("mission")
|
|
999
|
+
if mission is not None and (not isinstance(mission, str) or not mission):
|
|
1000
|
+
raise LaunchError(f"presets.{preset_name}.mission must be a non-empty string")
|
|
1001
|
+
trigger = preset.get("trigger")
|
|
1002
|
+
if trigger is not None and (not isinstance(trigger, str) or not trigger):
|
|
1003
|
+
raise LaunchError(f"presets.{preset_name}.trigger must be a non-empty string")
|
|
953
1004
|
review_family = preset.get("review_family", "cross")
|
|
954
1005
|
review_host = REVIEW_HOST[host]
|
|
955
1006
|
opposite = config.get("hosts", {}).get(review_host)
|
|
@@ -973,6 +1024,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
973
1024
|
"preset": preset_name,
|
|
974
1025
|
"label": label,
|
|
975
1026
|
"description": preset.get("description", f"Launch the {label} preset."),
|
|
1027
|
+
"mode": mode,
|
|
976
1028
|
"main_tier": main_tier,
|
|
977
1029
|
"frontier_effort": frontier_effort,
|
|
978
1030
|
"review_setup": review_setup,
|
|
@@ -988,6 +1040,8 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
988
1040
|
"review_backend": review_backend,
|
|
989
1041
|
"agent_templates": copy.deepcopy(config["hosts"][host].get("agent_templates")),
|
|
990
1042
|
"capabilities": copy.deepcopy(config.get("capabilities", {})),
|
|
1043
|
+
"mission": mission,
|
|
1044
|
+
"trigger": trigger,
|
|
991
1045
|
}
|
|
992
1046
|
|
|
993
1047
|
|
|
@@ -1173,6 +1227,11 @@ def customize(
|
|
|
1173
1227
|
"Read-only sandbox",
|
|
1174
1228
|
"Allow inspection while blocking file writes through the Codex sandbox.",
|
|
1175
1229
|
),
|
|
1230
|
+
MenuOption(
|
|
1231
|
+
STANDARD_POLICY,
|
|
1232
|
+
"Standard",
|
|
1233
|
+
"Pass no policy flag; use Codex's own default approval and sandbox behavior.",
|
|
1234
|
+
),
|
|
1176
1235
|
]
|
|
1177
1236
|
policy_field = "codex_execution_policy"
|
|
1178
1237
|
policy_title = "Codex execution policy"
|
|
@@ -1208,6 +1267,11 @@ def customize(
|
|
|
1208
1267
|
"Plan mode",
|
|
1209
1268
|
"Start Claude in planning mode without direct implementation.",
|
|
1210
1269
|
),
|
|
1270
|
+
MenuOption(
|
|
1271
|
+
STANDARD_POLICY,
|
|
1272
|
+
"Standard",
|
|
1273
|
+
"Pass no policy flag; use Claude's own default permission prompts.",
|
|
1274
|
+
),
|
|
1211
1275
|
]
|
|
1212
1276
|
policy_field = "claude_permission_mode"
|
|
1213
1277
|
policy_title = "Claude permission mode (not an OS sandbox)"
|
|
@@ -1386,6 +1450,336 @@ def customize(
|
|
|
1386
1450
|
raise KeyboardInterrupt
|
|
1387
1451
|
|
|
1388
1452
|
|
|
1453
|
+
DISTILL_PRESET = "session-distill"
|
|
1454
|
+
CORPUS_STATUS_PATH = pathlib.Path(
|
|
1455
|
+
os.environ.get(
|
|
1456
|
+
"AGENT_BIOS_CORPUS_STATUS",
|
|
1457
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/corpus-status.json"),
|
|
1458
|
+
)
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def load_corpus_status() -> dict[str, Any] | None:
|
|
1463
|
+
try:
|
|
1464
|
+
return json.loads(CORPUS_STATUS_PATH.read_text())
|
|
1465
|
+
except (OSError, ValueError):
|
|
1466
|
+
return None
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
|
|
1470
|
+
"""Panel body for the Session Distill area: which corpus content is
|
|
1471
|
+
live, through which mechanisms, and whether the corpus is rolled back."""
|
|
1472
|
+
if status is None:
|
|
1473
|
+
return ["corpus status not projected yet — run: agent-bios install"]
|
|
1474
|
+
current = status.get("current_version", "?")
|
|
1475
|
+
latest = status.get("latest_version", "?")
|
|
1476
|
+
head = f"Applied version {current}"
|
|
1477
|
+
if status.get("rolled_back_to"):
|
|
1478
|
+
head += f" (ROLLED BACK; latest is {latest})"
|
|
1479
|
+
layers = status.get("summary", {}).get("placed_by_layer", {})
|
|
1480
|
+
order = ("global", "guide", "hook", "enforcement", "gate")
|
|
1481
|
+
layer_text = " · ".join(
|
|
1482
|
+
f"{name} {layers[name]}" for name in order if layers.get(name)
|
|
1483
|
+
) or "none"
|
|
1484
|
+
by_status = status.get("summary", {}).get("by_status", {})
|
|
1485
|
+
return [
|
|
1486
|
+
head,
|
|
1487
|
+
f"Mechanisms {layer_text}",
|
|
1488
|
+
f"Ledger placed {by_status.get('placed', 0)} · "
|
|
1489
|
+
f"incubating {by_status.get('incubating', 0) + by_status.get('incubating-G', 0)} · "
|
|
1490
|
+
f"versions {len(status.get('versions', []))}",
|
|
1491
|
+
]
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _corpus_info(ui: TextualUI | None, title: str, lines: list[str]) -> None:
|
|
1495
|
+
"""Info screen in both UIs: options carry the content; only exit is back."""
|
|
1496
|
+
options = [MenuOption("back", "Back", "Return to Session Distill.")]
|
|
1497
|
+
try:
|
|
1498
|
+
choose(title, options, "back", ui, allow_back=True, corpus_lines=lines)
|
|
1499
|
+
except BackRequested:
|
|
1500
|
+
pass
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _corpus_rollback(status: dict[str, Any], version: str, ui: TextualUI | None) -> None:
|
|
1504
|
+
confirm = choose(
|
|
1505
|
+
f"Roll back corpus to {version}?",
|
|
1506
|
+
[
|
|
1507
|
+
MenuOption(
|
|
1508
|
+
"cancel", "Cancel", "Keep the currently deployed corpus content."
|
|
1509
|
+
),
|
|
1510
|
+
MenuOption(
|
|
1511
|
+
"rollback",
|
|
1512
|
+
f"Roll back to {version}",
|
|
1513
|
+
"Re-deploys globals/guides/hooks as of that corpus version. "
|
|
1514
|
+
"System deployment (launcher, wrappers) stays current. Roll "
|
|
1515
|
+
"forward again by selecting the latest version.",
|
|
1516
|
+
),
|
|
1517
|
+
],
|
|
1518
|
+
"cancel",
|
|
1519
|
+
ui,
|
|
1520
|
+
allow_back=True,
|
|
1521
|
+
)
|
|
1522
|
+
if confirm != "rollback":
|
|
1523
|
+
return
|
|
1524
|
+
script = pathlib.Path(status["repo"]) / "scripts/session-distill/corpus-state.py"
|
|
1525
|
+
result = subprocess.run(
|
|
1526
|
+
[sys.executable, str(script), "rollback", "--version", version],
|
|
1527
|
+
capture_output=True,
|
|
1528
|
+
text=True,
|
|
1529
|
+
)
|
|
1530
|
+
output = (result.stdout + result.stderr).strip().splitlines()
|
|
1531
|
+
tail = output[-1] if output else ""
|
|
1532
|
+
verdict = "Rollback complete." if result.returncode == 0 else "Rollback FAILED."
|
|
1533
|
+
_corpus_info(ui, verdict, [tail] if tail else [])
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _corpus_versions(ui: TextualUI | None) -> None:
|
|
1537
|
+
while True:
|
|
1538
|
+
status = load_corpus_status()
|
|
1539
|
+
if status is None:
|
|
1540
|
+
_corpus_info(ui, "Versions & rollback", corpus_summary_lines(None))
|
|
1541
|
+
return
|
|
1542
|
+
current = status.get("current_version")
|
|
1543
|
+
options = []
|
|
1544
|
+
for v in reversed(status.get("versions", [])):
|
|
1545
|
+
name = v["version"]
|
|
1546
|
+
label = f"{name} (current)" if name == current else name
|
|
1547
|
+
options.append(
|
|
1548
|
+
MenuOption(
|
|
1549
|
+
name,
|
|
1550
|
+
label,
|
|
1551
|
+
f"closed {v.get('closed', '?')} · commit {v.get('commit', '')[:12]} · "
|
|
1552
|
+
f"{v.get('summary', '')}",
|
|
1553
|
+
)
|
|
1554
|
+
)
|
|
1555
|
+
options.append(MenuOption("back", "Back", "Return to Session Distill."))
|
|
1556
|
+
try:
|
|
1557
|
+
selected = choose(
|
|
1558
|
+
"Versions & rollback",
|
|
1559
|
+
options,
|
|
1560
|
+
options[0].value,
|
|
1561
|
+
ui,
|
|
1562
|
+
allow_back=True,
|
|
1563
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1564
|
+
)
|
|
1565
|
+
except BackRequested:
|
|
1566
|
+
return
|
|
1567
|
+
if selected == "back":
|
|
1568
|
+
return
|
|
1569
|
+
if selected == current:
|
|
1570
|
+
_corpus_info(
|
|
1571
|
+
ui,
|
|
1572
|
+
f"{selected} is the currently applied version",
|
|
1573
|
+
["Select a different version to roll back or forward."],
|
|
1574
|
+
)
|
|
1575
|
+
continue
|
|
1576
|
+
_corpus_rollback(status, selected, ui)
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _corpus_packages(ui: TextualUI | None) -> None:
|
|
1580
|
+
"""v1: the corpus ships as a single core package; the list shape is ready
|
|
1581
|
+
for the domain-packaging backlog to populate with real packages."""
|
|
1582
|
+
status = load_corpus_status()
|
|
1583
|
+
if status is None:
|
|
1584
|
+
_corpus_info(ui, "Corpus packages", corpus_summary_lines(None))
|
|
1585
|
+
return
|
|
1586
|
+
layers = status.get("summary", {}).get("placed_by_layer", {})
|
|
1587
|
+
options = [
|
|
1588
|
+
MenuOption(
|
|
1589
|
+
"core",
|
|
1590
|
+
f"core corpus @ {status.get('current_version', '?')}",
|
|
1591
|
+
"All corpus domains in one package until domain packaging "
|
|
1592
|
+
f"lands (backlog). Layers: {json.dumps(layers, separators=(', ', ' '))}. "
|
|
1593
|
+
"Per-domain selection, versions, and rollback will appear here.",
|
|
1594
|
+
),
|
|
1595
|
+
MenuOption("back", "Back", "Return to Session Distill."),
|
|
1596
|
+
]
|
|
1597
|
+
try:
|
|
1598
|
+
choose(
|
|
1599
|
+
"Corpus packages",
|
|
1600
|
+
options,
|
|
1601
|
+
"back",
|
|
1602
|
+
ui,
|
|
1603
|
+
allow_back=True,
|
|
1604
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1605
|
+
)
|
|
1606
|
+
except BackRequested:
|
|
1607
|
+
pass
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
def distill_hub(config: dict[str, Any], ui: TextualUI | None) -> str:
|
|
1611
|
+
"""Session Distill area: status, packages, versions/rollback, session start.
|
|
1612
|
+
|
|
1613
|
+
Returns "start" to launch the session-distill preset, "back" otherwise.
|
|
1614
|
+
"""
|
|
1615
|
+
while True:
|
|
1616
|
+
status = load_corpus_status()
|
|
1617
|
+
options = [
|
|
1618
|
+
MenuOption(
|
|
1619
|
+
"start",
|
|
1620
|
+
"Start a session distill run",
|
|
1621
|
+
"Launch with the Session distill preset: the mission line "
|
|
1622
|
+
"directs the session to the workflow guide and the SSOT.",
|
|
1623
|
+
enabled=DISTILL_PRESET in config["presets"],
|
|
1624
|
+
unavailable_reason=f"preset {DISTILL_PRESET!r} not configured",
|
|
1625
|
+
),
|
|
1626
|
+
MenuOption(
|
|
1627
|
+
"packages",
|
|
1628
|
+
"Corpus packages",
|
|
1629
|
+
"What corpus content is applied, as installable packages "
|
|
1630
|
+
"(v1: single core corpus).",
|
|
1631
|
+
),
|
|
1632
|
+
MenuOption(
|
|
1633
|
+
"versions",
|
|
1634
|
+
"Versions & rollback",
|
|
1635
|
+
"Corpus content versions (distinct from system deployment); "
|
|
1636
|
+
"roll the corpus back or forward.",
|
|
1637
|
+
),
|
|
1638
|
+
MenuOption("back", "Back", "Return to the launch menu."),
|
|
1639
|
+
]
|
|
1640
|
+
try:
|
|
1641
|
+
selected = choose(
|
|
1642
|
+
"Session Distill",
|
|
1643
|
+
options,
|
|
1644
|
+
"start",
|
|
1645
|
+
ui,
|
|
1646
|
+
allow_back=True,
|
|
1647
|
+
corpus_lines=corpus_summary_lines(status),
|
|
1648
|
+
)
|
|
1649
|
+
except BackRequested:
|
|
1650
|
+
return "back"
|
|
1651
|
+
if selected == "back":
|
|
1652
|
+
return "back"
|
|
1653
|
+
if selected == "start":
|
|
1654
|
+
return "start"
|
|
1655
|
+
if selected == "packages":
|
|
1656
|
+
_corpus_packages(ui)
|
|
1657
|
+
elif selected == "versions":
|
|
1658
|
+
_corpus_versions(ui)
|
|
1659
|
+
|
|
1660
|
+
|
|
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(
|
|
1681
|
+
config: dict[str, Any],
|
|
1682
|
+
host: str,
|
|
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
|
+
"""
|
|
1695
|
+
presets = config["presets"]
|
|
1696
|
+
mode = resume_mode
|
|
1697
|
+
while True:
|
|
1698
|
+
if mode is None:
|
|
1699
|
+
mode_options = [
|
|
1700
|
+
MenuOption(
|
|
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
|
+
),
|
|
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:
|
|
1756
|
+
options.append(
|
|
1757
|
+
MenuOption(
|
|
1758
|
+
CUSTOM_PRESET,
|
|
1759
|
+
"Custom",
|
|
1760
|
+
"Open a settings hub for tiers, review setup, policy, and final confirmation.",
|
|
1761
|
+
)
|
|
1762
|
+
)
|
|
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))
|
|
1766
|
+
|
|
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)
|
|
1770
|
+
|
|
1771
|
+
try:
|
|
1772
|
+
selected = choose(
|
|
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
|
+
|
|
1389
1783
|
def select_plan(
|
|
1390
1784
|
config: dict[str, Any],
|
|
1391
1785
|
host: str,
|
|
@@ -1397,42 +1791,14 @@ def select_plan(
|
|
|
1397
1791
|
presets = config["presets"]
|
|
1398
1792
|
explicit_preset = preset_name
|
|
1399
1793
|
show_picker = explicit_preset is None
|
|
1794
|
+
resume_mode: str | None = None
|
|
1400
1795
|
while True:
|
|
1401
1796
|
selected_name = explicit_preset
|
|
1402
1797
|
selected_custom = custom_requested
|
|
1403
1798
|
if show_picker:
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
name,
|
|
1407
|
-
data["label"],
|
|
1408
|
-
data.get("description", f"Launch the {data['label']} preset."),
|
|
1409
|
-
)
|
|
1410
|
-
for name, data in presets.items()
|
|
1411
|
-
]
|
|
1412
|
-
options.append(
|
|
1413
|
-
MenuOption(
|
|
1414
|
-
CUSTOM_PRESET,
|
|
1415
|
-
"Custom",
|
|
1416
|
-
"Open a settings hub for tiers, review setup, policy, and final confirmation.",
|
|
1417
|
-
)
|
|
1799
|
+
selected_name, selected_custom, resume_mode = pick_mode_and_preset(
|
|
1800
|
+
config, host, ui, resume_mode
|
|
1418
1801
|
)
|
|
1419
|
-
default = "balanced" if "balanced" in presets else next(iter(presets))
|
|
1420
|
-
if ui is not None:
|
|
1421
|
-
ui.set_plan(build_plan(config, host, default))
|
|
1422
|
-
selected = choose(
|
|
1423
|
-
"Preset",
|
|
1424
|
-
options,
|
|
1425
|
-
default,
|
|
1426
|
-
ui,
|
|
1427
|
-
preview=lambda value: build_plan(
|
|
1428
|
-
config, host, default if value == CUSTOM_PRESET else value
|
|
1429
|
-
),
|
|
1430
|
-
)
|
|
1431
|
-
if selected == CUSTOM_PRESET:
|
|
1432
|
-
selected_name = default
|
|
1433
|
-
selected_custom = True
|
|
1434
|
-
else:
|
|
1435
|
-
selected_name = selected
|
|
1436
1802
|
if selected_name not in presets:
|
|
1437
1803
|
raise LaunchError(f"unknown preset: {selected_name}")
|
|
1438
1804
|
plan = build_plan(config, host, selected_name)
|
|
@@ -1468,7 +1834,7 @@ def codex_agent_configs(
|
|
|
1468
1834
|
if not isinstance(templates, dict):
|
|
1469
1835
|
raise LaunchError("Codex delegation requires [hosts.codex.agent_templates]")
|
|
1470
1836
|
rendered: dict[str, tuple[str, str]] = {}
|
|
1471
|
-
for tier in
|
|
1837
|
+
for tier in SPAWNABLE_TIERS:
|
|
1472
1838
|
source_value = templates.get(tier)
|
|
1473
1839
|
if not isinstance(source_value, str) or not source_value:
|
|
1474
1840
|
raise LaunchError(f"Codex agent template missing: {tier}")
|
|
@@ -1630,7 +1996,12 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
1630
1996
|
if plan["host"] == "codex"
|
|
1631
1997
|
else "Main and child model/effort defaults are CLI-projected."
|
|
1632
1998
|
)
|
|
1999
|
+
mission = plan.get("mission")
|
|
2000
|
+
if mission and plan.get("trigger"):
|
|
2001
|
+
mission = mission.replace("{trigger}", plan["trigger"])
|
|
2002
|
+
mission_prefix = f"Mission: {mission} " if mission else ""
|
|
1633
2003
|
return (
|
|
2004
|
+
f"{mission_prefix}"
|
|
1634
2005
|
f"LaunchPlan: main={plan['main_tier']} ({plan['tiers'][plan['main_tier']]['model']}/"
|
|
1635
2006
|
f"{tier_effort(plan, plan['main_tier'])}); tiers: {bindings}. "
|
|
1636
2007
|
f"Delegation={'on' if plan['delegation'] else 'off'}. Review family={family}. "
|
|
@@ -1643,7 +2014,7 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
1643
2014
|
|
|
1644
2015
|
def claude_agents(plan: dict[str, Any]) -> str:
|
|
1645
2016
|
roles = {}
|
|
1646
|
-
for tier in
|
|
2017
|
+
for tier in SPAWNABLE_TIERS:
|
|
1647
2018
|
binding = plan["tiers"][tier]
|
|
1648
2019
|
roles[tier] = {
|
|
1649
2020
|
"description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
|
|
@@ -1659,6 +2030,12 @@ def claude_agents(plan: dict[str, Any]) -> str:
|
|
|
1659
2030
|
|
|
1660
2031
|
def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[str]:
|
|
1661
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 []
|
|
1662
2039
|
main = plan["tiers"][plan["main_tier"]]
|
|
1663
2040
|
main_effort = tier_effort(plan, plan["main_tier"])
|
|
1664
2041
|
contract = run_contract(plan)
|
|
@@ -1671,11 +2048,12 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
1671
2048
|
"-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
|
|
1672
2049
|
]
|
|
1673
2050
|
policy = plan["codex_execution_policy"]
|
|
1674
|
-
|
|
1675
|
-
["--dangerously-bypass-approvals-and-sandbox"]
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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]
|
|
1679
2057
|
args += policy_args
|
|
1680
2058
|
if plan["delegation"]:
|
|
1681
2059
|
for tier, (path, description) in codex_agent_configs(plan, materialize_agents).items():
|
|
@@ -1699,11 +2077,12 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
1699
2077
|
if plan["delegation"]:
|
|
1700
2078
|
args += ["--agents", claude_agents(plan)]
|
|
1701
2079
|
policy = plan["claude_permission_mode"]
|
|
1702
|
-
|
|
1703
|
-
["--dangerously-skip-permissions"]
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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]
|
|
1707
2086
|
args += policy_args
|
|
1708
2087
|
if "onto" in effective_routes:
|
|
1709
2088
|
onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
|
|
@@ -1782,6 +2161,49 @@ def print_summary(
|
|
|
1782
2161
|
print(" Argv " + json.dumps([command, *args]), file=stream)
|
|
1783
2162
|
|
|
1784
2163
|
|
|
2164
|
+
SESSION_DISTILL_STATE = pathlib.Path(
|
|
2165
|
+
os.environ.get(
|
|
2166
|
+
"AGENT_BIOS_SESSION_DISTILL_STATE",
|
|
2167
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/session-distill-state.json"),
|
|
2168
|
+
)
|
|
2169
|
+
)
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
def _line_count(path: pathlib.Path) -> int:
|
|
2173
|
+
try:
|
|
2174
|
+
with path.open("rb") as fh:
|
|
2175
|
+
return sum(chunk.count(b"\n") for chunk in iter(lambda: fh.read(1 << 20), b""))
|
|
2176
|
+
except OSError:
|
|
2177
|
+
return 0
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
def session_distill_nudge(config: dict[str, Any]) -> str | None:
|
|
2181
|
+
"""Nudge when enough sessions accumulated since the last mining window.
|
|
2182
|
+
|
|
2183
|
+
The baseline is written by scripts/session-distill/update-state.py at
|
|
2184
|
+
window close; provider history line counts are a cheap proxy for new
|
|
2185
|
+
sessions. No state file means no nudge.
|
|
2186
|
+
"""
|
|
2187
|
+
try:
|
|
2188
|
+
state = json.loads(SESSION_DISTILL_STATE.read_text())
|
|
2189
|
+
baseline = int(state["history_lines_total"])
|
|
2190
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
2191
|
+
return None
|
|
2192
|
+
settings = config.get("session_distill", {})
|
|
2193
|
+
threshold = settings.get("nudge_after", 250) if isinstance(settings, dict) else 250
|
|
2194
|
+
current = _line_count(pathlib.Path.home() / ".claude/history.jsonl") + _line_count(
|
|
2195
|
+
pathlib.Path.home() / ".codex/history.jsonl"
|
|
2196
|
+
)
|
|
2197
|
+
delta = current - baseline
|
|
2198
|
+
if delta < threshold:
|
|
2199
|
+
return None
|
|
2200
|
+
return (
|
|
2201
|
+
f"session-distill due: ~{delta} new session entries since "
|
|
2202
|
+
f"{state.get('window_end', '?')} (threshold {threshold}) — launch the "
|
|
2203
|
+
"Session distill preset to run the next mining window"
|
|
2204
|
+
)
|
|
2205
|
+
|
|
2206
|
+
|
|
1785
2207
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
1786
2208
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1787
2209
|
parser.add_argument("--config", type=pathlib.Path, default=default_config_path())
|
|
@@ -1811,6 +2233,12 @@ def main(argv: list[str]) -> int:
|
|
|
1811
2233
|
config_path = args.config.expanduser()
|
|
1812
2234
|
config = load_config(config_path)
|
|
1813
2235
|
command, passthrough = resolve_backend(config, args.host)
|
|
2236
|
+
nudge = session_distill_nudge(config)
|
|
2237
|
+
if nudge:
|
|
2238
|
+
print(f"agent-launch: {nudge}", file=sys.stderr)
|
|
2239
|
+
nudged = config["presets"].get("session-distill")
|
|
2240
|
+
if isinstance(nudged, dict):
|
|
2241
|
+
nudged["description"] = f"{nudged.get('description', '')} ⚠ {nudge}".strip()
|
|
1814
2242
|
tty = sys.stdin.isatty() and sys.stdout.isatty()
|
|
1815
2243
|
if not tty and args.dry_run and not args.preset and not args.custom:
|
|
1816
2244
|
if "balanced" not in config["presets"]:
|
|
@@ -1848,6 +2276,12 @@ def main(argv: list[str]) -> int:
|
|
|
1848
2276
|
projected = [*project_args(plan, materialize_agents=not args.dry_run), *args.forward]
|
|
1849
2277
|
summary_stream = sys.stdout if tty or args.dry_run else sys.stderr
|
|
1850
2278
|
print_summary(plan, command, projected, summary_stream, bool(args.forward))
|
|
2279
|
+
trigger = plan.get("trigger")
|
|
2280
|
+
if trigger:
|
|
2281
|
+
# Black-on-yellow to match the Session Distill identity; the session
|
|
2282
|
+
# waits for this exact phrase before starting the workflow.
|
|
2283
|
+
line = f' Once the session is up, type "{trigger}" to begin the session-distill workflow. '
|
|
2284
|
+
print(f"\n\x1b[1;30;43m{line}\x1b[0m", file=summary_stream)
|
|
1851
2285
|
if args.dry_run:
|
|
1852
2286
|
print(json.dumps([command, *projected], ensure_ascii=False))
|
|
1853
2287
|
return 0
|