claude-dev-env 1.38.1 → 1.40.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 +10 -36
- package/_shared/pr-loop/audit-reply-template.md +147 -0
- package/_shared/pr-loop/fix-protocol.md +25 -4
- package/_shared/pr-loop/gh-payloads.md +37 -50
- package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
- package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
- package/_shared/pr-loop/scripts/preflight.py +129 -2
- package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/agents/pr-description-writer.md +150 -52
- package/bin/install.mjs +6 -7
- package/bin/install.test.mjs +8 -0
- package/commands/doc-gist.md +16 -0
- package/commands/plan.md +0 -2
- package/commands/review-plan.md +1 -1
- package/docs/CODE_RULES.md +122 -2
- package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1143 -129
- package/hooks/blocking/convergence_gate_blocker.py +130 -0
- package/hooks/blocking/destructive_command_blocker.py +74 -0
- package/hooks/blocking/gh_body_arg_blocker.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +119 -0
- package/hooks/blocking/pr_description_enforcer.py +57 -22
- package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
- package/hooks/blocking/test_code_rules_enforcer.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
- package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
- package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
- package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
- package/hooks/blocking/test_destructive_command_blocker.py +146 -0
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
- package/hooks/blocking/test_md_to_html_blocker.py +317 -0
- package/hooks/blocking/test_pr_description_enforcer.py +69 -8
- package/hooks/config/any_type_config.py +7 -0
- package/hooks/config/banned_identifiers_constants.py +11 -0
- package/hooks/config/blocking_check_limits.py +38 -0
- package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
- package/hooks/config/code_rules_enforcer_constants.py +53 -0
- package/hooks/config/convergence_branch_constants.py +9 -0
- package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
- package/hooks/config/html_companion_constants.py +20 -0
- package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
- package/hooks/config/pr_description_enforcer_constants.py +14 -0
- package/hooks/config/test_banned_identifiers_constants.py +17 -0
- package/hooks/hooks.json +28 -20
- package/hooks/pyproject.toml +69 -0
- package/hooks/validators/mypy_integration.py +47 -1
- package/hooks/validators/run_all_validators.py +3 -3
- package/hooks/validators/test_mypy_integration.py +50 -1
- package/hooks/workflow/doc_gist_auto_publish.py +144 -0
- package/hooks/workflow/md_to_html_companion.py +365 -0
- package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
- package/hooks/workflow/test_md_to_html_companion.py +452 -0
- package/package.json +1 -1
- package/rules/gh-body-file.md +2 -0
- package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
- package/scripts/check.ps1 +106 -0
- package/scripts/config/timing.py +11 -0
- package/scripts/sweep_empty_dirs.py +138 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_sweep_empty_dirs.py +183 -0
- package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
- package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
- package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
- package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
- package/skills/bugteam/CONSTRAINTS.md +21 -22
- package/skills/bugteam/EXAMPLES.md +3 -3
- package/skills/bugteam/PROMPTS.md +227 -67
- package/skills/bugteam/SKILL.md +132 -455
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +112 -39
- package/skills/bugteam/reference/audit-contract.md +4 -22
- package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
- package/skills/bugteam/reference/design-rationale.md +2 -2
- package/skills/bugteam/reference/github-pr-reviews.md +50 -57
- package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
- package/skills/bugteam/reference/team-setup.md +111 -9
- package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
- package/skills/bugteam/scripts/README.md +60 -0
- package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
- package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
- package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
- package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
- package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
- package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
- package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
- package/skills/bugteam/test_skill_additions.py +1 -11
- package/skills/code/SKILL.md +176 -0
- package/skills/copilot-review/SKILL.md +16 -0
- package/skills/doc-gist/SKILL.md +99 -0
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
- package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
- package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
- package/skills/doc-gist/references/examples/05-design-system.html +629 -0
- package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
- package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
- package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
- package/skills/doc-gist/references/examples/11-status-report.html +528 -0
- package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
- package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
- package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
- package/skills/doc-gist/references/examples/README.md +5 -0
- package/skills/doc-gist/scripts/config/__init__.py +0 -0
- package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
- package/skills/doc-gist/scripts/gist_upload.py +177 -0
- package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
- package/skills/findbugs/SKILL.md +96 -2
- package/skills/monitor-open-prs/SKILL.md +14 -32
- package/skills/monitor-open-prs/test_skill_contract.py +0 -11
- package/skills/pr-consistency-audit/SKILL.md +112 -0
- package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
- package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
- package/skills/pr-converge/SKILL.md +229 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +63 -0
- package/skills/pr-converge/reference/convergence-gates.md +138 -44
- package/skills/pr-converge/reference/examples.md +43 -11
- package/skills/pr-converge/reference/fix-protocol.md +6 -5
- package/skills/pr-converge/reference/ground-rules.md +5 -3
- package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
- package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
- package/skills/pr-converge/reference/per-tick.md +107 -31
- package/skills/pr-converge/reference/state-schema.md +22 -1
- package/skills/pr-converge/reference/stop-conditions.md +9 -7
- package/skills/pr-converge/scripts/README.md +34 -46
- package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
- package/skills/pr-converge/scripts/check_convergence.py +497 -0
- package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
- package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +157 -27
- package/skills/session-log/SKILL.md +216 -114
- package/skills/session-tidy/SKILL.md +1 -1
- package/skills/skill-builder/SKILL.md +138 -56
- package/skills/skill-builder/references/delegation-map.md +72 -113
- package/skills/skill-builder/references/progressive-disclosure.md +122 -0
- package/skills/skill-builder/references/self-audit-checklist.md +92 -0
- package/skills/skill-builder/references/skill-types.md +228 -0
- package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
- package/skills/skill-builder/templates/gap-analysis.md +15 -8
- package/skills/skill-builder/workflows/improve-skill.md +86 -57
- package/skills/skill-builder/workflows/new-skill.md +80 -168
- package/skills/skill-builder/workflows/polish-skill.md +78 -54
- package/skills/structure-prompt/SKILL.md +50 -0
- package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
- package/skills/structure-prompt/reference/block-classification.md +27 -0
- package/skills/structure-prompt/reference/canonical-case.md +48 -0
- package/skills/structure-prompt/reference/citation-depth.md +70 -0
- package/skills/structure-prompt/reference/cleanup.md +33 -0
- package/skills/structure-prompt/reference/constraints.md +33 -0
- package/skills/structure-prompt/reference/directives.md +37 -0
- package/skills/structure-prompt/reference/examples.md +72 -0
- package/skills/structure-prompt/reference/instantiation.md +51 -0
- package/skills/structure-prompt/reference/output-contract.md +72 -0
- package/skills/structure-prompt/reference/per-category.md +23 -0
- package/skills/structure-prompt/reference/persona.md +38 -0
- package/skills/structure-prompt/reference/research.md +33 -0
- package/skills/structure-prompt/reference/structure.md +28 -0
- package/agents/code-standards-agent.md +0 -93
- package/agents/groq-coder.md +0 -113
- package/agents/plan-executor.md +0 -226
- package/agents/project-docs-analyzer.md +0 -53
- package/agents/project-structure-organizer-agent.md +0 -72
- package/agents/skill-to-agent-converter.md +0 -370
- package/agents/skill-writer-agent.md +0 -470
- package/agents/user-docs-writer.md +0 -67
- package/agents/workflow-visual-documenter.md +0 -82
- package/commands/readability-review.md +0 -20
- package/hooks/mypy.ini +0 -2
- package/hooks/notification/attention_needed_notify.py +0 -71
- package/hooks/notification/claude_notification_handler.py +0 -67
- package/hooks/notification/notification_utils.py +0 -267
- package/hooks/notification/subagent_complete_notify.py +0 -381
- package/hooks/notification/test_attention_needed_notify.py +0 -47
- package/hooks/notification/test_claude_notification_handler.py +0 -54
- package/hooks/notification/test_notification_utils.py +0 -91
- package/hooks/notification/test_subagent_complete_notify.py +0 -79
- package/scripts/config/groq_bugteam_config.py +0 -230
- package/scripts/config/test_groq_bugteam_config.py +0 -83
- package/scripts/config/test_spec_implementer_prompt.py +0 -32
- package/scripts/groq_bugteam.README.md +0 -131
- package/scripts/groq_bugteam.py +0 -647
- package/scripts/groq_bugteam_dotenv.py +0 -40
- package/scripts/groq_bugteam_spec.py +0 -226
- package/scripts/test_groq_bugteam.py +0 -529
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
- package/scripts/test_groq_bugteam_dotenv.py +0 -66
- package/scripts/test_groq_bugteam_spec.py +0 -338
- package/skills/bugteam/SKILL_EVALS.md +0 -309
- package/skills/dream/SKILL.md +0 -118
- package/skills/ingest/SKILL.md +0 -40
- package/skills/npm-creator/SKILL.md +0 -187
- package/skills/readability-review/SKILL.md +0 -127
- package/skills/resume-review/SKILL.md +0 -261
- package/skills/rule-audit/SKILL.md +0 -307
- package/skills/rule-creator/SKILL.md +0 -150
- package/skills/searching-obsidian-vault/SKILL.md +0 -131
- package/skills/skill-writer/REFERENCE.md +0 -284
- package/skills/skill-writer/SKILL.md +0 -222
- package/skills/tdd-team/SKILL.md +0 -128
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Tests for md_to_html_companion hook.
|
|
2
|
+
|
|
3
|
+
This test suite validates that the md-to-html companion hook correctly
|
|
4
|
+
generates HTML from markdown input, handles edge cases, and produces
|
|
5
|
+
valid HTML output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_companion.py")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _RunHook:
|
|
19
|
+
def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
|
|
20
|
+
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
21
|
+
return subprocess.run(
|
|
22
|
+
[sys.executable, HOOK_SCRIPT_PATH],
|
|
23
|
+
input=payload,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
check=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_run_hook = _RunHook()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_generates_html_companion():
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
35
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
36
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
40
|
+
f.write("# Hello\n\nThis is a test.")
|
|
41
|
+
|
|
42
|
+
result = _run_hook(
|
|
43
|
+
"Write", {"file_path": md_path, "content": "# Hello\n\nThis is a test."}
|
|
44
|
+
)
|
|
45
|
+
assert result.returncode == 0
|
|
46
|
+
assert os.path.exists(html_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_html_contains_heading():
|
|
50
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
51
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
52
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
53
|
+
f.write("# Hello World")
|
|
54
|
+
|
|
55
|
+
_run_hook("Write", {"file_path": md_path, "content": "# Hello World"})
|
|
56
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
57
|
+
with open(html_path, encoding="utf-8") as f:
|
|
58
|
+
html = f.read()
|
|
59
|
+
assert "<h1>" in html
|
|
60
|
+
assert "Hello World" in html
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_html_wraps_in_template():
|
|
64
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
65
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
66
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
67
|
+
f.write("plain text")
|
|
68
|
+
|
|
69
|
+
_run_hook("Write", {"file_path": md_path, "content": "plain text"})
|
|
70
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
71
|
+
with open(html_path, encoding="utf-8") as f:
|
|
72
|
+
html = f.read()
|
|
73
|
+
assert "<!DOCTYPE html>" in html
|
|
74
|
+
assert "<style>" in html
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_skips_non_md_files():
|
|
78
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
79
|
+
py_path = os.path.join(tmp, "main.py")
|
|
80
|
+
html_path = os.path.join(tmp, "main.html")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
with open(py_path, "w", encoding="utf-8") as f:
|
|
84
|
+
f.write("x = 1")
|
|
85
|
+
|
|
86
|
+
result = _run_hook("Write", {"file_path": py_path, "content": "x = 1"})
|
|
87
|
+
assert result.returncode == 0
|
|
88
|
+
assert not os.path.exists(html_path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_skips_claude_dir():
|
|
92
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
93
|
+
claude_dir = os.path.join(tmp, ".claude")
|
|
94
|
+
md_path = os.path.join(claude_dir, "CLAUDE.md")
|
|
95
|
+
html_path = os.path.join(claude_dir, "CLAUDE.html")
|
|
96
|
+
|
|
97
|
+
os.makedirs(claude_dir, exist_ok=True)
|
|
98
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
99
|
+
f.write("# CLAUDE.md")
|
|
100
|
+
|
|
101
|
+
result = _run_hook("Write", {"file_path": md_path, "content": "# CLAUDE.md"})
|
|
102
|
+
assert result.returncode == 0
|
|
103
|
+
assert not os.path.exists(html_path)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_unknown_tool_passes():
|
|
107
|
+
result = _run_hook("Grep", {"pattern": "foo"})
|
|
108
|
+
assert result.returncode == 0
|
|
109
|
+
assert result.stdout == ""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_empty_file_path_passes():
|
|
113
|
+
result = _run_hook("Write", {"file_path": "", "content": "# Hello"})
|
|
114
|
+
assert result.returncode == 0
|
|
115
|
+
assert result.stdout == ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_nonexistent_md_passes():
|
|
119
|
+
result = _run_hook(
|
|
120
|
+
"Write",
|
|
121
|
+
{"file_path": "/nonexistent/path/guide.md", "content": "# Hello"},
|
|
122
|
+
)
|
|
123
|
+
assert result.returncode == 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_converts_code_fence():
|
|
127
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
128
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
129
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
130
|
+
f.write("```python\nprint('hi')\n```")
|
|
131
|
+
|
|
132
|
+
_run_hook(
|
|
133
|
+
"Write", {"file_path": md_path, "content": "```python\nprint('hi')\n```"}
|
|
134
|
+
)
|
|
135
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
136
|
+
with open(html_path, encoding="utf-8") as f:
|
|
137
|
+
html = f.read()
|
|
138
|
+
assert "<pre>" in html
|
|
139
|
+
assert "<code" in html
|
|
140
|
+
assert "print('hi')" in html
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_converts_bold():
|
|
144
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
145
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
146
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
147
|
+
f.write("This is **bold** text.")
|
|
148
|
+
|
|
149
|
+
_run_hook("Write", {"file_path": md_path, "content": "This is **bold** text."})
|
|
150
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
151
|
+
with open(html_path, encoding="utf-8") as f:
|
|
152
|
+
html = f.read()
|
|
153
|
+
assert "<strong>bold</strong>" in html
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_escapes_html_special_chars():
|
|
157
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
158
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
159
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
160
|
+
f.write("Use <div> for layout & choose \"text\" for quotes.")
|
|
161
|
+
|
|
162
|
+
_run_hook(
|
|
163
|
+
"Write",
|
|
164
|
+
{
|
|
165
|
+
"file_path": md_path,
|
|
166
|
+
"content": "Use <div> for layout & choose \"text\" for quotes.",
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
170
|
+
with open(html_path, encoding="utf-8") as f:
|
|
171
|
+
html = f.read()
|
|
172
|
+
assert "<div>" in html
|
|
173
|
+
assert "&" in html
|
|
174
|
+
assert "<div>" not in html
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_escapes_code_block_content():
|
|
178
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
179
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
180
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
181
|
+
f.write("```\nif x < 5 and y > 3:\n print('hello')\n```")
|
|
182
|
+
|
|
183
|
+
_run_hook(
|
|
184
|
+
"Write",
|
|
185
|
+
{
|
|
186
|
+
"file_path": md_path,
|
|
187
|
+
"content": "```\nif x < 5 and y > 3:\n print('hello')\n```",
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
191
|
+
with open(html_path, encoding="utf-8") as f:
|
|
192
|
+
html = f.read()
|
|
193
|
+
assert "<" in html
|
|
194
|
+
assert "if x" in html
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_lists_are_wrapped_in_ul():
|
|
198
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
199
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
200
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
201
|
+
f.write("- item one\n- item two\n- item three")
|
|
202
|
+
|
|
203
|
+
_run_hook(
|
|
204
|
+
"Write",
|
|
205
|
+
{
|
|
206
|
+
"file_path": md_path,
|
|
207
|
+
"content": "- item one\n- item two\n- item three",
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
211
|
+
with open(html_path, encoding="utf-8") as f:
|
|
212
|
+
html = f.read()
|
|
213
|
+
assert "<ul>" in html
|
|
214
|
+
assert "</ul>" in html
|
|
215
|
+
assert html.index("<ul>") < html.index("<li>item one</li>")
|
|
216
|
+
assert html.index("</li>") < html.index("</ul>")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_ordered_lists_are_wrapped_in_ol():
|
|
220
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
221
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
222
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
223
|
+
f.write("1. first\n2. second")
|
|
224
|
+
|
|
225
|
+
_run_hook(
|
|
226
|
+
"Write",
|
|
227
|
+
{"file_path": md_path, "content": "1. first\n2. second"},
|
|
228
|
+
)
|
|
229
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
230
|
+
with open(html_path, encoding="utf-8") as f:
|
|
231
|
+
html = f.read()
|
|
232
|
+
assert "<ol>" in html
|
|
233
|
+
assert "</ol>" in html
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_handles_curly_braces_in_body():
|
|
237
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
238
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
239
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
240
|
+
f.write("# JS Example\n\nUse `{ foo: 1 }` in code.")
|
|
241
|
+
|
|
242
|
+
_run_hook(
|
|
243
|
+
"Write",
|
|
244
|
+
{
|
|
245
|
+
"file_path": md_path,
|
|
246
|
+
"content": "# JS Example\n\nUse `{ foo: 1 }` in code.",
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
250
|
+
with open(html_path, encoding="utf-8") as f:
|
|
251
|
+
html = f.read()
|
|
252
|
+
assert "{ foo: 1 }" in html
|
|
253
|
+
assert "{{" not in html
|
|
254
|
+
assert "JS Example" in html
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_escapes_title_in_html_output():
|
|
258
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
259
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
260
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
261
|
+
f.write("# Hackers <3 Markdown & <scripts>")
|
|
262
|
+
|
|
263
|
+
_run_hook(
|
|
264
|
+
"Write",
|
|
265
|
+
{
|
|
266
|
+
"file_path": md_path,
|
|
267
|
+
"content": "# Hackers <3 Markdown & <scripts>",
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
271
|
+
with open(html_path, encoding="utf-8") as f:
|
|
272
|
+
html = f.read()
|
|
273
|
+
assert "<title>Hackers <3 Markdown & <scripts></title>" in html
|
|
274
|
+
assert "<script>" not in html
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_skips_root_readme():
|
|
278
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
279
|
+
original_cwd = os.getcwd()
|
|
280
|
+
try:
|
|
281
|
+
os.chdir(tmp)
|
|
282
|
+
for each_name in ("README.md", "readme.md"):
|
|
283
|
+
with open(each_name, "w", encoding="utf-8") as f:
|
|
284
|
+
f.write("# Test")
|
|
285
|
+
result = _run_hook(
|
|
286
|
+
"Write", {"file_path": each_name, "content": "# Test"}
|
|
287
|
+
)
|
|
288
|
+
assert result.returncode == 0
|
|
289
|
+
expected_html = each_name.replace(".md", ".html")
|
|
290
|
+
assert not os.path.exists(expected_html)
|
|
291
|
+
finally:
|
|
292
|
+
os.chdir(original_cwd)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_skips_root_changelog():
|
|
296
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
297
|
+
original_cwd = os.getcwd()
|
|
298
|
+
try:
|
|
299
|
+
os.chdir(tmp)
|
|
300
|
+
for each_name in ("CHANGELOG.md", "changelog.md"):
|
|
301
|
+
with open(each_name, "w", encoding="utf-8") as f:
|
|
302
|
+
f.write("# Test")
|
|
303
|
+
result = _run_hook(
|
|
304
|
+
"Write", {"file_path": each_name, "content": "# Test"}
|
|
305
|
+
)
|
|
306
|
+
assert result.returncode == 0
|
|
307
|
+
expected_html = each_name.replace(".md", ".html")
|
|
308
|
+
assert not os.path.exists(expected_html)
|
|
309
|
+
finally:
|
|
310
|
+
os.chdir(original_cwd)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_language_class_valid():
|
|
314
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
315
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
316
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
317
|
+
f.write("```python\nx = 1\n```")
|
|
318
|
+
|
|
319
|
+
_run_hook("Write", {"file_path": md_path, "content": "```python\nx = 1\n```"})
|
|
320
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
321
|
+
with open(html_path, encoding="utf-8") as f:
|
|
322
|
+
html = f.read()
|
|
323
|
+
assert 'class="language-python"' in html
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_language_class_skips_invalid():
|
|
327
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
328
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
329
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
330
|
+
f.write("```my lang\nx = 1\n```")
|
|
331
|
+
|
|
332
|
+
_run_hook("Write", {"file_path": md_path, "content": "```my lang\nx = 1\n```"})
|
|
333
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
334
|
+
with open(html_path, encoding="utf-8") as f:
|
|
335
|
+
html = f.read()
|
|
336
|
+
assert "<pre><code>" in html
|
|
337
|
+
assert 'class="language-' not in html
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_language_class_allows_valid_chars():
|
|
341
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
342
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
343
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
344
|
+
f.write("```c++\nint x = 1;\n```")
|
|
345
|
+
|
|
346
|
+
_run_hook("Write", {"file_path": md_path, "content": "```c++\nint x = 1;\n```"})
|
|
347
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
348
|
+
with open(html_path, encoding="utf-8") as f:
|
|
349
|
+
html = f.read()
|
|
350
|
+
assert 'class="language-c++"' in html
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_link_text_asterisks_remain_literal():
|
|
354
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
355
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
356
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
357
|
+
f.write("See [text *not italic*](url).")
|
|
358
|
+
|
|
359
|
+
_run_hook(
|
|
360
|
+
"Write",
|
|
361
|
+
{"file_path": md_path, "content": "See [text *not italic*](url)."},
|
|
362
|
+
)
|
|
363
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
364
|
+
with open(html_path, encoding="utf-8") as f:
|
|
365
|
+
html = f.read()
|
|
366
|
+
assert '<a href="url">text *not italic*</a>' in html
|
|
367
|
+
assert "<em>" not in html
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_handles_parentheses_in_links():
|
|
371
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
372
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
373
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
374
|
+
f.write(
|
|
375
|
+
"See [Python]"
|
|
376
|
+
"(https://en.wikipedia.org/wiki/Python_(programming_language))."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
_run_hook(
|
|
380
|
+
"Write",
|
|
381
|
+
{
|
|
382
|
+
"file_path": md_path,
|
|
383
|
+
"content": "See [Python]"
|
|
384
|
+
"(https://en.wikipedia.org/wiki/Python_(programming_language)).",
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
388
|
+
with open(html_path, encoding="utf-8") as f:
|
|
389
|
+
html = f.read()
|
|
390
|
+
assert (
|
|
391
|
+
'href="https://en.wikipedia.org/wiki/Python_(programming_language)"'
|
|
392
|
+
in html
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_does_not_skip_nested_readme():
|
|
397
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
398
|
+
nested_dir = os.path.join(tmp, "docs")
|
|
399
|
+
os.makedirs(nested_dir)
|
|
400
|
+
md_path = os.path.join(nested_dir, "README.md")
|
|
401
|
+
html_path = os.path.join(nested_dir, "README.html")
|
|
402
|
+
|
|
403
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
404
|
+
f.write("# Nested README")
|
|
405
|
+
|
|
406
|
+
result = _run_hook(
|
|
407
|
+
"Write",
|
|
408
|
+
{"file_path": md_path, "content": "# Nested README"},
|
|
409
|
+
)
|
|
410
|
+
assert result.returncode == 0
|
|
411
|
+
assert os.path.exists(html_path)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_inline_code_preserves_asterisks():
|
|
415
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
416
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
417
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
418
|
+
f.write("Type `**bold**` in a docstring.")
|
|
419
|
+
|
|
420
|
+
_run_hook(
|
|
421
|
+
"Write",
|
|
422
|
+
{
|
|
423
|
+
"file_path": md_path,
|
|
424
|
+
"content": "Type `**bold**` in a docstring.",
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
428
|
+
with open(html_path, encoding="utf-8") as f:
|
|
429
|
+
html = f.read()
|
|
430
|
+
assert "<code>**bold**</code>" in html
|
|
431
|
+
assert "<strong>" not in html
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_blocks_javascript_url_scheme():
|
|
435
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
436
|
+
md_path = os.path.join(tmp, "guide.md")
|
|
437
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
438
|
+
f.write("[click me](javascript:alert(1))")
|
|
439
|
+
|
|
440
|
+
_run_hook(
|
|
441
|
+
"Write",
|
|
442
|
+
{
|
|
443
|
+
"file_path": md_path,
|
|
444
|
+
"content": "[click me](javascript:alert(1))",
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
html_path = os.path.join(tmp, "guide.html")
|
|
448
|
+
with open(html_path, encoding="utf-8") as f:
|
|
449
|
+
html = f.read()
|
|
450
|
+
assert "javascript:" not in html
|
|
451
|
+
assert "click me" in html
|
|
452
|
+
assert "<a" not in html
|
package/package.json
CHANGED
package/rules/gh-body-file.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# gh --body-file Rule
|
|
2
2
|
|
|
3
|
+
**MCP note:** MCP tools accept `body` as a structured string parameter and are unaffected by shell quoting. This rule applies to `gh` CLI invocations issued through the `Bash` tool.
|
|
4
|
+
|
|
3
5
|
**Root cause:** In shell-invoked `gh` command contexts used in this repo, passing markdown body text via `--body "..."` can cause backticks to be stored as `\`` literals on GitHub instead of rendering as markdown code formatting. Quoting and escaping rules vary by execution environment (Bash, PowerShell, CMD), but the failure mode is the same: inline code and code fences can be broken in issues, PR descriptions, comments, and reviews written this way.
|
|
4
6
|
|
|
5
7
|
**Rule:** All `gh` commands that include markdown body content **must** use `--body-file <path>` with a temp file. Never pass body text as a string argument to `--body` or its shorthand `-b`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Install or remove the scheduled task that sweeps empty directories.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Registers a scheduled task that runs sweep_empty_dirs.py --once every N minutes
|
|
8
|
+
against a target directory. Defaults: every 5 minutes, age threshold 120 seconds.
|
|
9
|
+
|
|
10
|
+
Install-SweepEmptyDirs.ps1 -Target "C:\path\to\watch"
|
|
11
|
+
Install-SweepEmptyDirs.ps1 -Target "C:\path\to\watch" -IntervalMinutes 10 -AgeSeconds 300 # custom
|
|
12
|
+
Install-SweepEmptyDirs.ps1 -Remove
|
|
13
|
+
Install-SweepEmptyDirs.ps1 -Status
|
|
14
|
+
#>
|
|
15
|
+
|
|
16
|
+
param(
|
|
17
|
+
[Parameter(ParameterSetName = "install")]
|
|
18
|
+
[string]$Target,
|
|
19
|
+
|
|
20
|
+
[Parameter(ParameterSetName = "install")]
|
|
21
|
+
[ValidateRange(1, [int]::MaxValue)]
|
|
22
|
+
[int]$IntervalMinutes = 5,
|
|
23
|
+
|
|
24
|
+
[Parameter(ParameterSetName = "install")]
|
|
25
|
+
[ValidateRange(1, [int]::MaxValue)]
|
|
26
|
+
[int]$AgeSeconds = 120,
|
|
27
|
+
|
|
28
|
+
[Parameter(ParameterSetName = "install")]
|
|
29
|
+
[DateTime]$StartAt = (Get-Date),
|
|
30
|
+
|
|
31
|
+
[Parameter(ParameterSetName = "remove")]
|
|
32
|
+
[switch]$Remove,
|
|
33
|
+
|
|
34
|
+
[Parameter(ParameterSetName = "status")]
|
|
35
|
+
[switch]$Status
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
$TaskName = "SweepEmptyDirs"
|
|
39
|
+
|
|
40
|
+
if ($Status) {
|
|
41
|
+
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
42
|
+
if (-not $task) {
|
|
43
|
+
Write-Host "STATUS: $TaskName is not registered."
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
Write-Host "STATUS: $TaskName is registered."
|
|
47
|
+
Write-Host " State: $($task.State)"
|
|
48
|
+
Write-Host " Actions:"
|
|
49
|
+
foreach ($each_action in $task.Actions) {
|
|
50
|
+
Write-Host " $($each_action.Execute) $($each_action.Arguments)"
|
|
51
|
+
}
|
|
52
|
+
Write-Host " Triggers:"
|
|
53
|
+
foreach ($each_trigger in $task.Triggers) {
|
|
54
|
+
Write-Host " $($each_trigger.Repetition.Interval) (starting $($each_trigger.StartBoundary))"
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ($Remove) {
|
|
60
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
61
|
+
if (-not $?) {
|
|
62
|
+
Write-Warning "Failed to unregister scheduled task '$TaskName'."
|
|
63
|
+
} else {
|
|
64
|
+
Write-Host "$TaskName removed."
|
|
65
|
+
}
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$ScriptDir = Split-Path -Parent $PSCommandPath
|
|
70
|
+
$ScriptPath = Join-Path $ScriptDir "sweep_empty_dirs.py"
|
|
71
|
+
|
|
72
|
+
if (-not (Test-Path $ScriptPath)) {
|
|
73
|
+
Write-Error "sweep_empty_dirs.py not found at: $ScriptPath"
|
|
74
|
+
exit 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (-not $Target) {
|
|
78
|
+
Write-Error "Parameter -Target is required (the directory to watch)."
|
|
79
|
+
exit 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (-not (Test-Path -PathType Container $Target)) {
|
|
83
|
+
Write-Error "Target directory does not exist: $Target"
|
|
84
|
+
exit 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$_py = Get-Command py -ErrorAction SilentlyContinue
|
|
88
|
+
$PythonPath = if ($_py) { $_py.Source } else { (Get-Command python -ErrorAction SilentlyContinue).Source }
|
|
89
|
+
if (-not $PythonPath) {
|
|
90
|
+
Write-Error "Cannot find Python (py or python) on PATH."
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
& $PythonPath --version 2>$null
|
|
94
|
+
if (-not $?) {
|
|
95
|
+
Write-Error "Python found at $PythonPath but failed to run."
|
|
96
|
+
exit 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$Target = (Resolve-Path $Target).Path
|
|
100
|
+
$Target = [System.IO.Path]::TrimEndingDirectorySeparator($Target)
|
|
101
|
+
|
|
102
|
+
$Action = New-ScheduledTaskAction -Execute $PythonPath -Argument """$ScriptPath"" --once --age $AgeSeconds ""$Target"""
|
|
103
|
+
$Trigger = New-ScheduledTaskTrigger -Once -At $StartAt -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 31)
|
|
104
|
+
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
105
|
+
|
|
106
|
+
$null = Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force
|
|
107
|
+
if (-not $?) {
|
|
108
|
+
Write-Error "Failed to register scheduled task."
|
|
109
|
+
exit 1
|
|
110
|
+
}
|
|
111
|
+
Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$Target' (age > ${AgeSeconds}s)."
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
One-shot quality gate for the hooks package — runs ruff, mypy, and the
|
|
5
|
+
blocking pytest suite from a single entry point.
|
|
6
|
+
|
|
7
|
+
.DESCRIPTION
|
|
8
|
+
Resolves paths relative to $PSScriptRoot so the script works from any CWD
|
|
9
|
+
and from both the worktree (packages/claude-dev-env/scripts/check.ps1)
|
|
10
|
+
and the installed runtime (~/.claude/scripts/check.ps1, after install.mjs
|
|
11
|
+
propagates this file). Each tool runs sequentially; the first non-zero
|
|
12
|
+
exit code is preserved as the script's exit code so CI/pre-commit can
|
|
13
|
+
short-circuit on the first failure.
|
|
14
|
+
|
|
15
|
+
.PARAMETER SkipTests
|
|
16
|
+
Skip the pytest run. Useful during local iteration when you want only the
|
|
17
|
+
static-analysis gates.
|
|
18
|
+
|
|
19
|
+
.PARAMETER SkipMypy
|
|
20
|
+
Skip the mypy run.
|
|
21
|
+
|
|
22
|
+
.PARAMETER SkipRuff
|
|
23
|
+
Skip the ruff run.
|
|
24
|
+
|
|
25
|
+
.OUTPUTS
|
|
26
|
+
Per-tool status lines on stdout. Final summary line:
|
|
27
|
+
CHECK: OK
|
|
28
|
+
CHECK: FAILED tools=ruff,mypy,pytest
|
|
29
|
+
#>
|
|
30
|
+
[CmdletBinding()]
|
|
31
|
+
param(
|
|
32
|
+
[switch]$SkipTests,
|
|
33
|
+
[switch]$SkipMypy,
|
|
34
|
+
[switch]$SkipRuff
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = 'Stop'
|
|
38
|
+
|
|
39
|
+
$hooksRoot = Resolve-Path (Join-Path $PSScriptRoot '..' 'hooks')
|
|
40
|
+
$blockingRoot = Join-Path $hooksRoot 'blocking'
|
|
41
|
+
|
|
42
|
+
$failedTools = @()
|
|
43
|
+
$firstNonZeroExitCode = 0
|
|
44
|
+
|
|
45
|
+
function Invoke-Tool {
|
|
46
|
+
param(
|
|
47
|
+
[string]$Label,
|
|
48
|
+
[scriptblock]$Action
|
|
49
|
+
)
|
|
50
|
+
Write-Host ""
|
|
51
|
+
Write-Host "==> $Label" -ForegroundColor Cyan
|
|
52
|
+
& $Action
|
|
53
|
+
$exitCode = $LASTEXITCODE
|
|
54
|
+
if ($exitCode -ne 0) {
|
|
55
|
+
$script:failedTools += $Label
|
|
56
|
+
if ($script:firstNonZeroExitCode -eq 0) {
|
|
57
|
+
$script:firstNonZeroExitCode = $exitCode
|
|
58
|
+
}
|
|
59
|
+
Write-Host "$Label FAILED (exit $exitCode)" -ForegroundColor Red
|
|
60
|
+
} else {
|
|
61
|
+
Write-Host "$Label OK" -ForegroundColor Green
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (-not $SkipRuff) {
|
|
66
|
+
Invoke-Tool -Label 'ruff' -Action {
|
|
67
|
+
Push-Location $hooksRoot
|
|
68
|
+
try {
|
|
69
|
+
ruff check .
|
|
70
|
+
} finally {
|
|
71
|
+
Pop-Location
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (-not $SkipMypy) {
|
|
77
|
+
Invoke-Tool -Label 'mypy' -Action {
|
|
78
|
+
Push-Location $hooksRoot
|
|
79
|
+
try {
|
|
80
|
+
mypy --config-file (Join-Path $hooksRoot 'pyproject.toml') blocking validators
|
|
81
|
+
} finally {
|
|
82
|
+
Pop-Location
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (-not $SkipTests) {
|
|
88
|
+
Invoke-Tool -Label 'pytest' -Action {
|
|
89
|
+
Push-Location $blockingRoot
|
|
90
|
+
try {
|
|
91
|
+
python -m pytest (Get-ChildItem test_code_rules_enforcer*.py)
|
|
92
|
+
} finally {
|
|
93
|
+
Pop-Location
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Write-Host ""
|
|
99
|
+
if ($failedTools.Count -eq 0) {
|
|
100
|
+
Write-Host "CHECK: OK" -ForegroundColor Green
|
|
101
|
+
exit 0
|
|
102
|
+
} else {
|
|
103
|
+
$joined = ($failedTools -join ',')
|
|
104
|
+
Write-Host "CHECK: FAILED tools=$joined" -ForegroundColor Red
|
|
105
|
+
exit $firstNonZeroExitCode
|
|
106
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Timing constants for sweep_empty_dirs.
|
|
2
|
+
|
|
3
|
+
Per the project's configuration conventions, timeouts, delays, and retries
|
|
4
|
+
live in config/timing.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
DEFAULT_AGE_SECONDS: int = 120
|
|
8
|
+
"""Minimum age before an empty directory is eligible for deletion."""
|
|
9
|
+
|
|
10
|
+
DEFAULT_POLL_INTERVAL: int = 30
|
|
11
|
+
"""Seconds between sweep passes in continuous-watch mode."""
|