Skip to content

Commit 11cec34

Browse files
authored
Merge pull request #101 from Miyamura80/ai-writing-guard
Add wait skill and AI writing guard
2 parents 5bb5ff0 + c23f905 commit 11cec34

5 files changed

Lines changed: 135 additions & 0 deletions

File tree

.claude/skills/wait/SKILL.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
name: wait
3+
description: Pause execution for a requested number of minutes by sleeping one-minute increments to avoid exceeding shell timeouts.
4+
user_invocable: true
5+
triggers:
6+
- /wait
7+
- wait
8+
---
9+
10+
# Wait Skill
11+
12+
Use this skill whenever the user asks the assistant to pause or wait for a few minutes during a task. Instead of issuing a single long `sleep` command (which often hits the 2-minute shell timeout), run one-minute sleeps repeatedly for the requested duration.
13+
14+
## Workflow
15+
16+
1. **Determine the wait time.** Parse the user’s request for a duration expressed in minutes. If the request is vague, ask a clarifying question (e.g., “How many minutes should I wait?”) before running commands.
17+
2. **Enforce sane limits.** If the user requests a very large number of minutes, warn them and offer to break the wait into smaller chunks or confirm before proceeding.
18+
3. **Execute sequential Bash sleeps.** For each of the requested N minutes, issue a separate `bash` tool call with `sleep 60`. Before each call, report the upcoming iteration as `executing sleep: i/N: bash sleep 60` so observers know how many sleeps will run. Avoid bundling the sleeps into a single script; the goal is to keep every sleeping command under the 2-minute timeout.
19+
20+
4. **Report completion.** Once the loop finishes, notify the user that the wait is over and resume the primary task.
21+
22+
## Error handling
23+
24+
- If the shell command fails (e.g., `sleep` unavailable), report the failure and stop waiting.
25+
- If the user changes their mind mid-wait, cancel the remaining iterations and explain how much time was actually spent waiting.

.github/workflows/linter_require_ruff.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ jobs:
3434
run: make tech_debt
3535
- name: Run duplicate code check
3636
run: make duplicate_code
37+
- name: Run AI writing check
38+
run: uv run python scripts/check_ai_writing.py
3739
- name: Run import-linter
3840
run: make import_lint

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ repos:
4141
language: system
4242
pass_filenames: false
4343
always_run: true
44+
- id: ai-writing-check
45+
name: AI writing check
46+
entry: uv run python scripts/check_ai_writing.py
47+
language: system
48+
pass_filenames: false
49+
always_run: true

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Structure as: `init()` → `continue(id)` → `cleanup(id)`
137137
## Git Workflow
138138
- **Protected Branch**: `main` is protected. Do not push directly to `main`. Use PRs.
139139
- **Merge Strategy**: Squash and merge.
140+
- **Never force push**: Do not use `git push --force` or `--force-with-lease`. If you hit a git issue, stop and ask the user for guidance.
140141

141142
## Deprecated
142143

scripts/check_ai_writing.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import annotations
2+
3+
import pathlib
4+
from collections.abc import Iterable, Sequence
5+
6+
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
7+
EM_DASH = chr(0x2014)
8+
ROOT_SKIP_DIRS = {
9+
".git",
10+
".venv",
11+
".uv_cache",
12+
".uv-cache",
13+
".uv_tools",
14+
".uv-tools",
15+
".cache",
16+
"node_modules",
17+
".next",
18+
}
19+
RECURSIVE_SKIP_DIRS = {"__pycache__", ".pytest_cache"}
20+
SKIP_PATH_PREFIXES = {
21+
("docs", ".next"),
22+
("docs", "node_modules"),
23+
}
24+
SKIP_SUFFIXES = {
25+
".png",
26+
".jpg",
27+
".jpeg",
28+
".gif",
29+
".webp",
30+
".ico",
31+
".mp4",
32+
".mov",
33+
".mp3",
34+
".woff",
35+
".woff2",
36+
".ttf",
37+
".otf",
38+
".eot",
39+
".pdf",
40+
".zip",
41+
".tar",
42+
".gz",
43+
".bz2",
44+
".7z",
45+
".ckpt",
46+
".bin",
47+
".pyc",
48+
".pyo",
49+
".db",
50+
}
51+
52+
53+
def iter_text_files(root: pathlib.Path) -> Iterable[pathlib.Path]:
54+
for path in root.rglob("*"):
55+
if not path.is_file():
56+
continue
57+
rel = path.relative_to(root)
58+
rel_parts = rel.parts
59+
if rel_parts and rel_parts[0] in ROOT_SKIP_DIRS:
60+
continue
61+
if any(rel_parts[: len(prefix)] == prefix for prefix in SKIP_PATH_PREFIXES):
62+
continue
63+
dir_parts = rel_parts[:-1]
64+
if any(part in RECURSIVE_SKIP_DIRS for part in dir_parts):
65+
continue
66+
if path.suffix.lower() in SKIP_SUFFIXES:
67+
continue
68+
yield path
69+
70+
71+
def find_em_dashes(path: pathlib.Path) -> Sequence[tuple[int, str]]:
72+
try:
73+
text = path.read_text(encoding="utf-8", errors="ignore")
74+
except OSError:
75+
return []
76+
lines: list[tuple[int, str]] = []
77+
for lineno, line in enumerate(text.splitlines(), start=1):
78+
if EM_DASH in line:
79+
lines.append((lineno, line))
80+
return lines
81+
82+
83+
def main() -> int:
84+
violations: list[tuple[pathlib.Path, int, str]] = []
85+
for path in iter_text_files(REPO_ROOT):
86+
for lineno, line in find_em_dashes(path):
87+
violations.append((path.relative_to(REPO_ROOT), lineno, line.strip()))
88+
if violations:
89+
print(
90+
f"AI writing check failed: {EM_DASH!r} (em dash) detected in the repository"
91+
)
92+
for rel_path, lineno, snippet in violations:
93+
print(f"{rel_path}:{lineno}: {snippet}")
94+
print("Please remove the em dash or explain why it is acceptable.")
95+
return 1
96+
print("AI writing check passed (no em dash found).")
97+
return 0
98+
99+
100+
if __name__ == "__main__":
101+
raise SystemExit(main())

0 commit comments

Comments
 (0)