agent-bios 0.9.9 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/DEPENDENCIES.md +3 -3
  2. package/README.md +10 -2
  3. package/claude/CLAUDE.md +4 -41
  4. package/claude/guides/cli-multi-model-workflow.md +7 -7
  5. package/claude/guides/coding-staged-workflow.md +51 -49
  6. package/claude/guides/concept-economy.md +187 -0
  7. package/claude/guides/documentation-hygiene.md +112 -0
  8. package/claude/guides/review-request.md +9 -7
  9. package/claude/guides/session-distill-workflow.md +3 -3
  10. package/claude/guides/tooling-gotchas.md +10 -0
  11. package/claude/guides/verification-discipline.md +166 -0
  12. package/claude/hooks/tooling-gotchas-hook.py +323 -13
  13. package/codex/AGENTS.md +4 -41
  14. package/codex/guides/cli-multi-model-workflow.md +7 -7
  15. package/codex/guides/coding-staged-workflow.md +51 -49
  16. package/codex/guides/concept-economy.md +187 -0
  17. package/codex/guides/documentation-hygiene.md +112 -0
  18. package/codex/guides/review-request.md +9 -7
  19. package/codex/guides/session-distill-workflow.md +3 -3
  20. package/codex/guides/tooling-gotchas.md +10 -0
  21. package/codex/guides/verification-discipline.md +166 -0
  22. package/compose/assemble.py +10 -1
  23. package/compose/check-domains.py +882 -6
  24. package/compose/domains.json +10 -44
  25. package/install.sh +176 -16
  26. package/launch/agent-launch.py +790 -173
  27. package/launch/agent-launch.toml +56 -107
  28. package/launch/i18n/en.toml +66 -0
  29. package/launch/i18n/ja.toml +63 -0
  30. package/launch/i18n/ko.toml +63 -0
  31. package/package.json +9 -4
  32. package/provenance.json +1 -0
  33. package/wrappers/codex-run.sh +1 -1
  34. package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import argparse
7
7
  import copy
8
+ import fcntl
8
9
  import hashlib
9
10
  import json
10
11
  import os
@@ -87,6 +88,176 @@ USER_PRESETS_HEADER = (
87
88
  "# survive upgrades. Shipped presets live in the deployed profiles.toml and\n"
88
89
  "# are overridden by a preset of the same name here.\n"
89
90
  )
91
+ # ── UI language (interface text only) ─────────────────────────────────────────
92
+ # Deploy-managed catalogs, sibling directory of the config: launch/i18n/ in a
93
+ # checkout, ~/.config/agent-launch/i18n/ deployed. `en` is the reference
94
+ # key-set; the parity gate holds ko/ja to exactly it, both directions, so an
95
+ # incomplete catalog is unshippable rather than silently hybrid. Chrome
96
+ # language never reaches a rendered launch contract — a golden scenario pins
97
+ # that. Internal codes only here; "JP" is a display label for `ja`.
98
+ I18N_DIR_NAME = "i18n"
99
+ I18N_LANGUAGES = ("en", "ko", "ja")
100
+ I18N_ENV = "AGENT_LAUNCH_LANG"
101
+
102
+
103
+ def _module_catalog(language: str) -> dict[str, str]:
104
+ """The catalog shipped beside this module, or empty. The module-adjacent set
105
+ is the default for in-process consumers that never reach main() (gate legs
106
+ import the module and drive flows directly); load_catalogs() replaces it
107
+ with the config-sibling deployment when one exists."""
108
+ path = pathlib.Path(__file__).resolve().parent / I18N_DIR_NAME / f"{language}.toml"
109
+ try:
110
+ raw = tomllib.loads(path.read_text())
111
+ except (OSError, tomllib.TOMLDecodeError):
112
+ return {}
113
+ return {key: value for key, value in raw.items() if isinstance(value, str)}
114
+
115
+
116
+ _CATALOG_EN: dict[str, str] = _module_catalog("en")
117
+ _CATALOG: dict[str, str] = _CATALOG_EN
118
+ # Keys asked for but absent from the active catalog. The completeness gate makes
119
+ # this state unshippable; the set is the runtime tripwire for the state the gate
120
+ # should have made impossible (surfaced by the Stage-2 banner).
121
+ _CATALOG_MISSING: set[str] = set()
122
+
123
+
124
+ def i18n_dir(config_path: pathlib.Path) -> pathlib.Path:
125
+ return config_path.with_name(I18N_DIR_NAME)
126
+
127
+
128
+ # User-owned launcher preferences, sibling to the other two `.local.toml` files
129
+ # and owned the same way: written by the launcher on an explicit selection only,
130
+ # never deployed, never overwritten by an install. One table, one key, until
131
+ # another preference earns its place.
132
+ USER_LAUNCHER_NAME = "launcher.local.toml"
133
+
134
+
135
+ def user_launcher_path(config_path: pathlib.Path) -> pathlib.Path:
136
+ return config_path.with_name(USER_LAUNCHER_NAME)
137
+
138
+
139
+ def saved_language(config_path: pathlib.Path) -> str | None:
140
+ """The persisted UI language, or None. A malformed file or value is a
141
+ loud notice and None — a preference must never block launching."""
142
+ path = user_launcher_path(config_path)
143
+ if not path.is_file():
144
+ return None
145
+ try:
146
+ value = tomllib.loads(path.read_text()).get("ui", {}).get("language")
147
+ except (OSError, tomllib.TOMLDecodeError) as exc:
148
+ print(f"agent-launch: cannot read {path}: {exc}; using en", file=sys.stderr)
149
+ return None
150
+ if value in I18N_LANGUAGES:
151
+ return value
152
+ if value is not None:
153
+ print(
154
+ f"agent-launch: {path} names language {value!r}, not one of "
155
+ f"{'/'.join(I18N_LANGUAGES)}; using en", file=sys.stderr,
156
+ )
157
+ return None
158
+
159
+
160
+ def save_language(config_path: pathlib.Path, language: str) -> None:
161
+ """Persist the UI language atomically. The whole file is launcher-authored
162
+ (one table, one key), so a full rewrite loses nothing user-written."""
163
+ path = user_launcher_path(config_path)
164
+ content = (
165
+ "# agent-launch user preferences, written by the launcher.\n"
166
+ "# agent-bios install never deploys or verifies this file.\n"
167
+ f'\n[ui]\nlanguage = "{language}"\n'
168
+ )
169
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
170
+ temporary.write_text(content)
171
+ os.replace(temporary, path)
172
+
173
+
174
+ def ui_language(config_path: pathlib.Path | None = None) -> str:
175
+ """The active UI language. Env override first (what gates pin), then the
176
+ user preference file, then English."""
177
+ explicit = os.environ.get(I18N_ENV, "")
178
+ if explicit in I18N_LANGUAGES:
179
+ return explicit
180
+ if explicit:
181
+ print(
182
+ f"agent-launch: {I18N_ENV}={explicit!r} is not one of "
183
+ f"{'/'.join(I18N_LANGUAGES)}; using en", file=sys.stderr,
184
+ )
185
+ return "en"
186
+ if config_path is not None:
187
+ preferred = saved_language(config_path)
188
+ if preferred:
189
+ return preferred
190
+ return "en"
191
+
192
+
193
+ def load_catalogs(config_path: pathlib.Path) -> None:
194
+ """Load the English reference UI text catalog and the active language's catalog from
195
+ the config's sibling i18n directory, falling back to the module-adjacent set.
196
+
197
+ A selected language whose catalog fails to LOAD falls to English with a loud
198
+ notice — never silently. English missing from BOTH homes leaves keys
199
+ rendering as themselves, which the completeness gate makes unreachable on a
200
+ shipped tree."""
201
+ global _CATALOG, _CATALOG_EN
202
+ directory = i18n_dir(config_path)
203
+
204
+ def read(language: str) -> dict[str, str] | None:
205
+ path = directory / f"{language}.toml"
206
+ try:
207
+ raw = tomllib.loads(path.read_text())
208
+ except OSError:
209
+ return None
210
+ except tomllib.TOMLDecodeError as exc:
211
+ print(
212
+ f"agent-launch: UI text catalog {path} does not parse: {exc}",
213
+ file=sys.stderr,
214
+ )
215
+ return None
216
+ return {key: value for key, value in raw.items() if isinstance(value, str)}
217
+
218
+ deployed_en = read("en")
219
+ if deployed_en is not None:
220
+ _CATALOG_EN = deployed_en
221
+ elif not _CATALOG_EN:
222
+ print(
223
+ f"agent-launch: no UI text catalog for en under {directory} or beside "
224
+ "the launcher; UI text keys will render as themselves", file=sys.stderr,
225
+ )
226
+ language = ui_language(config_path)
227
+ if language == "en":
228
+ _CATALOG = _CATALOG_EN
229
+ return
230
+ selected = read(language)
231
+ if selected is None:
232
+ selected = _module_catalog(language)
233
+ if selected:
234
+ # A deployed catalog older than the launcher is invisible to the
235
+ # source-tree completeness gate; the skew is loud here instead of
236
+ # surfacing as silently-English strings one key at a time.
237
+ skew = len(set(_CATALOG_EN) - set(selected))
238
+ if skew:
239
+ print(
240
+ f"agent-launch: UI text catalog {language} is missing {skew} "
241
+ "key(s) vs en (deployed version skew?); those strings render "
242
+ "in English", file=sys.stderr,
243
+ )
244
+ _CATALOG = selected
245
+ return
246
+ print(
247
+ f"agent-launch: no UI text catalog for {language}; showing English",
248
+ file=sys.stderr,
249
+ )
250
+ _CATALOG = _CATALOG_EN
251
+
252
+
253
+ def t(key: str) -> str:
254
+ """One UI text string. Missing keys fall to English and trip the tripwire;
255
+ a key missing from English too renders as itself rather than crashing."""
256
+ value = _CATALOG.get(key)
257
+ if value is not None:
258
+ return value
259
+ _CATALOG_MISSING.add(key)
260
+ return _CATALOG_EN.get(key, key)
90
261
 
91
262
 
92
263
  @dataclass(frozen=True)
@@ -136,66 +307,41 @@ REVIEW_SETUPS = {
136
307
  "codex": "Use Codex's built-in /review when review gates fire.",
137
308
  },
138
309
  },
139
- "onto": {
140
- "label": "onto-mcp",
141
- "requirements": {"codex": ("onto",), "claude": ("onto",)},
142
- "description": (
143
- "Structured multi-lens review through onto-mcp. Best for concept/ontology "
144
- "and design work; needs onto installed."
145
- ),
146
- "contract": "Use onto-mcp as the structured review lens when review gates fire.",
147
- },
148
310
  "ultracode": {
149
- "label": "Ultracode",
150
- "requirements": {"codex": ("ultracode-for-codex",), "claude": ("ultracode-for-codex",)},
311
+ "label": "Deep review",
312
+ "requirements": {"codex": ("codex-exec",), "claude": ("codex-exec",)},
151
313
  "description": (
152
- "Workflow-orchestrated review fanned out over many agents. Best for "
153
- "exhaustive audits; slowest and most expensive."
314
+ "The review family's deep mechanism: plain codex exec at ultra effort on "
315
+ "a Codex seat, the Claude workflow when review lands on a Claude seat."
154
316
  ),
155
317
  "contract": (
156
- "Run the configured Ultracode CLI as a Codex-backed review route when "
157
- "review gates fire."
158
- ),
159
- },
160
- "hybrid": {
161
- "label": "Hybrid",
162
- "requirements": {
163
- "codex": ("onto", "ultracode-for-codex"),
164
- "claude": ("onto", "ultracode-for-codex"),
165
- },
166
- "description": (
167
- "onto + native + Ultracode together, acting on their union. Widest net for "
168
- "risky work; missing routes degrade rather than fail."
318
+ "Run the Codex CLI in non-interactive exec mode (codex exec -s read-only -m "
319
+ '<frontier-tier model> -c model_reasoning_effort="ultra", self-contained packet '
320
+ "on stdin) as a Codex-backed deep review route when review gates fire."
169
321
  ),
170
- "contract": {
171
- "codex": "Use onto plus native and Codex-backed Ultracode review kinds; act on their union.",
172
- "claude": "Use onto plus native multi-perspective and Codex-backed Ultracode review kinds; act on their union.",
173
- },
174
322
  },
175
323
  }
176
324
 
177
325
  # Each review setup decomposes into review routes. "native" is same-model
178
- # multi-perspective subagent review (needs delegation); "onto"/"ultracode" are
179
- # external routes gated on a resolvable capability command. A missing external
326
+ # multi-perspective subagent review (needs delegation); "ultracode" is an
327
+ # external route gated on a resolvable capability command. A missing external
180
328
  # route degrades to native instead of failing closed; only the delegation
181
329
  # contradiction (native required with delegation off) stays fail-closed.
182
330
  # The legacy route token "ultracode" is SETUP vocabulary, not a capability id — it was
183
331
  # used as one, so renaming the tool silently repointed the legacy route at whatever else
184
332
  # held the bare name. Bound explicitly here: the token stays frozen, the tool it resolves
185
333
  # to is stated once, and the two can differ.
186
- LEGACY_ULTRACODE_CAPABILITY = "ultracode-for-codex"
334
+ LEGACY_ULTRACODE_CAPABILITY = "codex-exec"
187
335
  # Every legacy route that gates on an installed tool, and which capability that is. The
188
336
  # token and the id were the same string in four places — availability, the install hint,
189
337
  # the executable line and the lowering — so each was a separate way for a rename to
190
338
  # repoint a frozen route at the wrong tool. One map, one place to be right.
191
- LEGACY_ROUTE_CAPABILITY = {"onto": "onto", "ultracode": LEGACY_ULTRACODE_CAPABILITY}
339
+ LEGACY_ROUTE_CAPABILITY = {"ultracode": LEGACY_ULTRACODE_CAPABILITY}
192
340
  REVIEW_ROUTES = {
193
341
  "none": (),
194
342
  "native-panel": ("native",),
195
343
  "slash-review": ("slash",),
196
- "onto": ("onto",),
197
344
  "ultracode": ("ultracode",),
198
- "hybrid": ("onto", "native", "ultracode"),
199
345
  }
200
346
  # Routes that are host-native review commands: they always resolve (no capability
201
347
  # to install) but run on the main's own family, so cross-family mode cannot dispatch
@@ -204,14 +350,11 @@ SAME_FAMILY_ROUTES = {"slash"}
204
350
  ROUTE_LABELS = {
205
351
  "native": "native same-model multi-perspective review",
206
352
  "slash": "host-native slash-command review",
207
- "onto": "onto structured-lens review",
208
- "ultracode": "Codex-backed Ultracode review",
353
+ "ultracode": "Codex-backed deep exec review",
209
354
  }
210
355
  # review_family selects whether review runs on the opposite model family (cross,
211
356
  # the default) or the same family as the main (same, today's projection).
212
357
  REVIEW_FAMILIES = {"cross", "same"}
213
- # onto llmOverride provider enum (onto MCP tool schema, settings-chain).
214
- ONTO_PROVIDERS = {"openai", "anthropic", "grok", "lmstudio"}
215
358
  # The opposite model family for cross review, keyed on the launch host.
216
359
  REVIEW_HOST = {"codex": "claude", "claude": "codex"}
217
360
  # The family a legacy cross contract ADVERTISES for each host, and the provider that
@@ -251,9 +394,7 @@ LEGACY_REVIEW_LOWERING = {
251
394
  "none": (False, ()),
252
395
  "native-panel": (True, ()),
253
396
  "slash-review": (True, ("host-review",)),
254
- "onto": (True, ("onto",)),
255
- "ultracode": (True, ("ultracode-for-codex",)),
256
- "hybrid": (True, ("onto", "ultracode-for-codex")),
397
+ "ultracode": (True, ("codex-exec",)),
257
398
  }
258
399
 
259
400
 
@@ -625,7 +766,7 @@ def legacy_review_plan(setup: str, family: str) -> ReviewPlan:
625
766
  # core and never configured — configuration names no MCP, CLI, API, or subagent.
626
767
  #
627
768
  # Stage 3 is the falsifying stage: if a shipped method needed an `if method ==
628
- # "onto"` branch anywhere below, the seam would be wrong. There is deliberately no
769
+ # "codex-exec"` branch anywhere below, the seam would be wrong. There is deliberately no
629
770
  # such branch, and gates/check_parity.py proves it by projecting a method whose name
630
771
  # is generated at runtime and appears in no source file.
631
772
 
@@ -746,10 +887,10 @@ def parse_review_method(method_id: str, raw: Any, context: str) -> ReviewMethod:
746
887
  # Two separate facts, and the gap between them is where a wrong map hid. `emits` is a
747
888
  # claim about the WORLD — what this tool actually reports — and only someone who knows
748
889
  # the tool can supply it. `severity_map` is OUR decision about how to translate it.
749
- # With the map alone, "is this complete?" is undecidable: the shipped ultracode
750
- # descriptor carried the canonical ladder copied from the panel, mapping not one of
751
- # the P0..P3 values the tool really emits, and nothing could tell that from a correct
752
- # identity map like onto's — whose vocabulary genuinely IS the ladder.
890
+ # With the map alone, "is this complete?" is undecidable: a shipped descriptor once
891
+ # carried the canonical ladder copied from the panel, mapping not one of the P0..P3
892
+ # values its tool really emitted, and nothing could tell that from a correct
893
+ # identity map whose vocabulary genuinely IS the ladder.
753
894
  severity_emits = raw.get("severity_emits")
754
895
  if (
755
896
  not isinstance(severity_emits, list)
@@ -2028,16 +2169,11 @@ def load_config(path: pathlib.Path) -> dict[str, Any]:
2028
2169
  hint = capability.get("install")
2029
2170
  if hint is not None and (not isinstance(hint, str) or not hint):
2030
2171
  raise LaunchError(f"capabilities.{name}.install must be a non-empty string")
2031
- onto = capabilities.get("onto")
2032
- if onto is not None and (
2033
- not isinstance(onto.get("command"), str) or not onto["command"]
2034
- ):
2035
- raise LaunchError("capabilities.onto.command must be a non-empty string")
2036
- ultracode = capabilities.get("ultracode-for-codex")
2037
- if ultracode is not None and (
2038
- not isinstance(ultracode.get("command"), str) or not ultracode["command"]
2172
+ codex_exec = capabilities.get("codex-exec")
2173
+ if codex_exec is not None and (
2174
+ not isinstance(codex_exec.get("command"), str) or not codex_exec["command"]
2039
2175
  ):
2040
- raise LaunchError("capabilities.ultracode-for-codex.command must be a non-empty string")
2176
+ raise LaunchError("capabilities.codex-exec.command must be a non-empty string")
2041
2177
  for host_name, host_data in data["hosts"].items():
2042
2178
  if not isinstance(host_data, dict):
2043
2179
  raise LaunchError(f"host must be a table: {host_name}")
@@ -2050,40 +2186,7 @@ def load_config(path: pathlib.Path) -> dict[str, Any]:
2050
2186
  raise LaunchError(
2051
2187
  f"hosts.{host_name}.models must be a non-empty list of strings"
2052
2188
  )
2053
- onto_review = host_data.get("onto_review")
2054
- if onto_review is not None:
2055
- if not isinstance(onto_review, dict):
2056
- raise LaunchError(f"hosts.{host_name}.onto_review must be a table")
2057
- if onto_review.get("provider") not in ONTO_PROVIDERS:
2058
- choices = ", ".join(sorted(ONTO_PROVIDERS))
2059
- raise LaunchError(
2060
- f"hosts.{host_name}.onto_review.provider must be one of: {choices}"
2061
- )
2062
- if not isinstance(onto_review.get("model"), str) or not onto_review["model"]:
2063
- raise LaunchError(
2064
- f"hosts.{host_name}.onto_review.model must be a non-empty string"
2065
- )
2066
- # The cross contract names the family of the review HOST while the override
2067
- # takes its provider from this pin, so pinning another provider makes the
2068
- # contract contradict itself — it says "run EVERY review route on
2069
- # Anthropic/Claude" and hands the reviewer a grok seat. Fail closed rather
2070
- # than advertise a family the review does not run on. Conditional because
2071
- # hosts.<h>.provider is optional on purpose (see below): a profile that never
2072
- # declares one is not asked to prove a match it cannot express.
2073
- host_provider = host_data.get("provider")
2074
- if (
2075
- isinstance(host_provider, str)
2076
- and host_provider
2077
- and onto_review["provider"] != host_provider
2078
- ):
2079
- raise LaunchError(
2080
- f"hosts.{host_name}.onto_review.provider is {onto_review['provider']!r} "
2081
- f"but that host speaks for {host_provider!r}; a cross-family contract "
2082
- f"names the host's family, so an onto seat on another provider would "
2083
- f"advertise a family it does not run on"
2084
- )
2085
- # OPTIONAL on purpose. Distinct from onto_review.provider (which pins what onto
2086
- # is *told* to run), this is the host's own family identity. Every schema-v1
2189
+ # OPTIONAL on purpose. This is the host's own family identity. Every schema-v1
2087
2190
  # profile written before it existed omits it, and requiring it would reject
2088
2191
  # those profiles during load — before any preset is even selected, so a
2089
2192
  # legacy-only config would stop launching at all. A host without a provider
@@ -2093,12 +2196,11 @@ def load_config(path: pathlib.Path) -> dict[str, Any]:
2093
2196
  if provider is not None and (not isinstance(provider, str) or not provider):
2094
2197
  raise LaunchError(f"hosts.{host_name}.provider must be a non-empty string")
2095
2198
  # A host whose legacy family is hardcoded may only declare the provider that
2096
- # family IS. Checking the onto pin against this host's own declaration is not
2097
- # enough on its own: declare `hosts.claude.provider = "grok"` with a matching
2098
- # pin and the two agree while the contract still prints "run EVERY review route
2099
- # on Anthropic/Claude" over a grok seat. Anchoring to LEGACY_HOST_FAMILY closes
2100
- # that, because the sentence and the provider now read from one place. Still
2101
- # only when declared — the field stays optional for schema-v1 profiles.
2199
+ # family IS: declare `hosts.claude.provider = "grok"` and the cross contract
2200
+ # still prints "run EVERY review route on Anthropic/Claude" over a grok seat.
2201
+ # Anchoring to LEGACY_HOST_FAMILY closes that, because the sentence and the
2202
+ # provider now read from one place. Still only when declared — the field stays
2203
+ # optional for schema-v1 profiles.
2102
2204
  if provider is not None and host_name in LEGACY_HOST_FAMILY:
2103
2205
  expected = LEGACY_HOST_FAMILY[host_name][1]
2104
2206
  if provider != expected:
@@ -2210,11 +2312,15 @@ def cross_helm_command(plan: dict[str, Any]) -> str | None:
2210
2312
 
2211
2313
 
2212
2314
  def cross_ultracode_command(plan: dict[str, Any]) -> str | None:
2213
- """The cross-family ultracode reviewer command, or None. For a claude main it
2214
- is the Codex-backed ultracode-for-codex (the $ultracode-for-codex skill); for
2215
- a codex main it is the claude backend (Claude Code /workflows ultracode)."""
2315
+ """The cross-family deep reviewer command, or None. For a claude main it is
2316
+ the Codex CLI itself (codex-exec, non-interactive exec mode); for a codex
2317
+ main it is the claude backend (Claude Code /workflows ultracode)."""
2216
2318
  if plan["review_host"] == "codex":
2217
- value = plan.get("capabilities", {}).get("ultracode-for-codex", {}).get("command", "")
2319
+ # Through capability_command, because the shipped codex-exec command is
2320
+ # `${backend}` — rereading the raw value would resolve the literal token.
2321
+ value = capability_command(
2322
+ plan.get("capabilities", {}).get(LEGACY_ULTRACODE_CAPABILITY, {}), plan, "codex"
2323
+ )
2218
2324
  else:
2219
2325
  value = plan["review_backend"]
2220
2326
  try:
@@ -2225,24 +2331,24 @@ def cross_ultracode_command(plan: dict[str, Any]) -> str | None:
2225
2331
 
2226
2332
  def route_availability(plan: dict[str, Any]) -> dict[str, bool]:
2227
2333
  """Which review routes can run right now. In same-family mode native gates on
2228
- delegation and onto/ultracode on a resolvable capability. In cross-family mode
2229
- native/ultracode gate on the opposite-family dispatcher resolving, and onto on
2230
- a mounted onto plus a review_onto pin to flip its family."""
2334
+ delegation and ultracode on a resolvable capability. In cross-family mode
2335
+ native/ultracode gate on the opposite-family dispatcher resolving."""
2231
2336
  capabilities = plan.get("capabilities", {})
2232
2337
  if plan.get("review_family", "cross") == "same":
2233
2338
  available = {"native": bool(plan["delegation"]), "slash": True}
2234
- for route, capability in LEGACY_ROUTE_CAPABILITY.items():
2235
- available[route] = _resolves(capabilities.get(capability, {}).get("command", ""))
2339
+ for route, capability_id in LEGACY_ROUTE_CAPABILITY.items():
2340
+ # Through capability_commands, because the shipped codex-exec command is
2341
+ # `${backend}`: the raw value is a token, not something PATH can answer.
2342
+ available[route] = any(
2343
+ _resolves(command)
2344
+ for command in capability_commands(capabilities.get(capability_id, {}), plan)
2345
+ )
2236
2346
  return available
2237
- onto_ok = _resolves(capabilities.get("onto", {}).get("command", "")) and bool(
2238
- plan.get("review_onto")
2239
- )
2240
2347
  return {
2241
2348
  "native": cross_native_command(plan) is not None,
2242
2349
  # The host's own review command exists, but only for its own family; the
2243
2350
  # cross branch of effective_review routes it to the PROPOSED floor.
2244
2351
  "slash": True,
2245
- "onto": onto_ok,
2246
2352
  "ultracode": cross_ultracode_command(plan) is not None,
2247
2353
  }
2248
2354
 
@@ -2960,10 +3066,9 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
2960
3066
  # Cross review needs the opposite host + backend; without them fall back to
2961
3067
  # same-family review (surfaced as review_family=same) rather than crashing.
2962
3068
  review_family = "same"
2963
- review_tiers, review_onto, review_backend = {}, None, ""
3069
+ review_tiers, review_backend = {}, ""
2964
3070
  else:
2965
3071
  review_tiers = copy.deepcopy(opposite["tiers"])
2966
- review_onto = copy.deepcopy(opposite.get("onto_review"))
2967
3072
  review_backend = opposite_backend["command"]
2968
3073
  return {
2969
3074
  "host": host,
@@ -2991,7 +3096,6 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
2991
3096
  "review_family": review_family,
2992
3097
  "review_host": review_host,
2993
3098
  "review_tiers": review_tiers,
2994
- "review_onto": review_onto,
2995
3099
  "review_backend": review_backend,
2996
3100
  "agent_templates": copy.deepcopy(config["hosts"][host].get("agent_templates")),
2997
3101
  "capabilities": copy.deepcopy(config.get("capabilities", {})),
@@ -3335,6 +3439,336 @@ def method_availability(method: ReviewMethod, config: dict[str, Any]) -> str:
3335
3439
  return method.description
3336
3440
 
3337
3441
 
3442
+ USER_METHODS_HEADER = (
3443
+ "# agent-launch user review methods (and the capabilities they drive).\n"
3444
+ "# Merged at launch and validated exactly like shipped entries; never\n"
3445
+ "# deployed or overwritten by agent-bios install. A name that collides\n"
3446
+ "# with a shipped one is refused at launch rather than preferred.\n"
3447
+ )
3448
+
3449
+
3450
+ def _toml_str(value: str) -> str:
3451
+ """Deterministic TOML string: JSON escaping is a valid TOML basic string."""
3452
+ return json.dumps(value, ensure_ascii=False)
3453
+
3454
+
3455
+ def _toml_str_list(values: list[str]) -> str:
3456
+ return "[" + ", ".join(_toml_str(v) for v in values) + "]"
3457
+
3458
+
3459
+ def _render_registration(answers: dict[str, Any]) -> str:
3460
+ """The candidate's TOML block. Serialization only — every judgement about the
3461
+ content belongs to the reader that will accept or refuse it."""
3462
+ lines = [""]
3463
+ if answers.get("capability"):
3464
+ cap = answers["capability"]
3465
+ lines += [f"[capabilities.{cap['name']}]", f"command = {_toml_str(cap['command'])}"]
3466
+ if cap.get("install"):
3467
+ lines.append(f"install = {_toml_str(cap['install'])}")
3468
+ lines.append(
3469
+ "offers = [{ operation = " + _toml_str(cap["operation"])
3470
+ + ", adapter = " + _toml_str(cap["adapter"])
3471
+ + ", hosts = " + _toml_str_list(cap["hosts"]) + " }]"
3472
+ )
3473
+ lines.append("")
3474
+ lines += [
3475
+ f"[review_methods.{answers['id']}]",
3476
+ f"label = {_toml_str(answers['label'])}",
3477
+ f"description = {_toml_str(answers['description'])}",
3478
+ ]
3479
+ if answers.get("capability"):
3480
+ lines += [
3481
+ f"capability = {_toml_str(answers['capability']['name'])}",
3482
+ f"operation = {_toml_str(answers['capability']['operation'])}",
3483
+ ]
3484
+ lines += [
3485
+ f"instructions = {_toml_str(answers['instructions'])}",
3486
+ 'output = "review-v1"',
3487
+ f"perspectives = {_toml_str_list(answers['perspectives'])}",
3488
+ f"trials = {answers['trials']}",
3489
+ 'order = "fixed"',
3490
+ "swap_augmentation = false",
3491
+ 'aggregation = "union"',
3492
+ f"severity_emits = {_toml_str_list(answers['severity_emits'])}",
3493
+ "severity_map = { "
3494
+ + ", ".join(
3495
+ f"{_toml_str(k)} = {_toml_str(v)}" for k, v in answers["severity_map"].items()
3496
+ )
3497
+ + " }",
3498
+ "",
3499
+ ]
3500
+ return "\n".join(lines)
3501
+
3502
+
3503
+ def _trial_registration(
3504
+ config_path: pathlib.Path, block: str
3505
+ ) -> str | None:
3506
+ """Prove the candidate through the REAL pipeline: a temp dir holding a copy of
3507
+ the live config plus the user's local files with the block appended, run
3508
+ through the genuine load_config — same merge order, same validation, same
3509
+ collision refusal. Returns None on acceptance, the reader's message on refusal."""
3510
+ trial = pathlib.Path(tempfile.mkdtemp(prefix="agent-launch-register-"))
3511
+ shutil.copy(config_path, trial / config_path.name)
3512
+ for sibling in (USER_PRESETS_NAME, USER_LAUNCHER_NAME):
3513
+ source = config_path.with_name(sibling)
3514
+ if source.is_file():
3515
+ shutil.copy(source, trial / sibling)
3516
+ methods_source = user_methods_path(config_path)
3517
+ existing = methods_source.read_text() if methods_source.is_file() else USER_METHODS_HEADER
3518
+ candidate = trial / USER_METHODS_NAME
3519
+ candidate.write_text(existing + block)
3520
+ candidate.chmod(0o600)
3521
+ try:
3522
+ load_config(trial / config_path.name)
3523
+ except LaunchError as exc:
3524
+ return str(exc)
3525
+ return None
3526
+
3527
+
3528
+ def register_reviewer_wizard(
3529
+ ui: TextualUI | None,
3530
+ config_path: pathlib.Path,
3531
+ config: dict[str, Any],
3532
+ registry: dict[str, Any],
3533
+ ) -> bool:
3534
+ """Guided, ADD-ONLY registration into the user-owned methods file.
3535
+
3536
+ The wizard owns questions and serialization and nothing else: the only
3537
+ validity oracle is the real reader, run over a trial copy before a byte is
3538
+ written. Refusal shows the reader's own message and keeps the answers for
3539
+ another pass; nothing lands until the trial is green, and the write is an
3540
+ append through an atomic replace so the user's own formatting survives.
3541
+ Editing or removing entries stays a hand edit of the named file."""
3542
+ answers: dict[str, Any] = {}
3543
+ target = user_methods_path(config_path)
3544
+ while True:
3545
+ try:
3546
+ answers["id"] = prompt_text(
3547
+ t("wizard.id.label"), answers.get("id", "my-reviewer"), ui
3548
+ )
3549
+ answers["label"] = prompt_text(
3550
+ t("wizard.label.label"), answers.get("label", "My reviewer"), ui
3551
+ )
3552
+ answers["description"] = prompt_text(
3553
+ t("wizard.description.label"),
3554
+ answers.get("description", "Registered by the guided flow."), ui,
3555
+ )
3556
+ drives = choose(
3557
+ t("wizard.drives.title"),
3558
+ [
3559
+ MenuOption("tool", t("wizard.drives.tool.label"),
3560
+ t("wizard.drives.tool.description")),
3561
+ MenuOption("panel", t("wizard.drives.panel.label"),
3562
+ t("wizard.drives.panel.description")),
3563
+ ],
3564
+ "tool" if answers.get("capability") else "panel",
3565
+ ui, allow_back=True,
3566
+ )
3567
+ if drives == "tool":
3568
+ cap = answers.get("capability") or {}
3569
+ cap["name"] = prompt_text(
3570
+ t("wizard.cap.name.label"), cap.get("name", answers["id"] + "-kit"), ui
3571
+ )
3572
+ cap["command"] = prompt_text(
3573
+ t("wizard.cap.command.label"), cap.get("command", cap["name"]), ui
3574
+ )
3575
+ install_line = prompt_text(
3576
+ t("wizard.cap.install.label"), cap.get("install") or "-", ui
3577
+ )
3578
+ cap["install"] = "" if install_line == "-" else install_line
3579
+ cap["operation"] = prompt_text(
3580
+ t("wizard.cap.operation.label"), cap.get("operation", "vendor-review"), ui
3581
+ )
3582
+ cap["adapter"] = choose(
3583
+ t("wizard.cap.adapter.title"),
3584
+ [
3585
+ MenuOption(name, name, REVIEW_ADAPTERS[name])
3586
+ for name in sorted(REVIEW_ADAPTERS)
3587
+ ],
3588
+ cap.get("adapter", "exec-stdio-v1"), ui, allow_back=True,
3589
+ )
3590
+ hosts_pick = choose(
3591
+ t("wizard.cap.hosts.title"),
3592
+ [
3593
+ MenuOption("both", t("wizard.cap.hosts.both"), ""),
3594
+ MenuOption("codex", "codex", ""),
3595
+ MenuOption("claude", "claude", ""),
3596
+ ],
3597
+ "both", ui, allow_back=True,
3598
+ )
3599
+ cap["hosts"] = (
3600
+ ["codex", "claude"] if hosts_pick == "both" else [hosts_pick]
3601
+ )
3602
+ answers["capability"] = cap
3603
+ answers.setdefault("perspectives", ["refutation"])
3604
+ answers.setdefault("trials", 1)
3605
+ default_instructions = "run {command} on {model}/{effort}"
3606
+ else:
3607
+ answers["capability"] = None
3608
+ raw = prompt_text(
3609
+ t("wizard.perspectives.label"),
3610
+ ", ".join(answers.get("perspectives") or ["correctness", "refutation"]),
3611
+ ui,
3612
+ )
3613
+ answers["perspectives"] = [
3614
+ item.strip() for item in raw.split(",") if item.strip()
3615
+ ]
3616
+ while True:
3617
+ trials_raw = prompt_text(
3618
+ t("wizard.trials.label"), str(answers.get("trials", 2)), ui
3619
+ )
3620
+ try:
3621
+ answers["trials"] = int(trials_raw)
3622
+ break
3623
+ except ValueError:
3624
+ continue
3625
+ default_instructions = (
3626
+ "dispatch {command} as a fresh read-only process for {trials} "
3627
+ "isolated passes over {perspectives} on {model}/{effort}"
3628
+ )
3629
+ answers["instructions"] = prompt_text(
3630
+ t("wizard.instructions.label"),
3631
+ answers.get("instructions", default_instructions), ui,
3632
+ )
3633
+ raw = prompt_text(
3634
+ t("wizard.emits.label"),
3635
+ ", ".join(answers.get("severity_emits") or SEVERITY_LADDER), ui,
3636
+ )
3637
+ answers["severity_emits"] = [
3638
+ item.strip() for item in raw.split(",") if item.strip()
3639
+ ]
3640
+ mapping = {}
3641
+ for value in answers["severity_emits"]:
3642
+ if value in SEVERITY_LADDER:
3643
+ mapping[value] = value
3644
+ continue
3645
+ mapping[value] = choose(
3646
+ t("wizard.map.title") + f" — {value}",
3647
+ [MenuOption(level, level, "") for level in SEVERITY_LADDER],
3648
+ "medium", ui, allow_back=True,
3649
+ )
3650
+ answers["severity_map"] = mapping
3651
+ except BackRequested:
3652
+ return False
3653
+
3654
+ block = _render_registration(answers)
3655
+ # A symlinked methods file is written THROUGH, never replaced: os.replace
3656
+ # on the link path would swap the link for a regular file and strand the
3657
+ # canonical target. Resolved once; every later read/compare/write uses it.
3658
+ write_target = (
3659
+ target.resolve() if (target.exists() or target.is_symlink()) else target
3660
+ )
3661
+ existed = write_target.is_file()
3662
+ # BYTES, captured BEFORE the trial: the write-time comparison against this
3663
+ # is what catches a concurrent hand edit between proof and landing, and
3664
+ # text-mode round-trips would silently normalize a CRLF file.
3665
+ before = write_target.read_bytes() if existed else USER_METHODS_HEADER.encode()
3666
+ verdict = _trial_registration(config_path, block)
3667
+ if verdict is None:
3668
+ confirm = choose(
3669
+ t("wizard.confirm.title"),
3670
+ [
3671
+ MenuOption("write", t("wizard.confirm.write.label"),
3672
+ t("wizard.confirm.write.description")),
3673
+ MenuOption("edit", t("wizard.confirm.edit.label"), ""),
3674
+ ],
3675
+ "write", ui, allow_back=True,
3676
+ corpus_lines=[t("wizard.confirm.target") + f" {target}", *block.splitlines()],
3677
+ )
3678
+ if confirm == "edit":
3679
+ continue
3680
+ # The compare-and-replace is atomic under an exclusive lock, closing
3681
+ # the recheck→replace window a bare comparison leaves open. The lock
3682
+ # file is advisory and launcher-owned; hand edits do not take it, so
3683
+ # the byte comparison inside the lock remains the real guard.
3684
+ lock_path = write_target.with_name(write_target.name + ".lock")
3685
+ # O_NOFOLLOW and no truncation: opening a lock path with "w" follows
3686
+ # a planted symlink and truncates whatever it points at, merely by
3687
+ # opening the wizard.
3688
+ lock_fd = os.open(
3689
+ lock_path, os.O_CREAT | os.O_WRONLY | os.O_NOFOLLOW, 0o600
3690
+ )
3691
+ with os.fdopen(lock_fd, "w") as lock_handle:
3692
+ fcntl.flock(lock_handle, fcntl.LOCK_EX)
3693
+ # Existence is part of the snapshot: a file created (or removed)
3694
+ # since the trial must read as a race even when its bytes happen
3695
+ # to equal the header the wizard would have invented.
3696
+ if write_target.is_file() != existed:
3697
+ _corpus_info(ui, t("wizard.title"), [t("wizard.raced.line")],
3698
+ back_hint=t("corpus.back.hint"))
3699
+ return False
3700
+ current = (
3701
+ write_target.read_bytes() if write_target.is_file()
3702
+ else USER_METHODS_HEADER.encode()
3703
+ )
3704
+ if current != before:
3705
+ _corpus_info(ui, t("wizard.title"), [t("wizard.raced.line")],
3706
+ back_hint=t("corpus.back.hint"))
3707
+ return False
3708
+ mode = (write_target.stat().st_mode & 0o777) if existed else 0o600
3709
+ temporary = write_target.with_name(
3710
+ f".{write_target.name}.{os.getpid()}.tmp"
3711
+ )
3712
+ temporary.write_bytes(before + block.encode())
3713
+ temporary.chmod(mode)
3714
+ os.replace(temporary, write_target)
3715
+ # Unreachable while the trial is the real pipeline — and
3716
+ # load-bearing exactly when it is not: a post-write failure must
3717
+ # restore the pre-write state (bytes, mode, or ABSENCE) and say
3718
+ # so, never leave a corrupt registry behind a green screen.
3719
+ try:
3720
+ fresh = load_config(config_path)
3721
+ except LaunchError:
3722
+ fresh = None
3723
+ if fresh is None or answers["id"] not in load_review_methods(fresh):
3724
+ # Restore ONLY when the file still holds exactly our write:
3725
+ # unconditional rollback would destroy an edit that landed in
3726
+ # the meantime; if the bytes moved, the human owns the merge.
3727
+ current = (
3728
+ write_target.read_bytes() if write_target.is_file() else b""
3729
+ )
3730
+ if current != before + block.encode():
3731
+ _corpus_info(
3732
+ ui, t("wizard.title"), [t("wizard.postwrite.line")],
3733
+ back_hint=t("corpus.back.hint"),
3734
+ )
3735
+ return False
3736
+ if existed:
3737
+ restore = write_target.with_name(
3738
+ f".{write_target.name}.{os.getpid()}.restore"
3739
+ )
3740
+ restore.write_bytes(before)
3741
+ restore.chmod(mode)
3742
+ os.replace(restore, write_target)
3743
+ else:
3744
+ write_target.unlink(missing_ok=True)
3745
+ _corpus_info(ui, t("wizard.title"), [t("wizard.postwrite.line")],
3746
+ back_hint=t("corpus.back.hint"))
3747
+ return False # pre-write state restored above
3748
+ config.clear()
3749
+ config.update(fresh)
3750
+ registry.clear()
3751
+ registry.update(load_review_methods(config))
3752
+ _corpus_info(
3753
+ ui, t("wizard.title"),
3754
+ [t("wizard.done.line") + f" {answers['id']}"],
3755
+ back_hint=t("corpus.back.hint"),
3756
+ )
3757
+ return True
3758
+ action = choose(
3759
+ t("wizard.refused.title"),
3760
+ [
3761
+ MenuOption("edit", t("wizard.refused.edit.label"),
3762
+ t("wizard.refused.edit.description")),
3763
+ MenuOption("contract", t("wizard.refused.contract.label"), ""),
3764
+ ],
3765
+ "edit", ui, allow_back=True,
3766
+ corpus_lines=[t("wizard.refused.header"), "", *verdict.splitlines()],
3767
+ )
3768
+ if action == "contract":
3769
+ register_reviewer_info(ui, config_path)
3770
+
3771
+
3338
3772
  def register_reviewer_info(ui: TextualUI | None, config_path: pathlib.Path) -> None:
3339
3773
  """Where a reviewer this launcher has never seen comes from.
3340
3774
 
@@ -3524,7 +3958,7 @@ def review_editor(
3524
3958
  addable[0], ui, allow_back=True,
3525
3959
  )
3526
3960
  if method_id == REGISTER_REVIEWER:
3527
- register_reviewer_info(ui, config_path)
3961
+ register_reviewer_wizard(ui, config_path, config, registry)
3528
3962
  continue
3529
3963
  except BackRequested:
3530
3964
  continue
@@ -3859,7 +4293,7 @@ def customize(
3859
4293
  missing = [
3860
4294
  route
3861
4295
  for route in REVIEW_ROUTES[name]
3862
- if route in ("onto", "ultracode") and not available_routes[route]
4296
+ if route in LEGACY_ROUTE_CAPABILITY and not available_routes[route]
3863
4297
  ]
3864
4298
  if missing:
3865
4299
  note = "degrades to same-family native (PROPOSED)" if cross else "degrades to native"
@@ -4186,13 +4620,29 @@ def corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
4186
4620
  f"{name} {layers[name]}" for name in order if layers.get(name)
4187
4621
  ) or "none"
4188
4622
  by_status = status.get("summary", {}).get("by_status", {})
4189
- return [
4623
+ lines = [
4190
4624
  head,
4191
4625
  f"Mechanisms {layer_text}",
4192
4626
  f"Ledger placed {by_status.get('placed', 0)} · "
4193
4627
  f"incubating {by_status.get('incubating', 0) + by_status.get('incubating-G', 0)} · "
4194
4628
  f"versions {len(status.get('versions', []))}",
4195
4629
  ]
4630
+ domains = status.get("domains")
4631
+ if isinstance(domains, dict):
4632
+ applied = domains.get("applied")
4633
+ lines.append(
4634
+ "Domains "
4635
+ + ("(none selected yet)" if applied is None else ", ".join(applied) or "core+infra only")
4636
+ )
4637
+ last_apply = status.get("last_apply")
4638
+ if isinstance(last_apply, dict) and last_apply.get("outcome") not in (None, "applied"):
4639
+ # A failed apply must be loud on the panel, not a fact buried in a log:
4640
+ # the deployed corpus and the requested selection disagree right now.
4641
+ lines.append(
4642
+ f"⚠ LAST APPLY {last_apply.get('outcome')} at {last_apply.get('at')} — "
4643
+ f"requested: {', '.join(last_apply.get('requested') or []) or 'core+infra only'}"
4644
+ )
4645
+ return lines
4196
4646
 
4197
4647
 
4198
4648
  def _corpus_info(
@@ -4384,11 +4834,161 @@ def mode_default_preset(presets: dict[str, Any], mode: str) -> str | None:
4384
4834
  return names[0] if names else None
4385
4835
 
4386
4836
 
4837
+ class CorpusApplyRequested(Exception):
4838
+ """Raised out of the interactive flow when the user confirms a corpus
4839
+ selection: the Textual app must be torn down before the installer owns the
4840
+ terminal, so the request travels as control flow and main() runs the apply
4841
+ in the plain terminal, then re-enters the picker."""
4842
+
4843
+ def __init__(self, selection: list[str]):
4844
+ super().__init__(",".join(selection))
4845
+ self.selection = selection
4846
+
4847
+
4848
+ CORPUS_OPTION = "__corpus__"
4849
+ # A plain value, because the numbered prompt prints it as the default label.
4850
+ # Collision with a domain id is structurally impossible: domain slugs come from
4851
+ # compose/domains.json, which this repo owns and which carries no "apply".
4852
+ CORPUS_APPLY = "apply"
4853
+
4854
+
4855
+ def run_corpus_apply(selection: list[str]) -> None:
4856
+ """Run the installer's onboard loop for the selection, in the caller's
4857
+ terminal. The installer alone deploys, canaries, and records the outcome;
4858
+ this function only streams it and reports the exit."""
4859
+ status = load_corpus_status() or {}
4860
+ domains = ",".join(selection) if selection else "none"
4861
+ front = shutil.which("agent-bios")
4862
+ if front:
4863
+ argv = [front, "onboard", "--domains", domains]
4864
+ else:
4865
+ repo = status.get("repo") or ""
4866
+ installer = pathlib.Path(repo) / "install.sh" if repo else None
4867
+ if installer is None or not installer.is_file():
4868
+ print(
4869
+ "agent-launch: no agent-bios on PATH and no usable repo in "
4870
+ f"corpus-status.json; run manually: agent-bios onboard --domains {domains}",
4871
+ file=sys.stderr,
4872
+ )
4873
+ return
4874
+ argv = ["bash", str(installer), "onboard", "--domains", domains]
4875
+ print(f"\nagent-launch: applying corpus selection: {' '.join(argv)}\n", flush=True)
4876
+ proc = subprocess.run(argv)
4877
+ if proc.returncode != 0:
4878
+ print(
4879
+ f"\nagent-launch: corpus apply FAILED (exit {proc.returncode}); the "
4880
+ "corpus panel shows the recorded outcome.", file=sys.stderr,
4881
+ )
4882
+ else:
4883
+ print("\nagent-launch: corpus selection applied.", flush=True)
4884
+
4885
+
4886
+ def corpus_checklist(ui: "TextualUI | None") -> None:
4887
+ """Toggle-and-apply loop over the optional domain packages.
4888
+
4889
+ The list and the applied set come from corpus-status.json — the installer's
4890
+ projection — never from repo paths this launcher cannot know. Selection state
4891
+ lives only on this screen; Apply hands the exact set to the installer
4892
+ (raising through the Textual app so the terminal is free), and Esc leaves the
4893
+ deployed corpus untouched."""
4894
+ status = load_corpus_status()
4895
+ domains = (status or {}).get("domains")
4896
+ if not isinstance(domains, dict) or not isinstance(domains.get("available"), list):
4897
+ _corpus_info(
4898
+ ui, t("corpus.title"),
4899
+ [t("corpus.unavailable.line1"), t("corpus.unavailable.line2")],
4900
+ back_hint=t("corpus.back.hint"),
4901
+ )
4902
+ return
4903
+ available = domains["available"]
4904
+ applied_raw = domains.get("applied")
4905
+ # None is "never selected", [] is a verified core+infra-only selection —
4906
+ # the projection keeps them distinct, so the Apply rule must too: a
4907
+ # first-time user applying core+infra only IS a change.
4908
+ applied = set(applied_raw or [])
4909
+ never_applied = applied_raw is None
4910
+ toggles = set(applied)
4911
+ while True:
4912
+ options = [
4913
+ MenuOption(
4914
+ name,
4915
+ f"[{'x' if name in toggles else ' '}] {name}",
4916
+ t("corpus.toggle.description"),
4917
+ )
4918
+ for name in available
4919
+ ]
4920
+ changed = toggles != applied or never_applied
4921
+ pending = "" if not changed else (
4922
+ " → " + (", ".join(sorted(toggles)) or "core+infra only")
4923
+ )
4924
+ options.append(
4925
+ MenuOption(
4926
+ CORPUS_APPLY,
4927
+ t("corpus.apply.label") + pending,
4928
+ t("corpus.apply.description"),
4929
+ enabled=changed,
4930
+ unavailable_reason=t("corpus.apply.unchanged"),
4931
+ )
4932
+ )
4933
+ try:
4934
+ choice = choose(
4935
+ t("corpus.title"), options, CORPUS_APPLY, ui, allow_back=True,
4936
+ corpus_lines=[t("corpus.core.line"), *corpus_summary_lines(status)],
4937
+ )
4938
+ except BackRequested:
4939
+ return
4940
+ if choice == CORPUS_APPLY:
4941
+ selection = sorted(toggles)
4942
+ if ui is None:
4943
+ run_corpus_apply(selection)
4944
+ return
4945
+ raise CorpusApplyRequested(selection)
4946
+ if choice in toggles:
4947
+ toggles.discard(choice)
4948
+ else:
4949
+ toggles.add(choice)
4950
+
4951
+
4952
+ LANGUAGE_OPTION = "__language__"
4953
+ # Language names render in their own language BY DESIGN — a reader hunting for
4954
+ # their language must be able to recognise it whatever UI language is active — so
4955
+ # these labels are deliberately catalog-independent.
4956
+ I18N_DISPLAY = {"en": "English", "ko": "한국어", "ja": "日本語"}
4957
+
4958
+
4959
+ def choose_language(
4960
+ config_path: pathlib.Path | None, ui: "TextualUI | None"
4961
+ ) -> None:
4962
+ """Pick and persist the UI language, then reload the catalogs so the
4963
+ menu the user returns to already speaks the choice."""
4964
+ if config_path is None:
4965
+ return
4966
+ descriptions = {
4967
+ "en": t("language.en.description"),
4968
+ "ko": t("language.ko.description"),
4969
+ "ja": t("language.ja.description"),
4970
+ }
4971
+ options = [
4972
+ MenuOption(code, I18N_DISPLAY[code], descriptions[code])
4973
+ for code in I18N_LANGUAGES
4974
+ ]
4975
+ try:
4976
+ picked = choose(
4977
+ t("language.title"), options, ui_language(config_path), ui,
4978
+ allow_back=True,
4979
+ )
4980
+ except BackRequested:
4981
+ return
4982
+ save_language(config_path, picked)
4983
+ load_catalogs(config_path)
4984
+
4985
+
4387
4986
  def pick_mode_and_preset(
4388
4987
  config: dict[str, Any],
4389
4988
  host: str,
4390
4989
  ui: TextualUI | None,
4391
4990
  resume_mode: str | None,
4991
+ config_path: pathlib.Path | None = None,
4392
4992
  ) -> tuple[str, bool, str]:
4393
4993
  """Root menu: a 3-way mode picker (Software Engineer / Builder / Session
4394
4994
  distill), then that mode's preset submenu (Software Engineer and Builder
@@ -4405,26 +5005,31 @@ def pick_mode_and_preset(
4405
5005
  while True:
4406
5006
  if mode is None:
4407
5007
  mode_options = [
4408
- MenuOption(
4409
- SWE_MODE,
4410
- "Software Engineer",
4411
- "Repo-scoped work that defers to the project's own "
4412
- "AGENTS.md/CLAUDE.md: a bare Vanilla session, or Custom to "
4413
- "configure one.",
4414
- ),
5008
+ MenuOption(SWE_MODE, t("mode.swe.label"), t("mode.swe.description")),
4415
5009
  MenuOption(
4416
5010
  DEFAULT_PRESET_MODE,
4417
- "Builder",
4418
- "Tune tiers, review routes, and permissions across fixed "
4419
- "presets; includes Custom.",
5011
+ t("mode.builder.label"),
5012
+ t("mode.builder.description"),
4420
5013
  ),
4421
5014
  MenuOption(
4422
5015
  DISTILL_MODE,
4423
- "Session distill",
4424
- "Enter the dedicated session-distill hub: status, "
4425
- "packages, versions, and session start.",
5016
+ t("mode.distill.label"),
5017
+ t("mode.distill.description"),
4426
5018
  ),
4427
5019
  ]
5020
+ mode_options.append(
5021
+ MenuOption(
5022
+ CORPUS_OPTION, t("corpus.label"), t("corpus.description")
5023
+ )
5024
+ )
5025
+ if config_path is not None:
5026
+ mode_options.append(
5027
+ MenuOption(
5028
+ LANGUAGE_OPTION,
5029
+ t("language.label"),
5030
+ t("language.description"),
5031
+ )
5032
+ )
4428
5033
 
4429
5034
  def preview_mode(value: str) -> dict[str, Any] | None:
4430
5035
  target = (
@@ -4438,13 +5043,21 @@ def pick_mode_and_preset(
4438
5043
  if ui is not None and initial is not None:
4439
5044
  ui.set_plan(initial)
4440
5045
  mode = choose(
4441
- "Mode",
5046
+ t("mode.title"),
4442
5047
  mode_options,
4443
5048
  DEFAULT_PRESET_MODE,
4444
5049
  ui,
4445
5050
  preview=preview_mode,
4446
5051
  corpus_lines=corpus_summary_lines(load_corpus_status()),
4447
5052
  )
5053
+ if mode == CORPUS_OPTION:
5054
+ corpus_checklist(ui)
5055
+ mode = None
5056
+ continue
5057
+ if mode == LANGUAGE_OPTION:
5058
+ choose_language(config_path, ui)
5059
+ mode = None
5060
+ continue
4448
5061
  if mode == DISTILL_MODE:
4449
5062
  if distill_hub(config, ui) != "start":
4450
5063
  mode = None
@@ -4511,7 +5124,7 @@ def select_plan(
4511
5124
  selected_custom = custom_requested
4512
5125
  if show_picker:
4513
5126
  selected_name, selected_custom, resume_mode = pick_mode_and_preset(
4514
- config, host, ui, resume_mode
5127
+ config, host, ui, resume_mode, config_path
4515
5128
  )
4516
5129
  if selected_name not in presets:
4517
5130
  raise LaunchError(f"unknown preset: {selected_name}")
@@ -4605,7 +5218,16 @@ def _same_review_route(plan: dict[str, Any], effective: list[str], dropped: list
4605
5218
  if isinstance(route, dict):
4606
5219
  route = route[plan["host"]]
4607
5220
  if "ultracode" in effective:
4608
- route = f"{route} Ultracode executable: {resolve_command(plan['capabilities'][LEGACY_ULTRACODE_CAPABILITY]['command'])}."
5221
+ # Through capability_commands: the shipped codex-exec command is `${backend}`,
5222
+ # and route_availability has already proven one of these resolves.
5223
+ executable = next(
5224
+ resolve_command(command)
5225
+ for command in capability_commands(
5226
+ plan["capabilities"].get(LEGACY_ULTRACODE_CAPABILITY, {}), plan
5227
+ )
5228
+ if _resolves(command)
5229
+ )
5230
+ route = f"{route} Deep-exec executable: {executable}."
4609
5231
  if "ultracode" in REVIEW_ROUTES[requested]:
4610
5232
  route = (
4611
5233
  f"{route} If the Codex-backed route is unavailable or unauthenticated "
@@ -4635,7 +5257,7 @@ def _cross_review_route(
4635
5257
  native = cross_native_command(plan)
4636
5258
  if review_host == "codex":
4637
5259
  helm = cross_helm_command(plan)
4638
- fanout = f" (or {helm} --mode review for hybrid fan-out)" if helm else ""
5260
+ fanout = f" (or {helm} --mode review for review fan-out)" if helm else ""
4639
5261
  parts.append(
4640
5262
  f"native: dispatch {native} --profile hermetic --model <review tier> "
4641
5263
  f"--effort <e> --sandbox read-only{fanout}, self-contained packet on stdin, "
@@ -4647,31 +5269,19 @@ def _cross_review_route(
4647
5269
  "--permission-mode plan --append-system-prompt <read-only reviewer role>, "
4648
5270
  "self-contained packet, bounded report."
4649
5271
  )
4650
- if "onto" in effective:
4651
- onto = plan["review_onto"]
4652
- # Neither `auth` nor `effort`, both deliberately. onto's per-call override
4653
- # REPLACES a seat whose route differs and OVERLAYs one whose route matches,
4654
- # so a field stated here wins over the user's own onto settings.
4655
- #
4656
- # auth: onto selects the metered route only when a seat says so in writing —
4657
- # either `auth = "api_key"` or an `api_key_env` naming the variable to call —
4658
- # and defaults to the subscription worker otherwise. So an omitted auth cannot
4659
- # land on metered, and stating one only overrides a user who chose it: the
4660
- # switch to oauth is a route change, which drops their api_key_env with it.
4661
- #
4662
- # effort: a legacy setup names no reviewer effort at all (the enum hands the
4663
- # reader the whole tier table), so there is none to state, and inventing one
4664
- # would flatten every per-seat effort the user tuned.
4665
- parts.append(
4666
- f'onto: call onto_review/onto_prepare_review with llmOverride='
4667
- f'{{"provider":"{onto["provider"]}","model":"{onto["model"]}"}} so onto runs {review_family}.'
4668
- )
4669
5272
  if "ultracode" in effective:
4670
5273
  ultracode = cross_ultracode_command(plan)
4671
5274
  if review_host == "codex":
5275
+ # `ultra` is the mechanism, not a seat binding: the whole point of this
5276
+ # route is the deepest single pass, so the effort is stated as a literal
5277
+ # the way the claude arm states its keyword. The model stays the review
5278
+ # host's own frontier tier — the seat table remains the model authority.
5279
+ deep_model = plan["review_tiers"].get("frontier", {}).get("model", "<frontier model>")
4672
5280
  parts.append(
4673
- f"ultracode: run {ultracode} (the $ultracode-for-codex Codex skill) "
4674
- f"for {review_family} workflow-orchestration review."
5281
+ f'deep exec: run {ultracode} exec -s read-only -m {deep_model} '
5282
+ f'-c model_reasoning_effort="ultra" with a self-contained review packet '
5283
+ f"on stdin for {review_family} deep review (append -c "
5284
+ f'service_tier="fast" only when a faster, shallower pass is explicitly wanted).'
4675
5285
  )
4676
5286
  else:
4677
5287
  parts.append(
@@ -4680,13 +5290,7 @@ def _cross_review_route(
4680
5290
  "workflow-orchestration review."
4681
5291
  )
4682
5292
  if dropped:
4683
- detail = [
4684
- f"onto (add a [hosts.{review_host}].onto_review pin)"
4685
- if route == "onto" and not plan.get("review_onto")
4686
- else route
4687
- for route in dropped
4688
- ]
4689
- parts.append(f"Unavailable cross-family route(s) at launch: {', '.join(detail)}.")
5293
+ parts.append(f"Unavailable cross-family route(s) at launch: {', '.join(dropped)}.")
4690
5294
  if floor == "native":
4691
5295
  parts.append(
4692
5296
  "No cross-family route resolved at launch; using same-family native "
@@ -4808,10 +5412,9 @@ def review_mcp_servers(plan: dict[str, Any]) -> list[tuple[str, str]]:
4808
5412
  a method's identity, so a third-party method reusing the tag is wired too."""
4809
5413
  report = plan.get("review_report")
4810
5414
  if report is None:
4811
- effective, _, _ = effective_review(plan)
4812
- if "onto" not in effective:
4813
- return []
4814
- return [("onto", resolve_command(plan["capabilities"]["onto"]["command"]))]
5415
+ # No legacy route is MCP-backed: both deep routes are host CLIs dispatched
5416
+ # as subprocesses, so a legacy plan registers nothing.
5417
+ return []
4815
5418
  # One entry per capability AND HOST, in first-selected order. Two methods may share a
4816
5419
  # capability, and appending per row registered it twice — visible on codex as
4817
5420
  # duplicated -c overrides and invisible on claude, whose dict collapsed them. Keying on
@@ -5239,6 +5842,11 @@ def main(argv: list[str]) -> int:
5239
5842
  exec_backend(command, [*passthrough, *args.forward])
5240
5843
 
5241
5844
  interactive_setup = not args.preset or args.custom
5845
+ if interactive_setup:
5846
+ # UI text catalogs feed only interactive screens; headless runs (dry-run,
5847
+ # --preset) render no interface text and must not gain a stderr notice from a
5848
+ # fixture directory that ships no catalogs.
5849
+ load_catalogs(config_path)
5242
5850
  use_textual = (
5243
5851
  interactive_setup
5244
5852
  and tty
@@ -5252,14 +5860,23 @@ def main(argv: list[str]) -> int:
5252
5860
  "agent-launch: rich terminal UI unavailable; using numbered prompts.",
5253
5861
  file=sys.stderr,
5254
5862
  )
5255
- if use_textual:
5256
- plan = run_textual_flow(
5257
- config, args.host, args.preset, args.custom, config_path
5258
- )
5259
- else:
5260
- plan = select_plan(
5261
- config, args.host, args.preset, args.custom, config_path=config_path
5262
- )
5863
+ while True:
5864
+ try:
5865
+ if use_textual:
5866
+ plan = run_textual_flow(
5867
+ config, args.host, args.preset, args.custom, config_path
5868
+ )
5869
+ else:
5870
+ plan = select_plan(
5871
+ config, args.host, args.preset, args.custom, config_path=config_path
5872
+ )
5873
+ break
5874
+ except CorpusApplyRequested as request:
5875
+ # The Textual app is already torn down; the installer owns the
5876
+ # terminal for the duration, and the picker re-opens on a fresh
5877
+ # status projection afterwards.
5878
+ run_corpus_apply(request.selection)
5879
+ continue
5263
5880
  validate_review_setup(plan)
5264
5881
  projected = [*project_args(plan, materialize_agents=not args.dry_run), *args.forward]
5265
5882
  summary_stream = sys.stdout if tty or args.dry_run else sys.stderr