agent-bios 0.9.8 → 0.9.9

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 (49) hide show
  1. package/DEPENDENCIES.md +19 -19
  2. package/README.md +34 -11
  3. package/claude/CLAUDE.md +2 -1
  4. package/claude/guides/claude-prompting.md +1 -1
  5. package/claude/guides/cli-multi-model-workflow.md +19 -1
  6. package/claude/guides/coding-staged-workflow.md +32 -0
  7. package/claude/guides/gpt-prompting.md +1 -1
  8. package/claude/guides/learning-flow.md +5 -5
  9. package/claude/guides/llm-capability-boundary.md +6 -1
  10. package/claude/guides/session-distill-workflow.md +19 -9
  11. package/claude/guides/tooling-gotchas.md +16 -0
  12. package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
  13. package/claude/hooks/tooling-gotchas-hook.py +7 -0
  14. package/codex/AGENTS.md +2 -1
  15. package/codex/guides/claude-prompting.md +1 -1
  16. package/codex/guides/cli-multi-model-workflow.md +19 -1
  17. package/codex/guides/coding-staged-workflow.md +32 -0
  18. package/codex/guides/gpt-prompting.md +1 -1
  19. package/codex/guides/learning-flow.md +5 -5
  20. package/codex/guides/llm-capability-boundary.md +6 -1
  21. package/codex/guides/session-distill-workflow.md +19 -9
  22. package/codex/guides/tooling-gotchas.md +16 -0
  23. package/{scripts → compose}/assemble.py +184 -16
  24. package/{scripts → compose}/canary.sh +14 -5
  25. package/{scripts → compose}/check-domains.py +9 -3
  26. package/{config → compose}/domains.json +1 -0
  27. package/{scripts → compose}/pkgid.py +8 -1
  28. package/compose/prune-backups.py +204 -0
  29. package/{scripts → compose}/register-hooks.py +3 -3
  30. package/{scripts/install.sh → install.sh} +401 -104
  31. package/launch/agent-launch.py +5294 -0
  32. package/launch/agent-launch.toml +376 -0
  33. package/{scripts → launch}/check-prompting-targets.sh +1 -1
  34. package/{scripts → launch}/provision-venv.sh +1 -1
  35. package/{scripts → learn}/check-learning.py +7 -7
  36. package/{scripts → learn}/collect-learning.py +10 -10
  37. package/{config → learn}/learning.schema.json +3 -3
  38. package/{scripts → learn}/migrate-learnings.py +95 -54
  39. package/{scripts → learn}/redact.py +4 -4
  40. package/package.json +25 -24
  41. package/wrappers/claude-run.sh +162 -0
  42. package/{scripts → wrappers}/codex-run.sh +62 -6
  43. package/config/agent-launch.toml +0 -143
  44. package/scripts/agent-launch.py +0 -2350
  45. package/scripts/check-parity.sh +0 -2003
  46. /package/{shell → launch}/agent-launch.zsh +0 -0
  47. /package/{config → learn}/promotions.json +0 -0
  48. /package/{scripts/session-cost.py → session-cost.py} +0 -0
  49. /package/{scripts → wrappers}/codex-helm.sh +0 -0
@@ -1,2350 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Interactive preflight launcher for Codex and Claude."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import copy
8
- import hashlib
9
- import json
10
- import os
11
- import pathlib
12
- import shutil
13
- import subprocess
14
- import sys
15
- import textwrap
16
- import tomllib
17
- from dataclasses import dataclass
18
- from typing import Any, NoReturn
19
-
20
-
21
- TIER_ORDER = ("frontier", "helm", "workhorse", "sweep")
22
- # Tiers projected as spawnable subagents. HELM is the main role, never a
23
- # spawnable worker — registering it invited helm-as-subagent misuse.
24
- SPAWNABLE_TIERS = ("frontier", "workhorse", "sweep")
25
- EFFORT_ORDER = ("low", "medium", "high", "xhigh", "max", "ultra")
26
- HOST_EFFORTS = {
27
- "codex": {"low", "medium", "high", "xhigh", "max", "ultra"},
28
- "claude": {"low", "medium", "high", "xhigh", "max"},
29
- }
30
-
31
- TIER_DESCRIPTIONS = {
32
- "frontier": "Highest-capability specialist for hard, high-impact work and deep review.",
33
- "helm": "Primary orchestrator for planning, delegation, integration, and final decisions.",
34
- "workhorse": "Cost-conscious executor for high-volume implementation and analysis.",
35
- "sweep": "Fast, low-cost worker for narrow searches, checks, and mechanical tasks.",
36
- }
37
- EFFORT_DESCRIPTIONS = {
38
- "low": "Minimize reasoning cost and latency for straightforward work.",
39
- "medium": "Use moderate reasoning for routine work with some ambiguity.",
40
- "high": "Spend more reasoning on complex implementation and analysis.",
41
- "xhigh": "Use deeper reasoning for difficult coordination and high-impact decisions.",
42
- "max": "Use the host's strongest standard reasoning mode for frontier work.",
43
- "ultra": "Use Codex Ultra for exceptionally deep, divisible frontier work.",
44
- }
45
- CUSTOM_PRESET = "__custom__"
46
- OTHER_MODEL = "__other_model__"
47
- # Root-menu grouping for presets: "builder" (tunable tier presets, includes
48
- # Custom), "software-engineer" (repo-scoped work under the project's own
49
- # AGENTS.md/CLAUDE.md — the bare Vanilla session plus Custom), "distill" (opens
50
- # the Session Distill hub). A preset with a missing/unknown mode defaults to
51
- # "builder" so presets written before this field existed (including saved/local
52
- # user presets, and any that still carry the former "general" value) keep working.
53
- SWE_MODE = "software-engineer"
54
- DEFAULT_PRESET_MODE = "builder"
55
- # The mode value that opens the Session Distill hub instead of a preset
56
- # submenu; doubles as the option value on the root Mode menu so selecting it
57
- # needs no translation.
58
- DISTILL_MODE = "distill"
59
- PRESET_MODES = (SWE_MODE, DEFAULT_PRESET_MODE, DISTILL_MODE)
60
- # User-saved presets live beside the deployed config, in a file the installer
61
- # neither deploys nor verifies, so they survive `agent-bios install`.
62
- USER_PRESETS_NAME = "presets.local.toml"
63
- USER_PRESETS_HEADER = (
64
- "# agent-launch user presets, written by the launcher's save action.\n"
65
- "# agent-bios install never deploys or verifies this file, so presets here\n"
66
- "# survive upgrades. Shipped presets live in the deployed profiles.toml and\n"
67
- "# are overridden by a preset of the same name here.\n"
68
- )
69
-
70
-
71
- @dataclass(frozen=True)
72
- class MenuOption:
73
- value: str
74
- label: str
75
- description: str
76
- enabled: bool = True
77
- unavailable_reason: str = ""
78
- STANDARD_POLICY = "standard"
79
- CODEX_POLICIES = {"bypass", "workspace-write", "read-only", STANDARD_POLICY}
80
- CLAUDE_POLICIES = {"acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan", STANDARD_POLICY}
81
- REVIEW_SETUPS = {
82
- "none": {
83
- "label": "None",
84
- "requirements": {"codex": (), "claude": ()},
85
- "description": "No review route. Fastest; you review it yourself.",
86
- "contract": "No additional review route requested.",
87
- },
88
- "native-panel": {
89
- "label": "Native panel",
90
- "requirements": {"codex": (), "claude": ()},
91
- "description": (
92
- "Subagents review from different angles. Broad coverage, no dependency; "
93
- "same-model reviewers share the main's blind spots."
94
- ),
95
- "contract": "Use native multi-perspective subagents when review gates fire.",
96
- },
97
- "slash-review": {
98
- "label": "Slash review",
99
- "requirements": {"codex": (), "claude": ()},
100
- "description": {
101
- "claude": (
102
- "The built-in /code-review command (add `ultra` for the deep cloud pass). "
103
- "Cheapest good diff review; same-family, so it is a weaker check than cross."
104
- ),
105
- "codex": (
106
- "The built-in /review command. Cheapest good diff review; same-family, "
107
- "so it is a weaker check than cross."
108
- ),
109
- },
110
- "contract": {
111
- "claude": (
112
- "Use Claude Code's built-in /code-review when review gates fire; use "
113
- "/code-review ultra for a deep multi-agent pass on high-risk changes."
114
- ),
115
- "codex": "Use Codex's built-in /review when review gates fire.",
116
- },
117
- },
118
- "onto": {
119
- "label": "onto-mcp",
120
- "requirements": {"codex": ("onto",), "claude": ("onto",)},
121
- "description": (
122
- "Structured multi-lens review through onto-mcp. Best for concept/ontology "
123
- "and design work; needs onto installed."
124
- ),
125
- "contract": "Use onto-mcp as the structured review lens when review gates fire.",
126
- },
127
- "ultracode": {
128
- "label": "Ultracode",
129
- "requirements": {"codex": ("ultracode",), "claude": ("ultracode",)},
130
- "description": (
131
- "Workflow-orchestrated review fanned out over many agents. Best for "
132
- "exhaustive audits; slowest and most expensive."
133
- ),
134
- "contract": (
135
- "Run the configured Ultracode CLI as a Codex-backed review route when "
136
- "review gates fire."
137
- ),
138
- },
139
- "hybrid": {
140
- "label": "Hybrid",
141
- "requirements": {
142
- "codex": ("onto", "ultracode"),
143
- "claude": ("onto", "ultracode"),
144
- },
145
- "description": (
146
- "onto + native + Ultracode together, acting on their union. Widest net for "
147
- "risky work; missing routes degrade rather than fail."
148
- ),
149
- "contract": {
150
- "codex": "Use onto plus native and Codex-backed Ultracode review kinds; act on their union.",
151
- "claude": "Use onto plus native multi-perspective and Codex-backed Ultracode review kinds; act on their union.",
152
- },
153
- },
154
- }
155
-
156
- # Each review setup decomposes into review routes. "native" is same-model
157
- # multi-perspective subagent review (needs delegation); "onto"/"ultracode" are
158
- # external routes gated on a resolvable capability command. A missing external
159
- # route degrades to native instead of failing closed; only the delegation
160
- # contradiction (native required with delegation off) stays fail-closed.
161
- REVIEW_ROUTES = {
162
- "none": (),
163
- "native-panel": ("native",),
164
- "slash-review": ("slash",),
165
- "onto": ("onto",),
166
- "ultracode": ("ultracode",),
167
- "hybrid": ("onto", "native", "ultracode"),
168
- }
169
- # Routes that are host-native review commands: they always resolve (no capability
170
- # to install) but run on the main's own family, so cross-family mode cannot dispatch
171
- # them to the opposite family and labels them PROPOSED instead.
172
- SAME_FAMILY_ROUTES = {"slash"}
173
- ROUTE_LABELS = {
174
- "native": "native same-model multi-perspective review",
175
- "slash": "host-native slash-command review",
176
- "onto": "onto structured-lens review",
177
- "ultracode": "Codex-backed Ultracode review",
178
- }
179
- # review_family selects whether review runs on the opposite model family (cross,
180
- # the default) or the same family as the main (same, today's projection).
181
- REVIEW_FAMILIES = {"cross", "same"}
182
- # onto llmOverride provider enum (onto MCP tool schema, settings-chain).
183
- ONTO_PROVIDERS = {"openai", "anthropic", "grok", "lmstudio"}
184
- # The opposite model family for cross review, keyed on the launch host.
185
- REVIEW_HOST = {"codex": "claude", "claude": "codex"}
186
-
187
-
188
- class LaunchError(RuntimeError):
189
- pass
190
-
191
-
192
- class BackRequested(RuntimeError):
193
- pass
194
-
195
-
196
- def resolve_command(value: str) -> str:
197
- expanded = os.path.expanduser(value)
198
- if os.path.sep in expanded:
199
- command = expanded if pathlib.Path(expanded).is_file() and os.access(expanded, os.X_OK) else None
200
- else:
201
- command = shutil.which(expanded)
202
- if not command:
203
- raise LaunchError(f"executable not found: {value}")
204
- return command
205
-
206
-
207
- def expand_config_path(value: str) -> pathlib.Path:
208
- codex_home = os.environ.get("CODEX_HOME", str(pathlib.Path.home() / ".codex"))
209
- expanded = value.replace("${CODEX_HOME}", codex_home)
210
- return pathlib.Path(os.path.expandvars(os.path.expanduser(expanded)))
211
-
212
-
213
- def exec_backend(command: str, args: list[str], env: dict[str, str] | None = None) -> NoReturn:
214
- os.execve(command, [command, *args], os.environ.copy() if env is None else env)
215
-
216
-
217
- def default_config_path() -> pathlib.Path:
218
- explicit = os.environ.get("AGENT_LAUNCH_CONFIG")
219
- if explicit:
220
- return pathlib.Path(explicit).expanduser()
221
- installed = pathlib.Path.home() / ".config/agent-launch/profiles.toml"
222
- if installed.is_file():
223
- return installed
224
- return pathlib.Path(__file__).resolve().parent.parent / "config/agent-launch.toml"
225
-
226
-
227
- def user_presets_path(config_path: pathlib.Path) -> pathlib.Path:
228
- return config_path.with_name(USER_PRESETS_NAME)
229
-
230
-
231
- def load_user_presets(path: pathlib.Path) -> dict[str, Any]:
232
- if not path.is_file():
233
- return {}
234
- try:
235
- data = tomllib.loads(path.read_text())
236
- except (OSError, tomllib.TOMLDecodeError) as exc:
237
- raise LaunchError(f"cannot load user presets {path}: {exc}") from exc
238
- unexpected = sorted(set(data) - {"presets"})
239
- if unexpected:
240
- raise LaunchError(
241
- f"{path} may only define [presets.*]; found: {', '.join(unexpected)}"
242
- )
243
- presets = data.get("presets", {})
244
- if not isinstance(presets, dict):
245
- raise LaunchError(f"[presets] must be a table in {path}")
246
- return presets
247
-
248
-
249
- def load_config(path: pathlib.Path) -> dict[str, Any]:
250
- try:
251
- data = tomllib.loads(path.read_text())
252
- except (OSError, tomllib.TOMLDecodeError) as exc:
253
- raise LaunchError(f"cannot load config {path}: {exc}") from exc
254
- if type(data.get("schema_version")) is not int or data["schema_version"] != 1:
255
- raise LaunchError(f"unsupported schema_version in {path}")
256
- for key in ("backends", "hosts", "presets"):
257
- if not isinstance(data.get(key), dict) or not data[key]:
258
- raise LaunchError(f"config requires non-empty [{key}]")
259
- # Merged before the preset checks below so user presets face the same validation.
260
- data["presets"].update(load_user_presets(user_presets_path(path)))
261
- capabilities = data.get("capabilities", {})
262
- if not isinstance(capabilities, dict):
263
- raise LaunchError("[capabilities] must be a table")
264
- for name, capability in capabilities.items():
265
- if not isinstance(capability, dict):
266
- raise LaunchError(f"capability must be a table: {name}")
267
- hint = capability.get("install")
268
- if hint is not None and (not isinstance(hint, str) or not hint):
269
- raise LaunchError(f"capabilities.{name}.install must be a non-empty string")
270
- onto = capabilities.get("onto")
271
- if onto is not None and (
272
- not isinstance(onto.get("command"), str) or not onto["command"]
273
- ):
274
- raise LaunchError("capabilities.onto.command must be a non-empty string")
275
- ultracode = capabilities.get("ultracode")
276
- if ultracode is not None and (
277
- not isinstance(ultracode.get("command"), str) or not ultracode["command"]
278
- ):
279
- raise LaunchError("capabilities.ultracode.command must be a non-empty string")
280
- for host_name, host_data in data["hosts"].items():
281
- if not isinstance(host_data, dict):
282
- raise LaunchError(f"host must be a table: {host_name}")
283
- models = host_data.get("models")
284
- if models is not None and (
285
- not isinstance(models, list)
286
- or not models
287
- or not all(isinstance(model, str) and model for model in models)
288
- ):
289
- raise LaunchError(
290
- f"hosts.{host_name}.models must be a non-empty list of strings"
291
- )
292
- onto_review = host_data.get("onto_review")
293
- if onto_review is not None:
294
- if not isinstance(onto_review, dict):
295
- raise LaunchError(f"hosts.{host_name}.onto_review must be a table")
296
- if onto_review.get("provider") not in ONTO_PROVIDERS:
297
- choices = ", ".join(sorted(ONTO_PROVIDERS))
298
- raise LaunchError(
299
- f"hosts.{host_name}.onto_review.provider must be one of: {choices}"
300
- )
301
- if not isinstance(onto_review.get("model"), str) or not onto_review["model"]:
302
- raise LaunchError(
303
- f"hosts.{host_name}.onto_review.model must be a non-empty string"
304
- )
305
- for name, preset in data["presets"].items():
306
- if not isinstance(preset, dict):
307
- raise LaunchError(f"preset must be a table: {name}")
308
- label = preset.get("label")
309
- if not isinstance(label, str) or not label:
310
- raise LaunchError(f"presets.{name}.label must be a non-empty string")
311
- description = preset.get("description")
312
- if description is not None and (
313
- not isinstance(description, str) or not description
314
- ):
315
- raise LaunchError(f"presets.{name}.description must be a non-empty string")
316
- for field, allowed in (
317
- ("codex_execution_policy", CODEX_POLICIES),
318
- ("claude_permission_mode", CLAUDE_POLICIES),
319
- ):
320
- value = preset.get(field)
321
- if not isinstance(value, str) or value not in allowed:
322
- choices = ", ".join(sorted(allowed))
323
- raise LaunchError(f"presets.{name}.{field} must be one of: {choices}")
324
- review_family = preset.get("review_family")
325
- if review_family is not None and review_family not in REVIEW_FAMILIES:
326
- raise LaunchError(f"presets.{name}.review_family must be one of: cross, same")
327
- return data
328
-
329
-
330
- def resolve_backend(config: dict[str, Any], host: str) -> tuple[str, list[str]]:
331
- try:
332
- backend = config["backends"][host]
333
- command_value = backend["command"]
334
- passthrough_value = backend.get("passthrough_args", [])
335
- except (KeyError, TypeError) as exc:
336
- raise LaunchError(f"backend is not configured for {host}") from exc
337
- if not isinstance(command_value, str) or not command_value:
338
- raise LaunchError(f"backend command must be a non-empty string: {host}")
339
- command = resolve_command(command_value)
340
- if not isinstance(passthrough_value, list) or not all(
341
- isinstance(arg, str) for arg in passthrough_value
342
- ):
343
- raise LaunchError(f"backend passthrough_args must be strings: {host}")
344
- return command, passthrough_value
345
-
346
-
347
- def validate_effort(host: str, model: str, effort: Any, context: str) -> str:
348
- if not isinstance(effort, str) or effort not in HOST_EFFORTS[host]:
349
- raise LaunchError(f"unsupported effort for {context}: {effort!r}")
350
- if host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
351
- raise LaunchError(f"unsupported effort for {context}: gpt-5.6-luna/ultra")
352
- return effort
353
-
354
-
355
- def _resolves(value: str) -> bool:
356
- try:
357
- resolve_command(value)
358
- return True
359
- except LaunchError:
360
- return False
361
-
362
-
363
- def cross_native_command(plan: dict[str, Any]) -> str | None:
364
- """Absolute command a cross-family main dispatches to for native review, or
365
- None if unresolvable. The codex reviewer wrapper lives off PATH under
366
- CODEX_HOME/bin; the claude reviewer is the claude backend on PATH."""
367
- if plan["review_host"] == "codex":
368
- path = expand_config_path("${CODEX_HOME}/bin/codex-run")
369
- return str(path) if path.is_file() and os.access(path, os.X_OK) else None
370
- try:
371
- return resolve_command(plan["review_backend"])
372
- except LaunchError:
373
- return None
374
-
375
-
376
- def cross_helm_command(plan: dict[str, Any]) -> str | None:
377
- """The codex fan-out reviewer wrapper (hybrid), off PATH under CODEX_HOME/bin."""
378
- if plan["review_host"] != "codex":
379
- return None
380
- path = expand_config_path("${CODEX_HOME}/bin/codex-helm")
381
- return str(path) if path.is_file() and os.access(path, os.X_OK) else None
382
-
383
-
384
- def cross_ultracode_command(plan: dict[str, Any]) -> str | None:
385
- """The cross-family ultracode reviewer command, or None. For a claude main it
386
- is the Codex-backed ultracode-for-codex (the $ultracode-for-codex skill); for
387
- a codex main it is the claude backend (Claude Code /workflows ultracode)."""
388
- if plan["review_host"] == "codex":
389
- value = plan.get("capabilities", {}).get("ultracode", {}).get("command", "")
390
- else:
391
- value = plan["review_backend"]
392
- try:
393
- return resolve_command(value)
394
- except LaunchError:
395
- return None
396
-
397
-
398
- def route_availability(plan: dict[str, Any]) -> dict[str, bool]:
399
- """Which review routes can run right now. In same-family mode native gates on
400
- delegation and onto/ultracode on a resolvable capability. In cross-family mode
401
- native/ultracode gate on the opposite-family dispatcher resolving, and onto on
402
- a mounted onto plus a review_onto pin to flip its family."""
403
- capabilities = plan.get("capabilities", {})
404
- if plan.get("review_family", "cross") == "same":
405
- available = {"native": bool(plan["delegation"]), "slash": True}
406
- for route, capability in (("onto", "onto"), ("ultracode", "ultracode")):
407
- available[route] = _resolves(capabilities.get(capability, {}).get("command", ""))
408
- return available
409
- onto_ok = _resolves(capabilities.get("onto", {}).get("command", "")) and bool(
410
- plan.get("review_onto")
411
- )
412
- return {
413
- "native": cross_native_command(plan) is not None,
414
- # The host's own review command exists, but only for its own family; the
415
- # cross branch of effective_review routes it to the PROPOSED floor.
416
- "slash": True,
417
- "onto": onto_ok,
418
- "ultracode": cross_ultracode_command(plan) is not None,
419
- }
420
-
421
-
422
- def install_hint(plan: dict[str, Any], routes: list[str]) -> str:
423
- """One-line install guidance for the capability-backed routes that are missing.
424
- Empty when a route has no capability, no configured install, or is already there."""
425
- capabilities = plan.get("capabilities", {})
426
- hints = []
427
- for route in routes:
428
- hint = capabilities.get(route, {}).get("install")
429
- if hint and hint not in hints:
430
- hints.append(hint)
431
- return f"; install: {' && '.join(hints)}" if hints else ""
432
-
433
-
434
- def effective_review(plan: dict[str, Any]) -> tuple[list[str], list[str], str | None]:
435
- """Resolve the requested review setup to the routes that can actually run.
436
-
437
- Returns (effective_routes, dropped_routes, floor). In same-family mode this
438
- reproduces the earlier degrade-to-native behavior (floor None; a dropped
439
- external route degrades to native in effective; native requires delegation).
440
- In cross-family mode the effective routes run on the opposite family; when no
441
- cross route resolves the floor names the same-family route that runs instead,
442
- labeled PROPOSED — a SAME_FAMILY_ROUTES route floors as itself (it needs no
443
- delegation, being the host's own command), otherwise 'native' when delegation
444
- is on. A requested non-none setup with no cross route and no fallback
445
- (delegation off) fails closed."""
446
- requested = plan["review_setup"]
447
- if requested not in REVIEW_ROUTES:
448
- raise LaunchError(f"unknown review setup: {requested!r}")
449
- wanted = list(REVIEW_ROUTES[requested])
450
- available = route_availability(plan)
451
- if plan.get("review_family", "cross") == "same":
452
- if "native" in wanted and not plan["delegation"]:
453
- raise LaunchError(f"review setup {requested!r} requires delegation")
454
- effective = [route for route in wanted if available[route]]
455
- dropped = [route for route in wanted if not available[route]]
456
- if dropped and "native" not in effective and plan["delegation"]:
457
- effective.append("native")
458
- return effective, dropped, None
459
- # A same-family route cannot be dispatched cross-family, so it is never
460
- # "effective" here; it becomes the PROPOSED floor below instead.
461
- effective = [
462
- route
463
- for route in wanted
464
- if available[route] and route not in SAME_FAMILY_ROUTES
465
- ]
466
- dropped = [route for route in wanted if not available[route]]
467
- floor = None
468
- if not effective and requested != "none":
469
- same_family = [
470
- route
471
- for route in wanted
472
- if route in SAME_FAMILY_ROUTES and available[route]
473
- ]
474
- if same_family:
475
- floor = same_family[0]
476
- elif plan["delegation"]:
477
- floor = "native"
478
- else:
479
- raise LaunchError(
480
- f"review setup {requested!r} has no available cross-family route "
481
- "and no same-family fallback (delegation off)"
482
- )
483
- return effective, dropped, floor
484
-
485
-
486
- def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
487
- if plan is None:
488
- return ["No setup selected."]
489
- host = plan["host"]
490
- execution = (
491
- plan["codex_execution_policy"]
492
- if host == "codex"
493
- else plan["claude_permission_mode"]
494
- )
495
- lines = [
496
- f"Host {host} | Preset {plan['label']}",
497
- f"Main {plan['main_tier'].upper()} | Review {plan['review_setup']}",
498
- f"Delegation {'on' if plan['delegation'] else 'off'} | Execution {execution}",
499
- ]
500
- for tier in TIER_ORDER:
501
- binding = plan["tiers"][tier]
502
- effort = (
503
- plan["frontier_effort"] if tier == "frontier" else binding["effort"]
504
- )
505
- lines.append(f"{tier.upper():<10} {binding['model']} / {effort}")
506
- return lines
507
-
508
-
509
- def textual_importable() -> bool:
510
- try:
511
- import textual # noqa: F401
512
- except Exception:
513
- return False
514
- return True
515
-
516
-
517
- def venv_python() -> pathlib.Path | None:
518
- """Locate a managed venv interpreter that provides textual. AGENT_LAUNCH_VENV
519
- overrides the default install location."""
520
- roots = []
521
- override = os.environ.get("AGENT_LAUNCH_VENV")
522
- if override:
523
- roots.append(pathlib.Path(override))
524
- roots.append(pathlib.Path.home() / ".local/share/agent-launch/venv")
525
- for root in roots:
526
- interpreter = root.expanduser() / "bin" / "python"
527
- if interpreter.is_file() and os.access(interpreter, os.X_OK):
528
- return interpreter
529
- return None
530
-
531
-
532
- def maybe_reexec_into_venv() -> None:
533
- """When textual is not importable under the current interpreter, re-exec once
534
- into the managed venv that provides it. Guarded against infinite re-exec and
535
- only ever reached on the interactive TUI path, so direct/non-TTY launches keep
536
- running under the system interpreter."""
537
- if os.environ.get("AGENT_LAUNCH_REEXEC") == "1":
538
- return
539
- interpreter = venv_python()
540
- if interpreter is None:
541
- return
542
- env = os.environ.copy()
543
- env["AGENT_LAUNCH_REEXEC"] = "1"
544
- os.execve(str(interpreter), [str(interpreter), *sys.argv], env)
545
-
546
-
547
- # --- Textual preflight UI (optional; the numbered fallback covers its absence) ---
548
- # select_plan/customize call the same choose()/prompt_text() seam regardless of
549
- # renderer. TextualUI drives Textual screens from a worker thread via
550
- # call_from_thread(push_screen_wait), preserving the synchronous controller flow
551
- # and its BackRequested/KeyboardInterrupt contract.
552
-
553
- _UI_BACK = "\x00back"
554
- _UI_CANCEL = "\x00cancel"
555
-
556
-
557
- def _build_app_class():
558
- """Import textual lazily and build the App/Screen classes, so importing this
559
- module and every non-interactive path stays free of the textual dependency."""
560
- from textual import work
561
- from textual.app import App
562
- from textual.binding import Binding
563
- from textual.screen import ModalScreen
564
- from textual.widgets import Input, OptionList, Static
565
- from textual.widgets.option_list import Option
566
- from textual.theme import Theme
567
-
568
- # Host-matched palettes so the preflight reads as the CLI it launches.
569
- # Both are taken from the host's own artifact: Codex from its ~/.codex dark
570
- # appearance theme (accent #339cff, surface #181818, diff #40c977/#fa423e),
571
- # Claude from its binary's dark palette (coral #da7756, surface #1a1a19,
572
- # ink scale #f9f9f7 / #c3c2b7 / #898781, grid #2c2c2a).
573
- host_themes = {
574
- "codex": Theme(
575
- name="codex", primary="#339cff", secondary="#ad7bf9",
576
- accent="#5fb0ff", foreground="#ffffff", background="#141414",
577
- surface="#181818", panel="#222222", success="#40c977",
578
- warning="#e6a700", error="#fa423e", dark=True,
579
- ),
580
- "claude": Theme(
581
- name="claude", primary="#da7756", secondary="#c3c2b7",
582
- accent="#da7756", foreground="#f9f9f7", background="#0d0d0d",
583
- surface="#1a1a19", panel="#2c2c2a", success="#0ca30c",
584
- warning="#fab219", error="#e06c6c", dark=True,
585
- ),
586
- }
587
-
588
- app_css = """
589
- Screen { background: $surface; }
590
- #al-title { background: $primary; color: black; text-style: bold; padding: 0 1; }
591
- #al-setup {
592
- border: round $primary; border-title-color: $primary;
593
- border-title-style: bold; padding: 0 1; height: auto;
594
- }
595
- #al-detail {
596
- border: round $secondary; border-title-color: $secondary;
597
- border-title-style: bold; padding: 0 1; height: 5;
598
- }
599
- #al-corpus-title { background: $warning; color: black; text-style: bold; padding: 0 1; }
600
- #al-corpus {
601
- border: round $warning; padding: 0 1; height: auto;
602
- }
603
- #al-hdr { color: $text-muted; text-style: bold; padding: 0 1; }
604
- OptionList { height: 1fr; border: none; padding: 0 1; }
605
- #al-footer { color: $text-muted; dock: bottom; padding: 0 1; background: $panel; }
606
- Input { margin: 0 1; }
607
- """
608
-
609
- def setup_panel(plan):
610
- panel = Static("\n".join(setup_summary_lines(plan)), id="al-setup")
611
- panel.border_title = "Current setup"
612
- release = version_label()
613
- if release:
614
- panel.border_subtitle = release
615
- return panel
616
-
617
- class MenuScreen(ModalScreen):
618
- BINDINGS = [
619
- Binding("escape", "back", "back", priority=True),
620
- Binding("q", "cancel", "cancel", priority=True),
621
- Binding("ctrl+c", "cancel", "cancel", priority=True),
622
- ]
623
-
624
- def __init__(
625
- self, title, options, default, allow_back, plan, preview=None, corpus=None
626
- ):
627
- super().__init__()
628
- self._title = title
629
- self._options = options
630
- self._default = default
631
- self._allow_back = allow_back
632
- self._plan = plan
633
- self._preview = preview
634
- self._corpus = corpus
635
-
636
- def compose(self):
637
- yield Static(self._title, id="al-title")
638
- yield setup_panel(self._plan)
639
- if self._corpus:
640
- yield Static("Corpus status", id="al-corpus-title")
641
- yield Static("\n".join(self._corpus), id="al-corpus")
642
- detail = Static("", id="al-detail")
643
- detail.border_title = "About highlighted option"
644
- yield detail
645
- yield Static(
646
- f"Options (1-{len(self._options)} of {len(self._options)})", id="al-hdr"
647
- )
648
- option_list = OptionList()
649
- for option in self._options:
650
- label = option.label + ("" if option.enabled else " [unavailable]")
651
- option_list.add_option(
652
- Option(label, id=option.value, disabled=not option.enabled)
653
- )
654
- yield option_list
655
- footer = (
656
- "Up/Down move | Enter select | Esc back | q cancel"
657
- if self._allow_back
658
- else "Up/Down move | Enter select | Esc cancel | q cancel"
659
- )
660
- yield Static(footer, id="al-footer")
661
-
662
- def on_mount(self):
663
- option_list = self.query_one(OptionList)
664
- index = next(
665
- (
666
- position
667
- for position, option in enumerate(self._options)
668
- if option.value == self._default and option.enabled
669
- ),
670
- next(
671
- (
672
- position
673
- for position, option in enumerate(self._options)
674
- if option.enabled
675
- ),
676
- 0,
677
- ),
678
- )
679
- option_list.highlighted = index
680
- option_list.focus()
681
- self._describe(index)
682
-
683
- def on_option_list_option_highlighted(self, event):
684
- self._describe(event.option_index)
685
-
686
- def _describe(self, index):
687
- option = self._options[index]
688
- detail = option.description
689
- if not option.enabled and option.unavailable_reason:
690
- detail = f"{detail} Unavailable: {option.unavailable_reason}"
691
- self.query_one("#al-detail", Static).update(detail)
692
- # Live-preview the highlighted option's effect in the setup panel.
693
- if self._preview is not None:
694
- try:
695
- preview_plan = self._preview(option.value)
696
- self.query_one("#al-setup", Static).update(
697
- "\n".join(setup_summary_lines(preview_plan))
698
- )
699
- except Exception:
700
- pass
701
-
702
- def on_option_list_option_selected(self, event):
703
- self.dismiss(event.option.id)
704
-
705
- def action_back(self):
706
- self.dismiss(_UI_BACK if self._allow_back else _UI_CANCEL)
707
-
708
- def action_cancel(self):
709
- self.dismiss(_UI_CANCEL)
710
-
711
- class InputScreen(ModalScreen):
712
- BINDINGS = [
713
- Binding("escape", "cancel", "cancel", priority=True),
714
- Binding("ctrl+c", "cancel", "cancel", priority=True),
715
- ]
716
-
717
- def __init__(self, label, default, plan):
718
- super().__init__()
719
- self._label = label
720
- self._default = default
721
- self._plan = plan
722
-
723
- def compose(self):
724
- yield Static(self._label, id="al-title")
725
- yield setup_panel(self._plan)
726
- box = Static(
727
- f"Current value: {self._default}\n"
728
- "New value (leave blank to keep current):",
729
- id="al-detail",
730
- )
731
- box.border_title = "Edit model"
732
- yield box
733
- yield Static("", id="al-hdr")
734
- yield Input(placeholder="type a model id, or leave blank to keep current")
735
- yield Static(
736
- "Enter confirm | Esc/Ctrl-C cancel | q + Enter cancel", id="al-footer"
737
- )
738
-
739
- def on_mount(self):
740
- self.query_one(Input).focus()
741
-
742
- def on_input_submitted(self, event):
743
- value = event.value.strip()
744
- if value.lower() == "q":
745
- self.dismiss(_UI_CANCEL)
746
- else:
747
- self.dismiss(value or self._default)
748
-
749
- def action_cancel(self):
750
- self.dismiss(_UI_CANCEL)
751
-
752
- class PreflightApp(App):
753
- ENABLE_COMMAND_PALETTE = False
754
- CSS = app_css
755
- BINDINGS = [Binding("ctrl+q", "noop", show=False)]
756
-
757
- def __init__(self, config, host, preset_name, custom_requested, config_path):
758
- super().__init__()
759
- self._flow_args = (config, host, preset_name, custom_requested, config_path)
760
- self._host = host
761
- self.outcome = None
762
-
763
- def action_noop(self):
764
- pass
765
-
766
- def on_mount(self):
767
- theme = host_themes.get(self._host)
768
- if theme is not None:
769
- self.register_theme(theme)
770
- self.theme = theme.name
771
- self.run_flow()
772
-
773
- @work(thread=True)
774
- def run_flow(self):
775
- config, host, preset_name, custom_requested, config_path = self._flow_args
776
- ui = TextualUI(self, MenuScreen, InputScreen)
777
- try:
778
- self.outcome = (
779
- "plan",
780
- select_plan(
781
- config, host, preset_name, custom_requested, ui, config_path
782
- ),
783
- )
784
- except BaseException as exc: # surfaced to main; re-raised for the exit code
785
- self.outcome = ("error", exc)
786
- finally:
787
- self.call_from_thread(self.exit)
788
-
789
- return PreflightApp
790
-
791
-
792
- class TextualUI:
793
- def __init__(self, app, menu_screen, input_screen):
794
- self.app = app
795
- self._menu_screen = menu_screen
796
- self._input_screen = input_screen
797
- self.plan: dict[str, Any] | None = None
798
-
799
- def set_plan(self, plan: dict[str, Any]) -> None:
800
- self.plan = plan
801
-
802
- def choose(
803
- self,
804
- title: str,
805
- options: list[MenuOption],
806
- default: str,
807
- allow_back: bool,
808
- preview=None,
809
- corpus_lines: list[str] | None = None,
810
- ) -> str:
811
- result = self.app.call_from_thread(
812
- self.app.push_screen_wait,
813
- self._menu_screen(
814
- title, options, default, allow_back, self.plan, preview, corpus_lines
815
- ),
816
- )
817
- if result == _UI_BACK:
818
- raise BackRequested
819
- if result == _UI_CANCEL:
820
- raise KeyboardInterrupt
821
- return result
822
-
823
- def prompt_text(self, label: str, default: str) -> str:
824
- result = self.app.call_from_thread(
825
- self.app.push_screen_wait,
826
- self._input_screen(label, default, self.plan),
827
- )
828
- if result == _UI_CANCEL:
829
- raise KeyboardInterrupt
830
- return result
831
-
832
-
833
- def run_textual_flow(
834
- config: dict[str, Any],
835
- host: str,
836
- preset_name: str | None,
837
- custom_requested: bool,
838
- config_path: pathlib.Path,
839
- ) -> dict[str, Any]:
840
- app = _build_app_class()(config, host, preset_name, custom_requested, config_path)
841
- app.run()
842
- if app.outcome is None:
843
- raise KeyboardInterrupt
844
- kind, value = app.outcome
845
- if kind == "error":
846
- raise value
847
- return value
848
-
849
-
850
- def choose_lines(
851
- title: str,
852
- options: list[MenuOption],
853
- default: str,
854
- allow_back: bool,
855
- corpus_lines: list[str] | None = None,
856
- ) -> str:
857
- print(f"\n{title}")
858
- if corpus_lines:
859
- print(" -- Corpus status --")
860
- for line in corpus_lines:
861
- print(f" {line}")
862
- print(" --")
863
- for index, option in enumerate(options, 1):
864
- marker = "" if option.enabled else " [unavailable]"
865
- selected = " *" if option.value == default and option.enabled else ""
866
- label = option.label
867
- print(f" {index}. {label}{marker}{selected} - {option.description}")
868
- if not option.enabled:
869
- print(f" Unavailable: {option.unavailable_reason}")
870
- while True:
871
- actions = "b back | q cancel" if allow_back else "q cancel"
872
- raw = read_input(f"Select [{default}] ({actions}): ").strip()
873
- if not raw:
874
- default_option = next((option for option in options if option.value == default), None)
875
- if default_option and default_option.enabled:
876
- return default
877
- reason = default_option.unavailable_reason if default_option else "unknown option"
878
- print(f"Unavailable: {reason}. Select an enabled option.")
879
- continue
880
- if raw.lower() in {"q", "quit"}:
881
- raise KeyboardInterrupt
882
- if raw.lower() in {"b", "back"}:
883
- if allow_back:
884
- raise BackRequested
885
- print("No previous menu. Use q to cancel.")
886
- continue
887
- if raw.isdigit() and 1 <= int(raw) <= len(options):
888
- option = options[int(raw) - 1]
889
- if option.enabled:
890
- return option.value
891
- print(f"Unavailable: {option.unavailable_reason}")
892
-
893
-
894
- def choose(
895
- title: str,
896
- options: list[MenuOption],
897
- default: str,
898
- ui: TextualUI | None = None,
899
- allow_back: bool = False,
900
- preview=None,
901
- corpus_lines: list[str] | None = None,
902
- ) -> str:
903
- if not any(option.enabled for option in options):
904
- raise LaunchError(f"no available options for {title}")
905
- if ui is not None:
906
- return ui.choose(title, options, default, allow_back, preview, corpus_lines)
907
- return choose_lines(title, options, default, allow_back, corpus_lines)
908
-
909
-
910
- def read_input(prompt: str) -> str:
911
- try:
912
- return input(prompt)
913
- except EOFError as exc:
914
- raise LaunchError(
915
- "interactive input ended; use --preset NAME for a non-interactive configured launch"
916
- ) from exc
917
-
918
-
919
- def prompt_text(label: str, default: str, ui: TextualUI | None = None) -> str:
920
- if ui is not None:
921
- return ui.prompt_text(label, default)
922
- value = read_input(f"{label} [{default}] (q cancel): ").strip()
923
- if value.lower() == "q":
924
- raise KeyboardInterrupt
925
- return value or default
926
-
927
-
928
- def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> list[str]:
929
- """Selectable model catalog for a host: the configured [hosts.<host>].models
930
- list when present, else the distinct models already bound to the tiers."""
931
- models = config["hosts"][host].get("models")
932
- if models:
933
- return list(models)
934
- catalog: list[str] = []
935
- for tier in TIER_ORDER:
936
- model = tiers[tier]["model"]
937
- if model not in catalog:
938
- catalog.append(model)
939
- return catalog
940
-
941
-
942
- def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str, Any]:
943
- try:
944
- preset = copy.deepcopy(config["presets"][preset_name])
945
- tiers = copy.deepcopy(config["hosts"][host]["tiers"])
946
- except (KeyError, TypeError) as exc:
947
- raise LaunchError(f"invalid preset or host binding: {host}/{preset_name}") from exc
948
- if not isinstance(preset, dict) or not isinstance(tiers, dict):
949
- raise LaunchError(f"invalid preset or host binding: {host}/{preset_name}")
950
- if set(tiers) != set(TIER_ORDER):
951
- raise LaunchError(f"{host} tiers must be exactly: {', '.join(TIER_ORDER)}")
952
- for tier, binding in tiers.items():
953
- if not isinstance(binding, dict):
954
- raise LaunchError(f"invalid binding: {host}.{tier}")
955
- model = binding.get("model")
956
- if not isinstance(model, str) or not model:
957
- raise LaunchError(f"invalid binding: {host}.{tier}")
958
- validate_effort(host, model, binding.get("effort"), f"{host}.{tier}")
959
- all_overrides = preset.get("tier_overrides", {})
960
- if not isinstance(all_overrides, dict):
961
- raise LaunchError(f"tier_overrides must be a table in preset {preset_name}")
962
- host_overrides = all_overrides.get(host, {})
963
- if not isinstance(host_overrides, dict):
964
- raise LaunchError(f"tier_overrides.{host} must be a table in preset {preset_name}")
965
- for tier, override in host_overrides.items():
966
- if tier not in tiers:
967
- raise LaunchError(f"unknown tier in {preset_name}.tier_overrides.{host}: {tier}")
968
- if not isinstance(override, dict):
969
- raise LaunchError(f"tier override must be a table: {preset_name}.tier_overrides.{host}.{tier}")
970
- override_model = override.get("model", tiers[tier]["model"])
971
- if not isinstance(override_model, str) or not override_model:
972
- raise LaunchError(f"invalid override model: {preset_name}.tier_overrides.{host}.{tier}")
973
- override_effort = override.get("effort", tiers[tier]["effort"])
974
- validate_effort(host, override_model, override_effort, f"{preset_name}.tier_overrides.{host}.{tier}")
975
- tiers[tier] = {"model": override_model, "effort": override_effort}
976
- main_tier = preset.get("main_tier")
977
- if not isinstance(main_tier, str) or main_tier not in tiers:
978
- raise LaunchError(f"invalid main_tier in preset {preset_name}: {main_tier}")
979
- frontier_effort = preset.get("frontier_effort", tiers["frontier"]["effort"])
980
- if isinstance(frontier_effort, dict):
981
- frontier_effort = frontier_effort.get(host)
982
- validate_effort(host, tiers["frontier"]["model"], frontier_effort, f"{preset_name}.frontier")
983
- delegation = preset.get("delegation", True)
984
- if not isinstance(delegation, bool):
985
- raise LaunchError(f"delegation must be boolean in preset {preset_name}")
986
- review_setup = preset.get("review_setup", "none")
987
- if not isinstance(review_setup, str) or review_setup not in REVIEW_SETUPS:
988
- raise LaunchError(f"unknown review setup in preset {preset_name}: {review_setup!r}")
989
- codex_policy = preset.get("codex_execution_policy")
990
- claude_policy = preset.get("claude_permission_mode")
991
- if not isinstance(codex_policy, str) or codex_policy not in CODEX_POLICIES:
992
- raise LaunchError(f"invalid Codex policy in preset {preset_name}: {codex_policy!r}")
993
- if not isinstance(claude_policy, str) or claude_policy not in CLAUDE_POLICIES:
994
- raise LaunchError(f"invalid Claude policy in preset {preset_name}: {claude_policy!r}")
995
- label = preset.get("label", preset_name)
996
- if not isinstance(label, str) or not label:
997
- raise LaunchError(f"invalid label in preset {preset_name}")
998
- mode = preset.get("mode")
999
- if not isinstance(mode, str) or mode not in PRESET_MODES:
1000
- # Missing/unknown mode (older or user/local presets) defaults to builder
1001
- # rather than failing closed, so existing presets keep working unchanged.
1002
- mode = DEFAULT_PRESET_MODE
1003
- mission = preset.get("mission")
1004
- if mission is not None and (not isinstance(mission, str) or not mission):
1005
- raise LaunchError(f"presets.{preset_name}.mission must be a non-empty string")
1006
- trigger = preset.get("trigger")
1007
- if trigger is not None and (not isinstance(trigger, str) or not trigger):
1008
- raise LaunchError(f"presets.{preset_name}.trigger must be a non-empty string")
1009
- review_family = preset.get("review_family", "cross")
1010
- review_host = REVIEW_HOST[host]
1011
- opposite = config.get("hosts", {}).get(review_host)
1012
- opposite_backend = config.get("backends", {}).get(review_host, {})
1013
- if (
1014
- not isinstance(opposite, dict)
1015
- or not isinstance(opposite.get("tiers"), dict)
1016
- or not isinstance(opposite_backend, dict)
1017
- or not opposite_backend.get("command")
1018
- ):
1019
- # Cross review needs the opposite host + backend; without them fall back to
1020
- # same-family review (surfaced as review_family=same) rather than crashing.
1021
- review_family = "same"
1022
- review_tiers, review_onto, review_backend = {}, None, ""
1023
- else:
1024
- review_tiers = copy.deepcopy(opposite["tiers"])
1025
- review_onto = copy.deepcopy(opposite.get("onto_review"))
1026
- review_backend = opposite_backend["command"]
1027
- return {
1028
- "host": host,
1029
- "preset": preset_name,
1030
- "label": label,
1031
- "description": preset.get("description", f"Launch the {label} preset."),
1032
- "mode": mode,
1033
- "main_tier": main_tier,
1034
- "frontier_effort": frontier_effort,
1035
- "review_setup": review_setup,
1036
- "delegation": delegation,
1037
- "codex_execution_policy": codex_policy,
1038
- "claude_permission_mode": claude_policy,
1039
- "tiers": tiers,
1040
- "available_models": host_models(config, host, tiers),
1041
- "review_family": review_family,
1042
- "review_host": review_host,
1043
- "review_tiers": review_tiers,
1044
- "review_onto": review_onto,
1045
- "review_backend": review_backend,
1046
- "agent_templates": copy.deepcopy(config["hosts"][host].get("agent_templates")),
1047
- "capabilities": copy.deepcopy(config.get("capabilities", {})),
1048
- "mission": mission,
1049
- "trigger": trigger,
1050
- }
1051
-
1052
-
1053
- def review_description(host: str, name: str) -> str:
1054
- """The user-facing one-liner for the menu: what the setup is for and what it
1055
- costs. Distinct from ["contract"], which is the instruction sent to the agent."""
1056
- description = REVIEW_SETUPS[name]["description"]
1057
- return description[host] if isinstance(description, dict) else description
1058
-
1059
-
1060
- def effort_options(host: str, model: str) -> list[MenuOption]:
1061
- options = []
1062
- for effort in EFFORT_ORDER:
1063
- enabled = effort in HOST_EFFORTS[host]
1064
- reason = f"not supported by {host}" if not enabled else ""
1065
- if enabled and host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
1066
- enabled = False
1067
- reason = "not supported by gpt-5.6-luna"
1068
- options.append(
1069
- MenuOption(effort, effort, EFFORT_DESCRIPTIONS[effort], enabled, reason)
1070
- )
1071
- return options
1072
-
1073
-
1074
- def model_options(plan: dict[str, Any]) -> list[MenuOption]:
1075
- options = [
1076
- MenuOption(model, model, f"Use {model} as this tier's model.")
1077
- for model in plan["available_models"]
1078
- ]
1079
- options.append(
1080
- MenuOption(
1081
- OTHER_MODEL,
1082
- "Other (enter a model id)",
1083
- "Type any model id the backend accepts, even if it is not in the catalog.",
1084
- )
1085
- )
1086
- return options
1087
-
1088
-
1089
- def valid_preset_name(name: str) -> bool:
1090
- return bool(name) and name[0].isalnum() and all(
1091
- character.isalnum() or character in "-_" for character in name
1092
- )
1093
-
1094
-
1095
- def preset_from_plan(
1096
- plan: dict[str, Any], config: dict[str, Any], name: str
1097
- ) -> tuple[dict[str, Any], dict[str, Any]]:
1098
- """Project the plan into a saveable named preset plus host-scoped tier
1099
- overrides. Only tier bindings that differ from the host defaults are recorded,
1100
- scoped to the plan's host so the preset stays correct on the other host, which
1101
- falls back to its own defaults."""
1102
- host = plan["host"]
1103
- default_tiers = config["hosts"][host]["tiers"]
1104
- fields = {
1105
- "label": name,
1106
- "description": "Saved custom launch setup.",
1107
- "main_tier": plan["main_tier"],
1108
- "review_setup": plan["review_setup"],
1109
- "review_family": plan.get("review_family", "cross"),
1110
- "delegation": plan["delegation"],
1111
- "codex_execution_policy": plan["codex_execution_policy"],
1112
- "claude_permission_mode": plan["claude_permission_mode"],
1113
- }
1114
- overrides: dict[str, dict[str, str]] = {}
1115
- for tier in TIER_ORDER:
1116
- override: dict[str, str] = {}
1117
- if plan["tiers"][tier]["model"] != default_tiers[tier]["model"]:
1118
- override["model"] = plan["tiers"][tier]["model"]
1119
- if tier_effort(plan, tier) != default_tiers[tier]["effort"]:
1120
- override["effort"] = tier_effort(plan, tier)
1121
- if override:
1122
- overrides[tier] = override
1123
- return fields, ({host: overrides} if overrides else {})
1124
-
1125
-
1126
- def _toml_scalar(value: Any) -> str:
1127
- if isinstance(value, bool):
1128
- return "true" if value else "false"
1129
- if isinstance(value, str):
1130
- return json.dumps(value, ensure_ascii=False)
1131
- raise LaunchError(f"cannot serialize preset value: {value!r}")
1132
-
1133
-
1134
- def render_preset_block(
1135
- name: str, fields: dict[str, Any], tier_overrides: dict[str, Any]
1136
- ) -> str:
1137
- lines = [f"[presets.{name}]"]
1138
- for key, value in fields.items():
1139
- lines.append(f"{key} = {_toml_scalar(value)}")
1140
- for host, tiers in tier_overrides.items():
1141
- for tier, override in tiers.items():
1142
- lines.append("")
1143
- lines.append(f"[presets.{name}.tier_overrides.{host}.{tier}]")
1144
- for key, value in override.items():
1145
- lines.append(f"{key} = {_toml_scalar(value)}")
1146
- return "\n".join(lines) + "\n"
1147
-
1148
-
1149
- def remove_preset_block(text: str, name: str) -> str:
1150
- """Drop an existing [presets.<name>] table and its sub-tables, leaving the
1151
- rest of the file (other presets, comments, host bindings) intact."""
1152
- exact = f"[presets.{name}]"
1153
- sub_prefix = f"[presets.{name}."
1154
- kept = []
1155
- skipping = False
1156
- for line in text.splitlines(keepends=True):
1157
- header = line.strip()
1158
- if header.startswith("["):
1159
- skipping = header == exact or header.startswith(sub_prefix)
1160
- if skipping:
1161
- continue
1162
- kept.append(line)
1163
- return "".join(kept)
1164
-
1165
-
1166
- def save_preset(
1167
- plan: dict[str, Any], config: dict[str, Any], config_path: pathlib.Path, name: str
1168
- ) -> None:
1169
- if not valid_preset_name(name):
1170
- raise LaunchError(
1171
- "preset name must start alphanumeric and use only letters, digits, '-' or '_'"
1172
- )
1173
- fields, tier_overrides = preset_from_plan(plan, config, name)
1174
- block = render_preset_block(name, fields, tier_overrides)
1175
- target = user_presets_path(config_path)
1176
- try:
1177
- kept = remove_preset_block(target.read_text(), name).rstrip("\n")
1178
- except FileNotFoundError:
1179
- kept = USER_PRESETS_HEADER.rstrip("\n")
1180
- text = kept + "\n\n" + block
1181
- temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
1182
- try:
1183
- target.parent.mkdir(parents=True, exist_ok=True)
1184
- temporary.write_text(text)
1185
- os.replace(temporary, target)
1186
- except OSError as exc:
1187
- raise LaunchError(f"cannot save preset to {target}: {exc}") from exc
1188
-
1189
-
1190
- def customize(
1191
- plan: dict[str, Any],
1192
- config: dict[str, Any],
1193
- config_path: pathlib.Path,
1194
- ui: TextualUI | None = None,
1195
- ) -> None:
1196
- tier_options = [
1197
- MenuOption(tier, tier.upper(), TIER_DESCRIPTIONS[tier]) for tier in TIER_ORDER
1198
- ]
1199
- available_routes = route_availability(plan)
1200
- cross = plan.get("review_family", "cross") == "cross"
1201
- review_options = []
1202
- for name, spec in REVIEW_SETUPS.items():
1203
- description = review_description(plan["host"], name)
1204
- if cross and name != "none":
1205
- description = f"Runs cross-family on {plan['review_host']} models. {description}"
1206
- missing = [
1207
- route
1208
- for route in REVIEW_ROUTES[name]
1209
- if route in ("onto", "ultracode") and not available_routes[route]
1210
- ]
1211
- if missing:
1212
- note = "degrades to same-family native (PROPOSED)" if cross else "degrades to native"
1213
- description = (
1214
- f"{description} ({', '.join(missing)} unavailable now; {note}"
1215
- f"{install_hint(plan, missing)})"
1216
- )
1217
- review_options.append(MenuOption(name, spec["label"], description))
1218
- if plan["host"] == "codex":
1219
- policies = [
1220
- MenuOption(
1221
- "bypass",
1222
- "Bypass approvals and sandbox",
1223
- "Run without Codex approval or sandbox restrictions.",
1224
- ),
1225
- MenuOption(
1226
- "workspace-write",
1227
- "Workspace write sandbox",
1228
- "Allow workspace writes under Codex's workspace-write sandbox.",
1229
- ),
1230
- MenuOption(
1231
- "read-only",
1232
- "Read-only sandbox",
1233
- "Allow inspection while blocking file writes through the Codex sandbox.",
1234
- ),
1235
- MenuOption(
1236
- STANDARD_POLICY,
1237
- "Standard",
1238
- "Pass no policy flag; use Codex's own default approval and sandbox behavior.",
1239
- ),
1240
- ]
1241
- policy_field = "codex_execution_policy"
1242
- policy_title = "Codex execution policy"
1243
- else:
1244
- policies = [
1245
- MenuOption(
1246
- "bypassPermissions",
1247
- "Bypass permissions",
1248
- "Skip Claude permission prompts; this is not an OS sandbox setting.",
1249
- ),
1250
- MenuOption(
1251
- "acceptEdits",
1252
- "Accept edits",
1253
- "Automatically accept file edits while retaining other permission checks.",
1254
- ),
1255
- MenuOption(
1256
- "auto",
1257
- "Auto permissions",
1258
- "Use Claude's automatic permission mode.",
1259
- ),
1260
- MenuOption(
1261
- "manual",
1262
- "Manual permissions",
1263
- "Ask before actions according to Claude's manual permission flow.",
1264
- ),
1265
- MenuOption(
1266
- "dontAsk",
1267
- "Do not ask",
1268
- "Use Claude's dontAsk permission mode.",
1269
- ),
1270
- MenuOption(
1271
- "plan",
1272
- "Plan mode",
1273
- "Start Claude in planning mode without direct implementation.",
1274
- ),
1275
- MenuOption(
1276
- STANDARD_POLICY,
1277
- "Standard",
1278
- "Pass no policy flag; use Claude's own default permission prompts.",
1279
- ),
1280
- ]
1281
- policy_field = "claude_permission_mode"
1282
- policy_title = "Claude permission mode (not an OS sandbox)"
1283
- selected_action = "main"
1284
- while True:
1285
- policy_label = next(
1286
- option.label for option in policies if option.value == plan[policy_field]
1287
- )
1288
- hub_options = [
1289
- MenuOption(
1290
- "main",
1291
- f"Main tier: {plan['main_tier'].upper()}",
1292
- "Choose the primary orchestrator used for this session.",
1293
- ),
1294
- MenuOption(
1295
- "review",
1296
- f"Review setup: {REVIEW_SETUPS[plan['review_setup']]['label']}",
1297
- "Choose the review routes requested when review gates fire.",
1298
- ),
1299
- MenuOption(
1300
- "policy",
1301
- f"Execution policy: {policy_label}",
1302
- "Choose the host-specific permission or sandbox behavior.",
1303
- ),
1304
- ]
1305
- for tier in TIER_ORDER:
1306
- binding = plan["tiers"][tier]
1307
- hub_options.append(
1308
- MenuOption(
1309
- f"tier:{tier}",
1310
- f"{tier.upper()}: {binding['model']} / {tier_effort(plan, tier)}",
1311
- f"Edit the {tier.upper()} model and reasoning effort, then return here.",
1312
- )
1313
- )
1314
- hub_options += [
1315
- MenuOption(
1316
- "save",
1317
- "Save these settings globally and start",
1318
- "Save this setup as a named preset in your user config for reuse elsewhere, then launch.",
1319
- ),
1320
- MenuOption(
1321
- "start",
1322
- "Start with these settings",
1323
- "Confirm the complete setup shown above and continue to launch.",
1324
- ),
1325
- MenuOption(
1326
- "exit",
1327
- "Exit without launching",
1328
- "Discard this launch and return to the shell.",
1329
- ),
1330
- ]
1331
-
1332
- try:
1333
- action = choose(
1334
- "Custom settings",
1335
- hub_options,
1336
- selected_action,
1337
- ui,
1338
- allow_back=True,
1339
- )
1340
- except BackRequested:
1341
- raise
1342
- selected_action = action
1343
-
1344
- try:
1345
- if action == "main":
1346
- plan["main_tier"] = choose(
1347
- "Main tier",
1348
- tier_options,
1349
- plan["main_tier"],
1350
- ui,
1351
- allow_back=True,
1352
- preview=lambda value: {**plan, "main_tier": value},
1353
- )
1354
- elif action == "review":
1355
- plan["review_setup"] = choose(
1356
- "Review setup",
1357
- review_options,
1358
- plan["review_setup"],
1359
- ui,
1360
- allow_back=True,
1361
- preview=lambda value: {**plan, "review_setup": value},
1362
- )
1363
- elif action == "policy":
1364
- plan[policy_field] = choose(
1365
- policy_title,
1366
- policies,
1367
- plan[policy_field],
1368
- ui,
1369
- allow_back=True,
1370
- preview=lambda value: {**plan, policy_field: value},
1371
- )
1372
- except BackRequested:
1373
- continue
1374
-
1375
- if action.startswith("tier:"):
1376
- tier = action.split(":", 1)[1]
1377
- binding = plan["tiers"][tier]
1378
- while True:
1379
- default_model = (
1380
- binding["model"]
1381
- if binding["model"] in plan["available_models"]
1382
- else OTHER_MODEL
1383
- )
1384
- try:
1385
- chosen_model = choose(
1386
- f"{tier.upper()} model",
1387
- model_options(plan),
1388
- default_model,
1389
- ui,
1390
- allow_back=True,
1391
- preview=lambda value: {
1392
- **plan,
1393
- "tiers": {
1394
- **plan["tiers"],
1395
- tier: {
1396
- **binding,
1397
- "model": binding["model"]
1398
- if value == OTHER_MODEL
1399
- else value,
1400
- },
1401
- },
1402
- },
1403
- )
1404
- except BackRequested:
1405
- break
1406
- if chosen_model == OTHER_MODEL:
1407
- binding["model"] = prompt_text(
1408
- f"{tier.upper()} model", binding["model"], ui
1409
- )
1410
- else:
1411
- binding["model"] = chosen_model
1412
- current_effort = tier_effort(plan, tier)
1413
- try:
1414
- binding["effort"] = choose(
1415
- f"{tier.upper()} effort",
1416
- effort_options(plan["host"], binding["model"]),
1417
- current_effort,
1418
- ui,
1419
- allow_back=True,
1420
- preview=lambda value: {
1421
- **plan,
1422
- "frontier_effort": value
1423
- if tier == "frontier"
1424
- else plan["frontier_effort"],
1425
- "tiers": {
1426
- **plan["tiers"],
1427
- tier: {**binding, "effort": value},
1428
- },
1429
- },
1430
- )
1431
- except BackRequested:
1432
- continue
1433
- break
1434
- if tier == "frontier":
1435
- plan["frontier_effort"] = binding["effort"]
1436
- continue
1437
-
1438
- if action in ("save", "start"):
1439
- for tier, binding in plan["tiers"].items():
1440
- validate_effort(
1441
- plan["host"],
1442
- binding["model"],
1443
- tier_effort(plan, tier),
1444
- f"custom.{tier}",
1445
- )
1446
- validate_review_setup(plan)
1447
- if action == "save":
1448
- name = prompt_text(
1449
- "Save as preset name", f"{plan['preset']}-custom", ui
1450
- )
1451
- save_preset(plan, config, config_path, name)
1452
- plan["_launch_confirmed"] = True
1453
- return
1454
- if action == "exit":
1455
- raise KeyboardInterrupt
1456
-
1457
-
1458
- DISTILL_PRESET = "session-distill"
1459
- CORPUS_STATUS_PATH = pathlib.Path(
1460
- os.environ.get(
1461
- "AGENT_BIOS_CORPUS_STATUS",
1462
- str(pathlib.Path.home() / ".local/share/agent-bios/corpus-status.json"),
1463
- )
1464
- )
1465
-
1466
- VERSION_INFO_PATH = pathlib.Path(
1467
- os.environ.get(
1468
- "AGENT_LAUNCH_VERSION_FILE",
1469
- str(pathlib.Path.home() / ".local/share/agent-bios/version.json"),
1470
- )
1471
- )
1472
-
1473
-
1474
- def load_corpus_status() -> dict[str, Any] | None:
1475
- try:
1476
- return json.loads(CORPUS_STATUS_PATH.read_text())
1477
- except (OSError, ValueError):
1478
- return None
1479
-
1480
-
1481
- def version_label() -> str | None:
1482
- """The deployed agent-bios version + release date for the TUI, or None when
1483
- the marker is absent (uninstalled / dev checkout). `agent-bios install`
1484
- writes it from package.json (version + releaseDate). This is the deploy /
1485
- system version — distinct from the corpus content version in the distill hub."""
1486
- try:
1487
- info = json.loads(VERSION_INFO_PATH.read_text())
1488
- except (OSError, ValueError):
1489
- return None
1490
- version = info.get("version") if isinstance(info, dict) else None
1491
- if not version:
1492
- return None
1493
- released = info.get("releaseDate")
1494
- return f"agent-bios v{version} · {released}" if released else f"agent-bios v{version}"
1495
-
1496
-
1497
- def corpus_summary_lines(status: dict[str, Any] | None) -> list[str]:
1498
- """Panel body for the Session Distill area: which corpus content is
1499
- live, through which mechanisms, and whether the corpus is rolled back."""
1500
- if status is None:
1501
- return ["corpus status not projected yet — run: agent-bios install"]
1502
- current = status.get("current_version", "?")
1503
- latest = status.get("latest_version", "?")
1504
- head = f"Applied version {current}"
1505
- if status.get("rolled_back_to"):
1506
- head += f" (ROLLED BACK; latest is {latest})"
1507
- layers = status.get("summary", {}).get("placed_by_layer", {})
1508
- order = ("global", "guide", "hook", "enforcement", "gate")
1509
- layer_text = " · ".join(
1510
- f"{name} {layers[name]}" for name in order if layers.get(name)
1511
- ) or "none"
1512
- by_status = status.get("summary", {}).get("by_status", {})
1513
- return [
1514
- head,
1515
- f"Mechanisms {layer_text}",
1516
- f"Ledger placed {by_status.get('placed', 0)} · "
1517
- f"incubating {by_status.get('incubating', 0) + by_status.get('incubating-G', 0)} · "
1518
- f"versions {len(status.get('versions', []))}",
1519
- ]
1520
-
1521
-
1522
- def _corpus_info(ui: TextualUI | None, title: str, lines: list[str]) -> None:
1523
- """Info screen in both UIs: options carry the content; only exit is back."""
1524
- options = [MenuOption("back", "Back", "Return to Session Distill.")]
1525
- try:
1526
- choose(title, options, "back", ui, allow_back=True, corpus_lines=lines)
1527
- except BackRequested:
1528
- pass
1529
-
1530
-
1531
- def _corpus_rollback(status: dict[str, Any], version: str, ui: TextualUI | None) -> None:
1532
- confirm = choose(
1533
- f"Roll back corpus to {version}?",
1534
- [
1535
- MenuOption(
1536
- "cancel", "Cancel", "Keep the currently deployed corpus content."
1537
- ),
1538
- MenuOption(
1539
- "rollback",
1540
- f"Roll back to {version}",
1541
- "Re-deploys globals/guides/hooks as of that corpus version. "
1542
- "System deployment (launcher, wrappers) stays current. Roll "
1543
- "forward again by selecting the latest version.",
1544
- ),
1545
- ],
1546
- "cancel",
1547
- ui,
1548
- allow_back=True,
1549
- )
1550
- if confirm != "rollback":
1551
- return
1552
- script = pathlib.Path(status["repo"]) / "scripts/session-distill/corpus-state.py"
1553
- result = subprocess.run(
1554
- [sys.executable, str(script), "rollback", "--version", version],
1555
- capture_output=True,
1556
- text=True,
1557
- )
1558
- output = (result.stdout + result.stderr).strip().splitlines()
1559
- tail = output[-1] if output else ""
1560
- verdict = "Rollback complete." if result.returncode == 0 else "Rollback FAILED."
1561
- _corpus_info(ui, verdict, [tail] if tail else [])
1562
-
1563
-
1564
- def _corpus_versions(ui: TextualUI | None) -> None:
1565
- while True:
1566
- status = load_corpus_status()
1567
- if status is None:
1568
- _corpus_info(ui, "Versions & rollback", corpus_summary_lines(None))
1569
- return
1570
- current = status.get("current_version")
1571
- options = []
1572
- for v in reversed(status.get("versions", [])):
1573
- name = v["version"]
1574
- label = f"{name} (current)" if name == current else name
1575
- options.append(
1576
- MenuOption(
1577
- name,
1578
- label,
1579
- f"closed {v.get('closed', '?')} · commit {v.get('commit', '')[:12]} · "
1580
- f"{v.get('summary', '')}",
1581
- )
1582
- )
1583
- options.append(MenuOption("back", "Back", "Return to Session Distill."))
1584
- try:
1585
- selected = choose(
1586
- "Versions & rollback",
1587
- options,
1588
- options[0].value,
1589
- ui,
1590
- allow_back=True,
1591
- corpus_lines=corpus_summary_lines(status),
1592
- )
1593
- except BackRequested:
1594
- return
1595
- if selected == "back":
1596
- return
1597
- if selected == current:
1598
- _corpus_info(
1599
- ui,
1600
- f"{selected} is the currently applied version",
1601
- ["Select a different version to roll back or forward."],
1602
- )
1603
- continue
1604
- _corpus_rollback(status, selected, ui)
1605
-
1606
-
1607
- def _corpus_packages(ui: TextualUI | None) -> None:
1608
- """v1: the corpus ships as a single core package; the list shape is ready
1609
- for the domain-packaging backlog to populate with real packages."""
1610
- status = load_corpus_status()
1611
- if status is None:
1612
- _corpus_info(ui, "Corpus packages", corpus_summary_lines(None))
1613
- return
1614
- layers = status.get("summary", {}).get("placed_by_layer", {})
1615
- options = [
1616
- MenuOption(
1617
- "core",
1618
- f"core corpus @ {status.get('current_version', '?')}",
1619
- "All corpus domains in one package until domain packaging "
1620
- f"lands (backlog). Layers: {json.dumps(layers, separators=(', ', ' '))}. "
1621
- "Per-domain selection, versions, and rollback will appear here.",
1622
- ),
1623
- MenuOption("back", "Back", "Return to Session Distill."),
1624
- ]
1625
- try:
1626
- choose(
1627
- "Corpus packages",
1628
- options,
1629
- "back",
1630
- ui,
1631
- allow_back=True,
1632
- corpus_lines=corpus_summary_lines(status),
1633
- )
1634
- except BackRequested:
1635
- pass
1636
-
1637
-
1638
- def distill_hub(config: dict[str, Any], ui: TextualUI | None) -> str:
1639
- """Session Distill area: status, packages, versions/rollback, session start.
1640
-
1641
- Returns "start" to launch the session-distill preset, "back" otherwise.
1642
- """
1643
- while True:
1644
- status = load_corpus_status()
1645
- options = [
1646
- MenuOption(
1647
- "start",
1648
- "Start a session distill run",
1649
- "Launch with the Session distill preset: the mission line "
1650
- "directs the session to the workflow guide and the SSOT.",
1651
- enabled=DISTILL_PRESET in config["presets"],
1652
- unavailable_reason=f"preset {DISTILL_PRESET!r} not configured",
1653
- ),
1654
- MenuOption(
1655
- "packages",
1656
- "Corpus packages",
1657
- "What corpus content is applied, as installable packages "
1658
- "(v1: single core corpus).",
1659
- ),
1660
- MenuOption(
1661
- "versions",
1662
- "Versions & rollback",
1663
- "Corpus content versions (distinct from system deployment); "
1664
- "roll the corpus back or forward.",
1665
- ),
1666
- MenuOption("back", "Back", "Return to the launch menu."),
1667
- ]
1668
- try:
1669
- selected = choose(
1670
- "Session Distill",
1671
- options,
1672
- "start",
1673
- ui,
1674
- allow_back=True,
1675
- corpus_lines=corpus_summary_lines(status),
1676
- )
1677
- except BackRequested:
1678
- return "back"
1679
- if selected == "back":
1680
- return "back"
1681
- if selected == "start":
1682
- return "start"
1683
- if selected == "packages":
1684
- _corpus_packages(ui)
1685
- elif selected == "versions":
1686
- _corpus_versions(ui)
1687
-
1688
-
1689
- def preset_mode(data: dict[str, Any]) -> str:
1690
- """A preset's root-menu group. Missing/unknown falls back to builder — the
1691
- same default build_plan applies — so listing presets for a menu never
1692
- raises on a not-yet-migrated user preset."""
1693
- mode = data.get("mode")
1694
- return mode if isinstance(mode, str) and mode in PRESET_MODES else DEFAULT_PRESET_MODE
1695
-
1696
-
1697
- def mode_default_preset(presets: dict[str, Any], mode: str) -> str | None:
1698
- """First configured preset in a mode; builder prefers 'balanced' so the
1699
- long-standing everyday default stays the highlighted choice. None means
1700
- the mode has no presets configured (only reachable via a stripped-down
1701
- user config; Custom still covers builder in that case)."""
1702
- names = [name for name, data in presets.items() if preset_mode(data) == mode]
1703
- if mode == DEFAULT_PRESET_MODE and "balanced" in names:
1704
- return "balanced"
1705
- return names[0] if names else None
1706
-
1707
-
1708
- def pick_mode_and_preset(
1709
- config: dict[str, Any],
1710
- host: str,
1711
- ui: TextualUI | None,
1712
- resume_mode: str | None,
1713
- ) -> tuple[str, bool, str]:
1714
- """Root menu: a 3-way mode picker (Software Engineer / Builder / Session
1715
- distill), then that mode's preset submenu (Software Engineer and Builder
1716
- both list Custom).
1717
-
1718
- Esc in the submenu returns to the mode picker; Esc at the mode picker
1719
- cancels the launcher, matching the picker's prior root Esc/q semantics.
1720
- Returns (preset_name, custom_requested, mode) — the mode is handed back so
1721
- a later Esc out of the Custom hub resumes this same submenu instead of
1722
- dropping all the way back to the top mode picker.
1723
- """
1724
- presets = config["presets"]
1725
- mode = resume_mode
1726
- while True:
1727
- if mode is None:
1728
- mode_options = [
1729
- MenuOption(
1730
- SWE_MODE,
1731
- "Software Engineer",
1732
- "Repo-scoped work that defers to the project's own "
1733
- "AGENTS.md/CLAUDE.md: a bare Vanilla session, or Custom to "
1734
- "configure one.",
1735
- ),
1736
- MenuOption(
1737
- DEFAULT_PRESET_MODE,
1738
- "Builder",
1739
- "Tune tiers, review routes, and permissions across fixed "
1740
- "presets; includes Custom.",
1741
- ),
1742
- MenuOption(
1743
- DISTILL_MODE,
1744
- "Session distill",
1745
- "Enter the dedicated session-distill hub: status, "
1746
- "packages, versions, and session start.",
1747
- ),
1748
- ]
1749
-
1750
- def preview_mode(value: str) -> dict[str, Any] | None:
1751
- target = (
1752
- DISTILL_PRESET
1753
- if value == DISTILL_MODE
1754
- else mode_default_preset(presets, value)
1755
- )
1756
- return build_plan(config, host, target) if target else None
1757
-
1758
- initial = preview_mode(DEFAULT_PRESET_MODE)
1759
- if ui is not None and initial is not None:
1760
- ui.set_plan(initial)
1761
- mode = choose(
1762
- "Mode",
1763
- mode_options,
1764
- DEFAULT_PRESET_MODE,
1765
- ui,
1766
- preview=preview_mode,
1767
- corpus_lines=corpus_summary_lines(load_corpus_status()),
1768
- )
1769
- if mode == DISTILL_MODE:
1770
- if distill_hub(config, ui) != "start":
1771
- mode = None
1772
- continue
1773
- if DISTILL_PRESET not in presets:
1774
- raise LaunchError(f"preset {DISTILL_PRESET!r} missing from config")
1775
- return DISTILL_PRESET, False, mode
1776
- options = [
1777
- MenuOption(
1778
- name,
1779
- data["label"],
1780
- data.get("description", f"Launch the {data['label']} preset."),
1781
- )
1782
- for name, data in presets.items()
1783
- if preset_mode(data) == mode
1784
- ]
1785
- if mode in (DEFAULT_PRESET_MODE, SWE_MODE):
1786
- options.append(
1787
- MenuOption(
1788
- CUSTOM_PRESET,
1789
- "Custom",
1790
- "Open a settings hub for tiers, review setup, policy, and final confirmation.",
1791
- )
1792
- )
1793
- default = mode_default_preset(presets, mode) or CUSTOM_PRESET
1794
- # Custom always starts from the builder default (an applied baseline), so a
1795
- # Custom entered from Software Engineer mode applies its settings instead of
1796
- # inheriting Vanilla's bare short-circuit. (SE-specific review defaults are
1797
- # deferred; today Custom shares the builder baseline.)
1798
- custom_base = mode_default_preset(presets, DEFAULT_PRESET_MODE) or default
1799
- if ui is not None and default != CUSTOM_PRESET:
1800
- ui.set_plan(build_plan(config, host, default))
1801
-
1802
- def preview_preset(value: str) -> dict[str, Any]:
1803
- target = custom_base if value == CUSTOM_PRESET else value
1804
- return build_plan(config, host, target)
1805
-
1806
- try:
1807
- selected = choose(
1808
- "Preset", options, default, ui, allow_back=True, preview=preview_preset
1809
- )
1810
- except BackRequested:
1811
- mode = None
1812
- continue
1813
- if selected == CUSTOM_PRESET:
1814
- return custom_base, True, mode
1815
- return selected, False, mode
1816
-
1817
-
1818
- def select_plan(
1819
- config: dict[str, Any],
1820
- host: str,
1821
- preset_name: str | None,
1822
- custom_requested: bool,
1823
- ui: TextualUI | None = None,
1824
- config_path: pathlib.Path | None = None,
1825
- ) -> dict[str, Any]:
1826
- presets = config["presets"]
1827
- explicit_preset = preset_name
1828
- show_picker = explicit_preset is None
1829
- resume_mode: str | None = None
1830
- while True:
1831
- selected_name = explicit_preset
1832
- selected_custom = custom_requested
1833
- if show_picker:
1834
- selected_name, selected_custom, resume_mode = pick_mode_and_preset(
1835
- config, host, ui, resume_mode
1836
- )
1837
- if selected_name not in presets:
1838
- raise LaunchError(f"unknown preset: {selected_name}")
1839
- plan = build_plan(config, host, selected_name)
1840
- if selected_custom:
1841
- plan["label"] = f"Custom ({plan['label']})"
1842
- # A customized plan is always an applied setup, never the bare Vanilla
1843
- # short-circuit — even if custom_base fell back to an SE-mode preset
1844
- # under a stripped config with no builder presets. project_args
1845
- # projects the bare backend iff mode == SWE_MODE, so force a Custom
1846
- # launch out of that mode.
1847
- if plan.get("mode") == SWE_MODE:
1848
- plan["mode"] = DEFAULT_PRESET_MODE
1849
- if ui is not None:
1850
- ui.set_plan(plan)
1851
- if selected_custom:
1852
- try:
1853
- customize(plan, config, config_path, ui)
1854
- except BackRequested:
1855
- if show_picker:
1856
- continue
1857
- raise KeyboardInterrupt
1858
- return plan
1859
-
1860
-
1861
- def validate_review_setup(plan: dict[str, Any]) -> None:
1862
- # Fail-closed only on the delegation contradiction (or an unknown setup); a
1863
- # missing external capability degrades to native in effective_review rather
1864
- # than raising here.
1865
- effective_review(plan)
1866
-
1867
-
1868
- def tier_effort(plan: dict[str, Any], tier: str) -> str:
1869
- return plan["frontier_effort"] if tier == "frontier" else plan["tiers"][tier]["effort"]
1870
-
1871
-
1872
- def codex_agent_configs(
1873
- plan: dict[str, Any], materialize: bool
1874
- ) -> dict[str, tuple[pathlib.Path, str]]:
1875
- templates = plan.get("agent_templates")
1876
- if not isinstance(templates, dict):
1877
- raise LaunchError("Codex delegation requires [hosts.codex.agent_templates]")
1878
- rendered: dict[str, tuple[str, str]] = {}
1879
- for tier in SPAWNABLE_TIERS:
1880
- source_value = templates.get(tier)
1881
- if not isinstance(source_value, str) or not source_value:
1882
- raise LaunchError(f"Codex agent template missing: {tier}")
1883
- source = expand_config_path(source_value)
1884
- try:
1885
- data = tomllib.loads(source.read_text())
1886
- except (OSError, tomllib.TOMLDecodeError) as exc:
1887
- raise LaunchError(f"cannot load Codex agent template {source}: {exc}") from exc
1888
- description = data.get("description")
1889
- if not isinstance(description, str) or not description:
1890
- raise LaunchError(f"Codex agent template requires description: {source}")
1891
- data["model"] = plan["tiers"][tier]["model"]
1892
- data["model_reasoning_effort"] = tier_effort(plan, tier)
1893
- lines = []
1894
- for key, value in data.items():
1895
- if not isinstance(value, (str, int, float, bool)):
1896
- raise LaunchError(f"unsupported Codex agent template value: {source}:{key}")
1897
- lines.append(f"{key} = {json.dumps(value)}")
1898
- rendered[tier] = ("\n".join(lines) + "\n", description)
1899
- digest = hashlib.sha256(
1900
- json.dumps(rendered, sort_keys=True, separators=(",", ":")).encode()
1901
- ).hexdigest()[:20]
1902
- cache_root = pathlib.Path(
1903
- os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")
1904
- ) / "agent-launch/codex-agents" / digest
1905
- if materialize:
1906
- cache_root.mkdir(parents=True, exist_ok=True)
1907
- result = {}
1908
- for tier, (content, description) in rendered.items():
1909
- target = cache_root / f"{tier}.toml"
1910
- if materialize and (not target.is_file() or target.read_text() != content):
1911
- temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
1912
- temporary.write_text(content)
1913
- temporary.chmod(0o600)
1914
- os.replace(temporary, target)
1915
- result[tier] = (target, description)
1916
- return result
1917
-
1918
-
1919
- def _same_review_route(plan: dict[str, Any], effective: list[str], dropped: list[str]) -> str:
1920
- requested = plan["review_setup"]
1921
- if dropped:
1922
- kept = ", ".join(ROUTE_LABELS[route] for route in effective) or "no additional review route"
1923
- route = f"{', '.join(dropped)} unavailable at launch; degraded to {kept}."
1924
- else:
1925
- route = REVIEW_SETUPS[requested]["contract"]
1926
- if isinstance(route, dict):
1927
- route = route[plan["host"]]
1928
- if "ultracode" in effective:
1929
- route = f"{route} Ultracode executable: {resolve_command(plan['capabilities']['ultracode']['command'])}."
1930
- if "ultracode" in REVIEW_ROUTES[requested]:
1931
- route = (
1932
- f"{route} If the Codex-backed route is unavailable or unauthenticated "
1933
- "at use time, fall back to native same-model subagent review."
1934
- )
1935
- return route
1936
-
1937
-
1938
- def _cross_review_route(
1939
- plan: dict[str, Any], effective: list[str], dropped: list[str], floor: str | None
1940
- ) -> str:
1941
- review_host = plan["review_host"]
1942
- main_family = "Anthropic/Claude" if plan["host"] == "claude" else "OpenAI/Codex"
1943
- review_family = "OpenAI/Codex" if review_host == "codex" else "Anthropic/Claude"
1944
- review_bindings = ", ".join(
1945
- f"{tier}={plan['review_tiers'][tier]['model']}/{plan['review_tiers'][tier]['effort']}"
1946
- for tier in TIER_ORDER
1947
- if tier in plan["review_tiers"]
1948
- )
1949
- parts = [
1950
- f"Cross-family review: this main is {main_family}; run EVERY review route on "
1951
- f"{review_family} ({review_host}) models. Do not use your own same-family "
1952
- "subagents for primary review — they are the delegation/fallback floor only.",
1953
- f"Reviewer tier bindings ({review_host}): {review_bindings}.",
1954
- ]
1955
- if "native" in effective:
1956
- native = cross_native_command(plan)
1957
- if review_host == "codex":
1958
- helm = cross_helm_command(plan)
1959
- fanout = f" (or {helm} --mode review for hybrid fan-out)" if helm else ""
1960
- parts.append(
1961
- f"native: dispatch {native} --profile hermetic --model <review tier> "
1962
- f"--effort <e> --sandbox read-only{fanout}, self-contained packet on stdin, "
1963
- "bounded read-only report."
1964
- )
1965
- else:
1966
- parts.append(
1967
- f"native: dispatch {native} -p --model <review tier> --effort <e> "
1968
- "--permission-mode plan --append-system-prompt <read-only reviewer role>, "
1969
- "self-contained packet, bounded report."
1970
- )
1971
- if "onto" in effective:
1972
- onto = plan["review_onto"]
1973
- parts.append(
1974
- f'onto: call onto_review/onto_prepare_review with llmOverride='
1975
- f'{{"provider":"{onto["provider"]}","model":"{onto["model"]}"}} so onto runs {review_family}.'
1976
- )
1977
- if "ultracode" in effective:
1978
- ultracode = cross_ultracode_command(plan)
1979
- if review_host == "codex":
1980
- parts.append(
1981
- f"ultracode: run {ultracode} (the $ultracode-for-codex Codex skill) "
1982
- f"for {review_family} workflow-orchestration review."
1983
- )
1984
- else:
1985
- parts.append(
1986
- f"ultracode: run {ultracode} --effort ultracode -p <self-contained review packet> "
1987
- f"(Claude Code /workflows ultracode mode, headless) for {review_family} "
1988
- "workflow-orchestration review."
1989
- )
1990
- if dropped:
1991
- detail = [
1992
- f"onto (add a [hosts.{review_host}].onto_review pin)"
1993
- if route == "onto" and not plan.get("review_onto")
1994
- else route
1995
- for route in dropped
1996
- ]
1997
- parts.append(f"Unavailable cross-family route(s) at launch: {', '.join(detail)}.")
1998
- if floor == "native":
1999
- parts.append(
2000
- "No cross-family route resolved at launch; using same-family native "
2001
- "subagent review labeled PROPOSED (family collapse)."
2002
- )
2003
- elif floor:
2004
- setup_contract = REVIEW_SETUPS[plan["review_setup"]]["contract"]
2005
- if isinstance(setup_contract, dict):
2006
- setup_contract = setup_contract[plan["host"]]
2007
- parts.append(
2008
- f"This review route runs on this main's own family, so it cannot be "
2009
- f"dispatched cross-family; its verdicts are PROPOSED (family collapse). "
2010
- f"{setup_contract}"
2011
- )
2012
- parts.append(
2013
- "Cross-family reviewers are dispatched as read-only subprocesses, not "
2014
- "CLI-native subagents; spawning them needs this main's execution policy to "
2015
- "permit subprocesses, so a read-only or restrictive policy blocks the dispatch "
2016
- "and collapses to same-family native. If a route is unavailable or "
2017
- "unauthenticated at use time, fall back to native same-model subagent review "
2018
- "via the configured child agents and label those verdicts PROPOSED (family collapse)."
2019
- )
2020
- return " ".join(parts)
2021
-
2022
-
2023
- def run_contract(plan: dict[str, Any]) -> str:
2024
- bindings = ", ".join(
2025
- f"{tier}={data['model']}/{tier_effort(plan, tier)}"
2026
- for tier, data in plan["tiers"].items()
2027
- )
2028
- requested = plan["review_setup"]
2029
- family = plan.get("review_family", "cross")
2030
- effective, dropped, floor = effective_review(plan)
2031
- if family == "same":
2032
- route = _same_review_route(plan, effective, dropped)
2033
- else:
2034
- route = _cross_review_route(plan, effective, dropped, floor)
2035
- authority = (
2036
- "Main and native child model/effort defaults are config-projected. Use the installed "
2037
- "codex-run adapter when a separate child root or stricter reach boundary matters."
2038
- if plan["host"] == "codex"
2039
- else "Main and child model/effort defaults are CLI-projected."
2040
- )
2041
- mission = plan.get("mission")
2042
- if mission and plan.get("trigger"):
2043
- mission = mission.replace("{trigger}", plan["trigger"])
2044
- mission_prefix = f"Mission: {mission} " if mission else ""
2045
- return (
2046
- f"{mission_prefix}"
2047
- f"LaunchPlan: main={plan['main_tier']} ({plan['tiers'][plan['main_tier']]['model']}/"
2048
- f"{tier_effort(plan, plan['main_tier'])}); tiers: {bindings}. "
2049
- f"Delegation={'on' if plan['delegation'] else 'off'}. Review family={family}. "
2050
- f"Review setup={requested}: {route} "
2051
- f"{authority} Forwarded backend arguments are expert overrides and may supersede these "
2052
- "defaults. Status is configured/requested; completion is not enforced by "
2053
- "this interactive launcher."
2054
- )
2055
-
2056
-
2057
- def claude_agents(plan: dict[str, Any]) -> str:
2058
- roles = {}
2059
- for tier in SPAWNABLE_TIERS:
2060
- binding = plan["tiers"][tier]
2061
- roles[tier] = {
2062
- "description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
2063
- "prompt": (
2064
- f"Act as the bounded {tier.upper()} role at requested effort "
2065
- f"{tier_effort(plan, tier)}. Return evidence and verification; stay in scope."
2066
- ),
2067
- "model": binding["model"],
2068
- "effort": tier_effort(plan, tier),
2069
- }
2070
- return json.dumps(roles, separators=(",", ":"))
2071
-
2072
-
2073
- def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[str]:
2074
- host = plan["host"]
2075
- if plan.get("mode") == SWE_MODE:
2076
- # Software Engineer's Vanilla preset is the plain backend with nothing
2077
- # applied: no launch contract, no tier pinning, no agents, no
2078
- # permission/sandbox flag — it defers entirely to the repo's own
2079
- # AGENTS.md/CLAUDE.md. Short-circuit so that invariant holds
2080
- # structurally. Custom entered from this mode re-bases to the builder
2081
- # default, so its plan mode is "builder" and its settings DO apply.
2082
- return []
2083
- main = plan["tiers"][plan["main_tier"]]
2084
- main_effort = tier_effort(plan, plan["main_tier"])
2085
- contract = run_contract(plan)
2086
- effective_routes, _, _ = effective_review(plan)
2087
- if host == "codex":
2088
- args = [
2089
- "--model", main["model"],
2090
- "-c", f'model_reasoning_effort="{main_effort}"',
2091
- "-c", f"developer_instructions={json.dumps(contract)}",
2092
- "-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
2093
- ]
2094
- policy = plan["codex_execution_policy"]
2095
- if policy == "bypass":
2096
- policy_args = ["--dangerously-bypass-approvals-and-sandbox"]
2097
- elif policy == STANDARD_POLICY:
2098
- policy_args = []
2099
- else:
2100
- policy_args = ["--sandbox", policy]
2101
- args += policy_args
2102
- if plan["delegation"]:
2103
- for tier, (path, description) in codex_agent_configs(plan, materialize_agents).items():
2104
- args += [
2105
- "-c", f"agents.{tier}.description={json.dumps(description)}",
2106
- "-c", f"agents.{tier}.config_file={json.dumps(str(path))}",
2107
- ]
2108
- if "onto" in effective_routes:
2109
- onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
2110
- args += [
2111
- "-c", "mcp_servers.onto.enabled=true",
2112
- "-c", f"mcp_servers.onto.command={json.dumps(onto_command)}",
2113
- "-c", 'mcp_servers.onto.args=["mcp"]',
2114
- ]
2115
- return args
2116
- args = [
2117
- "--model", main["model"],
2118
- "--effort", main_effort,
2119
- "--append-system-prompt", contract,
2120
- ]
2121
- if plan["delegation"]:
2122
- args += ["--agents", claude_agents(plan)]
2123
- policy = plan["claude_permission_mode"]
2124
- if policy == "bypassPermissions":
2125
- policy_args = ["--dangerously-skip-permissions"]
2126
- elif policy == STANDARD_POLICY:
2127
- policy_args = []
2128
- else:
2129
- policy_args = ["--permission-mode", policy]
2130
- args += policy_args
2131
- if "onto" in effective_routes:
2132
- onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
2133
- mcp_config = {
2134
- "mcpServers": {"onto": {"command": onto_command, "args": ["mcp"]}}
2135
- }
2136
- args += [
2137
- "--mcp-config",
2138
- json.dumps(mcp_config, separators=(",", ":")),
2139
- ]
2140
- return args
2141
-
2142
-
2143
- def print_summary(
2144
- plan: dict[str, Any],
2145
- command: str,
2146
- args: list[str],
2147
- stream: Any = sys.stdout,
2148
- forwarded_args: bool = False,
2149
- ) -> None:
2150
- main = plan["tiers"][plan["main_tier"]]
2151
- print("\nLaunch summary", file=stream)
2152
- print(f" Host {plan['host']}", file=stream)
2153
- print(f" Preset {plan['label']}", file=stream)
2154
- print(
2155
- f" Main {plan['main_tier'].upper()} · {main['model']} · "
2156
- f"{tier_effort(plan, plan['main_tier'])}",
2157
- file=stream,
2158
- )
2159
- for tier in TIER_ORDER:
2160
- binding = plan["tiers"][tier]
2161
- effort = tier_effort(plan, tier)
2162
- print(f" {tier.upper():<14} {binding['model']} · {effort}", file=stream)
2163
- effective, dropped, floor = effective_review(plan)
2164
- family = plan.get("review_family", "cross")
2165
- review_display = plan["review_setup"]
2166
- if family == "cross":
2167
- review_display += f" · cross-family review on {plan['review_host']}"
2168
- if effective:
2169
- review_display += f" via {'+'.join(effective)}"
2170
- if dropped:
2171
- review_display += f" (dropped {','.join(dropped)}{install_hint(plan, dropped)})"
2172
- if floor:
2173
- review_display += f" → same-family {floor} PROPOSED"
2174
- elif dropped:
2175
- review_display += (
2176
- f" → effective {'+'.join(effective) or 'none'} "
2177
- f"({','.join(dropped)} unavailable{install_hint(plan, dropped)})"
2178
- )
2179
- print(
2180
- f" Review setup {review_display} · "
2181
- "configured/requested · completed: not enforced",
2182
- file=stream,
2183
- )
2184
- tier_authority = (
2185
- "base main + native child model/effort configured"
2186
- if plan["host"] == "codex"
2187
- else "base main + child model/effort configured"
2188
- )
2189
- print(f" Tier authority {tier_authority}", file=stream)
2190
- if forwarded_args:
2191
- print(
2192
- " Overrides forwarded backend args appended last; may supersede defaults",
2193
- file=stream,
2194
- )
2195
- if plan["host"] == "codex":
2196
- print(f" Execution Codex {plan['codex_execution_policy']}", file=stream)
2197
- else:
2198
- print(
2199
- f" Execution Claude {plan['claude_permission_mode']} "
2200
- "(permission mode, not OS sandbox)",
2201
- file=stream,
2202
- )
2203
- print(f" Backend {command}", file=stream)
2204
- if os.environ.get("AGENT_LAUNCH_DEBUG") == "1":
2205
- print(" Argv " + json.dumps([command, *args]), file=stream)
2206
-
2207
-
2208
- SESSION_DISTILL_STATE = pathlib.Path(
2209
- os.environ.get(
2210
- "AGENT_BIOS_SESSION_DISTILL_STATE",
2211
- str(pathlib.Path.home() / ".local/share/agent-bios/session-distill-state.json"),
2212
- )
2213
- )
2214
-
2215
-
2216
- def _line_count(path: pathlib.Path) -> int:
2217
- try:
2218
- with path.open("rb") as fh:
2219
- return sum(chunk.count(b"\n") for chunk in iter(lambda: fh.read(1 << 20), b""))
2220
- except OSError:
2221
- return 0
2222
-
2223
-
2224
- def session_distill_nudge(config: dict[str, Any]) -> str | None:
2225
- """Nudge when enough sessions accumulated since the last mining window.
2226
-
2227
- The baseline is written by scripts/session-distill/update-state.py at
2228
- window close; provider history line counts are a cheap proxy for new
2229
- sessions. No state file means no nudge.
2230
- """
2231
- try:
2232
- state = json.loads(SESSION_DISTILL_STATE.read_text())
2233
- baseline = int(state["history_lines_total"])
2234
- except (OSError, ValueError, KeyError, TypeError):
2235
- return None
2236
- settings = config.get("session_distill", {})
2237
- threshold = settings.get("nudge_after", 250) if isinstance(settings, dict) else 250
2238
- current = _line_count(pathlib.Path.home() / ".claude/history.jsonl") + _line_count(
2239
- pathlib.Path.home() / ".codex/history.jsonl"
2240
- )
2241
- delta = current - baseline
2242
- if delta < threshold:
2243
- return None
2244
- return (
2245
- f"session-distill due: ~{delta} new session entries since "
2246
- f"{state.get('window_end', '?')} (threshold {threshold}) — launch the "
2247
- "Session distill preset to run the next mining window"
2248
- )
2249
-
2250
-
2251
- def parse_args(argv: list[str]) -> argparse.Namespace:
2252
- parser = argparse.ArgumentParser(description=__doc__)
2253
- parser.add_argument("--config", type=pathlib.Path, default=default_config_path())
2254
- parser.add_argument(
2255
- "--no-tui",
2256
- action="store_true",
2257
- help="disable the rich terminal preflight; configured-launch flags still apply",
2258
- )
2259
- parser.add_argument("--preset", help="launch a named preset without the preset picker")
2260
- parser.add_argument(
2261
- "--custom",
2262
- action="store_true",
2263
- help="open customization after preset selection",
2264
- )
2265
- parser.add_argument("--yes", action="store_true", help="skip launch confirmation")
2266
- parser.add_argument("--dry-run", action="store_true", help="print projection without launching")
2267
- parser.add_argument("host", choices=("codex", "claude"))
2268
- parser.add_argument("forward", nargs=argparse.REMAINDER)
2269
- args = parser.parse_args(argv)
2270
- if args.forward[:1] == ["--"]:
2271
- args.forward = args.forward[1:]
2272
- return args
2273
-
2274
-
2275
- def main(argv: list[str]) -> int:
2276
- args = parse_args(argv)
2277
- config_path = args.config.expanduser()
2278
- config = load_config(config_path)
2279
- command, passthrough = resolve_backend(config, args.host)
2280
- nudge = session_distill_nudge(config)
2281
- if nudge:
2282
- print(f"agent-launch: {nudge}", file=sys.stderr)
2283
- nudged = config["presets"].get("session-distill")
2284
- if isinstance(nudged, dict):
2285
- nudged["description"] = f"{nudged.get('description', '')} ⚠ {nudge}".strip()
2286
- tty = sys.stdin.isatty() and sys.stdout.isatty()
2287
- if not tty and args.dry_run and not args.preset and not args.custom:
2288
- if "balanced" not in config["presets"]:
2289
- raise LaunchError(
2290
- "bare non-TTY --dry-run requires a 'balanced' preset; use --preset NAME"
2291
- )
2292
- args.preset = "balanced"
2293
- bypass = args.no_tui or bool(args.forward) or os.environ.get("AGENT_LAUNCH_TUI") == "0"
2294
- if (bypass or not tty) and not args.preset and not args.custom and not args.dry_run:
2295
- exec_backend(command, [*passthrough, *args.forward])
2296
-
2297
- interactive_setup = not args.preset or args.custom
2298
- use_textual = (
2299
- interactive_setup
2300
- and tty
2301
- and os.environ.get("TERM", "") not in {"", "dumb"}
2302
- )
2303
- if use_textual and not textual_importable():
2304
- maybe_reexec_into_venv()
2305
- if not textual_importable():
2306
- use_textual = False
2307
- print(
2308
- "agent-launch: rich terminal UI unavailable; using numbered prompts.",
2309
- file=sys.stderr,
2310
- )
2311
- if use_textual:
2312
- plan = run_textual_flow(
2313
- config, args.host, args.preset, args.custom, config_path
2314
- )
2315
- else:
2316
- plan = select_plan(
2317
- config, args.host, args.preset, args.custom, config_path=config_path
2318
- )
2319
- validate_review_setup(plan)
2320
- projected = [*project_args(plan, materialize_agents=not args.dry_run), *args.forward]
2321
- summary_stream = sys.stdout if tty or args.dry_run else sys.stderr
2322
- print_summary(plan, command, projected, summary_stream, bool(args.forward))
2323
- trigger = plan.get("trigger")
2324
- if trigger:
2325
- # Black-on-yellow to match the Session Distill identity; the session
2326
- # waits for this exact phrase before starting the workflow.
2327
- line = f' Once the session is up, type "{trigger}" to begin the session-distill workflow. '
2328
- print(f"\n\x1b[1;30;43m{line}\x1b[0m", file=summary_stream)
2329
- if args.dry_run:
2330
- print(json.dumps([command, *projected], ensure_ascii=False))
2331
- return 0
2332
- if tty and not args.yes and not plan.get("_launch_confirmed", False):
2333
- confirmation = read_input("Launch? [Y/n/q]: ").strip().lower()
2334
- if confirmation in {"n", "no", "q", "quit"}:
2335
- return 130
2336
- env = os.environ.copy()
2337
- env["AGENT_LAUNCH_ACTIVE"] = "1"
2338
- summary_stream.flush()
2339
- exec_backend(command, projected, env)
2340
-
2341
-
2342
- if __name__ == "__main__":
2343
- try:
2344
- raise SystemExit(main(sys.argv[1:]))
2345
- except LaunchError as exc:
2346
- print(f"agent-launch: {exc}", file=sys.stderr)
2347
- raise SystemExit(2)
2348
- except KeyboardInterrupt:
2349
- print("\nCancelled.", file=sys.stderr)
2350
- raise SystemExit(130)