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.
- package/DEPENDENCIES.md +35 -12
- package/README.md +346 -31
- package/claude/CLAUDE.md +2 -2
- package/claude/agents/frontier.md +1 -1
- package/claude/agents/sweep.md +3 -3
- package/claude/agents/workhorse.md +2 -2
- package/claude/guides/claude-prompting.md +72 -39
- package/claude/guides/cli-multi-model-workflow.md +33 -15
- package/claude/guides/gpt-prompting.md +103 -39
- package/claude/guides/review-request.md +27 -0
- package/claude/guides/session-distill-workflow.md +54 -2
- package/claude/guides/slide-writing/RUNBOOK.md +137 -0
- package/claude/guides/slide-writing/scripts/pair.py +979 -0
- package/claude/guides/slide-writing/scripts/render.mjs +82 -0
- package/claude/guides/slide-writing.md +195 -0
- package/claude/guides/svg-visualization-guide.md +9 -0
- package/claude/guides/verification-discipline.md +5 -1
- package/claude/hooks/tooling-gotchas-hook.py +7 -5
- package/codex/AGENTS.md +2 -2
- package/codex/agents/frontier.toml +2 -1
- package/codex/agents/reviewer.toml +1 -1
- package/codex/agents/sweep.toml +3 -3
- package/codex/agents/workhorse.toml +1 -1
- package/codex/config-additions.toml +1 -1
- package/codex/guides/claude-prompting.md +72 -39
- package/codex/guides/cli-multi-model-workflow.md +33 -15
- package/codex/guides/gpt-prompting.md +103 -39
- package/codex/guides/review-request.md +27 -0
- package/codex/guides/session-distill-workflow.md +54 -2
- package/codex/guides/slide-writing/RUNBOOK.md +137 -0
- package/codex/guides/slide-writing/scripts/pair.py +979 -0
- package/codex/guides/slide-writing/scripts/render.mjs +82 -0
- package/codex/guides/slide-writing.md +195 -0
- package/codex/guides/svg-visualization-guide.md +9 -0
- package/codex/guides/verification-discipline.md +5 -1
- package/compose/assemble.py +290 -14
- package/compose/bootstrap/SKILL.md +119 -0
- package/compose/check-domains.py +102 -9
- package/compose/corpus-state.py +4 -0
- package/compose/corpus.py +387 -0
- package/compose/corpus_catalog.py +882 -0
- package/compose/corpus_install.py +1617 -0
- package/compose/corpus_session.py +726 -0
- package/compose/corpus_store.py +1414 -0
- package/compose/corpus_transaction.py +236 -0
- package/compose/corpus_ui.py +644 -0
- package/compose/domains.json +101 -100
- package/install.sh +63 -18
- package/launch/agent-launch.py +1216 -179
- package/launch/agent-launch.toml +12 -16
- package/launch/i18n/en.toml +112 -7
- package/launch/i18n/ja.toml +112 -7
- package/launch/i18n/ko.toml +112 -7
- package/learn/collect-learning.py +46 -19
- package/learn/migrate-learnings.py +10 -1
- package/package.json +11 -3
- package/provenance.json +1 -1
- package/session-cost.py +22 -2
- package/wrappers/codex-helm.sh +3 -3
package/launch/agent-launch.py
CHANGED
|
@@ -519,13 +519,17 @@ LEGACY_REVIEW_LOWERING = {
|
|
|
519
519
|
|
|
520
520
|
@dataclass(frozen=True)
|
|
521
521
|
class ReviewBinding:
|
|
522
|
-
"""Which verifier adjudicates.
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 +=
|
|
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
|
|
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 ["
|
|
4738
|
+
return [t("setup.none")]
|
|
4559
4739
|
if plan_projects_nothing(plan):
|
|
4560
4740
|
return [
|
|
4561
|
-
|
|
4562
|
-
"
|
|
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
|
-
|
|
4568
|
-
if
|
|
4569
|
-
else
|
|
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
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
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
|
-
|
|
4589
|
-
|
|
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
|
-
|
|
4600
|
-
|
|
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 = "
|
|
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("
|
|
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("
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
4886
|
-
"
|
|
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="
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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"]
|
|
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"]
|
|
5381
|
-
|
|
5382
|
-
|
|
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]
|
|
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 =
|
|
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']
|
|
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
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6692
|
-
|
|
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
|
-
|
|
7241
|
-
|
|
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
|
-
|
|
7283
|
-
|
|
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()}:
|
|
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: {
|
|
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
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
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
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
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
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
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
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
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
|
-
|
|
7561
|
-
|
|
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
|
|
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
|
-
|
|
7814
|
-
|
|
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
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
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
|
-
|
|
8239
|
-
|
|
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=[
|
|
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"),
|
|
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,
|
|
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(
|
|
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",
|
|
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
|
-
"
|
|
8430
|
-
"
|
|
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
|
-
"
|
|
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]
|
|
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}={
|
|
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}={
|
|
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}={
|
|
8879
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9814
|
+
else "Main and child bindings are CLI-projected."
|
|
8986
9815
|
)
|
|
8987
9816
|
else:
|
|
8988
9817
|
authority = (
|
|
8989
|
-
"The main
|
|
9818
|
+
"The main binding is config-projected; no child binding is."
|
|
8990
9819
|
if plan["host"] == "codex"
|
|
8991
|
-
else "The main
|
|
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} ({
|
|
9000
|
-
|
|
9001
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
9105
|
-
|
|
9106
|
-
"
|
|
9107
|
-
|
|
9108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9404
|
-
"--effort", main_effort
|
|
9405
|
-
|
|
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
|
|
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()} ·
|
|
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(
|
|
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
|
|
10421
|
+
"base main + native child bindings configured"
|
|
9531
10422
|
if plan["host"] == "codex"
|
|
9532
|
-
else "base main + child
|
|
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
|
|
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
|
-
|
|
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)
|