claude-dev-env 8.6.1 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +10 -8
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapters.py +30 -0
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -49,7 +49,7 @@ Review each hit. Ignore:
|
|
|
49
49
|
|
|
50
50
|
- Stage only clean files
|
|
51
51
|
- Prefer `--body-file` for `gh` posts (also required by the gh-cli-conventions rule)
|
|
52
|
-
-
|
|
52
|
+
- No hook re-scans the next Write, commit, or post; `pii_prevention_blocker` was retired. Re-run this sweep before the next commit or post.
|
|
53
53
|
|
|
54
54
|
## Accepted residual (do not over-scrub)
|
|
55
55
|
|
|
@@ -76,6 +76,7 @@ from advisor_scripts_constants.model_tier_run_validator_constants import (
|
|
|
76
76
|
CLI_SUCCESS_EXIT_CODE,
|
|
77
77
|
CLI_USAGE_MESSAGE,
|
|
78
78
|
CLI_VALIDATION_FAILURE_EXIT_CODE,
|
|
79
|
+
EVIDENCE_MUST_BE_OBJECT_MESSAGE,
|
|
79
80
|
HOST_PROFILE_CLAUDE,
|
|
80
81
|
HOST_PROFILE_CODEX,
|
|
81
82
|
HOST_PROFILE_JSON_KEY,
|
|
@@ -242,7 +243,7 @@ def _validate_advisor_evidence(
|
|
|
242
243
|
run: ModelTierRun,
|
|
243
244
|
) -> None:
|
|
244
245
|
if not isinstance(evidence, Mapping):
|
|
245
|
-
raise ModelTierRunError(
|
|
246
|
+
raise ModelTierRunError(EVIDENCE_MUST_BE_OBJECT_MESSAGE)
|
|
246
247
|
raw_schema_version = evidence.get("schema_version")
|
|
247
248
|
if isinstance(raw_schema_version, bool) or raw_schema_version != 1:
|
|
248
249
|
raise ModelTierRunError("evidence.schema_version must be 1")
|
|
@@ -415,7 +416,7 @@ def load_model_tier_run_from_json_path(from_path: Path) -> ModelTierRun:
|
|
|
415
416
|
raise TypeError(HOST_PROFILE_MUST_BE_STRING_MESSAGE)
|
|
416
417
|
raw_evidence = parsed_payload.get("evidence")
|
|
417
418
|
if raw_evidence is not None and not isinstance(raw_evidence, dict):
|
|
418
|
-
raise TypeError(
|
|
419
|
+
raise TypeError(EVIDENCE_MUST_BE_OBJECT_MESSAGE)
|
|
419
420
|
return ModelTierRun(
|
|
420
421
|
own_tier=parsed_payload["own_tier"],
|
|
421
422
|
candidate_tiers=list(parsed_payload["candidate_tiers"]),
|
|
@@ -35,6 +35,9 @@ load_model_tier_run_from_json_path = (
|
|
|
35
35
|
)
|
|
36
36
|
ADVISOR_MODEL_TIER = model_tier_run_validator.ADVISOR_MODEL_TIER
|
|
37
37
|
CODEX_BIND_SUCCESS_TOKEN = model_tier_run_validator.CODEX_BIND_SUCCESS_TOKEN
|
|
38
|
+
EVIDENCE_MUST_BE_OBJECT_MESSAGE = (
|
|
39
|
+
model_tier_run_validator.EVIDENCE_MUST_BE_OBJECT_MESSAGE
|
|
40
|
+
)
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
def _native_codex_evidence(
|
|
@@ -136,6 +139,38 @@ def test_cli_reads_versioned_advisor_evidence(tmp_path: Path) -> None:
|
|
|
136
139
|
assert loaded_run.evidence["consult"]["report_back_status"] == "recorded"
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
def test_non_object_evidence_reports_the_same_message_at_validate_and_load(
|
|
143
|
+
tmp_path: Path,
|
|
144
|
+
) -> None:
|
|
145
|
+
run = ModelTierRun(
|
|
146
|
+
own_tier="Opus",
|
|
147
|
+
candidate_tiers=[ADVISOR_MODEL_TIER],
|
|
148
|
+
attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
|
|
149
|
+
selected_tier=ADVISOR_MODEL_TIER,
|
|
150
|
+
host_profile="Codex",
|
|
151
|
+
evidence=["not", "an", "object"],
|
|
152
|
+
)
|
|
153
|
+
with pytest.raises(ModelTierRunError, match=EVIDENCE_MUST_BE_OBJECT_MESSAGE):
|
|
154
|
+
validate_model_tier_run(run)
|
|
155
|
+
|
|
156
|
+
log_path = tmp_path / "model-tier-run.json"
|
|
157
|
+
log_path.write_text(
|
|
158
|
+
json.dumps(
|
|
159
|
+
{
|
|
160
|
+
"own_tier": "Opus",
|
|
161
|
+
"candidate_tiers": [ADVISOR_MODEL_TIER],
|
|
162
|
+
"attempts": [{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
|
|
163
|
+
"selected_tier": ADVISOR_MODEL_TIER,
|
|
164
|
+
"host_profile": "Codex",
|
|
165
|
+
"evidence": "not an object",
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
encoding="utf-8",
|
|
169
|
+
)
|
|
170
|
+
with pytest.raises(TypeError, match=EVIDENCE_MUST_BE_OBJECT_MESSAGE):
|
|
171
|
+
load_model_tier_run_from_json_path(from_path=log_path)
|
|
172
|
+
|
|
173
|
+
|
|
139
174
|
def test_clean_single_spawn_at_top_of_slice_passes() -> None:
|
|
140
175
|
run = ModelTierRun(
|
|
141
176
|
own_tier="Opus",
|
|
@@ -175,9 +175,7 @@ def _collect_introduced_identifiers(
|
|
|
175
175
|
"""
|
|
176
176
|
all_identifier_tuples: set[IdentifierTuple] = set()
|
|
177
177
|
for each_file_path, _, each_text in all_added_lines:
|
|
178
|
-
if
|
|
179
|
-
each_file_path
|
|
180
|
-
) in ALL_SWEEP_CODE_FILE_EXTENSIONS and not _is_test_file(each_file_path):
|
|
178
|
+
if _is_swept_non_test_code_file(each_file_path):
|
|
181
179
|
all_identifier_tuples.update(_identifier_tuples_in_text(each_text))
|
|
182
180
|
return frozenset(all_identifier_tuples)
|
|
183
181
|
|
|
@@ -301,6 +299,13 @@ def _is_test_file(file_path: str) -> bool:
|
|
|
301
299
|
)
|
|
302
300
|
|
|
303
301
|
|
|
302
|
+
def _is_swept_non_test_code_file(file_path: str) -> bool:
|
|
303
|
+
"""Return whether a diff path names a swept code file outside a test module."""
|
|
304
|
+
return _file_extension(
|
|
305
|
+
file_path
|
|
306
|
+
) in ALL_SWEEP_CODE_FILE_EXTENSIONS and not _is_test_file(file_path)
|
|
307
|
+
|
|
308
|
+
|
|
304
309
|
def _comment_fragments(line_text: str) -> list[str]:
|
|
305
310
|
"""Return the comment tail of a code line, or an empty list when absent."""
|
|
306
311
|
all_fragments: list[str] = []
|
|
@@ -616,9 +621,7 @@ def _stem_identifier_tuple_for_code_file(each_file_path: str) -> IdentifierTuple
|
|
|
616
621
|
``None`` when the path is not a swept code file, is a test file, or its
|
|
617
622
|
stem carries fewer than the minimum identifier token count.
|
|
618
623
|
"""
|
|
619
|
-
if
|
|
620
|
-
each_file_path
|
|
621
|
-
):
|
|
624
|
+
if not _is_swept_non_test_code_file(each_file_path):
|
|
622
625
|
return None
|
|
623
626
|
stem_tuple = _identifier_token_tuple(Path(each_file_path).stem)
|
|
624
627
|
if len(stem_tuple) < MINIMUM_IDENTIFIER_TOKEN_COUNT:
|
|
@@ -747,9 +750,7 @@ def _identifier_names_on_added_code_lines(diff_text: str) -> frozenset[str]:
|
|
|
747
750
|
"""
|
|
748
751
|
all_names: set[str] = set()
|
|
749
752
|
for each_file_path, _, each_text in _parse_added_lines(diff_text):
|
|
750
|
-
if
|
|
751
|
-
each_file_path
|
|
752
|
-
) not in ALL_SWEEP_CODE_FILE_EXTENSIONS or _is_test_file(each_file_path):
|
|
753
|
+
if not _is_swept_non_test_code_file(each_file_path):
|
|
753
754
|
continue
|
|
754
755
|
all_found_names = SNAKE_CASE_IDENTIFIER_PATTERN.findall(each_text)
|
|
755
756
|
all_found_names += CAMEL_CASE_IDENTIFIER_PATTERN.findall(each_text)
|
|
@@ -759,3 +759,16 @@ def test_flags_near_miss_when_second_leading_token_is_plural_variant() -> None:
|
|
|
759
759
|
findings = sweep_diff(diff)
|
|
760
760
|
assert len(findings) == 1
|
|
761
761
|
assert "retry policies limit" in findings[0]
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def test_a_swept_code_file_outside_the_test_modules_reads_as_swept() -> None:
|
|
765
|
+
assert sweep_module._is_swept_non_test_code_file("api/store.py") is True
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_a_test_module_does_not_read_as_a_swept_code_file() -> None:
|
|
769
|
+
assert sweep_module._is_swept_non_test_code_file("api/test_store.py") is False
|
|
770
|
+
assert sweep_module._is_swept_non_test_code_file("api/tests/store.py") is False
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def test_a_markdown_document_does_not_read_as_a_swept_code_file() -> None:
|
|
774
|
+
assert sweep_module._is_swept_non_test_code_file("docs/README.md") is False
|
package/bin/install.test.mjs
CHANGED
|
@@ -2928,6 +2928,31 @@ test('retired skills leave install groups and stay in the cleanup registry', ()
|
|
|
2928
2928
|
}
|
|
2929
2929
|
});
|
|
2930
2930
|
|
|
2931
|
+
test('the README agents section names the shipped agent, not archived personal agents', () => {
|
|
2932
|
+
const readme = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8');
|
|
2933
|
+
const agentsSection = readme.slice(
|
|
2934
|
+
readme.indexOf('### Agents'),
|
|
2935
|
+
readme.indexOf('### Commands'),
|
|
2936
|
+
);
|
|
2937
|
+
assert.match(agentsSection, /`poteto-agent`/);
|
|
2938
|
+
for (const archivedAgentName of [
|
|
2939
|
+
'clean-coder',
|
|
2940
|
+
'code-quality-agent',
|
|
2941
|
+
'git-commit-crafter',
|
|
2942
|
+
'issue-tracker',
|
|
2943
|
+
'plan-packet-validator',
|
|
2944
|
+
'pr-description-writer',
|
|
2945
|
+
'session-advisor',
|
|
2946
|
+
'skill-writer-agent',
|
|
2947
|
+
]) {
|
|
2948
|
+
assert.equal(
|
|
2949
|
+
agentsSection.includes(`\`${archivedAgentName}\``),
|
|
2950
|
+
false,
|
|
2951
|
+
`${archivedAgentName} is archived and should not appear as a shipped agent`,
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
|
|
2931
2956
|
test('the README names current session tools', () => {
|
|
2932
2957
|
const readme = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8');
|
|
2933
2958
|
assert.doesNotMatch(readme, /--only journal|session-log|session-tidy/);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Building a high trust system for agent driven software delivery
|
|
2
|
+
|
|
3
|
+
Source: https://x.com/poteto/status/2102050467505430555/video/1
|
|
4
|
+
|
|
5
|
+
This document carries the model behind [`rules/correction-lens.md`](../rules/correction-lens.md).
|
|
6
|
+
|
|
7
|
+
## The central idea
|
|
8
|
+
|
|
9
|
+
Lauren's 2,000 pull requests came after she built a system that let agents do useful work while she was away.
|
|
10
|
+
|
|
11
|
+
The system had several parts. The codebase showed agents the preferred patterns. Tools let agents run the product and gather evidence. Continuous integration, or CI, checked every proposed change. Rules and review bots caught repeated mistakes. Skills taught agents how to perform work in the team's preferred way. Automations connected incoming problems to agents that could investigate them.
|
|
12
|
+
|
|
13
|
+
These parts created trust over time. An agent earned more freedom by showing that it could make a change, check the result, respond to failures, and open a pull request that met the repository's standards.
|
|
14
|
+
|
|
15
|
+
The goal for a repository is clear. An agent should be able to take a task through the full path to a pull request. The repository should provide enough evidence for a merge decision without requiring a person to inspect every changed line.
|
|
16
|
+
|
|
17
|
+
## Why high volume exposes weak systems
|
|
18
|
+
|
|
19
|
+
When people first work with agents, they often watch every conversation. They correct the agent's approach, stop mistakes, and supply missing context. That works for a small number of tasks. It breaks when many agents work at the same time.
|
|
20
|
+
|
|
21
|
+
A large group of agents multiplies whatever the repository already permits. If the checks are weak, the team receives many noisy pull requests. Regressions and bugs spread faster. The team spends its time repairing agent output, and the product waits.
|
|
22
|
+
|
|
23
|
+
The answer is to build trust before adding more agents. Start with a small workflow that an agent can repeat. Watch the failures. Turn each repeated correction into a stronger control. Increase the number of agents after the controls catch the common mistakes.
|
|
24
|
+
|
|
25
|
+
The high pull request count is the result of this work. The first target is a system that can check changes without waiting for one person.
|
|
26
|
+
|
|
27
|
+
## Verification gives the agent evidence
|
|
28
|
+
|
|
29
|
+
Verification asks whether the code does what the feature needs. A checkout button should complete a checkout. A changed screen should respond to the user's action. A performance change should meet a measured limit.
|
|
30
|
+
|
|
31
|
+
Teach the agent how to start the product, reach a feature, perform the relevant action, and inspect the result. The agent can collect a browser trace, a memory snapshot, a test result, or a performance measurement. The evidence should come from a repeatable procedure.
|
|
32
|
+
|
|
33
|
+
A useful verification setup has two parts. The first is a command line tool that runs the product and collects the required evidence. The second is a feature map that explains how the product works.
|
|
34
|
+
|
|
35
|
+
The command line tool gives every agent session the same procedure. Agents do not invent a new test script for every task. The tool supplies the commands, inputs, and output format. Invest in the important product paths so the tool can handle the work agents perform often.
|
|
36
|
+
|
|
37
|
+
The feature map gives the agent context that a short bug report often lacks. It describes the product's features, the paths a user takes to reach them, the controls that matter, and the expected result. Keep the map with the repository and maintain it as the product changes.
|
|
38
|
+
|
|
39
|
+
The tool and the map work together. The agent can reproduce a report, inspect the product, collect evidence, and explain the result. This turns verification into a shared team capability.
|
|
40
|
+
|
|
41
|
+
Verification does not answer every quality question. It shows that the feature works during the check. It does not by itself show that the code is easy to maintain, that the performance will stay within limits, or that the change follows the team's design rules.
|
|
42
|
+
|
|
43
|
+
Add skills for those questions. A skill can teach an agent how to debug, develop a feature, refactor code, inspect performance, or review a change. Keep the skills in a shared repository. Combine them with verification so the agent can check behavior and code quality.
|
|
44
|
+
|
|
45
|
+
Measured data makes this process stronger. Performance numbers, memory measurements, error counts, and test results give the agent something to inspect. The agent can respond to a number more reliably than to a vague instruction such as "make this faster."
|
|
46
|
+
|
|
47
|
+
## The codebase is part of the control system
|
|
48
|
+
|
|
49
|
+
Agents extend the patterns they find in the files they read. The codebase becomes working memory for the agent. An agent usually adapts an existing structure to a new task. It rarely redesigns the whole system during each pull request.
|
|
50
|
+
|
|
51
|
+
This gives the codebase a direct effect on future changes. One clear pattern reduces guessing. Several conflicting patterns force the agent to choose. The repository should make the safe choice easy to find and easy to repeat.
|
|
52
|
+
|
|
53
|
+
Start with the code structure. Change the design so that a bad pattern cannot be expressed, or so that the preferred pattern is the easiest path. Good data structures and clear boundaries can remove entire classes of errors.
|
|
54
|
+
|
|
55
|
+
Make each feature easy to locate. Define where its code lives. Define how it connects to other parts of the product. Define which parts may import each other. A clear layout gives the agent a smaller search space and fewer decisions.
|
|
56
|
+
|
|
57
|
+
The framework Lauren describes follows this idea. It sets conventions for code location and dependency boundaries. A performance problem caused by code crossing into the wrong process can become impossible through import rules. The method carries. Put team knowledge into code structure so agents find it while they work.
|
|
58
|
+
|
|
59
|
+
Strict conventions may feel inconvenient to people who already know the system. They help agents and new contributors because the repository answers more questions by itself. A designer, product manager, or busy engineer can ask an agent to make a change and still benefit from the same safe paths.
|
|
60
|
+
|
|
61
|
+
## Build controls in layers
|
|
62
|
+
|
|
63
|
+
The controls work best in a clear order. Each layer handles a different kind of mistake.
|
|
64
|
+
|
|
65
|
+
First, make the mistake impossible through the code structure, architecture, or data model. This is the strongest control because the agent cannot choose the bad pattern.
|
|
66
|
+
|
|
67
|
+
Second, add static checks. Linters, compiler diagnostics, type checks, and CI inspect every proposed change. Continuous integration means that the repository runs the required checks for each pull request. When an agent repeats a mistake, turn the mistake into a check that runs every time.
|
|
68
|
+
|
|
69
|
+
Third, add rules, automated review, and skills. These explain how the team wants work done. They help with decisions that are difficult to encode in code or static checks. A rule can describe a preferred pattern. A review bot can point out a problem. A skill can give the agent a repeatable procedure.
|
|
70
|
+
|
|
71
|
+
Fourth, keep human review for judgment that the lower layers cannot express. People can decide whether a change fits the product, whether the design is sound, or whether a tradeoff is acceptable. People should not spend every review repeating the same correction that a check could catch.
|
|
72
|
+
|
|
73
|
+
Every repeated review comment is evidence that a lower layer needs improvement. Move that lesson into the code structure, a static check, CI, an automated review, or a skill. This gives the next agent the lesson without requiring a person to repeat it.
|
|
74
|
+
|
|
75
|
+
## Turn corrections into repository improvements
|
|
76
|
+
|
|
77
|
+
An agent correction should change the system when the same mistake can happen again. Fixing one pull request removes one symptom. Adding a control protects later pull requests.
|
|
78
|
+
|
|
79
|
+
Suppose an agent copies a workaround. Remove the workaround where possible. Add a rule that prevents new copies. Add a skill that explains the preferred solution. Ask an agent to clean the old copies so the repository presents better examples.
|
|
80
|
+
|
|
81
|
+
Lauren gives comments as one example. Comments can explain an edge case or a temporary workaround. In the Cursor codebase, agents began using comments as permission to keep workarounds, and the underlying problem stayed. The team chose to ban comments in Dune so agents would stop copying that pattern.
|
|
82
|
+
|
|
83
|
+
The same reasoning applies to any pattern that creates poor code. When a pattern keeps appearing, decide which control should own the correction. Put the knowledge in the repository, where every later session reads it.
|
|
84
|
+
|
|
85
|
+
This maintenance role is like gardening. Someone needs to look for small workarounds, weak patterns, and new code debt before they spread. That person removes old debt, keeps one conventional path for common work, and adds a check when a bad pattern appears.
|
|
86
|
+
|
|
87
|
+
The standard is simple. Keep the repository in a state that you would want an agent to copy. Every example teaches the next agent what to do.
|
|
88
|
+
|
|
89
|
+
## The inner delivery loop
|
|
90
|
+
|
|
91
|
+
The inner loop is the path from a task to a pull request. A dependable repository gives the agent a repeatable sequence.
|
|
92
|
+
|
|
93
|
+
1. The agent receives a task with enough context to identify the relevant feature and code.
|
|
94
|
+
2. The agent makes the smallest change that solves the task.
|
|
95
|
+
3. The agent runs the product or the relevant tests through the shared verification tools.
|
|
96
|
+
4. The agent collects evidence about behavior, performance, and other required limits.
|
|
97
|
+
5. CI, static checks, repository rules, and automated review inspect the change.
|
|
98
|
+
6. The agent responds to failures, repairs the change, and runs the checks again.
|
|
99
|
+
7. The agent opens or updates the pull request with the evidence.
|
|
100
|
+
8. The repository grants merge permission after the required checks pass and the remaining judgment calls are settled.
|
|
101
|
+
|
|
102
|
+
The agent earns freedom through this loop. The system depends on the agent producing evidence that the repository can inspect.
|
|
103
|
+
|
|
104
|
+
## The outer delivery loop
|
|
105
|
+
|
|
106
|
+
Work can begin outside the code editor. A message, error report, performance alert, or product signal can start the next task.
|
|
107
|
+
|
|
108
|
+
Connect an agent to the tools that hold this information. The talk mentions Slack, an error tracker, performance data, and a database service. The exact tools depend on the repository. The important point is that the agent can gather enough context to decide what happened and what task should follow.
|
|
109
|
+
|
|
110
|
+
A routine can watch a message or an alert and start an agent. The agent can reproduce the problem, make a change, run the checks, and open a pull request. Other bots can call the same verification tools, repository rules, and skills for more complex work.
|
|
111
|
+
|
|
112
|
+
A set of small connections can start useful work. The repository supplies the controls. The agent calls the tools. CI and review inspect the result.
|
|
113
|
+
|
|
114
|
+
## How the pieces compound
|
|
115
|
+
|
|
116
|
+
Each control improves the next one. A clear code pattern gives the agent a safe starting point. The verification tool gives it product evidence. CI catches repeatable mistakes. Automated review finds code quality problems. Skills give the agent procedures for work that needs more judgment. The outer loop supplies new tasks.
|
|
117
|
+
|
|
118
|
+
Together, these controls let many agents work in parallel. The agents do not need a person to approve every small action because the environment checks the work. People can focus on product decisions, system design, and questions that the checks cannot settle.
|
|
119
|
+
|
|
120
|
+
The result is a repository where an agent with limited context can still make a good change. The environment carries the team's knowledge. The agent follows the safe paths. The pull request provides evidence for its merge decision.
|
|
121
|
+
|
|
122
|
+
## A practical way to apply the model
|
|
123
|
+
|
|
124
|
+
Start with one recurring task. Give an agent a fixed way to run the relevant part of the product and collect evidence. Record the expected result. Run the same procedure for several changes and study the failures.
|
|
125
|
+
|
|
126
|
+
When a failure repeats, ask where the correction belongs. Change the code structure if the pattern can be removed. Add a static check or CI step if the problem can be detected. Add a rule, review bot, or skill when the decision needs guidance. Improve human review when the question still needs judgment.
|
|
127
|
+
|
|
128
|
+
Keep the codebase clean as the agent population grows. Remove workarounds. Reduce competing patterns. Add one clear path for common work. Maintain the feature map and verification tools as part of the repository.
|
|
129
|
+
|
|
130
|
+
Then connect incoming signals to the same workflow. Let an agent start from a bug report or alert, reproduce the problem, make the change, run the checks, and open the pull request. Increase parallel work after the system can catch the mistakes that matter.
|
|
131
|
+
|
|
132
|
+
The key lesson is to move knowledge out of individual conversations and into the environment. A person teaches the system once. The repository, checks, tools, and skills carry that lesson into every later pull request.
|
|
@@ -23,14 +23,14 @@ try:
|
|
|
23
23
|
sys.path.insert(0, hooks_root_directory)
|
|
24
24
|
|
|
25
25
|
from hooks_constants.multi_edit_reconstruction import edits_for_tool
|
|
26
|
-
from hooks_constants.precommit_code_rules_gate_constants import GIT_COMMAND_TIMEOUT_SECONDS
|
|
27
26
|
from hooks_constants.refactor_guard_constants import (
|
|
28
27
|
ALL_PYTHON_KEYWORDS,
|
|
29
28
|
CHANGED_SURFACE_MATCH_RATIO,
|
|
29
|
+
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
30
|
+
GIT_EXECUTABLE_TOKEN,
|
|
30
31
|
MAXIMUM_REFACTOR_LINE_DELTA,
|
|
31
32
|
REFACTOR_LINE_DELTA_DIVISOR,
|
|
32
33
|
)
|
|
33
|
-
from hooks_constants.session_edit_stage_gate_constants import GIT_EXECUTABLE_TOKEN
|
|
34
34
|
except ImportError as import_error:
|
|
35
35
|
raise ImportError(
|
|
36
36
|
"refactor_guard: cannot import its sibling modules; "
|
|
@@ -125,3 +125,24 @@ def test_untracked_probe_should_start_without_a_console_window(
|
|
|
125
125
|
|
|
126
126
|
assert refactor_guard.is_new_file(str(tmp_path / "module.py")) is True
|
|
127
127
|
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_the_git_query_carries_the_advisory_s_own_executable_and_timeout(
|
|
131
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
132
|
+
) -> None:
|
|
133
|
+
"""refactor_guard_constants supplies both names the git call is built from."""
|
|
134
|
+
all_calls: list[tuple[list[str], object]] = []
|
|
135
|
+
|
|
136
|
+
def record_call(
|
|
137
|
+
command: list[str], **keyword_arguments: object
|
|
138
|
+
) -> subprocess.CompletedProcess[str]:
|
|
139
|
+
all_calls.append((command, keyword_arguments["timeout"]))
|
|
140
|
+
return subprocess.CompletedProcess(command, 1, "", "")
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(refactor_guard.subprocess, "run", record_call)
|
|
143
|
+
|
|
144
|
+
refactor_guard.is_new_file(str(tmp_path / "module.py"))
|
|
145
|
+
|
|
146
|
+
command, timeout = all_calls[0]
|
|
147
|
+
assert command[:2] == ["git", "ls-files"]
|
|
148
|
+
assert timeout == refactor_guard.GIT_COMMAND_TIMEOUT_SECONDS
|
|
@@ -13,6 +13,16 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
_ENFORCER_SCRIPT_PATH = Path(__file__).with_name("code_rules_enforcer.py")
|
|
15
15
|
|
|
16
|
+
STRIP_CODE_AND_QUOTES_SOURCE = (
|
|
17
|
+
"import re\n"
|
|
18
|
+
"\n"
|
|
19
|
+
"def strip_code_and_quotes(text: str) -> str:\n"
|
|
20
|
+
" without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
|
|
21
|
+
" without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
|
|
22
|
+
" without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
|
|
23
|
+
" return without_quotes.strip()\n"
|
|
24
|
+
)
|
|
25
|
+
|
|
16
26
|
|
|
17
27
|
def repository_root() -> Path:
|
|
18
28
|
"""Return the root of the checkout these tests run inside.
|
|
@@ -48,7 +48,7 @@ def get_file_extension(file_path: str) -> str:
|
|
|
48
48
|
def is_hook_infrastructure(file_path: str) -> bool:
|
|
49
49
|
"""Check if file is a Claude Code hook (standalone infrastructure, not project code)."""
|
|
50
50
|
path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
|
|
51
|
-
return any(pattern
|
|
51
|
+
return any(pattern in path_lower for pattern in ALL_HOOK_INFRASTRUCTURE_PATTERNS)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def is_agent_home_tooling(file_path: str) -> bool:
|
|
@@ -66,10 +66,7 @@ def is_agent_home_tooling(file_path: str) -> bool:
|
|
|
66
66
|
True when the path sits under a recognized agent home directory.
|
|
67
67
|
"""
|
|
68
68
|
path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
|
|
69
|
-
return any(
|
|
70
|
-
each_pattern.replace("\\", "/") in path_lower
|
|
71
|
-
for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS
|
|
72
|
-
)
|
|
69
|
+
return any(each_pattern in path_lower for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS)
|
|
73
70
|
|
|
74
71
|
|
|
75
72
|
def is_test_file(file_path: str) -> bool:
|
|
@@ -106,7 +103,7 @@ def is_workflow_registry_file(file_path: str) -> bool:
|
|
|
106
103
|
These are module-level singletons, not misplaced literal constants.
|
|
107
104
|
"""
|
|
108
105
|
path_lower = file_path.lower().replace("\\", "/")
|
|
109
|
-
return any(pattern
|
|
106
|
+
return any(pattern in path_lower for pattern in ALL_WORKFLOW_REGISTRY_PATTERNS)
|
|
110
107
|
|
|
111
108
|
|
|
112
109
|
def is_spec_file(file_path: str) -> bool:
|
|
@@ -523,7 +520,7 @@ def _is_dedicated_constants_module(file_path: str) -> bool:
|
|
|
523
520
|
def is_migration_file(file_path: str) -> bool:
|
|
524
521
|
"""Check if file is a Django migration (must be self-contained)."""
|
|
525
522
|
path_lower = file_path.lower().replace("\\", "/")
|
|
526
|
-
return any(pattern
|
|
523
|
+
return any(pattern in path_lower for pattern in ALL_MIGRATION_PATH_PATTERNS)
|
|
527
524
|
|
|
528
525
|
|
|
529
526
|
def docstring_line_numbers(content: str) -> set[int]:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Add hooks/blocking and hooks/ to sys.path for every test collected under this directory."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Iterator
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import Callable
|
|
8
11
|
|
|
@@ -14,6 +17,25 @@ for each_directory in (_BLOCKING_DIRECTORY, _HOOKS_DIRECTORY):
|
|
|
14
17
|
if each_directory not in sys.path:
|
|
15
18
|
sys.path.insert(0, each_directory)
|
|
16
19
|
|
|
20
|
+
_HOOK_INFRASTRUCTURE_TAIL = Path("packages") / "claude-dev-env" / "hooks" / "blocking"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def hook_blocking_dir() -> Iterator[Path]:
|
|
25
|
+
"""Build a temporary tree whose tail mirrors a production hook directory.
|
|
26
|
+
|
|
27
|
+
``is_hook_infrastructure`` matches on the path tail, so a target inside
|
|
28
|
+
the yielded directory routes the way a
|
|
29
|
+
``packages/claude-dev-env/hooks/blocking`` target does.
|
|
30
|
+
"""
|
|
31
|
+
base_directory = Path(tempfile.mkdtemp())
|
|
32
|
+
blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
|
|
33
|
+
blocking_directory.mkdir(parents=True)
|
|
34
|
+
try:
|
|
35
|
+
yield blocking_directory
|
|
36
|
+
finally:
|
|
37
|
+
shutil.rmtree(base_directory, ignore_errors=False)
|
|
38
|
+
|
|
17
39
|
|
|
18
40
|
@pytest.fixture
|
|
19
41
|
def three_operation_patch() -> str:
|
|
@@ -22,6 +22,10 @@ try:
|
|
|
22
22
|
sys.path.insert(0, _hooks_dir)
|
|
23
23
|
|
|
24
24
|
from blocking.code_rules_shared import is_ephemeral_path
|
|
25
|
+
from hooks_constants.code_rules_enforcer_constants import (
|
|
26
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER,
|
|
27
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER,
|
|
28
|
+
)
|
|
25
29
|
from hooks_constants.hook_block_logger import log_hook_block
|
|
26
30
|
from hooks_constants.multi_edit_reconstruction import joined_new_strings
|
|
27
31
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
@@ -34,8 +38,6 @@ try:
|
|
|
34
38
|
ALL_HASH_AND_SLASH_EXTENSIONS,
|
|
35
39
|
ALL_HASH_ONLY_EXTENSIONS,
|
|
36
40
|
ALL_MARKDOWN_EXTENSIONS,
|
|
37
|
-
BLOCK_COMMENT_CLOSE_MARKER,
|
|
38
|
-
BLOCK_COMMENT_OPEN_MARKER,
|
|
39
41
|
DOUBLE_QUOTE_BODY_GROUP,
|
|
40
42
|
DOUBLE_QUOTED_SPAN_PATTERN,
|
|
41
43
|
INLINE_CODE_PATTERN,
|
|
@@ -90,13 +92,13 @@ def _consume_block_comment_open(
|
|
|
90
92
|
on this same line (``None`` when the whole line was consumed), and
|
|
91
93
|
whether the comment is still open past this line.
|
|
92
94
|
"""
|
|
93
|
-
slash_star_index = stripped.find(
|
|
95
|
+
slash_star_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_OPENER)
|
|
94
96
|
close_star_index = stripped.find(
|
|
95
|
-
|
|
97
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER, slash_star_index + len(JAVASCRIPT_BLOCK_COMMENT_OPENER)
|
|
96
98
|
)
|
|
97
99
|
if close_star_index < 0:
|
|
98
100
|
return [(each_line_number, stripped[slash_star_index:])], None, True
|
|
99
|
-
close_end = close_star_index + len(
|
|
101
|
+
close_end = close_star_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
|
|
100
102
|
entry = (each_line_number, stripped[slash_star_index:close_end])
|
|
101
103
|
after_close = stripped[close_end:].lstrip()
|
|
102
104
|
return [entry], (after_close or None), False
|
|
@@ -110,10 +112,10 @@ def _consume_block_comment_continuation(
|
|
|
110
112
|
Returns the extracted entries, whether the comment is still open past
|
|
111
113
|
this line, and whether the caller should move to the next source line.
|
|
112
114
|
"""
|
|
113
|
-
close_index = stripped.find(
|
|
115
|
+
close_index = stripped.find(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
|
|
114
116
|
if close_index < 0:
|
|
115
117
|
return [(each_line_number, stripped)], True, True
|
|
116
|
-
close_end = close_index + len(
|
|
118
|
+
close_end = close_index + len(JAVASCRIPT_BLOCK_COMMENT_CLOSER)
|
|
117
119
|
return [(each_line_number, stripped[:close_end])], False, False
|
|
118
120
|
|
|
119
121
|
|
|
@@ -140,7 +142,7 @@ def _extract_comment_lines(text: str, extension: str = "") -> list[tuple[int, st
|
|
|
140
142
|
):
|
|
141
143
|
all_comment_lines.append((each_line_number, stripped))
|
|
142
144
|
continue
|
|
143
|
-
if
|
|
145
|
+
if JAVASCRIPT_BLOCK_COMMENT_OPENER in stripped and not is_in_block_comment:
|
|
144
146
|
entries, leftover, is_in_block_comment = _consume_block_comment_open(
|
|
145
147
|
each_line_number, stripped
|
|
146
148
|
)
|
|
@@ -24,6 +24,8 @@ from collections.abc import Iterator
|
|
|
24
24
|
|
|
25
25
|
import pytest
|
|
26
26
|
|
|
27
|
+
from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE
|
|
28
|
+
|
|
27
29
|
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
28
30
|
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
29
31
|
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
@@ -39,17 +41,6 @@ _hook_spec.loader.exec_module(_hook_module)
|
|
|
39
41
|
check_duplicate_function_body_across_files = _hook_module.check_duplicate_function_body_across_files
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
SHARED_HELPER_SOURCE = (
|
|
43
|
-
"import re\n"
|
|
44
|
-
"\n"
|
|
45
|
-
"def strip_code_and_quotes(text: str) -> str:\n"
|
|
46
|
-
" without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
|
|
47
|
-
" without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
|
|
48
|
-
" without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
|
|
49
|
-
" return without_quotes.strip()\n"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
44
|
@pytest.fixture
|
|
54
45
|
def module_dir() -> Iterator[pathlib.Path]:
|
|
55
46
|
base_directory = pathlib.Path(tempfile.mkdtemp())
|
|
@@ -9,18 +9,15 @@ points (the ``main()`` stdin path and the pre-check CLI) with a hook-infrastruct
|
|
|
9
9
|
target, so each lane's behavior is proven on the same path a live Write or a
|
|
10
10
|
pre-check run would take, rather than by calling the check function directly.
|
|
11
11
|
|
|
12
|
-
Each test
|
|
13
|
-
|
|
14
|
-
target
|
|
12
|
+
Each test takes the ``hook_blocking_dir`` fixture from ``conftest.py``, which builds
|
|
13
|
+
a temporary tree matching the production hook directory so ``is_hook_infrastructure``
|
|
14
|
+
routes a target inside it the same way it would for the production directory.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import pathlib
|
|
20
|
-
import shutil
|
|
21
20
|
import sys
|
|
22
|
-
import tempfile
|
|
23
|
-
from collections.abc import Iterator
|
|
24
21
|
|
|
25
22
|
import pytest
|
|
26
23
|
|
|
@@ -33,35 +30,13 @@ if str(_HOOKS_PARENT) not in sys.path:
|
|
|
33
30
|
|
|
34
31
|
from code_rules_enforcer import main # noqa: E402
|
|
35
32
|
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
33
|
+
STRIP_CODE_AND_QUOTES_SOURCE as SHARED_HELPER_SOURCE,
|
|
36
34
|
run_precheck,
|
|
37
35
|
run_write_entrypoint,
|
|
38
36
|
)
|
|
39
37
|
|
|
40
38
|
pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
|
|
41
39
|
|
|
42
|
-
SHARED_HELPER_SOURCE = (
|
|
43
|
-
"import re\n"
|
|
44
|
-
"\n"
|
|
45
|
-
"def strip_code_and_quotes(text: str) -> str:\n"
|
|
46
|
-
" without_fences = re.sub(r'```.*?```', '', text, flags=re.DOTALL)\n"
|
|
47
|
-
" without_inline = re.sub(r'`[^`]*`', '', without_fences)\n"
|
|
48
|
-
" without_quotes = re.sub(r'(?m)^>.*$', '', without_inline)\n"
|
|
49
|
-
" return without_quotes.strip()\n"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
_HOOK_INFRASTRUCTURE_TAIL = pathlib.Path("packages") / "claude-dev-env" / "hooks" / "blocking"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.fixture
|
|
56
|
-
def hook_blocking_dir() -> Iterator[pathlib.Path]:
|
|
57
|
-
base_directory = pathlib.Path(tempfile.mkdtemp())
|
|
58
|
-
blocking_directory = base_directory / _HOOK_INFRASTRUCTURE_TAIL
|
|
59
|
-
blocking_directory.mkdir(parents=True)
|
|
60
|
-
try:
|
|
61
|
-
yield blocking_directory
|
|
62
|
-
finally:
|
|
63
|
-
shutil.rmtree(base_directory, ignore_errors=False)
|
|
64
|
-
|
|
65
40
|
|
|
66
41
|
def _run_main_with_write_payload(
|
|
67
42
|
file_path: str,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Tests for the shared constants this directory's test-support module provides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from code_rules_enforcer_test_support import STRIP_CODE_AND_QUOTES_SOURCE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def should_define_a_strip_code_and_quotes_function_that_removes_fenced_code() -> None:
|
|
12
|
+
module_namespace: dict[str, object] = {}
|
|
13
|
+
exec(STRIP_CODE_AND_QUOTES_SOURCE, module_namespace)
|
|
14
|
+
strip_code_and_quotes = cast("Callable[[str], str]", module_namespace["strip_code_and_quotes"])
|
|
15
|
+
|
|
16
|
+
stripped = strip_code_and_quotes("keep this\n```\ncode block\n```\n> a quote\nkeep this too")
|
|
17
|
+
|
|
18
|
+
assert "code block" not in stripped
|
|
19
|
+
assert "a quote" not in stripped
|
|
20
|
+
assert "keep this" in stripped
|
|
21
|
+
assert "keep this too" in stripped
|