claude-dev-env 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/_shared/advisor/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +74 -108
- package/_shared/advisor/reference/advisor-block.md +37 -0
- package/_shared/advisor/reference/cli-chain.md +45 -0
- package/_shared/advisor/reference/consult-format.md +41 -0
- package/_shared/advisor/reference/lifecycle.md +21 -0
- package/_shared/advisor/reference/sol-rung.md +31 -0
- package/_shared/advisor/reference/spawn-walk-log.md +31 -0
- package/_shared/advisor/reference/third-party-bind.md +30 -0
- package/_shared/advisor/reference/warm-up.md +33 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
- package/_shared/advisor/scripts/tier_model_ids.py +24 -0
- package/docs/references/CLAUDE.md +2 -1
- package/docs/references/advisor-tool.md +26 -8
- package/docs/references/team-advisor-skill.md +3 -3
- package/docs/references/weak-executor-advisor.md +91 -0
- package/hooks/blocking/test_fable_spawn_gate.py +18 -11
- package/package.json +1 -1
- package/skills/_shared/advisor/CLAUDE.md +1 -1
- package/skills/_shared/advisor/scripts/README.md +2 -0
- package/skills/grokify/SKILL.md +1 -1
- package/skills/grokify/templates/handoff-template.md +2 -2
- package/skills/orchestrator/SKILL.md +5 -4
- package/skills/team-advisor/SKILL.md +7 -4
- package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Behavioral tests for the executable Codex Sol advisor path."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
_ProcessRunner = Callable[..., subprocess.CompletedProcess[str]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_sol_module() -> ModuleType:
|
|
18
|
+
scripts_root = Path(__file__).parent.parent
|
|
19
|
+
config_root = scripts_root / "config"
|
|
20
|
+
sys.path.insert(0, str(config_root))
|
|
21
|
+
specification = importlib.util.spec_from_file_location(
|
|
22
|
+
"codex_sol_advisor", scripts_root / "codex_sol_advisor.py"
|
|
23
|
+
)
|
|
24
|
+
assert specification is not None and specification.loader is not None
|
|
25
|
+
module = importlib.util.module_from_spec(specification)
|
|
26
|
+
sys.modules[specification.name] = module
|
|
27
|
+
specification.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
sol_advisor = _load_sol_module()
|
|
32
|
+
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
33
|
+
USAGE_PROBE_PATH = (
|
|
34
|
+
SCRIPTS_ROOT.parents[2]
|
|
35
|
+
/ "skills"
|
|
36
|
+
/ "codex-review"
|
|
37
|
+
/ "scripts"
|
|
38
|
+
/ "codex_usage_probe.py"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _probe_process(
|
|
43
|
+
payload: object,
|
|
44
|
+
returncode: int = 0,
|
|
45
|
+
) -> subprocess.CompletedProcess[str]:
|
|
46
|
+
return subprocess.CompletedProcess(
|
|
47
|
+
[sys.executable, str(USAGE_PROBE_PATH)],
|
|
48
|
+
returncode,
|
|
49
|
+
json.dumps(payload),
|
|
50
|
+
"",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _event_stream(
|
|
55
|
+
session_id: str = "thread-1",
|
|
56
|
+
guidance: str = "PLAN\ninspect the change",
|
|
57
|
+
) -> str:
|
|
58
|
+
return "\n".join(
|
|
59
|
+
[
|
|
60
|
+
json.dumps({"type": "thread.started", "thread_id": session_id}),
|
|
61
|
+
json.dumps(
|
|
62
|
+
{
|
|
63
|
+
"type": "item.completed",
|
|
64
|
+
"item": {
|
|
65
|
+
"type": "agent_message",
|
|
66
|
+
"text": guidance,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _two_step_process_runner(calls: list[list[str]], guidance: str) -> _ProcessRunner:
|
|
75
|
+
def process_runner(
|
|
76
|
+
arguments: list[str], **kwargs: object
|
|
77
|
+
) -> subprocess.CompletedProcess[str]:
|
|
78
|
+
calls.append(arguments)
|
|
79
|
+
if len(calls) == 1:
|
|
80
|
+
return _probe_process({"percent_left": 90})
|
|
81
|
+
return subprocess.CompletedProcess(
|
|
82
|
+
arguments, 0, _event_stream(guidance=guidance), ""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return process_runner
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_sol_flag_accepts_documented_truthy_values() -> None:
|
|
89
|
+
assert sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "yes"})
|
|
90
|
+
assert not sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "0"})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_resolve_usage_probe_path_uses_supplied_home_directory(tmp_path: Path) -> None:
|
|
94
|
+
probe_path = sol_advisor.resolve_usage_probe_path(tmp_path)
|
|
95
|
+
|
|
96
|
+
assert probe_path == (
|
|
97
|
+
tmp_path
|
|
98
|
+
/ ".claude"
|
|
99
|
+
/ "skills"
|
|
100
|
+
/ "codex-review"
|
|
101
|
+
/ "scripts"
|
|
102
|
+
/ "codex_usage_probe.py"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_argument_parser_accepts_bind_and_resume_modes() -> None:
|
|
107
|
+
parser = sol_advisor.build_argument_parser()
|
|
108
|
+
|
|
109
|
+
bind_arguments = parser.parse_args(["--bind", "--cwd", "."])
|
|
110
|
+
resume_arguments = parser.parse_args(
|
|
111
|
+
["--resume", "thread-1", "--cwd", "."]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert bind_arguments.bind
|
|
115
|
+
assert bind_arguments.resume is None
|
|
116
|
+
assert resume_arguments.resume == "thread-1"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_main_serializes_stable_result_field(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
|
|
120
|
+
monkeypatch.setattr(
|
|
121
|
+
sol_advisor,
|
|
122
|
+
"run_codex_sol_advisor",
|
|
123
|
+
lambda **kwargs: sol_advisor.CodexSolAdvisorReply(
|
|
124
|
+
session_id="thread-1",
|
|
125
|
+
guidance="PLAN\ninspect",
|
|
126
|
+
successful=True,
|
|
127
|
+
reason=None,
|
|
128
|
+
is_fallback=False,
|
|
129
|
+
signal="PLAN",
|
|
130
|
+
sol_enabled=True,
|
|
131
|
+
selected_tier=sol_advisor.ADVISOR_MODEL_TIER,
|
|
132
|
+
outcome=sol_advisor.CODEX_BIND_SUCCESS_TOKEN,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
|
|
136
|
+
|
|
137
|
+
exit_code = sol_advisor.main(["--bind", "--cwd", "."])
|
|
138
|
+
payload = json.loads(capsys.readouterr().out)
|
|
139
|
+
|
|
140
|
+
assert exit_code == 0
|
|
141
|
+
assert payload["result"] == "codex"
|
|
142
|
+
assert "outcome" not in payload
|
|
143
|
+
assert payload["sol_enabled"] is True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_bind_and_resume_arguments_match_installed_codex_interface() -> None:
|
|
147
|
+
expected_common_arguments = [
|
|
148
|
+
"codex",
|
|
149
|
+
"exec",
|
|
150
|
+
"--model",
|
|
151
|
+
sol_advisor.ADVISOR_CODEX_MODEL_ID,
|
|
152
|
+
"--config",
|
|
153
|
+
'model_reasoning_effort="xhigh"',
|
|
154
|
+
"--sandbox",
|
|
155
|
+
"read-only",
|
|
156
|
+
"--json",
|
|
157
|
+
]
|
|
158
|
+
assert sol_advisor.build_codex_arguments() == [
|
|
159
|
+
*expected_common_arguments,
|
|
160
|
+
"-",
|
|
161
|
+
]
|
|
162
|
+
assert sol_advisor.build_codex_arguments("thread-1") == [
|
|
163
|
+
*expected_common_arguments,
|
|
164
|
+
"resume",
|
|
165
|
+
"thread-1",
|
|
166
|
+
"-",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_successful_probe_requires_finite_meter_above_configured_gate() -> None:
|
|
171
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
172
|
+
return _probe_process({"percent_left": 90})
|
|
173
|
+
|
|
174
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
175
|
+
|
|
176
|
+
assert preflight.eligible
|
|
177
|
+
assert preflight.percent_left == 90
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.mark.parametrize("percent_left", [10, 10.0, 10.000])
|
|
181
|
+
def test_meter_at_configured_threshold_falls_back(
|
|
182
|
+
percent_left: float,
|
|
183
|
+
) -> None:
|
|
184
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
185
|
+
return _probe_process({"percent_left": percent_left})
|
|
186
|
+
|
|
187
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
188
|
+
|
|
189
|
+
assert not preflight.eligible
|
|
190
|
+
assert preflight.percent_left == percent_left
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.parametrize(
|
|
194
|
+
"payload",
|
|
195
|
+
[
|
|
196
|
+
{"percent_left": None},
|
|
197
|
+
{"percent_left": "90"},
|
|
198
|
+
{"percent_left": True},
|
|
199
|
+
{"percent_left": float("nan")},
|
|
200
|
+
["percent_left", 90],
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
def test_unknown_and_malformed_meter_falls_back(payload: object) -> None:
|
|
204
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
205
|
+
return _probe_process(payload)
|
|
206
|
+
|
|
207
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
208
|
+
|
|
209
|
+
assert not preflight.eligible
|
|
210
|
+
assert preflight.percent_left is None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_malformed_probe_json_falls_back() -> None:
|
|
214
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
215
|
+
return subprocess.CompletedProcess(arguments, 0, "{broken", "")
|
|
216
|
+
|
|
217
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
218
|
+
|
|
219
|
+
assert not preflight.eligible
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_probe_nonzero_exit_falls_back() -> None:
|
|
223
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
224
|
+
return _probe_process({"percent_left": 90}, returncode=2)
|
|
225
|
+
|
|
226
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
227
|
+
|
|
228
|
+
assert not preflight.eligible
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_probe_timeout_falls_back() -> None:
|
|
232
|
+
def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
233
|
+
raise subprocess.TimeoutExpired(arguments, 30)
|
|
234
|
+
|
|
235
|
+
preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
|
|
236
|
+
|
|
237
|
+
assert not preflight.eligible
|
|
238
|
+
assert "timed out" in preflight.reason
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("signal", ["ENDORSE", "CORRECTION", "PLAN", "STOP"])
|
|
242
|
+
def test_jsonl_parser_accepts_each_exact_guidance_signal(signal: str) -> None:
|
|
243
|
+
reply = sol_advisor.parse_codex_jsonl_reply(
|
|
244
|
+
_event_stream(guidance=f"{signal}\nadditional guidance"),
|
|
245
|
+
existing_session_id=None,
|
|
246
|
+
is_sol_enabled=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert reply.successful
|
|
250
|
+
assert not reply.is_fallback
|
|
251
|
+
assert reply.session_id == "thread-1"
|
|
252
|
+
assert reply.guidance == f"{signal}\nadditional guidance"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@pytest.mark.parametrize(
|
|
256
|
+
"jsonl_text",
|
|
257
|
+
[
|
|
258
|
+
"{broken",
|
|
259
|
+
json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
|
|
260
|
+
_event_stream(session_id="", guidance="PLAN\ninspect"),
|
|
261
|
+
_event_stream(guidance="ENDORSE: ready"),
|
|
262
|
+
_event_stream(guidance="\n unknown signal\nmore"),
|
|
263
|
+
json.dumps(
|
|
264
|
+
{
|
|
265
|
+
"type": "item.completed",
|
|
266
|
+
"item": {"type": "agent_message", "text": "PLAN"},
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
],
|
|
270
|
+
)
|
|
271
|
+
def test_jsonl_parser_returns_typed_fallback_for_invalid_reply(
|
|
272
|
+
jsonl_text: str,
|
|
273
|
+
) -> None:
|
|
274
|
+
reply = sol_advisor.parse_codex_jsonl_reply(
|
|
275
|
+
jsonl_text,
|
|
276
|
+
existing_session_id=None,
|
|
277
|
+
is_sol_enabled=True,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
assert not reply.successful
|
|
281
|
+
assert reply.is_fallback
|
|
282
|
+
assert reply.session_id is None
|
|
283
|
+
assert reply.guidance is None
|
|
284
|
+
assert reply.reason
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_bind_runs_probe_then_codex_with_read_only_xhigh_settings() -> None:
|
|
288
|
+
calls: list[tuple[list[str], dict[str, object]]] = []
|
|
289
|
+
|
|
290
|
+
def process_runner(
|
|
291
|
+
arguments: list[str], **kwargs: object
|
|
292
|
+
) -> subprocess.CompletedProcess[str]:
|
|
293
|
+
calls.append((arguments, kwargs))
|
|
294
|
+
if len(calls) == 1:
|
|
295
|
+
return _probe_process({"percent_left": 90})
|
|
296
|
+
return subprocess.CompletedProcess(
|
|
297
|
+
arguments, 0, _event_stream(guidance="PLAN\ninspect"), ""
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
301
|
+
prompt="reply only",
|
|
302
|
+
working_directory=Path("."),
|
|
303
|
+
preflight=None,
|
|
304
|
+
probe_path=USAGE_PROBE_PATH,
|
|
305
|
+
session_id=None,
|
|
306
|
+
process_runner=process_runner,
|
|
307
|
+
setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
assert reply.successful
|
|
311
|
+
assert len(calls) == 2
|
|
312
|
+
assert calls[1][0] == sol_advisor.build_codex_arguments()
|
|
313
|
+
assert calls[1][1]["cwd"] == "."
|
|
314
|
+
assert calls[1][1]["shell"] is False
|
|
315
|
+
assert calls[1][1]["timeout"]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_team_advisor_path_preserves_sol_routing_fields() -> None:
|
|
319
|
+
team_advisor_path = SCRIPTS_ROOT.parents[2] / "skills" / "team-advisor" / "SKILL.md"
|
|
320
|
+
sol_rung_path = SCRIPTS_ROOT.parent / "reference" / "sol-rung.md"
|
|
321
|
+
assert "advisor-protocol.md" in team_advisor_path.read_text(encoding="utf-8")
|
|
322
|
+
sol_rung = sol_rung_path.read_text(encoding="utf-8")
|
|
323
|
+
assert "codex_sol_advisor.py" in sol_rung
|
|
324
|
+
assert "--resume <session_id>" in sol_rung
|
|
325
|
+
calls: list[list[str]] = []
|
|
326
|
+
|
|
327
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
328
|
+
prompt="first consult",
|
|
329
|
+
working_directory=Path("."),
|
|
330
|
+
preflight=None,
|
|
331
|
+
probe_path=USAGE_PROBE_PATH,
|
|
332
|
+
session_id=None,
|
|
333
|
+
process_runner=_two_step_process_runner(calls, guidance="PLAN\ninspect"),
|
|
334
|
+
setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert reply.successful
|
|
338
|
+
assert reply.sol_enabled
|
|
339
|
+
assert reply.selected_tier == sol_advisor.ADVISOR_MODEL_TIER
|
|
340
|
+
assert reply.outcome == sol_advisor.CODEX_BIND_SUCCESS_TOKEN
|
|
341
|
+
assert reply.signal == "PLAN"
|
|
342
|
+
assert reply.guidance == "PLAN\ninspect"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
|
|
346
|
+
calls: list[list[str]] = []
|
|
347
|
+
|
|
348
|
+
def process_runner(
|
|
349
|
+
arguments: list[str], **kwargs: object
|
|
350
|
+
) -> subprocess.CompletedProcess[str]:
|
|
351
|
+
calls.append(arguments)
|
|
352
|
+
return _probe_process({"percent_left": 10})
|
|
353
|
+
|
|
354
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
355
|
+
prompt="first consult",
|
|
356
|
+
working_directory=Path("."),
|
|
357
|
+
preflight=None,
|
|
358
|
+
probe_path=USAGE_PROBE_PATH,
|
|
359
|
+
session_id=None,
|
|
360
|
+
process_runner=process_runner,
|
|
361
|
+
setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
assert not reply.successful
|
|
365
|
+
assert reply.is_fallback
|
|
366
|
+
assert reply.sol_enabled
|
|
367
|
+
assert reply.selected_tier == sol_advisor.ADVISOR_FALLBACK_TIER
|
|
368
|
+
assert reply.outcome == sol_advisor.ADVISOR_FALLBACK_RESULT
|
|
369
|
+
assert len(calls) == 1
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_disabled_flag_uses_default_optional_routing_inputs() -> None:
|
|
373
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
374
|
+
prompt="first consult",
|
|
375
|
+
working_directory=Path("."),
|
|
376
|
+
preflight=sol_advisor.SolPreflight(
|
|
377
|
+
eligible=True,
|
|
378
|
+
percent_left=90,
|
|
379
|
+
reason="test preflight",
|
|
380
|
+
),
|
|
381
|
+
probe_path=None,
|
|
382
|
+
setting_by_name=None,
|
|
383
|
+
session_id=None,
|
|
384
|
+
process_runner=subprocess.run,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
assert reply.is_fallback
|
|
388
|
+
assert reply.reason == "Sol advisor flag is disabled"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_resume_runs_the_usage_gate_before_codex() -> None:
|
|
392
|
+
calls: list[list[str]] = []
|
|
393
|
+
|
|
394
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
395
|
+
prompt="resume",
|
|
396
|
+
working_directory=Path("."),
|
|
397
|
+
preflight=None,
|
|
398
|
+
session_id="thread-1",
|
|
399
|
+
probe_path=USAGE_PROBE_PATH,
|
|
400
|
+
process_runner=_two_step_process_runner(calls, guidance="ENDORSE\nready"),
|
|
401
|
+
setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
assert reply.successful
|
|
405
|
+
assert calls == [
|
|
406
|
+
[sys.executable, str(USAGE_PROBE_PATH)],
|
|
407
|
+
sol_advisor.build_codex_arguments("thread-1"),
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.parametrize(
|
|
412
|
+
"codex_failure",
|
|
413
|
+
[
|
|
414
|
+
"nonzero",
|
|
415
|
+
"timeout",
|
|
416
|
+
"malformed_jsonl",
|
|
417
|
+
"missing_session",
|
|
418
|
+
"missing_guidance",
|
|
419
|
+
"invalid_signal",
|
|
420
|
+
],
|
|
421
|
+
)
|
|
422
|
+
def test_codex_failure_modes_always_return_fallback(
|
|
423
|
+
codex_failure: str,
|
|
424
|
+
) -> None:
|
|
425
|
+
def process_runner(
|
|
426
|
+
arguments: list[str], **kwargs: object
|
|
427
|
+
) -> subprocess.CompletedProcess[str]:
|
|
428
|
+
if len(arguments) == 2:
|
|
429
|
+
return _probe_process({"percent_left": 90})
|
|
430
|
+
if codex_failure == "timeout":
|
|
431
|
+
raise subprocess.TimeoutExpired(arguments, 30)
|
|
432
|
+
if codex_failure == "nonzero":
|
|
433
|
+
return subprocess.CompletedProcess(
|
|
434
|
+
arguments, 3, _event_stream(guidance="PLAN"), ""
|
|
435
|
+
)
|
|
436
|
+
if codex_failure == "malformed_jsonl":
|
|
437
|
+
return subprocess.CompletedProcess(arguments, 0, "{broken", "")
|
|
438
|
+
if codex_failure == "missing_session":
|
|
439
|
+
return subprocess.CompletedProcess(
|
|
440
|
+
arguments,
|
|
441
|
+
0,
|
|
442
|
+
json.dumps(
|
|
443
|
+
{
|
|
444
|
+
"type": "item.completed",
|
|
445
|
+
"item": {"type": "agent_message", "text": "PLAN"},
|
|
446
|
+
}
|
|
447
|
+
),
|
|
448
|
+
"",
|
|
449
|
+
)
|
|
450
|
+
if codex_failure == "missing_guidance":
|
|
451
|
+
return subprocess.CompletedProcess(
|
|
452
|
+
arguments,
|
|
453
|
+
0,
|
|
454
|
+
json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
|
|
455
|
+
"",
|
|
456
|
+
)
|
|
457
|
+
return subprocess.CompletedProcess(
|
|
458
|
+
arguments, 0, _event_stream(guidance="PLAN: inspect"), ""
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
reply = sol_advisor.run_codex_sol_advisor(
|
|
462
|
+
prompt="bind",
|
|
463
|
+
working_directory=Path("."),
|
|
464
|
+
preflight=None,
|
|
465
|
+
probe_path=USAGE_PROBE_PATH,
|
|
466
|
+
session_id=None,
|
|
467
|
+
process_runner=process_runner,
|
|
468
|
+
setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
assert not reply.successful
|
|
472
|
+
assert reply.is_fallback
|
|
473
|
+
assert reply.session_id is None
|
|
474
|
+
assert reply.guidance is None
|
|
@@ -33,6 +33,8 @@ main = model_tier_run_validator.main
|
|
|
33
33
|
load_model_tier_run_from_json_path = (
|
|
34
34
|
model_tier_run_validator.load_model_tier_run_from_json_path
|
|
35
35
|
)
|
|
36
|
+
ADVISOR_MODEL_TIER = model_tier_run_validator.ADVISOR_MODEL_TIER
|
|
37
|
+
CODEX_BIND_SUCCESS_TOKEN = model_tier_run_validator.CODEX_BIND_SUCCESS_TOKEN
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
def test_clean_single_spawn_at_top_of_slice_passes() -> None:
|
|
@@ -45,6 +47,66 @@ def test_clean_single_spawn_at_top_of_slice_passes() -> None:
|
|
|
45
47
|
assert validate_model_tier_run(run) is None
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
def test_sol_codex_bind_is_first_success_when_enabled() -> None:
|
|
51
|
+
run = ModelTierRun(
|
|
52
|
+
own_tier="Opus",
|
|
53
|
+
candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
|
|
54
|
+
attempts=[{"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN}],
|
|
55
|
+
selected_tier=ADVISOR_MODEL_TIER,
|
|
56
|
+
is_sol_enabled=True,
|
|
57
|
+
)
|
|
58
|
+
assert validate_model_tier_run(run) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_sol_failure_falls_through_to_fable_when_enabled() -> None:
|
|
62
|
+
run = ModelTierRun(
|
|
63
|
+
own_tier="Opus",
|
|
64
|
+
candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
|
|
65
|
+
attempts=[
|
|
66
|
+
{"tier": ADVISOR_MODEL_TIER, "result": "unavailable"},
|
|
67
|
+
{"tier": "Fable", "result": "spawned"},
|
|
68
|
+
],
|
|
69
|
+
selected_tier="Fable",
|
|
70
|
+
is_sol_enabled=True,
|
|
71
|
+
)
|
|
72
|
+
assert validate_model_tier_run(run) is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_sol_rung_precedes_third_party_cli_floor_when_enabled() -> None:
|
|
76
|
+
run = ModelTierRun(
|
|
77
|
+
own_tier="ThirdParty",
|
|
78
|
+
candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
|
|
79
|
+
attempts=[{"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN}],
|
|
80
|
+
selected_tier=ADVISOR_MODEL_TIER,
|
|
81
|
+
is_sol_enabled=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert validate_model_tier_run(run) is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_sol_codex_result_requires_sol_candidate() -> None:
|
|
88
|
+
run = ModelTierRun(
|
|
89
|
+
own_tier="Opus",
|
|
90
|
+
candidate_tiers=["Fable", "Opus"],
|
|
91
|
+
attempts=[{"tier": "Fable", "result": CODEX_BIND_SUCCESS_TOKEN}],
|
|
92
|
+
selected_tier="Fable",
|
|
93
|
+
)
|
|
94
|
+
with pytest.raises(ModelTierRunError):
|
|
95
|
+
validate_model_tier_run(run)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_sol_spawned_result_does_not_count_as_codex_success() -> None:
|
|
99
|
+
run = ModelTierRun(
|
|
100
|
+
own_tier="Opus",
|
|
101
|
+
candidate_tiers=[ADVISOR_MODEL_TIER, "Fable", "Opus"],
|
|
102
|
+
attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
|
|
103
|
+
selected_tier=ADVISOR_MODEL_TIER,
|
|
104
|
+
is_sol_enabled=True,
|
|
105
|
+
)
|
|
106
|
+
with pytest.raises(ModelTierRunError):
|
|
107
|
+
validate_model_tier_run(run)
|
|
108
|
+
|
|
109
|
+
|
|
48
110
|
def test_fallthrough_to_floor_tier_passes() -> None:
|
|
49
111
|
run = ModelTierRun(
|
|
50
112
|
own_tier="Opus",
|
|
@@ -223,6 +285,23 @@ def test_cli_validates_json_log_file(tmp_path: Path) -> None:
|
|
|
223
285
|
assert loaded_run.selected_tier == "Fable"
|
|
224
286
|
|
|
225
287
|
|
|
288
|
+
def test_cli_rejects_non_boolean_sol_enabled(tmp_path: Path) -> None:
|
|
289
|
+
log_path = tmp_path / "invalid-sol-enabled.json"
|
|
290
|
+
log_path.write_text(
|
|
291
|
+
json.dumps(
|
|
292
|
+
{
|
|
293
|
+
"own_tier": "Opus",
|
|
294
|
+
"candidate_tiers": ["Fable", "Opus"],
|
|
295
|
+
"attempts": [{"tier": "Fable", "result": "spawned"}],
|
|
296
|
+
"selected_tier": "Fable",
|
|
297
|
+
"sol_enabled": "false",
|
|
298
|
+
}
|
|
299
|
+
),
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
)
|
|
302
|
+
assert main([str(log_path)]) == 2
|
|
303
|
+
|
|
304
|
+
|
|
226
305
|
def test_cli_rejects_incomplete_fallback_log(tmp_path: Path) -> None:
|
|
227
306
|
log_path = tmp_path / "incomplete-walk.json"
|
|
228
307
|
log_path.write_text(
|
|
@@ -10,6 +10,26 @@ from types import ModuleType
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
constants_root = Path(__file__).parent.parent / "config"
|
|
14
|
+
if str(constants_root) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(constants_root))
|
|
16
|
+
|
|
17
|
+
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
18
|
+
ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS,
|
|
19
|
+
ALL_CLI_MODEL_ID_BY_TIER,
|
|
20
|
+
ALL_KNOWN_TIER_NAMES,
|
|
21
|
+
ALL_MODEL_TIERS,
|
|
22
|
+
HOST_PROFILE_CLAUDE,
|
|
23
|
+
HOST_PROFILE_THIRD_PARTY,
|
|
24
|
+
THIRD_PARTY_MODEL_TIER,
|
|
25
|
+
)
|
|
26
|
+
from advisor_scripts_constants.advisor_route_constants import ( # noqa: E402
|
|
27
|
+
ADVISOR_CODEX_MODEL_ID,
|
|
28
|
+
ADVISOR_MODEL_TIER,
|
|
29
|
+
ALL_CODEX_MODEL_ID_BY_TIER,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
13
33
|
def _load_tier_model_ids_module() -> ModuleType:
|
|
14
34
|
scripts_root = Path(__file__).parent.parent
|
|
15
35
|
module_path = scripts_root / "tier_model_ids.py"
|
|
@@ -26,21 +46,9 @@ def _load_tier_model_ids_module() -> ModuleType:
|
|
|
26
46
|
|
|
27
47
|
tier_model_ids = _load_tier_model_ids_module()
|
|
28
48
|
resolve_cli_model_id = tier_model_ids.resolve_cli_model_id
|
|
49
|
+
resolve_codex_model_id = tier_model_ids.resolve_codex_model_id
|
|
29
50
|
canonical_tier_name = tier_model_ids.canonical_tier_name
|
|
30
51
|
detect_host_profile = tier_model_ids.detect_host_profile
|
|
31
|
-
constants_root = Path(__file__).parent.parent / "config"
|
|
32
|
-
if str(constants_root) not in sys.path:
|
|
33
|
-
sys.path.insert(0, str(constants_root))
|
|
34
|
-
|
|
35
|
-
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
36
|
-
ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS,
|
|
37
|
-
ALL_CLI_MODEL_ID_BY_TIER,
|
|
38
|
-
ALL_KNOWN_TIER_NAMES,
|
|
39
|
-
ALL_MODEL_TIERS,
|
|
40
|
-
HOST_PROFILE_CLAUDE,
|
|
41
|
-
HOST_PROFILE_THIRD_PARTY,
|
|
42
|
-
THIRD_PARTY_MODEL_TIER,
|
|
43
|
-
)
|
|
44
52
|
|
|
45
53
|
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
46
54
|
DOCUMENTED_RESOLVE_ONE_LINER = (
|
|
@@ -94,14 +102,28 @@ def test_sendmessage_reply_wait_is_positive_bound() -> None:
|
|
|
94
102
|
assert ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS == 120
|
|
95
103
|
|
|
96
104
|
|
|
105
|
+
def test_resolve_codex_model_id_maps_sol() -> None:
|
|
106
|
+
assert resolve_codex_model_id(f" {ADVISOR_MODEL_TIER.lower()} ") == (
|
|
107
|
+
ADVISOR_CODEX_MODEL_ID
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_resolve_codex_model_id_rejects_claude_tier() -> None:
|
|
112
|
+
with pytest.raises(ValueError, match="not a known model tier"):
|
|
113
|
+
resolve_codex_model_id("Opus")
|
|
114
|
+
|
|
115
|
+
|
|
97
116
|
def test_cli_model_alias_map_keys_match_known_tiers() -> None:
|
|
98
|
-
|
|
117
|
+
all_cli_tiers = (*ALL_MODEL_TIERS, THIRD_PARTY_MODEL_TIER)
|
|
118
|
+
assert set(ALL_CLI_MODEL_ID_BY_TIER) == set(all_cli_tiers)
|
|
99
119
|
assert set(ALL_MODEL_TIERS).issubset(set(ALL_KNOWN_TIER_NAMES))
|
|
120
|
+
assert ADVISOR_MODEL_TIER in ALL_KNOWN_TIER_NAMES
|
|
100
121
|
assert THIRD_PARTY_MODEL_TIER in ALL_KNOWN_TIER_NAMES
|
|
101
122
|
assert THIRD_PARTY_MODEL_TIER not in ALL_MODEL_TIERS
|
|
102
|
-
assert all(
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
assert all(ALL_CLI_MODEL_ID_BY_TIER[each_tier] for each_tier in all_cli_tiers)
|
|
124
|
+
assert ALL_CODEX_MODEL_ID_BY_TIER == {
|
|
125
|
+
ADVISOR_MODEL_TIER: ADVISOR_CODEX_MODEL_ID,
|
|
126
|
+
}
|
|
105
127
|
|
|
106
128
|
|
|
107
129
|
def test_canonical_tier_name_strips_and_normalizes() -> None:
|
|
@@ -45,6 +45,9 @@ from advisor_scripts_constants.model_tier_run_validator_constants import ( # no
|
|
|
45
45
|
UNKNOWN_HOST_PROFILE_ERROR,
|
|
46
46
|
UNKNOWN_LADDER_NAME_ERROR,
|
|
47
47
|
)
|
|
48
|
+
from advisor_scripts_constants.advisor_route_constants import ( # noqa: E402
|
|
49
|
+
ALL_CODEX_MODEL_ID_BY_TIER,
|
|
50
|
+
)
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def canonical_tier_name(tier_name: str) -> str | None:
|
|
@@ -111,6 +114,27 @@ def resolve_cli_model_id(tier: str) -> str:
|
|
|
111
114
|
return maybe_model_alias
|
|
112
115
|
|
|
113
116
|
|
|
117
|
+
def resolve_codex_model_id(tier: str) -> str:
|
|
118
|
+
"""Return the dated Codex model id for a Codex-backed advisor tier.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
tier: Ladder tier name whose Codex model id should be resolved.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The configured Codex model id.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: When ``tier`` is not a known Codex-backed tier.
|
|
128
|
+
"""
|
|
129
|
+
maybe_canonical_tier = canonical_tier_name(tier)
|
|
130
|
+
if maybe_canonical_tier is None:
|
|
131
|
+
raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
|
|
132
|
+
maybe_model_id = ALL_CODEX_MODEL_ID_BY_TIER.get(maybe_canonical_tier)
|
|
133
|
+
if maybe_model_id is None:
|
|
134
|
+
raise ValueError(UNKNOWN_LADDER_NAME_ERROR.format(tier))
|
|
135
|
+
return maybe_model_id
|
|
136
|
+
|
|
137
|
+
|
|
114
138
|
def detect_host_profile(
|
|
115
139
|
setting_by_name: Mapping[str, str] | None = None,
|
|
116
140
|
) -> str:
|
|
@@ -10,7 +10,8 @@ Pointer documents to external sources, standard terminology, and internal tool o
|
|
|
10
10
|
| `code-review-enforcement.md` | How the code-review gates work: the two required efforts (push at low, PR creation at xhigh), the stamp bound to the branch-surface hash, the single sanctioned minter, the two-layer stamp-directory guard, and the bypass surfaces the gates leave open |
|
|
11
11
|
| `prose-style-enforcement.md` | How `CLAUDE_PROSE_STYLE_ENFORCEMENT` arms opinionated prose gates (default off) while AskUserQuestion lean-block stays always on |
|
|
12
12
|
| `advisor-tool.md` | Canonical consult bones for any stronger reviewer: when to call, hard rule before first write, how to treat advice; maps to the Anthropic advisor tool |
|
|
13
|
-
| `team-advisor-skill.md` | `/team-advisor` map: sole-consumer warm bind, ref index, and
|
|
13
|
+
| `team-advisor-skill.md` | `/team-advisor` map: sole-consumer warm bind, ref index, and advisor selection |
|
|
14
|
+
| `weak-executor-advisor.md` | Consult profile a below-advisor-tier executor (Sonnet, Haiku) follows on top of `advisor-tool.md`: spawn-prompt steering, context packaging, two-timing rule, consult budget, failure branches |
|
|
14
15
|
|
|
15
16
|
## Role
|
|
16
17
|
|