agent-bios 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCIES.md +89 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/claude/CLAUDE.md +138 -0
- package/claude/guides/cli-multi-model-workflow.md +194 -0
- package/claude/guides/coding-staged-workflow.md +70 -0
- package/claude/guides/implementation-map.md +34 -0
- package/claude/guides/llm-capability-boundary-examples.md +123 -0
- package/claude/guides/llm-capability-boundary-patterns.md +339 -0
- package/claude/guides/llm-capability-boundary.md +255 -0
- package/claude/guides/mock-realization-boundary.md +275 -0
- package/claude/guides/svg-visualization-guide.md +321 -0
- package/codex/AGENTS.md +139 -0
- package/codex/agents/frontier.toml +8 -0
- package/codex/agents/reviewer.toml +9 -0
- package/codex/agents/sweep.toml +9 -0
- package/codex/agents/workhorse.toml +8 -0
- package/codex/guides/cli-multi-model-workflow.md +194 -0
- package/codex/guides/coding-staged-workflow.md +70 -0
- package/codex/guides/implementation-map.md +34 -0
- package/codex/guides/llm-capability-boundary-examples.md +123 -0
- package/codex/guides/llm-capability-boundary-patterns.md +339 -0
- package/codex/guides/llm-capability-boundary.md +255 -0
- package/codex/guides/mock-realization-boundary.md +275 -0
- package/codex/guides/svg-visualization-guide.md +321 -0
- package/config/agent-launch.toml +94 -0
- package/package.json +54 -0
- package/scripts/agent-launch.py +1742 -0
- package/scripts/check-parity.sh +1703 -0
- package/scripts/codex-helm.sh +370 -0
- package/scripts/codex-run.sh +176 -0
- package/scripts/install.sh +310 -0
- package/scripts/provision-venv.sh +28 -0
- package/scripts/session-cost.py +106 -0
- package/shell/agent-launch.zsh +38 -0
|
@@ -0,0 +1,1742 @@
|
|
|
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 sys
|
|
14
|
+
import textwrap
|
|
15
|
+
import tomllib
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, NoReturn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
TIER_ORDER = ("frontier", "helm", "workhorse", "sweep")
|
|
21
|
+
EFFORT_ORDER = ("low", "medium", "high", "xhigh", "max", "ultra")
|
|
22
|
+
HOST_EFFORTS = {
|
|
23
|
+
"codex": {"low", "medium", "high", "xhigh", "max", "ultra"},
|
|
24
|
+
"claude": {"low", "medium", "high", "xhigh", "max"},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TIER_DESCRIPTIONS = {
|
|
28
|
+
"frontier": "Highest-capability specialist for hard, high-impact work and deep review.",
|
|
29
|
+
"helm": "Primary orchestrator for planning, delegation, integration, and final decisions.",
|
|
30
|
+
"workhorse": "Cost-conscious executor for high-volume implementation and analysis.",
|
|
31
|
+
"sweep": "Fast, low-cost worker for narrow searches, checks, and mechanical tasks.",
|
|
32
|
+
}
|
|
33
|
+
EFFORT_DESCRIPTIONS = {
|
|
34
|
+
"low": "Minimize reasoning cost and latency for straightforward work.",
|
|
35
|
+
"medium": "Use moderate reasoning for routine work with some ambiguity.",
|
|
36
|
+
"high": "Spend more reasoning on complex implementation and analysis.",
|
|
37
|
+
"xhigh": "Use deeper reasoning for difficult coordination and high-impact decisions.",
|
|
38
|
+
"max": "Use the host's strongest standard reasoning mode for frontier work.",
|
|
39
|
+
"ultra": "Use Codex Ultra for exceptionally deep, divisible frontier work.",
|
|
40
|
+
}
|
|
41
|
+
CUSTOM_PRESET = "__custom__"
|
|
42
|
+
OTHER_MODEL = "__other_model__"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class MenuOption:
|
|
47
|
+
value: str
|
|
48
|
+
label: str
|
|
49
|
+
description: str
|
|
50
|
+
enabled: bool = True
|
|
51
|
+
unavailable_reason: str = ""
|
|
52
|
+
CODEX_POLICIES = {"bypass", "workspace-write", "read-only"}
|
|
53
|
+
CLAUDE_POLICIES = {"acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"}
|
|
54
|
+
REVIEW_SETUPS = {
|
|
55
|
+
"none": {
|
|
56
|
+
"label": "None",
|
|
57
|
+
"requirements": {"codex": (), "claude": ()},
|
|
58
|
+
"contract": "No additional review route requested.",
|
|
59
|
+
},
|
|
60
|
+
"native-panel": {
|
|
61
|
+
"label": "Native panel",
|
|
62
|
+
"requirements": {"codex": (), "claude": ()},
|
|
63
|
+
"contract": "Use native multi-perspective subagents when review gates fire.",
|
|
64
|
+
},
|
|
65
|
+
"onto": {
|
|
66
|
+
"label": "onto-mcp",
|
|
67
|
+
"requirements": {"codex": ("onto",), "claude": ("onto",)},
|
|
68
|
+
"contract": "Use onto-mcp as the structured review lens when review gates fire.",
|
|
69
|
+
},
|
|
70
|
+
"ultracode": {
|
|
71
|
+
"label": "Ultracode",
|
|
72
|
+
"requirements": {"codex": ("ultracode",), "claude": ("ultracode",)},
|
|
73
|
+
"contract": (
|
|
74
|
+
"Run the configured Ultracode CLI as a Codex-backed review route when "
|
|
75
|
+
"review gates fire."
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
"hybrid": {
|
|
79
|
+
"label": "Hybrid",
|
|
80
|
+
"requirements": {
|
|
81
|
+
"codex": ("onto", "ultracode"),
|
|
82
|
+
"claude": ("onto", "ultracode"),
|
|
83
|
+
},
|
|
84
|
+
"contract": {
|
|
85
|
+
"codex": "Use onto plus native and Codex-backed Ultracode review kinds; act on their union.",
|
|
86
|
+
"claude": "Use onto plus native multi-perspective and Codex-backed Ultracode review kinds; act on their union.",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Each review setup decomposes into review routes. "native" is same-model
|
|
92
|
+
# multi-perspective subagent review (needs delegation); "onto"/"ultracode" are
|
|
93
|
+
# external routes gated on a resolvable capability command. A missing external
|
|
94
|
+
# route degrades to native instead of failing closed; only the delegation
|
|
95
|
+
# contradiction (native required with delegation off) stays fail-closed.
|
|
96
|
+
REVIEW_ROUTES = {
|
|
97
|
+
"none": (),
|
|
98
|
+
"native-panel": ("native",),
|
|
99
|
+
"onto": ("onto",),
|
|
100
|
+
"ultracode": ("ultracode",),
|
|
101
|
+
"hybrid": ("onto", "native", "ultracode"),
|
|
102
|
+
}
|
|
103
|
+
ROUTE_LABELS = {
|
|
104
|
+
"native": "native same-model multi-perspective review",
|
|
105
|
+
"onto": "onto structured-lens review",
|
|
106
|
+
"ultracode": "Codex-backed Ultracode review",
|
|
107
|
+
}
|
|
108
|
+
# review_family selects whether review runs on the opposite model family (cross,
|
|
109
|
+
# the default) or the same family as the main (same, today's projection).
|
|
110
|
+
REVIEW_FAMILIES = {"cross", "same"}
|
|
111
|
+
# onto llmOverride provider enum (onto MCP tool schema, settings-chain).
|
|
112
|
+
ONTO_PROVIDERS = {"openai", "anthropic", "grok", "lmstudio"}
|
|
113
|
+
# The opposite model family for cross review, keyed on the launch host.
|
|
114
|
+
REVIEW_HOST = {"codex": "claude", "claude": "codex"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class LaunchError(RuntimeError):
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BackRequested(RuntimeError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_command(value: str) -> str:
|
|
126
|
+
expanded = os.path.expanduser(value)
|
|
127
|
+
if os.path.sep in expanded:
|
|
128
|
+
command = expanded if pathlib.Path(expanded).is_file() and os.access(expanded, os.X_OK) else None
|
|
129
|
+
else:
|
|
130
|
+
command = shutil.which(expanded)
|
|
131
|
+
if not command:
|
|
132
|
+
raise LaunchError(f"executable not found: {value}")
|
|
133
|
+
return command
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def expand_config_path(value: str) -> pathlib.Path:
|
|
137
|
+
codex_home = os.environ.get("CODEX_HOME", str(pathlib.Path.home() / ".codex"))
|
|
138
|
+
expanded = value.replace("${CODEX_HOME}", codex_home)
|
|
139
|
+
return pathlib.Path(os.path.expandvars(os.path.expanduser(expanded)))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def exec_backend(command: str, args: list[str], env: dict[str, str] | None = None) -> NoReturn:
|
|
143
|
+
os.execve(command, [command, *args], os.environ.copy() if env is None else env)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def default_config_path() -> pathlib.Path:
|
|
147
|
+
explicit = os.environ.get("AGENT_LAUNCH_CONFIG")
|
|
148
|
+
if explicit:
|
|
149
|
+
return pathlib.Path(explicit).expanduser()
|
|
150
|
+
installed = pathlib.Path.home() / ".config/agent-launch/profiles.toml"
|
|
151
|
+
if installed.is_file():
|
|
152
|
+
return installed
|
|
153
|
+
return pathlib.Path(__file__).resolve().parent.parent / "config/agent-launch.toml"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_config(path: pathlib.Path) -> dict[str, Any]:
|
|
157
|
+
try:
|
|
158
|
+
data = tomllib.loads(path.read_text())
|
|
159
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
160
|
+
raise LaunchError(f"cannot load config {path}: {exc}") from exc
|
|
161
|
+
if type(data.get("schema_version")) is not int or data["schema_version"] != 1:
|
|
162
|
+
raise LaunchError(f"unsupported schema_version in {path}")
|
|
163
|
+
for key in ("backends", "hosts", "presets"):
|
|
164
|
+
if not isinstance(data.get(key), dict) or not data[key]:
|
|
165
|
+
raise LaunchError(f"config requires non-empty [{key}]")
|
|
166
|
+
capabilities = data.get("capabilities", {})
|
|
167
|
+
if not isinstance(capabilities, dict):
|
|
168
|
+
raise LaunchError("[capabilities] must be a table")
|
|
169
|
+
for name, capability in capabilities.items():
|
|
170
|
+
if not isinstance(capability, dict):
|
|
171
|
+
raise LaunchError(f"capability must be a table: {name}")
|
|
172
|
+
onto = capabilities.get("onto")
|
|
173
|
+
if onto is not None and (
|
|
174
|
+
not isinstance(onto.get("command"), str) or not onto["command"]
|
|
175
|
+
):
|
|
176
|
+
raise LaunchError("capabilities.onto.command must be a non-empty string")
|
|
177
|
+
ultracode = capabilities.get("ultracode")
|
|
178
|
+
if ultracode is not None and (
|
|
179
|
+
not isinstance(ultracode.get("command"), str) or not ultracode["command"]
|
|
180
|
+
):
|
|
181
|
+
raise LaunchError("capabilities.ultracode.command must be a non-empty string")
|
|
182
|
+
for host_name, host_data in data["hosts"].items():
|
|
183
|
+
if not isinstance(host_data, dict):
|
|
184
|
+
raise LaunchError(f"host must be a table: {host_name}")
|
|
185
|
+
models = host_data.get("models")
|
|
186
|
+
if models is not None and (
|
|
187
|
+
not isinstance(models, list)
|
|
188
|
+
or not models
|
|
189
|
+
or not all(isinstance(model, str) and model for model in models)
|
|
190
|
+
):
|
|
191
|
+
raise LaunchError(
|
|
192
|
+
f"hosts.{host_name}.models must be a non-empty list of strings"
|
|
193
|
+
)
|
|
194
|
+
onto_review = host_data.get("onto_review")
|
|
195
|
+
if onto_review is not None:
|
|
196
|
+
if not isinstance(onto_review, dict):
|
|
197
|
+
raise LaunchError(f"hosts.{host_name}.onto_review must be a table")
|
|
198
|
+
if onto_review.get("provider") not in ONTO_PROVIDERS:
|
|
199
|
+
choices = ", ".join(sorted(ONTO_PROVIDERS))
|
|
200
|
+
raise LaunchError(
|
|
201
|
+
f"hosts.{host_name}.onto_review.provider must be one of: {choices}"
|
|
202
|
+
)
|
|
203
|
+
if not isinstance(onto_review.get("model"), str) or not onto_review["model"]:
|
|
204
|
+
raise LaunchError(
|
|
205
|
+
f"hosts.{host_name}.onto_review.model must be a non-empty string"
|
|
206
|
+
)
|
|
207
|
+
for name, preset in data["presets"].items():
|
|
208
|
+
if not isinstance(preset, dict):
|
|
209
|
+
raise LaunchError(f"preset must be a table: {name}")
|
|
210
|
+
label = preset.get("label")
|
|
211
|
+
if not isinstance(label, str) or not label:
|
|
212
|
+
raise LaunchError(f"presets.{name}.label must be a non-empty string")
|
|
213
|
+
description = preset.get("description")
|
|
214
|
+
if description is not None and (
|
|
215
|
+
not isinstance(description, str) or not description
|
|
216
|
+
):
|
|
217
|
+
raise LaunchError(f"presets.{name}.description must be a non-empty string")
|
|
218
|
+
for field, allowed in (
|
|
219
|
+
("codex_execution_policy", CODEX_POLICIES),
|
|
220
|
+
("claude_permission_mode", CLAUDE_POLICIES),
|
|
221
|
+
):
|
|
222
|
+
value = preset.get(field)
|
|
223
|
+
if not isinstance(value, str) or value not in allowed:
|
|
224
|
+
choices = ", ".join(sorted(allowed))
|
|
225
|
+
raise LaunchError(f"presets.{name}.{field} must be one of: {choices}")
|
|
226
|
+
review_family = preset.get("review_family")
|
|
227
|
+
if review_family is not None and review_family not in REVIEW_FAMILIES:
|
|
228
|
+
raise LaunchError(f"presets.{name}.review_family must be one of: cross, same")
|
|
229
|
+
return data
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def resolve_backend(config: dict[str, Any], host: str) -> tuple[str, list[str]]:
|
|
233
|
+
try:
|
|
234
|
+
backend = config["backends"][host]
|
|
235
|
+
command_value = backend["command"]
|
|
236
|
+
passthrough_value = backend.get("passthrough_args", [])
|
|
237
|
+
except (KeyError, TypeError) as exc:
|
|
238
|
+
raise LaunchError(f"backend is not configured for {host}") from exc
|
|
239
|
+
if not isinstance(command_value, str) or not command_value:
|
|
240
|
+
raise LaunchError(f"backend command must be a non-empty string: {host}")
|
|
241
|
+
command = resolve_command(command_value)
|
|
242
|
+
if not isinstance(passthrough_value, list) or not all(
|
|
243
|
+
isinstance(arg, str) for arg in passthrough_value
|
|
244
|
+
):
|
|
245
|
+
raise LaunchError(f"backend passthrough_args must be strings: {host}")
|
|
246
|
+
return command, passthrough_value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_effort(host: str, model: str, effort: Any, context: str) -> str:
|
|
250
|
+
if not isinstance(effort, str) or effort not in HOST_EFFORTS[host]:
|
|
251
|
+
raise LaunchError(f"unsupported effort for {context}: {effort!r}")
|
|
252
|
+
if host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
|
|
253
|
+
raise LaunchError(f"unsupported effort for {context}: gpt-5.6-luna/ultra")
|
|
254
|
+
return effort
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _resolves(value: str) -> bool:
|
|
258
|
+
try:
|
|
259
|
+
resolve_command(value)
|
|
260
|
+
return True
|
|
261
|
+
except LaunchError:
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def cross_native_command(plan: dict[str, Any]) -> str | None:
|
|
266
|
+
"""Absolute command a cross-family main dispatches to for native review, or
|
|
267
|
+
None if unresolvable. The codex reviewer wrapper lives off PATH under
|
|
268
|
+
CODEX_HOME/bin; the claude reviewer is the claude backend on PATH."""
|
|
269
|
+
if plan["review_host"] == "codex":
|
|
270
|
+
path = expand_config_path("${CODEX_HOME}/bin/codex-run")
|
|
271
|
+
return str(path) if path.is_file() and os.access(path, os.X_OK) else None
|
|
272
|
+
try:
|
|
273
|
+
return resolve_command(plan["review_backend"])
|
|
274
|
+
except LaunchError:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def cross_helm_command(plan: dict[str, Any]) -> str | None:
|
|
279
|
+
"""The codex fan-out reviewer wrapper (hybrid), off PATH under CODEX_HOME/bin."""
|
|
280
|
+
if plan["review_host"] != "codex":
|
|
281
|
+
return None
|
|
282
|
+
path = expand_config_path("${CODEX_HOME}/bin/codex-helm")
|
|
283
|
+
return str(path) if path.is_file() and os.access(path, os.X_OK) else None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cross_ultracode_command(plan: dict[str, Any]) -> str | None:
|
|
287
|
+
"""The cross-family ultracode reviewer command, or None. For a claude main it
|
|
288
|
+
is the Codex-backed ultracode-for-codex (the $ultracode-for-codex skill); for
|
|
289
|
+
a codex main it is the claude backend (Claude Code /workflows ultracode)."""
|
|
290
|
+
if plan["review_host"] == "codex":
|
|
291
|
+
value = plan.get("capabilities", {}).get("ultracode", {}).get("command", "")
|
|
292
|
+
else:
|
|
293
|
+
value = plan["review_backend"]
|
|
294
|
+
try:
|
|
295
|
+
return resolve_command(value)
|
|
296
|
+
except LaunchError:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def route_availability(plan: dict[str, Any]) -> dict[str, bool]:
|
|
301
|
+
"""Which review routes can run right now. In same-family mode native gates on
|
|
302
|
+
delegation and onto/ultracode on a resolvable capability. In cross-family mode
|
|
303
|
+
native/ultracode gate on the opposite-family dispatcher resolving, and onto on
|
|
304
|
+
a mounted onto plus a review_onto pin to flip its family."""
|
|
305
|
+
capabilities = plan.get("capabilities", {})
|
|
306
|
+
if plan.get("review_family", "cross") == "same":
|
|
307
|
+
available = {"native": bool(plan["delegation"])}
|
|
308
|
+
for route, capability in (("onto", "onto"), ("ultracode", "ultracode")):
|
|
309
|
+
available[route] = _resolves(capabilities.get(capability, {}).get("command", ""))
|
|
310
|
+
return available
|
|
311
|
+
onto_ok = _resolves(capabilities.get("onto", {}).get("command", "")) and bool(
|
|
312
|
+
plan.get("review_onto")
|
|
313
|
+
)
|
|
314
|
+
return {
|
|
315
|
+
"native": cross_native_command(plan) is not None,
|
|
316
|
+
"onto": onto_ok,
|
|
317
|
+
"ultracode": cross_ultracode_command(plan) is not None,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def effective_review(plan: dict[str, Any]) -> tuple[list[str], list[str], str | None]:
|
|
322
|
+
"""Resolve the requested review setup to the routes that can actually run.
|
|
323
|
+
|
|
324
|
+
Returns (effective_routes, dropped_routes, floor). In same-family mode this
|
|
325
|
+
reproduces the earlier degrade-to-native behavior (floor None; a dropped
|
|
326
|
+
external route degrades to native in effective; native requires delegation).
|
|
327
|
+
In cross-family mode the effective routes run on the opposite family; when no
|
|
328
|
+
cross route resolves and delegation is on, floor='native' marks a same-family
|
|
329
|
+
PROPOSED fallback, and a requested non-none setup with no cross route and no
|
|
330
|
+
fallback (delegation off) fails closed."""
|
|
331
|
+
requested = plan["review_setup"]
|
|
332
|
+
if requested not in REVIEW_ROUTES:
|
|
333
|
+
raise LaunchError(f"unknown review setup: {requested!r}")
|
|
334
|
+
wanted = list(REVIEW_ROUTES[requested])
|
|
335
|
+
available = route_availability(plan)
|
|
336
|
+
if plan.get("review_family", "cross") == "same":
|
|
337
|
+
if "native" in wanted and not plan["delegation"]:
|
|
338
|
+
raise LaunchError(f"review setup {requested!r} requires delegation")
|
|
339
|
+
effective = [route for route in wanted if available[route]]
|
|
340
|
+
dropped = [route for route in wanted if not available[route]]
|
|
341
|
+
if dropped and "native" not in effective and plan["delegation"]:
|
|
342
|
+
effective.append("native")
|
|
343
|
+
return effective, dropped, None
|
|
344
|
+
effective = [route for route in wanted if available[route]]
|
|
345
|
+
dropped = [route for route in wanted if not available[route]]
|
|
346
|
+
floor = None
|
|
347
|
+
if not effective and requested != "none":
|
|
348
|
+
if plan["delegation"]:
|
|
349
|
+
floor = "native"
|
|
350
|
+
else:
|
|
351
|
+
raise LaunchError(
|
|
352
|
+
f"review setup {requested!r} has no available cross-family route "
|
|
353
|
+
"and no same-family fallback (delegation off)"
|
|
354
|
+
)
|
|
355
|
+
return effective, dropped, floor
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def setup_summary_lines(plan: dict[str, Any] | None) -> list[str]:
|
|
359
|
+
if plan is None:
|
|
360
|
+
return ["No setup selected."]
|
|
361
|
+
host = plan["host"]
|
|
362
|
+
execution = (
|
|
363
|
+
plan["codex_execution_policy"]
|
|
364
|
+
if host == "codex"
|
|
365
|
+
else plan["claude_permission_mode"]
|
|
366
|
+
)
|
|
367
|
+
lines = [
|
|
368
|
+
f"Host {host} | Preset {plan['label']}",
|
|
369
|
+
f"Main {plan['main_tier'].upper()} | Review {plan['review_setup']}",
|
|
370
|
+
f"Delegation {'on' if plan['delegation'] else 'off'} | Execution {execution}",
|
|
371
|
+
]
|
|
372
|
+
for tier in TIER_ORDER:
|
|
373
|
+
binding = plan["tiers"][tier]
|
|
374
|
+
effort = (
|
|
375
|
+
plan["frontier_effort"] if tier == "frontier" else binding["effort"]
|
|
376
|
+
)
|
|
377
|
+
lines.append(f"{tier.upper():<10} {binding['model']} / {effort}")
|
|
378
|
+
return lines
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def textual_importable() -> bool:
|
|
382
|
+
try:
|
|
383
|
+
import textual # noqa: F401
|
|
384
|
+
except Exception:
|
|
385
|
+
return False
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def venv_python() -> pathlib.Path | None:
|
|
390
|
+
"""Locate a managed venv interpreter that provides textual. AGENT_LAUNCH_VENV
|
|
391
|
+
overrides the default install location."""
|
|
392
|
+
roots = []
|
|
393
|
+
override = os.environ.get("AGENT_LAUNCH_VENV")
|
|
394
|
+
if override:
|
|
395
|
+
roots.append(pathlib.Path(override))
|
|
396
|
+
roots.append(pathlib.Path.home() / ".local/share/agent-launch/venv")
|
|
397
|
+
for root in roots:
|
|
398
|
+
interpreter = root.expanduser() / "bin" / "python"
|
|
399
|
+
if interpreter.is_file() and os.access(interpreter, os.X_OK):
|
|
400
|
+
return interpreter
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def maybe_reexec_into_venv() -> None:
|
|
405
|
+
"""When textual is not importable under the current interpreter, re-exec once
|
|
406
|
+
into the managed venv that provides it. Guarded against infinite re-exec and
|
|
407
|
+
only ever reached on the interactive TUI path, so direct/non-TTY launches keep
|
|
408
|
+
running under the system interpreter."""
|
|
409
|
+
if os.environ.get("AGENT_LAUNCH_REEXEC") == "1":
|
|
410
|
+
return
|
|
411
|
+
interpreter = venv_python()
|
|
412
|
+
if interpreter is None:
|
|
413
|
+
return
|
|
414
|
+
env = os.environ.copy()
|
|
415
|
+
env["AGENT_LAUNCH_REEXEC"] = "1"
|
|
416
|
+
os.execve(str(interpreter), [str(interpreter), *sys.argv], env)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# --- Textual preflight UI (optional; the numbered fallback covers its absence) ---
|
|
420
|
+
# select_plan/customize call the same choose()/prompt_text() seam regardless of
|
|
421
|
+
# renderer. TextualUI drives Textual screens from a worker thread via
|
|
422
|
+
# call_from_thread(push_screen_wait), preserving the synchronous controller flow
|
|
423
|
+
# and its BackRequested/KeyboardInterrupt contract.
|
|
424
|
+
|
|
425
|
+
_UI_BACK = "\x00back"
|
|
426
|
+
_UI_CANCEL = "\x00cancel"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _build_app_class():
|
|
430
|
+
"""Import textual lazily and build the App/Screen classes, so importing this
|
|
431
|
+
module and every non-interactive path stays free of the textual dependency."""
|
|
432
|
+
from textual import work
|
|
433
|
+
from textual.app import App
|
|
434
|
+
from textual.binding import Binding
|
|
435
|
+
from textual.screen import ModalScreen
|
|
436
|
+
from textual.widgets import Input, OptionList, Static
|
|
437
|
+
from textual.widgets.option_list import Option
|
|
438
|
+
from textual.theme import Theme
|
|
439
|
+
|
|
440
|
+
# Host-matched palettes so the preflight reads as the CLI it launches.
|
|
441
|
+
# Both are taken from the host's own artifact: Codex from its ~/.codex dark
|
|
442
|
+
# appearance theme (accent #339cff, surface #181818, diff #40c977/#fa423e),
|
|
443
|
+
# Claude from its binary's dark palette (coral #da7756, surface #1a1a19,
|
|
444
|
+
# ink scale #f9f9f7 / #c3c2b7 / #898781, grid #2c2c2a).
|
|
445
|
+
host_themes = {
|
|
446
|
+
"codex": Theme(
|
|
447
|
+
name="codex", primary="#339cff", secondary="#ad7bf9",
|
|
448
|
+
accent="#5fb0ff", foreground="#ffffff", background="#141414",
|
|
449
|
+
surface="#181818", panel="#222222", success="#40c977",
|
|
450
|
+
warning="#e6a700", error="#fa423e", dark=True,
|
|
451
|
+
),
|
|
452
|
+
"claude": Theme(
|
|
453
|
+
name="claude", primary="#da7756", secondary="#c3c2b7",
|
|
454
|
+
accent="#da7756", foreground="#f9f9f7", background="#0d0d0d",
|
|
455
|
+
surface="#1a1a19", panel="#2c2c2a", success="#0ca30c",
|
|
456
|
+
warning="#fab219", error="#e06c6c", dark=True,
|
|
457
|
+
),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
app_css = """
|
|
461
|
+
Screen { background: $surface; }
|
|
462
|
+
#al-title { color: $accent; text-style: bold; padding: 0 1; }
|
|
463
|
+
#al-setup {
|
|
464
|
+
border: round $primary; border-title-color: $primary;
|
|
465
|
+
border-title-style: bold; padding: 0 1; height: auto;
|
|
466
|
+
}
|
|
467
|
+
#al-detail {
|
|
468
|
+
border: round $secondary; border-title-color: $secondary;
|
|
469
|
+
border-title-style: bold; padding: 0 1; height: 5;
|
|
470
|
+
}
|
|
471
|
+
#al-hdr { color: $text-muted; text-style: bold; padding: 0 1; }
|
|
472
|
+
OptionList { height: 1fr; border: none; padding: 0 1; }
|
|
473
|
+
#al-footer { color: $text-muted; dock: bottom; padding: 0 1; background: $panel; }
|
|
474
|
+
Input { margin: 0 1; }
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
def setup_panel(plan):
|
|
478
|
+
panel = Static("\n".join(setup_summary_lines(plan)), id="al-setup")
|
|
479
|
+
panel.border_title = "Current setup"
|
|
480
|
+
return panel
|
|
481
|
+
|
|
482
|
+
class MenuScreen(ModalScreen):
|
|
483
|
+
BINDINGS = [
|
|
484
|
+
Binding("escape", "back", "back", priority=True),
|
|
485
|
+
Binding("q", "cancel", "cancel", priority=True),
|
|
486
|
+
Binding("ctrl+c", "cancel", "cancel", priority=True),
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
def __init__(self, title, options, default, allow_back, plan, preview=None):
|
|
490
|
+
super().__init__()
|
|
491
|
+
self._title = title
|
|
492
|
+
self._options = options
|
|
493
|
+
self._default = default
|
|
494
|
+
self._allow_back = allow_back
|
|
495
|
+
self._plan = plan
|
|
496
|
+
self._preview = preview
|
|
497
|
+
|
|
498
|
+
def compose(self):
|
|
499
|
+
yield Static(self._title, id="al-title")
|
|
500
|
+
yield setup_panel(self._plan)
|
|
501
|
+
detail = Static("", id="al-detail")
|
|
502
|
+
detail.border_title = "About highlighted option"
|
|
503
|
+
yield detail
|
|
504
|
+
yield Static(
|
|
505
|
+
f"Options (1-{len(self._options)} of {len(self._options)})", id="al-hdr"
|
|
506
|
+
)
|
|
507
|
+
option_list = OptionList()
|
|
508
|
+
for option in self._options:
|
|
509
|
+
label = option.label + ("" if option.enabled else " [unavailable]")
|
|
510
|
+
option_list.add_option(
|
|
511
|
+
Option(label, id=option.value, disabled=not option.enabled)
|
|
512
|
+
)
|
|
513
|
+
yield option_list
|
|
514
|
+
footer = (
|
|
515
|
+
"Up/Down move | Enter select | Esc back | q cancel"
|
|
516
|
+
if self._allow_back
|
|
517
|
+
else "Up/Down move | Enter select | Esc cancel | q cancel"
|
|
518
|
+
)
|
|
519
|
+
yield Static(footer, id="al-footer")
|
|
520
|
+
|
|
521
|
+
def on_mount(self):
|
|
522
|
+
option_list = self.query_one(OptionList)
|
|
523
|
+
index = next(
|
|
524
|
+
(
|
|
525
|
+
position
|
|
526
|
+
for position, option in enumerate(self._options)
|
|
527
|
+
if option.value == self._default and option.enabled
|
|
528
|
+
),
|
|
529
|
+
next(
|
|
530
|
+
(
|
|
531
|
+
position
|
|
532
|
+
for position, option in enumerate(self._options)
|
|
533
|
+
if option.enabled
|
|
534
|
+
),
|
|
535
|
+
0,
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
option_list.highlighted = index
|
|
539
|
+
option_list.focus()
|
|
540
|
+
self._describe(index)
|
|
541
|
+
|
|
542
|
+
def on_option_list_option_highlighted(self, event):
|
|
543
|
+
self._describe(event.option_index)
|
|
544
|
+
|
|
545
|
+
def _describe(self, index):
|
|
546
|
+
option = self._options[index]
|
|
547
|
+
detail = option.description
|
|
548
|
+
if not option.enabled and option.unavailable_reason:
|
|
549
|
+
detail = f"{detail} Unavailable: {option.unavailable_reason}"
|
|
550
|
+
self.query_one("#al-detail", Static).update(detail)
|
|
551
|
+
# Live-preview the highlighted option's effect in the setup panel.
|
|
552
|
+
if self._preview is not None:
|
|
553
|
+
try:
|
|
554
|
+
preview_plan = self._preview(option.value)
|
|
555
|
+
self.query_one("#al-setup", Static).update(
|
|
556
|
+
"\n".join(setup_summary_lines(preview_plan))
|
|
557
|
+
)
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
def on_option_list_option_selected(self, event):
|
|
562
|
+
self.dismiss(event.option.id)
|
|
563
|
+
|
|
564
|
+
def action_back(self):
|
|
565
|
+
self.dismiss(_UI_BACK if self._allow_back else _UI_CANCEL)
|
|
566
|
+
|
|
567
|
+
def action_cancel(self):
|
|
568
|
+
self.dismiss(_UI_CANCEL)
|
|
569
|
+
|
|
570
|
+
class InputScreen(ModalScreen):
|
|
571
|
+
BINDINGS = [
|
|
572
|
+
Binding("escape", "cancel", "cancel", priority=True),
|
|
573
|
+
Binding("ctrl+c", "cancel", "cancel", priority=True),
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
def __init__(self, label, default, plan):
|
|
577
|
+
super().__init__()
|
|
578
|
+
self._label = label
|
|
579
|
+
self._default = default
|
|
580
|
+
self._plan = plan
|
|
581
|
+
|
|
582
|
+
def compose(self):
|
|
583
|
+
yield Static(self._label, id="al-title")
|
|
584
|
+
yield setup_panel(self._plan)
|
|
585
|
+
box = Static(
|
|
586
|
+
f"Current value: {self._default}\n"
|
|
587
|
+
"New value (leave blank to keep current):",
|
|
588
|
+
id="al-detail",
|
|
589
|
+
)
|
|
590
|
+
box.border_title = "Edit model"
|
|
591
|
+
yield box
|
|
592
|
+
yield Static("", id="al-hdr")
|
|
593
|
+
yield Input(placeholder="type a model id, or leave blank to keep current")
|
|
594
|
+
yield Static(
|
|
595
|
+
"Enter confirm | Esc/Ctrl-C cancel | q + Enter cancel", id="al-footer"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def on_mount(self):
|
|
599
|
+
self.query_one(Input).focus()
|
|
600
|
+
|
|
601
|
+
def on_input_submitted(self, event):
|
|
602
|
+
value = event.value.strip()
|
|
603
|
+
if value.lower() == "q":
|
|
604
|
+
self.dismiss(_UI_CANCEL)
|
|
605
|
+
else:
|
|
606
|
+
self.dismiss(value or self._default)
|
|
607
|
+
|
|
608
|
+
def action_cancel(self):
|
|
609
|
+
self.dismiss(_UI_CANCEL)
|
|
610
|
+
|
|
611
|
+
class PreflightApp(App):
|
|
612
|
+
ENABLE_COMMAND_PALETTE = False
|
|
613
|
+
CSS = app_css
|
|
614
|
+
BINDINGS = [Binding("ctrl+q", "noop", show=False)]
|
|
615
|
+
|
|
616
|
+
def __init__(self, config, host, preset_name, custom_requested, config_path):
|
|
617
|
+
super().__init__()
|
|
618
|
+
self._flow_args = (config, host, preset_name, custom_requested, config_path)
|
|
619
|
+
self._host = host
|
|
620
|
+
self.outcome = None
|
|
621
|
+
|
|
622
|
+
def action_noop(self):
|
|
623
|
+
pass
|
|
624
|
+
|
|
625
|
+
def on_mount(self):
|
|
626
|
+
theme = host_themes.get(self._host)
|
|
627
|
+
if theme is not None:
|
|
628
|
+
self.register_theme(theme)
|
|
629
|
+
self.theme = theme.name
|
|
630
|
+
self.run_flow()
|
|
631
|
+
|
|
632
|
+
@work(thread=True)
|
|
633
|
+
def run_flow(self):
|
|
634
|
+
config, host, preset_name, custom_requested, config_path = self._flow_args
|
|
635
|
+
ui = TextualUI(self, MenuScreen, InputScreen)
|
|
636
|
+
try:
|
|
637
|
+
self.outcome = (
|
|
638
|
+
"plan",
|
|
639
|
+
select_plan(
|
|
640
|
+
config, host, preset_name, custom_requested, ui, config_path
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
except BaseException as exc: # surfaced to main; re-raised for the exit code
|
|
644
|
+
self.outcome = ("error", exc)
|
|
645
|
+
finally:
|
|
646
|
+
self.call_from_thread(self.exit)
|
|
647
|
+
|
|
648
|
+
return PreflightApp
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class TextualUI:
|
|
652
|
+
def __init__(self, app, menu_screen, input_screen):
|
|
653
|
+
self.app = app
|
|
654
|
+
self._menu_screen = menu_screen
|
|
655
|
+
self._input_screen = input_screen
|
|
656
|
+
self.plan: dict[str, Any] | None = None
|
|
657
|
+
|
|
658
|
+
def set_plan(self, plan: dict[str, Any]) -> None:
|
|
659
|
+
self.plan = plan
|
|
660
|
+
|
|
661
|
+
def choose(
|
|
662
|
+
self,
|
|
663
|
+
title: str,
|
|
664
|
+
options: list[MenuOption],
|
|
665
|
+
default: str,
|
|
666
|
+
allow_back: bool,
|
|
667
|
+
preview=None,
|
|
668
|
+
) -> str:
|
|
669
|
+
result = self.app.call_from_thread(
|
|
670
|
+
self.app.push_screen_wait,
|
|
671
|
+
self._menu_screen(title, options, default, allow_back, self.plan, preview),
|
|
672
|
+
)
|
|
673
|
+
if result == _UI_BACK:
|
|
674
|
+
raise BackRequested
|
|
675
|
+
if result == _UI_CANCEL:
|
|
676
|
+
raise KeyboardInterrupt
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
def prompt_text(self, label: str, default: str) -> str:
|
|
680
|
+
result = self.app.call_from_thread(
|
|
681
|
+
self.app.push_screen_wait,
|
|
682
|
+
self._input_screen(label, default, self.plan),
|
|
683
|
+
)
|
|
684
|
+
if result == _UI_CANCEL:
|
|
685
|
+
raise KeyboardInterrupt
|
|
686
|
+
return result
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def run_textual_flow(
|
|
690
|
+
config: dict[str, Any],
|
|
691
|
+
host: str,
|
|
692
|
+
preset_name: str | None,
|
|
693
|
+
custom_requested: bool,
|
|
694
|
+
config_path: pathlib.Path,
|
|
695
|
+
) -> dict[str, Any]:
|
|
696
|
+
app = _build_app_class()(config, host, preset_name, custom_requested, config_path)
|
|
697
|
+
app.run()
|
|
698
|
+
if app.outcome is None:
|
|
699
|
+
raise KeyboardInterrupt
|
|
700
|
+
kind, value = app.outcome
|
|
701
|
+
if kind == "error":
|
|
702
|
+
raise value
|
|
703
|
+
return value
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def choose_lines(
|
|
707
|
+
title: str, options: list[MenuOption], default: str, allow_back: bool
|
|
708
|
+
) -> str:
|
|
709
|
+
print(f"\n{title}")
|
|
710
|
+
for index, option in enumerate(options, 1):
|
|
711
|
+
marker = "" if option.enabled else " [unavailable]"
|
|
712
|
+
selected = " *" if option.value == default and option.enabled else ""
|
|
713
|
+
print(f" {index}. {option.label}{marker}{selected} - {option.description}")
|
|
714
|
+
if not option.enabled:
|
|
715
|
+
print(f" Unavailable: {option.unavailable_reason}")
|
|
716
|
+
while True:
|
|
717
|
+
actions = "b back | q cancel" if allow_back else "q cancel"
|
|
718
|
+
raw = read_input(f"Select [{default}] ({actions}): ").strip()
|
|
719
|
+
if not raw:
|
|
720
|
+
default_option = next((option for option in options if option.value == default), None)
|
|
721
|
+
if default_option and default_option.enabled:
|
|
722
|
+
return default
|
|
723
|
+
reason = default_option.unavailable_reason if default_option else "unknown option"
|
|
724
|
+
print(f"Unavailable: {reason}. Select an enabled option.")
|
|
725
|
+
continue
|
|
726
|
+
if raw.lower() in {"q", "quit"}:
|
|
727
|
+
raise KeyboardInterrupt
|
|
728
|
+
if raw.lower() in {"b", "back"}:
|
|
729
|
+
if allow_back:
|
|
730
|
+
raise BackRequested
|
|
731
|
+
print("No previous menu. Use q to cancel.")
|
|
732
|
+
continue
|
|
733
|
+
if raw.isdigit() and 1 <= int(raw) <= len(options):
|
|
734
|
+
option = options[int(raw) - 1]
|
|
735
|
+
if option.enabled:
|
|
736
|
+
return option.value
|
|
737
|
+
print(f"Unavailable: {option.unavailable_reason}")
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def choose(
|
|
741
|
+
title: str,
|
|
742
|
+
options: list[MenuOption],
|
|
743
|
+
default: str,
|
|
744
|
+
ui: TextualUI | None = None,
|
|
745
|
+
allow_back: bool = False,
|
|
746
|
+
preview=None,
|
|
747
|
+
) -> str:
|
|
748
|
+
if not any(option.enabled for option in options):
|
|
749
|
+
raise LaunchError(f"no available options for {title}")
|
|
750
|
+
if ui is not None:
|
|
751
|
+
return ui.choose(title, options, default, allow_back, preview)
|
|
752
|
+
return choose_lines(title, options, default, allow_back)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def read_input(prompt: str) -> str:
|
|
756
|
+
try:
|
|
757
|
+
return input(prompt)
|
|
758
|
+
except EOFError as exc:
|
|
759
|
+
raise LaunchError(
|
|
760
|
+
"interactive input ended; use --preset NAME for a non-interactive configured launch"
|
|
761
|
+
) from exc
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def prompt_text(label: str, default: str, ui: TextualUI | None = None) -> str:
|
|
765
|
+
if ui is not None:
|
|
766
|
+
return ui.prompt_text(label, default)
|
|
767
|
+
value = read_input(f"{label} [{default}] (q cancel): ").strip()
|
|
768
|
+
if value.lower() == "q":
|
|
769
|
+
raise KeyboardInterrupt
|
|
770
|
+
return value or default
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> list[str]:
|
|
774
|
+
"""Selectable model catalog for a host: the configured [hosts.<host>].models
|
|
775
|
+
list when present, else the distinct models already bound to the tiers."""
|
|
776
|
+
models = config["hosts"][host].get("models")
|
|
777
|
+
if models:
|
|
778
|
+
return list(models)
|
|
779
|
+
catalog: list[str] = []
|
|
780
|
+
for tier in TIER_ORDER:
|
|
781
|
+
model = tiers[tier]["model"]
|
|
782
|
+
if model not in catalog:
|
|
783
|
+
catalog.append(model)
|
|
784
|
+
return catalog
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str, Any]:
|
|
788
|
+
try:
|
|
789
|
+
preset = copy.deepcopy(config["presets"][preset_name])
|
|
790
|
+
tiers = copy.deepcopy(config["hosts"][host]["tiers"])
|
|
791
|
+
except (KeyError, TypeError) as exc:
|
|
792
|
+
raise LaunchError(f"invalid preset or host binding: {host}/{preset_name}") from exc
|
|
793
|
+
if not isinstance(preset, dict) or not isinstance(tiers, dict):
|
|
794
|
+
raise LaunchError(f"invalid preset or host binding: {host}/{preset_name}")
|
|
795
|
+
if set(tiers) != set(TIER_ORDER):
|
|
796
|
+
raise LaunchError(f"{host} tiers must be exactly: {', '.join(TIER_ORDER)}")
|
|
797
|
+
for tier, binding in tiers.items():
|
|
798
|
+
if not isinstance(binding, dict):
|
|
799
|
+
raise LaunchError(f"invalid binding: {host}.{tier}")
|
|
800
|
+
model = binding.get("model")
|
|
801
|
+
if not isinstance(model, str) or not model:
|
|
802
|
+
raise LaunchError(f"invalid binding: {host}.{tier}")
|
|
803
|
+
validate_effort(host, model, binding.get("effort"), f"{host}.{tier}")
|
|
804
|
+
all_overrides = preset.get("tier_overrides", {})
|
|
805
|
+
if not isinstance(all_overrides, dict):
|
|
806
|
+
raise LaunchError(f"tier_overrides must be a table in preset {preset_name}")
|
|
807
|
+
host_overrides = all_overrides.get(host, {})
|
|
808
|
+
if not isinstance(host_overrides, dict):
|
|
809
|
+
raise LaunchError(f"tier_overrides.{host} must be a table in preset {preset_name}")
|
|
810
|
+
for tier, override in host_overrides.items():
|
|
811
|
+
if tier not in tiers:
|
|
812
|
+
raise LaunchError(f"unknown tier in {preset_name}.tier_overrides.{host}: {tier}")
|
|
813
|
+
if not isinstance(override, dict):
|
|
814
|
+
raise LaunchError(f"tier override must be a table: {preset_name}.tier_overrides.{host}.{tier}")
|
|
815
|
+
override_model = override.get("model", tiers[tier]["model"])
|
|
816
|
+
if not isinstance(override_model, str) or not override_model:
|
|
817
|
+
raise LaunchError(f"invalid override model: {preset_name}.tier_overrides.{host}.{tier}")
|
|
818
|
+
override_effort = override.get("effort", tiers[tier]["effort"])
|
|
819
|
+
validate_effort(host, override_model, override_effort, f"{preset_name}.tier_overrides.{host}.{tier}")
|
|
820
|
+
tiers[tier] = {"model": override_model, "effort": override_effort}
|
|
821
|
+
main_tier = preset.get("main_tier")
|
|
822
|
+
if not isinstance(main_tier, str) or main_tier not in tiers:
|
|
823
|
+
raise LaunchError(f"invalid main_tier in preset {preset_name}: {main_tier}")
|
|
824
|
+
frontier_effort = preset.get("frontier_effort", tiers["frontier"]["effort"])
|
|
825
|
+
if isinstance(frontier_effort, dict):
|
|
826
|
+
frontier_effort = frontier_effort.get(host)
|
|
827
|
+
validate_effort(host, tiers["frontier"]["model"], frontier_effort, f"{preset_name}.frontier")
|
|
828
|
+
delegation = preset.get("delegation", True)
|
|
829
|
+
if not isinstance(delegation, bool):
|
|
830
|
+
raise LaunchError(f"delegation must be boolean in preset {preset_name}")
|
|
831
|
+
review_setup = preset.get("review_setup", "none")
|
|
832
|
+
if not isinstance(review_setup, str) or review_setup not in REVIEW_SETUPS:
|
|
833
|
+
raise LaunchError(f"unknown review setup in preset {preset_name}: {review_setup!r}")
|
|
834
|
+
codex_policy = preset.get("codex_execution_policy")
|
|
835
|
+
claude_policy = preset.get("claude_permission_mode")
|
|
836
|
+
if not isinstance(codex_policy, str) or codex_policy not in CODEX_POLICIES:
|
|
837
|
+
raise LaunchError(f"invalid Codex policy in preset {preset_name}: {codex_policy!r}")
|
|
838
|
+
if not isinstance(claude_policy, str) or claude_policy not in CLAUDE_POLICIES:
|
|
839
|
+
raise LaunchError(f"invalid Claude policy in preset {preset_name}: {claude_policy!r}")
|
|
840
|
+
label = preset.get("label", preset_name)
|
|
841
|
+
if not isinstance(label, str) or not label:
|
|
842
|
+
raise LaunchError(f"invalid label in preset {preset_name}")
|
|
843
|
+
review_family = preset.get("review_family", "cross")
|
|
844
|
+
review_host = REVIEW_HOST[host]
|
|
845
|
+
opposite = config.get("hosts", {}).get(review_host)
|
|
846
|
+
opposite_backend = config.get("backends", {}).get(review_host, {})
|
|
847
|
+
if (
|
|
848
|
+
not isinstance(opposite, dict)
|
|
849
|
+
or not isinstance(opposite.get("tiers"), dict)
|
|
850
|
+
or not isinstance(opposite_backend, dict)
|
|
851
|
+
or not opposite_backend.get("command")
|
|
852
|
+
):
|
|
853
|
+
# Cross review needs the opposite host + backend; without them fall back to
|
|
854
|
+
# same-family review (surfaced as review_family=same) rather than crashing.
|
|
855
|
+
review_family = "same"
|
|
856
|
+
review_tiers, review_onto, review_backend = {}, None, ""
|
|
857
|
+
else:
|
|
858
|
+
review_tiers = copy.deepcopy(opposite["tiers"])
|
|
859
|
+
review_onto = copy.deepcopy(opposite.get("onto_review"))
|
|
860
|
+
review_backend = opposite_backend["command"]
|
|
861
|
+
return {
|
|
862
|
+
"host": host,
|
|
863
|
+
"preset": preset_name,
|
|
864
|
+
"label": label,
|
|
865
|
+
"description": preset.get("description", f"Launch the {label} preset."),
|
|
866
|
+
"main_tier": main_tier,
|
|
867
|
+
"frontier_effort": frontier_effort,
|
|
868
|
+
"review_setup": review_setup,
|
|
869
|
+
"delegation": delegation,
|
|
870
|
+
"codex_execution_policy": codex_policy,
|
|
871
|
+
"claude_permission_mode": claude_policy,
|
|
872
|
+
"tiers": tiers,
|
|
873
|
+
"available_models": host_models(config, host, tiers),
|
|
874
|
+
"review_family": review_family,
|
|
875
|
+
"review_host": review_host,
|
|
876
|
+
"review_tiers": review_tiers,
|
|
877
|
+
"review_onto": review_onto,
|
|
878
|
+
"review_backend": review_backend,
|
|
879
|
+
"agent_templates": copy.deepcopy(config["hosts"][host].get("agent_templates")),
|
|
880
|
+
"capabilities": copy.deepcopy(config.get("capabilities", {})),
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def review_description(host: str, name: str) -> str:
|
|
885
|
+
contract = REVIEW_SETUPS[name]["contract"]
|
|
886
|
+
return contract[host] if isinstance(contract, dict) else contract
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def effort_options(host: str, model: str) -> list[MenuOption]:
|
|
890
|
+
options = []
|
|
891
|
+
for effort in EFFORT_ORDER:
|
|
892
|
+
enabled = effort in HOST_EFFORTS[host]
|
|
893
|
+
reason = f"not supported by {host}" if not enabled else ""
|
|
894
|
+
if enabled and host == "codex" and model == "gpt-5.6-luna" and effort == "ultra":
|
|
895
|
+
enabled = False
|
|
896
|
+
reason = "not supported by gpt-5.6-luna"
|
|
897
|
+
options.append(
|
|
898
|
+
MenuOption(effort, effort, EFFORT_DESCRIPTIONS[effort], enabled, reason)
|
|
899
|
+
)
|
|
900
|
+
return options
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def model_options(plan: dict[str, Any]) -> list[MenuOption]:
|
|
904
|
+
options = [
|
|
905
|
+
MenuOption(model, model, f"Use {model} as this tier's model.")
|
|
906
|
+
for model in plan["available_models"]
|
|
907
|
+
]
|
|
908
|
+
options.append(
|
|
909
|
+
MenuOption(
|
|
910
|
+
OTHER_MODEL,
|
|
911
|
+
"Other (enter a model id)",
|
|
912
|
+
"Type any model id the backend accepts, even if it is not in the catalog.",
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
return options
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def valid_preset_name(name: str) -> bool:
|
|
919
|
+
return bool(name) and name[0].isalnum() and all(
|
|
920
|
+
character.isalnum() or character in "-_" for character in name
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def preset_from_plan(
|
|
925
|
+
plan: dict[str, Any], config: dict[str, Any], name: str
|
|
926
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
927
|
+
"""Project the plan into a saveable named preset plus host-scoped tier
|
|
928
|
+
overrides. Only tier bindings that differ from the host defaults are recorded,
|
|
929
|
+
scoped to the plan's host so the preset stays correct on the other host, which
|
|
930
|
+
falls back to its own defaults."""
|
|
931
|
+
host = plan["host"]
|
|
932
|
+
default_tiers = config["hosts"][host]["tiers"]
|
|
933
|
+
fields = {
|
|
934
|
+
"label": name,
|
|
935
|
+
"description": "Saved custom launch setup.",
|
|
936
|
+
"main_tier": plan["main_tier"],
|
|
937
|
+
"review_setup": plan["review_setup"],
|
|
938
|
+
"review_family": plan.get("review_family", "cross"),
|
|
939
|
+
"delegation": plan["delegation"],
|
|
940
|
+
"codex_execution_policy": plan["codex_execution_policy"],
|
|
941
|
+
"claude_permission_mode": plan["claude_permission_mode"],
|
|
942
|
+
}
|
|
943
|
+
overrides: dict[str, dict[str, str]] = {}
|
|
944
|
+
for tier in TIER_ORDER:
|
|
945
|
+
override: dict[str, str] = {}
|
|
946
|
+
if plan["tiers"][tier]["model"] != default_tiers[tier]["model"]:
|
|
947
|
+
override["model"] = plan["tiers"][tier]["model"]
|
|
948
|
+
if tier_effort(plan, tier) != default_tiers[tier]["effort"]:
|
|
949
|
+
override["effort"] = tier_effort(plan, tier)
|
|
950
|
+
if override:
|
|
951
|
+
overrides[tier] = override
|
|
952
|
+
return fields, ({host: overrides} if overrides else {})
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _toml_scalar(value: Any) -> str:
|
|
956
|
+
if isinstance(value, bool):
|
|
957
|
+
return "true" if value else "false"
|
|
958
|
+
if isinstance(value, str):
|
|
959
|
+
return json.dumps(value, ensure_ascii=False)
|
|
960
|
+
raise LaunchError(f"cannot serialize preset value: {value!r}")
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def render_preset_block(
|
|
964
|
+
name: str, fields: dict[str, Any], tier_overrides: dict[str, Any]
|
|
965
|
+
) -> str:
|
|
966
|
+
lines = [f"[presets.{name}]"]
|
|
967
|
+
for key, value in fields.items():
|
|
968
|
+
lines.append(f"{key} = {_toml_scalar(value)}")
|
|
969
|
+
for host, tiers in tier_overrides.items():
|
|
970
|
+
for tier, override in tiers.items():
|
|
971
|
+
lines.append("")
|
|
972
|
+
lines.append(f"[presets.{name}.tier_overrides.{host}.{tier}]")
|
|
973
|
+
for key, value in override.items():
|
|
974
|
+
lines.append(f"{key} = {_toml_scalar(value)}")
|
|
975
|
+
return "\n".join(lines) + "\n"
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def remove_preset_block(text: str, name: str) -> str:
|
|
979
|
+
"""Drop an existing [presets.<name>] table and its sub-tables, leaving the
|
|
980
|
+
rest of the file (other presets, comments, host bindings) intact."""
|
|
981
|
+
exact = f"[presets.{name}]"
|
|
982
|
+
sub_prefix = f"[presets.{name}."
|
|
983
|
+
kept = []
|
|
984
|
+
skipping = False
|
|
985
|
+
for line in text.splitlines(keepends=True):
|
|
986
|
+
header = line.strip()
|
|
987
|
+
if header.startswith("["):
|
|
988
|
+
skipping = header == exact or header.startswith(sub_prefix)
|
|
989
|
+
if skipping:
|
|
990
|
+
continue
|
|
991
|
+
kept.append(line)
|
|
992
|
+
return "".join(kept)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def save_preset(
|
|
996
|
+
plan: dict[str, Any], config: dict[str, Any], config_path: pathlib.Path, name: str
|
|
997
|
+
) -> None:
|
|
998
|
+
if not valid_preset_name(name):
|
|
999
|
+
raise LaunchError(
|
|
1000
|
+
"preset name must start alphanumeric and use only letters, digits, '-' or '_'"
|
|
1001
|
+
)
|
|
1002
|
+
fields, tier_overrides = preset_from_plan(plan, config, name)
|
|
1003
|
+
block = render_preset_block(name, fields, tier_overrides)
|
|
1004
|
+
text = remove_preset_block(config_path.read_text(), name).rstrip("\n") + "\n\n" + block
|
|
1005
|
+
temporary = config_path.with_name(f".{config_path.name}.{os.getpid()}.tmp")
|
|
1006
|
+
try:
|
|
1007
|
+
temporary.write_text(text)
|
|
1008
|
+
os.replace(temporary, config_path)
|
|
1009
|
+
except OSError as exc:
|
|
1010
|
+
raise LaunchError(f"cannot save preset to {config_path}: {exc}") from exc
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def customize(
|
|
1014
|
+
plan: dict[str, Any],
|
|
1015
|
+
config: dict[str, Any],
|
|
1016
|
+
config_path: pathlib.Path,
|
|
1017
|
+
ui: TextualUI | None = None,
|
|
1018
|
+
) -> None:
|
|
1019
|
+
tier_options = [
|
|
1020
|
+
MenuOption(tier, tier.upper(), TIER_DESCRIPTIONS[tier]) for tier in TIER_ORDER
|
|
1021
|
+
]
|
|
1022
|
+
available_routes = route_availability(plan)
|
|
1023
|
+
cross = plan.get("review_family", "cross") == "cross"
|
|
1024
|
+
review_options = []
|
|
1025
|
+
for name, spec in REVIEW_SETUPS.items():
|
|
1026
|
+
description = review_description(plan["host"], name)
|
|
1027
|
+
if cross and name != "none":
|
|
1028
|
+
description = f"Runs cross-family on {plan['review_host']} models. {description}"
|
|
1029
|
+
missing = [
|
|
1030
|
+
route
|
|
1031
|
+
for route in REVIEW_ROUTES[name]
|
|
1032
|
+
if route in ("onto", "ultracode") and not available_routes[route]
|
|
1033
|
+
]
|
|
1034
|
+
if missing:
|
|
1035
|
+
note = "degrades to same-family native (PROPOSED)" if cross else "degrades to native"
|
|
1036
|
+
description = f"{description} ({', '.join(missing)} unavailable now; {note})"
|
|
1037
|
+
review_options.append(MenuOption(name, spec["label"], description))
|
|
1038
|
+
if plan["host"] == "codex":
|
|
1039
|
+
policies = [
|
|
1040
|
+
MenuOption(
|
|
1041
|
+
"bypass",
|
|
1042
|
+
"Bypass approvals and sandbox",
|
|
1043
|
+
"Run without Codex approval or sandbox restrictions.",
|
|
1044
|
+
),
|
|
1045
|
+
MenuOption(
|
|
1046
|
+
"workspace-write",
|
|
1047
|
+
"Workspace write sandbox",
|
|
1048
|
+
"Allow workspace writes under Codex's workspace-write sandbox.",
|
|
1049
|
+
),
|
|
1050
|
+
MenuOption(
|
|
1051
|
+
"read-only",
|
|
1052
|
+
"Read-only sandbox",
|
|
1053
|
+
"Allow inspection while blocking file writes through the Codex sandbox.",
|
|
1054
|
+
),
|
|
1055
|
+
]
|
|
1056
|
+
policy_field = "codex_execution_policy"
|
|
1057
|
+
policy_title = "Codex execution policy"
|
|
1058
|
+
else:
|
|
1059
|
+
policies = [
|
|
1060
|
+
MenuOption(
|
|
1061
|
+
"bypassPermissions",
|
|
1062
|
+
"Bypass permissions",
|
|
1063
|
+
"Skip Claude permission prompts; this is not an OS sandbox setting.",
|
|
1064
|
+
),
|
|
1065
|
+
MenuOption(
|
|
1066
|
+
"acceptEdits",
|
|
1067
|
+
"Accept edits",
|
|
1068
|
+
"Automatically accept file edits while retaining other permission checks.",
|
|
1069
|
+
),
|
|
1070
|
+
MenuOption(
|
|
1071
|
+
"auto",
|
|
1072
|
+
"Auto permissions",
|
|
1073
|
+
"Use Claude's automatic permission mode.",
|
|
1074
|
+
),
|
|
1075
|
+
MenuOption(
|
|
1076
|
+
"manual",
|
|
1077
|
+
"Manual permissions",
|
|
1078
|
+
"Ask before actions according to Claude's manual permission flow.",
|
|
1079
|
+
),
|
|
1080
|
+
MenuOption(
|
|
1081
|
+
"dontAsk",
|
|
1082
|
+
"Do not ask",
|
|
1083
|
+
"Use Claude's dontAsk permission mode.",
|
|
1084
|
+
),
|
|
1085
|
+
MenuOption(
|
|
1086
|
+
"plan",
|
|
1087
|
+
"Plan mode",
|
|
1088
|
+
"Start Claude in planning mode without direct implementation.",
|
|
1089
|
+
),
|
|
1090
|
+
]
|
|
1091
|
+
policy_field = "claude_permission_mode"
|
|
1092
|
+
policy_title = "Claude permission mode (not an OS sandbox)"
|
|
1093
|
+
selected_action = "main"
|
|
1094
|
+
while True:
|
|
1095
|
+
policy_label = next(
|
|
1096
|
+
option.label for option in policies if option.value == plan[policy_field]
|
|
1097
|
+
)
|
|
1098
|
+
hub_options = [
|
|
1099
|
+
MenuOption(
|
|
1100
|
+
"main",
|
|
1101
|
+
f"Main tier: {plan['main_tier'].upper()}",
|
|
1102
|
+
"Choose the primary orchestrator used for this session.",
|
|
1103
|
+
),
|
|
1104
|
+
MenuOption(
|
|
1105
|
+
"review",
|
|
1106
|
+
f"Review setup: {REVIEW_SETUPS[plan['review_setup']]['label']}",
|
|
1107
|
+
"Choose the review routes requested when review gates fire.",
|
|
1108
|
+
),
|
|
1109
|
+
MenuOption(
|
|
1110
|
+
"policy",
|
|
1111
|
+
f"Execution policy: {policy_label}",
|
|
1112
|
+
"Choose the host-specific permission or sandbox behavior.",
|
|
1113
|
+
),
|
|
1114
|
+
]
|
|
1115
|
+
for tier in TIER_ORDER:
|
|
1116
|
+
binding = plan["tiers"][tier]
|
|
1117
|
+
hub_options.append(
|
|
1118
|
+
MenuOption(
|
|
1119
|
+
f"tier:{tier}",
|
|
1120
|
+
f"{tier.upper()}: {binding['model']} / {tier_effort(plan, tier)}",
|
|
1121
|
+
f"Edit the {tier.upper()} model and reasoning effort, then return here.",
|
|
1122
|
+
)
|
|
1123
|
+
)
|
|
1124
|
+
hub_options += [
|
|
1125
|
+
MenuOption(
|
|
1126
|
+
"save",
|
|
1127
|
+
"Save these settings globally and start",
|
|
1128
|
+
"Save this setup as a named preset in your user config for reuse elsewhere, then launch.",
|
|
1129
|
+
),
|
|
1130
|
+
MenuOption(
|
|
1131
|
+
"start",
|
|
1132
|
+
"Start with these settings",
|
|
1133
|
+
"Confirm the complete setup shown above and continue to launch.",
|
|
1134
|
+
),
|
|
1135
|
+
MenuOption(
|
|
1136
|
+
"exit",
|
|
1137
|
+
"Exit without launching",
|
|
1138
|
+
"Discard this launch and return to the shell.",
|
|
1139
|
+
),
|
|
1140
|
+
]
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
action = choose(
|
|
1144
|
+
"Custom settings",
|
|
1145
|
+
hub_options,
|
|
1146
|
+
selected_action,
|
|
1147
|
+
ui,
|
|
1148
|
+
allow_back=True,
|
|
1149
|
+
)
|
|
1150
|
+
except BackRequested:
|
|
1151
|
+
raise
|
|
1152
|
+
selected_action = action
|
|
1153
|
+
|
|
1154
|
+
try:
|
|
1155
|
+
if action == "main":
|
|
1156
|
+
plan["main_tier"] = choose(
|
|
1157
|
+
"Main tier",
|
|
1158
|
+
tier_options,
|
|
1159
|
+
plan["main_tier"],
|
|
1160
|
+
ui,
|
|
1161
|
+
allow_back=True,
|
|
1162
|
+
preview=lambda value: {**plan, "main_tier": value},
|
|
1163
|
+
)
|
|
1164
|
+
elif action == "review":
|
|
1165
|
+
plan["review_setup"] = choose(
|
|
1166
|
+
"Review setup",
|
|
1167
|
+
review_options,
|
|
1168
|
+
plan["review_setup"],
|
|
1169
|
+
ui,
|
|
1170
|
+
allow_back=True,
|
|
1171
|
+
preview=lambda value: {**plan, "review_setup": value},
|
|
1172
|
+
)
|
|
1173
|
+
elif action == "policy":
|
|
1174
|
+
plan[policy_field] = choose(
|
|
1175
|
+
policy_title,
|
|
1176
|
+
policies,
|
|
1177
|
+
plan[policy_field],
|
|
1178
|
+
ui,
|
|
1179
|
+
allow_back=True,
|
|
1180
|
+
preview=lambda value: {**plan, policy_field: value},
|
|
1181
|
+
)
|
|
1182
|
+
except BackRequested:
|
|
1183
|
+
continue
|
|
1184
|
+
|
|
1185
|
+
if action.startswith("tier:"):
|
|
1186
|
+
tier = action.split(":", 1)[1]
|
|
1187
|
+
binding = plan["tiers"][tier]
|
|
1188
|
+
while True:
|
|
1189
|
+
default_model = (
|
|
1190
|
+
binding["model"]
|
|
1191
|
+
if binding["model"] in plan["available_models"]
|
|
1192
|
+
else OTHER_MODEL
|
|
1193
|
+
)
|
|
1194
|
+
try:
|
|
1195
|
+
chosen_model = choose(
|
|
1196
|
+
f"{tier.upper()} model",
|
|
1197
|
+
model_options(plan),
|
|
1198
|
+
default_model,
|
|
1199
|
+
ui,
|
|
1200
|
+
allow_back=True,
|
|
1201
|
+
preview=lambda value: {
|
|
1202
|
+
**plan,
|
|
1203
|
+
"tiers": {
|
|
1204
|
+
**plan["tiers"],
|
|
1205
|
+
tier: {
|
|
1206
|
+
**binding,
|
|
1207
|
+
"model": binding["model"]
|
|
1208
|
+
if value == OTHER_MODEL
|
|
1209
|
+
else value,
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
)
|
|
1214
|
+
except BackRequested:
|
|
1215
|
+
break
|
|
1216
|
+
if chosen_model == OTHER_MODEL:
|
|
1217
|
+
binding["model"] = prompt_text(
|
|
1218
|
+
f"{tier.upper()} model", binding["model"], ui
|
|
1219
|
+
)
|
|
1220
|
+
else:
|
|
1221
|
+
binding["model"] = chosen_model
|
|
1222
|
+
current_effort = tier_effort(plan, tier)
|
|
1223
|
+
try:
|
|
1224
|
+
binding["effort"] = choose(
|
|
1225
|
+
f"{tier.upper()} effort",
|
|
1226
|
+
effort_options(plan["host"], binding["model"]),
|
|
1227
|
+
current_effort,
|
|
1228
|
+
ui,
|
|
1229
|
+
allow_back=True,
|
|
1230
|
+
preview=lambda value: {
|
|
1231
|
+
**plan,
|
|
1232
|
+
"frontier_effort": value
|
|
1233
|
+
if tier == "frontier"
|
|
1234
|
+
else plan["frontier_effort"],
|
|
1235
|
+
"tiers": {
|
|
1236
|
+
**plan["tiers"],
|
|
1237
|
+
tier: {**binding, "effort": value},
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
)
|
|
1241
|
+
except BackRequested:
|
|
1242
|
+
continue
|
|
1243
|
+
break
|
|
1244
|
+
if tier == "frontier":
|
|
1245
|
+
plan["frontier_effort"] = binding["effort"]
|
|
1246
|
+
continue
|
|
1247
|
+
|
|
1248
|
+
if action in ("save", "start"):
|
|
1249
|
+
for tier, binding in plan["tiers"].items():
|
|
1250
|
+
validate_effort(
|
|
1251
|
+
plan["host"],
|
|
1252
|
+
binding["model"],
|
|
1253
|
+
tier_effort(plan, tier),
|
|
1254
|
+
f"custom.{tier}",
|
|
1255
|
+
)
|
|
1256
|
+
validate_review_setup(plan)
|
|
1257
|
+
if action == "save":
|
|
1258
|
+
name = prompt_text(
|
|
1259
|
+
"Save as preset name", f"{plan['preset']}-custom", ui
|
|
1260
|
+
)
|
|
1261
|
+
save_preset(plan, config, config_path, name)
|
|
1262
|
+
plan["_launch_confirmed"] = True
|
|
1263
|
+
return
|
|
1264
|
+
if action == "exit":
|
|
1265
|
+
raise KeyboardInterrupt
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def select_plan(
|
|
1269
|
+
config: dict[str, Any],
|
|
1270
|
+
host: str,
|
|
1271
|
+
preset_name: str | None,
|
|
1272
|
+
custom_requested: bool,
|
|
1273
|
+
ui: TextualUI | None = None,
|
|
1274
|
+
config_path: pathlib.Path | None = None,
|
|
1275
|
+
) -> dict[str, Any]:
|
|
1276
|
+
presets = config["presets"]
|
|
1277
|
+
explicit_preset = preset_name
|
|
1278
|
+
show_picker = explicit_preset is None
|
|
1279
|
+
while True:
|
|
1280
|
+
selected_name = explicit_preset
|
|
1281
|
+
selected_custom = custom_requested
|
|
1282
|
+
if show_picker:
|
|
1283
|
+
options = [
|
|
1284
|
+
MenuOption(
|
|
1285
|
+
name,
|
|
1286
|
+
data["label"],
|
|
1287
|
+
data.get("description", f"Launch the {data['label']} preset."),
|
|
1288
|
+
)
|
|
1289
|
+
for name, data in presets.items()
|
|
1290
|
+
]
|
|
1291
|
+
options.append(
|
|
1292
|
+
MenuOption(
|
|
1293
|
+
CUSTOM_PRESET,
|
|
1294
|
+
"Custom",
|
|
1295
|
+
"Open a settings hub for tiers, review setup, policy, and final confirmation.",
|
|
1296
|
+
)
|
|
1297
|
+
)
|
|
1298
|
+
default = "balanced" if "balanced" in presets else next(iter(presets))
|
|
1299
|
+
if ui is not None:
|
|
1300
|
+
ui.set_plan(build_plan(config, host, default))
|
|
1301
|
+
selected = choose(
|
|
1302
|
+
"Preset",
|
|
1303
|
+
options,
|
|
1304
|
+
default,
|
|
1305
|
+
ui,
|
|
1306
|
+
preview=lambda value: build_plan(
|
|
1307
|
+
config, host, default if value == CUSTOM_PRESET else value
|
|
1308
|
+
),
|
|
1309
|
+
)
|
|
1310
|
+
if selected == CUSTOM_PRESET:
|
|
1311
|
+
selected_name = default
|
|
1312
|
+
selected_custom = True
|
|
1313
|
+
else:
|
|
1314
|
+
selected_name = selected
|
|
1315
|
+
if selected_name not in presets:
|
|
1316
|
+
raise LaunchError(f"unknown preset: {selected_name}")
|
|
1317
|
+
plan = build_plan(config, host, selected_name)
|
|
1318
|
+
if selected_custom:
|
|
1319
|
+
plan["label"] = f"Custom ({plan['label']})"
|
|
1320
|
+
if ui is not None:
|
|
1321
|
+
ui.set_plan(plan)
|
|
1322
|
+
if selected_custom:
|
|
1323
|
+
try:
|
|
1324
|
+
customize(plan, config, config_path, ui)
|
|
1325
|
+
except BackRequested:
|
|
1326
|
+
if show_picker:
|
|
1327
|
+
continue
|
|
1328
|
+
raise KeyboardInterrupt
|
|
1329
|
+
return plan
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def validate_review_setup(plan: dict[str, Any]) -> None:
|
|
1333
|
+
# Fail-closed only on the delegation contradiction (or an unknown setup); a
|
|
1334
|
+
# missing external capability degrades to native in effective_review rather
|
|
1335
|
+
# than raising here.
|
|
1336
|
+
effective_review(plan)
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def tier_effort(plan: dict[str, Any], tier: str) -> str:
|
|
1340
|
+
return plan["frontier_effort"] if tier == "frontier" else plan["tiers"][tier]["effort"]
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def codex_agent_configs(
|
|
1344
|
+
plan: dict[str, Any], materialize: bool
|
|
1345
|
+
) -> dict[str, tuple[pathlib.Path, str]]:
|
|
1346
|
+
templates = plan.get("agent_templates")
|
|
1347
|
+
if not isinstance(templates, dict):
|
|
1348
|
+
raise LaunchError("Codex delegation requires [hosts.codex.agent_templates]")
|
|
1349
|
+
rendered: dict[str, tuple[str, str]] = {}
|
|
1350
|
+
for tier in ("frontier", "workhorse", "sweep"):
|
|
1351
|
+
source_value = templates.get(tier)
|
|
1352
|
+
if not isinstance(source_value, str) or not source_value:
|
|
1353
|
+
raise LaunchError(f"Codex agent template missing: {tier}")
|
|
1354
|
+
source = expand_config_path(source_value)
|
|
1355
|
+
try:
|
|
1356
|
+
data = tomllib.loads(source.read_text())
|
|
1357
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
1358
|
+
raise LaunchError(f"cannot load Codex agent template {source}: {exc}") from exc
|
|
1359
|
+
description = data.get("description")
|
|
1360
|
+
if not isinstance(description, str) or not description:
|
|
1361
|
+
raise LaunchError(f"Codex agent template requires description: {source}")
|
|
1362
|
+
data["model"] = plan["tiers"][tier]["model"]
|
|
1363
|
+
data["model_reasoning_effort"] = tier_effort(plan, tier)
|
|
1364
|
+
lines = []
|
|
1365
|
+
for key, value in data.items():
|
|
1366
|
+
if not isinstance(value, (str, int, float, bool)):
|
|
1367
|
+
raise LaunchError(f"unsupported Codex agent template value: {source}:{key}")
|
|
1368
|
+
lines.append(f"{key} = {json.dumps(value)}")
|
|
1369
|
+
rendered[tier] = ("\n".join(lines) + "\n", description)
|
|
1370
|
+
digest = hashlib.sha256(
|
|
1371
|
+
json.dumps(rendered, sort_keys=True, separators=(",", ":")).encode()
|
|
1372
|
+
).hexdigest()[:20]
|
|
1373
|
+
cache_root = pathlib.Path(
|
|
1374
|
+
os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")
|
|
1375
|
+
) / "agent-launch/codex-agents" / digest
|
|
1376
|
+
if materialize:
|
|
1377
|
+
cache_root.mkdir(parents=True, exist_ok=True)
|
|
1378
|
+
result = {}
|
|
1379
|
+
for tier, (content, description) in rendered.items():
|
|
1380
|
+
target = cache_root / f"{tier}.toml"
|
|
1381
|
+
if materialize and (not target.is_file() or target.read_text() != content):
|
|
1382
|
+
temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
|
1383
|
+
temporary.write_text(content)
|
|
1384
|
+
temporary.chmod(0o600)
|
|
1385
|
+
os.replace(temporary, target)
|
|
1386
|
+
result[tier] = (target, description)
|
|
1387
|
+
return result
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _same_review_route(plan: dict[str, Any], effective: list[str], dropped: list[str]) -> str:
|
|
1391
|
+
requested = plan["review_setup"]
|
|
1392
|
+
if dropped:
|
|
1393
|
+
kept = ", ".join(ROUTE_LABELS[route] for route in effective) or "no additional review route"
|
|
1394
|
+
route = f"{', '.join(dropped)} unavailable at launch; degraded to {kept}."
|
|
1395
|
+
else:
|
|
1396
|
+
route = REVIEW_SETUPS[requested]["contract"]
|
|
1397
|
+
if isinstance(route, dict):
|
|
1398
|
+
route = route[plan["host"]]
|
|
1399
|
+
if "ultracode" in effective:
|
|
1400
|
+
route = f"{route} Ultracode executable: {resolve_command(plan['capabilities']['ultracode']['command'])}."
|
|
1401
|
+
if "ultracode" in REVIEW_ROUTES[requested]:
|
|
1402
|
+
route = (
|
|
1403
|
+
f"{route} If the Codex-backed route is unavailable or unauthenticated "
|
|
1404
|
+
"at use time, fall back to native same-model subagent review."
|
|
1405
|
+
)
|
|
1406
|
+
return route
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _cross_review_route(
|
|
1410
|
+
plan: dict[str, Any], effective: list[str], dropped: list[str], floor: str | None
|
|
1411
|
+
) -> str:
|
|
1412
|
+
review_host = plan["review_host"]
|
|
1413
|
+
main_family = "Anthropic/Claude" if plan["host"] == "claude" else "OpenAI/Codex"
|
|
1414
|
+
review_family = "OpenAI/Codex" if review_host == "codex" else "Anthropic/Claude"
|
|
1415
|
+
review_bindings = ", ".join(
|
|
1416
|
+
f"{tier}={plan['review_tiers'][tier]['model']}/{plan['review_tiers'][tier]['effort']}"
|
|
1417
|
+
for tier in TIER_ORDER
|
|
1418
|
+
if tier in plan["review_tiers"]
|
|
1419
|
+
)
|
|
1420
|
+
parts = [
|
|
1421
|
+
f"Cross-family review: this main is {main_family}; run EVERY review route on "
|
|
1422
|
+
f"{review_family} ({review_host}) models. Do not use your own same-family "
|
|
1423
|
+
"subagents for primary review — they are the delegation/fallback floor only.",
|
|
1424
|
+
f"Reviewer tier bindings ({review_host}): {review_bindings}.",
|
|
1425
|
+
]
|
|
1426
|
+
if "native" in effective:
|
|
1427
|
+
native = cross_native_command(plan)
|
|
1428
|
+
if review_host == "codex":
|
|
1429
|
+
helm = cross_helm_command(plan)
|
|
1430
|
+
fanout = f" (or {helm} --mode review for hybrid fan-out)" if helm else ""
|
|
1431
|
+
parts.append(
|
|
1432
|
+
f"native: dispatch {native} --profile hermetic --model <review tier> "
|
|
1433
|
+
f"--effort <e> --sandbox read-only{fanout}, self-contained packet on stdin, "
|
|
1434
|
+
"bounded read-only report."
|
|
1435
|
+
)
|
|
1436
|
+
else:
|
|
1437
|
+
parts.append(
|
|
1438
|
+
f"native: dispatch {native} -p --model <review tier> --effort <e> "
|
|
1439
|
+
"--permission-mode plan --append-system-prompt <read-only reviewer role>, "
|
|
1440
|
+
"self-contained packet, bounded report."
|
|
1441
|
+
)
|
|
1442
|
+
if "onto" in effective:
|
|
1443
|
+
onto = plan["review_onto"]
|
|
1444
|
+
parts.append(
|
|
1445
|
+
f'onto: call onto_review/onto_prepare_review with llmOverride='
|
|
1446
|
+
f'{{"provider":"{onto["provider"]}","model":"{onto["model"]}"}} so onto runs {review_family}.'
|
|
1447
|
+
)
|
|
1448
|
+
if "ultracode" in effective:
|
|
1449
|
+
ultracode = cross_ultracode_command(plan)
|
|
1450
|
+
if review_host == "codex":
|
|
1451
|
+
parts.append(
|
|
1452
|
+
f"ultracode: run {ultracode} (the $ultracode-for-codex Codex skill) "
|
|
1453
|
+
f"for {review_family} workflow-orchestration review."
|
|
1454
|
+
)
|
|
1455
|
+
else:
|
|
1456
|
+
parts.append(
|
|
1457
|
+
f"ultracode: run {ultracode} --effort ultracode -p <self-contained review packet> "
|
|
1458
|
+
f"(Claude Code /workflows ultracode mode, headless) for {review_family} "
|
|
1459
|
+
"workflow-orchestration review."
|
|
1460
|
+
)
|
|
1461
|
+
if dropped:
|
|
1462
|
+
detail = [
|
|
1463
|
+
f"onto (add a [hosts.{review_host}].onto_review pin)"
|
|
1464
|
+
if route == "onto" and not plan.get("review_onto")
|
|
1465
|
+
else route
|
|
1466
|
+
for route in dropped
|
|
1467
|
+
]
|
|
1468
|
+
parts.append(f"Unavailable cross-family route(s) at launch: {', '.join(detail)}.")
|
|
1469
|
+
if floor == "native":
|
|
1470
|
+
parts.append(
|
|
1471
|
+
"No cross-family route resolved at launch; using same-family native "
|
|
1472
|
+
"subagent review labeled PROPOSED (family collapse)."
|
|
1473
|
+
)
|
|
1474
|
+
parts.append(
|
|
1475
|
+
"Cross-family reviewers are dispatched as read-only subprocesses, not "
|
|
1476
|
+
"CLI-native subagents; spawning them needs this main's execution policy to "
|
|
1477
|
+
"permit subprocesses, so a read-only or restrictive policy blocks the dispatch "
|
|
1478
|
+
"and collapses to same-family native. If a route is unavailable or "
|
|
1479
|
+
"unauthenticated at use time, fall back to native same-model subagent review "
|
|
1480
|
+
"via the configured child agents and label those verdicts PROPOSED (family collapse)."
|
|
1481
|
+
)
|
|
1482
|
+
return " ".join(parts)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def run_contract(plan: dict[str, Any]) -> str:
|
|
1486
|
+
bindings = ", ".join(
|
|
1487
|
+
f"{tier}={data['model']}/{tier_effort(plan, tier)}"
|
|
1488
|
+
for tier, data in plan["tiers"].items()
|
|
1489
|
+
)
|
|
1490
|
+
requested = plan["review_setup"]
|
|
1491
|
+
family = plan.get("review_family", "cross")
|
|
1492
|
+
effective, dropped, floor = effective_review(plan)
|
|
1493
|
+
if family == "same":
|
|
1494
|
+
route = _same_review_route(plan, effective, dropped)
|
|
1495
|
+
else:
|
|
1496
|
+
route = _cross_review_route(plan, effective, dropped, floor)
|
|
1497
|
+
authority = (
|
|
1498
|
+
"Main and native child model/effort defaults are config-projected. Use the installed "
|
|
1499
|
+
"codex-run adapter when a separate child root or stricter reach boundary matters."
|
|
1500
|
+
if plan["host"] == "codex"
|
|
1501
|
+
else "Main and child model/effort defaults are CLI-projected."
|
|
1502
|
+
)
|
|
1503
|
+
return (
|
|
1504
|
+
f"LaunchPlan: main={plan['main_tier']} ({plan['tiers'][plan['main_tier']]['model']}/"
|
|
1505
|
+
f"{tier_effort(plan, plan['main_tier'])}); tiers: {bindings}. "
|
|
1506
|
+
f"Delegation={'on' if plan['delegation'] else 'off'}. Review family={family}. "
|
|
1507
|
+
f"Review setup={requested}: {route} "
|
|
1508
|
+
f"{authority} Forwarded backend arguments are expert overrides and may supersede these "
|
|
1509
|
+
"defaults. Status is configured/requested; completion is not enforced by "
|
|
1510
|
+
"this interactive launcher."
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def claude_agents(plan: dict[str, Any]) -> str:
|
|
1515
|
+
roles = {}
|
|
1516
|
+
for tier in TIER_ORDER:
|
|
1517
|
+
binding = plan["tiers"][tier]
|
|
1518
|
+
roles[tier] = {
|
|
1519
|
+
"description": f"{tier.upper()} tier: {binding['model']} at {tier_effort(plan, tier)}",
|
|
1520
|
+
"prompt": (
|
|
1521
|
+
f"Act as the bounded {tier.upper()} role at requested effort "
|
|
1522
|
+
f"{tier_effort(plan, tier)}. Return evidence and verification; stay in scope."
|
|
1523
|
+
),
|
|
1524
|
+
"model": binding["model"],
|
|
1525
|
+
"effort": tier_effort(plan, tier),
|
|
1526
|
+
}
|
|
1527
|
+
return json.dumps(roles, separators=(",", ":"))
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[str]:
|
|
1531
|
+
host = plan["host"]
|
|
1532
|
+
main = plan["tiers"][plan["main_tier"]]
|
|
1533
|
+
main_effort = tier_effort(plan, plan["main_tier"])
|
|
1534
|
+
contract = run_contract(plan)
|
|
1535
|
+
effective_routes, _, _ = effective_review(plan)
|
|
1536
|
+
if host == "codex":
|
|
1537
|
+
args = [
|
|
1538
|
+
"--model", main["model"],
|
|
1539
|
+
"-c", f'model_reasoning_effort="{main_effort}"',
|
|
1540
|
+
"-c", f"developer_instructions={json.dumps(contract)}",
|
|
1541
|
+
"-c", f"features.multi_agent={'true' if plan['delegation'] else 'false'}",
|
|
1542
|
+
]
|
|
1543
|
+
policy = plan["codex_execution_policy"]
|
|
1544
|
+
policy_args = (
|
|
1545
|
+
["--dangerously-bypass-approvals-and-sandbox"]
|
|
1546
|
+
if policy == "bypass"
|
|
1547
|
+
else ["--sandbox", policy]
|
|
1548
|
+
)
|
|
1549
|
+
args += policy_args
|
|
1550
|
+
if plan["delegation"]:
|
|
1551
|
+
for tier, (path, description) in codex_agent_configs(plan, materialize_agents).items():
|
|
1552
|
+
args += [
|
|
1553
|
+
"-c", f"agents.{tier}.description={json.dumps(description)}",
|
|
1554
|
+
"-c", f"agents.{tier}.config_file={json.dumps(str(path))}",
|
|
1555
|
+
]
|
|
1556
|
+
if "onto" in effective_routes:
|
|
1557
|
+
onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
|
|
1558
|
+
args += [
|
|
1559
|
+
"-c", "mcp_servers.onto.enabled=true",
|
|
1560
|
+
"-c", f"mcp_servers.onto.command={json.dumps(onto_command)}",
|
|
1561
|
+
"-c", 'mcp_servers.onto.args=["mcp"]',
|
|
1562
|
+
]
|
|
1563
|
+
return args
|
|
1564
|
+
args = [
|
|
1565
|
+
"--model", main["model"],
|
|
1566
|
+
"--effort", main_effort,
|
|
1567
|
+
"--append-system-prompt", contract,
|
|
1568
|
+
]
|
|
1569
|
+
if plan["delegation"]:
|
|
1570
|
+
args += ["--agents", claude_agents(plan)]
|
|
1571
|
+
policy = plan["claude_permission_mode"]
|
|
1572
|
+
policy_args = (
|
|
1573
|
+
["--dangerously-skip-permissions"]
|
|
1574
|
+
if policy == "bypassPermissions"
|
|
1575
|
+
else ["--permission-mode", policy]
|
|
1576
|
+
)
|
|
1577
|
+
args += policy_args
|
|
1578
|
+
if "onto" in effective_routes:
|
|
1579
|
+
onto_command = resolve_command(plan["capabilities"]["onto"]["command"])
|
|
1580
|
+
mcp_config = {
|
|
1581
|
+
"mcpServers": {"onto": {"command": onto_command, "args": ["mcp"]}}
|
|
1582
|
+
}
|
|
1583
|
+
args += [
|
|
1584
|
+
"--mcp-config",
|
|
1585
|
+
json.dumps(mcp_config, separators=(",", ":")),
|
|
1586
|
+
]
|
|
1587
|
+
return args
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def print_summary(
|
|
1591
|
+
plan: dict[str, Any],
|
|
1592
|
+
command: str,
|
|
1593
|
+
args: list[str],
|
|
1594
|
+
stream: Any = sys.stdout,
|
|
1595
|
+
forwarded_args: bool = False,
|
|
1596
|
+
) -> None:
|
|
1597
|
+
main = plan["tiers"][plan["main_tier"]]
|
|
1598
|
+
print("\nLaunch summary", file=stream)
|
|
1599
|
+
print(f" Host {plan['host']}", file=stream)
|
|
1600
|
+
print(f" Preset {plan['label']}", file=stream)
|
|
1601
|
+
print(
|
|
1602
|
+
f" Main {plan['main_tier'].upper()} · {main['model']} · "
|
|
1603
|
+
f"{tier_effort(plan, plan['main_tier'])}",
|
|
1604
|
+
file=stream,
|
|
1605
|
+
)
|
|
1606
|
+
for tier in TIER_ORDER:
|
|
1607
|
+
binding = plan["tiers"][tier]
|
|
1608
|
+
effort = tier_effort(plan, tier)
|
|
1609
|
+
print(f" {tier.upper():<14} {binding['model']} · {effort}", file=stream)
|
|
1610
|
+
effective, dropped, floor = effective_review(plan)
|
|
1611
|
+
family = plan.get("review_family", "cross")
|
|
1612
|
+
review_display = plan["review_setup"]
|
|
1613
|
+
if family == "cross":
|
|
1614
|
+
review_display += f" · cross-family review on {plan['review_host']}"
|
|
1615
|
+
if effective:
|
|
1616
|
+
review_display += f" via {'+'.join(effective)}"
|
|
1617
|
+
if dropped:
|
|
1618
|
+
review_display += f" (dropped {','.join(dropped)})"
|
|
1619
|
+
if floor == "native":
|
|
1620
|
+
review_display += " → same-family native PROPOSED"
|
|
1621
|
+
elif dropped:
|
|
1622
|
+
review_display += (
|
|
1623
|
+
f" → effective {'+'.join(effective) or 'none'} "
|
|
1624
|
+
f"({','.join(dropped)} unavailable)"
|
|
1625
|
+
)
|
|
1626
|
+
print(
|
|
1627
|
+
f" Review setup {review_display} · "
|
|
1628
|
+
"configured/requested · completed: not enforced",
|
|
1629
|
+
file=stream,
|
|
1630
|
+
)
|
|
1631
|
+
tier_authority = (
|
|
1632
|
+
"base main + native child model/effort configured"
|
|
1633
|
+
if plan["host"] == "codex"
|
|
1634
|
+
else "base main + child model/effort configured"
|
|
1635
|
+
)
|
|
1636
|
+
print(f" Tier authority {tier_authority}", file=stream)
|
|
1637
|
+
if forwarded_args:
|
|
1638
|
+
print(
|
|
1639
|
+
" Overrides forwarded backend args appended last; may supersede defaults",
|
|
1640
|
+
file=stream,
|
|
1641
|
+
)
|
|
1642
|
+
if plan["host"] == "codex":
|
|
1643
|
+
print(f" Execution Codex {plan['codex_execution_policy']}", file=stream)
|
|
1644
|
+
else:
|
|
1645
|
+
print(
|
|
1646
|
+
f" Execution Claude {plan['claude_permission_mode']} "
|
|
1647
|
+
"(permission mode, not OS sandbox)",
|
|
1648
|
+
file=stream,
|
|
1649
|
+
)
|
|
1650
|
+
print(f" Backend {command}", file=stream)
|
|
1651
|
+
if os.environ.get("AGENT_LAUNCH_DEBUG") == "1":
|
|
1652
|
+
print(" Argv " + json.dumps([command, *args]), file=stream)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
1656
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
1657
|
+
parser.add_argument("--config", type=pathlib.Path, default=default_config_path())
|
|
1658
|
+
parser.add_argument(
|
|
1659
|
+
"--no-tui",
|
|
1660
|
+
action="store_true",
|
|
1661
|
+
help="disable the rich terminal preflight; configured-launch flags still apply",
|
|
1662
|
+
)
|
|
1663
|
+
parser.add_argument("--preset", help="launch a named preset without the preset picker")
|
|
1664
|
+
parser.add_argument(
|
|
1665
|
+
"--custom",
|
|
1666
|
+
action="store_true",
|
|
1667
|
+
help="open customization after preset selection",
|
|
1668
|
+
)
|
|
1669
|
+
parser.add_argument("--yes", action="store_true", help="skip launch confirmation")
|
|
1670
|
+
parser.add_argument("--dry-run", action="store_true", help="print projection without launching")
|
|
1671
|
+
parser.add_argument("host", choices=("codex", "claude"))
|
|
1672
|
+
parser.add_argument("forward", nargs=argparse.REMAINDER)
|
|
1673
|
+
args = parser.parse_args(argv)
|
|
1674
|
+
if args.forward[:1] == ["--"]:
|
|
1675
|
+
args.forward = args.forward[1:]
|
|
1676
|
+
return args
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def main(argv: list[str]) -> int:
|
|
1680
|
+
args = parse_args(argv)
|
|
1681
|
+
config_path = args.config.expanduser()
|
|
1682
|
+
config = load_config(config_path)
|
|
1683
|
+
command, passthrough = resolve_backend(config, args.host)
|
|
1684
|
+
tty = sys.stdin.isatty() and sys.stdout.isatty()
|
|
1685
|
+
if not tty and args.dry_run and not args.preset and not args.custom:
|
|
1686
|
+
if "balanced" not in config["presets"]:
|
|
1687
|
+
raise LaunchError(
|
|
1688
|
+
"bare non-TTY --dry-run requires a 'balanced' preset; use --preset NAME"
|
|
1689
|
+
)
|
|
1690
|
+
args.preset = "balanced"
|
|
1691
|
+
bypass = args.no_tui or bool(args.forward) or os.environ.get("AGENT_LAUNCH_TUI") == "0"
|
|
1692
|
+
if (bypass or not tty) and not args.preset and not args.custom and not args.dry_run:
|
|
1693
|
+
exec_backend(command, [*passthrough, *args.forward])
|
|
1694
|
+
|
|
1695
|
+
interactive_setup = not args.preset or args.custom
|
|
1696
|
+
use_textual = (
|
|
1697
|
+
interactive_setup
|
|
1698
|
+
and tty
|
|
1699
|
+
and os.environ.get("TERM", "") not in {"", "dumb"}
|
|
1700
|
+
)
|
|
1701
|
+
if use_textual and not textual_importable():
|
|
1702
|
+
maybe_reexec_into_venv()
|
|
1703
|
+
if not textual_importable():
|
|
1704
|
+
use_textual = False
|
|
1705
|
+
print(
|
|
1706
|
+
"agent-launch: rich terminal UI unavailable; using numbered prompts.",
|
|
1707
|
+
file=sys.stderr,
|
|
1708
|
+
)
|
|
1709
|
+
if use_textual:
|
|
1710
|
+
plan = run_textual_flow(
|
|
1711
|
+
config, args.host, args.preset, args.custom, config_path
|
|
1712
|
+
)
|
|
1713
|
+
else:
|
|
1714
|
+
plan = select_plan(
|
|
1715
|
+
config, args.host, args.preset, args.custom, config_path=config_path
|
|
1716
|
+
)
|
|
1717
|
+
validate_review_setup(plan)
|
|
1718
|
+
projected = [*project_args(plan, materialize_agents=not args.dry_run), *args.forward]
|
|
1719
|
+
summary_stream = sys.stdout if tty or args.dry_run else sys.stderr
|
|
1720
|
+
print_summary(plan, command, projected, summary_stream, bool(args.forward))
|
|
1721
|
+
if args.dry_run:
|
|
1722
|
+
print(json.dumps([command, *projected], ensure_ascii=False))
|
|
1723
|
+
return 0
|
|
1724
|
+
if tty and not args.yes and not plan.get("_launch_confirmed", False):
|
|
1725
|
+
confirmation = read_input("Launch? [Y/n/q]: ").strip().lower()
|
|
1726
|
+
if confirmation in {"n", "no", "q", "quit"}:
|
|
1727
|
+
return 130
|
|
1728
|
+
env = os.environ.copy()
|
|
1729
|
+
env["AGENT_LAUNCH_ACTIVE"] = "1"
|
|
1730
|
+
summary_stream.flush()
|
|
1731
|
+
exec_backend(command, projected, env)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
if __name__ == "__main__":
|
|
1735
|
+
try:
|
|
1736
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
1737
|
+
except LaunchError as exc:
|
|
1738
|
+
print(f"agent-launch: {exc}", file=sys.stderr)
|
|
1739
|
+
raise SystemExit(2)
|
|
1740
|
+
except KeyboardInterrupt:
|
|
1741
|
+
print("\nCancelled.", file=sys.stderr)
|
|
1742
|
+
raise SystemExit(130)
|