agent-bios 0.15.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 (59) 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 +72 -39
  8. package/claude/guides/cli-multi-model-workflow.md +33 -15
  9. package/claude/guides/gpt-prompting.md +103 -39
  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 +72 -39
  26. package/codex/guides/cli-multi-model-workflow.md +33 -15
  27. package/codex/guides/gpt-prompting.md +103 -39
  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 +4 -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/install.sh +63 -18
  49. package/launch/agent-launch.py +1216 -179
  50. package/launch/agent-launch.toml +12 -16
  51. package/launch/i18n/en.toml +112 -7
  52. package/launch/i18n/ja.toml +112 -7
  53. package/launch/i18n/ko.toml +112 -7
  54. package/learn/collect-learning.py +46 -19
  55. package/learn/migrate-learnings.py +10 -1
  56. package/package.json +11 -3
  57. package/provenance.json +1 -1
  58. package/session-cost.py +22 -2
  59. package/wrappers/codex-helm.sh +3 -3
@@ -519,13 +519,17 @@ LEGACY_REVIEW_LOWERING = {
519
519
 
520
520
  @dataclass(frozen=True)
521
521
  class ReviewBinding:
522
- """Which verifier adjudicates. `model` and `effort` are inseparable: a `tier`
523
- 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
+ """
524
528
 
525
529
  provider: str
526
530
  host: str
527
531
  model: str
528
- effort: str
532
+ effort: str | None
529
533
  service_tier: str = DEFAULT_SERVICE_TIER
530
534
  tier: str | None = None
531
535
 
@@ -678,11 +682,18 @@ def parse_review_binding(
678
682
  else:
679
683
  if not isinstance(raw["model"], str) or not raw["model"]:
680
684
  raise LaunchError(f"{context}.model must be a non-empty string")
681
- 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:
682
691
  raise LaunchError(
683
692
  f"{context}.effort is required with model: a model without an effort would "
684
693
  "leave the verifier's rigour for the designer to pick at dispatch time"
685
694
  )
695
+ if is_haiku and "effort" in raw:
696
+ validate_effort("claude", raw["model"], raw["effort"], context)
686
697
  # Only now may an absent host end the parse. Everything above holds whatever hosts
687
698
  # exist, and leaving any of it below this line let a malformed binding through as
688
699
  # "unseated" — accepted today, rejected the day the host appears, and in the
@@ -717,7 +728,12 @@ def parse_review_binding(
717
728
  else:
718
729
  tier = None
719
730
  model = raw["model"]
720
- 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)
721
737
  return ReviewBinding(provider, host, model, effort, service_tier, tier)
722
738
 
723
739
 
@@ -889,6 +905,11 @@ def legacy_review_plan(setup: str, family: str) -> ReviewPlan:
889
905
  )
890
906
 
891
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
+
892
913
  # ── review methods: the declarative surface (DESIGN.md §1) ──────────────────
893
914
  # A METHOD says how to review. A CAPABILITY says how an installed tool exposes an
894
915
  # operation. A BINDING says which verifier adjudicates. The MECHANISM is derived by
@@ -1555,6 +1576,11 @@ def render_review_method(
1555
1576
  discipline clause to every row, method-blind, the way the severity translation is
1556
1577
  appended — an author never writes it and a slot never carries it."""
1557
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
+ )
1558
1584
  slots = {
1559
1585
  "command": mechanism.command or "",
1560
1586
  "model": binding.model,
@@ -1608,7 +1634,7 @@ def render_review_method(
1608
1634
  )
1609
1635
  rendered = (
1610
1636
  f"{method.method_id}: {body} "
1611
- f"[{mechanism.shape}; {binding.model}/{binding.effort}"
1637
+ f"[{mechanism.shape}; {format_model_effort(binding.model, binding.effort)}"
1612
1638
  f"{controls_clause(method)}{criterion_clause(criterion)}{severity_translation(method)}]"
1613
1639
  )
1614
1640
  # The assembled ROW, after the body. The body check above catches a SLOT value and says
@@ -1786,6 +1812,10 @@ def independence_grade(reviewer: ReviewBinding, main: ReviewBinding) -> str:
1786
1812
  return "provider_difference"
1787
1813
  if reviewer.model != main.model:
1788
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"
1789
1819
  if EFFORT_ORDER.index(reviewer.effort) > EFFORT_ORDER.index(main.effort):
1790
1820
  return "higher_effort"
1791
1821
  return "perspective_floor"
@@ -1845,7 +1875,8 @@ def _resolve_one(
1845
1875
  return ReviewMethodReport(
1846
1876
  method.method_id, STATUS_DROPPED, None,
1847
1877
  f"{detail}; it would have run on "
1848
- 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}",
1849
1880
  )
1850
1881
  grade = independence_grade(binding, main)
1851
1882
  # Accumulated, not assigned: more than one of these can be true at once, and the
@@ -1976,7 +2007,10 @@ def render_review_report(report: ReviewReport) -> str:
1976
2007
  if row.grade:
1977
2008
  head += f"/{row.grade}"
1978
2009
  if row.model:
1979
- 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
+ )
1980
2014
  if row.detail:
1981
2015
  head += f" — {row.detail}"
1982
2016
  lines.append(head)
@@ -4085,6 +4119,85 @@ def exec_backend(command: str, args: list[str], env: dict[str, str] | None = Non
4085
4119
  os.execve(command, [command, *args], os.environ.copy() if env is None else env)
4086
4120
 
4087
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
+
4088
4201
  def default_config_path() -> pathlib.Path:
4089
4202
  explicit = os.environ.get("AGENT_LAUNCH_CONFIG")
4090
4203
  if explicit:
@@ -4337,7 +4450,51 @@ def resolve_backend(config: dict[str, Any], host: str) -> tuple[str, list[str]]:
4337
4450
  return command, bare_value
4338
4451
 
4339
4452
 
4340
- 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
4341
4498
  if not isinstance(effort, str) or effort not in HOST_EFFORTS[host]:
4342
4499
  raise LaunchError(f"unsupported effort for {context}: {effort!r}")
4343
4500
  if host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
@@ -4517,6 +4674,11 @@ def plan_projects_nothing(plan: dict[str, Any]) -> bool:
4517
4674
  return plan.get("mode") == SWE_MODE
4518
4675
 
4519
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
+
4520
4682
  def active_tiers(plan: dict[str, Any]) -> tuple[str, ...]:
4521
4683
  """The tiers this launch actually binds, in TIER_ORDER: the main one, plus the
4522
4684
  children argv really carries.
@@ -4548,35 +4710,73 @@ def inactive_tier_reason(plan: dict[str, Any]) -> str:
4548
4710
  """Why the inactive tiers are inactive. Delegation-off removes every child at once
4549
4711
  and is reported as itself; otherwise the cause is that the main is not a spawnable
4550
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"
4551
4715
  if not plan["delegation"]:
4552
4716
  return "delegation is off"
4553
4717
  return f"the only spawnable child tiers are {', '.join(SPAWNABLE_TIERS)}"
4554
4718
 
4555
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
+
4556
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."""
4557
4737
  if plan is None:
4558
- return ["No setup selected."]
4738
+ return [t("setup.none")]
4559
4739
  if plan_projects_nothing(plan):
4560
4740
  return [
4561
- f"Host {plan['host']} | Preset {plan['label']}",
4562
- "Bare backend — no launch contract, no tier bindings, no review route,",
4563
- "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"),
4564
4743
  ]
4565
4744
  host = plan["host"]
4566
4745
  execution = (
4567
- plan["codex_execution_policy"]
4568
- if host == "codex"
4569
- 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
+ )
4570
4753
  )
4571
4754
  lines = [
4572
- f"Host {host} | Preset {plan['label']}",
4755
+ t("setup.host").format(host=host, preset=plan["label"]),
4573
4756
  # A composable plan has no legacy name and printed the literal "None" here — the
4574
4757
  # same defect print_summary carried, in the one place a user reads before
4575
4758
  # launching. The legacy branch keeps emitting the raw setup value, byte for byte.
4576
- f"Main {plan['main_tier'].upper()} | Review "
4577
- f"{review_setup_label(plan) if plan.get('review_report') else plan['review_setup']}",
4578
- 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
+ ),
4579
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
+ )
4580
4780
  for tier in active_tiers(plan):
4581
4781
  # The set run_contract, both argv builders and print_summary take. This panel
4582
4782
  # calls itself the "Current setup" and listed a binding row for every tier whatever
@@ -4585,10 +4785,10 @@ def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
4585
4785
  # TUI and the Custom hub actually show (round 22, #8). With delegation ON it still
4586
4786
  # printed a HELM row under a non-HELM main, which nothing binds (round 23, #1).
4587
4787
  binding = plan["tiers"][tier]
4588
- effort = (
4589
- 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), ' · ')}"
4590
4791
  )
4591
- lines.append(f"{tier.upper():<10} {binding['model']} / {effort}")
4592
4792
  inactive = inactive_tiers(plan)
4593
4793
  if inactive:
4594
4794
  # Named rather than dropped, matching the contract's and the summary's wording, so
@@ -4596,8 +4796,11 @@ def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
4596
4796
  # rather than "Children" because under delegation-on the one inactive tier is HELM,
4597
4797
  # which is precisely not a child.
4598
4798
  lines.append(
4599
- f"{'Inactive':<10} {', '.join(inactive)} — inactive, not projected because "
4600
- 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
+ )
4601
4804
  )
4602
4805
  return lines
4603
4806
 
@@ -4678,6 +4881,7 @@ def _build_app_class():
4678
4881
  from textual.screen import ModalScreen
4679
4882
  from textual.widgets import Input, OptionList, Static
4680
4883
  from textual.widgets.option_list import Option
4884
+ from rich.text import Text
4681
4885
  from textual.theme import Theme
4682
4886
 
4683
4887
  # Host-matched palettes so the preflight reads as the CLI it launches.
@@ -4724,6 +4928,12 @@ def _build_app_class():
4724
4928
  border: round $secondary; border-title-color: $secondary;
4725
4929
  border-title-style: bold; padding: 0 1; height: 5;
4726
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; }
4727
4937
  #al-corpus-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
4728
4938
  /* No cap and no scroller of its own: one nested scroll region inside another is a
4729
4939
  worse answer than a body that simply scrolls. */
@@ -4741,7 +4951,7 @@ def _build_app_class():
4741
4951
 
4742
4952
  def setup_panel(plan):
4743
4953
  panel = Static("\n".join(setup_summary_lines(plan)), id="al-setup")
4744
- panel.border_title = "Current setup"
4954
+ panel.border_title = t("tui.setup.title")
4745
4955
  release = version_label()
4746
4956
  if release:
4747
4957
  panel.border_subtitle = release
@@ -4752,12 +4962,23 @@ def _build_app_class():
4752
4962
  # on_mount focuses the option list, whose Up/Down are the only movement keys the
4753
4963
  # footer advertises, and the body holds no focusable widget of its own. A scrollbar
4754
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.
4755
4970
  BINDINGS = [
4756
- 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),
4757
4974
  Binding("q", "cancel", "cancel", priority=True),
4758
4975
  Binding("ctrl+c", "cancel", "cancel", priority=True),
4759
4976
  Binding("pagedown", "body_down", "scroll", priority=True),
4760
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),
4761
4982
  ]
4762
4983
 
4763
4984
  def _body(self):
@@ -4774,8 +4995,33 @@ def _build_app_class():
4774
4995
  if body is not None:
4775
4996
  body.scroll_page_up(animate=False)
4776
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
+
4777
5022
  def __init__(
4778
- 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,
4779
5025
  ):
4780
5026
  super().__init__()
4781
5027
  self._title = title
@@ -4785,6 +5031,10 @@ def _build_app_class():
4785
5031
  self._plan = plan
4786
5032
  self._preview = preview
4787
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
4788
5038
 
4789
5039
  def compose(self):
4790
5040
  yield Static(self._title, id="al-title")
@@ -4795,17 +5045,28 @@ def _build_app_class():
4795
5045
  with VerticalScroll(id=BODY_PANEL_ID):
4796
5046
  yield setup_panel(self._plan)
4797
5047
  if self._corpus:
4798
- yield Static("Corpus status", id="al-corpus-title")
5048
+ yield Static(t("tui.corpus.title"), id="al-corpus-title")
4799
5049
  yield Static("\n".join(self._corpus), id=CORPUS_PANEL_ID)
4800
- detail = Static("", id="al-detail")
4801
- 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")
4802
5053
  yield detail
4803
5054
  yield Static(
4804
- 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"
4805
5056
  )
4806
5057
  option_list = OptionList()
4807
5058
  for option in self._options:
4808
- 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()}]"
4809
5070
  option_list.add_option(
4810
5071
  Option(label, id=option.value, disabled=not option.enabled)
4811
5072
  )
@@ -4813,9 +5074,9 @@ def _build_app_class():
4813
5074
  # The panel keys are advertised only when there is a panel: a footer naming a
4814
5075
  # key that does nothing teaches the same wrong thing a screen stating an
4815
5076
  # unenforced rule does.
4816
- footer = "Up/Down move | Enter select | PgUp/PgDn scroll | "
4817
- footer += "Esc back | q cancel" if self._allow_back else "Esc cancel | q cancel"
4818
- yield Static(footer, id="al-footer")
5077
+ yield Static(
5078
+ menu_footer(self._allow_back, self._confirm is not None), id="al-footer"
5079
+ )
4819
5080
 
4820
5081
  def on_mount(self):
4821
5082
  option_list = self.query_one(OptionList)
@@ -4845,8 +5106,10 @@ def _build_app_class():
4845
5106
  option = self._options[index]
4846
5107
  detail = option.description
4847
5108
  if not option.enabled and option.unavailable_reason:
4848
- detail = f"{detail} Unavailable: {option.unavailable_reason}"
5109
+ detail = f"{detail} {t('prompt.unavailable')}: {option.unavailable_reason}"
4849
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)
4850
5113
  # Live-preview the highlighted option's effect in the setup panel.
4851
5114
  if self._preview is not None:
4852
5115
  try:
@@ -4858,7 +5121,25 @@ def _build_app_class():
4858
5121
  pass
4859
5122
 
4860
5123
  def on_option_list_option_selected(self, event):
4861
- 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)
4862
5143
 
4863
5144
  def action_back(self):
4864
5145
  self.dismiss(_UI_BACK if self._allow_back else _UI_CANCEL)
@@ -4882,8 +5163,8 @@ def _build_app_class():
4882
5163
  yield Static(self._label, id="al-title")
4883
5164
  yield setup_panel(self._plan)
4884
5165
  box = Static(
4885
- f"Current value: {self._default}\n"
4886
- "New value (leave blank to keep current):",
5166
+ t("tui.input.current").format(value=self._default)
5167
+ + "\n" + t("tui.input.new"),
4887
5168
  id="al-detail",
4888
5169
  )
4889
5170
  # The screen already knows what it is asking for; it labelled every
@@ -4892,10 +5173,8 @@ def _build_app_class():
4892
5173
  box.border_title = self._label
4893
5174
  yield box
4894
5175
  yield Static("", id="al-hdr")
4895
- yield Input(placeholder="leave blank to keep the current value")
4896
- yield Static(
4897
- "Enter confirm | Esc/Ctrl-C cancel | q + Enter cancel", id="al-footer"
4898
- )
5176
+ yield Input(placeholder=t("tui.input.placeholder"))
5177
+ yield Static(t("tui.input.footer"), id="al-footer")
4899
5178
 
4900
5179
  def on_mount(self):
4901
5180
  self.query_one(Input).focus()
@@ -4972,11 +5251,13 @@ class TextualUI:
4972
5251
  allow_back: bool,
4973
5252
  preview=None,
4974
5253
  corpus_lines: list[str] | None = None,
5254
+ confirm: str | None = None,
4975
5255
  ) -> str:
4976
5256
  result = self.app.call_from_thread(
4977
5257
  self.app.push_screen_wait,
4978
5258
  self._menu_screen(
4979
- title, options, default, allow_back, self.plan, preview, corpus_lines
5259
+ title, options, default, allow_back, self.plan, preview, corpus_lines,
5260
+ confirm,
4980
5261
  ),
4981
5262
  )
4982
5263
  if result == _UI_BACK:
@@ -5012,6 +5293,29 @@ def run_textual_flow(
5012
5293
  return value
5013
5294
 
5014
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
+
5015
5319
  def choose_lines(
5016
5320
  title: str,
5017
5321
  options: list[MenuOption],
@@ -5030,7 +5334,10 @@ def choose_lines(
5030
5334
  marker = "" if option.enabled else f" [{unavailable.lower()}]"
5031
5335
  selected = " *" if option.value == default and option.enabled else ""
5032
5336
  label = option.label
5033
- 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}")
5034
5341
  if not option.enabled:
5035
5342
  print(f" {unavailable}: {option.unavailable_reason}")
5036
5343
  while True:
@@ -5066,11 +5373,14 @@ def choose(
5066
5373
  allow_back: bool = False,
5067
5374
  preview=None,
5068
5375
  corpus_lines: list[str] | None = None,
5376
+ confirm: str | None = None,
5069
5377
  ) -> str:
5070
5378
  if not any(option.enabled for option in options):
5071
5379
  raise LaunchError(f"no available options for {title}")
5072
5380
  if ui is not None:
5073
- 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
+ )
5074
5384
  return choose_lines(title, options, default, allow_back, corpus_lines)
5075
5385
 
5076
5386
 
@@ -5086,7 +5396,9 @@ def read_input(prompt: str) -> str:
5086
5396
  def prompt_text(label: str, default: str, ui: TextualUI | None = None) -> str:
5087
5397
  if ui is not None:
5088
5398
  return ui.prompt_text(label, default)
5089
- 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()
5090
5402
  if value.lower() == "q":
5091
5403
  raise KeyboardInterrupt
5092
5404
  return value or default
@@ -5108,7 +5420,7 @@ def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> lis
5108
5420
 
5109
5421
  def resolve_review_for_plan(
5110
5422
  review: ReviewPlan, config: dict[str, Any], host: str, main_model: str,
5111
- main_effort: str, context: str, criterion: bool = False,
5423
+ main_effort: str | None, context: str, criterion: bool = False,
5112
5424
  ) -> tuple[dict, "ReviewReport | None"]:
5113
5425
  """(method registry, resolved report) for a review against the main seat.
5114
5426
 
@@ -5312,13 +5624,21 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5312
5624
  override_model = effective_tier_model(config, host, tier, all_overrides)
5313
5625
  if not isinstance(override_model, str) or not override_model:
5314
5626
  raise LaunchError(f"invalid override model: {preset_name}.tier_overrides.{host}.{tier}")
5315
- 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
+ )
5316
5634
  validate_effort(host, override_model, override_effort, f"{preset_name}.tier_overrides.{host}.{tier}")
5317
- 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
5318
5638
  main_tier = preset.get("main_tier")
5319
5639
  if not isinstance(main_tier, str) or main_tier not in tiers:
5320
5640
  raise LaunchError(f"invalid main_tier in preset {preset_name}: {main_tier}")
5321
- authored_frontier_effort = preset.get("frontier_effort", tiers["frontier"]["effort"])
5641
+ authored_frontier_effort = preset.get("frontier_effort", tiers["frontier"].get("effort"))
5322
5642
  frontier_effort = authored_frontier_effort
5323
5643
  if isinstance(frontier_effort, dict):
5324
5644
  frontier_effort = frontier_effort.get(host)
@@ -5377,11 +5697,30 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5377
5697
  f"tier_overrides.{other}.frontier.effort={authored_override!r}; FRONTIER's "
5378
5698
  "effort has one value — remove one of them"
5379
5699
  )
5380
- tiers["frontier"] = {**tiers["frontier"], "effort": frontier_effort}
5381
- delegation = preset.get("delegation", True)
5382
- 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):
5383
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
+ )
5384
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
+ )
5385
5724
  review_arms = read_review_arms(preset)
5386
5725
  # Authoring [review] IS the opt-in. Shipped presets stay on review_setup, so the
5387
5726
  # default launch is byte-identical; only a preset that asks for the composable
@@ -5391,7 +5730,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5391
5730
  # Resolved HERE, not at render time: a composable preset whose base panel has no
5392
5731
  # isolated mechanism is an invalid launch, and that has to fail while the plan is
5393
5732
  # being built rather than halfway through printing a contract.
5394
- 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")
5395
5734
  # The criterion-discipline toggle. A BOOLEAN, deliberately: the criterion itself is
5396
5735
  # per-review and rides the packet, which packet_sha256 binds — a preset carrying its
5397
5736
  # content would put per-review text into the per-launch contract the golden pins.
@@ -5470,6 +5809,13 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
5470
5809
  "review_report": review_report,
5471
5810
  "review_methods": review_methods,
5472
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,
5473
5819
  "codex_execution_policy": codex_policy,
5474
5820
  "claude_permission_mode": claude_policy,
5475
5821
  "tiers": tiers,
@@ -5506,7 +5852,10 @@ AUTHOR_COMPOSABLE = "__author_composable__"
5506
5852
  def review_binding_label(binding: "ReviewBinding | None") -> str:
5507
5853
  if binding is None:
5508
5854
  return "not set"
5509
- 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
+ )
5510
5859
  return f"{binding.provider} · {seat}"
5511
5860
 
5512
5861
 
@@ -5731,15 +6080,15 @@ def choose_review_binding(
5731
6080
  seats = [
5732
6081
  MenuOption(
5733
6082
  f"tier:{tier}",
5734
- f"{tier.upper()}: {tiers[tier]['model']} / {tiers[tier]['effort']}",
5735
- 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.",
5736
6085
  )
5737
6086
  for tier in TIER_ORDER
5738
6087
  if isinstance(tiers.get(tier), dict)
5739
6088
  ]
5740
6089
  seats.append(
5741
- MenuOption(OTHER_MODEL, "Custom model and effort",
5742
- "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.")
5743
6092
  )
5744
6093
  default_seat = (
5745
6094
  f"tier:{current.tier}"
@@ -5754,15 +6103,20 @@ def choose_review_binding(
5754
6103
  model = prompt_text(
5755
6104
  f"{title} — model", current.model if current is not None else "", ui
5756
6105
  )
5757
- effort = choose(
5758
- f"{title} — effort", effort_options(host, model),
5759
- # HOST_EFFORTS values are SETS: indexing one raised TypeError before
5760
- # the effort picker drew. EFFORT_ORDER gives a deterministic first
5761
- # supported effort, where a set gives none at all.
5762
- current.effort if current is not None else host_default_effort(host), ui,
5763
- allow_back=True,
5764
- )
5765
- 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}
5766
6120
  return parse_review_binding(raw, config, f"custom.{title}")
5767
6121
 
5768
6122
 
@@ -6529,6 +6883,10 @@ def host_default_effort(host: str) -> str:
6529
6883
 
6530
6884
 
6531
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 []
6532
6890
  descriptions = effort_descriptions()
6533
6891
  options = []
6534
6892
  for effort in EFFORT_ORDER:
@@ -6580,7 +6938,8 @@ def review_binding_fields(binding: ReviewBinding) -> dict[str, str]:
6580
6938
  fields["tier"] = binding.tier
6581
6939
  else:
6582
6940
  fields["model"] = binding.model
6583
- fields["effort"] = binding.effort
6941
+ if binding.effort is not None:
6942
+ fields["effort"] = binding.effort
6584
6943
  if binding.service_tier != DEFAULT_SERVICE_TIER:
6585
6944
  fields["service_tier"] = binding.service_tier
6586
6945
  return fields
@@ -6660,10 +7019,14 @@ def preset_from_plan(
6660
7019
  # hub calls "Save these settings".
6661
7020
  "mode": preset_mode(plan),
6662
7021
  "main_tier": plan["main_tier"],
6663
- "delegation": plan["delegation"],
7022
+ "delegation": plan.get("delegation_requested", plan["delegation"]),
6664
7023
  "codex_execution_policy": plan["codex_execution_policy"],
6665
7024
  "claude_permission_mode": plan["claude_permission_mode"],
6666
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
6667
7030
  if plan.get("criterion"):
6668
7031
  # Save As from a criterion-toggled plan silently dropped the discipline: the
6669
7032
  # routed-name guard covers same-name shadowing, not a fresh name, and the
@@ -6688,8 +7051,12 @@ def preset_from_plan(
6688
7051
  override: dict[str, str] = {}
6689
7052
  if plan["tiers"][tier]["model"] != default_tiers[tier]["model"]:
6690
7053
  override["model"] = plan["tiers"][tier]["model"]
6691
- if tier_effort(plan, tier) != default_tiers[tier]["effort"]:
6692
- 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
6693
7060
  if override:
6694
7061
  overrides[tier] = override
6695
7062
  # The OTHER host's overrides are carried, not re-derived. "Scoped to the plan's host"
@@ -6761,6 +7128,15 @@ def preset_from_plan(
6761
7128
  effort = authored_frontier[other]
6762
7129
  else:
6763
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
6764
7140
  if not isinstance(effort, str) or not effort:
6765
7141
  # NAMED, not skipped. `build_plan` refuses such a profile now, so this is
6766
7142
  # the door for a plan assembled some other way — and the alternative here
@@ -7237,9 +7613,8 @@ def save_preset(
7237
7613
  # that host nothing.
7238
7614
  continue
7239
7615
  try:
7240
- projected = project_args(
7241
- build_plan(rebuilt, host, name), materialize_agents=False
7242
- )
7616
+ rebuilt_plan = build_plan(rebuilt, host, name)
7617
+ projected = project_args(rebuilt_plan, materialize_agents=False)
7243
7618
  except LaunchError as exc:
7244
7619
  raise LaunchError(
7245
7620
  f"saving {name!r} would write a preset that no longer builds on "
@@ -7263,6 +7638,13 @@ def save_preset(
7263
7638
  f"{_at(intended, where)} ({len(intended)} argument(s)); nothing was "
7264
7639
  f"written"
7265
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
+ )
7266
7648
  # Through the shared primitive, which is where the temporary's removal on
7267
7649
  # failure lives. This site had the same two lines and no cleanup, so an
7268
7650
  # `os.replace` that failed left the complete candidate sitting beside the
@@ -7279,9 +7661,23 @@ def customize(
7279
7661
  ui: TextualUI | None = None,
7280
7662
  ) -> None:
7281
7663
  descriptions = tier_descriptions()
7282
- tier_options = [
7283
- MenuOption(tier, tier.upper(), descriptions[tier]) for tier in TIER_ORDER
7284
- ]
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
+ ]
7285
7681
  available_routes = route_availability(plan)
7286
7682
  cross = plan.get("review_family", "cross") == "cross"
7287
7683
  review_options = []
@@ -7387,6 +7783,17 @@ def customize(
7387
7783
  t("custom.policy.label").format(policy=policy_label),
7388
7784
  t("custom.policy.description"),
7389
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
+ ),
7390
7797
  ]
7391
7798
  for tier in TIER_ORDER:
7392
7799
  binding = plan["tiers"][tier]
@@ -7395,7 +7802,8 @@ def customize(
7395
7802
  f"tier:{tier}",
7396
7803
  # Model and effort only — the row is data, and the tier name it
7397
7804
  # leads with is an identifier the contract uses, not UI text.
7398
- f"{tier.upper()}: {binding['model']} / {tier_effort(plan, tier)}",
7805
+ f"{tier.upper()}: "
7806
+ f"{format_model_effort(binding['model'], tier_effort(plan, tier), ' / ')}",
7399
7807
  t("custom.tier.description").format(tier=tier.upper()),
7400
7808
  )
7401
7809
  )
@@ -7433,29 +7841,61 @@ def customize(
7433
7841
  if action == "main":
7434
7842
  plan["main_tier"] = choose(
7435
7843
  t("tier.title"),
7436
- tier_options,
7844
+ tier_options(),
7437
7845
  plan["main_tier"],
7438
7846
  ui,
7439
7847
  allow_back=True,
7440
- 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)
7441
7860
  )
7442
7861
  reseat_review(plan, config)
7443
7862
  elif action == "review":
7444
7863
  if plan["review_plan"].source == "composable":
7864
+ if sweep_main(plan):
7865
+ raise LaunchError(sweep_review_reason)
7445
7866
  review_editor(plan, config, ui, config_path)
7446
7867
  else:
7447
- chosen = choose(
7448
- "Review setup",
7449
- review_options
7450
- + [
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 = [
7451
7878
  MenuOption(
7452
- AUTHOR_COMPOSABLE,
7453
- "Compose review (explicit bindings)…",
7454
- "Author the base panel and each method's exact "
7455
- "provider/model/effort instead of picking a combination "
7456
- 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
+ ),
7457
7886
  )
7458
- ],
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],
7459
7899
  plan["review_setup"],
7460
7900
  ui,
7461
7901
  allow_back=True,
@@ -7489,12 +7929,47 @@ def customize(
7489
7929
  allow_back=True,
7490
7930
  preview=lambda value: {**plan, policy_field: value},
7491
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"
7492
7963
  except BackRequested:
7493
7964
  continue
7494
7965
 
7495
7966
  if action.startswith("tier:"):
7496
7967
  tier = action.split(":", 1)[1]
7497
7968
  binding = plan["tiers"][tier]
7969
+ configured_default_effort = (
7970
+ config.get("hosts", {}).get(plan["host"], {}).get("tiers", {})
7971
+ .get(tier, {}).get("effort")
7972
+ )
7498
7973
  # `binding` IS the plan's dict, so every assignment below lands on the live
7499
7974
  # plan the moment it is made. Choosing a model and then backing out of the
7500
7975
  # effort screen — which loops to the model screen — and backing out again left
@@ -7502,6 +7977,7 @@ def customize(
7502
7977
  # snapshot is what Escape restores; the plan is only allowed to keep an edit
7503
7978
  # that reached the end of the edit.
7504
7979
  original_binding = dict(binding)
7980
+ original_frontier_effort = plan["frontier_effort"] if tier == "frontier" else None
7505
7981
  cancelled = False
7506
7982
  while True:
7507
7983
  default_model = (
@@ -7520,12 +7996,11 @@ def customize(
7520
7996
  **plan,
7521
7997
  "tiers": {
7522
7998
  **plan["tiers"],
7523
- tier: {
7524
- **binding,
7525
- "model": binding["model"]
7526
- if value == OTHER_MODEL
7527
- else value,
7528
- },
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
+ ),
7529
8004
  },
7530
8005
  },
7531
8006
  )
@@ -7538,27 +8013,46 @@ def customize(
7538
8013
  )
7539
8014
  else:
7540
8015
  binding["model"] = chosen_model
7541
- current_effort = tier_effort(plan, tier)
7542
- try:
7543
- binding["effort"] = choose(
7544
- t("effort.title").format(tier=tier.upper()),
7545
- effort_options(plan["host"], binding["model"]),
7546
- current_effort,
7547
- ui,
7548
- allow_back=True,
7549
- preview=lambda value: {
7550
- **plan,
7551
- "frontier_effort": value
7552
- if tier == "frontier"
7553
- else plan["frontier_effort"],
7554
- "tiers": {
7555
- **plan["tiers"],
7556
- 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
+ },
7557
8048
  },
7558
- },
7559
- )
7560
- except BackRequested:
7561
- 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)
7562
8056
  break
7563
8057
  # Only when the edit was actually completed. FRONTIER's effort has two homes —
7564
8058
  # the tier binding and `plan["frontier_effort"]`, which is the preset's
@@ -7569,9 +8063,11 @@ def customize(
7569
8063
  if cancelled:
7570
8064
  binding.clear()
7571
8065
  binding.update(original_binding)
8066
+ if tier == "frontier":
8067
+ plan["frontier_effort"] = original_frontier_effort
7572
8068
  else:
7573
8069
  if tier == "frontier":
7574
- plan["frontier_effort"] = binding["effort"]
8070
+ plan["frontier_effort"] = binding.get("effort")
7575
8071
  # Editing the MAIN tier's binding moves the seat the review was graded
7576
8072
  # against just as surely as picking a different main tier does.
7577
8073
  if tier == plan["main_tier"]:
@@ -7722,6 +8218,12 @@ def load_corpus_status() -> dict[str, Any] | None:
7722
8218
  before it drew, and each new consumer would add another field to remember. The
7723
8219
  depth is unbounded, so the readers DEGRADE instead — see corpus_summary_lines.
7724
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
7725
8227
  try:
7726
8228
  data = json.loads(CORPUS_STATUS_PATH.read_text(encoding="utf-8"))
7727
8229
  except (OSError, ValueError):
@@ -7804,26 +8306,195 @@ def status_list(value: Any) -> list:
7804
8306
  return value if isinstance(value, list) else []
7805
8307
 
7806
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
+
7807
8441
  def _corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
7808
8442
  """The panel body proper. The label column is derived at render time from the
7809
8443
  widest label in the ACTIVE language and measured in display cells: a pad written
7810
8444
  for English cannot align a translated panel."""
7811
8445
  if status is None:
7812
8446
  return [t("panel.unprojected")]
7813
- labels = (t("panel.version.label"), t("panel.mechanisms.label"),
7814
- 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"))
7815
8471
  column = max(display_width(label) for label in labels) + 2
7816
8472
 
7817
8473
  def row(label: str, value: str) -> str:
7818
8474
  return label + " " * max(1, column - display_width(label)) + value
7819
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
+
7820
8492
  # `versions is None` is the projection saying "this install cannot know" — a packaged
7821
8493
  # install has no author version/ledger registry (compose/corpus-state.py). It is not
7822
8494
  # `[]`, which would mean a checkout whose registry is genuinely empty, and it must not
7823
8495
  # render as `0 placed · 0 versions`: a fabricated zero is worse than a blank, because
7824
8496
  # a reader cannot tell it from a real count. The domain rows below stay real, which is
7825
8497
  # the whole point of degrading rather than withholding the projection.
7826
- unavailable = status.get("versions") is None
7827
8498
  summary = status.get("summary") or {}
7828
8499
  current = status.get("current_version", "?")
7829
8500
  latest = status.get("latest_version", "?")
@@ -7849,11 +8520,18 @@ def _corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
7849
8520
  versions=len(status_list(status.get("versions"))),
7850
8521
  )),
7851
8522
  ]
7852
- domains = status.get("domains")
7853
- if isinstance(domains, dict):
7854
- applied = domains.get("applied")
7855
- applied = None if applied is None else status_list(applied)
7856
- 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
7857
8535
  else ", ".join(applied) or t("corpus.core-only")))
7858
8536
  last_apply = status.get("last_apply")
7859
8537
  if isinstance(last_apply, dict) and last_apply.get("outcome") not in (None, "applied"):
@@ -8152,6 +8830,10 @@ class CorpusApplyRequested(Exception):
8152
8830
  self.selection = selection
8153
8831
 
8154
8832
 
8833
+ class CorpusStudioRequested(Exception):
8834
+ """Release Textual's terminal before starting the standalone manager."""
8835
+
8836
+
8155
8837
  CORPUS_OPTION = "__corpus__"
8156
8838
  # A plain value, because the numbered prompt prints it as the default label.
8157
8839
  # Collision with a domain id is structurally impossible: domain slugs come from
@@ -8196,6 +8878,107 @@ def run_corpus_apply(selection: list[str]) -> None:
8196
8878
  print("\nagent-launch: corpus selection applied.", flush=True)
8197
8879
 
8198
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
+
8199
8982
  def corpus_checklist(ui: "TextualUI | None") -> None:
8200
8983
  """Toggle-and-apply loop over the optional domain packages.
8201
8984
 
@@ -8231,12 +9014,25 @@ def corpus_checklist(ui: "TextualUI | None") -> None:
8231
9014
  applied = set(status_list(applied_raw))
8232
9015
  never_applied = applied_raw is None
8233
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
8234
9026
  while True:
8235
9027
  options = [
8236
9028
  MenuOption(
8237
9029
  name,
8238
- f"[{'x' if name in toggles else ' '}] {name}",
8239
- 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),
8240
9036
  )
8241
9037
  for name in available
8242
9038
  ]
@@ -8256,7 +9052,12 @@ def corpus_checklist(ui: "TextualUI | None") -> None:
8256
9052
  try:
8257
9053
  choice = choose(
8258
9054
  t("corpus.title"), options, CORPUS_APPLY, ui, allow_back=True,
8259
- 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,
8260
9061
  )
8261
9062
  except BackRequested:
8262
9063
  return
@@ -8335,25 +9136,33 @@ def pick_mode_and_preset(
8335
9136
  dropping all the way back to the top mode picker.
8336
9137
  """
8337
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()
8338
9142
  mode = resume_mode
8339
9143
  while True:
9144
+ status = load_corpus_status()
9145
+ corpus_description = _corpus_launch_description(status)
8340
9146
  if mode is None:
8341
9147
  mode_options = [
8342
- 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),
8343
9150
  MenuOption(
8344
9151
  DEFAULT_PRESET_MODE,
8345
9152
  t("mode.builder.label"),
8346
- t("mode.builder.description"),
9153
+ t("mode.builder.description") + "\n\n" + corpus_description,
8347
9154
  ),
8348
9155
  MenuOption(
8349
9156
  DISTILL_MODE,
8350
9157
  t("mode.distill.label"),
8351
- t("mode.distill.description"),
9158
+ t("mode.distill.description") + "\n\n" + corpus_description,
8352
9159
  ),
8353
9160
  ]
8354
9161
  mode_options.append(
8355
9162
  MenuOption(
8356
- 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"),
8357
9166
  )
8358
9167
  )
8359
9168
  if config_path is not None:
@@ -8396,9 +9205,15 @@ def pick_mode_and_preset(
8396
9205
  DEFAULT_PRESET_MODE,
8397
9206
  ui,
8398
9207
  preview=preview_mode,
8399
- corpus_lines=corpus_summary_lines(load_corpus_status()),
9208
+ corpus_lines=corpus_summary_lines(status),
8400
9209
  )
8401
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
8402
9217
  _corpus_screen(ui, lambda: corpus_checklist(ui))
8403
9218
  mode = None
8404
9219
  continue
@@ -8417,7 +9232,9 @@ def pick_mode_and_preset(
8417
9232
  MenuOption(
8418
9233
  name,
8419
9234
  data["label"],
8420
- 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,
8421
9238
  )
8422
9239
  for name, data in presets.items()
8423
9240
  if preset_mode(data) == mode
@@ -8426,8 +9243,9 @@ def pick_mode_and_preset(
8426
9243
  options.append(
8427
9244
  MenuOption(
8428
9245
  CUSTOM_PRESET,
8429
- "Custom",
8430
- "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,
8431
9249
  )
8432
9250
  )
8433
9251
  default = mode_default_preset(presets, mode) or CUSTOM_PRESET
@@ -8471,7 +9289,7 @@ def pick_mode_and_preset(
8471
9289
 
8472
9290
  try:
8473
9291
  selected = choose(
8474
- "Preset", options, default, ui, allow_back=True, preview=preview_preset
9292
+ t("preset.title"), options, default, ui, allow_back=True, preview=preview_preset
8475
9293
  )
8476
9294
  except BackRequested:
8477
9295
  mode = None
@@ -8550,8 +9368,8 @@ def validate_review_setup(plan: dict[str, Any]) -> None:
8550
9368
  effective_review(plan)
8551
9369
 
8552
9370
 
8553
- def tier_effort(plan: dict[str, Any], tier: str) -> str:
8554
- 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")
8555
9373
 
8556
9374
 
8557
9375
  def child_agent_registrations(
@@ -8597,6 +9415,11 @@ def child_agent_registrations(
8597
9415
  raise LaunchError(f"Codex agent template requires description: {source}")
8598
9416
  data["model"] = plan["tiers"][tier]["model"]
8599
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"]
8600
9423
  lines = []
8601
9424
  for key, value in data.items():
8602
9425
  if not isinstance(value, (str, int, float, bool)):
@@ -8739,7 +9562,9 @@ def _cross_review_route(
8739
9562
  main_family = LEGACY_HOST_FAMILY[plan["host"]][0]
8740
9563
  review_family = LEGACY_HOST_FAMILY[review_host][0]
8741
9564
  review_bindings = ", ".join(
8742
- 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
+ )}"
8743
9568
  for tier in TIER_ORDER
8744
9569
  if tier in plan["review_tiers"]
8745
9570
  )
@@ -8853,7 +9678,9 @@ def run_contract(
8853
9678
  # the session never received, which is round 18 #9's defect under a second cause
8854
9679
  # (round 23, #1).
8855
9680
  bindings = ", ".join(
8856
- 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
+ )}"
8857
9684
  for tier in active_tiers(plan)
8858
9685
  )
8859
9686
  tiers_clause = f"tiers: {bindings}"
@@ -8875,8 +9702,9 @@ def run_contract(
8875
9702
  # set after one of the two reasons it can be inactive, and it happens to be the
8876
9703
  # branch that can hold the tier the name is false of (round 24, #12).
8877
9704
  tiers_clause = (
8878
- f"tiers: {main_tier}={plan['tiers'][main_tier]['model']}/"
8879
- 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 "
8880
9708
  f"({', '.join(inactive)}) are "
8881
9709
  f"inactive and not projected because {inactive_tier_reason(plan)}"
8882
9710
  )
@@ -8933,7 +9761,8 @@ def run_contract(
8933
9761
  # is the point: a command or an argument carrying a space would otherwise read as two.
8934
9762
  # Empty on every route that registers nothing, which includes every legacy one.
8935
9763
  registrations = (
8936
- 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)
8937
9766
  )
8938
9767
  mcp_clause = ""
8939
9768
  if registrations:
@@ -8978,17 +9807,17 @@ def run_contract(
8978
9807
  )
8979
9808
  if plan["delegation"]:
8980
9809
  authority = (
8981
- "Main and native child model/effort defaults are config-projected. Use the "
9810
+ "Main and native child bindings are config-projected. Use the "
8982
9811
  "installed codex-run adapter when a separate child root or stricter reach "
8983
9812
  "boundary matters."
8984
9813
  if plan["host"] == "codex"
8985
- else "Main and child model/effort defaults are CLI-projected."
9814
+ else "Main and child bindings are CLI-projected."
8986
9815
  )
8987
9816
  else:
8988
9817
  authority = (
8989
- "The main model/effort default is config-projected; no child binding is."
9818
+ "The main binding is config-projected; no child binding is."
8990
9819
  if plan["host"] == "codex"
8991
- 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."
8992
9821
  )
8993
9822
  mission = plan.get("mission")
8994
9823
  if mission and plan.get("trigger"):
@@ -8996,9 +9825,11 @@ def run_contract(
8996
9825
  mission_prefix = f"Mission: {mission} " if mission else ""
8997
9826
  prose = (
8998
9827
  f"{mission_prefix}"
8999
- f"LaunchPlan: main={main_tier} ({plan['tiers'][main_tier]['model']}/"
9000
- f"{tier_effort(plan, main_tier)}); {tiers_clause}. "
9001
- 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)}"
9002
9833
  f"{child_clause}"
9003
9834
  f"{execution_clause(plan)}"
9004
9835
  f"{review_section}"
@@ -9058,6 +9889,18 @@ def execution_clause(plan: dict[str, Any]) -> str:
9058
9889
 
9059
9890
  `standard` is named rather than omitted, because "no flag" is itself the posture: the
9060
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
+ )
9061
9904
  if plan["host"] == "codex":
9062
9905
  policy = plan["codex_execution_policy"]
9063
9906
  if policy == STANDARD_POLICY:
@@ -9075,7 +9918,7 @@ def execution_clause(plan: dict[str, Any]) -> str:
9075
9918
  return f"Execution=claude permission mode {policy} (not an OS sandbox). "
9076
9919
 
9077
9920
 
9078
- def delegation_clause(delegation: bool) -> str:
9921
+ def delegation_clause(plan: dict[str, Any]) -> str:
9079
9922
  """How the contract states delegation — and, when it is on, WHOSE decision that was.
9080
9923
 
9081
9924
  Claude Code's own Opus-5 prompt bundle appends "Do not call the AgentTool unless the
@@ -9089,7 +9932,12 @@ def delegation_clause(delegation: bool) -> str:
9089
9932
  corpus's standing spawn policy is their instruction — so the contract says so in the
9090
9933
  words that clause is looking for. Nothing is overridden; a fact that was already true
9091
9934
  is simply stated where the reader can see it."""
9092
- 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"]:
9093
9941
  return "Delegation=off. "
9094
9942
  return (
9095
9943
  "Delegation=on — the user requested delegation by selecting this launch, and their "
@@ -9097,19 +9945,43 @@ def delegation_clause(delegation: bool) -> str:
9097
9945
  )
9098
9946
 
9099
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
+
9100
9958
  def claude_agents(plan: dict[str, Any]) -> str:
9101
9959
  roles = {}
9102
9960
  for tier in SPAWNABLE_TIERS:
9103
9961
  binding = plan["tiers"][tier]
9104
- roles[tier] = {
9105
- "description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
9106
- "prompt": (
9107
- f"Act as the bounded {tier.upper()} role at requested effort "
9108
- f"{tier_effort(plan, tier)}. Return evidence and verification; stay in scope."
9109
- ),
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,
9110
9978
  "model": binding["model"],
9111
- "effort": tier_effort(plan, tier),
9112
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
9113
9985
  return json.dumps(roles, separators=(",", ":"))
9114
9986
 
9115
9987
 
@@ -9117,6 +9989,10 @@ MCP_STDIO_ADAPTER = "mcp-stdio-v1"
9117
9989
  # The argv every stdio MCP capability registered here uses today. A capability
9118
9990
  # needing different arguments is a registry data addition, not a branch.
9119
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=(",", ":"))
9120
9996
 
9121
9997
 
9122
9998
  def review_mcp_servers(plan: dict[str, Any]) -> list[tuple[str, str, list[str]]]:
@@ -9362,7 +10238,10 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9362
10238
  # L7). The child projection is computed exactly where both of its consumers live:
9363
10239
  # codex argv with delegation on, and the contract's child clause, which renders under
9364
10240
  # the same condition.
9365
- 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)
9366
10245
  child_registrations = (
9367
10246
  child_agent_registrations(plan)
9368
10247
  if plan["delegation"] and host == "codex"
@@ -9376,7 +10255,7 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9376
10255
  "-c", f"developer_instructions={json.dumps(contract)}",
9377
10256
  "-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
9378
10257
  ]
9379
- policy = plan["codex_execution_policy"]
10258
+ policy = "read-only" if sweep_main(plan) else plan["codex_execution_policy"]
9380
10259
  if policy == "bypass":
9381
10260
  policy_args = ["--dangerously-bypass-approvals-and-sandbox"]
9382
10261
  elif policy == STANDARD_POLICY:
@@ -9399,15 +10278,23 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
9399
10278
  "-c", f"mcp_servers.{name}.args={json.dumps(server_args)}",
9400
10279
  ]
9401
10280
  return args
9402
- args = [
9403
- "--model", main["model"],
9404
- "--effort", main_effort,
9405
- "--append-system-prompt", contract,
9406
- ]
10281
+ args = ["--model", main["model"]]
10282
+ if main_effort is not None:
10283
+ args += ["--effort", main_effort]
10284
+ args += ["--append-system-prompt", contract]
9407
10285
  if plan["delegation"]:
9408
10286
  args += ["--agents", claude_agents(plan)]
9409
10287
  policy = plan["claude_permission_mode"]
9410
- 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":
9411
10298
  policy_args = ["--dangerously-skip-permissions"]
9412
10299
  elif policy == STANDARD_POLICY:
9413
10300
  policy_args = []
@@ -9455,8 +10342,8 @@ def print_summary(
9455
10342
  )
9456
10343
  else:
9457
10344
  print(
9458
- f" Main {plan['main_tier'].upper()} · {main['model']} · "
9459
- 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']), ' · ')}",
9460
10347
  file=stream,
9461
10348
  )
9462
10349
  # The same set run_contract and both argv builders take. This one did not branch at
@@ -9468,7 +10355,11 @@ def print_summary(
9468
10355
  for tier in active_tiers(plan):
9469
10356
  binding = plan["tiers"][tier]
9470
10357
  effort = tier_effort(plan, tier)
9471
- 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
+ )
9472
10363
  inactive = inactive_tiers(plan)
9473
10364
  if inactive:
9474
10365
  # Named rather than dropped, matching the contract's wording, so the reader still
@@ -9527,9 +10418,9 @@ def print_summary(
9527
10418
  )
9528
10419
  tier_authority = (
9529
10420
  (
9530
- "base main + native child model/effort configured"
10421
+ "base main + native child bindings configured"
9531
10422
  if plan["host"] == "codex"
9532
- else "base main + child model/effort configured"
10423
+ else "base main + child bindings configured"
9533
10424
  )
9534
10425
  if plan["delegation"]
9535
10426
  else "base main only; no child binding is projected"
@@ -9545,7 +10436,16 @@ def print_summary(
9545
10436
  # applies no permission flag — the Projection line says so — and naming one here would
9546
10437
  # contradict it two lines later.
9547
10438
  if not plan_projects_nothing(plan):
9548
- 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":
9549
10449
  print(f" Execution Codex {plan['codex_execution_policy']}", file=stream)
9550
10450
  else:
9551
10451
  print(
@@ -9553,6 +10453,18 @@ def print_summary(
9553
10453
  "(permission mode, not OS sandbox)",
9554
10454
  file=stream,
9555
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
+ )
9556
10468
  print(f" Backend {command}", file=stream)
9557
10469
  if os.environ.get("AGENT_LAUNCH_DEBUG") == "1":
9558
10470
  print(" Argv " + json.dumps([command, *args]), file=stream)
@@ -9623,6 +10535,15 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
9623
10535
  )
9624
10536
  parser.add_argument("--yes", action="store_true", help="skip launch confirmation")
9625
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")
9626
10547
  parser.add_argument(
9627
10548
  "--verify-receipts", nargs=2, metavar=("PLAN", "RECEIPTS"),
9628
10549
  help="adjudicate a ReviewPlan/v1 record against a ReviewReceipts/v1 bundle and exit",
@@ -9694,7 +10615,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
9694
10615
  hostless = (
9695
10616
  args.verify_receipts or args.emit_receipt or args.fold_receipts
9696
10617
  or args.check_adapter or args.compile_criterion or args.check_findings
9697
- or args.check_schema_flag
10618
+ or args.check_schema_flag or args.corpus
9698
10619
  )
9699
10620
  if args.host is None and not hostless:
9700
10621
  parser.error("the following arguments are required: host")
@@ -9934,6 +10855,9 @@ def _tolerate_narrow_stdout() -> None:
9934
10855
  def main(argv: list[str]) -> int:
9935
10856
  _tolerate_narrow_stdout()
9936
10857
  args = parse_args(drop_check_adapter_separator(argv))
10858
+ if args.corpus:
10859
+ open_corpus_studio()
10860
+ return 0
9937
10861
  if args.verify_receipts:
9938
10862
  return verify_receipts_command(
9939
10863
  *args.verify_receipts, config_path=args.config.expanduser(),
@@ -9956,8 +10880,59 @@ def main(argv: list[str]) -> int:
9956
10880
  raise LaunchError("--check-adapter takes a seat and then the adapter command")
9957
10881
  return check_adapter_command(args.check_adapter[0], args.check_adapter[1:])
9958
10882
  config_path = args.config.expanduser()
9959
- 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)
9960
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)
9961
10936
  nudge = session_distill_nudge(config)
9962
10937
  if nudge:
9963
10938
  print(f"agent-launch: {nudge}", file=sys.stderr)
@@ -9973,6 +10948,12 @@ def main(argv: list[str]) -> int:
9973
10948
  args.preset = "balanced"
9974
10949
  bypass = args.no_tui or bool(args.forward) or os.environ.get("AGENT_LAUNCH_TUI") == "0"
9975
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")
9976
10957
  # The bare launch: nothing decides policy but the backend's own bare-launch
9977
10958
  # arguments. Every other path below projects the preset's policy instead.
9978
10959
  exec_backend(command, [*bare_args, *args.forward])
@@ -10030,7 +11011,39 @@ def main(argv: list[str]) -> int:
10030
11011
  # status projection afterwards.
10031
11012
  run_corpus_apply(request.selection)
10032
11013
  continue
11014
+ except CorpusStudioRequested:
11015
+ open_corpus_studio()
11016
+ continue
10033
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"]
10034
11047
  projected_args = project_args(plan, materialize_agents=not args.dry_run)
10035
11048
  collisions = forwarded_collisions(projected_args, args.forward)
10036
11049
  if collisions:
@@ -10040,7 +11053,23 @@ def main(argv: list[str]) -> int:
10040
11053
  f"preset or --custom, or launch bare (no --preset) to pass them through"
10041
11054
  )
10042
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
+ )
10043
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)
10044
11073
  print_summary(plan, command, projected, summary_stream, bool(args.forward))
10045
11074
  trigger = plan.get("trigger")
10046
11075
  if trigger:
@@ -10067,6 +11096,11 @@ def main(argv: list[str]) -> int:
10067
11096
  env = os.environ.copy()
10068
11097
  env["AGENT_LAUNCH_ACTIVE"] = "1"
10069
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"])
10070
11104
  exec_backend(command, projected, env)
10071
11105
 
10072
11106
 
@@ -10076,6 +11110,9 @@ if __name__ == "__main__":
10076
11110
  except LaunchError as exc:
10077
11111
  print(f"agent-launch: {exc}", file=sys.stderr)
10078
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)
10079
11116
  except KeyboardInterrupt:
10080
11117
  print("\nCancelled.", file=sys.stderr)
10081
11118
  raise SystemExit(130)