-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathhumanize.py
More file actions
212 lines (177 loc) · 9.95 KB
/
Copy pathhumanize.py
File metadata and controls
212 lines (177 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env python3
"""Block AI buzzwords before a write, shell, or tool call lands, so text reads human.
Scans markdown whole, code files only in comments and docstrings, shell only in commit and PR text
plus heredoc bodies that cat or tee writes to a file, patches only in added lines, and MCP calls
only in message fields. Always blocks a few marks and stock words with a plain swap, and flags
common words only when they pile up. En-dash is allowed.
Words draw on Wikipedia "Signs of AI writing": https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing
"""
import json
import re
import shlex
import sys
from collections import Counter
from pathlib import Path
# TODO: build a companion guidance skill from that page's pitfalls, not only its word list
# always blocked on any hit, each mapped to a plain swap or a short "drop it" note
SWAP = {
"leverage": "use", "utilize": "use", "plethora": "many", "myriad": "many",
"delve": "look at", "paradigm": "model", "tapestry": "mix", "showcase": "show",
"prose": "text", "realm": "area", "landscape": "field", "innovative": "new",
"transformative": "major", "unprecedented": "new", "consolidate": "merge",
"modernize": "update", "streamline": "simplify", "flexible": "adjustable",
"establish": "set up", "enhanced": "better", "comprehensive": "full", "optimize": "improve",
"unequivocally": "clearly", "symphony": "mix", "delicate": "fragile", "begrudgingly": "reluctantly",
"merit": "worth", "albeit": "though", "reverent": "respectful", "revolutionizing": "changing",
"revolutionize": "change", "crucially": "drop it", "remarkably": "drop it", "seamlessly": "drop it",
"manifestation": "sign", "testament": "sign", "prominent": "clear", "underscoring": "showing",
"symbolizing": "showing", "cultivating": "building", "fostering": "building", "encompassing": "covering",
"facilitating": "helping", "emphasizing": "showing", "embodying": "showing", "underlies": "drives",
"evoke": "stir", "enduring": "lasting", "nestled": "in", "fascinating": "notable",
"vibrant": "lively", "game-changing": "big", "cutting-edge": "latest",
}
# always blocked cliches and filler openers, each mapped to a short fix note
PHRASES = [
(r"ever[- ]evolving", "drop 'ever-evolving'"),
(r"fast[- ]paced world", "drop 'fast-paced world'"),
(r"a testament to", "say what it shows"),
(r"vibrant tapestry", "drop the cliche"),
(r"aims to explore", "say 'covers'"),
(r"aims to bridge", "say what it connects"),
(r"foster innovation", "say what gets built"),
(r"measured steps", "drop the cliche"),
(r"practiced efficiency", "drop the cliche"),
(r"stark reminder", "drop the cliche"),
(r"it is important to note", "state the point"),
(r"as an ai language model", "drop it"),
(r"in conclusion", "drop it"),
(r"in summary", "drop it"),
(r"to sum up", "drop it"),
(r"plays? a \w+ role in shaping", "say what it does"),
]
# fine once, suspicious when repeated, flagged at LIMIT or more
# 'prompted' sits here on purpose, LLM comments and docstrings use it a lot
LIMIT = 3
OFTEN = ["crucial", "essential", "vital", "significant", "moreover", "furthermore", "additionally", "aligns", "explore", "prompted"]
MARKS = {"—": "em-dash, use commas or periods", "§": "section sign", ";": "semicolon, use a period or comma"}
SWAP_RE = re.compile(r"\b(" + "|".join(SWAP) + r")\b", re.IGNORECASE)
OFTEN_RE = re.compile(r"\b(" + "|".join(OFTEN) + r")\b", re.IGNORECASE)
# groups are the delimiter, the rest of the opening line, and the body
HEREDOC = re.compile(r"<<-?[ \t]*[\"']?([A-Za-z_]\w*)[\"']?([^\n]*)\r?\n(.*?)\r?\n[ \t]*\1[ \t]*$", re.DOTALL | re.MULTILINE)
FIELD_FLAGS = {"-f", "-F", "--field", "--raw-field"}
# only cat and tee put a heredoc body in a file unchanged, python or sed in front of it rewrites the text
SEPARATOR = re.compile(r"\|\||&&|[\n;|&]")
CAT_TEE = re.compile(r"^\s*(cat|tee)\b(.*)$", re.DOTALL)
REDIRECT = re.compile(r"(?<![0-9&])>>?[ \t]*(\"[^\"]*\"|'[^']*'|[^\s'\"|&;<>]+)")
# MCP input keys that carry human-facing message text, checked as markdown
TEXT_KEYS = {"body", "text", "markdown_text", "content", "description", "title",
"comment", "message", "subject", "note", "summary", "richtext", "rich_text"}
MD_EXT = {".md", ".markdown", ".mdx", ".txt"}
HASH_EXT = {".py", ".sh", ".bash", ".zsh", ".rb", ".yaml", ".yml", ".toml"}
C_EXT = {".js", ".ts", ".jsx", ".tsx", ".c", ".cc", ".cpp", ".h", ".hpp", ".java", ".go", ".rs", ".css", ".scss", ".swift", ".kt", ".php"}
def md_text(text):
"""Return markdown with fenced and inline code removed."""
text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
return re.sub(r"`[^`]*`", "", text)
def hash_comments(text):
"""Return line-start docstrings and hash comment tails from a hash-comment language."""
out = re.findall(r'^[ \t]*[rbuRBU]*"""(.*?)"""', text, flags=re.DOTALL | re.MULTILINE)
out += re.findall(r"^[ \t]*[rbuRBU]*'''(.*?)'''", text, flags=re.DOTALL | re.MULTILINE)
out += [m.group(1) for line in text.splitlines() if (m := re.search(r"(?:^|\s)#(.*)", line))]
return "\n".join(out)
def c_comments(text):
"""Return block comments and double-slash comment tails from a C-style language."""
out = re.findall(r"/\*(.*?)\*/", text, flags=re.DOTALL)
out += [m.group(1) for line in text.splitlines() if (m := re.search(r"(?:^|\s)//(.*)", line))]
return "\n".join(out)
def checked(path, text):
"""Return the text regions to check for the file type, empty for unknown types."""
ext = Path(path).suffix.lower()
if ext in MD_EXT:
return md_text(text)
if ext in HASH_EXT:
return hash_comments(text)
if ext in C_EXT:
return c_comments(text)
return ""
def heredocs(command):
"""Yield each heredoc body paired with the simple command that opens it."""
for m in HEREDOC.finditer(command):
yield SEPARATOR.split(command[: m.start()])[-1] + SEPARATOR.split(m.group(2))[0], m.group(3)
def heredoc_writes(command):
"""Return checkable text from heredoc bodies that cat or tee writes to a file, routed per target."""
out = []
for head, body in heredocs(command):
if not (writer := CAT_TEE.match(head)):
continue
targets = REDIRECT.findall(head)
if writer.group(1) == "tee":
targets += [a for a in REDIRECT.sub(" ", writer.group(2)).split() if not a.startswith("-")]
out += [checked(t.strip("\"'"), body) for t in targets]
return "\n".join(p for p in out if p)
def bash_text(command):
"""Return commit, PR, and comment message text from a git-commit or gh command, else empty."""
git_commit = re.search(r"\bgit\b[^|&]*\bcommit\b", command)
gh = re.search(r"\bgh\b", command)
if not (git_commit or gh):
return ""
parts = [body for head, body in heredocs(command) if re.search(r"\b(?:git|gh)\b", head)]
stripped = HEREDOC.sub(" ", command)
flags = {"-m", "--message"} if git_commit else set()
if gh:
flags |= {"-b", "--body", "-t", "--title"}
try:
tokens = shlex.split(stripped, comments=False)
except ValueError:
tokens = stripped.split()
for i, tok in enumerate(tokens):
key, sep, val = tok.partition("=")
if tok in flags and i + 1 < len(tokens):
parts.append(tokens[i + 1])
elif sep and key in flags:
parts.append(val)
elif gh and tok in FIELD_FLAGS and i + 1 < len(tokens):
field, _, value = tokens[i + 1].partition("=")
if field in {"body", "title"} and not value.startswith("@"):
parts.append(value)
return "\n".join(parts)
def patch_text(patch):
"""Return checkable text from a Codex apply_patch envelope, split by file type."""
out = []
for path, block in re.findall(r"^\*\*\* (?:Add|Update) File: (.+?)\s*$(.*?)(?=^\*\*\* |\Z)", patch, re.DOTALL | re.MULTILINE):
added = "\n".join(ln[1:] for ln in block.splitlines() if ln.startswith("+") and not ln.startswith("+++"))
out.append(checked(path, added))
return "\n".join(p for p in out if p)
def mcp_text(obj):
"""Return human-facing field values pulled from an MCP tool input, walked in full."""
if isinstance(obj, dict):
items = [v if k.lower() in TEXT_KEYS and isinstance(v, str) else mcp_text(v) for k, v in obj.items()]
elif isinstance(obj, list):
items = [mcp_text(v) for v in obj]
else:
return ""
return "\n".join(p for p in items if p)
def extract(tool, tool_input):
"""Return the checkable text for a tool call, routed by tool type."""
command = tool_input.get("command", "")
if isinstance(command, list): # Codex sends the shell tool an argv array, Claude Code a string
command = " ".join(str(c) for c in command)
if tool.startswith("mcp__"):
return md_text(mcp_text(tool_input))
if tool == "Bash":
return "\n".join(p for p in (md_text(bash_text(command)), heredoc_writes(command)) if p)
if tool == "apply_patch":
return patch_text(command)
chunks = [tool_input.get("content", ""), tool_input.get("new_string", "")]
chunks += [e.get("new_string", "") for e in tool_input.get("edits", []) if isinstance(e, dict)]
return checked(tool_input.get("file_path", ""), "\n".join(c for c in chunks if c))
data = json.load(sys.stdin)
text = extract(data.get("tool_name", ""), data.get("tool_input") or {})
notes = [f"remove {label}" for ch, label in MARKS.items() if ch in text]
notes += [f"'{w}' -> {SWAP[w]}" for w in dict.fromkeys(m.group(1).lower() for m in SWAP_RE.finditer(text))]
notes += [note for pat, note in PHRASES if re.search(pat, text, re.IGNORECASE)]
counts = Counter(m.group(1).lower() for m in OFTEN_RE.finditer(text))
notes += [f"'{w}' used {n} times, vary it" for w, n in counts.items() if n >= LIMIT]
if notes:
reason = "humanize: " + ", ".join(notes) + ". Applies to markdown, code comments, and message text."
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": reason}}))