claude-dev-env 8.69.0 → 8.69.1
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/pr-lifecycle/SKILL.md +3 -1
- package/.agents/skills/second-account-workers/SKILL.md +21 -3
- package/docs/rule-guides/features-start-with-an-eval.md +40 -0
- package/hooks/blocking/followup_pr_dedupe.py +55 -5
- package/hooks/blocking/pr_lifecycle_skill_gate.py +62 -11
- package/hooks/blocking/pull_request_proof.py +86 -20
- package/hooks/blocking/test_artifact_look_gate.py +1 -0
- package/hooks/blocking/test_followup_pr_dedupe.py +33 -0
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +117 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -5
- package/hooks/blocking/test_pull_request_proof.py +124 -1
- package/hooks/features/conduct-gates.md +1 -1
- package/hooks/features/write-edit-blocking.md +3 -5
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/artifact_look_gate_constants.py +1 -2
- package/hooks/hooks_constants/followup_pr_dedupe_constants.py +4 -0
- package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +3 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pull_request_proof_constants.py +20 -1
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +4 -11
- package/hooks/hooks_constants/transcript_skill_scan_constants.py +1 -0
- package/hooks/test_transcript_skill_scan.py +75 -1
- package/hooks/transcript_skill_scan.py +87 -15
- package/package.json +1 -1
- package/rules/anti-corollary-tests.md +2 -2
- package/rules/archiving-agent-config.md +2 -2
- package/rules/asd-ste100-language.md +2 -2
- package/rules/bdd.md +1 -1
- package/rules/build-needs-a-user.md +1 -1
- package/rules/cleanup-temp-files.md +3 -3
- package/rules/code-standards.md +1 -1
- package/rules/correction-lens.md +3 -3
- package/rules/destructive-commands.md +3 -3
- package/rules/doc-inventory-integrity.md +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/explore-thoroughly.md +1 -1
- package/rules/failure-blast-radius.md +1 -1
- package/rules/falsify-before-green.md +2 -2
- package/rules/features-start-with-an-eval.md +9 -0
- package/rules/filesystem-search.md +3 -3
- package/rules/flag-non-breaking-findings.md +2 -2
- package/rules/long-horizon-autonomy.md +2 -2
- package/rules/memory-stores-durable-facts.md +2 -2
- package/rules/no-contrast-framing.md +1 -1
- package/rules/no-cross-skill-duplicate-helpers.md +1 -1
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/plain-illustrative-docstrings.md +1 -1
- package/rules/prefer-existing-tools.md +2 -2
- package/rules/proof-before-pull-request.md +1 -1
- package/rules/request-scope.md +1 -1
- package/rules/research-mode.md +2 -2
- package/rules/retired-hook-prose.md +1 -1
- package/rules/session-title.md +1 -1
- package/rules/shell-invocation.md +2 -2
- package/rules/testing.md +1 -1
- package/rules/verify-before-asking.md +3 -3
- package/rules/verify-runtime-state.md +2 -2
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/account_broker.py +10 -3
- package/scripts/account_broker_support.py +7 -3
- package/scripts/claude_account_worker.py +24 -5
- package/scripts/claude_account_worker_process.py +5 -0
- package/scripts/claude_account_worker_report.py +16 -1
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +12 -0
- package/scripts/test_account_broker.py +38 -0
- package/scripts/test_account_broker_support.py +24 -0
- package/scripts/test_claude_account_worker.py +47 -0
- package/scripts/test_claude_account_worker_process.py +20 -0
- package/scripts/test_claude_account_worker_report.py +15 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +4 -4
- package/scripts/tests/test_rule_load_scopes.py +1 -0
- package/hooks/advisory/conftest.py +0 -10
- package/hooks/advisory/refactor_guard.py +0 -385
- package/hooks/advisory/refactor_guard_test_support.py +0 -46
- package/hooks/advisory/test_refactor_guard.py +0 -148
- package/hooks/advisory/test_refactor_guard_advisory.py +0 -242
- package/hooks/advisory/test_refactor_guard_eligibility.py +0 -166
- package/hooks/hooks_constants/refactor_guard_constants.py +0 -77
- package/hooks/hooks_constants/test_refactor_guard_constants.py +0 -21
|
@@ -19,15 +19,19 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
19
19
|
DEFAULT_TIMEOUT_MINUTES,
|
|
20
20
|
INVALID_TIMEOUT_MESSAGE,
|
|
21
21
|
LAUNCH_FAILURE_EXIT_CODE,
|
|
22
|
+
LIVE_LOG_FLAG,
|
|
22
23
|
MINIMUM_TIMEOUT_MINUTES,
|
|
23
24
|
MODEL_FLAG,
|
|
24
25
|
OUTPUT_FORMAT_FLAG,
|
|
25
26
|
OUTPUT_FORMAT_JSON,
|
|
27
|
+
OUTPUT_FORMAT_STREAM_JSON,
|
|
26
28
|
PERMISSION_MODE_FLAG,
|
|
27
29
|
PROMPT_FILE_FLAG,
|
|
28
30
|
REPORT_FILE_FLAG,
|
|
29
31
|
SINGLE_PROMPT_FLAG,
|
|
30
32
|
TIMEOUT_MINUTES_FLAG,
|
|
33
|
+
UNWRITABLE_LIVE_LOG_MESSAGE,
|
|
34
|
+
VERBOSE_FLAG,
|
|
31
35
|
UTF8_ENCODING,
|
|
32
36
|
)
|
|
33
37
|
|
|
@@ -50,15 +54,16 @@ def build_argument_parser() -> argparse.ArgumentParser:
|
|
|
50
54
|
parser.add_argument(MODEL_FLAG)
|
|
51
55
|
parser.add_argument(PERMISSION_MODE_FLAG, default=DEFAULT_PERMISSION_MODE)
|
|
52
56
|
parser.add_argument(TIMEOUT_MINUTES_FLAG, type=_positive_timeout_minutes, default=DEFAULT_TIMEOUT_MINUTES)
|
|
57
|
+
parser.add_argument(LIVE_LOG_FLAG, type=Path)
|
|
53
58
|
return parser
|
|
54
59
|
|
|
55
60
|
|
|
56
|
-
def _invocation(*, model: str | None, permission_mode: str) -> list[str]:
|
|
61
|
+
def _invocation(*, model: str | None, permission_mode: str, is_streamed: bool) -> list[str]:
|
|
62
|
+
format_flags = [OUTPUT_FORMAT_FLAG, OUTPUT_FORMAT_STREAM_JSON, VERBOSE_FLAG] if is_streamed else [OUTPUT_FORMAT_FLAG, OUTPUT_FORMAT_JSON]
|
|
57
63
|
arguments = [
|
|
58
64
|
CLAUDE_BINARY_NAME,
|
|
59
65
|
SINGLE_PROMPT_FLAG,
|
|
60
|
-
|
|
61
|
-
OUTPUT_FORMAT_JSON,
|
|
66
|
+
*format_flags,
|
|
62
67
|
PERMISSION_MODE_FLAG,
|
|
63
68
|
permission_mode,
|
|
64
69
|
]
|
|
@@ -81,20 +86,33 @@ def run_worker(
|
|
|
81
86
|
model: str | None,
|
|
82
87
|
permission_mode: str,
|
|
83
88
|
timeout_minutes: int,
|
|
89
|
+
live_log: Path | None = None,
|
|
84
90
|
) -> int:
|
|
85
|
-
"""Run one worker and write its structured report.
|
|
91
|
+
"""Run one worker and write its structured report.
|
|
92
|
+
|
|
93
|
+
With ``live_log`` set, the worker streams one JSON event per line into that
|
|
94
|
+
file while it runs, so a watching session can read progress before the end.
|
|
95
|
+
A live log that cannot be written ends the run before the broker starts.
|
|
96
|
+
"""
|
|
86
97
|
try:
|
|
87
98
|
prompt_text = prompt_file.read_text(encoding=UTF8_ENCODING)
|
|
88
99
|
except (OSError, UnicodeError):
|
|
89
100
|
report = pre_launch_failure_report("none", "prompt file is unreadable", LAUNCH_FAILURE_EXIT_CODE)
|
|
90
101
|
return finalize_report(report_file, report)
|
|
102
|
+
if live_log is not None:
|
|
103
|
+
try:
|
|
104
|
+
live_log.write_bytes(b"")
|
|
105
|
+
except OSError:
|
|
106
|
+
report = pre_launch_failure_report("none", UNWRITABLE_LIVE_LOG_MESSAGE, LAUNCH_FAILURE_EXIT_CODE)
|
|
107
|
+
return finalize_report(report_file, report)
|
|
91
108
|
try:
|
|
92
109
|
outcome, duration_seconds = invoke_worker(
|
|
93
|
-
all_arguments=_invocation(model=model, permission_mode=permission_mode),
|
|
110
|
+
all_arguments=_invocation(model=model, permission_mode=permission_mode, is_streamed=live_log is not None),
|
|
94
111
|
cwd=cwd,
|
|
95
112
|
prompt_text=prompt_text,
|
|
96
113
|
timeout_minutes=timeout_minutes,
|
|
97
114
|
runner=worker_job_runner,
|
|
115
|
+
live_log=live_log,
|
|
98
116
|
)
|
|
99
117
|
except BrokerConfigurationError as error:
|
|
100
118
|
report = pre_launch_failure_report("none", str(error), CONFIGURATION_FAILURE_EXIT_CODE)
|
|
@@ -122,6 +140,7 @@ def main() -> int:
|
|
|
122
140
|
model=arguments.model,
|
|
123
141
|
permission_mode=arguments.permission_mode,
|
|
124
142
|
timeout_minutes=arguments.timeout_minutes,
|
|
143
|
+
live_log=arguments.live_log,
|
|
125
144
|
)
|
|
126
145
|
|
|
127
146
|
|
|
@@ -17,6 +17,7 @@ def invoke_worker(
|
|
|
17
17
|
prompt_text: str,
|
|
18
18
|
timeout_minutes: int,
|
|
19
19
|
runner: Callable[..., JobOutcome],
|
|
20
|
+
live_log: Path | None = None,
|
|
20
21
|
monotonic_clock: Callable[[], float] = time.monotonic,
|
|
21
22
|
) -> tuple[JobOutcome, float]:
|
|
22
23
|
"""Run one broker job and measure its elapsed time.
|
|
@@ -28,12 +29,15 @@ def invoke_worker(
|
|
|
28
29
|
timeout_minutes: Job time limit in minutes; the runner receives it in seconds.
|
|
29
30
|
runner: Broker job runner, called with ``Product.CLAUDE``,
|
|
30
31
|
``encoding="utf-8"``, and ``errors="replace"``.
|
|
32
|
+
live_log: File the runner writes the job's standard output into as it
|
|
33
|
+
runs, passed only when set.
|
|
31
34
|
monotonic_clock: Clock read once before and once after the job.
|
|
32
35
|
|
|
33
36
|
Returns:
|
|
34
37
|
The runner's job outcome and the difference between the two clock reads.
|
|
35
38
|
"""
|
|
36
39
|
started_at = monotonic_clock()
|
|
40
|
+
all_log_options = {} if live_log is None else {"live_log": live_log}
|
|
37
41
|
outcome = runner(
|
|
38
42
|
Product.CLAUDE,
|
|
39
43
|
all_arguments,
|
|
@@ -42,5 +46,6 @@ def invoke_worker(
|
|
|
42
46
|
cwd=cwd,
|
|
43
47
|
encoding="utf-8",
|
|
44
48
|
errors="replace",
|
|
49
|
+
**all_log_options,
|
|
45
50
|
)
|
|
46
51
|
return outcome, monotonic_clock() - started_at
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
11
11
|
DURATION_DECIMAL_PLACES,
|
|
12
|
+
JSON_EVENT_TYPE_KEY,
|
|
12
13
|
JSON_IS_ERROR_KEY,
|
|
13
14
|
JSON_RESULT_KEY,
|
|
14
15
|
REPORT_ACCOUNT_KEY,
|
|
@@ -18,6 +19,7 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
18
19
|
REPORT_IS_ERROR_KEY,
|
|
19
20
|
REPORT_REASON_KEY,
|
|
20
21
|
REPORT_RESULT_KEY,
|
|
22
|
+
RESULT_EVENT_TYPE,
|
|
21
23
|
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
22
24
|
SUMMARY_LINE_TEMPLATE,
|
|
23
25
|
UTF8_ENCODING,
|
|
@@ -41,11 +43,24 @@ def _bounded_stdout_tail(stdout_text: str) -> str:
|
|
|
41
43
|
return stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
def _closing_event(stdout_text: str) -> dict[str, object] | None:
|
|
47
|
+
for each_line in reversed(stdout_text.splitlines()):
|
|
48
|
+
try:
|
|
49
|
+
parsed_line = json.loads(each_line)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(parsed_line, dict) and parsed_line.get(JSON_EVENT_TYPE_KEY) == RESULT_EVENT_TYPE:
|
|
53
|
+
return parsed_line
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
44
57
|
def _extract_payload(stdout_text: str) -> tuple[object, bool]:
|
|
45
58
|
try:
|
|
46
59
|
parsed_stdout = json.loads(stdout_text)
|
|
47
60
|
except json.JSONDecodeError:
|
|
48
|
-
|
|
61
|
+
parsed_stdout = _closing_event(stdout_text)
|
|
62
|
+
if parsed_stdout is None:
|
|
63
|
+
return _bounded_stdout_tail(stdout_text), True
|
|
49
64
|
if not isinstance(parsed_stdout, dict) or JSON_RESULT_KEY not in parsed_stdout:
|
|
50
65
|
return None, True
|
|
51
66
|
return parsed_stdout[JSON_RESULT_KEY], parsed_stdout.get(JSON_IS_ERROR_KEY) is True
|
|
@@ -43,6 +43,16 @@ OUTPUT_FORMAT_FLAG: str = "--output-format"
|
|
|
43
43
|
|
|
44
44
|
OUTPUT_FORMAT_JSON: str = "json"
|
|
45
45
|
|
|
46
|
+
OUTPUT_FORMAT_STREAM_JSON: str = "stream-json"
|
|
47
|
+
|
|
48
|
+
VERBOSE_FLAG: str = "--verbose"
|
|
49
|
+
|
|
50
|
+
LIVE_LOG_FLAG: str = "--live-log"
|
|
51
|
+
|
|
52
|
+
JSON_EVENT_TYPE_KEY: str = "type"
|
|
53
|
+
|
|
54
|
+
RESULT_EVENT_TYPE: str = "result"
|
|
55
|
+
|
|
46
56
|
SINGLE_PROMPT_FLAG: str = "-p"
|
|
47
57
|
|
|
48
58
|
SECONDS_PER_MINUTE: int = 60
|
|
@@ -67,6 +77,8 @@ SUMMARY_LINE_TEMPLATE: str = "account={account} exit_code={exit_code} report={re
|
|
|
67
77
|
|
|
68
78
|
INVALID_TIMEOUT_MESSAGE: str = "--timeout-minutes must be at least 1"
|
|
69
79
|
|
|
80
|
+
UNWRITABLE_LIVE_LOG_MESSAGE: str = "live log file is unwritable"
|
|
81
|
+
|
|
70
82
|
|
|
71
83
|
__all__ = [
|
|
72
84
|
"CLAUDE_BINARY_NAME",
|
|
@@ -909,6 +909,26 @@ def test_should_keep_a_spent_mark_without_a_reset_for_one_hour(
|
|
|
909
909
|
assert decision["resets_at"] == (NOW + timedelta(hours=1)).isoformat()
|
|
910
910
|
|
|
911
911
|
|
|
912
|
+
def test_should_mark_a_windows_launcher_path_account_spent(
|
|
913
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
914
|
+
) -> None:
|
|
915
|
+
launcher_name = "C:\\Users\\someone\\.local\\bin\\claude-first.cmd"
|
|
916
|
+
first = _account(launcher_name)
|
|
917
|
+
second = _account("second")
|
|
918
|
+
monkeypatch.setitem(
|
|
919
|
+
account_broker.all_product_adapters,
|
|
920
|
+
Product.CODEX,
|
|
921
|
+
_adapter((first, second), {launcher_name: _meters(80, 80), "second": _meters(80, 80)}),
|
|
922
|
+
)
|
|
923
|
+
_freeze_clock(monkeypatch, NOW)
|
|
924
|
+
|
|
925
|
+
code = account_broker.main(("choose", "--product", "codex", "--spent", f"{launcher_name}:{(NOW + timedelta(hours=2)).timestamp()}"))
|
|
926
|
+
|
|
927
|
+
decision = json.loads(capsys.readouterr().out)["decision"]
|
|
928
|
+
assert code == 0
|
|
929
|
+
assert decision["account"] == "second"
|
|
930
|
+
|
|
931
|
+
|
|
912
932
|
def test_should_reject_an_invalid_spent_reset(
|
|
913
933
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
914
934
|
) -> None:
|
|
@@ -1194,3 +1214,21 @@ def test_should_keep_the_parent_environment_for_codex_jobs(
|
|
|
1194
1214
|
run_job(Product.CODEX, ("codex", "exec", "task"))
|
|
1195
1215
|
|
|
1196
1216
|
assert all_child_environments[0]["CLAUDE_CODE_SESSION_ID"] == "parent-session"
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def test_should_hand_the_live_log_to_the_subprocess_runner(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1221
|
+
live_log = tmp_path / "events.jsonl"
|
|
1222
|
+
adapter = _adapter((_account("only"),), {"only": _meters(80, 80)})
|
|
1223
|
+
captured_options: dict[str, object] = {}
|
|
1224
|
+
|
|
1225
|
+
def runner(command: object, **options: object) -> subprocess.CompletedProcess[str]:
|
|
1226
|
+
captured_options.update(options)
|
|
1227
|
+
return subprocess.CompletedProcess(command, 0, "served", "")
|
|
1228
|
+
|
|
1229
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, adapter)
|
|
1230
|
+
with account_broker.override_subprocess_runner(runner):
|
|
1231
|
+
outcome, _ = account_broker._execute(Product.CODEX, ("job",), now=NOW, live_log=live_log)
|
|
1232
|
+
|
|
1233
|
+
assert outcome.returncode == 0
|
|
1234
|
+
assert captured_options["live_log"] == live_log
|
|
@@ -419,3 +419,27 @@ def test_should_pass_metacharacters_to_an_executable_unchanged(monkeypatch: pyte
|
|
|
419
419
|
support.subprocess_runner(["claude", "-p", 'say "a & b" | 100%!'])
|
|
420
420
|
|
|
421
421
|
assert all_launched_argv == [[resolved_command, "-p", 'say "a & b" | 100%!']]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_should_write_child_output_into_the_live_log_while_it_runs(tmp_path: Path) -> None:
|
|
425
|
+
live_log = tmp_path / "worker.jsonl"
|
|
426
|
+
child_script = "import sys, pathlib; print('first', flush=True); sys.stdout.write(pathlib.Path(sys.argv[1]).read_text())"
|
|
427
|
+
|
|
428
|
+
completed = support._run_captured_subprocess([sys.executable, "-c", child_script, str(live_log)], live_log=live_log)
|
|
429
|
+
|
|
430
|
+
assert completed.stdout == "first\nfirst\n"
|
|
431
|
+
assert live_log.read_text(encoding="utf-8") == "first\nfirst\n"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_should_match_order_names_to_an_account_named_by_its_windows_launcher_path() -> None:
|
|
435
|
+
launcher_account = Account(
|
|
436
|
+
Product.CLAUDE,
|
|
437
|
+
"C:\\Users\\someone\\.local\\bin\\claude-editor.cmd",
|
|
438
|
+
Path("/profiles/editor"),
|
|
439
|
+
False,
|
|
440
|
+
"C:\\Users\\someone\\.local\\bin\\claude-editor.cmd",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
ranked = support._with_priorities((launcher_account,), ("claude-org-jon", "claude-editor"))
|
|
444
|
+
|
|
445
|
+
assert ranked[0].priority == 1
|
|
@@ -45,6 +45,7 @@ def _run_worker(
|
|
|
45
45
|
*,
|
|
46
46
|
model: str | None = None,
|
|
47
47
|
permission_mode: str = "auto",
|
|
48
|
+
live_log: Path | None = None,
|
|
48
49
|
) -> tuple[int, dict[str, object], dict[str, object]]:
|
|
49
50
|
prompt_file = tmp_path / "brief.md"
|
|
50
51
|
prompt_file.write_text("standalone brief", encoding="utf-8")
|
|
@@ -65,6 +66,7 @@ def _run_worker(
|
|
|
65
66
|
model=model,
|
|
66
67
|
permission_mode=permission_mode,
|
|
67
68
|
timeout_minutes=60,
|
|
69
|
+
live_log=live_log,
|
|
68
70
|
)
|
|
69
71
|
return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
|
|
70
72
|
|
|
@@ -88,6 +90,51 @@ def test_should_send_prompt_and_flags_to_broker(
|
|
|
88
90
|
assert captured["timeout_seconds"] == 3600
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
def test_should_stream_events_into_the_live_log_when_one_is_named(
|
|
94
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
95
|
+
) -> None:
|
|
96
|
+
live_log = tmp_path / "worker.jsonl"
|
|
97
|
+
|
|
98
|
+
exit_code, report, captured = _run_worker(monkeypatch, tmp_path, _outcome(), live_log=live_log)
|
|
99
|
+
|
|
100
|
+
assert exit_code == 0
|
|
101
|
+
assert report["result"] == "ready"
|
|
102
|
+
assert captured["argv"][:5] == ["claude", "-p", "--output-format", "stream-json", "--verbose"]
|
|
103
|
+
assert captured["live_log"] == live_log
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_pass_no_live_log_when_none_is_named(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
108
|
+
) -> None:
|
|
109
|
+
_, _, captured = _run_worker(monkeypatch, tmp_path, _outcome())
|
|
110
|
+
|
|
111
|
+
assert "live_log" not in captured
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_report_an_unwritable_live_log_before_the_broker_runs(
|
|
115
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
116
|
+
) -> None:
|
|
117
|
+
prompt_file = tmp_path / "brief.md"
|
|
118
|
+
prompt_file.write_text("standalone brief", encoding="utf-8")
|
|
119
|
+
report_file = tmp_path / "report.json"
|
|
120
|
+
monkeypatch.setattr(worker, "worker_job_runner", lambda *args, **kwargs: pytest.fail("broker ran"))
|
|
121
|
+
|
|
122
|
+
exit_code = worker.run_worker(
|
|
123
|
+
prompt_file=prompt_file,
|
|
124
|
+
cwd=tmp_path,
|
|
125
|
+
report_file=report_file,
|
|
126
|
+
model=None,
|
|
127
|
+
permission_mode="auto",
|
|
128
|
+
timeout_minutes=60,
|
|
129
|
+
live_log=tmp_path / "missing" / "worker.jsonl",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
133
|
+
assert exit_code == report["exit_code"] == 1
|
|
134
|
+
assert report["account"] == "none"
|
|
135
|
+
assert report["reason"] == "live log file is unwritable"
|
|
136
|
+
|
|
137
|
+
|
|
91
138
|
def test_should_report_the_broker_wait_reason_and_exit_three(
|
|
92
139
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
93
140
|
) -> None:
|
|
@@ -45,3 +45,23 @@ def test_should_send_job_to_runner_and_return_outcome_with_elapsed_seconds(
|
|
|
45
45
|
},
|
|
46
46
|
)
|
|
47
47
|
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_pass_the_live_log_to_the_runner_when_one_is_named(tmp_path: Path) -> None:
|
|
51
|
+
live_log = tmp_path / "events.jsonl"
|
|
52
|
+
captured_options: dict[str, object] = {}
|
|
53
|
+
|
|
54
|
+
def recording_runner(product: Product, argv: list[str], **options: object) -> JobOutcome:
|
|
55
|
+
captured_options.update(options)
|
|
56
|
+
return JobOutcome(0, "{}", "", "main", (), "served", None, None)
|
|
57
|
+
|
|
58
|
+
invoke_worker(
|
|
59
|
+
all_arguments=["claude", "-p"],
|
|
60
|
+
cwd=tmp_path,
|
|
61
|
+
prompt_text="brief",
|
|
62
|
+
timeout_minutes=1,
|
|
63
|
+
runner=recording_runner,
|
|
64
|
+
live_log=live_log,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert captured_options["live_log"] == live_log
|
|
@@ -51,6 +51,21 @@ def test_should_build_report_from_json_result_and_round_duration() -> None:
|
|
|
51
51
|
assert report.wait_reset_at is None
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def test_should_take_the_result_event_from_streamed_output() -> None:
|
|
55
|
+
streamed_lines = "\n".join(
|
|
56
|
+
(
|
|
57
|
+
'{"type":"system","subtype":"init","session_id":"s1"}',
|
|
58
|
+
'{"type":"assistant","message":{"content":[]}}',
|
|
59
|
+
'{"type":"result","result":"ready","is_error":false}',
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
report = make_report("extra_2", "served", exit_code=0, duration_seconds=1.0, stdout_text=streamed_lines)
|
|
64
|
+
|
|
65
|
+
assert report.payload == "ready"
|
|
66
|
+
assert report.is_error is False
|
|
67
|
+
|
|
68
|
+
|
|
54
69
|
def test_should_mark_report_as_error_when_json_flags_error() -> None:
|
|
55
70
|
report = make_report(
|
|
56
71
|
"extra_2",
|
|
@@ -25,7 +25,7 @@ _HOOKS_CONFIGURATION = """
|
|
|
25
25
|
"""
|
|
26
26
|
_DISPATCHER_ROSTER_SOURCE = """
|
|
27
27
|
ALL_HOSTED_HOOK_ENTRIES = (
|
|
28
|
-
HostedHookEntry("advisory/
|
|
28
|
+
HostedHookEntry("advisory/migration_safety_advisor.py", is_blocking=False),
|
|
29
29
|
)
|
|
30
30
|
"""
|
|
31
31
|
|
|
@@ -40,7 +40,7 @@ def _build_package(repository_root: Path) -> None:
|
|
|
40
40
|
(package_root / "bin").mkdir(parents=True)
|
|
41
41
|
(package_root / "rules").mkdir(parents=True)
|
|
42
42
|
(hooks_root / "blocking" / "windows_rmtree_blocker.py").write_text("", "utf-8")
|
|
43
|
-
(hooks_root / "advisory" / "
|
|
43
|
+
(hooks_root / "advisory" / "migration_safety_advisor.py").write_text("", "utf-8")
|
|
44
44
|
(hooks_root / "observability" / "session_file_edit_tracker.py").write_text(
|
|
45
45
|
"", "utf-8"
|
|
46
46
|
)
|
|
@@ -96,7 +96,7 @@ def test_a_registered_hook_described_in_the_present_tense_passes(
|
|
|
96
96
|
_build_package(tmp_path)
|
|
97
97
|
all_messages = _diagnostics_for(
|
|
98
98
|
tmp_path,
|
|
99
|
-
"`session_file_edit_tracker` records each Write path, and `
|
|
99
|
+
"`session_file_edit_tracker` records each Write path, and `migration_safety_advisor` reports a risk.\n",
|
|
100
100
|
)
|
|
101
101
|
assert all_messages == ()
|
|
102
102
|
|
|
@@ -156,7 +156,7 @@ def test_a_second_hook_name_bounds_the_first_claim(tmp_path: Path) -> None:
|
|
|
156
156
|
_build_package(tmp_path)
|
|
157
157
|
all_messages = _diagnostics_for(
|
|
158
158
|
tmp_path,
|
|
159
|
-
"`destructive_command_blocker` was retired, and `
|
|
159
|
+
"`destructive_command_blocker` was retired, and `migration_safety_advisor` reports a risk.",
|
|
160
160
|
)
|
|
161
161
|
assert all_messages == ()
|
|
162
162
|
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"""Pytest registration for shared refactor guard test support."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
advisory_directory = str(Path(__file__).resolve().parent)
|
|
7
|
-
if advisory_directory not in sys.path:
|
|
8
|
-
sys.path.insert(0, advisory_directory)
|
|
9
|
-
|
|
10
|
-
from refactor_guard_test_support import git_repository # noqa: E402, F401
|