claude-dev-env 2.36.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/validators/README.md +35 -1
- package/package.json +1 -1
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/adapter_detectors.py +42 -2
- package/scripts/policy_lint/adapters.py +24 -0
- package/scripts/policy_lint/config/constants.py +11 -0
- package/scripts/policy_lint/engine.py +1 -1
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +10 -1
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +25 -1
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Bind and consult a read-only Codex CLI session through Astra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_scripts_directory = Path(__file__).resolve().parent
|
|
16
|
+
_config_directory = _scripts_directory / "config"
|
|
17
|
+
for each_import_directory in (_scripts_directory, _config_directory):
|
|
18
|
+
each_import_directory_text = str(each_import_directory)
|
|
19
|
+
if each_import_directory_text not in sys.path:
|
|
20
|
+
sys.path[:0] = [each_import_directory_text]
|
|
21
|
+
|
|
22
|
+
from advisor_scripts_constants.astra_advisor_constants import (
|
|
23
|
+
ADVISOR_CODEX_EXECUTABLE_ENV_VAR,
|
|
24
|
+
ALL_ASTRA_TRUTHY_VALUES,
|
|
25
|
+
ASTRA_BIND_FAILURE_REASON,
|
|
26
|
+
ASTRA_CODEX_TIMEOUT_REASON,
|
|
27
|
+
ASTRA_CODEX_TIMEOUT_SECONDS,
|
|
28
|
+
ASTRA_EFFORT_FLAG,
|
|
29
|
+
ASTRA_ENABLE_FLAG,
|
|
30
|
+
ASTRA_ENV_VAR,
|
|
31
|
+
ASTRA_EXECUTABLE_NOT_FOUND_REASON,
|
|
32
|
+
ASTRA_FALLBACK_KIND_BROKEN,
|
|
33
|
+
ASTRA_FALLBACK_KIND_DECLINED,
|
|
34
|
+
ASTRA_SESSION_ID_METAVAR,
|
|
35
|
+
CLAUDE_CONFIG_DIRECTORY_NAME,
|
|
36
|
+
CODEX_CONFIG_FLAG,
|
|
37
|
+
CODEX_EXECUTABLE,
|
|
38
|
+
CODEX_EXEC_SUBCOMMAND,
|
|
39
|
+
CODEX_JSON_FLAG,
|
|
40
|
+
CODEX_MODEL_FLAG,
|
|
41
|
+
CODEX_PROMPT_FROM_STDIN,
|
|
42
|
+
CODEX_READ_ONLY_SANDBOX,
|
|
43
|
+
CODEX_REASONING_CONFIG_TEMPLATE,
|
|
44
|
+
CODEX_RESUME_SUBCOMMAND,
|
|
45
|
+
CODEX_SANDBOX_FLAG,
|
|
46
|
+
USAGE_PROBE_FILENAME,
|
|
47
|
+
USAGE_PROBE_PACKAGE_DIRECTORY_NAME,
|
|
48
|
+
USAGE_PROBE_SCRIPTS_DIRECTORY_NAME,
|
|
49
|
+
USAGE_PROBE_SHARED_DIRECTORY_NAME,
|
|
50
|
+
)
|
|
51
|
+
from advisor_scripts_constants.advisor_route_constants import (
|
|
52
|
+
ADVISOR_CODEX_MODEL_ID,
|
|
53
|
+
ADVISOR_EFFORT_DEFAULT,
|
|
54
|
+
ADVISOR_EFFORT_ENV_VAR,
|
|
55
|
+
ALL_ADVISOR_EFFORT_LEVELS,
|
|
56
|
+
SPAWN_OUTCOME_KEY,
|
|
57
|
+
)
|
|
58
|
+
from codex_astra_reply import (
|
|
59
|
+
CodexAstraAdvisorReply,
|
|
60
|
+
build_fallback_reply,
|
|
61
|
+
parse_codex_jsonl_reply,
|
|
62
|
+
)
|
|
63
|
+
from codex_astra_preflight import AstraPreflight, run_astra_preflight
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolved_settings(
|
|
67
|
+
all_settings: Mapping[str, str] | None,
|
|
68
|
+
) -> Mapping[str, str]:
|
|
69
|
+
return os.environ if all_settings is None else all_settings
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_astra_advisor_enabled(all_settings: Mapping[str, str] | None) -> bool:
|
|
73
|
+
"""Return whether the Astra advisor flag is on.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
all_settings: Environment mapping, or None to read os.environ.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True when the flag holds a truthy value.
|
|
80
|
+
"""
|
|
81
|
+
raw_setting = _resolved_settings(all_settings).get(ASTRA_ENV_VAR, "")
|
|
82
|
+
return raw_setting.strip().lower() in ALL_ASTRA_TRUTHY_VALUES
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_advisor_effort(all_settings: Mapping[str, str] | None) -> str:
|
|
86
|
+
"""Return ADVISOR_EFFORT, or low when the value is missing or unknown.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
all_settings: Environment mapping, or None to read os.environ.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The requested effort, or low.
|
|
93
|
+
"""
|
|
94
|
+
requested = _resolved_settings(all_settings).get(ADVISOR_EFFORT_ENV_VAR, "")
|
|
95
|
+
normalized = requested.strip().lower()
|
|
96
|
+
return normalized if normalized in ALL_ADVISOR_EFFORT_LEVELS else ADVISOR_EFFORT_DEFAULT
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_usage_probe_path(home_directory: Path) -> Path:
|
|
100
|
+
"""Return the Codex usage-probe path under home_directory.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
home_directory: Home directory that holds the installed probe.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Path to the usage probe under that home.
|
|
107
|
+
"""
|
|
108
|
+
return (
|
|
109
|
+
home_directory
|
|
110
|
+
/ CLAUDE_CONFIG_DIRECTORY_NAME
|
|
111
|
+
/ USAGE_PROBE_SHARED_DIRECTORY_NAME
|
|
112
|
+
/ USAGE_PROBE_PACKAGE_DIRECTORY_NAME
|
|
113
|
+
/ USAGE_PROBE_SCRIPTS_DIRECTORY_NAME
|
|
114
|
+
/ USAGE_PROBE_FILENAME
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_codex_executable(all_settings: Mapping[str, str] | None) -> str | None:
|
|
119
|
+
"""Return the Codex executable from the override or PATH.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
all_settings: Environment mapping, or None to read os.environ.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Executable path or name, or None if neither is set.
|
|
126
|
+
"""
|
|
127
|
+
override = _resolved_settings(all_settings).get(ADVISOR_CODEX_EXECUTABLE_ENV_VAR, "").strip()
|
|
128
|
+
return override or shutil.which(CODEX_EXECUTABLE)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_codex_arguments(
|
|
132
|
+
codex_executable: str,
|
|
133
|
+
session_id: str | None = None,
|
|
134
|
+
reasoning_effort: str = ADVISOR_EFFORT_DEFAULT,
|
|
135
|
+
) -> list[str]:
|
|
136
|
+
"""Build the Codex command list for bind or resume.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
codex_executable: Codex executable path or name.
|
|
140
|
+
session_id: Session to resume, or None for a new bind.
|
|
141
|
+
reasoning_effort: Codex reasoning effort.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Command arguments for Codex exec.
|
|
145
|
+
"""
|
|
146
|
+
arguments = [
|
|
147
|
+
codex_executable,
|
|
148
|
+
CODEX_EXEC_SUBCOMMAND,
|
|
149
|
+
CODEX_MODEL_FLAG,
|
|
150
|
+
ADVISOR_CODEX_MODEL_ID,
|
|
151
|
+
CODEX_CONFIG_FLAG,
|
|
152
|
+
CODEX_REASONING_CONFIG_TEMPLATE.format(effort=reasoning_effort),
|
|
153
|
+
CODEX_SANDBOX_FLAG,
|
|
154
|
+
CODEX_READ_ONLY_SANDBOX,
|
|
155
|
+
CODEX_JSON_FLAG,
|
|
156
|
+
]
|
|
157
|
+
if session_id is not None:
|
|
158
|
+
arguments.extend([CODEX_RESUME_SUBCOMMAND, session_id])
|
|
159
|
+
arguments.append(CODEX_PROMPT_FROM_STDIN)
|
|
160
|
+
return arguments
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _resolve_preflight(
|
|
164
|
+
preflight: AstraPreflight | None,
|
|
165
|
+
probe_path: Path | None,
|
|
166
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
167
|
+
) -> AstraPreflight:
|
|
168
|
+
if preflight is not None:
|
|
169
|
+
return preflight
|
|
170
|
+
resolved_path = resolve_usage_probe_path(Path.home()) if probe_path is None else probe_path
|
|
171
|
+
return run_astra_preflight(resolved_path, process_runner)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _run_codex(
|
|
175
|
+
prompt: str,
|
|
176
|
+
working_directory: Path,
|
|
177
|
+
session_id: str | None,
|
|
178
|
+
all_settings: Mapping[str, str] | None,
|
|
179
|
+
executable: str,
|
|
180
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
181
|
+
) -> subprocess.CompletedProcess[str]:
|
|
182
|
+
return process_runner(
|
|
183
|
+
build_codex_arguments(
|
|
184
|
+
executable,
|
|
185
|
+
session_id=session_id,
|
|
186
|
+
reasoning_effort=resolve_advisor_effort(all_settings),
|
|
187
|
+
),
|
|
188
|
+
cwd=str(working_directory),
|
|
189
|
+
input=prompt,
|
|
190
|
+
capture_output=True,
|
|
191
|
+
text=True,
|
|
192
|
+
check=False,
|
|
193
|
+
shell=False,
|
|
194
|
+
timeout=ASTRA_CODEX_TIMEOUT_SECONDS,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _run_enabled_advisor(
|
|
199
|
+
prompt: str,
|
|
200
|
+
working_directory: Path,
|
|
201
|
+
setting_by_name: Mapping[str, str] | None,
|
|
202
|
+
session_id: str | None,
|
|
203
|
+
executable: str,
|
|
204
|
+
resolved_preflight: AstraPreflight,
|
|
205
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
206
|
+
) -> CodexAstraAdvisorReply:
|
|
207
|
+
if not resolved_preflight.eligible:
|
|
208
|
+
return build_fallback_reply(resolved_preflight.reason, True, resolved_preflight.fallback_kind)
|
|
209
|
+
try:
|
|
210
|
+
completed = _run_codex(
|
|
211
|
+
prompt, working_directory, session_id, setting_by_name, executable, process_runner
|
|
212
|
+
)
|
|
213
|
+
except subprocess.TimeoutExpired as error:
|
|
214
|
+
return build_fallback_reply(f"{ASTRA_CODEX_TIMEOUT_REASON}: {error}", True)
|
|
215
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
216
|
+
return build_fallback_reply(f"{ASTRA_BIND_FAILURE_REASON}: {error}", True)
|
|
217
|
+
if completed.returncode != 0:
|
|
218
|
+
return build_fallback_reply(
|
|
219
|
+
f"{ASTRA_BIND_FAILURE_REASON}: process exit {completed.returncode}", True
|
|
220
|
+
)
|
|
221
|
+
return parse_codex_jsonl_reply(completed.stdout, session_id, True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def run_codex_astra_advisor(
|
|
225
|
+
prompt: str,
|
|
226
|
+
working_directory: Path,
|
|
227
|
+
preflight: AstraPreflight | None,
|
|
228
|
+
probe_path: Path | None,
|
|
229
|
+
setting_by_name: Mapping[str, str] | None,
|
|
230
|
+
session_id: str | None,
|
|
231
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
232
|
+
) -> CodexAstraAdvisorReply:
|
|
233
|
+
"""Bind or resume a read-only Codex Astra advisor.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
prompt: Advisor prompt text.
|
|
237
|
+
working_directory: Working directory for Codex.
|
|
238
|
+
preflight: Usage probe result, or None to run the probe.
|
|
239
|
+
probe_path: Usage-probe path, or None for the installed path.
|
|
240
|
+
setting_by_name: Environment mapping, or None to read os.environ.
|
|
241
|
+
session_id: Session to resume, or None for a new bind.
|
|
242
|
+
process_runner: Callable that runs the probe and Codex.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Advisor reply, or a fallback.
|
|
246
|
+
"""
|
|
247
|
+
if not is_astra_advisor_enabled(setting_by_name):
|
|
248
|
+
return build_fallback_reply("Astra advisor flag is disabled", False, ASTRA_FALLBACK_KIND_DECLINED)
|
|
249
|
+
executable = resolve_codex_executable(setting_by_name)
|
|
250
|
+
if executable is None:
|
|
251
|
+
return build_fallback_reply(ASTRA_EXECUTABLE_NOT_FOUND_REASON, True)
|
|
252
|
+
resolved_preflight = _resolve_preflight(preflight, probe_path, process_runner)
|
|
253
|
+
return _run_enabled_advisor(prompt, working_directory, setting_by_name, session_id, executable, resolved_preflight, process_runner)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def build_argument_parser() -> argparse.ArgumentParser:
|
|
257
|
+
"""Build the command-line parser for bind and resume.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Argument parser for this helper.
|
|
261
|
+
"""
|
|
262
|
+
parser = argparse.ArgumentParser(description="Bind or consult a read-only Codex Astra advisor.")
|
|
263
|
+
mode_group = parser.add_mutually_exclusive_group(required=True)
|
|
264
|
+
mode_group.add_argument("--bind", action="store_true")
|
|
265
|
+
mode_group.add_argument("--resume", metavar=ASTRA_SESSION_ID_METAVAR)
|
|
266
|
+
parser.add_argument("--cwd", required=True, type=Path)
|
|
267
|
+
parser.add_argument(ASTRA_ENABLE_FLAG, dest="is_astra_requested", action="store_true")
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
ASTRA_EFFORT_FLAG,
|
|
270
|
+
dest="astra_effort",
|
|
271
|
+
choices=ALL_ADVISOR_EFFORT_LEVELS,
|
|
272
|
+
default=None,
|
|
273
|
+
)
|
|
274
|
+
return parser
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main(all_cli_arguments: Sequence[str]) -> int:
|
|
278
|
+
"""Run one bind or resume request from stdin.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
all_cli_arguments: CLI arguments after the program name.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
0 when the advisor reply succeeds, 1 on fallback.
|
|
285
|
+
"""
|
|
286
|
+
parsed = build_argument_parser().parse_args(list(all_cli_arguments))
|
|
287
|
+
all_settings = dict(os.environ)
|
|
288
|
+
if parsed.is_astra_requested:
|
|
289
|
+
all_settings[ASTRA_ENV_VAR] = "1"
|
|
290
|
+
if parsed.astra_effort is not None:
|
|
291
|
+
all_settings[ADVISOR_EFFORT_ENV_VAR] = parsed.astra_effort
|
|
292
|
+
reply = run_codex_astra_advisor(
|
|
293
|
+
sys.stdin.read(),
|
|
294
|
+
parsed.cwd,
|
|
295
|
+
None,
|
|
296
|
+
None,
|
|
297
|
+
all_settings,
|
|
298
|
+
parsed.resume if not parsed.bind else None,
|
|
299
|
+
subprocess.run,
|
|
300
|
+
)
|
|
301
|
+
payload = asdict(reply)
|
|
302
|
+
payload[SPAWN_OUTCOME_KEY] = payload.pop("outcome")
|
|
303
|
+
sys.stdout.write(f"{json.dumps(payload, sort_keys=True)}\n")
|
|
304
|
+
return 0 if reply.successful else 1
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Evaluate Codex usage before an Astra advisor request."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from advisor_scripts_constants.astra_advisor_constants import (
|
|
15
|
+
ASTRA_FALLBACK_KIND_BROKEN,
|
|
16
|
+
ASTRA_FALLBACK_KIND_DECLINED,
|
|
17
|
+
ASTRA_PREFLIGHT_FAILURE_REASON,
|
|
18
|
+
ASTRA_PROBE_TIMEOUT_REASON,
|
|
19
|
+
ASTRA_USAGE_PROBE_TIMEOUT_SECONDS,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AstraPreflight:
|
|
25
|
+
eligible: bool
|
|
26
|
+
percent_left: float | None
|
|
27
|
+
reason: str
|
|
28
|
+
fallback_kind: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _preflight_fallback(
|
|
32
|
+
reason: str, percent_left: float | None, fallback_kind: str
|
|
33
|
+
) -> AstraPreflight:
|
|
34
|
+
return AstraPreflight(False, percent_left, reason, fallback_kind)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_usage_gate(probe_path: Path) -> Callable[[float], bool]:
|
|
38
|
+
probe_directory = str(probe_path.parent)
|
|
39
|
+
if probe_directory not in sys.path:
|
|
40
|
+
sys.path.insert(0, probe_directory)
|
|
41
|
+
return importlib.import_module("codex_usage_probe").is_codex_review_required
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_probe_percent(stdout_text: str) -> tuple[float | None, str | None]:
|
|
45
|
+
try:
|
|
46
|
+
report = json.loads(stdout_text)
|
|
47
|
+
except (TypeError, json.JSONDecodeError):
|
|
48
|
+
return None, "usage report is malformed"
|
|
49
|
+
if not isinstance(report, dict):
|
|
50
|
+
return None, "usage report is malformed"
|
|
51
|
+
raw_percent = report.get("percent_left")
|
|
52
|
+
if isinstance(raw_percent, bool) or not isinstance(raw_percent, (int, float)):
|
|
53
|
+
return None, "usage meter is unknown" if raw_percent is None else "usage meter is malformed"
|
|
54
|
+
percent_left = float(raw_percent)
|
|
55
|
+
if not math.isfinite(percent_left):
|
|
56
|
+
return None, "usage meter is malformed"
|
|
57
|
+
return percent_left, None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run_probe(
|
|
61
|
+
probe_path: Path,
|
|
62
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
63
|
+
) -> subprocess.CompletedProcess[str]:
|
|
64
|
+
return process_runner(
|
|
65
|
+
[sys.executable, str(probe_path)],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
check=False,
|
|
69
|
+
shell=False,
|
|
70
|
+
timeout=ASTRA_USAGE_PROBE_TIMEOUT_SECONDS,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _preflight_from_probe(
|
|
75
|
+
probe_path: Path, completed: subprocess.CompletedProcess[str]
|
|
76
|
+
) -> AstraPreflight:
|
|
77
|
+
if completed.returncode != 0:
|
|
78
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: probe exit {completed.returncode}"
|
|
79
|
+
return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_BROKEN)
|
|
80
|
+
percent_left, parse_reason = _parse_probe_percent(completed.stdout)
|
|
81
|
+
if parse_reason is not None or percent_left is None:
|
|
82
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {parse_reason or 'usage meter is unknown'}"
|
|
83
|
+
return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_BROKEN)
|
|
84
|
+
if not _load_usage_gate(probe_path)(percent_left):
|
|
85
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: usage meter is at or below the gate"
|
|
86
|
+
return _preflight_fallback(reason, percent_left, ASTRA_FALLBACK_KIND_DECLINED)
|
|
87
|
+
return AstraPreflight(True, percent_left, "usage meter is above the Astra gate")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_astra_preflight(
|
|
91
|
+
probe_path: Path,
|
|
92
|
+
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
93
|
+
) -> AstraPreflight:
|
|
94
|
+
"""Run the usage probe and decide whether Astra may bind.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
probe_path: Path to the installed usage probe.
|
|
98
|
+
process_runner: Callable that runs the probe.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Eligibility, remaining usage percent, reason, and fallback kind.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
completed = _run_probe(probe_path, process_runner)
|
|
105
|
+
return _preflight_from_probe(probe_path, completed)
|
|
106
|
+
except subprocess.TimeoutExpired as error:
|
|
107
|
+
return _preflight_fallback(
|
|
108
|
+
f"{ASTRA_PROBE_TIMEOUT_REASON}: {error}", None, ASTRA_FALLBACK_KIND_BROKEN
|
|
109
|
+
)
|
|
110
|
+
except (OSError, subprocess.SubprocessError, ImportError, AttributeError, TypeError, ValueError) as error:
|
|
111
|
+
return _preflight_fallback(
|
|
112
|
+
f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {error}",
|
|
113
|
+
None,
|
|
114
|
+
ASTRA_FALLBACK_KIND_BROKEN,
|
|
115
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Parse Codex JSONL and build Astra advisor replies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from advisor_scripts_constants.advisor_route_constants import (
|
|
9
|
+
ADVISOR_FALLBACK_RESULT,
|
|
10
|
+
ADVISOR_FALLBACK_TIER,
|
|
11
|
+
ADVISOR_MODEL_TIER,
|
|
12
|
+
ALL_ADVISOR_GUIDANCE_SIGNALS,
|
|
13
|
+
CODEX_BIND_SUCCESS_TOKEN,
|
|
14
|
+
)
|
|
15
|
+
from advisor_scripts_constants.astra_advisor_constants import (
|
|
16
|
+
ASTRA_FALLBACK_KIND_BROKEN,
|
|
17
|
+
ASTRA_INVALID_SIGNAL_REASON,
|
|
18
|
+
ASTRA_MALFORMED_JSONL_REASON,
|
|
19
|
+
ASTRA_MISSING_SESSION_REASON,
|
|
20
|
+
ASTRA_REPLY_FAILURE_REASON,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CodexAstraAdvisorReply:
|
|
26
|
+
session_id: str | None
|
|
27
|
+
guidance: str | None
|
|
28
|
+
successful: bool
|
|
29
|
+
reason: str | None
|
|
30
|
+
is_fallback: bool
|
|
31
|
+
signal: str | None
|
|
32
|
+
astra_enabled: bool
|
|
33
|
+
selected_tier: str
|
|
34
|
+
outcome: str
|
|
35
|
+
fallback_kind: str | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_fallback_reply(
|
|
39
|
+
reason: str,
|
|
40
|
+
is_astra_enabled: bool,
|
|
41
|
+
fallback_kind: str | None = ASTRA_FALLBACK_KIND_BROKEN,
|
|
42
|
+
) -> CodexAstraAdvisorReply:
|
|
43
|
+
"""Build a failed advisor reply.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
reason: Why the request fell back.
|
|
47
|
+
is_astra_enabled: True when the Astra flag was on.
|
|
48
|
+
fallback_kind: Kind of fallback.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Failed reply with no session or guidance.
|
|
52
|
+
"""
|
|
53
|
+
return CodexAstraAdvisorReply(
|
|
54
|
+
None,
|
|
55
|
+
None,
|
|
56
|
+
False,
|
|
57
|
+
reason,
|
|
58
|
+
True,
|
|
59
|
+
None,
|
|
60
|
+
is_astra_enabled,
|
|
61
|
+
ADVISOR_FALLBACK_TIER,
|
|
62
|
+
ADVISOR_FALLBACK_RESULT,
|
|
63
|
+
fallback_kind,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_success_reply(
|
|
68
|
+
session_id: str, guidance: str, signal: str
|
|
69
|
+
) -> CodexAstraAdvisorReply:
|
|
70
|
+
"""Build a successful advisor reply.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
session_id: Codex session id.
|
|
74
|
+
guidance: Advisor text.
|
|
75
|
+
signal: First-line guidance signal.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Successful reply with Astra enabled.
|
|
79
|
+
"""
|
|
80
|
+
return CodexAstraAdvisorReply(
|
|
81
|
+
session_id,
|
|
82
|
+
guidance,
|
|
83
|
+
True,
|
|
84
|
+
None,
|
|
85
|
+
False,
|
|
86
|
+
signal,
|
|
87
|
+
True,
|
|
88
|
+
ADVISOR_MODEL_TIER,
|
|
89
|
+
CODEX_BIND_SUCCESS_TOKEN,
|
|
90
|
+
None,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _guidance_signal(guidance: str) -> str | None:
|
|
95
|
+
for each_line in guidance.splitlines():
|
|
96
|
+
candidate = each_line.strip()
|
|
97
|
+
if candidate:
|
|
98
|
+
return candidate if candidate in ALL_ADVISOR_GUIDANCE_SIGNALS else None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_event_line(each_line: str) -> tuple[str | None, str | None]:
|
|
103
|
+
event = json.loads(each_line)
|
|
104
|
+
if not isinstance(event, dict):
|
|
105
|
+
raise TypeError("event must be an object")
|
|
106
|
+
discovered_session_id: str | None = None
|
|
107
|
+
discovered_guidance: str | None = None
|
|
108
|
+
if event.get("type") == "thread.started":
|
|
109
|
+
raw_session = event.get("thread_id")
|
|
110
|
+
if isinstance(raw_session, str) and raw_session.strip():
|
|
111
|
+
discovered_session_id = raw_session.strip()
|
|
112
|
+
completed_event = event.get("item")
|
|
113
|
+
if event.get("type") == "item.completed" and isinstance(completed_event, dict):
|
|
114
|
+
message_text = completed_event.get("text")
|
|
115
|
+
if completed_event.get("type") == "agent_message" and isinstance(message_text, str):
|
|
116
|
+
discovered_guidance = message_text.strip()
|
|
117
|
+
return discovered_session_id, discovered_guidance
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _collect_reply_parts(jsonl_text: str) -> tuple[str | None, str | None]:
|
|
121
|
+
session_id: str | None = None
|
|
122
|
+
guidance: str | None = None
|
|
123
|
+
for each_line in jsonl_text.splitlines():
|
|
124
|
+
if each_line.strip():
|
|
125
|
+
discovered_session_id, discovered_guidance = _parse_event_line(each_line)
|
|
126
|
+
session_id = discovered_session_id or session_id
|
|
127
|
+
guidance = discovered_guidance or guidance
|
|
128
|
+
return session_id, guidance
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_codex_jsonl_reply(
|
|
132
|
+
jsonl_text: str,
|
|
133
|
+
existing_session_id: str | None,
|
|
134
|
+
is_astra_enabled: bool,
|
|
135
|
+
fallback_kind: str | None = None,
|
|
136
|
+
) -> CodexAstraAdvisorReply:
|
|
137
|
+
"""Parse Codex JSONL into an Astra advisor reply.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
jsonl_text: JSONL from Codex stdout.
|
|
141
|
+
existing_session_id: Session expected on resume, or None on bind.
|
|
142
|
+
is_astra_enabled: True when the Astra flag was on.
|
|
143
|
+
fallback_kind: Kind of fallback on a failed reply.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Advisor reply, or a fallback.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
session_id, guidance = _collect_reply_parts(jsonl_text)
|
|
150
|
+
except (TypeError, json.JSONDecodeError):
|
|
151
|
+
return build_fallback_reply(ASTRA_MALFORMED_JSONL_REASON, is_astra_enabled, fallback_kind)
|
|
152
|
+
if session_id is None or (existing_session_id is not None and session_id != existing_session_id):
|
|
153
|
+
return build_fallback_reply(ASTRA_MISSING_SESSION_REASON, is_astra_enabled, fallback_kind)
|
|
154
|
+
if not guidance:
|
|
155
|
+
return build_fallback_reply(ASTRA_REPLY_FAILURE_REASON, is_astra_enabled, fallback_kind)
|
|
156
|
+
signal = _guidance_signal(guidance)
|
|
157
|
+
if signal is None:
|
|
158
|
+
return build_fallback_reply(ASTRA_INVALID_SIGNAL_REASON, is_astra_enabled, fallback_kind)
|
|
159
|
+
return build_success_reply(session_id, guidance, signal)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Shared
|
|
1
|
+
"""Shared constants for advisor selection and replies."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
ADVISOR_MODEL_TIER: str = "
|
|
6
|
-
ADVISOR_CODEX_MODEL_ID: str = "gpt-
|
|
5
|
+
ADVISOR_MODEL_TIER: str = "Astra"
|
|
6
|
+
ADVISOR_CODEX_MODEL_ID: str = "gpt-6-astra"
|
|
7
7
|
ADVISOR_FALLBACK_TIER: str = "Fable"
|
|
8
8
|
ADVISOR_FALLBACK_RESULT: str = "fable"
|
|
9
9
|
ADVISOR_EFFORT_ENV_VAR: str = "ADVISOR_EFFORT"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Constants for the optional Codex Astra advisor bind."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CODEX_EXECUTABLE: str = "codex"
|
|
6
|
+
ADVISOR_CODEX_EXECUTABLE_ENV_VAR: str = "ADVISOR_CODEX_EXECUTABLE"
|
|
7
|
+
CODEX_READ_ONLY_SANDBOX: str = "read-only"
|
|
8
|
+
CODEX_JSON_FLAG: str = "--json"
|
|
9
|
+
CODEX_MODEL_FLAG: str = "--model"
|
|
10
|
+
CODEX_CONFIG_FLAG: str = "--config"
|
|
11
|
+
CODEX_REASONING_CONFIG_TEMPLATE: str = 'model_reasoning_effort="{effort}"'
|
|
12
|
+
CODEX_PROMPT_FROM_STDIN: str = "-"
|
|
13
|
+
CODEX_EXEC_SUBCOMMAND: str = "exec"
|
|
14
|
+
CODEX_RESUME_SUBCOMMAND: str = "resume"
|
|
15
|
+
CODEX_SANDBOX_FLAG: str = "--sandbox"
|
|
16
|
+
CLAUDE_CONFIG_DIRECTORY_NAME: str = ".claude"
|
|
17
|
+
USAGE_PROBE_SHARED_DIRECTORY_NAME: str = "_shared"
|
|
18
|
+
USAGE_PROBE_PACKAGE_DIRECTORY_NAME: str = "pr-loop"
|
|
19
|
+
USAGE_PROBE_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
|
20
|
+
USAGE_PROBE_FILENAME: str = "codex_usage_probe.py"
|
|
21
|
+
ASTRA_SESSION_ID_METAVAR: str = "SESSION_ID"
|
|
22
|
+
ASTRA_CODEX_TIMEOUT_SECONDS: float = 120.0
|
|
23
|
+
ASTRA_USAGE_PROBE_TIMEOUT_SECONDS: float = 30.0
|
|
24
|
+
ASTRA_ENV_VAR: str = "ADVISOR_ASTRA"
|
|
25
|
+
ALL_ASTRA_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
26
|
+
ASTRA_PREFLIGHT_FAILURE_REASON: str = "astra preflight did not establish an eligible Codex meter"
|
|
27
|
+
ASTRA_BIND_FAILURE_REASON: str = "Codex Astra bind failed"
|
|
28
|
+
ASTRA_REPLY_FAILURE_REASON: str = "Codex Astra returned no advisor guidance"
|
|
29
|
+
ASTRA_PROBE_TIMEOUT_REASON: str = "Codex Astra meter check timed out"
|
|
30
|
+
ASTRA_CODEX_TIMEOUT_REASON: str = "Codex Astra request timed out"
|
|
31
|
+
ASTRA_MALFORMED_JSONL_REASON: str = "Codex Astra returned malformed JSONL"
|
|
32
|
+
ASTRA_MISSING_SESSION_REASON: str = "Codex Astra returned no session id"
|
|
33
|
+
ASTRA_INVALID_SIGNAL_REASON: str = "Codex Astra returned an invalid guidance signal"
|
|
34
|
+
ASTRA_EXECUTABLE_NOT_FOUND_REASON: str = "Codex Astra could not find the codex executable on PATH"
|
|
35
|
+
ASTRA_FALLBACK_KIND_DECLINED: str = "declined"
|
|
36
|
+
ASTRA_FALLBACK_KIND_BROKEN: str = "broken"
|
|
37
|
+
ASTRA_ENABLE_FLAG: str = "--enable-astra"
|
|
38
|
+
ASTRA_EFFORT_FLAG: str = "--effort"
|