diff --git a/.claude/skills/wait/SKILL.md b/.claude/skills/wait/SKILL.md new file mode 100644 index 0000000..09cd906 --- /dev/null +++ b/.claude/skills/wait/SKILL.md @@ -0,0 +1,25 @@ +--- +name: wait +description: Pause execution for a requested number of minutes by sleeping one-minute increments to avoid exceeding shell timeouts. +user_invocable: true +triggers: + - /wait + - wait +--- + +# Wait Skill + +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. + +## Workflow + +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. +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. +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. + +4. **Report completion.** Once the loop finishes, notify the user that the wait is over and resume the primary task. + +## Error handling + +- If the shell command fails (e.g., `sleep` unavailable), report the failure and stop waiting. +- If the user changes their mind mid-wait, cancel the remaining iterations and explain how much time was actually spent waiting. diff --git a/.github/workflows/linter_require_ruff.yaml b/.github/workflows/linter_require_ruff.yaml index 5e6b4a4..3d75a39 100644 --- a/.github/workflows/linter_require_ruff.yaml +++ b/.github/workflows/linter_require_ruff.yaml @@ -34,5 +34,7 @@ jobs: run: make tech_debt - name: Run duplicate code check run: make duplicate_code + - name: Run AI writing check + run: uv run python scripts/check_ai_writing.py - name: Run import-linter run: make import_lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a4de1a..86c8cbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,3 +41,9 @@ repos: language: system pass_filenames: false always_run: true + - id: ai-writing-check + name: AI writing check + entry: uv run python scripts/check_ai_writing.py + language: system + pass_filenames: false + always_run: true diff --git a/CLAUDE.md b/CLAUDE.md index b814590..b7aa3a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,7 @@ Structure as: `init()` → `continue(id)` → `cleanup(id)` ## Git Workflow - **Protected Branch**: `main` is protected. Do not push directly to `main`. Use PRs. - **Merge Strategy**: Squash and merge. +- **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. ## Deprecated diff --git a/scripts/check_ai_writing.py b/scripts/check_ai_writing.py new file mode 100644 index 0000000..184e5b1 --- /dev/null +++ b/scripts/check_ai_writing.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import pathlib +from collections.abc import Iterable, Sequence + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +EM_DASH = chr(0x2014) +SKIP_DIRS = { + ".git", + ".venv", + ".uv_cache", + ".uv-cache", + ".uv_tools", + ".uv-tools", + ".cache", + ".pytest_cache", + "__pycache__", + "node_modules", + ".next", +} +SKIP_SUFFIXES = { + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".ico", + ".mp4", + ".mov", + ".mp3", + ".woff", + ".woff2", + ".ttf", + ".otf", + ".eot", + ".pdf", + ".zip", + ".tar", + ".gz", + ".bz2", + ".7z", + ".ckpt", + ".bin", + ".pyc", + ".pyo", + ".db", +} + + +def iter_text_files(root: pathlib.Path) -> Iterable[pathlib.Path]: + for path in root.rglob("*"): + if not path.is_file(): + continue + rel = path.relative_to(root) + if any(part in SKIP_DIRS for part in rel.parts): + continue + if path.suffix.lower() in SKIP_SUFFIXES: + continue + yield path + + +def find_em_dashes(path: pathlib.Path) -> Sequence[tuple[int, str]]: + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return [] + lines: list[tuple[int, str]] = [] + for lineno, line in enumerate(text.splitlines(), start=1): + if EM_DASH in line: + lines.append((lineno, line)) + return lines + + +def main() -> int: + violations: list[tuple[pathlib.Path, int, str]] = [] + for path in iter_text_files(REPO_ROOT): + for lineno, line in find_em_dashes(path): + violations.append((path.relative_to(REPO_ROOT), lineno, line.strip())) + if violations: + print( + f"AI writing check failed: {EM_DASH!r} (em dash) detected in the repository" + ) + for rel_path, lineno, snippet in violations: + print(f"{rel_path}:{lineno}: {snippet}") + print("Please remove the em dash or explain why it is acceptable.") + return 1 + print("AI writing check passed (no em dash found).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())