claude-dev-env 8.43.5 → 8.44.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/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/cli-chain.md +9 -12
- package/_shared/advisor/reference/lifecycle.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +3 -3
- package/bin/install.test.mjs +1 -1
- package/docs/second-claude-account.md +11 -13
- package/hooks/blocking/reply_length_gate.py +43 -11
- package/hooks/blocking/test_reply_length_gate.py +55 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/reply_length_gate_constants.py +11 -0
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +29 -39
- package/scripts/claude_account_worker.py +46 -297
- package/scripts/claude_account_worker_process.py +18 -154
- package/scripts/claude_account_worker_report.py +8 -4
- package/scripts/claude_chain_usage.py +7 -271
- package/scripts/codec_forwarding_test_support.py +11 -65
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
- package/scripts/invoke_code_review.py +34 -56
- package/scripts/resolve_worker_spawn.py +40 -72
- package/scripts/test_account_broker_guard.py +0 -4
- package/scripts/test_claude_account_worker.py +105 -350
- package/scripts/test_claude_chain_usage.py +50 -510
- package/scripts/test_dispatcher_profile_import.py +5 -9
- package/scripts/test_invoke_code_review.py +7 -6
- package/scripts/test_invoke_code_review_chain.py +26 -0
- package/scripts/test_invoke_code_review_cli.py +8 -8
- package/scripts/test_invoke_code_review_codec.py +3 -11
- package/scripts/test_invoke_code_review_contract.py +5 -1
- package/scripts/test_resolve_worker_spawn.py +118 -265
- package/scripts/test_resolve_worker_spawn_codec.py +9 -21
- package/scripts/tests/test_code_review_constants.py +2 -2
- package/scripts/claude_account_choice.py +0 -422
- package/scripts/claude_chain_runner.py +0 -1289
- package/scripts/test_claude_account_choice.py +0 -355
- package/scripts/test_claude_chain_runner.py +0 -1839
|
@@ -138,8 +138,8 @@ Drift signals and the per-host re-spawn / re-bind steps: [`reference/lifecycle.m
|
|
|
138
138
|
|
|
139
139
|
## CLI chain
|
|
140
140
|
|
|
141
|
-
The shared runner is `python "$HOME/.claude/scripts/
|
|
142
|
-
|
|
141
|
+
The shared runner is `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>`.
|
|
142
|
+
Account choice, failover, the tier-to-alias table, brief piping, and `--resume` session handling: [`reference/cli-chain.md`](reference/cli-chain.md).
|
|
143
143
|
|
|
144
144
|
**Third-party host:** the primary bind and consult path; the walk order and fail-closed rule live in [`reference/third-party-bind.md`](reference/third-party-bind.md).
|
|
145
145
|
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# CLI Claude-chain
|
|
2
2
|
|
|
3
3
|
Detail behind the `## CLI chain` section of [`advisor-protocol.md`](../advisor-protocol.md).
|
|
4
|
-
The shared runner is `python "$HOME/.claude/scripts/
|
|
4
|
+
The shared runner is `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>`.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Account choice
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
**Root advisor bind and consult** (the third-party host path): ordered-account mode. A non-usage failure terminates with `terminal_status=advisor_blocked`, exit code 4 on the CLI.
|
|
14
|
-
|
|
15
|
-
**General chain calls** (non-root automation): keep the default usage-ranked mode.
|
|
8
|
+
The broker applies the main account guard and chooses an eligible extra account
|
|
9
|
+
by remaining usage. On a usage-limit response, it tries another account. If no
|
|
10
|
+
account has room, it reports a reset time and exits with code 3. A non-usage
|
|
11
|
+
Claude failure has `JobOutcome.status == "advisor_blocked"`; the CLI exits with
|
|
12
|
+
code 4. The report path receives the broker's decision and attempt events.
|
|
16
13
|
|
|
17
14
|
## Tier-to-alias map
|
|
18
15
|
|
|
@@ -38,8 +35,8 @@ Drop the file once the consult completes.
|
|
|
38
35
|
|
|
39
36
|
## Session resume
|
|
40
37
|
|
|
41
|
-
Read the `session_id` out of the first call's JSON events.
|
|
38
|
+
Read the `session_id` out of the first call's JSON events or `JobOutcome.session_id`.
|
|
42
39
|
Pass it to `-p --resume <session_id> --output-format json` on every later consult — `-p` stays on the resume call too, since it is still a non-interactive invocation.
|
|
43
|
-
A session store belongs to the
|
|
40
|
+
A session store belongs to the account that minted it. The broker keeps account affinity when that account has room. If the account reaches a usage limit, a `--resume` on another account can fail.
|
|
44
41
|
Treat that failure as starting over.
|
|
45
42
|
Resend the charter plus a compact recap of the consults since the last one, capture the new `session_id` the fresh call returns, and continue from there.
|
|
@@ -22,6 +22,6 @@ Every other consumer reaches it by message alone; spawn, respawn, and shutdown b
|
|
|
22
22
|
|
|
23
23
|
The orchestrating session owns the CLI advisor bind for the whole run: first bind, re-bind on drift or lost `session_id`, and fail-closed report when the bound path cannot serve.
|
|
24
24
|
|
|
25
|
-
**Re-bind on drift.** If a reply shows a stale picture, the task pivots, or `--resume` fails after a usage-limit failover
|
|
25
|
+
**Re-bind on drift.** If a reply shows a stale picture, the task pivots, or `--resume` fails after a usage-limit failover, re-bind Opus through `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>` with the charter plus a compact recap of consults so far.
|
|
26
26
|
Capture the new `session_id`, and log a fresh Opus walk with `result: "cli"` on success, then Astra when that rung is open. An Astra re-bind uses `codex_astra_advisor.py --bind --enable-astra` with the same charter and recap, and records `result: "codex"` on success.
|
|
27
27
|
Executors keep reporting to the orchestrating session; advisor binding stays with that session alone.
|
|
@@ -22,15 +22,15 @@ When the Astra flag is off, follow the Claude-chain steps below.
|
|
|
22
22
|
3. **CLI bind (primary path):** for Opus, pipe a charter file into:
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
python "$HOME/.claude/scripts/
|
|
25
|
+
python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude -p --model <alias> --effort <effort> --output-format json
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Use `--model opus --effort` with the value of `ADVISOR_EFFORT` (default `xhigh`) on Opus.
|
|
29
29
|
User-facing wording follows [`rules/asd-ste100-language.md`](../../../rules/asd-ste100-language.md).
|
|
30
|
-
|
|
30
|
+
Account choice, failover, and the `advisor_blocked` status are in [`cli-chain.md`](cli-chain.md).
|
|
31
31
|
4. Stop at the first successful bind.
|
|
32
32
|
Record `{tier, result: "cli"}` for Opus or `{tier: "Astra", result: "codex"}` for the Astra helper, and set `selected_tier` to that tier.
|
|
33
|
-
Persist `session_id` from the JSON events
|
|
33
|
+
Persist `session_id` from the JSON events or `JobOutcome.session_id`; reply text is the `type == "result"` event's `.result` field.
|
|
34
34
|
Run every bind and every later consult with cwd set to the repo root the work is for. Claude sessions are project-scoped by working directory.
|
|
35
35
|
5. **Fail closed:** when every candidate fails (chain exhausted, `advisor_blocked`, or model unavailable), set `selected_tier = null` and a `fallback_reason`, report that the advisor is unreachable, and **stop**. ENDORSE / CORRECTION / PLAN / STOP come only from a bound advisor.
|
|
36
36
|
6. Assemble and paste each executor's Advisor block from [`advisor-block.md`](advisor-block.md). Executors report to the orchestrating session; that session consults the bound advisor and relays the four-signal reply.
|
package/bin/install.test.mjs
CHANGED
|
@@ -216,7 +216,7 @@ test('core includeDirectories ships _shared and scripts for advisor protocol and
|
|
|
216
216
|
);
|
|
217
217
|
assert.ok(
|
|
218
218
|
CORE_INCLUDE_DIRECTORIES.includes('scripts'),
|
|
219
|
-
'scripts must ship with --only core so
|
|
219
|
+
'scripts must ship with --only core so account_broker.py is available for advisor CLI fallback',
|
|
220
220
|
);
|
|
221
221
|
});
|
|
222
222
|
|
|
@@ -9,12 +9,12 @@ keeps its own sign-in and history.
|
|
|
9
9
|
| File | What it does |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `scripts/claude_account_profile.py` | Links each shared entry of `~/.claude` into the profile `~/.claude-profiles/ev`, moves stale copies into `.replaced/<time>/`, and writes the `claude-ev.cmd` launcher into `~/.local/bin` |
|
|
12
|
-
| `scripts/
|
|
13
|
-
| `scripts/claude_account_worker.py` | Runs one headless Claude worker
|
|
14
|
-
| `scripts/claude_account_worker_process.py` |
|
|
12
|
+
| `scripts/account_broker.py` | Reads the account roster and usage meters, chooses an account, and runs Claude with that account's home |
|
|
13
|
+
| `scripts/claude_account_worker.py` | Runs one headless Claude worker through the broker |
|
|
14
|
+
| `scripts/claude_account_worker_process.py` | Sends the brief to the broker and measures the run |
|
|
15
15
|
| `scripts/claude_account_worker_report.py` | Builds and writes the JSON report a worker leaves behind |
|
|
16
16
|
| `scripts/dev_env_scripts_constants/claude_account_worker_constants.py` | The worker's flag names, defaults, exit codes, and report keys |
|
|
17
|
-
| `scripts/dev_env_scripts_constants/claude_account_constants.py` | The profile name, the entries that stay per account, and the
|
|
17
|
+
| `scripts/dev_env_scripts_constants/claude_account_constants.py` | The profile name, the entries that stay per account, and the account thresholds |
|
|
18
18
|
|
|
19
19
|
The launcher sets `CLAUDE_CONFIG_DIR` to the profile and passes every argument to
|
|
20
20
|
`claude`, so `claude-ev -p "..."` runs like `claude -p "..."` on the second
|
|
@@ -43,19 +43,17 @@ to spend leftover usage that expires soon.
|
|
|
43
43
|
|---|---|
|
|
44
44
|
| Main week resets within 24 hours, main under 90% of its week, main under 50% of its 5-hour window | main |
|
|
45
45
|
| Otherwise, second under 95% of its week and under 90% of its 5-hour window | second |
|
|
46
|
-
| Second meter unreadable |
|
|
46
|
+
| Second meter unreadable | wait until the meter can be read |
|
|
47
47
|
| Otherwise | wait, with the next reset time |
|
|
48
48
|
|
|
49
49
|
An unreadable main meter never picks main.
|
|
50
50
|
|
|
51
51
|
```
|
|
52
|
-
python packages/claude-dev-env/scripts/
|
|
53
|
-
{"account": "second", "config_dir": "C:\\Users\\me\\.claude-profiles\\ev", "reason": "...",
|
|
54
|
-
"meters": {"main": {"session_used_percent": 12.0, "session_resets_at": "...",
|
|
55
|
-
"weekly_used_percent": 34.0, "weekly_resets_at": "..."},
|
|
56
|
-
"second": null}}
|
|
52
|
+
python packages/claude-dev-env/scripts/account_broker.py choose --product claude
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
The
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
The JSON output has a `decision` object and an `accounts` list. Each account's
|
|
56
|
+
`meters` holds its remaining percent and reset time for the 5-hour window and
|
|
57
|
+
the week. An unreadable meter appears as `null`. A wait decision has
|
|
58
|
+
`action: "wait"` and a `resets_at` timestamp. The broker exits with code 3 on
|
|
59
|
+
wait. The worker writes the same reset time to its report.
|
|
@@ -18,6 +18,10 @@ It denies a banned word or phrase, matched whole and case-insensitive::
|
|
|
18
18
|
The gate uses the ``banned_words`` list in ``~/.claude/reply-banned-words.json``,
|
|
19
19
|
or in the file that CLAUDE_REPLY_BANNED_WORDS_PATH names. Without a valid
|
|
20
20
|
list there, it uses ALL_DEFAULT_BANNED_WORDS.
|
|
21
|
+
|
|
22
|
+
The banned-word check also reads every text field of a decision card.
|
|
23
|
+
A decision card gets no length check.
|
|
24
|
+
|
|
21
25
|
Each non-empty line counts as its own sentence, so a list counts one
|
|
22
26
|
sentence per item. URLs, markdown link targets, inline code spans, and
|
|
23
27
|
fenced blocks carry no words.
|
|
@@ -52,8 +56,10 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
52
56
|
BANNED_WORDS_JSON_KEY,
|
|
53
57
|
BANNED_WORDS_PATH_ENV_VAR,
|
|
54
58
|
BLOCK_EXIT_CODE,
|
|
59
|
+
CARD_TEXT_SEPARATOR,
|
|
55
60
|
CLAUDE_HOME_DIRECTORY_NAME,
|
|
56
61
|
CONFIG_FILE_ENCODING,
|
|
62
|
+
DECISION_CARD_TOOL_NAME,
|
|
57
63
|
FENCED_BLOCK_PATTERN,
|
|
58
64
|
HOOK_EVENT_NAME,
|
|
59
65
|
INLINE_CODE_PATTERN,
|
|
@@ -163,26 +169,52 @@ def banned_word_violation(reply_text: str, all_banned_words: tuple[str, ...]) ->
|
|
|
163
169
|
return None
|
|
164
170
|
|
|
165
171
|
|
|
172
|
+
def all_card_texts(card_value: object) -> list[str]:
|
|
173
|
+
"""Collect every string inside a decision card's input, in order."""
|
|
174
|
+
if isinstance(card_value, str):
|
|
175
|
+
return [card_value]
|
|
176
|
+
if isinstance(card_value, dict):
|
|
177
|
+
return [
|
|
178
|
+
each_text
|
|
179
|
+
for each_value in card_value.values()
|
|
180
|
+
for each_text in all_card_texts(each_value)
|
|
181
|
+
]
|
|
182
|
+
if isinstance(card_value, list):
|
|
183
|
+
return [each_text for each_value in card_value for each_text in all_card_texts(each_value)]
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def tool_violation(tool_name: object, all_tool_input: dict[str, object]) -> tuple[str, str] | None:
|
|
188
|
+
"""Return the deny reason and the checked text for one call, or None when it passes."""
|
|
189
|
+
if tool_name == DECISION_CARD_TOOL_NAME:
|
|
190
|
+
card_text = CARD_TEXT_SEPARATOR.join(all_card_texts(all_tool_input))
|
|
191
|
+
card_violation = banned_word_violation(card_text, configured_banned_words())
|
|
192
|
+
return None if card_violation is None else (card_violation, card_text)
|
|
193
|
+
if tool_name not in ALL_CHECKED_TOOL_NAMES:
|
|
194
|
+
return None
|
|
195
|
+
reply_text = all_tool_input.get(TEXT_KEY)
|
|
196
|
+
if not isinstance(reply_text, str):
|
|
197
|
+
return None
|
|
198
|
+
violation = (
|
|
199
|
+
length_violation(reply_text)
|
|
200
|
+
or unlinked_pull_request_violation(reply_text)
|
|
201
|
+
or banned_word_violation(reply_text, configured_banned_words())
|
|
202
|
+
)
|
|
203
|
+
return None if violation is None else (violation, reply_text)
|
|
204
|
+
|
|
205
|
+
|
|
166
206
|
def main() -> int:
|
|
167
207
|
hook_input = read_hook_input_dictionary_from_stdin()
|
|
168
208
|
if hook_input is None:
|
|
169
209
|
return ALLOW_EXIT_CODE
|
|
170
210
|
tool_name = hook_input.get(TOOL_NAME_KEY)
|
|
171
|
-
if tool_name not in ALL_CHECKED_TOOL_NAMES:
|
|
172
|
-
return ALLOW_EXIT_CODE
|
|
173
211
|
tool_input = hook_input.get(TOOL_INPUT_KEY)
|
|
174
212
|
if not isinstance(tool_input, dict):
|
|
175
213
|
return ALLOW_EXIT_CODE
|
|
176
|
-
|
|
177
|
-
if
|
|
178
|
-
return ALLOW_EXIT_CODE
|
|
179
|
-
violation = (
|
|
180
|
-
length_violation(reply_text)
|
|
181
|
-
or unlinked_pull_request_violation(reply_text)
|
|
182
|
-
or banned_word_violation(reply_text, configured_banned_words())
|
|
183
|
-
)
|
|
184
|
-
if violation is None:
|
|
214
|
+
checked = tool_violation(tool_name, tool_input)
|
|
215
|
+
if checked is None:
|
|
185
216
|
return ALLOW_EXIT_CODE
|
|
217
|
+
violation, reply_text = checked
|
|
186
218
|
block_reason = violation + RETRY_INSTRUCTION
|
|
187
219
|
log_hook_block(
|
|
188
220
|
Path(__file__).name,
|
|
@@ -322,3 +322,58 @@ def test_should_keep_the_defaults_when_the_config_file_is_malformed(
|
|
|
322
322
|
config_path.write_text("{not json", encoding="utf-8")
|
|
323
323
|
exit_code, _ = run_gate(monkeypatch, capsys, REPLY_TOOL_NAME, {"text": "It likely passed."})
|
|
324
324
|
assert exit_code == 2
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
DECISION_TOOL_NAME = "mcp__hearthbot__ask_decision"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_should_deny_a_decision_card_that_hedges_in_an_option(
|
|
331
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
332
|
+
) -> None:
|
|
333
|
+
exit_code, stderr_text = run_gate(
|
|
334
|
+
monkeypatch,
|
|
335
|
+
capsys,
|
|
336
|
+
DECISION_TOOL_NAME,
|
|
337
|
+
{
|
|
338
|
+
"question": "Exclude the call button?",
|
|
339
|
+
"context": "Crops show the call button flagged in 7 themes.",
|
|
340
|
+
"options": [
|
|
341
|
+
{"label": "Exclude it", "consequence": "Three themes probably pass."},
|
|
342
|
+
{"label": "Leave it", "consequence": "Three themes keep a warning."},
|
|
343
|
+
],
|
|
344
|
+
"recommended": 0,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
assert exit_code == 2
|
|
348
|
+
assert '"probably"' in stderr_text
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_should_allow_a_long_decision_card_with_no_hedge(
|
|
352
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
353
|
+
) -> None:
|
|
354
|
+
exit_code, stderr_text = run_gate(
|
|
355
|
+
monkeypatch,
|
|
356
|
+
capsys,
|
|
357
|
+
DECISION_TOOL_NAME,
|
|
358
|
+
{
|
|
359
|
+
"question": "Exclude the call button?",
|
|
360
|
+
"context": " ".join([FIFTEEN_WORD_SENTENCE] * 5),
|
|
361
|
+
"options": [
|
|
362
|
+
{"label": "Exclude it", "consequence": SIXTEEN_WORD_SENTENCE},
|
|
363
|
+
{"label": "Leave it", "consequence": "Three themes keep a warning."},
|
|
364
|
+
],
|
|
365
|
+
"recommended": 0,
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
assert (exit_code, stderr_text) == (0, "")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@pytest.mark.parametrize(
|
|
372
|
+
"reply_text",
|
|
373
|
+
["It maybe passed.", "Perhaps it passed.", "It might be the cache.", "I am not sure it passed."],
|
|
374
|
+
)
|
|
375
|
+
def test_should_deny_the_hedges_the_default_list_adds(
|
|
376
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], reply_text: str
|
|
377
|
+
) -> None:
|
|
378
|
+
exit_code, _ = run_gate(monkeypatch, capsys, REPLY_TOOL_NAME, {"text": reply_text})
|
|
379
|
+
assert exit_code == 2
|
package/hooks/hooks.json
CHANGED
|
@@ -5,6 +5,8 @@ import re
|
|
|
5
5
|
MAXIMUM_SENTENCE_COUNT = 3
|
|
6
6
|
MAXIMUM_WORDS_PER_SENTENCE = 15
|
|
7
7
|
ALL_CHECKED_TOOL_NAMES = frozenset({"mcp__hearthbot__reply", "mcp__hearthbot__post_message"})
|
|
8
|
+
DECISION_CARD_TOOL_NAME = "mcp__hearthbot__ask_decision"
|
|
9
|
+
CARD_TEXT_SEPARATOR = "\n"
|
|
8
10
|
TOOL_NAME_KEY = "tool_name"
|
|
9
11
|
TOOL_INPUT_KEY = "tool_input"
|
|
10
12
|
TEXT_KEY = "text"
|
|
@@ -45,6 +47,15 @@ ALL_DEFAULT_BANNED_WORDS = (
|
|
|
45
47
|
"suspect",
|
|
46
48
|
"guess",
|
|
47
49
|
"my theory",
|
|
50
|
+
"maybe",
|
|
51
|
+
"perhaps",
|
|
52
|
+
"presumably",
|
|
53
|
+
"might be",
|
|
54
|
+
"may be",
|
|
55
|
+
"appears to",
|
|
56
|
+
"i believe",
|
|
57
|
+
"not sure",
|
|
58
|
+
"unsure",
|
|
48
59
|
)
|
|
49
60
|
BANNED_WORD_PART_SEPARATOR = r"\s+"
|
|
50
61
|
BANNED_WORD_PATTERN_TEMPLATE = r"(?<![A-Za-z0-9]){word_pattern}(?![A-Za-z0-9])"
|
package/package.json
CHANGED
|
@@ -24,13 +24,8 @@ _SCRIPTS_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
|
24
24
|
if _SCRIPTS_DIRECTORY not in sys.path:
|
|
25
25
|
sys.path.insert(0, _SCRIPTS_DIRECTORY)
|
|
26
26
|
|
|
27
|
-
import claude_chain_runner as chain_runner # noqa: E402
|
|
28
27
|
import invoke_code_review as invoker # noqa: E402
|
|
29
|
-
from
|
|
30
|
-
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
31
|
-
TERMINAL_STATUS_CHAIN_EXHAUSTED,
|
|
32
|
-
TERMINAL_STATUS_SERVED,
|
|
33
|
-
)
|
|
28
|
+
from dev_env_scripts_constants.account_broker_constants import JobOutcome, Product, WAIT_EXIT_CODE
|
|
34
29
|
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
35
30
|
CLI_SESSION_MODEL_FLAG,
|
|
36
31
|
CODE_REVIEW_MODEL_ALIAS,
|
|
@@ -111,27 +106,29 @@ def claude_served(
|
|
|
111
106
|
*,
|
|
112
107
|
returncode: int = FIXTURE_CHAIN_RETURNCODE,
|
|
113
108
|
stdout: str = FIXTURE_CHAIN_STDOUT,
|
|
114
|
-
) ->
|
|
115
|
-
return
|
|
116
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
109
|
+
) -> JobOutcome:
|
|
110
|
+
return JobOutcome(
|
|
117
111
|
returncode=returncode,
|
|
118
112
|
stdout=stdout,
|
|
119
113
|
stderr="",
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
account_name=FIXTURE_SERVED_COMMAND,
|
|
115
|
+
attempts=((FIXTURE_SERVED_COMMAND, "served"),),
|
|
116
|
+
status="served" if returncode == 0 else "advisor_blocked",
|
|
117
|
+
session_id=None,
|
|
118
|
+
wait_reset_at=None,
|
|
122
119
|
)
|
|
123
120
|
|
|
124
121
|
|
|
125
|
-
def claude_failed() ->
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
122
|
+
def claude_failed() -> JobOutcome:
|
|
123
|
+
return JobOutcome(
|
|
124
|
+
returncode=WAIT_EXIT_CODE,
|
|
129
125
|
stdout="",
|
|
130
|
-
stderr="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
stderr="",
|
|
127
|
+
account_name=None,
|
|
128
|
+
attempts=((FIXTURE_SERVED_COMMAND, "usage_limited"),),
|
|
129
|
+
status="exhausted",
|
|
130
|
+
session_id=None,
|
|
131
|
+
wait_reset_at=None,
|
|
135
132
|
)
|
|
136
133
|
|
|
137
134
|
|
|
@@ -179,7 +176,7 @@ class SeamCallLog:
|
|
|
179
176
|
@dataclass
|
|
180
177
|
class _SeamConfiguration:
|
|
181
178
|
host_profile: str
|
|
182
|
-
claude_outcome:
|
|
179
|
+
claude_outcome: JobOutcome | BaseException | None
|
|
183
180
|
should_dirty_tree_on_chain: bool
|
|
184
181
|
working_directory: Path | None
|
|
185
182
|
|
|
@@ -220,23 +217,21 @@ def _build_host_profile_seam(
|
|
|
220
217
|
|
|
221
218
|
def _build_claude_seam(
|
|
222
219
|
call_log: SeamCallLog, configuration: _SeamConfiguration
|
|
223
|
-
) -> Callable[...,
|
|
220
|
+
) -> Callable[..., JobOutcome]:
|
|
224
221
|
def fake_claude(
|
|
225
|
-
all_claude_arguments: list[str],
|
|
226
|
-
) ->
|
|
222
|
+
product: Product, all_claude_arguments: list[str], **options: object
|
|
223
|
+
) -> JobOutcome:
|
|
224
|
+
assert product is Product.CLAUDE
|
|
227
225
|
call_log.claude_calls += 1
|
|
228
|
-
call_log.claude_arguments = list(all_claude_arguments)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
timeout=timeout_seconds,
|
|
234
|
-
check=False,
|
|
235
|
-
)
|
|
226
|
+
call_log.claude_arguments = list(all_claude_arguments[1:])
|
|
227
|
+
call_log.is_stdin_empty = options.get("stdin_text") == ""
|
|
228
|
+
if options.get("cwd") is not None:
|
|
229
|
+
call_log.claude_working_directory = Path(options["cwd"])
|
|
230
|
+
call_log.all_observed_working_directories.append(Path(options["cwd"]))
|
|
236
231
|
_record_dirty_tree(configuration)
|
|
237
232
|
if isinstance(configuration.claude_outcome, BaseException):
|
|
238
233
|
raise configuration.claude_outcome
|
|
239
|
-
assert isinstance(configuration.claude_outcome,
|
|
234
|
+
assert isinstance(configuration.claude_outcome, JobOutcome)
|
|
240
235
|
return configuration.claude_outcome
|
|
241
236
|
|
|
242
237
|
return fake_claude
|
|
@@ -284,18 +279,13 @@ def _apply_seams(
|
|
|
284
279
|
"review_claude_runner",
|
|
285
280
|
_build_claude_seam(call_log, configuration),
|
|
286
281
|
)
|
|
287
|
-
monkeypatch.setattr(
|
|
288
|
-
chain_runner,
|
|
289
|
-
"chain_subprocess_runner",
|
|
290
|
-
_build_subprocess_seam(call_log),
|
|
291
|
-
)
|
|
292
282
|
|
|
293
283
|
|
|
294
284
|
def install_seams(
|
|
295
285
|
monkeypatch: pytest.MonkeyPatch,
|
|
296
286
|
*,
|
|
297
287
|
host_profile: str = HOST_PROFILE_CLAUDE,
|
|
298
|
-
claude_outcome:
|
|
288
|
+
claude_outcome: JobOutcome | BaseException | None = None,
|
|
299
289
|
should_dirty_tree_on_chain: bool = False,
|
|
300
290
|
working_directory: Path | None = None,
|
|
301
291
|
) -> SeamCallLog:
|