claude-dev-env 1.75.0 → 1.76.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/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
- package/skills/autoconverge/workflow/converge.mjs +86 -110
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Tests for the printf-token check on str.format-logger (automation_logging) calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
ENFORCER_FILENAME = "code_rules_enforcer.py"
|
|
10
|
+
ENFORCER_MODULE_NAME = "code_rules_enforcer_printf_under_test"
|
|
11
|
+
PRODUCTION_FILE_PATH = "shared_utils/web_automation/sample.py"
|
|
12
|
+
TEST_FILE_PATH = "shared_utils/web_automation/tests/test_sample.py"
|
|
13
|
+
FORMAT_LOGGER_IMPORT = (
|
|
14
|
+
"from shared_utils.automation_logging import log_error, log_info, log_debug\n"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_enforcer_module() -> ModuleType:
|
|
19
|
+
enforcer_path = Path(__file__).parent / ENFORCER_FILENAME
|
|
20
|
+
module_spec = importlib.util.spec_from_file_location(ENFORCER_MODULE_NAME, enforcer_path)
|
|
21
|
+
assert module_spec is not None
|
|
22
|
+
assert module_spec.loader is not None
|
|
23
|
+
enforcer_module = importlib.util.module_from_spec(module_spec)
|
|
24
|
+
module_spec.loader.exec_module(enforcer_module)
|
|
25
|
+
return enforcer_module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
enforcer = load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_should_flag_percent_s_in_format_logger_call() -> None:
|
|
32
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("Skipping %s after error: %s", name, err)\n'
|
|
33
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
34
|
+
assert len(issues) == 1
|
|
35
|
+
assert "printf token" in issues[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_flag_percent_d_in_format_logger_call() -> None:
|
|
39
|
+
source = FORMAT_LOGGER_IMPORT + 'log_debug("attempt %d of %d", index, total)\n'
|
|
40
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
41
|
+
assert len(issues) == 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_allow_brace_placeholders_in_format_logger_call() -> None:
|
|
45
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("Processing {} of {}", index, total)\n'
|
|
46
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
47
|
+
assert issues == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_ignore_percent_token_without_format_logger_import() -> None:
|
|
51
|
+
source = 'log_error("Skipping %s", name)\n'
|
|
52
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
53
|
+
assert issues == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_should_ignore_attribute_style_logger_call() -> None:
|
|
57
|
+
source = FORMAT_LOGGER_IMPORT + 'logger.info("delivered %s", message_id)\n'
|
|
58
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
59
|
+
assert issues == []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_should_inspect_message_literal_not_argument_value() -> None:
|
|
63
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("status: {}", "100%s done")\n'
|
|
64
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
65
|
+
assert issues == []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_should_exempt_test_files() -> None:
|
|
69
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("Skipping %s", name)\n'
|
|
70
|
+
issues = enforcer.check_logging_printf_tokens(source, TEST_FILE_PATH)
|
|
71
|
+
assert issues == []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_resolve_aliased_format_logger_import() -> None:
|
|
75
|
+
source = (
|
|
76
|
+
"from shared_utils.automation_logging import log_error as report_error\n"
|
|
77
|
+
+ 'report_error("failed %s", name)\n'
|
|
78
|
+
)
|
|
79
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
80
|
+
assert len(issues) == 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_should_allow_token_bearing_message_with_no_format_args() -> None:
|
|
84
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("Use %s for string substitution")\n'
|
|
85
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
86
|
+
assert issues == []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_should_allow_documentation_style_token_message_with_no_args() -> None:
|
|
90
|
+
source = FORMAT_LOGGER_IMPORT + 'log_warning("avoid %s-style tokens here")\n'
|
|
91
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
92
|
+
assert issues == []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_allow_percent_adjacent_to_word_in_format_message() -> None:
|
|
96
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("memory 80%free now", host)\n'
|
|
97
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
98
|
+
assert issues == []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_should_allow_doubled_percent_in_format_message() -> None:
|
|
102
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("100%% done", host)\n'
|
|
103
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
104
|
+
assert issues == []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_should_allow_trailing_percent_in_format_message() -> None:
|
|
108
|
+
source = FORMAT_LOGGER_IMPORT + 'log_info("100% done", host)\n'
|
|
109
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
110
|
+
assert issues == []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_should_flag_width_token_in_format_logger_call() -> None:
|
|
114
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("item %5d", index)\n'
|
|
115
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
116
|
+
assert len(issues) == 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_should_flag_precision_token_in_format_logger_call() -> None:
|
|
120
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("took %0.2f sec", elapsed)\n'
|
|
121
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
122
|
+
assert len(issues) == 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_should_flag_float_general_token_in_format_logger_call() -> None:
|
|
126
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("ratio %g", ratio)\n'
|
|
127
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
128
|
+
assert len(issues) == 1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_flag_scientific_token_in_format_logger_call() -> None:
|
|
132
|
+
source = FORMAT_LOGGER_IMPORT + 'log_error("value %e", measurement)\n'
|
|
133
|
+
issues = enforcer.check_logging_printf_tokens(source, PRODUCTION_FILE_PATH)
|
|
134
|
+
assert len(issues) == 1
|
|
@@ -418,7 +418,7 @@ def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
|
|
|
418
418
|
)
|
|
419
419
|
|
|
420
420
|
|
|
421
|
-
def
|
|
421
|
+
def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
|
|
422
422
|
tmp_path: pathlib.Path,
|
|
423
423
|
) -> None:
|
|
424
424
|
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
@@ -438,6 +438,71 @@ def test_workflow_verdict_covers_surface_false_for_missing_sidecar(
|
|
|
438
438
|
)
|
|
439
439
|
|
|
440
440
|
|
|
441
|
+
def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
|
|
442
|
+
tmp_path: pathlib.Path,
|
|
443
|
+
) -> None:
|
|
444
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
445
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
446
|
+
_write_agent_transcript(
|
|
447
|
+
subagents_dir,
|
|
448
|
+
"01",
|
|
449
|
+
VERIFIER_AGENT_TYPE,
|
|
450
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
451
|
+
should_write_sidecar=False,
|
|
452
|
+
)
|
|
453
|
+
corrupt_sidecar = (
|
|
454
|
+
subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
|
|
455
|
+
)
|
|
456
|
+
corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
|
|
457
|
+
assert (
|
|
458
|
+
workflow_verdict_covers_surface(
|
|
459
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
460
|
+
)
|
|
461
|
+
is False
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
|
|
466
|
+
tmp_path: pathlib.Path,
|
|
467
|
+
) -> None:
|
|
468
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
469
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
470
|
+
_write_agent_transcript(
|
|
471
|
+
subagents_dir,
|
|
472
|
+
"01",
|
|
473
|
+
VERIFIER_AGENT_TYPE,
|
|
474
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
475
|
+
should_write_sidecar=False,
|
|
476
|
+
)
|
|
477
|
+
invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
|
|
478
|
+
invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
|
|
479
|
+
assert (
|
|
480
|
+
workflow_verdict_covers_surface(
|
|
481
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
482
|
+
)
|
|
483
|
+
is False
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
|
|
488
|
+
tmp_path: pathlib.Path,
|
|
489
|
+
) -> None:
|
|
490
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
491
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
492
|
+
workflow_dir = subagents_dir / "workflows" / "wf_x"
|
|
493
|
+
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
494
|
+
(workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
|
|
495
|
+
(workflow_dir / "agent-01.meta.json").write_text(
|
|
496
|
+
json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
|
|
497
|
+
)
|
|
498
|
+
assert (
|
|
499
|
+
workflow_verdict_covers_surface(
|
|
500
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
501
|
+
)
|
|
502
|
+
is False
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
441
506
|
def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
|
|
442
507
|
tmp_path: pathlib.Path,
|
|
443
508
|
) -> None:
|
|
@@ -7,11 +7,13 @@ SubagentStop payload names the stopping subagent's own transcript
|
|
|
7
7
|
tests build that sidecar and assert the minter gates on the resolved type and
|
|
8
8
|
on the shared MINTING_AGENT_TYPE constant, so a rename in config propagates to
|
|
9
9
|
the minter without a second edit. A malformed or non-string sidecar resolves
|
|
10
|
-
nothing, and an absent sidecar mints nothing
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
nothing, and an absent sidecar mints nothing even when the transcript carries a
|
|
11
|
+
verdict fence — the main session controls the prompt of any Agent-tool subagent
|
|
12
|
+
it spawns, so a verdict fence in the transcript proves nothing about the agent
|
|
13
|
+
type, and only the harness-written sidecar attests a code-verifier. A further
|
|
14
|
+
test holds the shipped settings.json to the minter docstring's anti-forgery
|
|
15
|
+
claim: the main session is denied writes to the verdict directory, so only this
|
|
16
|
+
hook can mint a passing verdict.
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
import importlib.util
|
|
@@ -290,6 +292,63 @@ def test_minter_refuses_when_recomputed_surface_is_empty(
|
|
|
290
292
|
assert mint_for_payload(payload) is None
|
|
291
293
|
|
|
292
294
|
|
|
295
|
+
def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
|
|
296
|
+
tmp_path: pathlib.Path,
|
|
297
|
+
) -> None:
|
|
298
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
299
|
+
agent_transcript.write_text(
|
|
300
|
+
json.dumps(
|
|
301
|
+
{
|
|
302
|
+
"type": "assistant",
|
|
303
|
+
"message": {
|
|
304
|
+
"content": [
|
|
305
|
+
{
|
|
306
|
+
"type": "text",
|
|
307
|
+
"text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
+ "\n",
|
|
314
|
+
encoding="utf-8",
|
|
315
|
+
)
|
|
316
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
317
|
+
assert resolved_subagent_type(payload) is None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
|
|
321
|
+
tmp_path: pathlib.Path,
|
|
322
|
+
) -> None:
|
|
323
|
+
repo_root = tmp_path / "repo"
|
|
324
|
+
repo_root.mkdir()
|
|
325
|
+
_init_repo_with_upstream_and_edit(repo_root)
|
|
326
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
327
|
+
agent_transcript.write_text(
|
|
328
|
+
json.dumps(
|
|
329
|
+
{
|
|
330
|
+
"type": "assistant",
|
|
331
|
+
"message": {
|
|
332
|
+
"content": [
|
|
333
|
+
{
|
|
334
|
+
"type": "text",
|
|
335
|
+
"text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
+ "\n",
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
payload = {
|
|
345
|
+
"agent_transcript_path": str(agent_transcript),
|
|
346
|
+
"cwd": str(repo_root),
|
|
347
|
+
"agent_id": "no-sidecar-1",
|
|
348
|
+
}
|
|
349
|
+
assert mint_for_payload(payload) is None
|
|
350
|
+
|
|
351
|
+
|
|
293
352
|
def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
|
|
294
353
|
repo_root = tmp_path / "repo"
|
|
295
354
|
repo_root.mkdir()
|
|
@@ -423,17 +423,29 @@ def _subagents_directory_for_transcript(transcript_path: str) -> Path | None:
|
|
|
423
423
|
def _agent_type_for_transcript(transcript_file: Path) -> str | None:
|
|
424
424
|
"""Read an agent transcript's sidecar to learn the agent type it ran as.
|
|
425
425
|
|
|
426
|
+
The agent type comes solely from the harness-written
|
|
427
|
+
``agent-<id>.meta.json`` sidecar, which only the harness writes — the main
|
|
428
|
+
session can neither write the sidecar nor forge the agentType it records. A
|
|
429
|
+
verdict fence in the transcript is never used to infer the type, because the
|
|
430
|
+
main session controls the prompt of any Agent-tool subagent it spawns and
|
|
431
|
+
could instruct one to print such a fence.
|
|
432
|
+
|
|
426
433
|
Args:
|
|
427
434
|
transcript_file: An ``agent-*.jsonl`` transcript path.
|
|
428
435
|
|
|
429
436
|
Returns:
|
|
430
|
-
The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or
|
|
431
|
-
|
|
437
|
+
The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or None
|
|
438
|
+
when the sidecar is absent, unreadable, or unparseable, it does not hold
|
|
439
|
+
a JSON object, or it names no string ``agentType``.
|
|
432
440
|
"""
|
|
433
441
|
sidecar_file = transcript_file.with_suffix(AGENT_META_SIDECAR_SUFFIX)
|
|
434
442
|
try:
|
|
435
|
-
|
|
436
|
-
except (OSError,
|
|
443
|
+
sidecar_text = sidecar_file.read_text(encoding="utf-8")
|
|
444
|
+
except (OSError, UnicodeDecodeError):
|
|
445
|
+
return None
|
|
446
|
+
try:
|
|
447
|
+
sidecar_record = json.loads(sidecar_text)
|
|
448
|
+
except json.JSONDecodeError:
|
|
437
449
|
return None
|
|
438
450
|
if not isinstance(sidecar_record, dict):
|
|
439
451
|
return None
|
|
@@ -452,7 +464,9 @@ def _assistant_text_blocks(transcript_file: Path) -> list[str]:
|
|
|
452
464
|
when the file is missing, unreadable, or holds no assistant text.
|
|
453
465
|
"""
|
|
454
466
|
try:
|
|
455
|
-
transcript_lines =
|
|
467
|
+
transcript_lines = (
|
|
468
|
+
transcript_file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
469
|
+
)
|
|
456
470
|
except OSError:
|
|
457
471
|
return []
|
|
458
472
|
all_text_blocks: list[str] = []
|
|
@@ -6,14 +6,14 @@ SubagentStop payload names the stopping subagent's own transcript
|
|
|
6
6
|
(``agent_transcript_path``), which sits beside a harness-written
|
|
7
7
|
``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. The hook
|
|
8
8
|
reads that type from the sidecar, so it resolves identically in interactive,
|
|
9
|
-
background, and worktree-switched sessions
|
|
9
|
+
background, and worktree-switched sessions; only the harness writes the
|
|
10
|
+
sidecar, so the type it records cannot be forged. When that type is
|
|
10
11
|
``code-verifier``, the hook pulls the verdict block out of the agent's own
|
|
11
|
-
transcript (``agent_transcript_path``)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
matches the verified state.
|
|
12
|
+
transcript (``agent_transcript_path``), recomputes the live change-surface
|
|
13
|
+
hash for the session repository, and writes the verdict bound to that hash.
|
|
14
|
+
The companion ``verified_commit_gate.py`` (PreToolUse) then allows ``git
|
|
15
|
+
commit`` / ``git push`` only while the work tree still matches the verified
|
|
16
|
+
state.
|
|
17
17
|
|
|
18
18
|
The verifier's final message must end with a fenced block::
|
|
19
19
|
|
|
@@ -125,7 +125,8 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
|
|
|
125
125
|
``agent-<id>.meta.json`` naming the spawning ``agentType``. Reading the type
|
|
126
126
|
from this sidecar binds it to the stopping subagent itself, so it resolves
|
|
127
127
|
identically in interactive, background, and worktree-switched sessions and
|
|
128
|
-
needs no parent-transcript scan or flush retry.
|
|
128
|
+
needs no parent-transcript scan or flush retry. Only the harness writes this
|
|
129
|
+
sidecar, so a session cannot forge the type it reports.
|
|
129
130
|
|
|
130
131
|
Args:
|
|
131
132
|
agent_transcript_path: The stopping subagent's own transcript path from
|
|
@@ -133,7 +134,7 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
|
|
|
133
134
|
|
|
134
135
|
Returns:
|
|
135
136
|
The recorded ``agentType``, or None when the path is empty, the sidecar
|
|
136
|
-
is absent
|
|
137
|
+
is absent, unreadable, or unparseable, it does not hold a JSON object,
|
|
137
138
|
or it names no string ``agentType``.
|
|
138
139
|
"""
|
|
139
140
|
if not agent_transcript_path:
|
|
@@ -155,17 +156,19 @@ def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
|
|
|
155
156
|
|
|
156
157
|
The stopping subagent's own transcript (``agent_transcript_path``) sits
|
|
157
158
|
beside a harness-written ``agent-<id>.meta.json`` sidecar naming its
|
|
158
|
-
``agentType``.
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
``agentType``. The type comes solely from that sidecar, which only the
|
|
160
|
+
harness writes — the main session can neither write the sidecar nor forge
|
|
161
|
+
the agentType it records. A verdict fence in the transcript is never used to
|
|
162
|
+
infer the type, because the main session controls the prompt of any
|
|
163
|
+
Agent-tool subagent it spawns and could instruct one to print such a fence.
|
|
161
164
|
|
|
162
165
|
Args:
|
|
163
166
|
subagent_stop_payload: The SubagentStop hook payload.
|
|
164
167
|
|
|
165
168
|
Returns:
|
|
166
|
-
The agent type
|
|
167
|
-
``agent_transcript_path`` is empty, the sidecar is absent
|
|
168
|
-
|
|
169
|
+
The agent type the sidecar records, or None when the
|
|
170
|
+
``agent_transcript_path`` is empty, the sidecar is absent, unreadable,
|
|
171
|
+
or unparseable, it does not hold a JSON object, or it names no string
|
|
169
172
|
``agentType``.
|
|
170
173
|
"""
|
|
171
174
|
return _agent_type_from_meta_sidecar(
|
|
@@ -7,7 +7,6 @@ under the file-global-constants use-count rule (CODE_RULES §file-global-constan
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
MAX_BANNED_PREFIX_ISSUES: int = 3
|
|
12
11
|
MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
|
|
13
12
|
MAX_TYPED_DICT_PAIR_ISSUES: int = 3
|
|
@@ -24,8 +23,27 @@ MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
|
|
|
24
23
|
MAX_THIN_WRAPPER_ISSUES: int = 1
|
|
25
24
|
MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
|
|
26
25
|
MAX_LOGGING_FSTRING_ISSUES: int = 3
|
|
26
|
+
MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
|
|
27
27
|
MAX_WINDOWS_API_NONE_ISSUES: int = 3
|
|
28
28
|
MAX_E2E_TEST_NAMING_ISSUES: int = 3
|
|
29
|
+
MAX_IMPORT_BLOCK_SORT_ISSUES: int = 1
|
|
30
|
+
IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS: int = 15
|
|
31
|
+
IMPORT_BLOCK_SORT_RULE_CODE: str = "I001"
|
|
32
|
+
RUFF_STDIN_ENCODING: str = "utf-8"
|
|
33
|
+
RUFF_PYPROJECT_CONFIG_FILENAME: str = "pyproject.toml"
|
|
34
|
+
RUFF_PYPROJECT_TOOL_TABLE_MARKER: str = "[tool.ruff"
|
|
35
|
+
ALL_RUFF_STANDALONE_CONFIG_FILENAMES: tuple[str, ...] = ("ruff.toml", ".ruff.toml")
|
|
36
|
+
ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
|
|
37
|
+
"ruff",
|
|
38
|
+
"check",
|
|
39
|
+
"--select",
|
|
40
|
+
"I001",
|
|
41
|
+
"--no-cache",
|
|
42
|
+
"--output-format",
|
|
43
|
+
"json",
|
|
44
|
+
)
|
|
45
|
+
MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
|
|
46
|
+
MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
|
|
29
47
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
|
|
30
48
|
MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
|
|
31
49
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
|
|
@@ -85,6 +103,17 @@ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
|
|
|
85
103
|
ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
|
|
86
104
|
{"check", "checks", "test", "tests", "in", "for", "and", "the"}
|
|
87
105
|
)
|
|
106
|
+
ALL_FORMAT_LOGGER_FUNCTION_NAMES: frozenset[str] = frozenset(
|
|
107
|
+
{
|
|
108
|
+
"log_debug",
|
|
109
|
+
"log_info",
|
|
110
|
+
"log_ok",
|
|
111
|
+
"log_error",
|
|
112
|
+
"log_warning",
|
|
113
|
+
"log_batch",
|
|
114
|
+
"log_background",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
88
117
|
|
|
89
118
|
ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
|
|
90
119
|
"no consumer reads",
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"""Constants for the per-directory CLAUDE.md orphan-file-reference blocker.
|
|
2
2
|
|
|
3
3
|
A per-directory ``CLAUDE.md`` documents the files reachable from its own
|
|
4
|
-
directory in a markdown table whose first column names each file in backticks
|
|
5
|
-
|
|
4
|
+
directory in a markdown table whose first column names each file in backticks,
|
|
5
|
+
and shows run commands inside fenced code blocks that invoke those files. When a
|
|
6
|
+
first-column cell, or an interpreter invocation inside a run-command fence
|
|
7
|
+
(``python script.py``), names a bare filename that exists nowhere under the scan
|
|
6
8
|
root (the CLAUDE.md directory's parent, covering the directory, its
|
|
7
|
-
subdirectories, and its siblings), the
|
|
8
|
-
|
|
9
|
-
extensions that mark a cell as a file
|
|
10
|
-
scopes a prose region to one section,
|
|
11
|
-
|
|
12
|
-
subtree
|
|
9
|
+
subdirectories, and its siblings), the doc points a reader at a file that is not
|
|
10
|
+
there. This module holds the patterns that find those cells and run-command
|
|
11
|
+
invocations, the filename extensions that mark a cell or invocation as a file
|
|
12
|
+
reference, the region-boundary marker that scopes a prose region to one section,
|
|
13
|
+
the relative-path marker that exempts a cross-directory table block, the
|
|
14
|
+
directory names the subtree walk prunes, the subtree scan budget, and the
|
|
15
|
+
block-message text the hook emits.
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
18
|
import re
|
|
@@ -22,7 +25,9 @@ __all__ = [
|
|
|
22
25
|
"SEPARATOR_CELL_PATTERN",
|
|
23
26
|
"REGION_BOUNDARY_PATTERN",
|
|
24
27
|
"RELATIVE_PATH_SOURCE_PATTERN",
|
|
28
|
+
"RUN_COMMAND_SCRIPT_PATTERN",
|
|
25
29
|
"ALL_REFERENCED_FILE_EXTENSIONS",
|
|
30
|
+
"ALL_RUN_COMMAND_SCRIPT_EXTENSIONS",
|
|
26
31
|
"ALL_NOISE_DIRECTORY_NAMES",
|
|
27
32
|
"MAX_SUBTREE_FILES_SCANNED",
|
|
28
33
|
"MAX_ORPHAN_FILE_ISSUES",
|
|
@@ -45,6 +50,13 @@ REGION_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"^\s*#")
|
|
|
45
50
|
|
|
46
51
|
RELATIVE_PATH_SOURCE_PATTERN: re.Pattern[str] = re.compile(r"\.\.[\\/]")
|
|
47
52
|
|
|
53
|
+
RUN_COMMAND_SCRIPT_PATTERN: re.Pattern[str] = re.compile(
|
|
54
|
+
r"(?<![\w.])(?:python(?:\.exe)?|python3|node|pwsh|powershell(?:\.exe)?|bash|sh|ruby|perl)"
|
|
55
|
+
r"\b(?:\s+-\S+(?:\s+[\w./\\][\w./\\-]*(?=\s+\S))?)*"
|
|
56
|
+
r"\s+[\"']?"
|
|
57
|
+
r"([\w./\\-]+\.(?:py|mjs|js|ts|ps1|sh|rb|pl))\b"
|
|
58
|
+
)
|
|
59
|
+
|
|
48
60
|
ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
49
61
|
{
|
|
50
62
|
".py",
|
|
@@ -66,6 +78,19 @@ ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
|
66
78
|
}
|
|
67
79
|
)
|
|
68
80
|
|
|
81
|
+
ALL_RUN_COMMAND_SCRIPT_EXTENSIONS: frozenset[str] = frozenset(
|
|
82
|
+
{
|
|
83
|
+
".py",
|
|
84
|
+
".mjs",
|
|
85
|
+
".js",
|
|
86
|
+
".ts",
|
|
87
|
+
".ps1",
|
|
88
|
+
".sh",
|
|
89
|
+
".rb",
|
|
90
|
+
".pl",
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
69
94
|
ALL_NOISE_DIRECTORY_NAMES: frozenset[str] = frozenset(
|
|
70
95
|
{
|
|
71
96
|
".git",
|
|
@@ -81,27 +106,30 @@ MAX_SUBTREE_FILES_SCANNED: int = 5000
|
|
|
81
106
|
MAX_ORPHAN_FILE_ISSUES: int = 20
|
|
82
107
|
|
|
83
108
|
ORPHAN_FILE_MESSAGE_TEMPLATE: str = (
|
|
84
|
-
"CLAUDE.md
|
|
85
|
-
"{missing}. A per-directory CLAUDE.md
|
|
86
|
-
"subtree
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"directory
|
|
109
|
+
"CLAUDE.md references files that exist nowhere under {directory}: "
|
|
110
|
+
"{missing}. A per-directory CLAUDE.md names files in its own directory "
|
|
111
|
+
"subtree, both in its table cells and in the run commands its fenced code "
|
|
112
|
+
"blocks show; a cell or a run command naming a file absent from that subtree "
|
|
113
|
+
"points a reader at something that is not there. Drop the row or run command, "
|
|
114
|
+
"or correct it to name a file that exists in this directory, a subdirectory of "
|
|
115
|
+
"it, or a sibling directory under its parent."
|
|
90
116
|
)
|
|
91
117
|
|
|
92
118
|
ORPHAN_FILE_SYSTEM_MESSAGE: str = (
|
|
93
|
-
"CLAUDE.md
|
|
94
|
-
"drop the row or name an existing file"
|
|
119
|
+
"CLAUDE.md names a file that does not exist in its directory subtree - "
|
|
120
|
+
"drop the row or run command, or name an existing file"
|
|
95
121
|
)
|
|
96
122
|
|
|
97
123
|
ORPHAN_FILE_ADDITIONAL_CONTEXT: str = (
|
|
98
124
|
"Each first-column table cell wrapped in backticks that ends in a known file "
|
|
99
|
-
"extension
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
125
|
+
"extension, and each script an interpreter invocation inside a fenced run "
|
|
126
|
+
"command names (such as 'python script.py'), must name a file present under "
|
|
127
|
+
"the scan root: this CLAUDE.md's own directory, a subdirectory of it, or a "
|
|
128
|
+
"sibling directory under its parent. Cells holding a path with a slash, a "
|
|
129
|
+
"subdirectory ending in '/', or a slash-command are out of scope. A table "
|
|
130
|
+
"whose own block names an explicit relative-path source (a '../' token) "
|
|
131
|
+
"documents files outside the subtree and is out of scope. For each missing "
|
|
132
|
+
"file:\n"
|
|
133
|
+
" - delete the table row or the run command, or\n"
|
|
134
|
+
" - rename it to an existing file under the scan root."
|
|
107
135
|
)
|
|
@@ -13,7 +13,7 @@ ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
ALL_PYTHON_EXTENSIONS = {".py"}
|
|
16
|
-
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
|
|
16
|
+
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts"}
|
|
17
17
|
ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
|
|
18
18
|
|
|
19
19
|
ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
|
|
@@ -119,6 +119,46 @@ LOGGING_FSTRING_PATTERN = re.compile(
|
|
|
119
119
|
r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
|
|
120
120
|
r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
|
|
121
121
|
)
|
|
122
|
+
LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
123
|
+
r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
|
|
124
|
+
)
|
|
125
|
+
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
|
|
126
|
+
SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
|
|
127
|
+
r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
|
|
128
|
+
r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
|
|
129
|
+
re.DOTALL,
|
|
130
|
+
)
|
|
131
|
+
RESUME_TASK_ENUMERATION_PATTERN: re.Pattern[str] = re.compile(
|
|
132
|
+
r"(?<![A-Za-z])resume\s*\((?P<enumeration>[^)]*?)\)",
|
|
133
|
+
re.DOTALL,
|
|
134
|
+
)
|
|
135
|
+
TASK_DISPATCH_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
136
|
+
r"""(?<![A-Za-z0-9_])task\s*===\s*['"](?P<task>[a-z0-9-]+)['"]"""
|
|
137
|
+
)
|
|
138
|
+
ENUMERATION_LIST_ITEM_SEPARATOR_PATTERN: re.Pattern[str] = re.compile(
|
|
139
|
+
r"\s*,\s*|\s+and\s+"
|
|
140
|
+
)
|
|
141
|
+
ENUMERATION_LEADING_CONJUNCTION_PATTERN: re.Pattern[str] = re.compile(
|
|
142
|
+
r"^and\s+"
|
|
143
|
+
)
|
|
144
|
+
ALL_JAVASCRIPT_STRING_DELIMITERS: frozenset[str] = frozenset({"'", '"', "`"})
|
|
145
|
+
JAVASCRIPT_STRING_ESCAPE_CHARACTER: str = "\\"
|
|
146
|
+
JAVASCRIPT_LINE_COMMENT_OPENER: str = "//"
|
|
147
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER: str = "/*"
|
|
148
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER: str = "*/"
|
|
149
|
+
JAVASCRIPT_REGEX_DELIMITER: str = "/"
|
|
150
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_CHARACTERS: frozenset[str] = frozenset(
|
|
151
|
+
{"(", ",", "=", ":", "[", "{", "}", ";", "!", "&", "|", "?", "+", "-", "*", "%", "<", ">", "~", "^", "\n"}
|
|
152
|
+
)
|
|
153
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_KEYWORDS: frozenset[str] = frozenset(
|
|
154
|
+
{"return", "typeof", "case", "in", "of", "do", "else", "void", "delete", "instanceof", "new", "yield", "await", "throw"}
|
|
155
|
+
)
|
|
156
|
+
ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
157
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
158
|
+
)
|
|
159
|
+
HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
160
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
|
|
161
|
+
)
|
|
122
162
|
ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
|
|
123
163
|
"get", "items", "keys", "values", "update", "pop",
|
|
124
164
|
"setdefault", "copy", "clear",
|