claude-dev-env 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/advisor-tool.md +1 -1
- package/docs/references/prose-style-enforcement.md +25 -0
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +11 -0
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +26 -5
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +16 -7
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
|
@@ -156,9 +156,10 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
156
156
|
payload_text = json.dumps(
|
|
157
157
|
{
|
|
158
158
|
"stop_hook_active": False,
|
|
159
|
-
"last_assistant_message": "This is probably correct
|
|
159
|
+
"last_assistant_message": "This is probably correct.",
|
|
160
160
|
}
|
|
161
161
|
)
|
|
162
|
+
environment_by_key = {**os.environ, "CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}
|
|
162
163
|
completed = subprocess.run(
|
|
163
164
|
[sys.executable, _DISPATCHER_SCRIPT],
|
|
164
165
|
check=False,
|
|
@@ -166,6 +167,7 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
166
167
|
capture_output=True,
|
|
167
168
|
text=True,
|
|
168
169
|
encoding="utf-8",
|
|
170
|
+
env=environment_by_key,
|
|
169
171
|
)
|
|
170
172
|
assert completed.returncode == 0
|
|
171
173
|
parsed = json.loads(completed.stdout)
|
|
@@ -173,14 +175,16 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
173
175
|
assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
|
|
174
176
|
|
|
175
177
|
|
|
176
|
-
def
|
|
177
|
-
"""An
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
for _ in range(30)
|
|
178
|
+
def test_dispatcher_blocks_eli11_shape_violation_matching_standalone() -> None:
|
|
179
|
+
"""An ELI11 shape violation (too many bullets) blocks through the dispatcher."""
|
|
180
|
+
too_many_bullets_message = "\n".join(
|
|
181
|
+
f"- finding{each_index} detail for this line" for each_index in range(7)
|
|
181
182
|
)
|
|
182
183
|
payload_text = json.dumps(
|
|
183
|
-
{
|
|
184
|
+
{
|
|
185
|
+
"stop_hook_active": False,
|
|
186
|
+
"last_assistant_message": too_many_bullets_message,
|
|
187
|
+
}
|
|
184
188
|
)
|
|
185
189
|
completed = subprocess.run(
|
|
186
190
|
[sys.executable, _DISPATCHER_SCRIPT],
|
|
@@ -31,13 +31,13 @@ def _body_names_volatile_path(tool_name: str, tool_input: dict[str, object]) ->
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def test_scan_detects_job_scratch_path_backslash() -> None:
|
|
34
|
-
text = r"See C:\Users\example\.claude-
|
|
35
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
34
|
+
text = r"See C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"
|
|
35
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_scan_detects_job_scratch_path_forward_slash() -> None:
|
|
39
|
-
text = "artifact at /home/user/.claude-
|
|
40
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
39
|
+
text = "artifact at /home/user/.claude-profile-a/jobs/abc/tmp/out.png"
|
|
40
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_scan_detects_worktree_path() -> None:
|
|
@@ -92,12 +92,12 @@ def test_scan_home_anchored_worktree_path_is_detected() -> None:
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def test_scan_posix_absolute_job_scratch_path_is_detected() -> None:
|
|
95
|
-
text = "/home/me/.claude-
|
|
96
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
95
|
+
text = "/home/me/.claude-profile-a/jobs/j1/log.txt"
|
|
96
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def test_scan_bare_parenthesized_job_scratch_mention_is_allowed() -> None:
|
|
100
|
-
text = "(.claude-
|
|
100
|
+
text = "(.claude-profile-a/jobs/) as a directory name in prose"
|
|
101
101
|
assert scan_text_for_volatile_marker(text) is None
|
|
102
102
|
|
|
103
103
|
|
|
@@ -139,8 +139,8 @@ def test_scan_markdown_link_relative_worktree_target_is_detected() -> None:
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def test_scan_space_prefixed_relative_job_scratch_path_is_detected() -> None:
|
|
142
|
-
text = "see .claude-
|
|
143
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
142
|
+
text = "see .claude-profile-a/jobs/j1/log.txt"
|
|
143
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def test_scan_backslash_relative_worktree_path_is_detected() -> None:
|
|
@@ -171,7 +171,7 @@ def test_scan_sentence_final_marker_period_is_allowed() -> None:
|
|
|
171
171
|
def test_gh_comment_with_job_scratch_path_is_blocked() -> None:
|
|
172
172
|
command = (
|
|
173
173
|
'gh pr comment 669 --body "Contact sheet at '
|
|
174
|
-
r'C:\Users\example\.claude-
|
|
174
|
+
r'C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"'
|
|
175
175
|
)
|
|
176
176
|
assert _body_names_volatile_path("Bash", {"command": command})
|
|
177
177
|
|
|
@@ -223,7 +223,7 @@ def test_substring_mention_of_gh_post_does_not_classify() -> None:
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def test_gh_word_not_first_token_does_not_classify() -> None:
|
|
226
|
-
command = r'echo gh pr comment 42 --body "see C:\.claude-
|
|
226
|
+
command = r'echo gh pr comment 42 --body "see C:\.claude-profile-a\jobs\x"'
|
|
227
227
|
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
228
228
|
|
|
229
229
|
|
|
@@ -279,7 +279,7 @@ def test_gh_issue_edit_quoting_bare_constant_is_allowed(tmp_path: pathlib.Path)
|
|
|
279
279
|
body_file.write_text(
|
|
280
280
|
"The manifest skips these tooling-state prefixes:\n"
|
|
281
281
|
' ".claude/worktrees/",\n'
|
|
282
|
-
' ".claude-
|
|
282
|
+
' ".claude-profile-a/jobs/",\n'
|
|
283
283
|
"Neither is a machine-local path.",
|
|
284
284
|
encoding="utf-8",
|
|
285
285
|
)
|
|
@@ -21,7 +21,7 @@ another argument never classifies.
|
|
|
21
21
|
|
|
22
22
|
The marker scan runs in two tiers over the normalized body. Five bare markers
|
|
23
23
|
— the system temp locations and env tokens — match as a plain substring
|
|
24
|
-
anywhere. Two dot-relative directory markers, ``.claude-
|
|
24
|
+
anywhere. Two dot-relative directory markers, ``.claude-profile-a/jobs/`` and
|
|
25
25
|
``.claude/worktrees/``, match when either a slash sits immediately before the
|
|
26
26
|
marker or a path segment follows it. A machine-local absolute path
|
|
27
27
|
(``~/.claude/worktrees/wt/f.py``) and a relative path that names a child
|
|
@@ -117,7 +117,7 @@ def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
|
117
117
|
markers match anywhere::
|
|
118
118
|
|
|
119
119
|
ok: "the `.claude/worktrees/` manifest key" -> None
|
|
120
|
-
flag: r"C:\\Users\\me\\.claude-
|
|
120
|
+
flag: r"C:\\Users\\me\\.claude-profile-a\\jobs\\x" -> ".claude-profile-a/jobs/"
|
|
121
121
|
flag: "see .claude/worktrees/wt-1/notes.md" -> ".claude/worktrees/"
|
|
122
122
|
flag: "saved to %TEMP%\\out.txt" -> "%temp%"
|
|
123
123
|
|
package/hooks/hooks.json
CHANGED
|
@@ -176,6 +176,21 @@
|
|
|
176
176
|
"type": "command",
|
|
177
177
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/task_list_loop_starter.py",
|
|
178
178
|
"timeout": 10
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"type": "command",
|
|
182
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/working_style_prompt.py",
|
|
183
|
+
"timeout": 10
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"type": "command",
|
|
187
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/orchestrator_auto_starter.py",
|
|
188
|
+
"timeout": 10
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"type": "command",
|
|
192
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/issue_tracker_session_starter.py",
|
|
193
|
+
"timeout": 10
|
|
179
194
|
}
|
|
180
195
|
]
|
|
181
196
|
}
|
|
@@ -8,6 +8,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
|
|
10
10
|
| `any_type_config.py` | Config for the `Any`-type escape-hatch check |
|
|
11
|
+
| `ask_user_question_shape.py` | Pure AskUserQuestion lean-block shape analyzer: grades question and option-description prose against shared chat-detail markers and sentence/word caps with no hook I/O |
|
|
11
12
|
| `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
|
|
12
13
|
| `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
|
|
13
14
|
| `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
|
|
@@ -31,11 +32,12 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
31
32
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
32
33
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
33
34
|
| `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
|
|
34
|
-
| `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (
|
|
35
|
+
| `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (bullet cap, lead-line window, per-list-line word cap, overpacked list-line cap), the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
|
|
35
36
|
| `fable_spawn_gate_constants.py` | The spawn tool-name set, the fable model alias and the model-id segment split pattern, the `FABLE-SPAWN-AUTHORIZED` authorization token, the advisor-protocol document path, payload field names, the deny message, and the deny `additionalContext` recovery text for the fable-tier spawn gate |
|
|
36
37
|
| `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
|
|
37
38
|
| `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
|
|
38
39
|
| `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
|
|
40
|
+
| `hedging_uncertainty_constants.py` | Sentence-boundary pattern, explicit uncertainty label patterns, hedge-term list separator, and positive corrective block text for OP-07C sentence-scoped hedging |
|
|
39
41
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
40
42
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
41
43
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
@@ -54,15 +56,17 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
54
56
|
| `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
|
|
55
57
|
| `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
|
|
56
58
|
| `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
|
|
57
|
-
| `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker |
|
|
59
|
+
| `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker; classification that consumes those program and wrapper constants lives in `pytest_invocation.py` |
|
|
60
|
+
| `pytest_invocation.py` | Pure parsed-segment classifier for pytest runs through interpreters, shims, wrappers, and shell string-exec forms; no pipeline deny decision |
|
|
58
61
|
| `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
|
|
59
62
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
60
63
|
| `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
|
|
61
64
|
| `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
|
|
62
65
|
| `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
|
|
66
|
+
| `prose_matcher_precision_constants.py` | Sample floor, keep/narrow precision floors, matcher ids, label decisions, advisory log path, emit caps, and context fingerprint length for OP-07B prose-matcher advisory telemetry |
|
|
63
67
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
64
68
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
65
|
-
|
|
|
69
|
+
| project_paths_reader.py | Loads ~/.claude/project-paths.json and finds a home-bounded git root for registry checks |
|
|
66
70
|
| `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
|
|
67
71
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
68
72
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
@@ -72,8 +76,14 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
72
76
|
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
|
|
73
77
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
74
78
|
| `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
|
|
79
|
+
| `session_start_injector.py` | Pure SessionStart context injector: normalize `source` (`startup`/`resume`/`clear`/`compact`), honor enable/timeout, return structured injection results for starter hooks |
|
|
80
|
+
| `session_start_injector_constants.py` | Known SessionStart source values, injection status codes, enable env var, default timeout for the shared injector |
|
|
81
|
+
| `test_session_start_injector.py` | Behavioral tests for every known source, disabled, timeout, and unknown-source paths |
|
|
82
|
+
| `orchestrator_auto_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the orchestrator auto-starter |
|
|
83
|
+
| `issue_tracker_session_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the repository-gated issue-tracker starter |
|
|
75
84
|
| `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
|
|
76
85
|
| `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
|
|
86
|
+
| `shell_command_pipeline.py` | Quote-aware shell pipeline parsing: operator-paired segments after line-continuation join, comment strip, heredoc body drop, and parenthesis-group join |
|
|
77
87
|
| `shell_command_segments.py` | Shell tokenization helpers shared by the segment-walking blockers: segment splitting on control operators, a segment's effective leading program, and a token's basename |
|
|
78
88
|
| `shell_substitution_blocker_constants.py` | Tool name, payload keys, the four substitution-detection patterns, the deny decision shape, and the corrective message for the shell-substitution blocker |
|
|
79
89
|
| `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
|
|
@@ -91,6 +101,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
91
101
|
| `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
|
|
92
102
|
| `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
|
|
93
103
|
| `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
|
|
104
|
+
| `working_style_prompt_constants.py` | The fixed multi-paragraph working-style prompt text for the SessionStart working_style_prompt hook |
|
|
94
105
|
|
|
95
106
|
## Conventions
|
|
96
107
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Pure AskUserQuestion payload-shape analysis.
|
|
2
|
+
|
|
3
|
+
Maps an AskUserQuestion tool input to lean-block violations without hook I/O,
|
|
4
|
+
deny payloads, or environment state::
|
|
5
|
+
|
|
6
|
+
analyze_ask_user_question_shape({"questions": [...]})
|
|
7
|
+
ok: a lean question and short options -> empty violations
|
|
8
|
+
flag: a bullet list under the question -> named violation texts
|
|
9
|
+
|
|
10
|
+
Callers that need a deny reason compose with the shared lean-block message
|
|
11
|
+
constants; this module only grades shape.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from hooks_constants.plain_language_blocker_constants import (
|
|
20
|
+
ALL_CHAT_DETAIL_MARKERS,
|
|
21
|
+
ALL_LINE_ENDING_REPLACEMENTS,
|
|
22
|
+
COUNTABLE_WORD_PATTERN,
|
|
23
|
+
INLINE_CODE_PLACEHOLDER,
|
|
24
|
+
INLINE_CODE_SPAN_PATTERN,
|
|
25
|
+
MAXIMUM_OPTION_DESCRIPTION_SENTENCE_COUNT,
|
|
26
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT,
|
|
27
|
+
MAXIMUM_QUESTION_SENTENCE_COUNT,
|
|
28
|
+
MAXIMUM_QUESTION_WORD_COUNT,
|
|
29
|
+
OPTION_DESCRIPTION_SURFACE_NAME,
|
|
30
|
+
QUESTION_SURFACE_NAME,
|
|
31
|
+
SENTENCE_BOUNDARY_PATTERN,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AskUserQuestionShapeResult",
|
|
36
|
+
"analyze_ask_user_question_shape",
|
|
37
|
+
"count_prose_sentences",
|
|
38
|
+
"count_prose_words",
|
|
39
|
+
"find_chat_detail_markers",
|
|
40
|
+
"find_lean_block_violations",
|
|
41
|
+
"find_question_block_violations",
|
|
42
|
+
"mask_inline_code",
|
|
43
|
+
"normalize_line_endings",
|
|
44
|
+
"violations_for_one_question",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AskUserQuestionShapeResult:
|
|
50
|
+
"""Shape-grade for one AskUserQuestion tool input.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
all_violations: One violation text per broken lean-block rule, in
|
|
54
|
+
first-seen order, empty when the whole question block is lean.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
all_violations: tuple[str, ...]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_lean(self) -> bool:
|
|
61
|
+
"""True when the payload carries no lean-block violations."""
|
|
62
|
+
return not self.all_violations
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_line_endings(prose_text: str) -> str:
|
|
66
|
+
"""Return the prose with every line ending spelled as a bare line feed.
|
|
67
|
+
|
|
68
|
+
::
|
|
69
|
+
|
|
70
|
+
in: "Which gate?\\r\\n\\r\\nThe write gate reads it."
|
|
71
|
+
out: "Which gate?\\n\\nThe write gate reads it."
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
prose_text: One question text or one option description.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The prose with carriage returns folded into line feeds.
|
|
78
|
+
"""
|
|
79
|
+
normalized_text = prose_text
|
|
80
|
+
for each_line_ending, each_replacement in ALL_LINE_ENDING_REPLACEMENTS:
|
|
81
|
+
normalized_text = normalized_text.replace(each_line_ending, each_replacement)
|
|
82
|
+
return normalized_text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def mask_inline_code(prose_text: str) -> str:
|
|
86
|
+
"""Return the prose with each inline code span collapsed to a single word.
|
|
87
|
+
|
|
88
|
+
::
|
|
89
|
+
|
|
90
|
+
in: "Does `git diff --cached` list it?"
|
|
91
|
+
out: "Does code list it?"
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
prose_text: One question text or one option description.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The prose with every single-line inline code span replaced.
|
|
98
|
+
"""
|
|
99
|
+
return INLINE_CODE_SPAN_PATTERN.sub(INLINE_CODE_PLACEHOLDER, prose_text)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def count_prose_sentences(prose_text: str) -> int:
|
|
103
|
+
"""Return how many sentences one piece of question-block prose carries.
|
|
104
|
+
|
|
105
|
+
::
|
|
106
|
+
|
|
107
|
+
in: "Which gate runs first? Both read the same lines."
|
|
108
|
+
out: 2
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
prose_text: One question text or one option description.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The count of sentence closings the prose carries.
|
|
115
|
+
"""
|
|
116
|
+
return len(SENTENCE_BOUNDARY_PATTERN.findall(prose_text))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def count_prose_words(prose_text: str) -> int:
|
|
120
|
+
"""Return how many reader-visible words one piece of prose carries.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
prose_text: One question text or one option description.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The count of whitespace-separated tokens carrying a letter or digit.
|
|
127
|
+
"""
|
|
128
|
+
return len(COUNTABLE_WORD_PATTERN.findall(prose_text))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def find_chat_detail_markers(prose_text: str) -> list[str]:
|
|
132
|
+
"""Return the name of each chat-detail marker the prose carries.
|
|
133
|
+
|
|
134
|
+
::
|
|
135
|
+
|
|
136
|
+
ok: "Which gate should run first?" -> []
|
|
137
|
+
flag: "Which gate?\\n- write\\n- commit" -> ["a bullet or numbered
|
|
138
|
+
list marker"]
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prose_text: One question text or one option description.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
One marker name per marker found, in the order the markers are tried.
|
|
145
|
+
"""
|
|
146
|
+
return [
|
|
147
|
+
each_marker_name
|
|
148
|
+
for each_pattern, each_marker_name in ALL_CHAT_DETAIL_MARKERS
|
|
149
|
+
if each_pattern.search(prose_text)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_lean_block_violations(
|
|
154
|
+
prose_text: str,
|
|
155
|
+
surface_name: str,
|
|
156
|
+
maximum_sentence_count: int,
|
|
157
|
+
maximum_word_count: int,
|
|
158
|
+
) -> list[str]:
|
|
159
|
+
"""Return every lean-block rule one piece of question-block prose breaks.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
prose_text: One question text or one option description.
|
|
163
|
+
surface_name: How the violation text names this piece of prose.
|
|
164
|
+
maximum_sentence_count: The sentence cap this piece answers to.
|
|
165
|
+
maximum_word_count: The word cap this piece answers to.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
One violation text per broken rule, empty when the prose is lean.
|
|
169
|
+
"""
|
|
170
|
+
normalized_text = normalize_line_endings(prose_text)
|
|
171
|
+
masked_text = mask_inline_code(normalized_text)
|
|
172
|
+
all_violations = [
|
|
173
|
+
f"{surface_name} carries {each_marker_name}"
|
|
174
|
+
for each_marker_name in find_chat_detail_markers(normalized_text)
|
|
175
|
+
]
|
|
176
|
+
sentence_count = count_prose_sentences(masked_text)
|
|
177
|
+
if sentence_count > maximum_sentence_count:
|
|
178
|
+
all_violations.append(
|
|
179
|
+
f"{surface_name} runs {sentence_count} sentences, over the "
|
|
180
|
+
f"{maximum_sentence_count}-sentence cap"
|
|
181
|
+
)
|
|
182
|
+
word_count = count_prose_words(masked_text)
|
|
183
|
+
if word_count > maximum_word_count:
|
|
184
|
+
all_violations.append(
|
|
185
|
+
f"{surface_name} runs {word_count} words, over the "
|
|
186
|
+
f"{maximum_word_count}-word cap"
|
|
187
|
+
)
|
|
188
|
+
return all_violations
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def violations_for_one_question(question_by_key: Mapping[str, object]) -> list[str]:
|
|
192
|
+
"""Return the lean-block violations one question entry carries.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
question_by_key: One entry of the AskUserQuestion questions list.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
One violation text per broken rule across the question text and every
|
|
199
|
+
option description the entry carries.
|
|
200
|
+
"""
|
|
201
|
+
all_violations: list[str] = []
|
|
202
|
+
question_text = question_by_key.get("question", "")
|
|
203
|
+
if isinstance(question_text, str):
|
|
204
|
+
all_violations.extend(
|
|
205
|
+
find_lean_block_violations(
|
|
206
|
+
question_text,
|
|
207
|
+
QUESTION_SURFACE_NAME,
|
|
208
|
+
MAXIMUM_QUESTION_SENTENCE_COUNT,
|
|
209
|
+
MAXIMUM_QUESTION_WORD_COUNT,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
all_options = question_by_key.get("options", [])
|
|
213
|
+
if not isinstance(all_options, Sequence) or isinstance(all_options, (str, bytes)):
|
|
214
|
+
return all_violations
|
|
215
|
+
for each_option in all_options:
|
|
216
|
+
if not isinstance(each_option, Mapping):
|
|
217
|
+
continue
|
|
218
|
+
option_description = each_option.get("description", "")
|
|
219
|
+
if not isinstance(option_description, str):
|
|
220
|
+
continue
|
|
221
|
+
all_violations.extend(
|
|
222
|
+
find_lean_block_violations(
|
|
223
|
+
option_description,
|
|
224
|
+
OPTION_DESCRIPTION_SURFACE_NAME,
|
|
225
|
+
MAXIMUM_OPTION_DESCRIPTION_SENTENCE_COUNT,
|
|
226
|
+
MAXIMUM_OPTION_DESCRIPTION_WORD_COUNT,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
return all_violations
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def find_question_block_violations(payload_by_key: Mapping[str, object]) -> list[str]:
|
|
233
|
+
"""Return every lean-block violation an AskUserQuestion payload carries.
|
|
234
|
+
|
|
235
|
+
::
|
|
236
|
+
|
|
237
|
+
ok: "Which gate should run first?" + "Runs on every write." -> []
|
|
238
|
+
flag: a question with a three-bullet plan under it
|
|
239
|
+
-> ["the question carries a bullet or numbered list marker"]
|
|
240
|
+
|
|
241
|
+
Each violation text appears once, so two options breaking the same cap read
|
|
242
|
+
as one line.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
payload_by_key: The AskUserQuestion tool input carrying the questions list.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
One violation text per broken rule, in first-seen order, empty when the
|
|
249
|
+
whole question block is lean.
|
|
250
|
+
"""
|
|
251
|
+
all_questions = payload_by_key.get("questions", [])
|
|
252
|
+
if not isinstance(all_questions, Sequence) or isinstance(all_questions, (str, bytes)):
|
|
253
|
+
return []
|
|
254
|
+
all_violations: list[str] = []
|
|
255
|
+
for each_question in all_questions:
|
|
256
|
+
if isinstance(each_question, Mapping):
|
|
257
|
+
all_violations.extend(violations_for_one_question(each_question))
|
|
258
|
+
return list(dict.fromkeys(all_violations))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def analyze_ask_user_question_shape(
|
|
262
|
+
payload_by_key: Mapping[str, object],
|
|
263
|
+
) -> AskUserQuestionShapeResult:
|
|
264
|
+
"""Grade an AskUserQuestion tool input for lean-block shape only.
|
|
265
|
+
|
|
266
|
+
::
|
|
267
|
+
|
|
268
|
+
analyze_ask_user_question_shape({"questions": [
|
|
269
|
+
{"question": "Which gate?", "options": [{"description": "Runs on write."}]}
|
|
270
|
+
]}).is_lean
|
|
271
|
+
True
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
payload_by_key: The AskUserQuestion tool input carrying the questions list.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
A frozen result holding the ordered, de-duplicated violation texts.
|
|
278
|
+
"""
|
|
279
|
+
return AskUserQuestionShapeResult(
|
|
280
|
+
all_violations=tuple(find_question_block_violations(payload_by_key))
|
|
281
|
+
)
|
|
@@ -27,7 +27,8 @@ CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY: str = "tmp"
|
|
|
27
27
|
EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME: str = "CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT"
|
|
28
28
|
ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
29
29
|
LEADING_DRIVE_LETTER_PATTERN: re.Pattern[str] = re.compile(r"^[a-z]:")
|
|
30
|
-
|
|
30
|
+
ALL_AGENT_HOME_TOOLING_PATTERNS = {"/.grok/", "\\.grok\\", "\\.grok/"}
|
|
31
|
+
ALL_HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\.claude/hooks/", "/packages/claude-dev-env/hooks/", "\\packages\\claude-dev-env\\hooks\\"} | ALL_AGENT_HOME_TOOLING_PATTERNS
|
|
31
32
|
ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
|
|
32
33
|
ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
|
|
33
34
|
|
|
@@ -24,13 +24,10 @@ __all__ = [
|
|
|
24
24
|
"IMPERATIVE_OBJECT_TOKEN_PATTERN",
|
|
25
25
|
"LINK_TARGET_PATTERN",
|
|
26
26
|
"LIST_MARKER_PREFIX_PATTERN",
|
|
27
|
-
"LONG_FORM_ESCAPE_PREFIX",
|
|
28
27
|
"MARKDOWN_LEAD_MARKER_PATTERN",
|
|
29
28
|
"MAXIMUM_BULLET_LINE_COUNT",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"MAXIMUM_WORDS_PER_LINE",
|
|
33
|
-
"MINIMUM_ENFORCED_WORD_COUNT",
|
|
29
|
+
"MAXIMUM_OVERPACKED_LIST_LINE_COUNT",
|
|
30
|
+
"MAXIMUM_WORDS_PER_LIST_LINE",
|
|
34
31
|
"NUMBERED_STEP_PATTERN",
|
|
35
32
|
"TABLE_ROW_PATTERN",
|
|
36
33
|
"TARGET_BULLET_LINE_COUNT",
|
|
@@ -38,18 +35,14 @@ __all__ = [
|
|
|
38
35
|
"VIOLATION_SEPARATOR",
|
|
39
36
|
]
|
|
40
37
|
|
|
41
|
-
MAXIMUM_REPLY_WORD_COUNT = 120
|
|
42
|
-
MINIMUM_ENFORCED_WORD_COUNT = 60
|
|
43
38
|
MAXIMUM_BULLET_LINE_COUNT = 6
|
|
44
39
|
TARGET_BULLET_LINE_COUNT = 3
|
|
45
40
|
ACTION_FIRST_LEAD_LINE_COUNT = 6
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
MAXIMUM_WORDS_PER_LIST_LINE = 20
|
|
42
|
+
MAXIMUM_OVERPACKED_LIST_LINE_COUNT = 2
|
|
48
43
|
|
|
49
|
-
LONG_FORM_ESCAPE_PREFIX = "long form:"
|
|
50
44
|
VIOLATION_SEPARATOR = "; "
|
|
51
|
-
USER_FACING_ELI11_NOTICE = "
|
|
52
|
-
|
|
45
|
+
USER_FACING_ELI11_NOTICE = "Rewrite the reply into a short action-first shape..."
|
|
53
46
|
TABLE_ROW_PATTERN = re.compile(r"^[ \t]*\|.*\|[ \t]*$", re.MULTILINE)
|
|
54
47
|
LINK_TARGET_PATTERN = re.compile(r"(?:https?://|www\.)\S+", re.IGNORECASE)
|
|
55
48
|
COUNTABLE_WORD_PATTERN = re.compile(r"\S*[A-Za-z0-9]\S*")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Explicit uncertainty labels that co-occur with hedge words in one sentence.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
ok: This claim is unverified; the port is probably down.
|
|
6
|
+
flag: The port is probably down. (no label in that sentence)
|
|
7
|
+
|
|
8
|
+
A response-wide "unverified" label does not exempt a bare hedge in another
|
|
9
|
+
sentence. Classification of matcher precision (OP-07B) uses the same blocking
|
|
10
|
+
surface: only hedges that survive this filter count toward hard-block evidence.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
# Sentence ends at . ! ? when followed by space or end; keep the terminator
|
|
18
|
+
# attached to the sentence so labels next to periods still match.
|
|
19
|
+
SENTENCE_BOUNDARY_PATTERN = re.compile(r"(?<=[.!?])\s+")
|
|
20
|
+
|
|
21
|
+
ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS = (
|
|
22
|
+
re.compile(r"\bunverified\b", re.IGNORECASE),
|
|
23
|
+
re.compile(r"\bi don't know\b", re.IGNORECASE),
|
|
24
|
+
re.compile(r"\bi do not know\b", re.IGNORECASE),
|
|
25
|
+
re.compile(r"\bno source for this claim\b", re.IGNORECASE),
|
|
26
|
+
re.compile(r"\bwithout a (credible )?source\b", re.IGNORECASE),
|
|
27
|
+
re.compile(r"\bnamed (as )?unverified\b", re.IGNORECASE),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HEDGING_TERM_LIST_SEPARATOR = ", "
|
|
31
|
+
|
|
32
|
+
POSITIVE_CORRECTIVE_GUIDANCE = (
|
|
33
|
+
"State the claim with evidence that backs it, or label that claim unverified "
|
|
34
|
+
"in the same sentence as the uncertainty word. "
|
|
35
|
+
"Gather a source, run a live probe, or ask the user through AskUserQuestion — "
|
|
36
|
+
"then re-output the complete revised response."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
BLOCK_REASON_PREFIX = (
|
|
40
|
+
"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language without "
|
|
41
|
+
"an explicit uncertainty label in the same sentence: "
|
|
42
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Constants for the repository-gated issue-tracker SessionStart starter.
|
|
2
|
+
|
|
3
|
+
Default off. Set CLAUDE_ISSUE_TRACKER_SESSION_STARTER_ENABLED to 1/true/yes/on.
|
|
4
|
+
Even when enabled, injects only when the session cwd's git root is registered
|
|
5
|
+
in ~/.claude/project-paths.json. Missing registry fails closed (no inject).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ISSUE_TRACKER_SESSION_STARTER_ENABLED_ENV_VAR: str = (
|
|
11
|
+
"CLAUDE_ISSUE_TRACKER_SESSION_STARTER_ENABLED"
|
|
12
|
+
)
|
|
13
|
+
ALL_ISSUE_TRACKER_STARTER_ENABLED_ENV_VALUES: frozenset[str] = frozenset(
|
|
14
|
+
{"1", "true", "yes", "on"}
|
|
15
|
+
)
|
|
16
|
+
ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS: int = 50
|
|
17
|
+
|
|
18
|
+
ISSUE_TRACKER_SESSION_START_DIRECTIVE: str = (
|
|
19
|
+
"SessionStart issue-tracker opt-in is active for this registered repository. "
|
|
20
|
+
"Load the issue-tracker skill when the session needs one GitHub issue action "
|
|
21
|
+
"(open epic, file sub-issue, update status, refresh checklist, close sub-issue). "
|
|
22
|
+
"Manual issue-tracker agent spawn remains available unchanged."
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Constants for the opt-in orchestrator SessionStart auto-starter.
|
|
2
|
+
|
|
3
|
+
Default off. Set CLAUDE_ORCHESTRATOR_AUTO_STARTER_ENABLED to 1/true/yes/on to
|
|
4
|
+
emit an additionalContext directive that points the session at the orchestrator
|
|
5
|
+
skill. Manual /orchestrator invocation is unchanged.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ORCHESTRATOR_AUTO_STARTER_ENABLED_ENV_VAR: str = (
|
|
11
|
+
"CLAUDE_ORCHESTRATOR_AUTO_STARTER_ENABLED"
|
|
12
|
+
)
|
|
13
|
+
ALL_ORCHESTRATOR_STARTER_ENABLED_ENV_VALUES: frozenset[str] = frozenset(
|
|
14
|
+
{"1", "true", "yes", "on"}
|
|
15
|
+
)
|
|
16
|
+
ORCHESTRATOR_STARTER_TIMEOUT_MILLISECONDS: int = 50
|
|
17
|
+
|
|
18
|
+
ORCHESTRATOR_SESSION_START_DIRECTIVE: str = (
|
|
19
|
+
"SessionStart orchestrator opt-in is active. Load the orchestrator skill "
|
|
20
|
+
"and run in executor-advisor mode for this session's multi-step work. "
|
|
21
|
+
"Do not invent a second orchestrator path; follow the skill. Manual "
|
|
22
|
+
"/orchestrator remains available unchanged."
|
|
23
|
+
)
|
|
@@ -13,7 +13,10 @@ flags from its first operand, the wrapper commands whose run passes through to
|
|
|
13
13
|
the program behind them, the separator that rejoins a wrapper's argument
|
|
14
14
|
tokens, the empty commenter set that leaves ``#`` to the comment-start pattern,
|
|
15
15
|
the quote characters stripped before a basename read, and the deny message.
|
|
16
|
-
Segment helpers come from ``shell_command_segments.py``.
|
|
16
|
+
Segment helpers come from ``shell_command_segments.py``. Quote-aware pipeline
|
|
17
|
+
parsing lives in ``shell_command_pipeline``. Pytest invocation classification
|
|
18
|
+
that consumes these program, wrapper, and option constants lives in
|
|
19
|
+
``pytest_invocation``.
|
|
17
20
|
|
|
18
21
|
Two wrapper shapes reach the program behind them differently. ``sudo`` and
|
|
19
22
|
``uvx`` take their own option flags and then the command, so the step-over drops
|