agent-bios 0.14.0 → 0.16.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.
Files changed (60) hide show
  1. package/DEPENDENCIES.md +35 -12
  2. package/README.md +346 -31
  3. package/claude/CLAUDE.md +2 -2
  4. package/claude/agents/frontier.md +1 -1
  5. package/claude/agents/sweep.md +3 -3
  6. package/claude/agents/workhorse.md +2 -2
  7. package/claude/guides/claude-prompting.md +119 -34
  8. package/claude/guides/cli-multi-model-workflow.md +33 -15
  9. package/claude/guides/gpt-prompting.md +148 -28
  10. package/claude/guides/review-request.md +27 -0
  11. package/claude/guides/session-distill-workflow.md +54 -2
  12. package/claude/guides/slide-writing/RUNBOOK.md +137 -0
  13. package/claude/guides/slide-writing/scripts/pair.py +979 -0
  14. package/claude/guides/slide-writing/scripts/render.mjs +82 -0
  15. package/claude/guides/slide-writing.md +195 -0
  16. package/claude/guides/svg-visualization-guide.md +9 -0
  17. package/claude/guides/verification-discipline.md +5 -1
  18. package/claude/hooks/tooling-gotchas-hook.py +7 -5
  19. package/codex/AGENTS.md +2 -2
  20. package/codex/agents/frontier.toml +2 -1
  21. package/codex/agents/reviewer.toml +1 -1
  22. package/codex/agents/sweep.toml +3 -3
  23. package/codex/agents/workhorse.toml +1 -1
  24. package/codex/config-additions.toml +1 -1
  25. package/codex/guides/claude-prompting.md +119 -34
  26. package/codex/guides/cli-multi-model-workflow.md +33 -15
  27. package/codex/guides/gpt-prompting.md +148 -28
  28. package/codex/guides/review-request.md +27 -0
  29. package/codex/guides/session-distill-workflow.md +54 -2
  30. package/codex/guides/slide-writing/RUNBOOK.md +137 -0
  31. package/codex/guides/slide-writing/scripts/pair.py +979 -0
  32. package/codex/guides/slide-writing/scripts/render.mjs +82 -0
  33. package/codex/guides/slide-writing.md +195 -0
  34. package/codex/guides/svg-visualization-guide.md +9 -0
  35. package/codex/guides/verification-discipline.md +5 -1
  36. package/compose/assemble.py +290 -14
  37. package/compose/bootstrap/SKILL.md +119 -0
  38. package/compose/check-domains.py +102 -9
  39. package/compose/corpus-state.py +1174 -0
  40. package/compose/corpus.py +387 -0
  41. package/compose/corpus_catalog.py +882 -0
  42. package/compose/corpus_install.py +1617 -0
  43. package/compose/corpus_session.py +726 -0
  44. package/compose/corpus_store.py +1414 -0
  45. package/compose/corpus_transaction.py +236 -0
  46. package/compose/corpus_ui.py +644 -0
  47. package/compose/domains.json +101 -100
  48. package/compose/write-update-cache.py +53 -0
  49. package/install.sh +174 -24
  50. package/launch/agent-launch.py +1327 -184
  51. package/launch/agent-launch.toml +12 -16
  52. package/launch/i18n/en.toml +113 -7
  53. package/launch/i18n/ja.toml +113 -7
  54. package/launch/i18n/ko.toml +113 -7
  55. package/learn/collect-learning.py +46 -19
  56. package/learn/migrate-learnings.py +10 -1
  57. package/package.json +13 -3
  58. package/provenance.json +1 -1
  59. package/session-cost.py +22 -2
  60. package/wrappers/codex-helm.sh +3 -3
@@ -16,6 +16,7 @@ import string
16
16
  import subprocess
17
17
  import sys
18
18
  import tempfile
19
+ import time
19
20
  import unicodedata
20
21
  import textwrap
21
22
  import tomllib
@@ -518,13 +519,17 @@ LEGACY_REVIEW_LOWERING = {
518
519
 
519
520
  @dataclass(frozen=True)
520
521
  class ReviewBinding:
521
- """Which verifier adjudicates. `model` and `effort` are inseparable: a `tier`
522
- reference satisfies that by construction, a bare `model` must carry its own."""
522
+ """Which verifier adjudicates.
523
+
524
+ Review plans normally carry a model/effort pair. A tier may resolve to a
525
+ no-effort model; the selected-row renderer names the ledger limitation before
526
+ that row could serialize an ambiguous null.
527
+ """
523
528
 
524
529
  provider: str
525
530
  host: str
526
531
  model: str
527
- effort: str
532
+ effort: str | None
528
533
  service_tier: str = DEFAULT_SERVICE_TIER
529
534
  tier: str | None = None
530
535
 
@@ -677,11 +682,18 @@ def parse_review_binding(
677
682
  else:
678
683
  if not isinstance(raw["model"], str) or not raw["model"]:
679
684
  raise LaunchError(f"{context}.model must be a non-empty string")
680
- if "effort" not in raw:
685
+ # Preserve the host-independent validation for an unseated binding. The
686
+ # one exception is the actual Claude Haiku seat; accepting omission for
687
+ # any arbitrary model merely because this profile lacks its host would
688
+ # defer a malformed review binding until a later machine gains that host.
689
+ is_haiku = provider == "anthropic" and raw["model"] == "claude-haiku-4-5"
690
+ if not is_haiku and "effort" not in raw:
681
691
  raise LaunchError(
682
692
  f"{context}.effort is required with model: a model without an effort would "
683
693
  "leave the verifier's rigour for the designer to pick at dispatch time"
684
694
  )
695
+ if is_haiku and "effort" in raw:
696
+ validate_effort("claude", raw["model"], raw["effort"], context)
685
697
  # Only now may an absent host end the parse. Everything above holds whatever hosts
686
698
  # exist, and leaving any of it below this line let a malformed binding through as
687
699
  # "unseated" — accepted today, rejected the day the host appears, and in the
@@ -716,7 +728,12 @@ def parse_review_binding(
716
728
  else:
717
729
  tier = None
718
730
  model = raw["model"]
719
- effort = validate_effort(host, model, raw["effort"], context)
731
+ if model_requires_effort(host, model) and "effort" not in raw:
732
+ raise LaunchError(
733
+ f"{context}.effort is required with model: a model without an effort would "
734
+ "leave the verifier's rigour for the designer to pick at dispatch time"
735
+ )
736
+ effort = validate_effort(host, model, raw.get("effort"), context)
720
737
  return ReviewBinding(provider, host, model, effort, service_tier, tier)
721
738
 
722
739
 
@@ -888,6 +905,11 @@ def legacy_review_plan(setup: str, family: str) -> ReviewPlan:
888
905
  )
889
906
 
890
907
 
908
+ def review_is_requested(review: ReviewPlan) -> bool:
909
+ """Whether this plan would project any review capability or availability claim."""
910
+ return review.source == "composable" or review.legacy_setup != "none"
911
+
912
+
891
913
  # ── review methods: the declarative surface (DESIGN.md §1) ──────────────────
892
914
  # A METHOD says how to review. A CAPABILITY says how an installed tool exposes an
893
915
  # operation. A BINDING says which verifier adjudicates. The MECHANISM is derived by
@@ -1554,6 +1576,11 @@ def render_review_method(
1554
1576
  discipline clause to every row, method-blind, the way the severity translation is
1555
1577
  appended — an author never writes it and a slot never carries it."""
1556
1578
  binding = mechanism.binding
1579
+ if binding.effort is None:
1580
+ raise LaunchError(
1581
+ f"review method {method.method_id!r} cannot render {binding.model}: its "
1582
+ "instruction and ReviewPlan/v1 seat require an explicit effort"
1583
+ )
1557
1584
  slots = {
1558
1585
  "command": mechanism.command or "",
1559
1586
  "model": binding.model,
@@ -1607,7 +1634,7 @@ def render_review_method(
1607
1634
  )
1608
1635
  rendered = (
1609
1636
  f"{method.method_id}: {body} "
1610
- f"[{mechanism.shape}; {binding.model}/{binding.effort}"
1637
+ f"[{mechanism.shape}; {format_model_effort(binding.model, binding.effort)}"
1611
1638
  f"{controls_clause(method)}{criterion_clause(criterion)}{severity_translation(method)}]"
1612
1639
  )
1613
1640
  # The assembled ROW, after the body. The body check above catches a SLOT value and says
@@ -1785,6 +1812,10 @@ def independence_grade(reviewer: ReviewBinding, main: ReviewBinding) -> str:
1785
1812
  return "provider_difference"
1786
1813
  if reviewer.model != main.model:
1787
1814
  return "model_difference"
1815
+ # A no-effort seat is rejected before a composable review is serialized, but
1816
+ # keep this grade total for direct callers and future structural readers.
1817
+ if reviewer.effort is None or main.effort is None:
1818
+ return "perspective_floor"
1788
1819
  if EFFORT_ORDER.index(reviewer.effort) > EFFORT_ORDER.index(main.effort):
1789
1820
  return "higher_effort"
1790
1821
  return "perspective_floor"
@@ -1844,7 +1875,8 @@ def _resolve_one(
1844
1875
  return ReviewMethodReport(
1845
1876
  method.method_id, STATUS_DROPPED, None,
1846
1877
  f"{detail}; it would have run on "
1847
- f"{binding.provider}:{binding.model}/{binding.effort} via {mechanism.adapter}",
1878
+ f"{binding.provider}:{format_model_effort(binding.model, binding.effort)} "
1879
+ f"via {mechanism.adapter}",
1848
1880
  )
1849
1881
  grade = independence_grade(binding, main)
1850
1882
  # Accumulated, not assigned: more than one of these can be true at once, and the
@@ -1975,7 +2007,10 @@ def render_review_report(report: ReviewReport) -> str:
1975
2007
  if row.grade:
1976
2008
  head += f"/{row.grade}"
1977
2009
  if row.model:
1978
- head += f" on {row.provider}:{row.model}/{row.effort} via {row.mechanism}"
2010
+ head += (
2011
+ f" on {row.provider}:{format_model_effort(row.model, row.effort)} "
2012
+ f"via {row.mechanism}"
2013
+ )
1979
2014
  if row.detail:
1980
2015
  head += f" — {row.detail}"
1981
2016
  lines.append(head)
@@ -4084,6 +4119,85 @@ def exec_backend(command: str, args: list[str], env: dict[str, str] | None = Non
4084
4119
  os.execve(command, [command, *args], os.environ.copy() if env is None else env)
4085
4120
 
4086
4121
 
4122
+ def private_corpus_enabled() -> bool:
4123
+ explicit = os.environ.get("AGENT_BIOS_PRIVATE_CORPUS")
4124
+ if explicit is not None:
4125
+ return explicit == "1"
4126
+ state = pathlib.Path(os.environ.get("AGENT_BIOS_STATE_DIR", str(pathlib.Path.home() / ".local/share/agent-bios")))
4127
+ return (state / "runtime/private-install.json").is_file()
4128
+
4129
+
4130
+ def corpus_package_root() -> pathlib.Path:
4131
+ explicit = os.environ.get("AGENT_BIOS_PACKAGE_ROOT")
4132
+ if explicit:
4133
+ return pathlib.Path(explicit).resolve()
4134
+ source = pathlib.Path(__file__).resolve().parents[1]
4135
+ if (source / "compose/corpus_store.py").is_file():
4136
+ return source
4137
+ state = pathlib.Path(os.environ.get("AGENT_BIOS_STATE_DIR", str(pathlib.Path.home() / ".local/share/agent-bios")))
4138
+ try:
4139
+ root = pathlib.Path(json.loads((state / "runtime/private-install.json").read_text())["package_root"])
4140
+ except (OSError, ValueError, KeyError, TypeError) as exc:
4141
+ raise LaunchError("private corpus runtime missing; run agent-bios install") from exc
4142
+ if not (root / "compose/corpus_store.py").is_file():
4143
+ raise LaunchError("private corpus runtime missing; run agent-bios install")
4144
+ return root
4145
+
4146
+
4147
+ def corpus_store():
4148
+ root = corpus_package_root()
4149
+ module_root = str(root / "compose")
4150
+ if module_root not in sys.path:
4151
+ sys.path.insert(0, module_root)
4152
+ from corpus_store import CorpusStore
4153
+ return CorpusStore(root)
4154
+
4155
+
4156
+ def _corpus_generation(state_root: pathlib.Path, config_path: pathlib.Path) -> str:
4157
+ paths = {state_root / "runtime/private-install.json", state_root / "runtime/state.json", config_path}
4158
+ digest = hashlib.sha256()
4159
+ for path in sorted(paths):
4160
+ digest.update(str(path).encode() + b"\0")
4161
+ digest.update(path.read_bytes() if path.is_file() else b"<absent>")
4162
+ digest.update(b"\0")
4163
+ return digest.hexdigest()
4164
+
4165
+
4166
+ def _load_private_config(config_path: pathlib.Path, *, replay_only: bool = False):
4167
+ """Read one coherent configuration; verify its generation again at activation."""
4168
+ root = corpus_package_root()
4169
+ module_root = str(root / "compose")
4170
+ if module_root not in sys.path:
4171
+ sys.path.insert(0, module_root)
4172
+ from corpus_transaction import transaction_lock, guard_pending, confirmed_release, TransactionPendingError
4173
+ state_root = corpus_store().state_root
4174
+ with transaction_lock(state_root):
4175
+ try:
4176
+ guard_pending(state_root)
4177
+ except TransactionPendingError:
4178
+ if not replay_only:
4179
+ raise
4180
+ config_path = confirmed_release(state_root) / "launch/agent-launch.toml"
4181
+ config = load_config(config_path)
4182
+ return config, _corpus_generation(state_root, config_path)
4183
+
4184
+
4185
+ def _snapshot_from_config(store, config_path, generation, host, selected, *, dry_run, native):
4186
+ from corpus_transaction import transaction_lock, guard_pending
4187
+ with transaction_lock(store.state_root):
4188
+ guard_pending(store.state_root)
4189
+ if generation != _corpus_generation(store.state_root, config_path):
4190
+ raise LaunchError("corpus installation or launch settings changed during setup; reopen the launcher")
4191
+ return store.snapshot(host, selected, dry_run=dry_run, native=native)
4192
+
4193
+
4194
+ def open_corpus_studio() -> None:
4195
+ root = corpus_package_root()
4196
+ result = subprocess.run([sys.executable, str(root / "compose/corpus.py"), "--repo", str(root)])
4197
+ if result.returncode:
4198
+ print(f"agent-launch: Corpus Studio exited {result.returncode}", file=sys.stderr)
4199
+
4200
+
4087
4201
  def default_config_path() -> pathlib.Path:
4088
4202
  explicit = os.environ.get("AGENT_LAUNCH_CONFIG")
4089
4203
  if explicit:
@@ -4336,7 +4450,51 @@ def resolve_backend(config: dict[str, Any], host: str) -> tuple[str, list[str]]:
4336
4450
  return command, bare_value
4337
4451
 
4338
4452
 
4339
- def validate_effort(host: str, model: str, effort: Any, context: str) -> str:
4453
+ def model_requires_effort(host: str, model: str) -> bool:
4454
+ """Whether this concrete seat accepts the launcher's effort setting.
4455
+
4456
+ `effort` remains required for every supported model except Claude Haiku 4.5.
4457
+ Its absence is a property of that model capability, not a third effort value.
4458
+ """
4459
+ return not (host == "claude" and model == "claude-haiku-4-5")
4460
+
4461
+
4462
+ def format_model_effort(model: str, effort: str | None, separator: str = "/") -> str:
4463
+ """The display/contract spelling of a seat, without inventing an absent effort."""
4464
+ return f"{model}{separator}{effort}" if effort is not None else model
4465
+
4466
+
4467
+ def binding_for_selected_model(
4468
+ host: str,
4469
+ binding: dict[str, Any],
4470
+ model: str,
4471
+ *,
4472
+ default_effort: str | None = None,
4473
+ ) -> dict[str, Any]:
4474
+ """Return one editable tier binding after its model changes.
4475
+
4476
+ Crossing into Haiku removes the effort field. Crossing back starts at this
4477
+ tier's configured default (or the host fallback) instead of retaining that
4478
+ absence as an invalid pseudo-effort; other explicit effort choices are
4479
+ preserved.
4480
+ """
4481
+ updated = {**binding, "model": model}
4482
+ if model_requires_effort(host, model):
4483
+ if updated.get("effort") is None:
4484
+ updated["effort"] = default_effort or host_default_effort(host)
4485
+ else:
4486
+ updated.pop("effort", None)
4487
+ return updated
4488
+
4489
+
4490
+ def validate_effort(host: str, model: str, effort: Any, context: str) -> str | None:
4491
+ if not model_requires_effort(host, model):
4492
+ if effort is not None:
4493
+ raise LaunchError(
4494
+ f"unsupported effort for {context}: {model} does not accept an effort; "
4495
+ "remove the effort key"
4496
+ )
4497
+ return None
4340
4498
  if not isinstance(effort, str) or effort not in HOST_EFFORTS[host]:
4341
4499
  raise LaunchError(f"unsupported effort for {context}: {effort!r}")
4342
4500
  if host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
@@ -4516,6 +4674,11 @@ def plan_projects_nothing(plan: dict[str, Any]) -> bool:
4516
4674
  return plan.get("mode") == SWE_MODE
4517
4675
 
4518
4676
 
4677
+ def sweep_main(plan: dict[str, Any]) -> bool:
4678
+ """Whether this launch's main is the deliberately read-only SWEEP seat."""
4679
+ return plan["main_tier"] == "sweep"
4680
+
4681
+
4519
4682
  def active_tiers(plan: dict[str, Any]) -> tuple[str, ...]:
4520
4683
  """The tiers this launch actually binds, in TIER_ORDER: the main one, plus the
4521
4684
  children argv really carries.
@@ -4547,35 +4710,73 @@ def inactive_tier_reason(plan: dict[str, Any]) -> str:
4547
4710
  """Why the inactive tiers are inactive. Delegation-off removes every child at once
4548
4711
  and is reported as itself; otherwise the cause is that the main is not a spawnable
4549
4712
  tier's peer — the tier is neither this launch's main nor one of its children."""
4713
+ if sweep_main(plan):
4714
+ return "SWEEP main disables delegation to preserve its read-only one-rule-per-item boundary"
4550
4715
  if not plan["delegation"]:
4551
4716
  return "delegation is off"
4552
4717
  return f"the only spawnable child tiers are {', '.join(SPAWNABLE_TIERS)}"
4553
4718
 
4554
4719
 
4720
+ def setup_panel_column(label: str, width: int = 10) -> str:
4721
+ """A label padded to the panel's value column, measured in terminal CELLS.
4722
+
4723
+ `f"{label:<10}"` pads by character count, so a translated label of three CJK glyphs
4724
+ is padded as three and drawn as six — the value column moves for that row only."""
4725
+ return label + " " * max(1, width - display_width(label))
4726
+
4727
+
4555
4728
  def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
4729
+ """The panel every screen carries at its top. Labels are translated; VALUES never
4730
+ are — a host name, a model id, a tier slot, `on`/`off` are the words the user has to
4731
+ find again in a config file or a CLI flag, and a screen that renames them makes the
4732
+ two impossible to line up.
4733
+
4734
+ `inactive_tier_reason` stays English for a harder reason: `run_contract` renders the
4735
+ same call, and a launch contract must not vary by UI language. It is a shared value,
4736
+ not chrome, so it is passed through rather than translated."""
4556
4737
  if plan is None:
4557
- return ["No setup selected."]
4738
+ return [t("setup.none")]
4558
4739
  if plan_projects_nothing(plan):
4559
4740
  return [
4560
- f"Host {plan['host']} | Preset {plan['label']}",
4561
- "Bare backend — no launch contract, no tier bindings, no review route,",
4562
- "no permission flag. The repo's own AGENTS.md/CLAUDE.md is all that applies.",
4741
+ t("setup.host").format(host=plan["host"], preset=plan["label"]),
4742
+ *t("setup.bare").split("\n"),
4563
4743
  ]
4564
4744
  host = plan["host"]
4565
4745
  execution = (
4566
- plan["codex_execution_policy"]
4567
- if host == "codex"
4568
- else plan["claude_permission_mode"]
4746
+ "restricted"
4747
+ if sweep_main(plan)
4748
+ else (
4749
+ plan["codex_execution_policy"]
4750
+ if host == "codex"
4751
+ else plan["claude_permission_mode"]
4752
+ )
4569
4753
  )
4570
4754
  lines = [
4571
- f"Host {host} | Preset {plan['label']}",
4755
+ t("setup.host").format(host=host, preset=plan["label"]),
4572
4756
  # A composable plan has no legacy name and printed the literal "None" here — the
4573
4757
  # same defect print_summary carried, in the one place a user reads before
4574
4758
  # launching. The legacy branch keeps emitting the raw setup value, byte for byte.
4575
- f"Main {plan['main_tier'].upper()} | Review "
4576
- f"{review_setup_label(plan) if plan.get('review_report') else plan['review_setup']}",
4577
- f"Delegation {'on' if plan['delegation'] else 'off'} | Execution {execution}",
4759
+ t("setup.main").format(
4760
+ tier=plan["main_tier"].upper(),
4761
+ review=(review_setup_label(plan) if plan.get("review_report")
4762
+ else plan["review_setup"]),
4763
+ ),
4764
+ t("setup.delegation").format(
4765
+ delegation="on" if plan["delegation"] else "off", execution=execution,
4766
+ ),
4578
4767
  ]
4768
+ if private_corpus_enabled():
4769
+ lines.append(
4770
+ setup_panel_column(t("setup.global-instructions.label"))
4771
+ + " "
4772
+ + t("global-instructions.summary").format(
4773
+ choice=t(
4774
+ "global-instructions.include.label"
4775
+ if plan.get("include_global_instructions", True)
4776
+ else "global-instructions.exclude.label"
4777
+ )
4778
+ )
4779
+ )
4579
4780
  for tier in active_tiers(plan):
4580
4781
  # The set run_contract, both argv builders and print_summary take. This panel
4581
4782
  # calls itself the "Current setup" and listed a binding row for every tier whatever
@@ -4584,10 +4785,10 @@ def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
4584
4785
  # TUI and the Custom hub actually show (round 22, #8). With delegation ON it still
4585
4786
  # printed a HELM row under a non-HELM main, which nothing binds (round 23, #1).
4586
4787
  binding = plan["tiers"][tier]
4587
- effort = (
4588
- plan["frontier_effort"] if tier == "frontier" else binding["effort"]
4788
+ lines.append(
4789
+ f"{setup_panel_column(tier.upper())} "
4790
+ f"{format_model_effort(binding['model'], tier_effort(plan, tier), ' · ')}"
4589
4791
  )
4590
- lines.append(f"{tier.upper():<10} {binding['model']} / {effort}")
4591
4792
  inactive = inactive_tiers(plan)
4592
4793
  if inactive:
4593
4794
  # Named rather than dropped, matching the contract's and the summary's wording, so
@@ -4595,8 +4796,11 @@ def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
4595
4796
  # rather than "Children" because under delegation-on the one inactive tier is HELM,
4596
4797
  # which is precisely not a child.
4597
4798
  lines.append(
4598
- f"{'Inactive':<10} {', '.join(inactive)} — inactive, not projected because "
4599
- f"{inactive_tier_reason(plan)}"
4799
+ setup_panel_column(t("setup.inactive.label"))
4800
+ + " "
4801
+ + t("setup.inactive.value").format(
4802
+ tiers=", ".join(inactive), reason=inactive_tier_reason(plan),
4803
+ )
4600
4804
  )
4601
4805
  return lines
4602
4806
 
@@ -4677,6 +4881,7 @@ def _build_app_class():
4677
4881
  from textual.screen import ModalScreen
4678
4882
  from textual.widgets import Input, OptionList, Static
4679
4883
  from textual.widgets.option_list import Option
4884
+ from rich.text import Text
4680
4885
  from textual.theme import Theme
4681
4886
 
4682
4887
  # Host-matched palettes so the preflight reads as the CLI it launches.
@@ -4723,6 +4928,12 @@ def _build_app_class():
4723
4928
  border: round $secondary; border-title-color: $secondary;
4724
4929
  border-title-style: bold; padding: 0 1; height: 5;
4725
4930
  }
4931
+ #al-detail-scroll {
4932
+ border: round $secondary; border-title-color: $secondary;
4933
+ border-title-style: bold; padding: 0 1;
4934
+ height: auto; min-height: 5; max-height: 45vh;
4935
+ }
4936
+ #al-detail-scroll > #al-detail { border: none; padding: 0; height: auto; }
4726
4937
  #al-corpus-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
4727
4938
  /* No cap and no scroller of its own: one nested scroll region inside another is a
4728
4939
  worse answer than a body that simply scrolls. */
@@ -4740,7 +4951,7 @@ def _build_app_class():
4740
4951
 
4741
4952
  def setup_panel(plan):
4742
4953
  panel = Static("\n".join(setup_summary_lines(plan)), id="al-setup")
4743
- panel.border_title = "Current setup"
4954
+ panel.border_title = t("tui.setup.title")
4744
4955
  release = version_label()
4745
4956
  if release:
4746
4957
  panel.border_subtitle = release
@@ -4751,12 +4962,23 @@ def _build_app_class():
4751
4962
  # on_mount focuses the option list, whose Up/Down are the only movement keys the
4752
4963
  # footer advertises, and the body holds no focusable widget of its own. A scrollbar
4753
4964
  # that exists numerically is not a way for a keyboard user to read anything.
4965
+ # One key, one meaning, across every screen: arrows navigate (left leaves this
4966
+ # one), Enter decides, Space changes something that is not yet decided, Escape
4967
+ # aborts. Escape used to mean "back" here, which made the abort key the same key
4968
+ # as the one that goes up a level — a screen you cannot leave without deciding
4969
+ # whether you are cancelling.
4754
4970
  BINDINGS = [
4755
- Binding("escape", "back", "back", priority=True),
4971
+ Binding("left", "back", "back", priority=True),
4972
+ Binding("space", "pick", "pick", priority=True),
4973
+ Binding("escape", "cancel", "cancel", priority=True),
4756
4974
  Binding("q", "cancel", "cancel", priority=True),
4757
4975
  Binding("ctrl+c", "cancel", "cancel", priority=True),
4758
4976
  Binding("pagedown", "body_down", "scroll", priority=True),
4759
4977
  Binding("pageup", "body_up", "scroll", priority=True),
4978
+ Binding("shift+pagedown", "detail_down", "details", priority=True),
4979
+ Binding("shift+pageup", "detail_up", "details", priority=True),
4980
+ Binding("j", "detail_down", "details", priority=True),
4981
+ Binding("k", "detail_up", "details", priority=True),
4760
4982
  ]
4761
4983
 
4762
4984
  def _body(self):
@@ -4773,8 +4995,33 @@ def _build_app_class():
4773
4995
  if body is not None:
4774
4996
  body.scroll_page_up(animate=False)
4775
4997
 
4998
+ def action_detail_down(self):
4999
+ self.query_one("#al-detail-scroll", VerticalScroll).scroll_page_down(animate=False)
5000
+
5001
+ def action_detail_up(self):
5002
+ self.query_one("#al-detail-scroll", VerticalScroll).scroll_page_up(animate=False)
5003
+
5004
+ def _size_detail(self):
5005
+ # A queued refresh can run after this screen has been dismissed and its
5006
+ # children removed. Only the active, mounted screen owns layout work.
5007
+ if not self.is_mounted or self.app.screen is not self:
5008
+ return
5009
+ # Keep navigation and a readable reference viewport available even when
5010
+ # a long menu and a wrapped explanation compete for a short terminal.
5011
+ reserved = 3 + sum(
5012
+ self.query_one(selector).outer_size.height
5013
+ for selector in ("#al-title", "#al-hdr", "#al-footer", "OptionList")
5014
+ )
5015
+ self.query_one("#al-detail-scroll").styles.max_height = max(
5016
+ 5, min(int(self.size.height * 0.45), self.size.height - reserved),
5017
+ )
5018
+
5019
+ def on_resize(self):
5020
+ self.call_after_refresh(self._size_detail)
5021
+
4776
5022
  def __init__(
4777
- self, title, options, default, allow_back, plan, preview=None, corpus=None
5023
+ self, title, options, default, allow_back, plan, preview=None, corpus=None,
5024
+ confirm=None,
4778
5025
  ):
4779
5026
  super().__init__()
4780
5027
  self._title = title
@@ -4784,6 +5031,10 @@ def _build_app_class():
4784
5031
  self._plan = plan
4785
5032
  self._preview = preview
4786
5033
  self._corpus = corpus
5034
+ # The value Enter decides on, for a screen whose rows are changes rather than
5035
+ # choices. Without it Enter and Space would both mean "act on the highlighted
5036
+ # row", and a checklist would have no key that means "I am done".
5037
+ self._confirm = confirm
4787
5038
 
4788
5039
  def compose(self):
4789
5040
  yield Static(self._title, id="al-title")
@@ -4794,17 +5045,28 @@ def _build_app_class():
4794
5045
  with VerticalScroll(id=BODY_PANEL_ID):
4795
5046
  yield setup_panel(self._plan)
4796
5047
  if self._corpus:
4797
- yield Static("Corpus status", id="al-corpus-title")
5048
+ yield Static(t("tui.corpus.title"), id="al-corpus-title")
4798
5049
  yield Static("\n".join(self._corpus), id=CORPUS_PANEL_ID)
4799
- detail = Static("", id="al-detail")
4800
- detail.border_title = "About highlighted option"
5050
+ detail = VerticalScroll(Static("", id="al-detail", markup=False), id="al-detail-scroll")
5051
+ detail.border_title = t("tui.detail.title")
5052
+ detail.border_subtitle = t("tui.detail.scroll")
4801
5053
  yield detail
4802
5054
  yield Static(
4803
- f"Options (1-{len(self._options)} of {len(self._options)})", id="al-hdr"
5055
+ t("tui.options.header").format(count=len(self._options)), id="al-hdr"
4804
5056
  )
4805
5057
  option_list = OptionList()
4806
5058
  for option in self._options:
4807
- label = option.label + ("" if option.enabled else " [unavailable]")
5059
+ # A str prompt is parsed as markup, which eats any bracketed run that
5060
+ # looks like a tag: `[x]` vanished while `[ ]` survived, so a selected
5061
+ # row rendered blank and only a deselected one showed its box, and
5062
+ # `[unavailable]` never reached the screen at all. A Text is rendered
5063
+ # as written, and option labels carry manifest-supplied names this
5064
+ # module does not control.
5065
+ label = option.label
5066
+ if not isinstance(label, Text):
5067
+ label = Text(label)
5068
+ if not option.enabled:
5069
+ label = label + f" [{t('prompt.unavailable').lower()}]"
4808
5070
  option_list.add_option(
4809
5071
  Option(label, id=option.value, disabled=not option.enabled)
4810
5072
  )
@@ -4812,9 +5074,9 @@ def _build_app_class():
4812
5074
  # The panel keys are advertised only when there is a panel: a footer naming a
4813
5075
  # key that does nothing teaches the same wrong thing a screen stating an
4814
5076
  # unenforced rule does.
4815
- footer = "Up/Down move | Enter select | PgUp/PgDn scroll | "
4816
- footer += "Esc back | q cancel" if self._allow_back else "Esc cancel | q cancel"
4817
- yield Static(footer, id="al-footer")
5077
+ yield Static(
5078
+ menu_footer(self._allow_back, self._confirm is not None), id="al-footer"
5079
+ )
4818
5080
 
4819
5081
  def on_mount(self):
4820
5082
  option_list = self.query_one(OptionList)
@@ -4844,8 +5106,10 @@ def _build_app_class():
4844
5106
  option = self._options[index]
4845
5107
  detail = option.description
4846
5108
  if not option.enabled and option.unavailable_reason:
4847
- detail = f"{detail} Unavailable: {option.unavailable_reason}"
5109
+ detail = f"{detail} {t('prompt.unavailable')}: {option.unavailable_reason}"
4848
5110
  self.query_one("#al-detail", Static).update(detail)
5111
+ self.query_one("#al-detail-scroll", VerticalScroll).scroll_home(animate=False)
5112
+ self.call_after_refresh(self._size_detail)
4849
5113
  # Live-preview the highlighted option's effect in the setup panel.
4850
5114
  if self._preview is not None:
4851
5115
  try:
@@ -4857,7 +5121,25 @@ def _build_app_class():
4857
5121
  pass
4858
5122
 
4859
5123
  def on_option_list_option_selected(self, event):
4860
- self.dismiss(event.option.id)
5124
+ # Enter. On a screen with a confirm target that target is the decision, so a
5125
+ # row under the cursor is not what Enter acts on — Space is.
5126
+ if self._confirm is None:
5127
+ self.dismiss(event.option.id)
5128
+ return
5129
+ if any(
5130
+ option.value == self._confirm and option.enabled
5131
+ for option in self._options
5132
+ ):
5133
+ self.dismiss(self._confirm)
5134
+
5135
+ def action_pick(self):
5136
+ option_list = self.query_one(OptionList)
5137
+ index = option_list.highlighted
5138
+ if index is None:
5139
+ return
5140
+ option = self._options[index]
5141
+ if option.enabled and option.value != self._confirm:
5142
+ self.dismiss(option.value)
4861
5143
 
4862
5144
  def action_back(self):
4863
5145
  self.dismiss(_UI_BACK if self._allow_back else _UI_CANCEL)
@@ -4881,8 +5163,8 @@ def _build_app_class():
4881
5163
  yield Static(self._label, id="al-title")
4882
5164
  yield setup_panel(self._plan)
4883
5165
  box = Static(
4884
- f"Current value: {self._default}\n"
4885
- "New value (leave blank to keep current):",
5166
+ t("tui.input.current").format(value=self._default)
5167
+ + "\n" + t("tui.input.new"),
4886
5168
  id="al-detail",
4887
5169
  )
4888
5170
  # The screen already knows what it is asking for; it labelled every
@@ -4891,10 +5173,8 @@ def _build_app_class():
4891
5173
  box.border_title = self._label
4892
5174
  yield box
4893
5175
  yield Static("", id="al-hdr")
4894
- yield Input(placeholder="leave blank to keep the current value")
4895
- yield Static(
4896
- "Enter confirm | Esc/Ctrl-C cancel | q + Enter cancel", id="al-footer"
4897
- )
5176
+ yield Input(placeholder=t("tui.input.placeholder"))
5177
+ yield Static(t("tui.input.footer"), id="al-footer")
4898
5178
 
4899
5179
  def on_mount(self):
4900
5180
  self.query_one(Input).focus()
@@ -4971,11 +5251,13 @@ class TextualUI:
4971
5251
  allow_back: bool,
4972
5252
  preview=None,
4973
5253
  corpus_lines: list[str] | None = None,
5254
+ confirm: str | None = None,
4974
5255
  ) -> str:
4975
5256
  result = self.app.call_from_thread(
4976
5257
  self.app.push_screen_wait,
4977
5258
  self._menu_screen(
4978
- title, options, default, allow_back, self.plan, preview, corpus_lines
5259
+ title, options, default, allow_back, self.plan, preview, corpus_lines,
5260
+ confirm,
4979
5261
  ),
4980
5262
  )
4981
5263
  if result == _UI_BACK:
@@ -5011,6 +5293,29 @@ def run_textual_flow(
5011
5293
  return value
5012
5294
 
5013
5295
 
5296
+ def menu_footer(allow_back: bool, confirm: bool) -> str:
5297
+ """The key line under a menu, in the active language.
5298
+
5299
+ Built here rather than inline in the screen because two other things read it: the
5300
+ picker scenarios use it to tell WHICH screen is drawn, and a leg asserts the forms
5301
+ stay mutually exclusive and fit the terminal. A gate holding its own copy of this
5302
+ string is a second authority that drifts; asking the launcher is not.
5303
+
5304
+ The key NAMES are not translated — you press Enter, not 입력 — so each catalog entry
5305
+ is a key name and a verb, and only the verb moves."""
5306
+ parts = [t("tui.key.move"), t("tui.key.apply") if confirm else t("tui.key.select")]
5307
+ if confirm:
5308
+ parts.append(t("tui.key.toggle"))
5309
+ parts.append(t("tui.key.scroll"))
5310
+ if allow_back:
5311
+ parts.append(t("tui.key.back"))
5312
+ # `q` stays advertised because `q` stays bound: dropping the notice for a key that
5313
+ # still works teaches the same wrong thing as naming one that does not. The numbered
5314
+ # renderer names the same key from the same entry.
5315
+ parts += [t("tui.key.cancel"), t("prompt.cancel")]
5316
+ return " | ".join(parts)
5317
+
5318
+
5014
5319
  def choose_lines(
5015
5320
  title: str,
5016
5321
  options: list[MenuOption],
@@ -5029,7 +5334,10 @@ def choose_lines(
5029
5334
  marker = "" if option.enabled else f" [{unavailable.lower()}]"
5030
5335
  selected = " *" if option.value == default and option.enabled else ""
5031
5336
  label = option.label
5032
- print(f" {index}. {label}{marker}{selected} - {option.description}")
5337
+ description = option.description.splitlines() or [""]
5338
+ print(f" {index}. {label}{marker}{selected} - {description[0]}")
5339
+ for line in description[1:]:
5340
+ print(f" {line}")
5033
5341
  if not option.enabled:
5034
5342
  print(f" {unavailable}: {option.unavailable_reason}")
5035
5343
  while True:
@@ -5065,11 +5373,14 @@ def choose(
5065
5373
  allow_back: bool = False,
5066
5374
  preview=None,
5067
5375
  corpus_lines: list[str] | None = None,
5376
+ confirm: str | None = None,
5068
5377
  ) -> str:
5069
5378
  if not any(option.enabled for option in options):
5070
5379
  raise LaunchError(f"no available options for {title}")
5071
5380
  if ui is not None:
5072
- return ui.choose(title, options, default, allow_back, preview, corpus_lines)
5381
+ return ui.choose(
5382
+ title, options, default, allow_back, preview, corpus_lines, confirm
5383
+ )
5073
5384
  return choose_lines(title, options, default, allow_back, corpus_lines)
5074
5385
 
5075
5386
 
@@ -5085,7 +5396,9 @@ def read_input(prompt: str) -> str:
5085
5396
  def prompt_text(label: str, default: str, ui: TextualUI | None = None) -> str:
5086
5397
  if ui is not None:
5087
5398
  return ui.prompt_text(label, default)
5088
- value = read_input(f"{label} [{default}] (q cancel): ").strip()
5399
+ value = read_input(
5400
+ t("prompt.text").format(label=label, default=default, cancel=t("prompt.cancel"))
5401
+ ).strip()
5089
5402
  if value.lower() == "q":
5090
5403
  raise KeyboardInterrupt
5091
5404
  return value or default
@@ -5107,7 +5420,7 @@ def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> lis
5107
5420
 
5108
5421
  def resolve_review_for_plan(
5109
5422
  review: ReviewPlan, config: dict[str, Any], host: str, main_model: str,
5110
- main_effort: str, context: str, criterion: bool = False,
5423
+ main_effort: str | None, context: str, criterion: bool = False,
5111
5424
  ) -> tuple[dict, "ReviewReport | None"]:
5112
5425
  """(method registry, resolved report) for a review against the main seat.
5113
5426
 
@@ -5311,13 +5624,21 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5311
5624
  override_model = effective_tier_model(config, host, tier, all_overrides)
5312
5625
  if not isinstance(override_model, str) or not override_model:
5313
5626
  raise LaunchError(f"invalid override model: {preset_name}.tier_overrides.{host}.{tier}")
5314
- override_effort = override.get("effort", tiers[tier]["effort"])
5627
+ # Haiku has no configurable effort. A model override to it must therefore
5628
+ # clear the inherited tier setting rather than preserve an invalid value.
5629
+ override_effort = (
5630
+ override.get("effort")
5631
+ if "effort" in override
5632
+ else (tiers[tier].get("effort") if model_requires_effort(host, override_model) else None)
5633
+ )
5315
5634
  validate_effort(host, override_model, override_effort, f"{preset_name}.tier_overrides.{host}.{tier}")
5316
- tiers[tier] = {"model": override_model, "effort": override_effort}
5635
+ tiers[tier] = {"model": override_model}
5636
+ if override_effort is not None:
5637
+ tiers[tier]["effort"] = override_effort
5317
5638
  main_tier = preset.get("main_tier")
5318
5639
  if not isinstance(main_tier, str) or main_tier not in tiers:
5319
5640
  raise LaunchError(f"invalid main_tier in preset {preset_name}: {main_tier}")
5320
- authored_frontier_effort = preset.get("frontier_effort", tiers["frontier"]["effort"])
5641
+ authored_frontier_effort = preset.get("frontier_effort", tiers["frontier"].get("effort"))
5321
5642
  frontier_effort = authored_frontier_effort
5322
5643
  if isinstance(frontier_effort, dict):
5323
5644
  frontier_effort = frontier_effort.get(host)
@@ -5376,11 +5697,30 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5376
5697
  f"tier_overrides.{other}.frontier.effort={authored_override!r}; FRONTIER's "
5377
5698
  "effort has one value — remove one of them"
5378
5699
  )
5379
- tiers["frontier"] = {**tiers["frontier"], "effort": frontier_effort}
5380
- delegation = preset.get("delegation", True)
5381
- if not isinstance(delegation, bool):
5700
+ tiers["frontier"] = {**tiers["frontier"]}
5701
+ if frontier_effort is None:
5702
+ tiers["frontier"].pop("effort", None)
5703
+ else:
5704
+ tiers["frontier"]["effort"] = frontier_effort
5705
+ delegation_requested = preset.get("delegation", True)
5706
+ if not isinstance(delegation_requested, bool):
5382
5707
  raise LaunchError(f"delegation must be boolean in preset {preset_name}")
5708
+ # SWEEP's contract is one explicit read-only rule per item. Child delegation
5709
+ # would hand that main a writable escape through another tier, so its
5710
+ # projection is deliberately single-seat whatever the preset requested.
5711
+ delegation = delegation_requested and main_tier != "sweep"
5712
+ include_global_instructions = preset.get("include_global_instructions", True)
5713
+ if not isinstance(include_global_instructions, bool):
5714
+ raise LaunchError(
5715
+ f"include_global_instructions must be boolean in preset {preset_name}"
5716
+ )
5383
5717
  review = read_review(preset, preset_name, config, host)
5718
+ if main_tier == "sweep" and review_is_requested(review):
5719
+ raise LaunchError(
5720
+ f"presets.{preset_name} requests review, but SWEEP main exposes only its "
5721
+ "read-only one-rule-per-item surface and cannot dispatch a reviewer. Turn "
5722
+ "review off (the Solo setup) or choose HELM or WORKHORSE as main."
5723
+ )
5384
5724
  review_arms = read_review_arms(preset)
5385
5725
  # Authoring [review] IS the opt-in. Shipped presets stay on review_setup, so the
5386
5726
  # default launch is byte-identical; only a preset that asks for the composable
@@ -5390,7 +5730,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5390
5730
  # Resolved HERE, not at render time: a composable preset whose base panel has no
5391
5731
  # isolated mechanism is an invalid launch, and that has to fail while the plan is
5392
5732
  # being built rather than halfway through printing a contract.
5393
- main_effort = frontier_effort if main_tier == "frontier" else tiers[main_tier]["effort"]
5733
+ main_effort = frontier_effort if main_tier == "frontier" else tiers[main_tier].get("effort")
5394
5734
  # The criterion-discipline toggle. A BOOLEAN, deliberately: the criterion itself is
5395
5735
  # per-review and rides the packet, which packet_sha256 binds — a preset carrying its
5396
5736
  # content would put per-review text into the per-launch contract the golden pins.
@@ -5469,6 +5809,13 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5469
5809
  "review_report": review_report,
5470
5810
  "review_methods": review_methods,
5471
5811
  "delegation": delegation,
5812
+ # The persistent user choice, kept distinct from SWEEP's derived runtime
5813
+ # restriction so Save As and a later move back to another main do not
5814
+ # silently turn a requested fan-out off forever.
5815
+ "delegation_requested": delegation_requested,
5816
+ # The execution boundary decides whether a host can honour false. Keeping the
5817
+ # authored value here lets Custom repair an old unsupported saved choice first.
5818
+ "include_global_instructions": include_global_instructions,
5472
5819
  "codex_execution_policy": codex_policy,
5473
5820
  "claude_permission_mode": claude_policy,
5474
5821
  "tiers": tiers,
@@ -5505,7 +5852,10 @@ AUTHOR_COMPOSABLE = "__author_composable__"
5505
5852
  def review_binding_label(binding: "ReviewBinding | None") -> str:
5506
5853
  if binding is None:
5507
5854
  return "not set"
5508
- seat = f"tier {binding.tier}" if binding.tier else f"{binding.model}/{binding.effort}"
5855
+ seat = (
5856
+ f"tier {binding.tier}"
5857
+ if binding.tier else format_model_effort(binding.model, binding.effort)
5858
+ )
5509
5859
  return f"{binding.provider} · {seat}"
5510
5860
 
5511
5861
 
@@ -5730,15 +6080,15 @@ def choose_review_binding(
5730
6080
  seats = [
5731
6081
  MenuOption(
5732
6082
  f"tier:{tier}",
5733
- f"{tier.upper()}: {tiers[tier]['model']} / {tiers[tier]['effort']}",
5734
- f"Bind to the {host} {tier.upper()} tier; model and effort move together.",
6083
+ f"{tier.upper()}: {format_model_effort(tiers[tier]['model'], tiers[tier].get('effort'), ' / ')}",
6084
+ f"Bind to the {host} {tier.upper()} tier; its model and supported effort setting move together.",
5735
6085
  )
5736
6086
  for tier in TIER_ORDER
5737
6087
  if isinstance(tiers.get(tier), dict)
5738
6088
  ]
5739
6089
  seats.append(
5740
- MenuOption(OTHER_MODEL, "Custom model and effort",
5741
- "Name the exact model, then its reasoning effort.")
6090
+ MenuOption(OTHER_MODEL, "Custom model (and effort when supported)",
6091
+ "Name the exact model, then choose its reasoning effort when it supports one.")
5742
6092
  )
5743
6093
  default_seat = (
5744
6094
  f"tier:{current.tier}"
@@ -5753,15 +6103,20 @@ def choose_review_binding(
5753
6103
  model = prompt_text(
5754
6104
  f"{title} — model", current.model if current is not None else "", ui
5755
6105
  )
5756
- effort = choose(
5757
- f"{title} — effort", effort_options(host, model),
5758
- # HOST_EFFORTS values are SETS: indexing one raised TypeError before
5759
- # the effort picker drew. EFFORT_ORDER gives a deterministic first
5760
- # supported effort, where a set gives none at all.
5761
- current.effort if current is not None else host_default_effort(host), ui,
5762
- allow_back=True,
5763
- )
5764
- raw = {"provider": provider, "model": model, "effort": effort}
6106
+ if model_requires_effort(host, model):
6107
+ effort = choose(
6108
+ f"{title} — effort", effort_options(host, model),
6109
+ # HOST_EFFORTS values are SETS: indexing one raised TypeError before
6110
+ # the effort picker drew. EFFORT_ORDER gives a deterministic first
6111
+ # supported effort, where a set gives none at all. A previous no-effort
6112
+ # model starts at the supported default rather than preserving absence.
6113
+ (current.effort if current is not None and current.effort is not None
6114
+ else host_default_effort(host)), ui,
6115
+ allow_back=True,
6116
+ )
6117
+ raw = {"provider": provider, "model": model, "effort": effort}
6118
+ else:
6119
+ raw = {"provider": provider, "model": model}
5765
6120
  return parse_review_binding(raw, config, f"custom.{title}")
5766
6121
 
5767
6122
 
@@ -6528,6 +6883,10 @@ def host_default_effort(host: str) -> str:
6528
6883
 
6529
6884
 
6530
6885
  def effort_options(host: str, model: str) -> list[MenuOption]:
6886
+ if not model_requires_effort(host, model):
6887
+ # The caller skips this menu entirely. Returning no choices makes a direct
6888
+ # consumer unable to turn absence into a deceptive selected value.
6889
+ return []
6531
6890
  descriptions = effort_descriptions()
6532
6891
  options = []
6533
6892
  for effort in EFFORT_ORDER:
@@ -6579,7 +6938,8 @@ def review_binding_fields(binding: ReviewBinding) -> dict[str, str]:
6579
6938
  fields["tier"] = binding.tier
6580
6939
  else:
6581
6940
  fields["model"] = binding.model
6582
- fields["effort"] = binding.effort
6941
+ if binding.effort is not None:
6942
+ fields["effort"] = binding.effort
6583
6943
  if binding.service_tier != DEFAULT_SERVICE_TIER:
6584
6944
  fields["service_tier"] = binding.service_tier
6585
6945
  return fields
@@ -6659,10 +7019,14 @@ def preset_from_plan(
6659
7019
  # hub calls "Save these settings".
6660
7020
  "mode": preset_mode(plan),
6661
7021
  "main_tier": plan["main_tier"],
6662
- "delegation": plan["delegation"],
7022
+ "delegation": plan.get("delegation_requested", plan["delegation"]),
6663
7023
  "codex_execution_policy": plan["codex_execution_policy"],
6664
7024
  "claude_permission_mode": plan["claude_permission_mode"],
6665
7025
  }
7026
+ # True is the schema default, so existing saved presets stay byte-identical. False
7027
+ # changes native instruction scope and must survive Save As.
7028
+ if not plan.get("include_global_instructions", True):
7029
+ fields["include_global_instructions"] = False
6666
7030
  if plan.get("criterion"):
6667
7031
  # Save As from a criterion-toggled plan silently dropped the discipline: the
6668
7032
  # routed-name guard covers same-name shadowing, not a fresh name, and the
@@ -6687,8 +7051,12 @@ def preset_from_plan(
6687
7051
  override: dict[str, str] = {}
6688
7052
  if plan["tiers"][tier]["model"] != default_tiers[tier]["model"]:
6689
7053
  override["model"] = plan["tiers"][tier]["model"]
6690
- if tier_effort(plan, tier) != default_tiers[tier]["effort"]:
6691
- override["effort"] = tier_effort(plan, tier)
7054
+ effort = tier_effort(plan, tier)
7055
+ default_effort = default_tiers[tier].get("effort")
7056
+ # Omission is the only valid Haiku representation. A supporting model
7057
+ # remains subject to validate_effort before this save path runs.
7058
+ if effort is not None and effort != default_effort:
7059
+ override["effort"] = effort
6692
7060
  if override:
6693
7061
  overrides[tier] = override
6694
7062
  # The OTHER host's overrides are carried, not re-derived. "Scoped to the plan's host"
@@ -6760,6 +7128,15 @@ def preset_from_plan(
6760
7128
  effort = authored_frontier[other]
6761
7129
  else:
6762
7130
  effort = authored_frontier
7131
+ other_frontier_model = effective_tier_model(
7132
+ config, other, "frontier", plan.get("tier_overrides", {})
7133
+ )
7134
+ if effort is None and isinstance(other_frontier_model, str) and not model_requires_effort(
7135
+ other, other_frontier_model
7136
+ ):
7137
+ # The only no-effort model has no serializable top-level effort
7138
+ # value. Its tier override already carries the model selection.
7139
+ continue
6763
7140
  if not isinstance(effort, str) or not effort:
6764
7141
  # NAMED, not skipped. `build_plan` refuses such a profile now, so this is
6765
7142
  # the door for a plan assembled some other way — and the alternative here
@@ -7236,9 +7613,8 @@ def save_preset(
7236
7613
  # that host nothing.
7237
7614
  continue
7238
7615
  try:
7239
- projected = project_args(
7240
- build_plan(rebuilt, host, name), materialize_agents=False
7241
- )
7616
+ rebuilt_plan = build_plan(rebuilt, host, name)
7617
+ projected = project_args(rebuilt_plan, materialize_agents=False)
7242
7618
  except LaunchError as exc:
7243
7619
  raise LaunchError(
7244
7620
  f"saving {name!r} would write a preset that no longer builds on "
@@ -7262,6 +7638,13 @@ def save_preset(
7262
7638
  f"{_at(intended, where)} ({len(intended)} argument(s)); nothing was "
7263
7639
  f"written"
7264
7640
  )
7641
+ if rebuilt_plan["include_global_instructions"] != plan.get(
7642
+ "include_global_instructions", True
7643
+ ):
7644
+ raise LaunchError(
7645
+ f"saving {name!r} would write a different global instruction "
7646
+ "file setting; nothing was written"
7647
+ )
7265
7648
  # Through the shared primitive, which is where the temporary's removal on
7266
7649
  # failure lives. This site had the same two lines and no cleanup, so an
7267
7650
  # `os.replace` that failed left the complete candidate sitting beside the
@@ -7278,9 +7661,23 @@ def customize(
7278
7661
  ui: TextualUI | None = None,
7279
7662
  ) -> None:
7280
7663
  descriptions = tier_descriptions()
7281
- tier_options = [
7282
- MenuOption(tier, tier.upper(), descriptions[tier]) for tier in TIER_ORDER
7283
- ]
7664
+ sweep_review_reason = (
7665
+ "SWEEP main is read-only and cannot dispatch review. Turn review off or choose "
7666
+ "HELM or WORKHORSE."
7667
+ )
7668
+
7669
+ def tier_options() -> list[MenuOption]:
7670
+ review_requested = review_is_requested(plan["review_plan"])
7671
+ return [
7672
+ MenuOption(
7673
+ tier,
7674
+ tier.upper(),
7675
+ descriptions[tier],
7676
+ enabled=not (tier == "sweep" and review_requested),
7677
+ unavailable_reason=sweep_review_reason if tier == "sweep" and review_requested else "",
7678
+ )
7679
+ for tier in TIER_ORDER
7680
+ ]
7284
7681
  available_routes = route_availability(plan)
7285
7682
  cross = plan.get("review_family", "cross") == "cross"
7286
7683
  review_options = []
@@ -7386,6 +7783,17 @@ def customize(
7386
7783
  t("custom.policy.label").format(policy=policy_label),
7387
7784
  t("custom.policy.description"),
7388
7785
  ),
7786
+ MenuOption(
7787
+ "global-instructions",
7788
+ t("custom.global-instructions.label").format(
7789
+ choice=t(
7790
+ "global-instructions.include.label"
7791
+ if plan.get("include_global_instructions", True)
7792
+ else "global-instructions.exclude.label"
7793
+ )
7794
+ ),
7795
+ t("custom.global-instructions.description"),
7796
+ ),
7389
7797
  ]
7390
7798
  for tier in TIER_ORDER:
7391
7799
  binding = plan["tiers"][tier]
@@ -7394,7 +7802,8 @@ def customize(
7394
7802
  f"tier:{tier}",
7395
7803
  # Model and effort only — the row is data, and the tier name it
7396
7804
  # leads with is an identifier the contract uses, not UI text.
7397
- f"{tier.upper()}: {binding['model']} / {tier_effort(plan, tier)}",
7805
+ f"{tier.upper()}: "
7806
+ f"{format_model_effort(binding['model'], tier_effort(plan, tier), ' / ')}",
7398
7807
  t("custom.tier.description").format(tier=tier.upper()),
7399
7808
  )
7400
7809
  )
@@ -7432,29 +7841,61 @@ def customize(
7432
7841
  if action == "main":
7433
7842
  plan["main_tier"] = choose(
7434
7843
  t("tier.title"),
7435
- tier_options,
7844
+ tier_options(),
7436
7845
  plan["main_tier"],
7437
7846
  ui,
7438
7847
  allow_back=True,
7439
- preview=lambda value: {**plan, "main_tier": value},
7848
+ preview=lambda value: {
7849
+ **plan,
7850
+ "main_tier": value,
7851
+ "delegation": (
7852
+ plan.get("delegation_requested", plan["delegation"])
7853
+ and value != "sweep"
7854
+ ),
7855
+ },
7856
+ )
7857
+ plan["delegation"] = (
7858
+ plan.get("delegation_requested", plan["delegation"])
7859
+ and not sweep_main(plan)
7440
7860
  )
7441
7861
  reseat_review(plan, config)
7442
7862
  elif action == "review":
7443
7863
  if plan["review_plan"].source == "composable":
7864
+ if sweep_main(plan):
7865
+ raise LaunchError(sweep_review_reason)
7444
7866
  review_editor(plan, config, ui, config_path)
7445
7867
  else:
7446
- chosen = choose(
7447
- "Review setup",
7448
- review_options
7449
- + [
7868
+ available_review_options = review_options
7869
+ composer_option = MenuOption(
7870
+ AUTHOR_COMPOSABLE,
7871
+ "Compose review (explicit bindings)…",
7872
+ "Author the base panel and each method's exact "
7873
+ "provider/model/effort instead of picking a combination "
7874
+ f"name. {REVIEW_RECOMMENDATION}",
7875
+ )
7876
+ if sweep_main(plan):
7877
+ available_review_options = [
7450
7878
  MenuOption(
7451
- AUTHOR_COMPOSABLE,
7452
- "Compose review (explicit bindings)…",
7453
- "Author the base panel and each method's exact "
7454
- "provider/model/effort instead of picking a combination "
7455
- f"name. {REVIEW_RECOMMENDATION}",
7879
+ option.value,
7880
+ option.label,
7881
+ option.description,
7882
+ enabled=option.value == "none",
7883
+ unavailable_reason=(
7884
+ "" if option.value == "none" else sweep_review_reason
7885
+ ),
7456
7886
  )
7457
- ],
7887
+ for option in review_options
7888
+ ]
7889
+ composer_option = MenuOption(
7890
+ composer_option.value,
7891
+ composer_option.label,
7892
+ composer_option.description,
7893
+ enabled=False,
7894
+ unavailable_reason=sweep_review_reason,
7895
+ )
7896
+ chosen = choose(
7897
+ "Review setup",
7898
+ available_review_options + [composer_option],
7458
7899
  plan["review_setup"],
7459
7900
  ui,
7460
7901
  allow_back=True,
@@ -7488,12 +7929,47 @@ def customize(
7488
7929
  allow_back=True,
7489
7930
  preview=lambda value: {**plan, policy_field: value},
7490
7931
  )
7932
+ elif action == "global-instructions":
7933
+ exclude_available = private_corpus_enabled() and plan["host"] == "claude"
7934
+ exclude_reason = (
7935
+ t("global-instructions.exclude.codex-unavailable")
7936
+ if plan["host"] == "codex"
7937
+ else t("global-instructions.exclude.private-unavailable")
7938
+ )
7939
+ choice = choose(
7940
+ t("global-instructions.title"),
7941
+ [
7942
+ MenuOption(
7943
+ "include",
7944
+ t("global-instructions.include.label"),
7945
+ t("global-instructions.include.description"),
7946
+ ),
7947
+ MenuOption(
7948
+ "exclude",
7949
+ t("global-instructions.exclude.label"),
7950
+ t("global-instructions.exclude.description"),
7951
+ enabled=exclude_available,
7952
+ unavailable_reason=exclude_reason,
7953
+ ),
7954
+ ],
7955
+ "include" if plan.get("include_global_instructions", True) else "exclude",
7956
+ ui,
7957
+ allow_back=True,
7958
+ preview=lambda value: {
7959
+ **plan, "include_global_instructions": value == "include"
7960
+ },
7961
+ )
7962
+ plan["include_global_instructions"] = choice == "include"
7491
7963
  except BackRequested:
7492
7964
  continue
7493
7965
 
7494
7966
  if action.startswith("tier:"):
7495
7967
  tier = action.split(":", 1)[1]
7496
7968
  binding = plan["tiers"][tier]
7969
+ configured_default_effort = (
7970
+ config.get("hosts", {}).get(plan["host"], {}).get("tiers", {})
7971
+ .get(tier, {}).get("effort")
7972
+ )
7497
7973
  # `binding` IS the plan's dict, so every assignment below lands on the live
7498
7974
  # plan the moment it is made. Choosing a model and then backing out of the
7499
7975
  # effort screen — which loops to the model screen — and backing out again left
@@ -7501,6 +7977,7 @@ def customize(
7501
7977
  # snapshot is what Escape restores; the plan is only allowed to keep an edit
7502
7978
  # that reached the end of the edit.
7503
7979
  original_binding = dict(binding)
7980
+ original_frontier_effort = plan["frontier_effort"] if tier == "frontier" else None
7504
7981
  cancelled = False
7505
7982
  while True:
7506
7983
  default_model = (
@@ -7519,12 +7996,11 @@ def customize(
7519
7996
  **plan,
7520
7997
  "tiers": {
7521
7998
  **plan["tiers"],
7522
- tier: {
7523
- **binding,
7524
- "model": binding["model"]
7525
- if value == OTHER_MODEL
7526
- else value,
7527
- },
7999
+ tier: binding_for_selected_model(
8000
+ plan["host"], binding,
8001
+ binding["model"] if value == OTHER_MODEL else value,
8002
+ default_effort=configured_default_effort,
8003
+ ),
7528
8004
  },
7529
8005
  },
7530
8006
  )
@@ -7537,27 +8013,46 @@ def customize(
7537
8013
  )
7538
8014
  else:
7539
8015
  binding["model"] = chosen_model
7540
- current_effort = tier_effort(plan, tier)
7541
- try:
7542
- binding["effort"] = choose(
7543
- t("effort.title").format(tier=tier.upper()),
7544
- effort_options(plan["host"], binding["model"]),
7545
- current_effort,
7546
- ui,
7547
- allow_back=True,
7548
- preview=lambda value: {
7549
- **plan,
7550
- "frontier_effort": value
7551
- if tier == "frontier"
7552
- else plan["frontier_effort"],
7553
- "tiers": {
7554
- **plan["tiers"],
7555
- tier: {**binding, "effort": value},
8016
+ selected_binding = binding_for_selected_model(
8017
+ plan["host"], binding, binding["model"],
8018
+ default_effort=configured_default_effort,
8019
+ )
8020
+ binding.clear()
8021
+ binding.update(selected_binding)
8022
+ if tier == "frontier":
8023
+ # `tier_effort` deliberately projects the FRONTIER scalar.
8024
+ # Keep it coherent before the effort menu asks for its default
8025
+ # and before that menu previews an edited plan.
8026
+ plan["frontier_effort"] = binding.get("effort")
8027
+ if model_requires_effort(plan["host"], binding["model"]):
8028
+ # `binding_for_selected_model` supplies a default when the prior
8029
+ # model had no effort; supporting-to-supporting moves retain their
8030
+ # explicit chosen effort as before.
8031
+ current_effort = binding["effort"]
8032
+ try:
8033
+ binding["effort"] = choose(
8034
+ t("effort.title").format(tier=tier.upper()),
8035
+ effort_options(plan["host"], binding["model"]),
8036
+ current_effort,
8037
+ ui,
8038
+ allow_back=True,
8039
+ preview=lambda value: {
8040
+ **plan,
8041
+ "frontier_effort": value
8042
+ if tier == "frontier"
8043
+ else plan["frontier_effort"],
8044
+ "tiers": {
8045
+ **plan["tiers"],
8046
+ tier: {**binding, "effort": value},
8047
+ },
7556
8048
  },
7557
- },
7558
- )
7559
- except BackRequested:
7560
- continue
8049
+ )
8050
+ except BackRequested:
8051
+ continue
8052
+ else:
8053
+ # Claude Haiku 4.5 exposes no effort selector. Delete rather
8054
+ # than store a sentinel, so Save As emits no TOML effort key.
8055
+ binding.pop("effort", None)
7561
8056
  break
7562
8057
  # Only when the edit was actually completed. FRONTIER's effort has two homes —
7563
8058
  # the tier binding and `plan["frontier_effort"]`, which is the preset's
@@ -7568,9 +8063,11 @@ def customize(
7568
8063
  if cancelled:
7569
8064
  binding.clear()
7570
8065
  binding.update(original_binding)
8066
+ if tier == "frontier":
8067
+ plan["frontier_effort"] = original_frontier_effort
7571
8068
  else:
7572
8069
  if tier == "frontier":
7573
- plan["frontier_effort"] = binding["effort"]
8070
+ plan["frontier_effort"] = binding.get("effort")
7574
8071
  # Editing the MAIN tier's binding moves the seat the review was graded
7575
8072
  # against just as surely as picking a different main tier does.
7576
8073
  if tier == plan["main_tier"]:
@@ -7622,6 +8119,94 @@ VERSION_INFO_PATH = pathlib.Path(
7622
8119
  )
7623
8120
  )
7624
8121
 
8122
+ UPDATE_CHECK_PATH = pathlib.Path(
8123
+ os.environ.get(
8124
+ "AGENT_BIOS_UPDATE_CHECK_STATE",
8125
+ str(pathlib.Path.home() / ".local/share/agent-bios/update-check.json"),
8126
+ )
8127
+ )
8128
+
8129
+ # Once a day. The launcher never performs the lookup itself — it reads this cache and
8130
+ # at most SPAWNS the installer, detached, to refresh it. Two reasons, both load-bearing:
8131
+ # `npm view` routinely takes seconds and a launcher that blocks on the network is worse
8132
+ # than one that shows a stale badge, and the network operation belongs to the component
8133
+ # that already owns fetch-corpus (ENDPOINTS.md) rather than to the UI.
8134
+ UPDATE_CHECK_INTERVAL_S = 24 * 60 * 60
8135
+
8136
+
8137
+ def _version_tuple(text: str) -> tuple[int, ...] | None:
8138
+ """Numeric release prefix, or None when it is not one.
8139
+
8140
+ Deliberately refuses anything it does not fully understand rather than guessing:
8141
+ a prerelease like 1.2.3-rc1 returns None, so it is never compared and never
8142
+ announced. Announcing an upgrade to a version the user cannot get is worse than
8143
+ announcing nothing."""
8144
+ parts = text.strip().split(".")
8145
+ if not (2 <= len(parts) <= 4):
8146
+ return None
8147
+ out = []
8148
+ for part in parts:
8149
+ if not part.isdigit():
8150
+ return None
8151
+ out.append(int(part))
8152
+ return tuple(out)
8153
+
8154
+
8155
+ def read_update_cache() -> dict | None:
8156
+ try:
8157
+ data = json.loads(UPDATE_CHECK_PATH.read_text(encoding="utf-8"))
8158
+ except (OSError, ValueError):
8159
+ return None
8160
+ return data if isinstance(data, dict) else None
8161
+
8162
+
8163
+ def update_available(deployed: str | None, cache: dict | None) -> str | None:
8164
+ """The version to announce, or None.
8165
+
8166
+ None whenever anything is unknown — no cache, a cache whose lookup failed (it
8167
+ carries checked_at but no latest), an unparseable version on either side, or a
8168
+ latest that is not strictly greater. `latest` missing means "asked, no answer",
8169
+ which must not read as "up to date"; it simply says nothing."""
8170
+ if not deployed or not cache:
8171
+ return None
8172
+ latest = cache.get("latest")
8173
+ if not isinstance(latest, str):
8174
+ return None
8175
+ here, there = _version_tuple(deployed), _version_tuple(latest)
8176
+ if here is None or there is None:
8177
+ return None
8178
+ return latest if there > here else None
8179
+
8180
+
8181
+ def update_check_due(cache: dict | None, now: float) -> bool:
8182
+ if os.environ.get("AGENT_BIOS_UPDATE_CHECK") == "0":
8183
+ return False
8184
+ if cache is None:
8185
+ return True
8186
+ checked = cache.get("checked_at")
8187
+ if not isinstance(checked, (int, float)):
8188
+ return True
8189
+ return (now - checked) >= UPDATE_CHECK_INTERVAL_S
8190
+
8191
+
8192
+ def spawn_update_check() -> None:
8193
+ """Fire and forget. Every failure here is silent BY DESIGN — a background
8194
+ refresh that reported its own problems would interrupt a launch to say something
8195
+ the user did not ask for and cannot act on."""
8196
+ installer = shutil.which("agent-bios")
8197
+ if not installer:
8198
+ return
8199
+ try:
8200
+ subprocess.Popen(
8201
+ [installer, "update", "--check"],
8202
+ stdin=subprocess.DEVNULL,
8203
+ stdout=subprocess.DEVNULL,
8204
+ stderr=subprocess.DEVNULL,
8205
+ start_new_session=True,
8206
+ )
8207
+ except OSError:
8208
+ pass
8209
+
7625
8210
 
7626
8211
  def load_corpus_status() -> dict[str, Any] | None:
7627
8212
  """The corpus status projection, or None when the file is unreadable or is not
@@ -7633,6 +8218,12 @@ def load_corpus_status() -> dict[str, Any] | None:
7633
8218
  before it drew, and each new consumer would add another field to remember. The
7634
8219
  depth is unbounded, so the readers DEGRADE instead — see corpus_summary_lines.
7635
8220
  """
8221
+ if private_corpus_enabled():
8222
+ try:
8223
+ return {"private": corpus_store().status()}
8224
+ except (OSError, ValueError, RuntimeError) as exc:
8225
+ print(f"agent-launch: cannot read private corpus: {exc}", file=sys.stderr)
8226
+ return None
7636
8227
  try:
7637
8228
  data = json.loads(CORPUS_STATUS_PATH.read_text(encoding="utf-8"))
7638
8229
  except (OSError, ValueError):
@@ -7653,7 +8244,12 @@ def version_label() -> str | None:
7653
8244
  if not version:
7654
8245
  return None
7655
8246
  released = info.get("releaseDate")
7656
- return f"agent-bios v{version} · {released}" if released else f"agent-bios v{version}"
8247
+ label = f"agent-bios v{version} · {released}" if released else f"agent-bios v{version}"
8248
+ cache = read_update_cache()
8249
+ if update_check_due(cache, time.time()):
8250
+ spawn_update_check()
8251
+ newer = update_available(version, cache)
8252
+ return f"{label} · update v{newer} available" if newer else label
7657
8253
 
7658
8254
 
7659
8255
  def display_width(text: str) -> int:
@@ -7710,44 +8306,232 @@ def status_list(value: Any) -> list:
7710
8306
  return value if isinstance(value, list) else []
7711
8307
 
7712
8308
 
8309
+ _CORPUS_FACTS: dict[tuple, dict[str, Any] | None] = {}
8310
+
8311
+
8312
+ def size_label(chars: int) -> str:
8313
+ """Text size in KB. Characters, not tokens, and the caller says so on screen:
8314
+ this launcher has no tokenizer, and an estimate printed as a bare number is read
8315
+ as a measurement."""
8316
+ return f"{chars / 1024:.1f} KB"
8317
+
8318
+
8319
+ def corpus_facts(status: dict[str, Any] | None) -> dict[str, Any] | None:
8320
+ """What each domain package holds and what it costs, or None when the package the
8321
+ projection points at cannot be read.
8322
+
8323
+ Derived by running the package's OWN assembler over its own manifest and monolith,
8324
+ never by a second copy of the tier rule kept here. A size this module computed from
8325
+ its own reading of `domains.json` would drift from the bundle the installer actually
8326
+ writes, and a wrong number under a chooser is worse than no number: it is the basis
8327
+ the user was told to choose on.
8328
+
8329
+ Two figures per domain, never one. A domain's rules land in the global that every
8330
+ session and every subagent loads; its guides are deployed but read only when a rule
8331
+ points at one. Here they differ by more than an order of magnitude — 0.3-7 KB of
8332
+ rules against 12-114 KB of guides — so a single "size" would mislead on both.
8333
+
8334
+ Failure is None, in every direction: a missing package, a manifest the assembler
8335
+ refuses (it calls sys.exit, which is not an Exception), a monolith that disagrees
8336
+ with the manifest. This runs behind the root menu, so nothing it does may end the
8337
+ session, and a screen that cannot size the packages still lets the user pick them.
8338
+ """
8339
+ repo = (status or {}).get("repo")
8340
+ if not isinstance(repo, str) or not repo:
8341
+ return None
8342
+ manifest_path = pathlib.Path(repo) / "compose" / "domains.json"
8343
+ applied = tuple(sorted(status_list(((status or {}).get("domains") or {}).get("applied"))))
8344
+ try:
8345
+ stamp = (repo, manifest_path.stat().st_mtime, applied)
8346
+ except OSError:
8347
+ return None
8348
+ if stamp in _CORPUS_FACTS:
8349
+ return _CORPUS_FACTS[stamp]
8350
+ _CORPUS_FACTS[stamp] = None # a repeat of a failing read must not repeat the cost
8351
+ try:
8352
+ facts = _corpus_facts(pathlib.Path(repo), manifest_path, set(applied))
8353
+ except SystemExit:
8354
+ # assemble.die() on a manifest/monolith disagreement. Not an Exception, so it
8355
+ # would otherwise leave the launcher through every absorber in this file.
8356
+ return None
8357
+ except Exception as exc: # noqa: BLE001 — sizing is decoration; picking is not
8358
+ print(f"agent-launch: cannot size the corpus packages in {repo} "
8359
+ f"({type(exc).__name__}: {exc})", file=sys.stderr)
8360
+ return None
8361
+ _CORPUS_FACTS[stamp] = facts
8362
+ return facts
8363
+
8364
+
8365
+ def _load_assembler(repo: pathlib.Path):
8366
+ """The package's own assembler, loaded by PATH rather than by name.
8367
+
8368
+ `sys.path.insert` plus a plain import returns whatever is already cached under that
8369
+ name, so in a process that has imported some other `assemble` the figures would come
8370
+ from the wrong file and still look entirely plausible. Loading by location under a
8371
+ private name pins which file answers, and leaves sys.path alone for everyone else in
8372
+ the process."""
8373
+ import importlib.util
8374
+
8375
+ path = repo / "compose" / "assemble.py"
8376
+ spec = importlib.util.spec_from_file_location("agent_bios_assemble", path)
8377
+ if spec is None or spec.loader is None:
8378
+ raise ImportError(f"no assembler to load at {path}")
8379
+ module = importlib.util.module_from_spec(spec)
8380
+ sys.modules[spec.name] = module
8381
+ spec.loader.exec_module(module)
8382
+ return module
8383
+
8384
+
8385
+ def _corpus_facts(repo: pathlib.Path, manifest_path: pathlib.Path, applied: set) -> dict[str, Any]:
8386
+ """The derivation proper, with every failure left to the caller to absorb."""
8387
+ assemble = _load_assembler(repo)
8388
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
8389
+ monolith = (repo / "claude" / "CLAUDE.md").read_text(encoding="utf-8")
8390
+ guide_dir = repo / "claude" / "guides"
8391
+
8392
+ def guide_chars(names) -> int:
8393
+ return sum(
8394
+ (guide_dir / name).stat().st_size
8395
+ for name in names if (guide_dir / name).is_file()
8396
+ )
8397
+
8398
+ base_text, base_rules = assemble.build_bundle(monolith, manifest, set(), "claude")
8399
+ base_guides = set(assemble.filtered_files(manifest, "guides", set()))
8400
+ domains = {}
8401
+ for name, description in (manifest.get("domains") or {}).items():
8402
+ text, rules = assemble.build_bundle(monolith, manifest, {name}, "claude")
8403
+ # Every selection carries the universal files too, so a domain's OWN set is the
8404
+ # difference. Without the subtraction each domain claimed the two core guides.
8405
+ own = sorted(set(assemble.filtered_files(manifest, "guides", {name})) - base_guides)
8406
+ domains[name] = {
8407
+ "description": description if isinstance(description, str) else "",
8408
+ "rules": rules - base_rules,
8409
+ "chars": len(text) - len(base_text),
8410
+ "guides": len(own),
8411
+ "guide_chars": guide_chars(own),
8412
+ }
8413
+ # The applied set is assembled OUTRIGHT rather than summed from the per-domain
8414
+ # deltas above. A section header is emitted once per bundle but appears in every
8415
+ # delta that needs it, so the sum overstated the real deployed global — by 23
8416
+ # characters here, which is small and would have been reported as measured.
8417
+ known = applied & set(domains)
8418
+ selected_text, selected_rules = assemble.build_bundle(monolith, manifest, known, "claude")
8419
+ selected_guides = assemble.filtered_files(manifest, "guides", known)
8420
+ return {
8421
+ "domains": domains,
8422
+ "base": {
8423
+ "rules": base_rules,
8424
+ "chars": len(base_text),
8425
+ "guides": len(base_guides),
8426
+ "guide_chars": guide_chars(base_guides),
8427
+ },
8428
+ "selected": {
8429
+ "rules": selected_rules,
8430
+ "chars": len(selected_text),
8431
+ "guides": len(selected_guides),
8432
+ "guide_chars": guide_chars(selected_guides),
8433
+ # A projection can name a domain the installed package does not carry;
8434
+ # the figures above then describe a smaller corpus than the row above them
8435
+ # claims, so the gap is reported rather than folded in.
8436
+ "unknown": sorted(applied - set(domains)),
8437
+ },
8438
+ }
8439
+
8440
+
7713
8441
  def _corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
7714
8442
  """The panel body proper. The label column is derived at render time from the
7715
8443
  widest label in the ACTIVE language and measured in display cells: a pad written
7716
8444
  for English cannot align a translated panel."""
7717
8445
  if status is None:
7718
8446
  return [t("panel.unprojected")]
7719
- labels = (t("panel.version.label"), t("panel.mechanisms.label"),
7720
- t("panel.ledger.label"), t("panel.domains.label"))
8447
+ if isinstance(status.get("private"), dict):
8448
+ current = status["private"]
8449
+ selection = current.get("selection")
8450
+ selected = ", ".join(selection) if selection else t("corpus.launch.base")
8451
+ return [t("corpus.private.relationship"),
8452
+ f"{t('panel.domains.label')} {selected}",
8453
+ f"{t('panel.version.label')} {current.get('selected_baseline_ref') or '—'}"]
8454
+ # `versions is None` is the projection saying "this install cannot know" — a packaged
8455
+ # install has no author-side registry (compose/corpus-state.py). Three rows then read
8456
+ # "unavailable", which is true of the ledger and says nothing about the corpus the
8457
+ # user actually has. The package carries the manifest and the monolith the installer
8458
+ # assembled from, so those rows are answerable from it; the absence is still stated,
8459
+ # once, on the row it belongs to.
8460
+ unavailable = status.get("versions") is None
8461
+ domains = status.get("domains")
8462
+ applied_raw = domains.get("applied") if isinstance(domains, dict) else None
8463
+ facts = corpus_facts(status)
8464
+ # An unapplied install is NOT sized: what is deployed is whatever the install put
8465
+ # there, and the manifest cannot say which. A guess here would read as a reading.
8466
+ packaged = unavailable and facts is not None and applied_raw is not None
8467
+ labels = (
8468
+ (t("panel.rules.label"), t("panel.guides.label"))
8469
+ if packaged else (t("panel.version.label"), t("panel.mechanisms.label"))
8470
+ ) + (t("panel.ledger.label"), t("panel.domains.label"))
7721
8471
  column = max(display_width(label) for label in labels) + 2
7722
8472
 
7723
8473
  def row(label: str, value: str) -> str:
7724
8474
  return label + " " * max(1, column - display_width(label)) + value
7725
8475
 
8476
+ if packaged:
8477
+ selected = facts["selected"]
8478
+ lines = [
8479
+ row(labels[0], t("panel.rules.value").format(
8480
+ rules=selected["rules"], size=size_label(selected["chars"]))),
8481
+ row(labels[1], t("panel.guides.value").format(
8482
+ guides=selected["guides"], size=size_label(selected["guide_chars"]))),
8483
+ row(labels[2], t("panel.ledger.author-only")),
8484
+ ]
8485
+ if selected["unknown"]:
8486
+ # The projection names a package this install does not carry, so the two
8487
+ # rows above describe less than the domain row below them claims.
8488
+ lines.append(t("panel.domains.unknown").format(
8489
+ names=", ".join(selected["unknown"])))
8490
+ return lines + _corpus_domain_lines(status, applied_raw, row, labels[3])
8491
+
8492
+ # `versions is None` is the projection saying "this install cannot know" — a packaged
8493
+ # install has no author version/ledger registry (compose/corpus-state.py). It is not
8494
+ # `[]`, which would mean a checkout whose registry is genuinely empty, and it must not
8495
+ # render as `0 placed · 0 versions`: a fabricated zero is worse than a blank, because
8496
+ # a reader cannot tell it from a real count. The domain rows below stay real, which is
8497
+ # the whole point of degrading rather than withholding the projection.
8498
+ summary = status.get("summary") or {}
7726
8499
  current = status.get("current_version", "?")
7727
8500
  latest = status.get("latest_version", "?")
7728
- head = row(labels[0], str(current))
8501
+ # `str(current)` printed the literal "None" on a checkout whose registry exists but
8502
+ # holds no version — pre-existing, and visible the moment the row above it learned to
8503
+ # say "unavailable". A known absence reads as none; an unknown one says so.
8504
+ head = row(labels[0], t("panel.unavailable") if unavailable
8505
+ else str(current) if current else t("panel.layers.none"))
7729
8506
  if status.get("rolled_back_to"):
7730
8507
  head += " " + t("panel.rolled-back").format(latest=latest)
7731
- layers = status.get("summary", {}).get("placed_by_layer", {})
8508
+ layers = summary.get("placed_by_layer", {})
7732
8509
  order = ("global", "guide", "hook", "enforcement", "gate")
7733
8510
  layer_text = " · ".join(
7734
8511
  f"{name} {layers[name]}" for name in order if layers.get(name)
7735
8512
  ) or t("panel.layers.none")
7736
- by_status = status.get("summary", {}).get("by_status", {})
8513
+ by_status = summary.get("by_status", {})
7737
8514
  lines = [
7738
8515
  head,
7739
- row(labels[1], layer_text),
7740
- row(labels[2], t("panel.ledger.value").format(
8516
+ row(labels[1], t("panel.unavailable") if unavailable else layer_text),
8517
+ row(labels[2], t("panel.unavailable") if unavailable else t("panel.ledger.value").format(
7741
8518
  placed=by_status.get("placed", 0),
7742
8519
  incubating=by_status.get("incubating", 0) + by_status.get("incubating-G", 0),
7743
8520
  versions=len(status_list(status.get("versions"))),
7744
8521
  )),
7745
8522
  ]
7746
- domains = status.get("domains")
7747
- if isinstance(domains, dict):
7748
- applied = domains.get("applied")
7749
- applied = None if applied is None else status_list(applied)
7750
- lines.append(row(labels[3], t("panel.domains.unset") if applied is None
8523
+ return lines + _corpus_domain_lines(status, applied_raw, row, labels[3])
8524
+
8525
+
8526
+ def _corpus_domain_lines(status, applied_raw, row, label: str) -> list[str]:
8527
+ """The rows both panels end on: what is applied, and a failed apply if there is one.
8528
+
8529
+ Shared rather than written twice — a packaged panel that quietly dropped the failed
8530
+ apply would hide exactly the state the row exists for."""
8531
+ lines = []
8532
+ if isinstance(status.get("domains"), dict):
8533
+ applied = None if applied_raw is None else status_list(applied_raw)
8534
+ lines.append(row(label, t("panel.domains.unset") if applied is None
7751
8535
  else ", ".join(applied) or t("corpus.core-only")))
7752
8536
  last_apply = status.get("last_apply")
7753
8537
  if isinstance(last_apply, dict) and last_apply.get("outcome") not in (None, "applied"):
@@ -8046,6 +8830,10 @@ class CorpusApplyRequested(Exception):
8046
8830
  self.selection = selection
8047
8831
 
8048
8832
 
8833
+ class CorpusStudioRequested(Exception):
8834
+ """Release Textual's terminal before starting the standalone manager."""
8835
+
8836
+
8049
8837
  CORPUS_OPTION = "__corpus__"
8050
8838
  # A plain value, because the numbered prompt prints it as the default label.
8051
8839
  # Collision with a domain id is structurally impossible: domain slugs come from
@@ -8090,6 +8878,107 @@ def run_corpus_apply(selection: list[str]) -> None:
8090
8878
  print("\nagent-launch: corpus selection applied.", flush=True)
8091
8879
 
8092
8880
 
8881
+ def checkbox_label(checked: bool, name: str, trailing: str = ""):
8882
+ """`[✓] name`, with the mark in green wherever the renderer carries style.
8883
+
8884
+ Returned as a Text rather than an f-string because an option label is rendered as
8885
+ markup: `[x]` is consumed as a tag and disappears, while `[ ]` does not match the
8886
+ tag shape and survives. Built as a plain string only where rich is absent, which is
8887
+ the same path that has no colour to lose."""
8888
+ try:
8889
+ from rich.text import Text
8890
+ except ImportError:
8891
+ return f"[{'✓' if checked else ' '}] {name}{trailing}"
8892
+ box = ("[", ("✓", "bold green"), "] ") if checked else ("[ ] ",)
8893
+ return Text.assemble(*box, name, (trailing, "dim"))
8894
+
8895
+
8896
+ def _corpus_domain_content(name: str, fallback: str = "") -> str:
8897
+ """Localized content help; unfamiliar packages retain their manifest description."""
8898
+ descriptions = {
8899
+ "builder-base": t("corpus.domain.builder-base"),
8900
+ "llm-pipeline-dev": t("corpus.domain.llm-pipeline-dev"),
8901
+ "multi-agent-orchestration": t("corpus.domain.multi-agent-orchestration"),
8902
+ "visualization-docs": t("corpus.domain.visualization-docs"),
8903
+ "office-work": t("corpus.domain.office-work"),
8904
+ }
8905
+ return descriptions.get(name, fallback).strip()
8906
+
8907
+
8908
+ def corpus_domain_labels() -> dict[str, str]:
8909
+ return {
8910
+ "builder-base": t("corpus.domain.builder-base.label"),
8911
+ "llm-pipeline-dev": t("corpus.domain.llm-pipeline-dev.label"),
8912
+ "multi-agent-orchestration": t("corpus.domain.multi-agent-orchestration.label"),
8913
+ "visualization-docs": t("corpus.domain.visualization-docs.label"),
8914
+ "office-work": t("corpus.domain.office-work.label"),
8915
+ }
8916
+
8917
+
8918
+ def preset_descriptions() -> dict[str, str]:
8919
+ """Human-facing help for the built-in choices, independent of launch-contract text."""
8920
+ return {
8921
+ "balanced": t("preset.balanced.description"),
8922
+ "deep-review": t("preset.deep-review.description"),
8923
+ "fast-batch": t("preset.fast-batch.description"),
8924
+ "solo": t("preset.solo.description"),
8925
+ "vanilla": (t("corpus.private.vanilla") if private_corpus_enabled()
8926
+ else t("preset.vanilla.description")),
8927
+ }
8928
+
8929
+
8930
+ def _corpus_launch_description(status: dict[str, Any] | None) -> str:
8931
+ """Explain the installed selection shared by modes without claiming a launch applies it."""
8932
+ if private_corpus_enabled():
8933
+ return t("corpus.private.relationship")
8934
+ lines = [t("corpus.launch.relationship")]
8935
+ domains = (status or {}).get("domains")
8936
+ applied = domains.get("applied") if isinstance(domains, dict) else None
8937
+ if not isinstance(applied, list) or not all(isinstance(name, str) for name in applied):
8938
+ message = (t("panel.domains.unset")
8939
+ if isinstance(domains, dict) and "applied" in domains and applied is None
8940
+ else t("panel.unprojected"))
8941
+ return "\n".join([*lines, message])
8942
+ lines += [t("corpus.launch.applied"), t("corpus.launch.base")]
8943
+ facts = (corpus_facts(status) or {}).get("domains", {})
8944
+ labels = corpus_domain_labels()
8945
+ for name in applied:
8946
+ content = _corpus_domain_content(
8947
+ name, facts.get(name, {}).get("description") or t("corpus.launch.unknown"),
8948
+ )
8949
+ lines.append(f"• {labels.get(name, name)}: {content.splitlines()[0]}")
8950
+ return "\n".join(lines)
8951
+
8952
+
8953
+ def _domain_description(info: dict[str, Any] | None, name: str = "") -> str:
8954
+ """What this package is, and what choosing it costs.
8955
+
8956
+ The generic toggle hint was the description of every row, so the screen listed five
8957
+ names and said the same sentence about all of them — nothing to choose on. The
8958
+ package's own description carries what it is; the two figures carry what it costs,
8959
+ and they are kept apart because they are spent differently: rules are in the global
8960
+ that every session and every subagent loads, guides are on disk and read only when a
8961
+ rule points at one."""
8962
+ content = _corpus_domain_content(name, info.get("description", "") if info else "")
8963
+ if info is None:
8964
+ return "\n\n".join(filter(None, [content, t("corpus.toggle.description")]))
8965
+ # Both keys are named at a literal call site rather than chosen into a variable:
8966
+ # the catalog gate finds keys by reading the quoted argument of each t() call, so a
8967
+ # computed key is a string no language is ever checked for.
8968
+ if info["guides"]:
8969
+ sizes = t("corpus.size.detail").format(
8970
+ rules=info["rules"],
8971
+ size=size_label(info["chars"]),
8972
+ guides=info["guides"],
8973
+ guide_size=size_label(info["guide_chars"]),
8974
+ )
8975
+ else:
8976
+ sizes = t("corpus.size.detail.noguides").format(
8977
+ rules=info["rules"], size=size_label(info["chars"]),
8978
+ )
8979
+ return "\n\n".join(filter(None, [content, sizes, t("corpus.toggle.description")]))
8980
+
8981
+
8093
8982
  def corpus_checklist(ui: "TextualUI | None") -> None:
8094
8983
  """Toggle-and-apply loop over the optional domain packages.
8095
8984
 
@@ -8125,12 +9014,25 @@ def corpus_checklist(ui: "TextualUI | None") -> None:
8125
9014
  applied = set(status_list(applied_raw))
8126
9015
  never_applied = applied_raw is None
8127
9016
  toggles = set(applied)
9017
+ # What each package contains and costs. None when the installed package cannot be
9018
+ # read, and every use below falls back to the bare name — a checklist that cannot
9019
+ # size its packages is still a checklist, and this screen is how a user reaches the
9020
+ # installer that would repair the projection.
9021
+ facts = corpus_facts(status)
9022
+ sized = (facts or {}).get("domains", {})
9023
+ labels = corpus_domain_labels()
9024
+ display_names = {name: f"{labels[name]} ({name})" if name in labels else name for name in available}
9025
+ column = max(display_width(label) for label in display_names.values()) + 2
8128
9026
  while True:
8129
9027
  options = [
8130
9028
  MenuOption(
8131
9029
  name,
8132
- f"[{'x' if name in toggles else ' '}] {name}",
8133
- t("corpus.toggle.description"),
9030
+ checkbox_label(
9031
+ name in toggles,
9032
+ display_names[name] + " " * max(1, column - display_width(display_names[name])),
9033
+ "" if name not in sized else size_label(sized[name]["chars"]),
9034
+ ),
9035
+ _domain_description(sized.get(name), name),
8134
9036
  )
8135
9037
  for name in available
8136
9038
  ]
@@ -8150,7 +9052,12 @@ def corpus_checklist(ui: "TextualUI | None") -> None:
8150
9052
  try:
8151
9053
  choice = choose(
8152
9054
  t("corpus.title"), options, CORPUS_APPLY, ui, allow_back=True,
8153
- corpus_lines=[t("corpus.core.line"), *corpus_summary_lines(status)],
9055
+ corpus_lines=[
9056
+ t("corpus.core.line"),
9057
+ *([] if not sized else [t("corpus.size.legend")]),
9058
+ *corpus_summary_lines(status),
9059
+ ],
9060
+ confirm=CORPUS_APPLY,
8154
9061
  )
8155
9062
  except BackRequested:
8156
9063
  return
@@ -8229,25 +9136,33 @@ def pick_mode_and_preset(
8229
9136
  dropping all the way back to the top mode picker.
8230
9137
  """
8231
9138
  presets = config["presets"]
9139
+ # User-authored preset descriptions stay as authored, including a local preset
9140
+ # that replaces a built-in name. Translations belong only to the shipped choices.
9141
+ user_names = set(load_user_presets(user_presets_path(config_path))) if config_path else set()
8232
9142
  mode = resume_mode
8233
9143
  while True:
9144
+ status = load_corpus_status()
9145
+ corpus_description = _corpus_launch_description(status)
8234
9146
  if mode is None:
8235
9147
  mode_options = [
8236
- MenuOption(SWE_MODE, t("mode.swe.label"), t("mode.swe.description")),
9148
+ MenuOption(SWE_MODE, t("mode.swe.label"),
9149
+ t("mode.swe.description") + "\n\n" + corpus_description),
8237
9150
  MenuOption(
8238
9151
  DEFAULT_PRESET_MODE,
8239
9152
  t("mode.builder.label"),
8240
- t("mode.builder.description"),
9153
+ t("mode.builder.description") + "\n\n" + corpus_description,
8241
9154
  ),
8242
9155
  MenuOption(
8243
9156
  DISTILL_MODE,
8244
9157
  t("mode.distill.label"),
8245
- t("mode.distill.description"),
9158
+ t("mode.distill.description") + "\n\n" + corpus_description,
8246
9159
  ),
8247
9160
  ]
8248
9161
  mode_options.append(
8249
9162
  MenuOption(
8250
- CORPUS_OPTION, t("corpus.label"), t("corpus.description")
9163
+ CORPUS_OPTION,
9164
+ t("corpus.private.title") if private_corpus_enabled() else t("corpus.label"),
9165
+ t("corpus.private.description") if private_corpus_enabled() else t("corpus.description"),
8251
9166
  )
8252
9167
  )
8253
9168
  if config_path is not None:
@@ -8290,9 +9205,15 @@ def pick_mode_and_preset(
8290
9205
  DEFAULT_PRESET_MODE,
8291
9206
  ui,
8292
9207
  preview=preview_mode,
8293
- corpus_lines=corpus_summary_lines(load_corpus_status()),
9208
+ corpus_lines=corpus_summary_lines(status),
8294
9209
  )
8295
9210
  if mode == CORPUS_OPTION:
9211
+ if private_corpus_enabled():
9212
+ if ui is not None:
9213
+ raise CorpusStudioRequested()
9214
+ open_corpus_studio()
9215
+ mode = None
9216
+ continue
8296
9217
  _corpus_screen(ui, lambda: corpus_checklist(ui))
8297
9218
  mode = None
8298
9219
  continue
@@ -8311,7 +9232,9 @@ def pick_mode_and_preset(
8311
9232
  MenuOption(
8312
9233
  name,
8313
9234
  data["label"],
8314
- data.get("description", f"Launch the {data['label']} preset."),
9235
+ (preset_descriptions().get(name, data.get("description", ""))
9236
+ if name not in user_names else data.get("description", ""))
9237
+ + "\n\n" + corpus_description,
8315
9238
  )
8316
9239
  for name, data in presets.items()
8317
9240
  if preset_mode(data) == mode
@@ -8320,8 +9243,9 @@ def pick_mode_and_preset(
8320
9243
  options.append(
8321
9244
  MenuOption(
8322
9245
  CUSTOM_PRESET,
8323
- "Custom",
8324
- "Open a settings hub for tiers, review setup, policy, and final confirmation.",
9246
+ t("preset.custom.label"),
9247
+ t("preset.custom.description")
9248
+ + "\n\n" + corpus_description,
8325
9249
  )
8326
9250
  )
8327
9251
  default = mode_default_preset(presets, mode) or CUSTOM_PRESET
@@ -8365,7 +9289,7 @@ def pick_mode_and_preset(
8365
9289
 
8366
9290
  try:
8367
9291
  selected = choose(
8368
- "Preset", options, default, ui, allow_back=True, preview=preview_preset
9292
+ t("preset.title"), options, default, ui, allow_back=True, preview=preview_preset
8369
9293
  )
8370
9294
  except BackRequested:
8371
9295
  mode = None
@@ -8444,8 +9368,8 @@ def validate_review_setup(plan: dict[str, Any]) -> None:
8444
9368
  effective_review(plan)
8445
9369
 
8446
9370
 
8447
- def tier_effort(plan: dict[str, Any], tier: str) -> str:
8448
- return plan["frontier_effort"] if tier == "frontier" else plan["tiers"][tier]["effort"]
9371
+ def tier_effort(plan: dict[str, Any], tier: str) -> str | None:
9372
+ return plan["frontier_effort"] if tier == "frontier" else plan["tiers"][tier].get("effort")
8449
9373
 
8450
9374
 
8451
9375
  def child_agent_registrations(
@@ -8491,6 +9415,11 @@ def child_agent_registrations(
8491
9415
  raise LaunchError(f"Codex agent template requires description: {source}")
8492
9416
  data["model"] = plan["tiers"][tier]["model"]
8493
9417
  data["model_reasoning_effort"] = tier_effort(plan, tier)
9418
+ if plan.get("corpus_instruction_text"):
9419
+ # The child's own instruction body and the selected parent corpus are
9420
+ # composed before the content-addressed config path is derived.
9421
+ existing = data.get("developer_instructions", "")
9422
+ data["developer_instructions"] = existing + "\n\n" + plan["corpus_instruction_text"]
8494
9423
  lines = []
8495
9424
  for key, value in data.items():
8496
9425
  if not isinstance(value, (str, int, float, bool)):
@@ -8633,7 +9562,9 @@ def _cross_review_route(
8633
9562
  main_family = LEGACY_HOST_FAMILY[plan["host"]][0]
8634
9563
  review_family = LEGACY_HOST_FAMILY[review_host][0]
8635
9564
  review_bindings = ", ".join(
8636
- f"{tier}={plan['review_tiers'][tier]['model']}/{plan['review_tiers'][tier]['effort']}"
9565
+ f"{tier}={format_model_effort(
9566
+ plan['review_tiers'][tier]['model'], plan['review_tiers'][tier].get('effort')
9567
+ )}"
8637
9568
  for tier in TIER_ORDER
8638
9569
  if tier in plan["review_tiers"]
8639
9570
  )
@@ -8747,7 +9678,9 @@ def run_contract(
8747
9678
  # the session never received, which is round 18 #9's defect under a second cause
8748
9679
  # (round 23, #1).
8749
9680
  bindings = ", ".join(
8750
- f"{tier}={plan['tiers'][tier]['model']}/{tier_effort(plan, tier)}"
9681
+ f"{tier}={format_model_effort(
9682
+ plan['tiers'][tier]['model'], tier_effort(plan, tier)
9683
+ )}"
8751
9684
  for tier in active_tiers(plan)
8752
9685
  )
8753
9686
  tiers_clause = f"tiers: {bindings}"
@@ -8769,8 +9702,9 @@ def run_contract(
8769
9702
  # set after one of the two reasons it can be inactive, and it happens to be the
8770
9703
  # branch that can hold the tier the name is false of (round 24, #12).
8771
9704
  tiers_clause = (
8772
- f"tiers: {main_tier}={plan['tiers'][main_tier]['model']}/"
8773
- f"{tier_effort(plan, main_tier)} only; inactive tiers "
9705
+ f"tiers: {main_tier}={format_model_effort(
9706
+ plan['tiers'][main_tier]['model'], tier_effort(plan, main_tier)
9707
+ )} only; inactive tiers "
8774
9708
  f"({', '.join(inactive)}) are "
8775
9709
  f"inactive and not projected because {inactive_tier_reason(plan)}"
8776
9710
  )
@@ -8827,7 +9761,8 @@ def run_contract(
8827
9761
  # is the point: a command or an argument carrying a space would otherwise read as two.
8828
9762
  # Empty on every route that registers nothing, which includes every legacy one.
8829
9763
  registrations = (
8830
- review_mcp_servers(plan) if mcp_registrations is None else mcp_registrations
9764
+ [] if sweep_main(plan)
9765
+ else (review_mcp_servers(plan) if mcp_registrations is None else mcp_registrations)
8831
9766
  )
8832
9767
  mcp_clause = ""
8833
9768
  if registrations:
@@ -8872,17 +9807,17 @@ def run_contract(
8872
9807
  )
8873
9808
  if plan["delegation"]:
8874
9809
  authority = (
8875
- "Main and native child model/effort defaults are config-projected. Use the "
9810
+ "Main and native child bindings are config-projected. Use the "
8876
9811
  "installed codex-run adapter when a separate child root or stricter reach "
8877
9812
  "boundary matters."
8878
9813
  if plan["host"] == "codex"
8879
- else "Main and child model/effort defaults are CLI-projected."
9814
+ else "Main and child bindings are CLI-projected."
8880
9815
  )
8881
9816
  else:
8882
9817
  authority = (
8883
- "The main model/effort default is config-projected; no child binding is."
9818
+ "The main binding is config-projected; no child binding is."
8884
9819
  if plan["host"] == "codex"
8885
- else "The main model/effort default is CLI-projected; no child binding is."
9820
+ else "The main binding is CLI-projected; no child binding is."
8886
9821
  )
8887
9822
  mission = plan.get("mission")
8888
9823
  if mission and plan.get("trigger"):
@@ -8890,9 +9825,11 @@ def run_contract(
8890
9825
  mission_prefix = f"Mission: {mission} " if mission else ""
8891
9826
  prose = (
8892
9827
  f"{mission_prefix}"
8893
- f"LaunchPlan: main={main_tier} ({plan['tiers'][main_tier]['model']}/"
8894
- f"{tier_effort(plan, main_tier)}); {tiers_clause}. "
8895
- f"{delegation_clause(plan['delegation'])}"
9828
+ f"LaunchPlan: main={main_tier} ({format_model_effort(
9829
+ plan['tiers'][main_tier]['model'], tier_effort(plan, main_tier)
9830
+ )}); {tiers_clause}. "
9831
+ f"{delegation_clause(plan)}"
9832
+ f"{sweep_main_clause(plan)}"
8896
9833
  f"{child_clause}"
8897
9834
  f"{execution_clause(plan)}"
8898
9835
  f"{review_section}"
@@ -8952,6 +9889,18 @@ def execution_clause(plan: dict[str, Any]) -> str:
8952
9889
 
8953
9890
  `standard` is named rather than omitted, because "no flag" is itself the posture: the
8954
9891
  backend's own default applies, and a silent contract cannot say which one that is."""
9892
+ if sweep_main(plan):
9893
+ if plan["host"] == "codex":
9894
+ return (
9895
+ "Execution=SWEEP restricted: Codex runs with --sandbox read-only; "
9896
+ "the preset's ordinary execution policy is not projected. "
9897
+ )
9898
+ return (
9899
+ "Execution=SWEEP restricted: Claude runs with --restricted and only "
9900
+ "Read, Glob, and Grep available, plus --strict-mcp-config with an empty "
9901
+ "MCP table so inherited MCP servers are unavailable; the preset's ordinary "
9902
+ "permission mode is not projected. "
9903
+ )
8955
9904
  if plan["host"] == "codex":
8956
9905
  policy = plan["codex_execution_policy"]
8957
9906
  if policy == STANDARD_POLICY:
@@ -8969,7 +9918,7 @@ def execution_clause(plan: dict[str, Any]) -> str:
8969
9918
  return f"Execution=claude permission mode {policy} (not an OS sandbox). "
8970
9919
 
8971
9920
 
8972
- def delegation_clause(delegation: bool) -> str:
9921
+ def delegation_clause(plan: dict[str, Any]) -> str:
8973
9922
  """How the contract states delegation — and, when it is on, WHOSE decision that was.
8974
9923
 
8975
9924
  Claude Code's own Opus-5 prompt bundle appends "Do not call the AgentTool unless the
@@ -8983,7 +9932,12 @@ def delegation_clause(delegation: bool) -> str:
8983
9932
  corpus's standing spawn policy is their instruction — so the contract says so in the
8984
9933
  words that clause is looking for. Nothing is overridden; a fact that was already true
8985
9934
  is simply stated where the reader can see it."""
8986
- if not delegation:
9935
+ if sweep_main(plan):
9936
+ return (
9937
+ "Delegation=off: SWEEP main disables child delegation to preserve its "
9938
+ "read-only one-rule-per-item boundary. "
9939
+ )
9940
+ if not plan["delegation"]:
8987
9941
  return "Delegation=off. "
8988
9942
  return (
8989
9943
  "Delegation=on — the user requested delegation by selecting this launch, and their "
@@ -8991,19 +9945,43 @@ def delegation_clause(delegation: bool) -> str:
8991
9945
  )
8992
9946
 
8993
9947
 
9948
+ def sweep_main_clause(plan: dict[str, Any]) -> str:
9949
+ """The executable role boundary for a SWEEP main, stated in its contract."""
9950
+ if not sweep_main(plan):
9951
+ return ""
9952
+ return (
9953
+ "SWEEP main: apply one explicit read-only rule per item; do not make semantic "
9954
+ "judgments. "
9955
+ )
9956
+
9957
+
8994
9958
  def claude_agents(plan: dict[str, Any]) -> str:
8995
9959
  roles = {}
8996
9960
  for tier in SPAWNABLE_TIERS:
8997
9961
  binding = plan["tiers"][tier]
8998
- roles[tier] = {
8999
- "description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
9000
- "prompt": (
9001
- f"Act as the bounded {tier.upper()} role at requested effort "
9002
- f"{tier_effort(plan, tier)}. Return evidence and verification; stay in scope."
9003
- ),
9962
+ effort = tier_effort(plan, tier)
9963
+ prompt = (
9964
+ f"Act as the bounded {tier.upper()} role"
9965
+ + (f" at requested effort {effort}" if effort is not None else "")
9966
+ + ". Return evidence and verification; stay in scope."
9967
+ )
9968
+ if tier == "sweep":
9969
+ # Sweep is the mechanical, read-only lane. The narrow native tool
9970
+ # allowlist enforces its one-rule-per-item contract instead of merely
9971
+ # restating it in the prompt.
9972
+ prompt += " Apply one explicit read-only rule per item; do not make semantic judgments."
9973
+ if plan.get("corpus_instruction_text"):
9974
+ prompt += "\n\n" + plan["corpus_instruction_text"]
9975
+ role = {
9976
+ "description": f"{tier.upper()} tier: {format_model_effort(binding['model'], effort)}",
9977
+ "prompt": prompt,
9004
9978
  "model": binding["model"],
9005
- "effort": tier_effort(plan, tier),
9006
9979
  }
9980
+ if effort is not None:
9981
+ role["effort"] = effort
9982
+ if tier == "sweep":
9983
+ role["tools"] = ["Read", "Glob", "Grep"]
9984
+ roles[tier] = role
9007
9985
  return json.dumps(roles, separators=(",", ":"))
9008
9986
 
9009
9987
 
@@ -9011,6 +9989,10 @@ MCP_STDIO_ADAPTER = "mcp-stdio-v1"
9011
9989
  # The argv every stdio MCP capability registered here uses today. A capability
9012
9990
  # needing different arguments is a registry data addition, not a branch.
9013
9991
  MCP_STDIO_ARGS = ["mcp"]
9992
+ # SWEEP's native capability surface is intentionally empty. `--restricted` alone
9993
+ # does not exclude inherited MCP servers, so its matching strict flag and this
9994
+ # explicit empty table travel together in the Claude argv.
9995
+ SWEEP_EMPTY_MCP_CONFIG = json.dumps({"mcpServers": {}}, separators=(",", ":"))
9014
9996
 
9015
9997
 
9016
9998
  def review_mcp_servers(plan: dict[str, Any]) -> list[tuple[str, str, list[str]]]:
@@ -9256,7 +10238,10 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9256
10238
  # L7). The child projection is computed exactly where both of its consumers live:
9257
10239
  # codex argv with delegation on, and the contract's child clause, which renders under
9258
10240
  # the same condition.
9259
- mcp_registrations = review_mcp_servers(plan)
10241
+ # SWEEP exposes no MCP capability surface. Do not even compute a selected
10242
+ # registration set: a row that reaches argv through an inherited or review
10243
+ # path would contradict its strict empty MCP configuration.
10244
+ mcp_registrations = [] if sweep_main(plan) else review_mcp_servers(plan)
9260
10245
  child_registrations = (
9261
10246
  child_agent_registrations(plan)
9262
10247
  if plan["delegation"] and host == "codex"
@@ -9270,7 +10255,7 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9270
10255
  "-c", f"developer_instructions={json.dumps(contract)}",
9271
10256
  "-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
9272
10257
  ]
9273
- policy = plan["codex_execution_policy"]
10258
+ policy = "read-only" if sweep_main(plan) else plan["codex_execution_policy"]
9274
10259
  if policy == "bypass":
9275
10260
  policy_args = ["--dangerously-bypass-approvals-and-sandbox"]
9276
10261
  elif policy == STANDARD_POLICY:
@@ -9293,15 +10278,23 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9293
10278
  "-c", f"mcp_servers.{name}.args={json.dumps(server_args)}",
9294
10279
  ]
9295
10280
  return args
9296
- args = [
9297
- "--model", main["model"],
9298
- "--effort", main_effort,
9299
- "--append-system-prompt", contract,
9300
- ]
10281
+ args = ["--model", main["model"]]
10282
+ if main_effort is not None:
10283
+ args += ["--effort", main_effort]
10284
+ args += ["--append-system-prompt", contract]
9301
10285
  if plan["delegation"]:
9302
10286
  args += ["--agents", claude_agents(plan)]
9303
10287
  policy = plan["claude_permission_mode"]
9304
- if policy == "bypassPermissions":
10288
+ if sweep_main(plan):
10289
+ policy_args = [
10290
+ "--restricted",
10291
+ "--tools",
10292
+ "Read,Glob,Grep",
10293
+ "--strict-mcp-config",
10294
+ "--mcp-config",
10295
+ SWEEP_EMPTY_MCP_CONFIG,
10296
+ ]
10297
+ elif policy == "bypassPermissions":
9305
10298
  policy_args = ["--dangerously-skip-permissions"]
9306
10299
  elif policy == STANDARD_POLICY:
9307
10300
  policy_args = []
@@ -9349,8 +10342,8 @@ def print_summary(
9349
10342
  )
9350
10343
  else:
9351
10344
  print(
9352
- f" Main {plan['main_tier'].upper()} · {main['model']} · "
9353
- f"{tier_effort(plan, plan['main_tier'])}",
10345
+ f" Main {plan['main_tier'].upper()} · "
10346
+ f"{format_model_effort(main['model'], tier_effort(plan, plan['main_tier']), ' · ')}",
9354
10347
  file=stream,
9355
10348
  )
9356
10349
  # The same set run_contract and both argv builders take. This one did not branch at
@@ -9362,7 +10355,11 @@ def print_summary(
9362
10355
  for tier in active_tiers(plan):
9363
10356
  binding = plan["tiers"][tier]
9364
10357
  effort = tier_effort(plan, tier)
9365
- print(f" {tier.upper():<14} {binding['model']} · {effort}", file=stream)
10358
+ print(
10359
+ f" {tier.upper():<14} "
10360
+ f"{format_model_effort(binding['model'], effort, ' · ')}",
10361
+ file=stream,
10362
+ )
9366
10363
  inactive = inactive_tiers(plan)
9367
10364
  if inactive:
9368
10365
  # Named rather than dropped, matching the contract's wording, so the reader still
@@ -9421,9 +10418,9 @@ def print_summary(
9421
10418
  )
9422
10419
  tier_authority = (
9423
10420
  (
9424
- "base main + native child model/effort configured"
10421
+ "base main + native child bindings configured"
9425
10422
  if plan["host"] == "codex"
9426
- else "base main + child model/effort configured"
10423
+ else "base main + child bindings configured"
9427
10424
  )
9428
10425
  if plan["delegation"]
9429
10426
  else "base main only; no child binding is projected"
@@ -9439,7 +10436,16 @@ def print_summary(
9439
10436
  # applies no permission flag — the Projection line says so — and naming one here would
9440
10437
  # contradict it two lines later.
9441
10438
  if not plan_projects_nothing(plan):
9442
- if plan["host"] == "codex":
10439
+ if sweep_main(plan):
10440
+ if plan["host"] == "codex":
10441
+ print(" Execution SWEEP restricted · Codex sandbox read-only", file=stream)
10442
+ else:
10443
+ print(
10444
+ " Execution SWEEP restricted · Claude Read/Glob/Grep only · "
10445
+ "strict empty MCP",
10446
+ file=stream,
10447
+ )
10448
+ elif plan["host"] == "codex":
9443
10449
  print(f" Execution Codex {plan['codex_execution_policy']}", file=stream)
9444
10450
  else:
9445
10451
  print(
@@ -9447,6 +10453,18 @@ def print_summary(
9447
10453
  "(permission mode, not OS sandbox)",
9448
10454
  file=stream,
9449
10455
  )
10456
+ if private_corpus_enabled():
10457
+ print(
10458
+ " Global files "
10459
+ + t("global-instructions.summary").format(
10460
+ choice=t(
10461
+ "global-instructions.include.label"
10462
+ if plan.get("include_global_instructions", True)
10463
+ else "global-instructions.exclude.label"
10464
+ )
10465
+ ),
10466
+ file=stream,
10467
+ )
9450
10468
  print(f" Backend {command}", file=stream)
9451
10469
  if os.environ.get("AGENT_LAUNCH_DEBUG") == "1":
9452
10470
  print(" Argv " + json.dumps([command, *args]), file=stream)
@@ -9517,6 +10535,15 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
9517
10535
  )
9518
10536
  parser.add_argument("--yes", action="store_true", help="skip launch confirmation")
9519
10537
  parser.add_argument("--dry-run", action="store_true", help="print projection without launching")
10538
+ parser.add_argument("--corpus", action="store_true", help="open private Corpus Studio")
10539
+ parser.add_argument("--corpus-domains", help="domain selection for this activated session only")
10540
+ parser.add_argument("--corpus-native", action="store_true", help="opt into selected Claude corpus hook execution and native agents for this session only")
10541
+ parser.add_argument(
10542
+ "--exclude-global-instructions",
10543
+ action="store_true",
10544
+ help="omit only personal global AGENTS.md/CLAUDE.md files and imports for this private Claude session",
10545
+ )
10546
+ parser.add_argument("--resume-session", help="resume a host session with its pinned corpus")
9520
10547
  parser.add_argument(
9521
10548
  "--verify-receipts", nargs=2, metavar=("PLAN", "RECEIPTS"),
9522
10549
  help="adjudicate a ReviewPlan/v1 record against a ReviewReceipts/v1 bundle and exit",
@@ -9588,7 +10615,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
9588
10615
  hostless = (
9589
10616
  args.verify_receipts or args.emit_receipt or args.fold_receipts
9590
10617
  or args.check_adapter or args.compile_criterion or args.check_findings
9591
- or args.check_schema_flag
10618
+ or args.check_schema_flag or args.corpus
9592
10619
  )
9593
10620
  if args.host is None and not hostless:
9594
10621
  parser.error("the following arguments are required: host")
@@ -9828,6 +10855,9 @@ def _tolerate_narrow_stdout() -> None:
9828
10855
  def main(argv: list[str]) -> int:
9829
10856
  _tolerate_narrow_stdout()
9830
10857
  args = parse_args(drop_check_adapter_separator(argv))
10858
+ if args.corpus:
10859
+ open_corpus_studio()
10860
+ return 0
9831
10861
  if args.verify_receipts:
9832
10862
  return verify_receipts_command(
9833
10863
  *args.verify_receipts, config_path=args.config.expanduser(),
@@ -9850,8 +10880,59 @@ def main(argv: list[str]) -> int:
9850
10880
  raise LaunchError("--check-adapter takes a seat and then the adapter command")
9851
10881
  return check_adapter_command(args.check_adapter[0], args.check_adapter[1:])
9852
10882
  config_path = args.config.expanduser()
9853
- config = load_config(config_path)
10883
+ generation = None
10884
+ if private_corpus_enabled():
10885
+ bare = not args.preset and not args.custom and not args.dry_run and (
10886
+ args.no_tui or bool(args.forward) or os.environ.get("AGENT_LAUNCH_TUI") == "0"
10887
+ or not (sys.stdin.isatty() and sys.stdout.isatty()))
10888
+ config, generation = _load_private_config(
10889
+ config_path, replay_only=bool(args.resume_session or bare or args.preset == "vanilla"))
10890
+ else:
10891
+ config = load_config(config_path)
9854
10892
  command, bare_args = resolve_backend(config, args.host)
10893
+ if args.exclude_global_instructions and not args.resume_session:
10894
+ if not private_corpus_enabled():
10895
+ raise LaunchError(
10896
+ "excluding global instruction files requires an agent-bios activated session"
10897
+ )
10898
+ if args.host == "codex":
10899
+ raise LaunchError(
10900
+ "the current Codex adapter does not support a safe way to exclude only "
10901
+ "global instruction files"
10902
+ )
10903
+ # Make the CLI choice the starting state Custom sees, rather than a late override
10904
+ # that could contradict its visible row or the preset it saves. This is an
10905
+ # invocation-local copy: no profile or user preset is rewritten merely by using
10906
+ # the flag. The distill hub eventually starts its configured preset too.
10907
+ config = copy.deepcopy(config)
10908
+ for preset in config.get("presets", {}).values():
10909
+ if isinstance(preset, dict):
10910
+ preset["include_global_instructions"] = False
10911
+ if args.corpus_native and not private_corpus_enabled():
10912
+ raise LaunchError("--corpus-native requires a private installation; run agent-bios install first")
10913
+ if private_corpus_enabled():
10914
+ # Private templates retain the native host's config home and registrations.
10915
+ private_root = corpus_package_root()
10916
+ os.environ["AGENT_BIOS_PACKAGE_ROOT"] = str(private_root)
10917
+ config["hosts"]["codex"]["agent_templates"] = {
10918
+ tier: str(private_root / "codex/agents" / f"{tier}.toml") for tier in SPAWNABLE_TIERS
10919
+ }
10920
+ if args.resume_session:
10921
+ if (
10922
+ args.corpus_native
10923
+ or args.corpus_domains is not None
10924
+ or args.exclude_global_instructions
10925
+ ):
10926
+ raise LaunchError(
10927
+ "resume uses its pinned corpus/native/global-instruction configuration; "
10928
+ "start a new session to change it"
10929
+ )
10930
+ if not private_corpus_enabled():
10931
+ raise LaunchError("a corpus-pinned resume requires a private installation")
10932
+ store = corpus_store()
10933
+ import corpus_session
10934
+ return corpus_session.launch(command, [], store.state_root, args.host, {},
10935
+ resume_id=args.resume_session)
9855
10936
  nudge = session_distill_nudge(config)
9856
10937
  if nudge:
9857
10938
  print(f"agent-launch: {nudge}", file=sys.stderr)
@@ -9867,6 +10948,12 @@ def main(argv: list[str]) -> int:
9867
10948
  args.preset = "balanced"
9868
10949
  bypass = args.no_tui or bool(args.forward) or os.environ.get("AGENT_LAUNCH_TUI") == "0"
9869
10950
  if (bypass or not tty) and not args.preset and not args.custom and not args.dry_run:
10951
+ if (
10952
+ args.corpus_native
10953
+ or args.corpus_domains is not None
10954
+ or args.exclude_global_instructions
10955
+ ):
10956
+ raise LaunchError("corpus launch options require --preset NAME or an interactive configured launch")
9870
10957
  # The bare launch: nothing decides policy but the backend's own bare-launch
9871
10958
  # arguments. Every other path below projects the preset's policy instead.
9872
10959
  exec_backend(command, [*bare_args, *args.forward])
@@ -9924,7 +11011,39 @@ def main(argv: list[str]) -> int:
9924
11011
  # status projection afterwards.
9925
11012
  run_corpus_apply(request.selection)
9926
11013
  continue
11014
+ except CorpusStudioRequested:
11015
+ open_corpus_studio()
11016
+ continue
9927
11017
  validate_review_setup(plan)
11018
+ if not plan.get("include_global_instructions", True):
11019
+ if not private_corpus_enabled():
11020
+ raise LaunchError(
11021
+ "excluding global instruction files requires an agent-bios activated session"
11022
+ )
11023
+ if plan_projects_nothing(plan):
11024
+ raise LaunchError(
11025
+ "excluding global instruction files is unavailable for Vanilla; start a private configured session"
11026
+ )
11027
+ if args.host == "codex":
11028
+ raise LaunchError(
11029
+ "the current Codex adapter does not support a safe way to exclude only "
11030
+ "global instruction files"
11031
+ )
11032
+ snapshot = None
11033
+ store = None
11034
+ if private_corpus_enabled() and not plan_projects_nothing(plan):
11035
+ store = corpus_store()
11036
+ selected = None
11037
+ if args.corpus_domains is not None:
11038
+ raw = [x.strip() for x in args.corpus_domains.split(",") if x.strip()]
11039
+ if "none" in raw and raw != ["none"]:
11040
+ raise LaunchError("--corpus-domains none cannot be combined with other domains")
11041
+ selected = [] if raw == ["none"] else [
11042
+ x if x.startswith("@") else f"@agent-bios/core/{x}" for x in raw
11043
+ ]
11044
+ snapshot = _snapshot_from_config(store, config_path, generation, args.host, selected,
11045
+ dry_run=args.dry_run, native=args.corpus_native)
11046
+ plan["corpus_instruction_text"] = snapshot["instruction_text"]
9928
11047
  projected_args = project_args(plan, materialize_agents=not args.dry_run)
9929
11048
  collisions = forwarded_collisions(projected_args, args.forward)
9930
11049
  if collisions:
@@ -9934,7 +11053,23 @@ def main(argv: list[str]) -> int:
9934
11053
  f"preset or --custom, or launch bare (no --preset) to pass them through"
9935
11054
  )
9936
11055
  projected = [*projected_args, *args.forward]
11056
+ if snapshot is not None and args.dry_run:
11057
+ import corpus_session
11058
+ projected = corpus_session.compose_argv(
11059
+ command,
11060
+ projected,
11061
+ args.host,
11062
+ snapshot,
11063
+ include_global_instructions=plan["include_global_instructions"],
11064
+ )
9937
11065
  summary_stream = sys.stdout if tty or args.dry_run else sys.stderr
11066
+ if snapshot is not None:
11067
+ print(f" Corpus snapshot {snapshot['content_ref']} · private · next session only", file=summary_stream)
11068
+ if args.corpus_native:
11069
+ plugins = snapshot.get("assets", {}).get("claude_plugins", [])
11070
+ print(f" Native corpus opt-in: {len(plugins)} session-only plugin(s); selected hook code can execute.", file=summary_stream)
11071
+ for unavailable in snapshot.get("unavailable", []):
11072
+ print(f" Corpus unavailable: {unavailable}", file=summary_stream)
9938
11073
  print_summary(plan, command, projected, summary_stream, bool(args.forward))
9939
11074
  trigger = plan.get("trigger")
9940
11075
  if trigger:
@@ -9961,6 +11096,11 @@ def main(argv: list[str]) -> int:
9961
11096
  env = os.environ.copy()
9962
11097
  env["AGENT_LAUNCH_ACTIVE"] = "1"
9963
11098
  summary_stream.flush()
11099
+ if snapshot is not None:
11100
+ import corpus_session
11101
+ return corpus_session.launch(command, projected, store.state_root, args.host,
11102
+ snapshot, env=env,
11103
+ include_global_instructions=plan["include_global_instructions"])
9964
11104
  exec_backend(command, projected, env)
9965
11105
 
9966
11106
 
@@ -9970,6 +11110,9 @@ if __name__ == "__main__":
9970
11110
  except LaunchError as exc:
9971
11111
  print(f"agent-launch: {exc}", file=sys.stderr)
9972
11112
  raise SystemExit(2)
11113
+ except RuntimeError as exc:
11114
+ print(f"agent-launch: {type(exc).__name__}: {exc}", file=sys.stderr)
11115
+ raise SystemExit(2)
9973
11116
  except KeyboardInterrupt:
9974
11117
  print("\nCancelled.", file=sys.stderr)
9975
11118
  raise SystemExit(130)