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