Skip to content

Commit e0c9840

Browse files
committed
🔨 add wait skill and ai writing guard
1 parent 79768ef commit e0c9840

5 files changed

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

0 commit comments

Comments
 (0)