agent-bios 0.2.0 → 0.3.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/claude/CLAUDE.md +12 -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 +11 -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-learning-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 +12 -5
- package/codex/config-additions.toml +22 -0
- package/codex/guides/cli-multi-model-workflow.md +11 -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-learning-workflow.md +100 -0
- package/codex/guides/tooling-gotchas.md +155 -0
- package/config/agent-launch.toml +17 -0
- package/package.json +4 -1
- package/scripts/agent-launch.py +357 -12
- package/scripts/check-parity.sh +65 -3
- package/scripts/codex-helm.sh +19 -1
- package/scripts/codex-run.sh +19 -0
- package/scripts/install.sh +162 -2
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"},
|
|
@@ -539,6 +543,7 @@ _UI_CANCEL = "\x00cancel"
|
|
|
539
543
|
def _build_app_class():
|
|
540
544
|
"""Import textual lazily and build the App/Screen classes, so importing this
|
|
541
545
|
module and every non-interactive path stays free of the textual dependency."""
|
|
546
|
+
from rich.text import Text
|
|
542
547
|
from textual import work
|
|
543
548
|
from textual.app import App
|
|
544
549
|
from textual.binding import Binding
|
|
@@ -569,7 +574,7 @@ def _build_app_class():
|
|
|
569
574
|
|
|
570
575
|
app_css = """
|
|
571
576
|
Screen { background: $surface; }
|
|
572
|
-
#al-title {
|
|
577
|
+
#al-title { background: $primary; color: black; text-style: bold; padding: 0 1; }
|
|
573
578
|
#al-setup {
|
|
574
579
|
border: round $primary; border-title-color: $primary;
|
|
575
580
|
border-title-style: bold; padding: 0 1; height: auto;
|
|
@@ -578,6 +583,10 @@ def _build_app_class():
|
|
|
578
583
|
border: round $secondary; border-title-color: $secondary;
|
|
579
584
|
border-title-style: bold; padding: 0 1; height: 5;
|
|
580
585
|
}
|
|
586
|
+
#al-learning-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
|
|
587
|
+
#al-learning {
|
|
588
|
+
border: round $warning; padding: 0 1; height: auto;
|
|
589
|
+
}
|
|
581
590
|
#al-hdr { color: $text-muted; text-style: bold; padding: 0 1; }
|
|
582
591
|
OptionList { height: 1fr; border: none; padding: 0 1; }
|
|
583
592
|
#al-footer { color: $text-muted; dock: bottom; padding: 0 1; background: $panel; }
|
|
@@ -596,7 +605,9 @@ def _build_app_class():
|
|
|
596
605
|
Binding("ctrl+c", "cancel", "cancel", priority=True),
|
|
597
606
|
]
|
|
598
607
|
|
|
599
|
-
def __init__(
|
|
608
|
+
def __init__(
|
|
609
|
+
self, title, options, default, allow_back, plan, preview=None, learning=None
|
|
610
|
+
):
|
|
600
611
|
super().__init__()
|
|
601
612
|
self._title = title
|
|
602
613
|
self._options = options
|
|
@@ -604,10 +615,14 @@ def _build_app_class():
|
|
|
604
615
|
self._allow_back = allow_back
|
|
605
616
|
self._plan = plan
|
|
606
617
|
self._preview = preview
|
|
618
|
+
self._learning = learning
|
|
607
619
|
|
|
608
620
|
def compose(self):
|
|
609
621
|
yield Static(self._title, id="al-title")
|
|
610
622
|
yield setup_panel(self._plan)
|
|
623
|
+
if self._learning:
|
|
624
|
+
yield Static("Session Learning", id="al-learning-title")
|
|
625
|
+
yield Static("\n".join(self._learning), id="al-learning")
|
|
611
626
|
detail = Static("", id="al-detail")
|
|
612
627
|
detail.border_title = "About highlighted option"
|
|
613
628
|
yield detail
|
|
@@ -617,6 +632,19 @@ def _build_app_class():
|
|
|
617
632
|
option_list = OptionList()
|
|
618
633
|
for option in self._options:
|
|
619
634
|
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
|
|
620
648
|
option_list.add_option(
|
|
621
649
|
Option(label, id=option.value, disabled=not option.enabled)
|
|
622
650
|
)
|
|
@@ -775,10 +803,13 @@ class TextualUI:
|
|
|
775
803
|
default: str,
|
|
776
804
|
allow_back: bool,
|
|
777
805
|
preview=None,
|
|
806
|
+
learning_lines: list[str] | None = None,
|
|
778
807
|
) -> str:
|
|
779
808
|
result = self.app.call_from_thread(
|
|
780
809
|
self.app.push_screen_wait,
|
|
781
|
-
self._menu_screen(
|
|
810
|
+
self._menu_screen(
|
|
811
|
+
title, options, default, allow_back, self.plan, preview, learning_lines
|
|
812
|
+
),
|
|
782
813
|
)
|
|
783
814
|
if result == _UI_BACK:
|
|
784
815
|
raise BackRequested
|
|
@@ -814,13 +845,26 @@ def run_textual_flow(
|
|
|
814
845
|
|
|
815
846
|
|
|
816
847
|
def choose_lines(
|
|
817
|
-
title: str,
|
|
848
|
+
title: str,
|
|
849
|
+
options: list[MenuOption],
|
|
850
|
+
default: str,
|
|
851
|
+
allow_back: bool,
|
|
852
|
+
learning_lines: list[str] | None = None,
|
|
818
853
|
) -> str:
|
|
819
854
|
print(f"\n{title}")
|
|
855
|
+
if learning_lines:
|
|
856
|
+
print(" -- Session Learning --")
|
|
857
|
+
for line in learning_lines:
|
|
858
|
+
print(f" {line}")
|
|
859
|
+
print(" --")
|
|
820
860
|
for index, option in enumerate(options, 1):
|
|
821
861
|
marker = "" if option.enabled else " [unavailable]"
|
|
822
862
|
selected = " *" if option.value == default and option.enabled else ""
|
|
823
|
-
|
|
863
|
+
label = option.label
|
|
864
|
+
if option.value == LEARNING_HUB:
|
|
865
|
+
print(" " + "-" * 30)
|
|
866
|
+
label = f"◆ {label}"
|
|
867
|
+
print(f" {index}. {label}{marker}{selected} - {option.description}")
|
|
824
868
|
if not option.enabled:
|
|
825
869
|
print(f" Unavailable: {option.unavailable_reason}")
|
|
826
870
|
while True:
|
|
@@ -854,12 +898,13 @@ def choose(
|
|
|
854
898
|
ui: TextualUI | None = None,
|
|
855
899
|
allow_back: bool = False,
|
|
856
900
|
preview=None,
|
|
901
|
+
learning_lines: list[str] | None = None,
|
|
857
902
|
) -> str:
|
|
858
903
|
if not any(option.enabled for option in options):
|
|
859
904
|
raise LaunchError(f"no available options for {title}")
|
|
860
905
|
if ui is not None:
|
|
861
|
-
return ui.choose(title, options, default, allow_back, preview)
|
|
862
|
-
return choose_lines(title, options, default, allow_back)
|
|
906
|
+
return ui.choose(title, options, default, allow_back, preview, learning_lines)
|
|
907
|
+
return choose_lines(title, options, default, allow_back, learning_lines)
|
|
863
908
|
|
|
864
909
|
|
|
865
910
|
def read_input(prompt: str) -> str:
|
|
@@ -950,6 +995,12 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
950
995
|
label = preset.get("label", preset_name)
|
|
951
996
|
if not isinstance(label, str) or not label:
|
|
952
997
|
raise LaunchError(f"invalid label in preset {preset_name}")
|
|
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)
|
|
@@ -988,6 +1039,8 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
988
1039
|
"review_backend": review_backend,
|
|
989
1040
|
"agent_templates": copy.deepcopy(config["hosts"][host].get("agent_templates")),
|
|
990
1041
|
"capabilities": copy.deepcopy(config.get("capabilities", {})),
|
|
1042
|
+
"mission": mission,
|
|
1043
|
+
"trigger": trigger,
|
|
991
1044
|
}
|
|
992
1045
|
|
|
993
1046
|
|
|
@@ -1386,6 +1439,215 @@ def customize(
|
|
|
1386
1439
|
raise KeyboardInterrupt
|
|
1387
1440
|
|
|
1388
1441
|
|
|
1442
|
+
LEARNING_HUB = "__learning__"
|
|
1443
|
+
LEARNING_PRESET = "session-learning"
|
|
1444
|
+
LEARNING_STATUS_PATH = pathlib.Path(
|
|
1445
|
+
os.environ.get(
|
|
1446
|
+
"AGENT_BIOS_LEARNING_STATUS",
|
|
1447
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/learning-status.json"),
|
|
1448
|
+
)
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def load_learning_status() -> dict[str, Any] | None:
|
|
1453
|
+
try:
|
|
1454
|
+
return json.loads(LEARNING_STATUS_PATH.read_text())
|
|
1455
|
+
except (OSError, ValueError):
|
|
1456
|
+
return None
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def learning_summary_lines(status: dict[str, Any] | None) -> list[str]:
|
|
1460
|
+
"""Panel body for the Session Learning area: which learning content is
|
|
1461
|
+
live, through which mechanisms, and whether the corpus is rolled back."""
|
|
1462
|
+
if status is None:
|
|
1463
|
+
return ["learning status not projected yet — run: agent-bios install"]
|
|
1464
|
+
current = status.get("current_version", "?")
|
|
1465
|
+
latest = status.get("latest_version", "?")
|
|
1466
|
+
head = f"Applied version {current}"
|
|
1467
|
+
if status.get("rolled_back_to"):
|
|
1468
|
+
head += f" (ROLLED BACK; latest is {latest})"
|
|
1469
|
+
layers = status.get("summary", {}).get("placed_by_layer", {})
|
|
1470
|
+
order = ("global", "guide", "hook", "enforcement", "gate")
|
|
1471
|
+
layer_text = " · ".join(
|
|
1472
|
+
f"{name} {layers[name]}" for name in order if layers.get(name)
|
|
1473
|
+
) or "none"
|
|
1474
|
+
by_status = status.get("summary", {}).get("by_status", {})
|
|
1475
|
+
return [
|
|
1476
|
+
head,
|
|
1477
|
+
f"Mechanisms {layer_text}",
|
|
1478
|
+
f"Ledger placed {by_status.get('placed', 0)} · "
|
|
1479
|
+
f"incubating {by_status.get('incubating', 0) + by_status.get('incubating-G', 0)} · "
|
|
1480
|
+
f"versions {len(status.get('versions', []))}",
|
|
1481
|
+
]
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _learning_info(ui: TextualUI | None, title: str, lines: list[str]) -> None:
|
|
1485
|
+
"""Info screen in both UIs: options carry the content; only exit is back."""
|
|
1486
|
+
options = [MenuOption("back", "Back", "Return to Session Learning.")]
|
|
1487
|
+
try:
|
|
1488
|
+
choose(title, options, "back", ui, allow_back=True, learning_lines=lines)
|
|
1489
|
+
except BackRequested:
|
|
1490
|
+
pass
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
def _learning_rollback(status: dict[str, Any], version: str, ui: TextualUI | None) -> None:
|
|
1494
|
+
confirm = choose(
|
|
1495
|
+
f"Roll back learning corpus to {version}?",
|
|
1496
|
+
[
|
|
1497
|
+
MenuOption(
|
|
1498
|
+
"cancel", "Cancel", "Keep the currently deployed learning content."
|
|
1499
|
+
),
|
|
1500
|
+
MenuOption(
|
|
1501
|
+
"rollback",
|
|
1502
|
+
f"Roll back to {version}",
|
|
1503
|
+
"Re-deploys globals/guides/hooks as of that learning version. "
|
|
1504
|
+
"System deployment (launcher, wrappers) stays current. Roll "
|
|
1505
|
+
"forward again by selecting the latest version.",
|
|
1506
|
+
),
|
|
1507
|
+
],
|
|
1508
|
+
"cancel",
|
|
1509
|
+
ui,
|
|
1510
|
+
allow_back=True,
|
|
1511
|
+
)
|
|
1512
|
+
if confirm != "rollback":
|
|
1513
|
+
return
|
|
1514
|
+
script = pathlib.Path(status["repo"]) / "scripts/session-learning/learning-state.py"
|
|
1515
|
+
result = subprocess.run(
|
|
1516
|
+
[sys.executable, str(script), "rollback", "--version", version],
|
|
1517
|
+
capture_output=True,
|
|
1518
|
+
text=True,
|
|
1519
|
+
)
|
|
1520
|
+
output = (result.stdout + result.stderr).strip().splitlines()
|
|
1521
|
+
tail = output[-1] if output else ""
|
|
1522
|
+
verdict = "Rollback complete." if result.returncode == 0 else "Rollback FAILED."
|
|
1523
|
+
_learning_info(ui, verdict, [tail] if tail else [])
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def _learning_versions(ui: TextualUI | None) -> None:
|
|
1527
|
+
while True:
|
|
1528
|
+
status = load_learning_status()
|
|
1529
|
+
if status is None:
|
|
1530
|
+
_learning_info(ui, "Versions & rollback", learning_summary_lines(None))
|
|
1531
|
+
return
|
|
1532
|
+
current = status.get("current_version")
|
|
1533
|
+
options = []
|
|
1534
|
+
for v in reversed(status.get("versions", [])):
|
|
1535
|
+
name = v["version"]
|
|
1536
|
+
label = f"{name} (current)" if name == current else name
|
|
1537
|
+
options.append(
|
|
1538
|
+
MenuOption(
|
|
1539
|
+
name,
|
|
1540
|
+
label,
|
|
1541
|
+
f"closed {v.get('closed', '?')} · commit {v.get('commit', '')[:12]} · "
|
|
1542
|
+
f"{v.get('summary', '')}",
|
|
1543
|
+
)
|
|
1544
|
+
)
|
|
1545
|
+
options.append(MenuOption("back", "Back", "Return to Session Learning."))
|
|
1546
|
+
try:
|
|
1547
|
+
selected = choose(
|
|
1548
|
+
"Versions & rollback",
|
|
1549
|
+
options,
|
|
1550
|
+
options[0].value,
|
|
1551
|
+
ui,
|
|
1552
|
+
allow_back=True,
|
|
1553
|
+
learning_lines=learning_summary_lines(status),
|
|
1554
|
+
)
|
|
1555
|
+
except BackRequested:
|
|
1556
|
+
return
|
|
1557
|
+
if selected == "back":
|
|
1558
|
+
return
|
|
1559
|
+
if selected == current:
|
|
1560
|
+
_learning_info(
|
|
1561
|
+
ui,
|
|
1562
|
+
f"{selected} is the currently applied version",
|
|
1563
|
+
["Select a different version to roll back or forward."],
|
|
1564
|
+
)
|
|
1565
|
+
continue
|
|
1566
|
+
_learning_rollback(status, selected, ui)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _learning_packages(ui: TextualUI | None) -> None:
|
|
1570
|
+
"""v1: the corpus ships as a single core package; the list shape is ready
|
|
1571
|
+
for the domain-packaging backlog to populate with real packages."""
|
|
1572
|
+
status = load_learning_status()
|
|
1573
|
+
if status is None:
|
|
1574
|
+
_learning_info(ui, "Learning packages", learning_summary_lines(None))
|
|
1575
|
+
return
|
|
1576
|
+
layers = status.get("summary", {}).get("placed_by_layer", {})
|
|
1577
|
+
options = [
|
|
1578
|
+
MenuOption(
|
|
1579
|
+
"core",
|
|
1580
|
+
f"core corpus @ {status.get('current_version', '?')}",
|
|
1581
|
+
"All learning domains in one package until domain packaging "
|
|
1582
|
+
f"lands (backlog). Layers: {json.dumps(layers, separators=(', ', ' '))}. "
|
|
1583
|
+
"Per-domain selection, versions, and rollback will appear here.",
|
|
1584
|
+
),
|
|
1585
|
+
MenuOption("back", "Back", "Return to Session Learning."),
|
|
1586
|
+
]
|
|
1587
|
+
try:
|
|
1588
|
+
choose(
|
|
1589
|
+
"Learning packages",
|
|
1590
|
+
options,
|
|
1591
|
+
"back",
|
|
1592
|
+
ui,
|
|
1593
|
+
allow_back=True,
|
|
1594
|
+
learning_lines=learning_summary_lines(status),
|
|
1595
|
+
)
|
|
1596
|
+
except BackRequested:
|
|
1597
|
+
pass
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def learning_hub(config: dict[str, Any], ui: TextualUI | None) -> str:
|
|
1601
|
+
"""Session Learning area: status, packages, versions/rollback, session start.
|
|
1602
|
+
|
|
1603
|
+
Returns "start" to launch the learning-session preset, "back" otherwise.
|
|
1604
|
+
"""
|
|
1605
|
+
while True:
|
|
1606
|
+
status = load_learning_status()
|
|
1607
|
+
options = [
|
|
1608
|
+
MenuOption(
|
|
1609
|
+
"start",
|
|
1610
|
+
"Start a learning session",
|
|
1611
|
+
"Launch with the Session learning preset: the mission line "
|
|
1612
|
+
"directs the session to the workflow guide and the SSOT.",
|
|
1613
|
+
enabled=LEARNING_PRESET in config["presets"],
|
|
1614
|
+
unavailable_reason=f"preset {LEARNING_PRESET!r} not configured",
|
|
1615
|
+
),
|
|
1616
|
+
MenuOption(
|
|
1617
|
+
"packages",
|
|
1618
|
+
"Learning packages",
|
|
1619
|
+
"What learning content is applied, as installable packages "
|
|
1620
|
+
"(v1: single core corpus).",
|
|
1621
|
+
),
|
|
1622
|
+
MenuOption(
|
|
1623
|
+
"versions",
|
|
1624
|
+
"Versions & rollback",
|
|
1625
|
+
"Learning content versions (distinct from system deployment); "
|
|
1626
|
+
"roll the corpus back or forward.",
|
|
1627
|
+
),
|
|
1628
|
+
MenuOption("back", "Back", "Return to the launch menu."),
|
|
1629
|
+
]
|
|
1630
|
+
try:
|
|
1631
|
+
selected = choose(
|
|
1632
|
+
"Session Learning",
|
|
1633
|
+
options,
|
|
1634
|
+
"start",
|
|
1635
|
+
ui,
|
|
1636
|
+
allow_back=True,
|
|
1637
|
+
learning_lines=learning_summary_lines(status),
|
|
1638
|
+
)
|
|
1639
|
+
except BackRequested:
|
|
1640
|
+
return "back"
|
|
1641
|
+
if selected == "back":
|
|
1642
|
+
return "back"
|
|
1643
|
+
if selected == "start":
|
|
1644
|
+
return "start"
|
|
1645
|
+
if selected == "packages":
|
|
1646
|
+
_learning_packages(ui)
|
|
1647
|
+
elif selected == "versions":
|
|
1648
|
+
_learning_versions(ui)
|
|
1649
|
+
|
|
1650
|
+
|
|
1389
1651
|
def select_plan(
|
|
1390
1652
|
config: dict[str, Any],
|
|
1391
1653
|
host: str,
|
|
@@ -1416,21 +1678,44 @@ def select_plan(
|
|
|
1416
1678
|
"Open a settings hub for tiers, review setup, policy, and final confirmation.",
|
|
1417
1679
|
)
|
|
1418
1680
|
)
|
|
1681
|
+
options.append(
|
|
1682
|
+
MenuOption(
|
|
1683
|
+
LEARNING_HUB,
|
|
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
|
+
)
|
|
1419
1690
|
default = "balanced" if "balanced" in presets else next(iter(presets))
|
|
1420
1691
|
if ui is not None:
|
|
1421
1692
|
ui.set_plan(build_plan(config, host, default))
|
|
1693
|
+
|
|
1694
|
+
def preview_name(value: str) -> str:
|
|
1695
|
+
if value == CUSTOM_PRESET:
|
|
1696
|
+
return default
|
|
1697
|
+
if value == LEARNING_HUB:
|
|
1698
|
+
return LEARNING_PRESET if LEARNING_PRESET in presets else default
|
|
1699
|
+
return value
|
|
1700
|
+
|
|
1422
1701
|
selected = choose(
|
|
1423
1702
|
"Preset",
|
|
1424
1703
|
options,
|
|
1425
1704
|
default,
|
|
1426
1705
|
ui,
|
|
1427
|
-
preview=lambda value: build_plan(
|
|
1428
|
-
|
|
1429
|
-
),
|
|
1706
|
+
preview=lambda value: build_plan(config, host, preview_name(value)),
|
|
1707
|
+
learning_lines=learning_summary_lines(load_learning_status()),
|
|
1430
1708
|
)
|
|
1431
1709
|
if selected == CUSTOM_PRESET:
|
|
1432
1710
|
selected_name = default
|
|
1433
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
|
|
1434
1719
|
else:
|
|
1435
1720
|
selected_name = selected
|
|
1436
1721
|
if selected_name not in presets:
|
|
@@ -1468,7 +1753,7 @@ def codex_agent_configs(
|
|
|
1468
1753
|
if not isinstance(templates, dict):
|
|
1469
1754
|
raise LaunchError("Codex delegation requires [hosts.codex.agent_templates]")
|
|
1470
1755
|
rendered: dict[str, tuple[str, str]] = {}
|
|
1471
|
-
for tier in
|
|
1756
|
+
for tier in SPAWNABLE_TIERS:
|
|
1472
1757
|
source_value = templates.get(tier)
|
|
1473
1758
|
if not isinstance(source_value, str) or not source_value:
|
|
1474
1759
|
raise LaunchError(f"Codex agent template missing: {tier}")
|
|
@@ -1630,7 +1915,12 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
1630
1915
|
if plan["host"] == "codex"
|
|
1631
1916
|
else "Main and child model/effort defaults are CLI-projected."
|
|
1632
1917
|
)
|
|
1918
|
+
mission = plan.get("mission")
|
|
1919
|
+
if mission and plan.get("trigger"):
|
|
1920
|
+
mission = mission.replace("{trigger}", plan["trigger"])
|
|
1921
|
+
mission_prefix = f"Mission: {mission} " if mission else ""
|
|
1633
1922
|
return (
|
|
1923
|
+
f"{mission_prefix}"
|
|
1634
1924
|
f"LaunchPlan: main={plan['main_tier']} ({plan['tiers'][plan['main_tier']]['model']}/"
|
|
1635
1925
|
f"{tier_effort(plan, plan['main_tier'])}); tiers: {bindings}. "
|
|
1636
1926
|
f"Delegation={'on' if plan['delegation'] else 'off'}. Review family={family}. "
|
|
@@ -1643,7 +1933,7 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
1643
1933
|
|
|
1644
1934
|
def claude_agents(plan: dict[str, Any]) -> str:
|
|
1645
1935
|
roles = {}
|
|
1646
|
-
for tier in
|
|
1936
|
+
for tier in SPAWNABLE_TIERS:
|
|
1647
1937
|
binding = plan["tiers"][tier]
|
|
1648
1938
|
roles[tier] = {
|
|
1649
1939
|
"description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
|
|
@@ -1782,6 +2072,49 @@ def print_summary(
|
|
|
1782
2072
|
print(" Argv " + json.dumps([command, *args]), file=stream)
|
|
1783
2073
|
|
|
1784
2074
|
|
|
2075
|
+
SESSION_LEARNING_STATE = pathlib.Path(
|
|
2076
|
+
os.environ.get(
|
|
2077
|
+
"AGENT_BIOS_SESSION_LEARNING_STATE",
|
|
2078
|
+
str(pathlib.Path.home() / ".local/share/agent-bios/session-learning-state.json"),
|
|
2079
|
+
)
|
|
2080
|
+
)
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
def _line_count(path: pathlib.Path) -> int:
|
|
2084
|
+
try:
|
|
2085
|
+
with path.open("rb") as fh:
|
|
2086
|
+
return sum(chunk.count(b"\n") for chunk in iter(lambda: fh.read(1 << 20), b""))
|
|
2087
|
+
except OSError:
|
|
2088
|
+
return 0
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def session_learning_nudge(config: dict[str, Any]) -> str | None:
|
|
2092
|
+
"""Nudge when enough sessions accumulated since the last mining window.
|
|
2093
|
+
|
|
2094
|
+
The baseline is written by scripts/session-learning/update-state.py at
|
|
2095
|
+
window close; provider history line counts are a cheap proxy for new
|
|
2096
|
+
sessions. No state file means no nudge.
|
|
2097
|
+
"""
|
|
2098
|
+
try:
|
|
2099
|
+
state = json.loads(SESSION_LEARNING_STATE.read_text())
|
|
2100
|
+
baseline = int(state["history_lines_total"])
|
|
2101
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
2102
|
+
return None
|
|
2103
|
+
settings = config.get("session_learning", {})
|
|
2104
|
+
threshold = settings.get("nudge_after", 250) if isinstance(settings, dict) else 250
|
|
2105
|
+
current = _line_count(pathlib.Path.home() / ".claude/history.jsonl") + _line_count(
|
|
2106
|
+
pathlib.Path.home() / ".codex/history.jsonl"
|
|
2107
|
+
)
|
|
2108
|
+
delta = current - baseline
|
|
2109
|
+
if delta < threshold:
|
|
2110
|
+
return None
|
|
2111
|
+
return (
|
|
2112
|
+
f"session-learning due: ~{delta} new session entries since "
|
|
2113
|
+
f"{state.get('window_end', '?')} (threshold {threshold}) — launch the "
|
|
2114
|
+
"Session learning preset to run the next mining window"
|
|
2115
|
+
)
|
|
2116
|
+
|
|
2117
|
+
|
|
1785
2118
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
1786
2119
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1787
2120
|
parser.add_argument("--config", type=pathlib.Path, default=default_config_path())
|
|
@@ -1811,6 +2144,12 @@ def main(argv: list[str]) -> int:
|
|
|
1811
2144
|
config_path = args.config.expanduser()
|
|
1812
2145
|
config = load_config(config_path)
|
|
1813
2146
|
command, passthrough = resolve_backend(config, args.host)
|
|
2147
|
+
nudge = session_learning_nudge(config)
|
|
2148
|
+
if nudge:
|
|
2149
|
+
print(f"agent-launch: {nudge}", file=sys.stderr)
|
|
2150
|
+
nudged = config["presets"].get("session-learning")
|
|
2151
|
+
if isinstance(nudged, dict):
|
|
2152
|
+
nudged["description"] = f"{nudged.get('description', '')} ⚠ {nudge}".strip()
|
|
1814
2153
|
tty = sys.stdin.isatty() and sys.stdout.isatty()
|
|
1815
2154
|
if not tty and args.dry_run and not args.preset and not args.custom:
|
|
1816
2155
|
if "balanced" not in config["presets"]:
|
|
@@ -1848,6 +2187,12 @@ def main(argv: list[str]) -> int:
|
|
|
1848
2187
|
projected = [*project_args(plan, materialize_agents=not args.dry_run), *args.forward]
|
|
1849
2188
|
summary_stream = sys.stdout if tty or args.dry_run else sys.stderr
|
|
1850
2189
|
print_summary(plan, command, projected, summary_stream, bool(args.forward))
|
|
2190
|
+
trigger = plan.get("trigger")
|
|
2191
|
+
if trigger:
|
|
2192
|
+
# Black-on-yellow to match the Session Learning identity; the session
|
|
2193
|
+
# waits for this exact phrase before starting the workflow.
|
|
2194
|
+
line = f' Once the session is up, type "{trigger}" to begin the learning workflow. '
|
|
2195
|
+
print(f"\n\x1b[1;30;43m{line}\x1b[0m", file=summary_stream)
|
|
1851
2196
|
if args.dry_run:
|
|
1852
2197
|
print(json.dumps([command, *projected], ensure_ascii=False))
|
|
1853
2198
|
return 0
|
package/scripts/check-parity.sh
CHANGED
|
@@ -111,6 +111,8 @@ current-state dashboard|claude/CLAUDE.md|claude/guides/implementation-map.md
|
|
|
111
111
|
Verification Menus|claude/CLAUDE.md|claude/guides/coding-staged-workflow.md
|
|
112
112
|
real Microsoft Excel engine|claude/CLAUDE.md|claude/guides/coding-staged-workflow.md
|
|
113
113
|
severity contract|README.md|claude/guides/coding-staged-workflow.md
|
|
114
|
+
Ambient state|claude/CLAUDE.md|claude/guides/tooling-gotchas.md
|
|
115
|
+
the full lifecycle of what you create|claude/CLAUDE.md|claude/guides/tooling-gotchas.md
|
|
114
116
|
ANCHORS
|
|
115
117
|
|
|
116
118
|
# The launcher's Textual preflight UI tests need the managed venv (textual).
|
|
@@ -264,6 +266,33 @@ for filename, (expected_name, expected_model, expected_effort) in expected_agent
|
|
|
264
266
|
f"want {expected_effort!r}"
|
|
265
267
|
)
|
|
266
268
|
|
|
269
|
+
# codex/config-additions.toml is the additive fragment install merges into the
|
|
270
|
+
# live ~/.codex/config.toml; its agent entries are projections of the canonical
|
|
271
|
+
# codex/agents/*.toml templates and must not drift from them.
|
|
272
|
+
fragment_path = pathlib.Path("codex/config-additions.toml")
|
|
273
|
+
if not fragment_path.is_file():
|
|
274
|
+
mark_fail("required file missing: codex/config-additions.toml")
|
|
275
|
+
else:
|
|
276
|
+
fragment = tomllib.loads(fragment_path.read_text())
|
|
277
|
+
if fragment.get("features", {}).get("multi_agent") is not True:
|
|
278
|
+
mark_fail("config-additions must set features.multi_agent = true")
|
|
279
|
+
fragment_agents = fragment.get("agents", {})
|
|
280
|
+
if set(fragment_agents) != {"frontier", "workhorse", "sweep"}:
|
|
281
|
+
mark_fail(
|
|
282
|
+
"config-additions agents must be exactly the spawnable tiers "
|
|
283
|
+
f"(no helm): {sorted(fragment_agents)}"
|
|
284
|
+
)
|
|
285
|
+
for tier, spec in fragment_agents.items():
|
|
286
|
+
template_path = agent_dir / f"{tier}.toml"
|
|
287
|
+
template = tomllib.loads(template_path.read_text()) if template_path.is_file() else {}
|
|
288
|
+
if spec.get("description") != template.get("description"):
|
|
289
|
+
mark_fail(f"config-additions {tier} description drifted from {template_path}")
|
|
290
|
+
if spec.get("config_file") != f"${{CODEX_HOME}}/agents/{tier}.toml":
|
|
291
|
+
mark_fail(
|
|
292
|
+
f"config-additions {tier} config_file must be "
|
|
293
|
+
f"${{CODEX_HOME}}/agents/{tier}.toml"
|
|
294
|
+
)
|
|
295
|
+
|
|
267
296
|
|
|
268
297
|
def table_row(path, slot):
|
|
269
298
|
for raw_line in path.read_text().splitlines():
|
|
@@ -522,7 +551,7 @@ if launcher.is_file() and launch_profile:
|
|
|
522
551
|
def set_plan(self, plan):
|
|
523
552
|
self.plan = plan
|
|
524
553
|
|
|
525
|
-
def choose(self, title, options, default, allow_back, preview=None):
|
|
554
|
+
def choose(self, title, options, default, allow_back, preview=None, learning_lines=None):
|
|
526
555
|
if title == "Preset":
|
|
527
556
|
if preview is not None:
|
|
528
557
|
balanced = launcher_module.setup_summary_lines(preview("balanced"))
|
|
@@ -748,6 +777,7 @@ if launcher.is_file() and launch_profile:
|
|
|
748
777
|
(b"Esc cancel | q cancel", b"\x1b[B"),
|
|
749
778
|
(b"hybrid onto", b"\x1b[B"),
|
|
750
779
|
(b"high-volume", b"\x1b[B"),
|
|
780
|
+
(b"learning-refinement", b"\x1b[B"),
|
|
751
781
|
(b"Open a settings hub", b"\r"),
|
|
752
782
|
)
|
|
753
783
|
down = b"\x1b[B"
|
|
@@ -1098,10 +1128,11 @@ if launcher.is_file() and launch_profile:
|
|
|
1098
1128
|
# TERM=dumb routes to numbered prompts (textual renders on any usable
|
|
1099
1129
|
# terminal, so the numbered fallback is gated on TERM/non-TTY/textual
|
|
1100
1130
|
# availability, not a terminfo probe). 'b' is the numbered back command.
|
|
1131
|
+
custom_number = str(len(fake_data["presets"]) + 1).encode()
|
|
1101
1132
|
transcript, picker_status = run_picker_scenario(
|
|
1102
1133
|
"numbered fallback (TERM=dumb)",
|
|
1103
1134
|
(
|
|
1104
|
-
(b"Open a settings hub", b"
|
|
1135
|
+
(b"Open a settings hub", custom_number + b"\n"),
|
|
1105
1136
|
(b"Custom settings", b"b\n"),
|
|
1106
1137
|
(b"HELM default for everyday work", b"1\n"),
|
|
1107
1138
|
),
|
|
@@ -1114,6 +1145,28 @@ if launcher.is_file() and launch_profile:
|
|
|
1114
1145
|
):
|
|
1115
1146
|
mark_fail("agent-launch numbered fallback (TERM=dumb) did not preserve numbered back")
|
|
1116
1147
|
|
|
1148
|
+
# Session Learning hub: enter, render status, back out, launch balanced.
|
|
1149
|
+
learning_number = str(len(fake_data["presets"]) + 2).encode()
|
|
1150
|
+
transcript, picker_status = run_picker_scenario(
|
|
1151
|
+
"numbered learning hub (TERM=dumb)",
|
|
1152
|
+
(
|
|
1153
|
+
(b"Session Learning", learning_number + b"\n"),
|
|
1154
|
+
(b"Versions & rollback", b"4\n"),
|
|
1155
|
+
(b"HELM default for everyday work", b"1\n"),
|
|
1156
|
+
),
|
|
1157
|
+
term="dumb",
|
|
1158
|
+
)
|
|
1159
|
+
learning_panel_rendered = (
|
|
1160
|
+
b"Applied version" in transcript or b"not projected yet" in transcript
|
|
1161
|
+
)
|
|
1162
|
+
if (
|
|
1163
|
+
picker_status != 0
|
|
1164
|
+
or b"Traceback" in transcript
|
|
1165
|
+
or not learning_panel_rendered
|
|
1166
|
+
or b"Preset Balanced" not in transcript
|
|
1167
|
+
):
|
|
1168
|
+
mark_fail("agent-launch numbered learning hub did not render or return")
|
|
1169
|
+
|
|
1117
1170
|
passed = invoke([
|
|
1118
1171
|
sys.executable, str(launcher), "--no-tui", "codex", "--", "exec", "--json", "probe"
|
|
1119
1172
|
], env=env)
|
|
@@ -1421,7 +1474,16 @@ if launcher.is_file() and launch_profile:
|
|
|
1421
1474
|
except (ValueError, IndexError, json.JSONDecodeError) as exc:
|
|
1422
1475
|
mark_fail(f"agent-launch Claude --agents projection is not valid JSON: {exc}")
|
|
1423
1476
|
else:
|
|
1424
|
-
|
|
1477
|
+
expected_spawnable = {
|
|
1478
|
+
tier: expected_claude_tiers[tier]
|
|
1479
|
+
for tier in ("frontier", "workhorse", "sweep")
|
|
1480
|
+
}
|
|
1481
|
+
if set(agents_arg) != set(expected_spawnable):
|
|
1482
|
+
mark_fail(
|
|
1483
|
+
"Claude --agents must project exactly the spawnable tiers "
|
|
1484
|
+
f"(HELM is the main, never a subagent): {sorted(agents_arg)}"
|
|
1485
|
+
)
|
|
1486
|
+
for tier, (model, effort) in expected_spawnable.items():
|
|
1425
1487
|
role = agents_arg.get(tier, {})
|
|
1426
1488
|
if role.get("model") != model or role.get("effort") != effort:
|
|
1427
1489
|
mark_fail(f"agent-launch Claude {tier} role projection drifted: {role!r}")
|
package/scripts/codex-helm.sh
CHANGED
|
@@ -268,9 +268,27 @@ EOF
|
|
|
268
268
|
EOF
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
# Review packets must carry the whole subject: range-based diffs silently omit
|
|
272
|
+
# staged-but-uncommitted changes and untracked files, so the dispatcher itself
|
|
273
|
+
# appends the subject tree's actual state to the packet.
|
|
274
|
+
scope_note=""
|
|
275
|
+
if [ "$mode" = "review" ] && { [ -n "$cd_dir" ] || [ "$reach" != "hermetic" ]; }; then
|
|
276
|
+
scope_root="${cd_dir:-$PWD}"
|
|
277
|
+
if git -C "$scope_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
278
|
+
scope_status="$(git -C "$scope_root" status --porcelain 2>/dev/null || true)"
|
|
279
|
+
if [ -n "$scope_status" ]; then
|
|
280
|
+
scope_note="Review scope manifest (dispatcher-generated \`git status --porcelain\` of the subject tree; uncommitted/untracked entries are part of the review subject unless the task says otherwise):
|
|
281
|
+
$scope_status"
|
|
282
|
+
echo "codex-helm: subject tree has uncommitted/untracked entries; scope manifest appended to the packet" >&2
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
fi
|
|
286
|
+
|
|
271
287
|
final_prompt="$(build_preamble)
|
|
272
288
|
|
|
273
|
-
|
|
289
|
+
${scope_note:+$scope_note
|
|
290
|
+
|
|
291
|
+
}User task:
|
|
274
292
|
$user_prompt"
|
|
275
293
|
|
|
276
294
|
cleanup_dirs=()
|