---
name: typo-sweep
description: |
  Walk a folder of git repos and open one typo-fix PR per repo.
  Per repo: fetch trunk, scan docs and comments only, fix up to ~50 substitutions
  in atomic per-file commits, push, open PR, monitor CI, chain fix-pr-checks on Danger failures.
  Use when asked to "sweep typos", "fix typos across repos", or similar.
user-invocable: true
allowed-tools: Bash, Edit, Read, Write, Glob, Grep, Skill, AskUserQuestion, Task
---

# Typo Sweep

Walk a folder of git repos and open one mechanical typo-fix PR per repo.

## Arguments

`$ARGUMENTS` — absolute path to a folder containing repos.
If empty, ask the user via `AskUserQuestion`.

## Sidecar files (next to this `SKILL.md`)

- `typos.txt` — curated misspelling → correction list.
- `scan.sh` — ripgrep wrapper that scans a directory using `typos.txt`.
- `apply-fix.py` — word-boundary substitution with two guards:
  - **Content match (drift-safe):** pass the scan's matched line text as a 5th arg and the fix lands on the line with that content wherever it moved, skipping (loudly) a line that changed or was already fixed upstream — never guessing.
  - **Single-quote guard:** refuses an apostrophe correction (`don't`, `today's`, …) that would land inside a single-quoted string literal (`it '...'`, `it('...')`) and break it. Comments and double-quoted strings proceed.
  - Still refuses when the word isn't present, so misalignments fail loudly.
  - `test_apply_fix.py` pins these behaviors: `python3 test_apply_fix.py`.

Reference them via `$(dirname "$0")` inside scripts, or via the absolute paths under `~/.claude/skills/typo-sweep/` (or `~/.dotfiles/agents/skills/typo-sweep/`).

## Workflow

### 0. Resolve the target folder

If `$ARGUMENTS` names an existing directory, use it.
Otherwise ask the user with `AskUserQuestion` — don't guess.

### 1. Enumerate repos

For each subdirectory of the target, check `git -C <dir> rev-parse --is-inside-work-tree`.
Skip non-repos.
Skip worktree containers (`.git-worktrees/`, `*-worktrees`).

Present the list back to the user briefly, then process sequentially.
**One repo at a time — no parallelism.**

### 2. Per-repo loop

For each repo:

#### a. Fetch the default branch

```
git -C <repo> fetch origin
default=$(git -C <repo> remote show origin | awk '/HEAD branch/ {print $NF}')
```

If the fetch fails (VPN-only or otherwise unreachable remote), skip and report.

#### b. Create a worktree

Path: `~/Developer/git-worktrees/<repo-basename>/typo-fixes`.
Branch: `mokagio/typo-fixes` (the user's prefix for shared repos; see `~/.config/agents/rules/git.md`).

```
git -C <repo> worktree add ~/Developer/git-worktrees/<basename>/typo-fixes -b mokagio/typo-fixes origin/<default>
```

If the branch already exists locally (prior sweep mid-flight), surface and ask the user before clobbering.

#### c. Scan

```
$SKILL_DIR/scan.sh <worktree> /tmp/typo-sweep/<basename>.txt
```

Output is `path:line:col:matched line` per finding.

#### d. Curate findings

**Drop** (do not fix):

- **Vendored / third-party code** — `Pods/`, `node_modules/`, `vendor/`, `Carthage/`, `External/`, `ThirdParty/`, `third_party/`, `third-party/`. The scanner already excludes most of these, but watch for repo-specific vendored dirs.
- **Generated files** — anything whose header says "Generated by", "DO NOT EDIT", "Autogenerated".
  Notable case: `*+Generated.swift` from SwiftGen (scanner excludes by glob).
  If the typo lives in an `.strings` file *comment* (`/* ... */`), that's the source of truth — fix it there.
- **Code identifiers** — function/class/variable/property names, enum cases, constant *values*, struct fields, JSON/dict keys, parameter labels, IBOutlet names.
  Rule of thumb: if the match is on a line that doesn't start with a comment marker and isn't inside a string/comment, skip it. When in doubt, `Read` the surrounding lines.
- **User-facing localization VALUES** — the english string in `NSLocalizedString("text", ...)`, Android `<string>` resources, `formatMessage('key')` translation keys.
  Only fix the *comment* / translator-hint argument (`NSLocalizedString("...", comment: "fix typos HERE")`).
- **URL slugs, settings keys, translation keys** — anything kebab-cased that looks like a stable identifier (`why-doesnt-npm-upgrade-install`, `todays-stats-card-enabled-site-settings`, `'dont-ask-again'`).
- **Foreign-language metadata** — `fastlane/metadata/<locale>/*` and `fastlane/*_metadata/<locale>/*` for non-English locales (scanner excludes by glob, but verify).

**Fix** (target):

- `.md`, `.txt`, `.rst` files (docs, READMEs).
- Source-code comments (`//`, `/* */`, `#`, `///`, `*` in JavaDoc, etc.).
- Test description strings — `expectation(description: "...")`, `XCTFail("msg")`, `XCTAssert(..., "msg")`, backticked Kotlin test function names.
- Translator-hint comments — the `comment:` arg of `NSLocalizedString`.
- Comments inside `.strings` files (`/* ... */` — these annotate strings for SwiftGen and translators).

**Watch out for context-sensitive corrections**:

- `happend` → "happen" or "happened" depending on tense. "Should never happend" wants `happen`, not `happened`.
- `zeroes` is a valid English plural and a verb — leave it alone.
- `doesnt` in a Kotlin backticked test name fixes to `doesn't` (apostrophe inside backticks is fine), but verify the rename doesn't break a golden filename or CI test filter.
- **Apostrophe into a single-quoted string breaks code.** `dont`/`doesnt`/`todays` → `don't`/`doesn't`/`today's` inside a single-quoted Ruby/JS string (RSpec `it '...'`, JS `it('...')`) terminates the string. `apply-fix.py` refuses these, but prefer not to queue them. Comments and double-quoted strings are safe.
- **Lint runs on changed files.** A comment fix pulls its file into the PR's changed set, surfacing pre-existing lint debt (e.g. TS typecheck, detekt), and adding a char (`today's`) can push a backtick test name past a line-length limit. If CI flags a file you only touched a comment in, drop that file/line from the PR rather than fixing unrelated debt.

When unsure, skip the fix.

#### e. Apply fixes — cap at ~50 substitutions per PR

```
python3 $SKILL_DIR/apply-fix.py <worktree>/<file> <line> <old> <new> "<matched line text>"
```

Pass the scan's matched line text (the part after `path:line:col:` in the `scan.sh` output) as the 5th arg. This makes the fix drift-safe — it lands on the line with that content even if line numbers shifted, and skips loudly if the line changed or was already fixed upstream. The 4-arg form (no matched line) still works but addresses by line number only.

After each substitution count, stop adding more fixes for this PR once you hit **50 substitutions** (≈ 50 changed file lines / ≈ 100 diff lines).
A small follow-up sweep can pick up the remainder later.

If the repo has fewer than 50 typos, fix them all.

#### f. Commit per file

One commit per file, atomic. Title pattern:

```
Fix typo in `<basename>`             # exactly 1 substitution
Fix typos in `<basename>`            # >1 substitutions
```

If the title exceeds 50 characters, drop the backticks first.
If still too long, the un-backticked form is preferable to truncating the filename.

Use `agentic-commit` with a heredoc message footer:

```
agentic-commit -C <worktree> -- <file> <<'EOF'
<title>

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
```

#### g. Push and open the PR

```
git -C <worktree> push -u origin mokagio/typo-fixes
```

Then `gh pr create`. Use `--head mokagio/typo-fixes` explicitly to avoid the "branch must first be pushed" race on freshly-pushed branches.

PR description: lean — see `~/.config/agents/rules/github.md`.
State that this is a mechanical, comment-only change with no behaviour impact and that each commit is one file for easy review.
Follow the repo's `PULL_REQUEST_TEMPLATE.md` if present.
Sign off with:

```
*Posted by Claude (Opus 4.7) on behalf of @mokagio with approval.*
```

Assign to `@mokagio`.

#### h. Monitor CI immediately — not optional

Per `AGENTS.md`:

```
Skill(skill="ci-monitor", args="<owner/repo>#<number>")
```

If `ci-monitor` reports `danger/pr-check` failures (missing label, missing milestone, past-due milestone):

```
Skill(skill="fix-pr-checks", args="<owner/repo>#<number>")
```

Then continue monitoring until Danger re-runs and resolves.
Don't move to the next repo until the current PR's checks are green or only known-flaky failures remain (report those).

#### i. Loop

Move to the next repo.

### 3. Empty / unfixable repos

A repo with no typos, or only unfixable ones (all generated / identifiers / i18n values): **clean up the worktree and branch**, don't open an empty PR.

```
git -C <repo> worktree remove ~/Developer/git-worktrees/<basename>/typo-fixes
git -C <repo> branch -D mokagio/typo-fixes
```

### 4. Stop conditions

- No repos left.
- The user interrupts.

## Constraints

- **One PR per repo per sweep.** Cap at ~50 substitutions; leftovers belong to a later sweep.
- **Never modify the main checkout** — always work in the worktree.
- **Never amend** — new commits only.
- **Sequential** — one repo at a time.
- **No PR for zero diffs** — clean up the worktree and branch instead.

## Reporting

After each PR:

```
<owner/repo>#<number> — <PR URL>, N fixes, CI: <status>
```

At the end:

```
Sweep complete.
N PRs opened, M repos skipped (reason).
Outstanding: <any PRs still red after fix-pr-checks>.
```
