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.
- package/DEPENDENCIES.md +19 -19
- package/README.md +34 -11
- package/claude/CLAUDE.md +2 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +19 -1
- package/claude/guides/coding-staged-workflow.md +32 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/learning-flow.md +5 -5
- package/claude/guides/llm-capability-boundary.md +6 -1
- package/claude/guides/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +16 -0
- package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
- package/claude/hooks/tooling-gotchas-hook.py +7 -0
- package/codex/AGENTS.md +2 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +19 -1
- package/codex/guides/coding-staged-workflow.md +32 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/learning-flow.md +5 -5
- package/codex/guides/llm-capability-boundary.md +6 -1
- package/codex/guides/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +16 -0
- package/{scripts → compose}/assemble.py +184 -16
- package/{scripts → compose}/canary.sh +14 -5
- package/{scripts → compose}/check-domains.py +9 -3
- package/{config → compose}/domains.json +1 -0
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/{scripts → compose}/register-hooks.py +3 -3
- package/{scripts/install.sh → install.sh} +401 -104
- package/launch/agent-launch.py +5294 -0
- package/launch/agent-launch.toml +376 -0
- package/{scripts → launch}/check-prompting-targets.sh +1 -1
- package/{scripts → launch}/provision-venv.sh +1 -1
- package/{scripts → learn}/check-learning.py +7 -7
- package/{scripts → learn}/collect-learning.py +10 -10
- package/{config → learn}/learning.schema.json +3 -3
- package/{scripts → learn}/migrate-learnings.py +95 -54
- package/{scripts → learn}/redact.py +4 -4
- package/package.json +25 -24
- package/wrappers/claude-run.sh +162 -0
- package/{scripts → wrappers}/codex-run.sh +62 -6
- package/config/agent-launch.toml +0 -143
- package/scripts/agent-launch.py +0 -2350
- package/scripts/check-parity.sh +0 -2003
- /package/{shell → launch}/agent-launch.zsh +0 -0
- /package/{config → learn}/promotions.json +0 -0
- /package/{scripts/session-cost.py → session-cost.py} +0 -0
- /package/{scripts → wrappers}/codex-helm.sh +0 -0
package/scripts/agent-launch.py
DELETED
|
@@ -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)
|