{"owner":"lwouis","repo":"alt-tab-macos","hasSkills":true,"totalSkillsCount":7,"totalTokensCount":10316,"categories":["anthropic-skill","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/audit-specs-tests/SKILL.md",".claude/skills/translate-missing-l10n/SKILL.md",".claude/skills/coverage-explore/SKILL.md","AGENTS.md",".claude/skills/rework-comments/SKILL.md",".claude/skills/assets-optimization/SKILL.md","CLAUDE.md"],"skills":{".claude/skills/audit-specs-tests/SKILL.md":"---\nname: audit-specs-tests\ndescription: Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite.\n---\n\n# /audit-specs-tests\n\n## Project context\n\nAltTab co-locates tests and a living spec next to the source they cover. The convention (see the plan in `.claude/plans/` and `src/pro/license/` as a worked example) is a **triad** per concept:\n\n```\nsrc/<feature>/\n├── Foo.swift          # production / kernel (app target, or app+test for kernels)\n├── FooSpecs.md        # the living spec (not compiled)\n└── FooTests.swift     # XCTest (unit-tests target only)\n```\n\nThe spec is **documentation tied 1:1 to the test methods**. Its `## Test scenarios` section lists every test, grouped, as:\n\n```markdown\n### A. <Group name>\n- **testAFooDoesBar** — one-line description of what it pins.\n```\n\nThis skill's job is to keep the three in sync: every `func test…` ⇔ exactly one `- **test…** —` spec line, descriptions roughly agree, and no triad is missing a leg.\n\n## What counts as a triad\n\nPair by base name in the **same folder**: `<Base>Tests.swift` ⇔ `<Base>Specs.md`. A `<Base>.swift` (or a kernel like `SelectionResolver.swift`, `SearchTestable.swift`) in the folder is the code under test, but a spec may legitimately cover several classes (e.g. `LicenseManagerSpecs.md` documents `Clock`/`Keychain`/`LicenseAPI` too) — so a missing same-name `.swift` is a note, not an error.\n\n## Workflow\n\n1. **Enumerate.** Find all test and spec files:\n   ```sh\n   find src -name '*Tests.swift' | sort\n   find src -name '*Specs.md' | sort\n   ```\n   Also list any test files still under `unit-tests/` and flag them as **not yet migrated** (the target end-state is zero test files outside `src/`).\n\n2. **For each `*Tests.swift`, extract the test methods and their doc-comments.** Test names:\n   ```sh\n   grep -nE '^\\s*func test[A-Za-z0-9_]+\\(' src/<path>/FooTests.swift\n   ```\n   For each, capture the immediately-preceding `///` or `//` comment lines (the test's intent) and the enclosing `// MARK: -` group. Note methods wrapped in `#if DEBUG`.\n\n3. **For each `*Specs.md`, parse the scenario index.** In the `## Test scenarios` section, collect every `### <Group>` heading and every `- **testName** — description` bullet. Build the set of spec-listed test names and their descriptions.\n\n4. **Cross-check each triad and classify findings:**\n   - **Orphan test** — `func testX` exists but no `- **testX** —` line in the spec. (Most common drift; spec is stale.)\n   - **Orphan scenario** — spec lists `testX` but no `func testX` in the suite. (Test deleted/renamed; spec stale.)\n   - **Group mismatch** — a test's `// MARK:` group and its spec `###` group disagree (readability drift, not fatal).\n   - **Description drift** — the spec bullet and the test's doc-comment describe materially different behavior. Judge semantically, don't string-match; only flag real divergence.\n   - **Unpaired file** — a `*Tests.swift` with no `*Specs.md` (or vice-versa).\n\n5. **Lighter code check (kernels only).** For decision-kernel files (`*Testable.swift`, `*Resolver.swift`), list `public`/`static` entry-point functions and `enum … Decision` cases, and flag any with no scenario referencing them — a hint that a branch is undocumented/untested. Skip this for AppKit-coupled production files (intentionally not unit-tested here).\n\n6. **Optionally repair.** If asked, regenerate a spec's `## Test scenarios` section from the live test methods + their doc-comments (preserving the existing `### Group` order where it matches `// MARK:` sections). Never invent behavior — only restate what the test asserts. Do not edit `*Tests.swift` from this skill.\n\n## Reporting\n\n- A per-triad status table: `folder/Base` · #tests · #scenarios · status (✅ in sync / ⚠️ drift / ❌ unpaired).\n- A punch list grouped by finding type (orphan tests, orphan scenarios, description drift, unpaired, undocumented kernel branches), each with the file + line.\n- The count of test files still outside `src/` (migration progress).\n- If nothing is wrong, say so plainly. Then offer to fix the drift (regenerate stale spec scenario indexes) and list exactly which files would change.\n",".claude/skills/translate-missing-l10n/SKILL.md":"---\nname: translate-missing-l10n\ndescription: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.\n---\n\n# /translate-missing-l10n\n\n## Project context\n\nAltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.\n\n## Target languages (20)\n\n`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`\n\n(`en` is the source — handled automatically by the apply step.)\n\n## Translation guidelines\n\n- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\\n`, `\\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@` ≡ `%1$@ %2$@`), but the count must match.\n- **Do not translate proper nouns**: \"AltTab\", \"macOS\", \"Mission Control\", \"Spaces\", \"Dock\".\n- **Do not translate modifier key names**: \"Cmd\", \"Option\", \"Alt\", \"Shift\", \"Ctrl\", \"Control\", \"Fn\", \"Command\".\n- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale (\"Réglages\" vs \"Préférences\" in French) over a literal translation.\n- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.\n- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.\n- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.\n\n## Workflow\n\n1. **Refresh the source.** Run:\n   ```sh\n   bash scripts/l10n/extract_l10n_strings.sh\n   ```\n   This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.\n\n2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:\n   ```\n   /* engineer comment */\n   \"key\" = \"value\";\n   ```\n   Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.\n\n3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `\"key\" = \"translation\";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.\n\n4. **Stop if nothing is missing.** Report and exit.\n\n5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.\n\n6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:\n   ```json\n   {\n     \"fr\": { \"About %@\": \"À propos de %@\", \"Quit\": \"Quitter\" },\n     \"ja\": { \"About %@\": \"%@について\" }\n   }\n   ```\n   Then run:\n   ```sh\n   npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json\n   ```\n   The helper:\n   - Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.\n   - Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `\"key\" = \"key\";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.\n   - Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.\n   - Exits with code `2` if any entries were rejected.\n\n7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.\n\n## Reporting\n\nAfter the run, report:\n\n- Number of languages processed.\n- Total translations produced and merged.\n- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.\n- Any format-specifier rejections that required manual fixes.\n",".claude/skills/coverage-explore/SKILL.md":"---\nname: coverage-explore\ndescription: Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next.\n---\n\n# /coverage-explore\n\n## Project context\n\nAltTab follows the **Humble Object** pattern: logic lives in dependency-free **decision kernels**\n(`SelectionResolver.swift`, `SearchModeResolver.swift`, the `*Testable.swift` files) compiled into the\n`unit-tests` target and tested in isolation; the AppKit layer (windows, views, event taps) is kept thin\nand is deliberately *not* unit-tested. So coverage is meaningful **per kernel / per feature-source** —\nthose should trend to ~100%, every decision branch exercised — and an aggregate app-wide % is not a\ngoal.\n\n**Coverage is documentation here, not a build artifact.** The only stored coverage is:\n1. the per-feature badge (`> **Line coverage:** …`) atop each `*Specs.md`, and\n2. the root **`src/coverage.md`** table.\n\nThere is intentionally **nothing else**: no lcov, no HTML, no committed `.xcresult`, and coverage is\n**not** gathered on routine runs (`scripts/run_tests.sh`, CI, or Xcode) — only when this skill runs it.\n(Coverage isn't usefully viewable in IntelliJ, so there's no IDE-integration artifact to maintain.)\n\n## Workflow\n\n1. **Measure into a throwaway bundle** (then delete it — store nothing technical):\n   ```sh\n   rm -rf /tmp/altcov.xcresult\n   set -o pipefail && xcodebuild test \\\n     -project alt-tab-macos.xcodeproj -scheme Test -configuration Debug \\\n     -enableCodeCoverage YES -resultBundlePath /tmp/altcov.xcresult | scripts/xcbeautify\n   ```\n   Debug, like `ai/test.sh` — Release means wholemodule + -O, which both rebuilds the whole test\n   target on every edit and lets the optimizer merge away counter regions, so its percentages are\n   less faithful to the source.\n   If the build is red, stop and report — coverage on a failing suite is meaningless.\n\n2. **Parse** `xcrun xccov view --report --json /tmp/altcov.xcresult`. Each `targets[].files[]` has\n   `path`, `lineCoverage`, `coveredLines`, `executableLines`, `functions[]`. For line-by-line counts of\n   one file while investigating a gap: `xcrun xccov view --file <abs-path> /tmp/altcov.xcresult`.\n\n3. **Refresh `src/coverage.md`.** Rewrite the table with every `src/` file (excluding `*Tests.swift`)\n   that the test target compiles, sorted worst-coverage-first, as `| NN% (cov/exec) | \\`path\\` |`.\n   Keep the header note explaining the Humble-Object scope and that this + the badges are the only\n   stored coverage. Update the \"updated\" date.\n\n4. **Offer to refresh the per-spec badges.** For each `<Base>Specs.md` whose `<Base>.swift` (or kernel)\n   is in the report, propose updating its `> **Line coverage:** …` line with the new % + today's date.\n   Ask first; only apply the ones the user accepts. Keep honest notes where a file is intentionally\n   partial (e.g. `PreferencesMigrations.swift`).\n\n5. **Rank gaps & suggest tests.** For kernels below 100%, list the uncovered functions/regions, map each\n   to the behavior it represents (read the source), and write ready-to-paste additions: a\n   `- **testGroupScenario** — …` spec line + a short XCTest stub in that suite's builder style. Don't add\n   tests automatically unless asked.\n\n6. **Clean up:** `rm -rf /tmp/altcov.xcresult`. Leave no technical coverage files in the tree.\n\n## Reporting\n\n- Headline: a small ascending table of kernel `lineCoverage`, and the delta vs the previous `src/coverage.md`.\n- Ranked gap list: file → uncovered function/branch → the behavior it represents.\n- Suggested scenarios (spec lines + test stubs) for the top gaps.\n- State exactly what was rewritten (`src/coverage.md`, which badges). Flag files that are partial *by\n  design* (AppKit glue, excluded migrations) so a low number isn't mistaken for a problem.\n","AGENTS.md":"# macOS development\n- Don't use xcode directly to develop\n- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.\n- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.\n- Use guard closes as much as possible to separate the happy-path under them\n- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)\n- When possible, follow the triad pattern: specs in *Specs.md, unit-tests in *Tests.swift, and *swift for the implementation. Document features and their edge-cases this way\n- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O. Use observer APIs; don't poll.\n\n# Main-thread IPC\nOrdinary AppKit calls can be a synchronous XPC round trip, so they can hang for as long as the other process takes. `searchField.stringValue = \"\"` froze a user's main thread for 3.0s (#5981): it resigned first responder, which deactivates the system text-input context, which timed out. Assume any AppKit call that changes first responder, orders a window, or resizes one talks to another process.\n- Everything under `NSResponder` and `NSCell` (so `NSWindow`, `NSView`, `NSControl`, `NSTextField`, `NSApplication`) plus `NSTextInputContext` is main-thread-only, and AppKit asserts on it at runtime. For those the levers are ordering and deferral, not dispatch: the three off-main schedulers (`AXCallScheduler`, `CGSCallScheduler`, `ProcessCallScheduler`) can't take them.\n- `NSWorkspace` and `NSRunningApplication` are the documented exception: their headers state they are thread safe, so LaunchServices reads (`runningApplications`, `frontmostApplication`, `bundleURL`, `icon`, `activate`) may run off-main, and `Application.fetchAppIcon` already relies on that. Don't \"fix\" them back onto main. `src/main-thread-ipc.md` has the audited call sites and the evidence for both halves of this.\n- On the latency-critical paths (summoning, a keystroke while cycling or searching, dismissal with focus), nothing that can stall may run before the work the user is waiting for. Put the visible work first; put the bookkeeping after it.\n- A CoreAnimation transaction commits when the runloop turn ENDS. Setting `alphaValue` or calling `orderOut` does not reach the screen before that, so blocking later in the same turn keeps the old frame up. Defer with `DispatchQueue.main.async` and re-check the state in the block.\n- `MainThreadStall.step()` at the top of a main-thread function names it in the log when it runs long. Use it to measure rather than guessing; temporarily lower `thresholdInMs` and add marks around single statements to attribute a cost.\n\n# Comments\nA wrong comment costs several times more than a missing one, for humans and agents alike. So write for low drift, not for low line count. A correct, non-obvious comment can be as long as it needs to be.\n- Comment what the code cannot show: OS/API behaviour (macOS, CGS, SkyLight, AppKit), measured timings, private-API notes, invariants, and why a guard that looks removable isn't. Prefer measured evidence over recollection.\n- Don't comment what the code already says. Don't narrate history: no \"this used to be X\", no reverted approaches, no commentary on earlier comments. If a past bug is the reason a constraint exists, name the test that pins it instead.\n- State each rule once, at the widest scope it applies to. Restating it further down is what makes two copies drift apart.\n- One intent per comment, and keep it short. Split a paragraph that mixes a rule, its history, and an aside.\n- When you change code, re-read the comments around it. Updating them is part of the change, not a follow-up.\n- Run `/rework-comments` to audit or rework comments across a file, a folder, or all of `src/`.\n\n# Workflow\n- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change\n- Run the unit tests with ai/test.sh. Don't use scripts/run_tests.sh: that's the CI entrypoint and its Release config rebuilds the whole test target on every edit (34s vs 1.3s)\n- In a git worktree, copy `config/local.xcconfig` over from the main checkout before building. It's gitignored, so a fresh worktree doesn't have it, and `debug.xcconfig` / `release.xcconfig` `#include?` it. Without it the build dies at CodeSign (\"no identity found\") on a dependency framework, BEFORE the app target's own Swift compile — so it looks like the code compiled when nothing of `src/` was checked. `ai/test.sh` doesn't cover that either: the `unit-tests` target has its own source list and never builds the app target\n- Git commit messages must respect our pre-hook conventions, and must be clear and high-level, written for end-users (changelog)\n\n# License / Keychain invariant\n- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or `kSecAttrAccessGroup` with a stable group identifier).\n- Do not introduce legacy `SecKeychain*` API or `kSecAccessControl` (biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.\n",".claude/skills/rework-comments/SKILL.md":"---\nname: rework-comments\ndescription: Audit and rework code comments across AltTab so they help an AI agent instead of misleading it. Triages every comment by whether its claim is verifiable and non-derivable, deletes process narration and repeated lessons, shrinks history to test names, and flags comments that have drifted out of sync with the code. Use for a whole-codebase pass, for one file or folder that feels over-commented, after a big refactor, or whenever an agent has been misled by a comment.\n---\n\n# /rework-comments\n\nRework comments so the ones that survive are the ones an agent can act on. Runs on the whole of `src/`, or on a path you pass as an argument.\n\n## Why this exists (the evidence)\n\nMeasured on recent models, including Claude Code on Sonnet 4.6:\n\n- **Wrong beats absent, by a lot.** Misleading comments cost ~13.5pp on code-reasoning benchmarks; *removing* comments costs 2.4–8.2pp ([CodeCrash](https://arxiv.org/pdf/2504.14119)). A stale comment is 2–6x more expensive than no comment. It also inflates reasoning tokens 2–3x.\n- **Volume is not the lever.** 660 Claude Code trials on minimal-pair repos found no pass-rate difference from cleanliness, and a dedicated comment-volume ablation showed comment lines are *not* what drives agent token cost or file re-reads ([Trivedi & Schmitt, SonarSource 2026](https://arxiv.org/abs/2605.20049)).\n- **Short and single-intent beats long and mixed.** Across 80k translations, ~5-word descriptive comments outperformed 19-word author comments that mixed several intents ([study](https://co-r-e.com/method/code-comments-translation)).\n- **Repetition and conflict cost attention.** Anthropic cut 80%+ of Claude Code's system prompt with no eval loss, and names redundancy across sources and conflicting statements as the specific costs ([Claude 5 context engineering](https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models)).\n\nSo: **do not cut on line count. Cut on drift surface.** The goal is fewer claims that can go stale, not fewer characters. A correct, non-obvious comment stays no matter how long it is.\n\n## The triage rule\n\nFor each comment, ask two questions in order.\n\n**1. Can a reader verify this claim from the code in front of them?**\n\n- **No, and it is a fact about the outside world** (macOS / CGS / SkyLight / AppKit behaviour, an OS timing quirk, a measured capture, an API contract) → **KEEP**. This is the irreplaceable category. It cannot be re-derived without reproducing the bug.\n- **No, and it is a fact about our own history or process** (\"this used to be X\", \"we tried Y and reverted\", \"this comment used to say\", \"the lesson written three times in this file\") → **CUT**. An agent cannot act on it, and it reads as a live claim about current code.\n- **Yes, it restates what the code says** → **CUT**, unless it names a consequence the code doesn't show.\n\n**2. What breaks if it goes?**\n\n- *\"Someone re-simplifies this and reintroduces a bug\"* → **KEEP, but shrink to the guardrail.** One sentence for the constraint, plus the test name that enforces it. The test is what actually holds the line; the prose only needs to point at it.\n- *\"Nothing\"* → **CUT.**\n\nEverything else is a judgment call, and the default on a judgment call is keep. A wrongly-deleted true comment costs more than a kept mediocre one.\n\n## Categories, in priority order\n\nWork these in order. The first is the one that pays.\n\n**P1 — Drifted comments (the only category that hurts correctness).**\nA comment whose claim contradicts the code beside it, names a symbol that no longer exists, describes a branch that moved, or states an ordering the code no longer has. Fix the comment, or delete it if the fact is gone. **Never fix by changing the code to match the comment** — report that as a possible bug instead and leave both alone.\n\n**P2 — Repeated lessons.**\nThe same rule stated in more than one place in a file (or across sibling files). Keep the clearest statement at the highest-scope declaration (the `enum`/`struct`/file header), delete the restatements. If a restatement carries a detail the canonical one lacks, move the detail up rather than keeping both.\n\n**P3 — Process narration and meta-commentary.**\nAbandoned approaches, \"this was tried and reverted\", commentary about what a previous version of the comment said, apologies, self-aware asides about the comment itself. If a reverted approach is genuinely load-bearing (someone will obviously retry it), compress to one clause: `// tried pairing with newlyDiscovered; breaks testX`.\n\n**P4 — Restatement of the code.**\n`// increment the counter` above `count += 1`. Delete outright. Per AGENTS.md, if a block needs a comment to be followable, prefer splitting it into a named sub-method.\n\n**P5 — Structurally broken comments.**\nBullet lists interrupted mid-flow by a paragraph so the trailing bullets appear to belong to a different list; comments whose indentation detached them from the statement they explain; doc comments that drifted above the wrong declaration. Partial reads of these give wrong answers, so they behave like P1.\n\n## What always survives\n\nDo not touch these, even in a file that is 2:1 comments to code:\n\n- Invariants and preconditions stated once at the top of a type (e.g. the fullscreen Space invariant in `TabGroupResolver.swift`).\n- Non-derivable OS/API behaviour, especially with measured evidence (`the OS creates a new tab at 0×0 and sizes it ~640ms later`).\n- Private-API notes, `#`-issue references, and undocumented-behaviour warnings.\n- Doc comments on kernel entry points that the matching `*Specs.md` refers to.\n- The reason a guard exists, when the guard looks removable without it.\n- Licence headers, `// MARK:` structure, `swiftlint` directives.\n\n## Workflow\n\n1. **Scope and baseline.** Default to `src/`; use the path argument if given.\n   ```sh\n   find src -name '*.swift' -not -path '*_test-support*' | wc -l\n   ```\n   Measure comment-to-code ratio per file and repo-wide:\n   ```sh\n   for f in $(find src -name '*.swift' -not -path '*_test-support*'); do\n     awk -v F=\"$f\" '{ if ($0 ~ /^[[:space:]]*(\\/\\/|\\/\\*|\\*)/) c++; else if ($0 !~ /^[[:space:]]*$/) k++ }\n       END { if (k > 0) printf \"%.2f %d %d %s\\n\", c/k, c, k, F }' \"$f\"\n   done | sort -rn | head -40\n   ```\n   Report the repo baseline ratio and the outliers. **The ratio picks reading order, it is not a target.** A 3:1 file of measured OS facts is fine; a 0.4:1 file of stale restatement is not.\n\n2. **Read whole files, never grep for comments in isolation.** Every triage question is about the comment *against its code*. A comment excerpt cannot be judged.\n\n3. **Triage per file.** Walk the comments top to bottom, tag each P1–P5 or KEEP. For P1, verify the drift by reading the code the comment describes — if you cannot confirm it drifted, it stays.\n\n4. **Apply.** Edit comments only. One file at a time. On a whole-codebase run, checkpoint progress to `.claude/scratch/rework-comments-progress.md` (file · status · findings) after each file, so an interrupted run resumes without re-reading.\n\n5. **Verify nothing but comments changed.** This is the safety property of the whole skill:\n   ```sh\n   git diff -U0 -- '*.swift' | grep -E '^[+-]' | grep -vE '^(\\+\\+\\+|---)' \\\n     | grep -vE '^[+-][[:space:]]*(//|/\\*|\\*|\\*/)' | grep -vE '^[+-][[:space:]]*$'\n   ```\n   This must print nothing. If it prints a line, you changed code — revert that hunk. The one false positive: editing a **trailing** comment (`let x = 2  // note`) rewrites a code line, so it shows up here. Confirm by eye that the code before the `//` is byte-identical, then move on.\n\n6. **Build.** Copy the commands from `ai/build.sh` and run them. Comment-only edits can still break a build (an unterminated block comment, a doc comment detached from its declaration).\n\n7. **Cross-check the specs.** If you touched a kernel's doc comments, run `/audit-specs-tests` for the affected triads — the `*Specs.md` may quote the prose you changed.\n\n## Reporting\n\n- Repo baseline ratio, and a table of the files you reworked: path · before/after comment lines · categories applied.\n- **P1 findings listed separately and first**, each with file:line, the claim, and what the code actually does. These are the ones the user most needs to see, and the ones most likely to indicate a real bug rather than a doc error.\n- Anything you deliberately kept that looks over-commented, with the one-line reason (usually: measured OS behaviour).\n- Any place where the comment and the code disagreed and **the comment looked right** — a suspected bug, left untouched, for the user to decide.\n- If a file is already in good shape, say so and move on. Do not manufacture edits to show effort.\n",".claude/skills/assets-optimization/SKILL.md":"---\nname: assets-optimization\ndescription: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.\n---\n\n# /assets-optimization — AltTab asset audit and optimization\n\n## Goal\n\nEvery byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.\n\nThis skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.\n\n## When to use\n\n- A designer drops new exports into `~/Desktop/` or `resources/`.\n- Someone asks \"why is the bundle so big?\".\n- After adding a new icon, illustration, app icon variant, or menubar variant.\n- Periodic audit when nothing else is broken.\n\n## Step 1: Inventory\n\nRun a one-shot enumeration so you know what you're working with:\n\n```sh\nfind resources -type f \\( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \\) \\\n  -exec ls -la {} \\; | awk '{printf \"%8d  %s\\n\", $5, $9}' | sort -k2\n```\n\nGroup what you see by directory. For AltTab the relevant buckets are:\n\n- `resources/icons/menubar/` — small template icons shown in the macOS menubar.\n- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).\n- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).\n- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch** — `.icns` is required by the bundle and Apple's tooling produces near-optimal output already.\n- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).\n\nFor each asset, decide what category it falls into:\n\n| Source content | Right format | Why |\n|---|---|---|\n| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |\n| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |\n| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |\n| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |\n| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |\n\nIf an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.\n\n## Step 2: Vector PDFs — Figma exports\n\nFigma's \"Export → PDF\" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel. The script then rounds path coordinates to 3 decimals and shrinks sampled gradients to 13x13, which halves the menubar icons again for at most 4/255 of difference at @1x-@3x (see the constants atop the script).\n\nWhat Figma adds that AppKit doesn't need:\n\n1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:\n   - the page `Resources/ColorSpace` dict\n   - every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)\n   - every Image XObject's direct `/ColorSpace` key\n   - every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)\n2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.\n3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags (\"Document\" / \"Part\" structural roles). AppKit's PDF renderer ignores them.\n4. **`/Info` dict** — `Producer=\"Figma\"`, `Title=\"Menubar 22x22@1x white\"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.\n5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.\n6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.\n\nUse the script:\n\n```sh\npython3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf\n```\n\nIt edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.\n\nVerify each file still renders by sips'ing it back to PNG and eyeballing:\n\n```sh\nfor f in resources/icons/menubar/*.pdf; do\n  sips -s format png \"$f\" --out \"/tmp/$(basename $f .pdf).png\" -Z 300 >/dev/null 2>&1\ndone\n```\n\nOpen the PNGs in Preview to confirm nothing visual changed.\n\n## Step 3: SF Symbols via font subset\n\nEvery SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.\n\nHow it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = \"\"`. At runtime, `NSFont(name: \"SF Pro Text\", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.\n\nTo add a new SF Symbol:\n\n1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)\n2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = \"􀝥\"  // SF Symbol name`.\n3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).\n4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.\n5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.\n\nThe script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.\n\n### Historical note: SF Symbols via PDF (deprecated, scripts removed)\n\nA previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):\n\n- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.\n- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:\n  `NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform(\"vectorGlyph\") (CUINamedVectorGlyph) → .perform(\"CGPath\") (real CGPath)`.\n- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.\n- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.\n\nGit: see commit `990c1e79` (\"feat: pro improve assets\") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.\n\n## Step 4: Raster → HEIC at q50\n\nFor anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.\n\nPipeline (built-in to macOS via `sips`):\n\n```sh\nsips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic\n```\n\n- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.\n- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.\n\nUse the script for batch conversion:\n\n```sh\nbash scripts/assets/encode_heic.sh ~/Desktop 1000 50\n```\n\nThat walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.\n\n### Visual review (mandatory)\n\nBefore swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:\n\n```sh\nsips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1\nopen /tmp/sample-decoded.png /Users/you/Desktop/sample.png\n```\n\nPick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:\n\n- No banding in flat color regions\n- Text edges still crisp at native display size\n- No haloing around anti-aliased edges\n- Color rendition matches\n\nIf anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.\n\n### Bumping quality\n\nIf q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.\n\n## Step 5: pbxproj registration\n\nWhenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:\n\n1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.\n2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = \"name.ext\"`.\n3. **PBXGroup section** — the file's entry inside its parent group's `children`.\n4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.\n\nFor pure extension swaps (no new files), `sed -i '' 's|old\\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c \"import secrets; print(secrets.token_hex(12).upper())\"`) and insert in all four places.\n\nIf you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with \"missing file\" or ships dangling references.\n\n## Step 6: Verify\n\n```sh\nbash ai/build.sh        # must show ** BUILD SUCCEEDED **\nbash ai/run.sh          # launch the app and visually inspect every asset\n```\n\nWalk through every UI surface that loads an asset:\n\n- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)\n- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)\n- Permissions window — open by revoking a permission\n- Preferences → Appearance — illustration thumbnails change per show/hide row\n\nCompare `git diff --stat` before committing. Asset replacements should net negative on bundle size.\n\n## Reporting\n\nAfter the run, report:\n\n- File-by-file before/after sizes for everything that changed.\n- Total bundle delta in KB.\n- Any files left untouched and why (e.g., `app.icns` — bundle-required format).\n- The encoder settings used (especially HEIC quality if not q50, so the next person knows).\n- Anything the visual review revealed (e.g., \"had to bump to q60 for `thumbnails_dark` because gradient banding at q50\").\n","CLAUDE.md":"@include ./AGENTS.md\n"},"files":{".claude/skills/audit-specs-tests/SKILL.md":"---\nname: audit-specs-tests\ndescription: Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite.\n---\n\n# /audit-specs-tests\n\n## Project context\n\nAltTab co-locates tests and a living spec next to the source they cover. The convention (see the plan in `.claude/plans/` and `src/pro/license/` as a worked example) is a **triad** per concept:\n\n```\nsrc/<feature>/\n├── Foo.swift          # production / kernel (app target, or app+test for kernels)\n├── FooSpecs.md        # the living spec (not compiled)\n└── FooTests.swift     # XCTest (unit-tests target only)\n```\n\nThe spec is **documentation tied 1:1 to the test methods**. Its `## Test scenarios` section lists every test, grouped, as:\n\n```markdown\n### A. <Group name>\n- **testAFooDoesBar** — one-line description of what it pins.\n```\n\nThis skill's job is to keep the three in sync: every `func test…` ⇔ exactly one `- **test…** —` spec line, descriptions roughly agree, and no triad is missing a leg.\n\n## What counts as a triad\n\nPair by base name in the **same folder**: `<Base>Tests.swift` ⇔ `<Base>Specs.md`. A `<Base>.swift` (or a kernel like `SelectionResolver.swift`, `SearchTestable.swift`) in the folder is the code under test, but a spec may legitimately cover several classes (e.g. `LicenseManagerSpecs.md` documents `Clock`/`Keychain`/`LicenseAPI` too) — so a missing same-name `.swift` is a note, not an error.\n\n## Workflow\n\n1. **Enumerate.** Find all test and spec files:\n   ```sh\n   find src -name '*Tests.swift' | sort\n   find src -name '*Specs.md' | sort\n   ```\n   Also list any test files still under `unit-tests/` and flag them as **not yet migrated** (the target end-state is zero test files outside `src/`).\n\n2. **For each `*Tests.swift`, extract the test methods and their doc-comments.** Test names:\n   ```sh\n   grep -nE '^\\s*func test[A-Za-z0-9_]+\\(' src/<path>/FooTests.swift\n   ```\n   For each, capture the immediately-preceding `///` or `//` comment lines (the test's intent) and the enclosing `// MARK: -` group. Note methods wrapped in `#if DEBUG`.\n\n3. **For each `*Specs.md`, parse the scenario index.** In the `## Test scenarios` section, collect every `### <Group>` heading and every `- **testName** — description` bullet. Build the set of spec-listed test names and their descriptions.\n\n4. **Cross-check each triad and classify findings:**\n   - **Orphan test** — `func testX` exists but no `- **testX** —` line in the spec. (Most common drift; spec is stale.)\n   - **Orphan scenario** — spec lists `testX` but no `func testX` in the suite. (Test deleted/renamed; spec stale.)\n   - **Group mismatch** — a test's `// MARK:` group and its spec `###` group disagree (readability drift, not fatal).\n   - **Description drift** — the spec bullet and the test's doc-comment describe materially different behavior. Judge semantically, don't string-match; only flag real divergence.\n   - **Unpaired file** — a `*Tests.swift` with no `*Specs.md` (or vice-versa).\n\n5. **Lighter code check (kernels only).** For decision-kernel files (`*Testable.swift`, `*Resolver.swift`), list `public`/`static` entry-point functions and `enum … Decision` cases, and flag any with no scenario referencing them — a hint that a branch is undocumented/untested. Skip this for AppKit-coupled production files (intentionally not unit-tested here).\n\n6. **Optionally repair.** If asked, regenerate a spec's `## Test scenarios` section from the live test methods + their doc-comments (preserving the existing `### Group` order where it matches `// MARK:` sections). Never invent behavior — only restate what the test asserts. Do not edit `*Tests.swift` from this skill.\n\n## Reporting\n\n- A per-triad status table: `folder/Base` · #tests · #scenarios · status (✅ in sync / ⚠️ drift / ❌ unpaired).\n- A punch list grouped by finding type (orphan tests, orphan scenarios, description drift, unpaired, undocumented kernel branches), each with the file + line.\n- The count of test files still outside `src/` (migration progress).\n- If nothing is wrong, say so plainly. Then offer to fix the drift (regenerate stale spec scenario indexes) and list exactly which files would change.\n",".claude/skills/translate-missing-l10n/SKILL.md":"---\nname: translate-missing-l10n\ndescription: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.\n---\n\n# /translate-missing-l10n\n\n## Project context\n\nAltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.\n\n## Target languages (20)\n\n`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`\n\n(`en` is the source — handled automatically by the apply step.)\n\n## Translation guidelines\n\n- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\\n`, `\\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@` ≡ `%1$@ %2$@`), but the count must match.\n- **Do not translate proper nouns**: \"AltTab\", \"macOS\", \"Mission Control\", \"Spaces\", \"Dock\".\n- **Do not translate modifier key names**: \"Cmd\", \"Option\", \"Alt\", \"Shift\", \"Ctrl\", \"Control\", \"Fn\", \"Command\".\n- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale (\"Réglages\" vs \"Préférences\" in French) over a literal translation.\n- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.\n- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.\n- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.\n\n## Workflow\n\n1. **Refresh the source.** Run:\n   ```sh\n   bash scripts/l10n/extract_l10n_strings.sh\n   ```\n   This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.\n\n2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:\n   ```\n   /* engineer comment */\n   \"key\" = \"value\";\n   ```\n   Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.\n\n3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `\"key\" = \"translation\";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.\n\n4. **Stop if nothing is missing.** Report and exit.\n\n5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.\n\n6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:\n   ```json\n   {\n     \"fr\": { \"About %@\": \"À propos de %@\", \"Quit\": \"Quitter\" },\n     \"ja\": { \"About %@\": \"%@について\" }\n   }\n   ```\n   Then run:\n   ```sh\n   npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json\n   ```\n   The helper:\n   - Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.\n   - Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `\"key\" = \"key\";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.\n   - Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.\n   - Exits with code `2` if any entries were rejected.\n\n7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.\n\n## Reporting\n\nAfter the run, report:\n\n- Number of languages processed.\n- Total translations produced and merged.\n- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.\n- Any format-specifier rejections that required manual fixes.\n",".claude/skills/coverage-explore/SKILL.md":"---\nname: coverage-explore\ndescription: Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next.\n---\n\n# /coverage-explore\n\n## Project context\n\nAltTab follows the **Humble Object** pattern: logic lives in dependency-free **decision kernels**\n(`SelectionResolver.swift`, `SearchModeResolver.swift`, the `*Testable.swift` files) compiled into the\n`unit-tests` target and tested in isolation; the AppKit layer (windows, views, event taps) is kept thin\nand is deliberately *not* unit-tested. So coverage is meaningful **per kernel / per feature-source** —\nthose should trend to ~100%, every decision branch exercised — and an aggregate app-wide % is not a\ngoal.\n\n**Coverage is documentation here, not a build artifact.** The only stored coverage is:\n1. the per-feature badge (`> **Line coverage:** …`) atop each `*Specs.md`, and\n2. the root **`src/coverage.md`** table.\n\nThere is intentionally **nothing else**: no lcov, no HTML, no committed `.xcresult`, and coverage is\n**not** gathered on routine runs (`scripts/run_tests.sh`, CI, or Xcode) — only when this skill runs it.\n(Coverage isn't usefully viewable in IntelliJ, so there's no IDE-integration artifact to maintain.)\n\n## Workflow\n\n1. **Measure into a throwaway bundle** (then delete it — store nothing technical):\n   ```sh\n   rm -rf /tmp/altcov.xcresult\n   set -o pipefail && xcodebuild test \\\n     -project alt-tab-macos.xcodeproj -scheme Test -configuration Debug \\\n     -enableCodeCoverage YES -resultBundlePath /tmp/altcov.xcresult | scripts/xcbeautify\n   ```\n   Debug, like `ai/test.sh` — Release means wholemodule + -O, which both rebuilds the whole test\n   target on every edit and lets the optimizer merge away counter regions, so its percentages are\n   less faithful to the source.\n   If the build is red, stop and report — coverage on a failing suite is meaningless.\n\n2. **Parse** `xcrun xccov view --report --json /tmp/altcov.xcresult`. Each `targets[].files[]` has\n   `path`, `lineCoverage`, `coveredLines`, `executableLines`, `functions[]`. For line-by-line counts of\n   one file while investigating a gap: `xcrun xccov view --file <abs-path> /tmp/altcov.xcresult`.\n\n3. **Refresh `src/coverage.md`.** Rewrite the table with every `src/` file (excluding `*Tests.swift`)\n   that the test target compiles, sorted worst-coverage-first, as `| NN% (cov/exec) | \\`path\\` |`.\n   Keep the header note explaining the Humble-Object scope and that this + the badges are the only\n   stored coverage. Update the \"updated\" date.\n\n4. **Offer to refresh the per-spec badges.** For each `<Base>Specs.md` whose `<Base>.swift` (or kernel)\n   is in the report, propose updating its `> **Line coverage:** …` line with the new % + today's date.\n   Ask first; only apply the ones the user accepts. Keep honest notes where a file is intentionally\n   partial (e.g. `PreferencesMigrations.swift`).\n\n5. **Rank gaps & suggest tests.** For kernels below 100%, list the uncovered functions/regions, map each\n   to the behavior it represents (read the source), and write ready-to-paste additions: a\n   `- **testGroupScenario** — …` spec line + a short XCTest stub in that suite's builder style. Don't add\n   tests automatically unless asked.\n\n6. **Clean up:** `rm -rf /tmp/altcov.xcresult`. Leave no technical coverage files in the tree.\n\n## Reporting\n\n- Headline: a small ascending table of kernel `lineCoverage`, and the delta vs the previous `src/coverage.md`.\n- Ranked gap list: file → uncovered function/branch → the behavior it represents.\n- Suggested scenarios (spec lines + test stubs) for the top gaps.\n- State exactly what was rewritten (`src/coverage.md`, which badges). Flag files that are partial *by\n  design* (AppKit glue, excluded migrations) so a low number isn't mistaken for a problem.\n","AGENTS.md":"# macOS development\n- Don't use xcode directly to develop\n- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.\n- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.\n- Use guard closes as much as possible to separate the happy-path under them\n- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)\n- When possible, follow the triad pattern: specs in *Specs.md, unit-tests in *Tests.swift, and *swift for the implementation. Document features and their edge-cases this way\n- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O. Use observer APIs; don't poll.\n\n# Main-thread IPC\nOrdinary AppKit calls can be a synchronous XPC round trip, so they can hang for as long as the other process takes. `searchField.stringValue = \"\"` froze a user's main thread for 3.0s (#5981): it resigned first responder, which deactivates the system text-input context, which timed out. Assume any AppKit call that changes first responder, orders a window, or resizes one talks to another process.\n- Everything under `NSResponder` and `NSCell` (so `NSWindow`, `NSView`, `NSControl`, `NSTextField`, `NSApplication`) plus `NSTextInputContext` is main-thread-only, and AppKit asserts on it at runtime. For those the levers are ordering and deferral, not dispatch: the three off-main schedulers (`AXCallScheduler`, `CGSCallScheduler`, `ProcessCallScheduler`) can't take them.\n- `NSWorkspace` and `NSRunningApplication` are the documented exception: their headers state they are thread safe, so LaunchServices reads (`runningApplications`, `frontmostApplication`, `bundleURL`, `icon`, `activate`) may run off-main, and `Application.fetchAppIcon` already relies on that. Don't \"fix\" them back onto main. `src/main-thread-ipc.md` has the audited call sites and the evidence for both halves of this.\n- On the latency-critical paths (summoning, a keystroke while cycling or searching, dismissal with focus), nothing that can stall may run before the work the user is waiting for. Put the visible work first; put the bookkeeping after it.\n- A CoreAnimation transaction commits when the runloop turn ENDS. Setting `alphaValue` or calling `orderOut` does not reach the screen before that, so blocking later in the same turn keeps the old frame up. Defer with `DispatchQueue.main.async` and re-check the state in the block.\n- `MainThreadStall.step()` at the top of a main-thread function names it in the log when it runs long. Use it to measure rather than guessing; temporarily lower `thresholdInMs` and add marks around single statements to attribute a cost.\n\n# Comments\nA wrong comment costs several times more than a missing one, for humans and agents alike. So write for low drift, not for low line count. A correct, non-obvious comment can be as long as it needs to be.\n- Comment what the code cannot show: OS/API behaviour (macOS, CGS, SkyLight, AppKit), measured timings, private-API notes, invariants, and why a guard that looks removable isn't. Prefer measured evidence over recollection.\n- Don't comment what the code already says. Don't narrate history: no \"this used to be X\", no reverted approaches, no commentary on earlier comments. If a past bug is the reason a constraint exists, name the test that pins it instead.\n- State each rule once, at the widest scope it applies to. Restating it further down is what makes two copies drift apart.\n- One intent per comment, and keep it short. Split a paragraph that mixes a rule, its history, and an aside.\n- When you change code, re-read the comments around it. Updating them is part of the change, not a follow-up.\n- Run `/rework-comments` to audit or rework comments across a file, a folder, or all of `src/`.\n\n# Workflow\n- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change\n- Run the unit tests with ai/test.sh. Don't use scripts/run_tests.sh: that's the CI entrypoint and its Release config rebuilds the whole test target on every edit (34s vs 1.3s)\n- In a git worktree, copy `config/local.xcconfig` over from the main checkout before building. It's gitignored, so a fresh worktree doesn't have it, and `debug.xcconfig` / `release.xcconfig` `#include?` it. Without it the build dies at CodeSign (\"no identity found\") on a dependency framework, BEFORE the app target's own Swift compile — so it looks like the code compiled when nothing of `src/` was checked. `ai/test.sh` doesn't cover that either: the `unit-tests` target has its own source list and never builds the app target\n- Git commit messages must respect our pre-hook conventions, and must be clear and high-level, written for end-users (changelog)\n\n# License / Keychain invariant\n- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or `kSecAttrAccessGroup` with a stable group identifier).\n- Do not introduce legacy `SecKeychain*` API or `kSecAccessControl` (biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.\n",".claude/skills/rework-comments/SKILL.md":"---\nname: rework-comments\ndescription: Audit and rework code comments across AltTab so they help an AI agent instead of misleading it. Triages every comment by whether its claim is verifiable and non-derivable, deletes process narration and repeated lessons, shrinks history to test names, and flags comments that have drifted out of sync with the code. Use for a whole-codebase pass, for one file or folder that feels over-commented, after a big refactor, or whenever an agent has been misled by a comment.\n---\n\n# /rework-comments\n\nRework comments so the ones that survive are the ones an agent can act on. Runs on the whole of `src/`, or on a path you pass as an argument.\n\n## Why this exists (the evidence)\n\nMeasured on recent models, including Claude Code on Sonnet 4.6:\n\n- **Wrong beats absent, by a lot.** Misleading comments cost ~13.5pp on code-reasoning benchmarks; *removing* comments costs 2.4–8.2pp ([CodeCrash](https://arxiv.org/pdf/2504.14119)). A stale comment is 2–6x more expensive than no comment. It also inflates reasoning tokens 2–3x.\n- **Volume is not the lever.** 660 Claude Code trials on minimal-pair repos found no pass-rate difference from cleanliness, and a dedicated comment-volume ablation showed comment lines are *not* what drives agent token cost or file re-reads ([Trivedi & Schmitt, SonarSource 2026](https://arxiv.org/abs/2605.20049)).\n- **Short and single-intent beats long and mixed.** Across 80k translations, ~5-word descriptive comments outperformed 19-word author comments that mixed several intents ([study](https://co-r-e.com/method/code-comments-translation)).\n- **Repetition and conflict cost attention.** Anthropic cut 80%+ of Claude Code's system prompt with no eval loss, and names redundancy across sources and conflicting statements as the specific costs ([Claude 5 context engineering](https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models)).\n\nSo: **do not cut on line count. Cut on drift surface.** The goal is fewer claims that can go stale, not fewer characters. A correct, non-obvious comment stays no matter how long it is.\n\n## The triage rule\n\nFor each comment, ask two questions in order.\n\n**1. Can a reader verify this claim from the code in front of them?**\n\n- **No, and it is a fact about the outside world** (macOS / CGS / SkyLight / AppKit behaviour, an OS timing quirk, a measured capture, an API contract) → **KEEP**. This is the irreplaceable category. It cannot be re-derived without reproducing the bug.\n- **No, and it is a fact about our own history or process** (\"this used to be X\", \"we tried Y and reverted\", \"this comment used to say\", \"the lesson written three times in this file\") → **CUT**. An agent cannot act on it, and it reads as a live claim about current code.\n- **Yes, it restates what the code says** → **CUT**, unless it names a consequence the code doesn't show.\n\n**2. What breaks if it goes?**\n\n- *\"Someone re-simplifies this and reintroduces a bug\"* → **KEEP, but shrink to the guardrail.** One sentence for the constraint, plus the test name that enforces it. The test is what actually holds the line; the prose only needs to point at it.\n- *\"Nothing\"* → **CUT.**\n\nEverything else is a judgment call, and the default on a judgment call is keep. A wrongly-deleted true comment costs more than a kept mediocre one.\n\n## Categories, in priority order\n\nWork these in order. The first is the one that pays.\n\n**P1 — Drifted comments (the only category that hurts correctness).**\nA comment whose claim contradicts the code beside it, names a symbol that no longer exists, describes a branch that moved, or states an ordering the code no longer has. Fix the comment, or delete it if the fact is gone. **Never fix by changing the code to match the comment** — report that as a possible bug instead and leave both alone.\n\n**P2 — Repeated lessons.**\nThe same rule stated in more than one place in a file (or across sibling files). Keep the clearest statement at the highest-scope declaration (the `enum`/`struct`/file header), delete the restatements. If a restatement carries a detail the canonical one lacks, move the detail up rather than keeping both.\n\n**P3 — Process narration and meta-commentary.**\nAbandoned approaches, \"this was tried and reverted\", commentary about what a previous version of the comment said, apologies, self-aware asides about the comment itself. If a reverted approach is genuinely load-bearing (someone will obviously retry it), compress to one clause: `// tried pairing with newlyDiscovered; breaks testX`.\n\n**P4 — Restatement of the code.**\n`// increment the counter` above `count += 1`. Delete outright. Per AGENTS.md, if a block needs a comment to be followable, prefer splitting it into a named sub-method.\n\n**P5 — Structurally broken comments.**\nBullet lists interrupted mid-flow by a paragraph so the trailing bullets appear to belong to a different list; comments whose indentation detached them from the statement they explain; doc comments that drifted above the wrong declaration. Partial reads of these give wrong answers, so they behave like P1.\n\n## What always survives\n\nDo not touch these, even in a file that is 2:1 comments to code:\n\n- Invariants and preconditions stated once at the top of a type (e.g. the fullscreen Space invariant in `TabGroupResolver.swift`).\n- Non-derivable OS/API behaviour, especially with measured evidence (`the OS creates a new tab at 0×0 and sizes it ~640ms later`).\n- Private-API notes, `#`-issue references, and undocumented-behaviour warnings.\n- Doc comments on kernel entry points that the matching `*Specs.md` refers to.\n- The reason a guard exists, when the guard looks removable without it.\n- Licence headers, `// MARK:` structure, `swiftlint` directives.\n\n## Workflow\n\n1. **Scope and baseline.** Default to `src/`; use the path argument if given.\n   ```sh\n   find src -name '*.swift' -not -path '*_test-support*' | wc -l\n   ```\n   Measure comment-to-code ratio per file and repo-wide:\n   ```sh\n   for f in $(find src -name '*.swift' -not -path '*_test-support*'); do\n     awk -v F=\"$f\" '{ if ($0 ~ /^[[:space:]]*(\\/\\/|\\/\\*|\\*)/) c++; else if ($0 !~ /^[[:space:]]*$/) k++ }\n       END { if (k > 0) printf \"%.2f %d %d %s\\n\", c/k, c, k, F }' \"$f\"\n   done | sort -rn | head -40\n   ```\n   Report the repo baseline ratio and the outliers. **The ratio picks reading order, it is not a target.** A 3:1 file of measured OS facts is fine; a 0.4:1 file of stale restatement is not.\n\n2. **Read whole files, never grep for comments in isolation.** Every triage question is about the comment *against its code*. A comment excerpt cannot be judged.\n\n3. **Triage per file.** Walk the comments top to bottom, tag each P1–P5 or KEEP. For P1, verify the drift by reading the code the comment describes — if you cannot confirm it drifted, it stays.\n\n4. **Apply.** Edit comments only. One file at a time. On a whole-codebase run, checkpoint progress to `.claude/scratch/rework-comments-progress.md` (file · status · findings) after each file, so an interrupted run resumes without re-reading.\n\n5. **Verify nothing but comments changed.** This is the safety property of the whole skill:\n   ```sh\n   git diff -U0 -- '*.swift' | grep -E '^[+-]' | grep -vE '^(\\+\\+\\+|---)' \\\n     | grep -vE '^[+-][[:space:]]*(//|/\\*|\\*|\\*/)' | grep -vE '^[+-][[:space:]]*$'\n   ```\n   This must print nothing. If it prints a line, you changed code — revert that hunk. The one false positive: editing a **trailing** comment (`let x = 2  // note`) rewrites a code line, so it shows up here. Confirm by eye that the code before the `//` is byte-identical, then move on.\n\n6. **Build.** Copy the commands from `ai/build.sh` and run them. Comment-only edits can still break a build (an unterminated block comment, a doc comment detached from its declaration).\n\n7. **Cross-check the specs.** If you touched a kernel's doc comments, run `/audit-specs-tests` for the affected triads — the `*Specs.md` may quote the prose you changed.\n\n## Reporting\n\n- Repo baseline ratio, and a table of the files you reworked: path · before/after comment lines · categories applied.\n- **P1 findings listed separately and first**, each with file:line, the claim, and what the code actually does. These are the ones the user most needs to see, and the ones most likely to indicate a real bug rather than a doc error.\n- Anything you deliberately kept that looks over-commented, with the one-line reason (usually: measured OS behaviour).\n- Any place where the comment and the code disagreed and **the comment looked right** — a suspected bug, left untouched, for the user to decide.\n- If a file is already in good shape, say so and move on. Do not manufacture edits to show effort.\n",".claude/skills/assets-optimization/SKILL.md":"---\nname: assets-optimization\ndescription: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.\n---\n\n# /assets-optimization — AltTab asset audit and optimization\n\n## Goal\n\nEvery byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.\n\nThis skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.\n\n## When to use\n\n- A designer drops new exports into `~/Desktop/` or `resources/`.\n- Someone asks \"why is the bundle so big?\".\n- After adding a new icon, illustration, app icon variant, or menubar variant.\n- Periodic audit when nothing else is broken.\n\n## Step 1: Inventory\n\nRun a one-shot enumeration so you know what you're working with:\n\n```sh\nfind resources -type f \\( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \\) \\\n  -exec ls -la {} \\; | awk '{printf \"%8d  %s\\n\", $5, $9}' | sort -k2\n```\n\nGroup what you see by directory. For AltTab the relevant buckets are:\n\n- `resources/icons/menubar/` — small template icons shown in the macOS menubar.\n- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).\n- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).\n- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch** — `.icns` is required by the bundle and Apple's tooling produces near-optimal output already.\n- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).\n\nFor each asset, decide what category it falls into:\n\n| Source content | Right format | Why |\n|---|---|---|\n| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |\n| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |\n| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |\n| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |\n| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |\n\nIf an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.\n\n## Step 2: Vector PDFs — Figma exports\n\nFigma's \"Export → PDF\" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel. The script then rounds path coordinates to 3 decimals and shrinks sampled gradients to 13x13, which halves the menubar icons again for at most 4/255 of difference at @1x-@3x (see the constants atop the script).\n\nWhat Figma adds that AppKit doesn't need:\n\n1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:\n   - the page `Resources/ColorSpace` dict\n   - every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)\n   - every Image XObject's direct `/ColorSpace` key\n   - every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)\n2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.\n3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags (\"Document\" / \"Part\" structural roles). AppKit's PDF renderer ignores them.\n4. **`/Info` dict** — `Producer=\"Figma\"`, `Title=\"Menubar 22x22@1x white\"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.\n5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.\n6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.\n\nUse the script:\n\n```sh\npython3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf\n```\n\nIt edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.\n\nVerify each file still renders by sips'ing it back to PNG and eyeballing:\n\n```sh\nfor f in resources/icons/menubar/*.pdf; do\n  sips -s format png \"$f\" --out \"/tmp/$(basename $f .pdf).png\" -Z 300 >/dev/null 2>&1\ndone\n```\n\nOpen the PNGs in Preview to confirm nothing visual changed.\n\n## Step 3: SF Symbols via font subset\n\nEvery SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.\n\nHow it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = \"\"`. At runtime, `NSFont(name: \"SF Pro Text\", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.\n\nTo add a new SF Symbol:\n\n1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)\n2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = \"􀝥\"  // SF Symbol name`.\n3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).\n4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.\n5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.\n\nThe script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.\n\n### Historical note: SF Symbols via PDF (deprecated, scripts removed)\n\nA previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):\n\n- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.\n- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:\n  `NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform(\"vectorGlyph\") (CUINamedVectorGlyph) → .perform(\"CGPath\") (real CGPath)`.\n- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.\n- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.\n\nGit: see commit `990c1e79` (\"feat: pro improve assets\") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.\n\n## Step 4: Raster → HEIC at q50\n\nFor anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.\n\nPipeline (built-in to macOS via `sips`):\n\n```sh\nsips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic\n```\n\n- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.\n- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.\n\nUse the script for batch conversion:\n\n```sh\nbash scripts/assets/encode_heic.sh ~/Desktop 1000 50\n```\n\nThat walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.\n\n### Visual review (mandatory)\n\nBefore swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:\n\n```sh\nsips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1\nopen /tmp/sample-decoded.png /Users/you/Desktop/sample.png\n```\n\nPick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:\n\n- No banding in flat color regions\n- Text edges still crisp at native display size\n- No haloing around anti-aliased edges\n- Color rendition matches\n\nIf anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.\n\n### Bumping quality\n\nIf q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.\n\n## Step 5: pbxproj registration\n\nWhenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:\n\n1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.\n2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = \"name.ext\"`.\n3. **PBXGroup section** — the file's entry inside its parent group's `children`.\n4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.\n\nFor pure extension swaps (no new files), `sed -i '' 's|old\\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c \"import secrets; print(secrets.token_hex(12).upper())\"`) and insert in all four places.\n\nIf you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with \"missing file\" or ships dangling references.\n\n## Step 6: Verify\n\n```sh\nbash ai/build.sh        # must show ** BUILD SUCCEEDED **\nbash ai/run.sh          # launch the app and visually inspect every asset\n```\n\nWalk through every UI surface that loads an asset:\n\n- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)\n- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)\n- Permissions window — open by revoking a permission\n- Preferences → Appearance — illustration thumbnails change per show/hide row\n\nCompare `git diff --stat` before committing. Asset replacements should net negative on bundle size.\n\n## Reporting\n\nAfter the run, report:\n\n- File-by-file before/after sizes for everything that changed.\n- Total bundle delta in KB.\n- Any files left untouched and why (e.g., `app.icns` — bundle-required format).\n- The encoder settings used (especially HEIC quality if not q50, so the next person knows).\n- Anything the visual review revealed (e.g., \"had to bump to q60 for `thumbnails_dark` because gradient banding at q50\").\n","CLAUDE.md":"@include ./AGENTS.md\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# macOS development\n- Don't use xcode directly to develop\n- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.\n- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.\n- Use guard closes as much as possible to separate the happy-path under them\n- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)\n- When possible, follow the triad pattern: specs in *Specs.md, unit-tests in *Tests.swift, and *swift for the implementation. Document features and their edge-cases this way\n- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O. Use observer APIs; don't poll.\n\n# Main-thread IPC\nOrdinary AppKit calls can be a synchronous XPC round trip, so they can hang for as long as the other process takes. `searchField.stringValue = \"\"` froze a user's main thread for 3.0s (#5981): it resigned first responder, which deactivates the system text-input context, which timed out. Assume any AppKit call that changes first responder, orders a window, or resizes one talks to another process.\n- Everything under `NSResponder` and `NSCell` (so `NSWindow`, `NSView`, `NSControl`, `NSTextField`, `NSApplication`) plus `NSTextInputContext` is main-thread-only, and AppKit asserts on it at runtime. For those the levers are ordering and deferral, not dispatch: the three off-main schedulers (`AXCallScheduler`, `CGSCallScheduler`, `ProcessCallScheduler`) can't take them.\n- `NSWorkspace` and `NSRunningApplication` are the documented exception: their headers state they are thread safe, so LaunchServices reads (`runningApplications`, `frontmostApplication`, `bundleURL`, `icon`, `activate`) may run off-main, and `Application.fetchAppIcon` already relies on that. Don't \"fix\" them back onto main. `src/main-thread-ipc.md` has the audited call sites and the evidence for both halves of this.\n- On the latency-critical paths (summoning, a keystroke while cycling or searching, dismissal with focus), nothing that can stall may run before the work the user is waiting for. Put the visible work first; put the bookkeeping after it.\n- A CoreAnimation transaction commits when the runloop turn ENDS. Setting `alphaValue` or calling `orderOut` does not reach the screen before that, so blocking later in the same turn keeps the old frame up. Defer with `DispatchQueue.main.async` and re-check the state in the block.\n- `MainThreadStall.step()` at the top of a main-thread function names it in the log when it runs long. Use it to measure rather than guessing; temporarily lower `thresholdInMs` and add marks around single statements to attribute a cost.\n\n# Comments\nA wrong comment costs several times more than a missing one, for humans and agents alike. So write for low drift, not for low line count. A correct, non-obvious comment can be as long as it needs to be.\n- Comment what the code cannot show: OS/API behaviour (macOS, CGS, SkyLight, AppKit), measured timings, private-API notes, invariants, and why a guard that looks removable isn't. Prefer measured evidence over recollection.\n- Don't comment what the code already says. Don't narrate history: no \"this used to be X\", no reverted approaches, no commentary on earlier comments. If a past bug is the reason a constraint exists, name the test that pins it instead.\n- State each rule once, at the widest scope it applies to. Restating it further down is what makes two copies drift apart.\n- One intent per comment, and keep it short. Split a paragraph that mixes a rule, its history, and an aside.\n- When you change code, re-read the comments around it. Updating them is part of the change, not a follow-up.\n- Run `/rework-comments` to audit or rework comments across a file, a folder, or all of `src/`.\n\n# Workflow\n- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change\n- Run the unit tests with ai/test.sh. Don't use scripts/run_tests.sh: that's the CI entrypoint and its Release config rebuilds the whole test target on every edit (34s vs 1.3s)\n- In a git worktree, copy `config/local.xcconfig` over from the main checkout before building. It's gitignored, so a fresh worktree doesn't have it, and `debug.xcconfig` / `release.xcconfig` `#include?` it. Without it the build dies at CodeSign (\"no identity found\") on a dependency framework, BEFORE the app target's own Swift compile — so it looks like the code compiled when nothing of `src/` was checked. `ai/test.sh` doesn't cover that either: the `unit-tests` target has its own source list and never builds the app target\n- Git commit messages must respect our pre-hook conventions, and must be clear and high-level, written for end-users (changelog)\n\n# License / Keychain invariant\n- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or `kSecAttrAccessGroup` with a stable group identifier).\n- Do not introduce legacy `SecKeychain*` API or `kSecAccessControl` (biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.\n","isInternal":false,"tokens":1267,"sizeBytes":5444},{"name":"SKILL.md","path":".claude/skills/assets-optimization/SKILL.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/.claude/skills/assets-optimization/SKILL.md","title":"assets-optimization","category":"anthropic-skill","format":"markdown","content":"---\nname: assets-optimization\ndescription: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.\n---\n\n# /assets-optimization — AltTab asset audit and optimization\n\n## Goal\n\nEvery byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.\n\nThis skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.\n\n## When to use\n\n- A designer drops new exports into `~/Desktop/` or `resources/`.\n- Someone asks \"why is the bundle so big?\".\n- After adding a new icon, illustration, app icon variant, or menubar variant.\n- Periodic audit when nothing else is broken.\n\n## Step 1: Inventory\n\nRun a one-shot enumeration so you know what you're working with:\n\n```sh\nfind resources -type f \\( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \\) \\\n  -exec ls -la {} \\; | awk '{printf \"%8d  %s\\n\", $5, $9}' | sort -k2\n```\n\nGroup what you see by directory. For AltTab the relevant buckets are:\n\n- `resources/icons/menubar/` — small template icons shown in the macOS menubar.\n- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).\n- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).\n- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch** — `.icns` is required by the bundle and Apple's tooling produces near-optimal output already.\n- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).\n\nFor each asset, decide what category it falls into:\n\n| Source content | Right format | Why |\n|---|---|---|\n| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |\n| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |\n| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |\n| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |\n| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |\n\nIf an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.\n\n## Step 2: Vector PDFs — Figma exports\n\nFigma's \"Export → PDF\" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel. The script then rounds path coordinates to 3 decimals and shrinks sampled gradients to 13x13, which halves the menubar icons again for at most 4/255 of difference at @1x-@3x (see the constants atop the script).\n\nWhat Figma adds that AppKit doesn't need:\n\n1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:\n   - the page `Resources/ColorSpace` dict\n   - every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)\n   - every Image XObject's direct `/ColorSpace` key\n   - every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)\n2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.\n3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags (\"Document\" / \"Part\" structural roles). AppKit's PDF renderer ignores them.\n4. **`/Info` dict** — `Producer=\"Figma\"`, `Title=\"Menubar 22x22@1x white\"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.\n5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.\n6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.\n\nUse the script:\n\n```sh\npython3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf\n```\n\nIt edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.\n\nVerify each file still renders by sips'ing it back to PNG and eyeballing:\n\n```sh\nfor f in resources/icons/menubar/*.pdf; do\n  sips -s format png \"$f\" --out \"/tmp/$(basename $f .pdf).png\" -Z 300 >/dev/null 2>&1\ndone\n```\n\nOpen the PNGs in Preview to confirm nothing visual changed.\n\n## Step 3: SF Symbols via font subset\n\nEvery SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.\n\nHow it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = \"\"`. At runtime, `NSFont(name: \"SF Pro Text\", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.\n\nTo add a new SF Symbol:\n\n1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)\n2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = \"􀝥\"  // SF Symbol name`.\n3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).\n4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.\n5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.\n\nThe script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.\n\n### Historical note: SF Symbols via PDF (deprecated, scripts removed)\n\nA previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):\n\n- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.\n- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:\n  `NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform(\"vectorGlyph\") (CUINamedVectorGlyph) → .perform(\"CGPath\") (real CGPath)`.\n- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.\n- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.\n\nGit: see commit `990c1e79` (\"feat: pro improve assets\") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.\n\n## Step 4: Raster → HEIC at q50\n\nFor anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.\n\nPipeline (built-in to macOS via `sips`):\n\n```sh\nsips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic\n```\n\n- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.\n- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.\n\nUse the script for batch conversion:\n\n```sh\nbash scripts/assets/encode_heic.sh ~/Desktop 1000 50\n```\n\nThat walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.\n\n### Visual review (mandatory)\n\nBefore swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:\n\n```sh\nsips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1\nopen /tmp/sample-decoded.png /Users/you/Desktop/sample.png\n```\n\nPick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:\n\n- No banding in flat color regions\n- Text edges still crisp at native display size\n- No haloing around anti-aliased edges\n- Color rendition matches\n\nIf anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.\n\n### Bumping quality\n\nIf q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.\n\n## Step 5: pbxproj registration\n\nWhenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:\n\n1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.\n2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = \"name.ext\"`.\n3. **PBXGroup section** — the file's entry inside its parent group's `children`.\n4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.\n\nFor pure extension swaps (no new files), `sed -i '' 's|old\\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c \"import secrets; print(secrets.token_hex(12).upper())\"`) and insert in all four places.\n\nIf you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with \"missing file\" or ships dangling references.\n\n## Step 6: Verify\n\n```sh\nbash ai/build.sh        # must show ** BUILD SUCCEEDED **\nbash ai/run.sh          # launch the app and visually inspect every asset\n```\n\nWalk through every UI surface that loads an asset:\n\n- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)\n- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)\n- Permissions window — open by revoking a permission\n- Preferences → Appearance — illustration thumbnails change per show/hide row\n\nCompare `git diff --stat` before committing. Asset replacements should net negative on bundle size.\n\n## Reporting\n\nAfter the run, report:\n\n- File-by-file before/after sizes for everything that changed.\n- Total bundle delta in KB.\n- Any files left untouched and why (e.g., `app.icns` — bundle-required format).\n- The encoder settings used (especially HEIC quality if not q50, so the next person knows).\n- Anything the visual review revealed (e.g., \"had to bump to q60 for `thumbnails_dark` because gradient banding at q50\").\n","frontmatter":{"name":"assets-optimization","description":"Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit."},"isInternal":false,"tokens":3467,"sizeBytes":13697},{"name":"SKILL.md","path":".claude/skills/audit-specs-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/.claude/skills/audit-specs-tests/SKILL.md","title":"audit-specs-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: audit-specs-tests\ndescription: Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite.\n---\n\n# /audit-specs-tests\n\n## Project context\n\nAltTab co-locates tests and a living spec next to the source they cover. The convention (see the plan in `.claude/plans/` and `src/pro/license/` as a worked example) is a **triad** per concept:\n\n```\nsrc/<feature>/\n├── Foo.swift          # production / kernel (app target, or app+test for kernels)\n├── FooSpecs.md        # the living spec (not compiled)\n└── FooTests.swift     # XCTest (unit-tests target only)\n```\n\nThe spec is **documentation tied 1:1 to the test methods**. Its `## Test scenarios` section lists every test, grouped, as:\n\n```markdown\n### A. <Group name>\n- **testAFooDoesBar** — one-line description of what it pins.\n```\n\nThis skill's job is to keep the three in sync: every `func test…` ⇔ exactly one `- **test…** —` spec line, descriptions roughly agree, and no triad is missing a leg.\n\n## What counts as a triad\n\nPair by base name in the **same folder**: `<Base>Tests.swift` ⇔ `<Base>Specs.md`. A `<Base>.swift` (or a kernel like `SelectionResolver.swift`, `SearchTestable.swift`) in the folder is the code under test, but a spec may legitimately cover several classes (e.g. `LicenseManagerSpecs.md` documents `Clock`/`Keychain`/`LicenseAPI` too) — so a missing same-name `.swift` is a note, not an error.\n\n## Workflow\n\n1. **Enumerate.** Find all test and spec files:\n   ```sh\n   find src -name '*Tests.swift' | sort\n   find src -name '*Specs.md' | sort\n   ```\n   Also list any test files still under `unit-tests/` and flag them as **not yet migrated** (the target end-state is zero test files outside `src/`).\n\n2. **For each `*Tests.swift`, extract the test methods and their doc-comments.** Test names:\n   ```sh\n   grep -nE '^\\s*func test[A-Za-z0-9_]+\\(' src/<path>/FooTests.swift\n   ```\n   For each, capture the immediately-preceding `///` or `//` comment lines (the test's intent) and the enclosing `// MARK: -` group. Note methods wrapped in `#if DEBUG`.\n\n3. **For each `*Specs.md`, parse the scenario index.** In the `## Test scenarios` section, collect every `### <Group>` heading and every `- **testName** — description` bullet. Build the set of spec-listed test names and their descriptions.\n\n4. **Cross-check each triad and classify findings:**\n   - **Orphan test** — `func testX` exists but no `- **testX** —` line in the spec. (Most common drift; spec is stale.)\n   - **Orphan scenario** — spec lists `testX` but no `func testX` in the suite. (Test deleted/renamed; spec stale.)\n   - **Group mismatch** — a test's `// MARK:` group and its spec `###` group disagree (readability drift, not fatal).\n   - **Description drift** — the spec bullet and the test's doc-comment describe materially different behavior. Judge semantically, don't string-match; only flag real divergence.\n   - **Unpaired file** — a `*Tests.swift` with no `*Specs.md` (or vice-versa).\n\n5. **Lighter code check (kernels only).** For decision-kernel files (`*Testable.swift`, `*Resolver.swift`), list `public`/`static` entry-point functions and `enum … Decision` cases, and flag any with no scenario referencing them — a hint that a branch is undocumented/untested. Skip this for AppKit-coupled production files (intentionally not unit-tested here).\n\n6. **Optionally repair.** If asked, regenerate a spec's `## Test scenarios` section from the live test methods + their doc-comments (preserving the existing `### Group` order where it matches `// MARK:` sections). Never invent behavior — only restate what the test asserts. Do not edit `*Tests.swift` from this skill.\n\n## Reporting\n\n- A per-triad status table: `folder/Base` · #tests · #scenarios · status (✅ in sync / ⚠️ drift / ❌ unpaired).\n- A punch list grouped by finding type (orphan tests, orphan scenarios, description drift, unpaired, undocumented kernel branches), each with the file + line.\n- The count of test files still outside `src/` (migration progress).\n- If nothing is wrong, say so plainly. Then offer to fix the drift (regenerate stale spec scenario indexes) and list exactly which files would change.\n","frontmatter":{"name":"audit-specs-tests","description":"Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite."},"isInternal":false,"tokens":1189,"sizeBytes":4647},{"name":"SKILL.md","path":".claude/skills/coverage-explore/SKILL.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/.claude/skills/coverage-explore/SKILL.md","title":"coverage-explore","category":"anthropic-skill","format":"markdown","content":"---\nname: coverage-explore\ndescription: Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next.\n---\n\n# /coverage-explore\n\n## Project context\n\nAltTab follows the **Humble Object** pattern: logic lives in dependency-free **decision kernels**\n(`SelectionResolver.swift`, `SearchModeResolver.swift`, the `*Testable.swift` files) compiled into the\n`unit-tests` target and tested in isolation; the AppKit layer (windows, views, event taps) is kept thin\nand is deliberately *not* unit-tested. So coverage is meaningful **per kernel / per feature-source** —\nthose should trend to ~100%, every decision branch exercised — and an aggregate app-wide % is not a\ngoal.\n\n**Coverage is documentation here, not a build artifact.** The only stored coverage is:\n1. the per-feature badge (`> **Line coverage:** …`) atop each `*Specs.md`, and\n2. the root **`src/coverage.md`** table.\n\nThere is intentionally **nothing else**: no lcov, no HTML, no committed `.xcresult`, and coverage is\n**not** gathered on routine runs (`scripts/run_tests.sh`, CI, or Xcode) — only when this skill runs it.\n(Coverage isn't usefully viewable in IntelliJ, so there's no IDE-integration artifact to maintain.)\n\n## Workflow\n\n1. **Measure into a throwaway bundle** (then delete it — store nothing technical):\n   ```sh\n   rm -rf /tmp/altcov.xcresult\n   set -o pipefail && xcodebuild test \\\n     -project alt-tab-macos.xcodeproj -scheme Test -configuration Debug \\\n     -enableCodeCoverage YES -resultBundlePath /tmp/altcov.xcresult | scripts/xcbeautify\n   ```\n   Debug, like `ai/test.sh` — Release means wholemodule + -O, which both rebuilds the whole test\n   target on every edit and lets the optimizer merge away counter regions, so its percentages are\n   less faithful to the source.\n   If the build is red, stop and report — coverage on a failing suite is meaningless.\n\n2. **Parse** `xcrun xccov view --report --json /tmp/altcov.xcresult`. Each `targets[].files[]` has\n   `path`, `lineCoverage`, `coveredLines`, `executableLines`, `functions[]`. For line-by-line counts of\n   one file while investigating a gap: `xcrun xccov view --file <abs-path> /tmp/altcov.xcresult`.\n\n3. **Refresh `src/coverage.md`.** Rewrite the table with every `src/` file (excluding `*Tests.swift`)\n   that the test target compiles, sorted worst-coverage-first, as `| NN% (cov/exec) | \\`path\\` |`.\n   Keep the header note explaining the Humble-Object scope and that this + the badges are the only\n   stored coverage. Update the \"updated\" date.\n\n4. **Offer to refresh the per-spec badges.** For each `<Base>Specs.md` whose `<Base>.swift` (or kernel)\n   is in the report, propose updating its `> **Line coverage:** …` line with the new % + today's date.\n   Ask first; only apply the ones the user accepts. Keep honest notes where a file is intentionally\n   partial (e.g. `PreferencesMigrations.swift`).\n\n5. **Rank gaps & suggest tests.** For kernels below 100%, list the uncovered functions/regions, map each\n   to the behavior it represents (read the source), and write ready-to-paste additions: a\n   `- **testGroupScenario** — …` spec line + a short XCTest stub in that suite's builder style. Don't add\n   tests automatically unless asked.\n\n6. **Clean up:** `rm -rf /tmp/altcov.xcresult`. Leave no technical coverage files in the tree.\n\n## Reporting\n\n- Headline: a small ascending table of kernel `lineCoverage`, and the delta vs the previous `src/coverage.md`.\n- Ranked gap list: file → uncovered function/branch → the behavior it represents.\n- Suggested scenarios (spec lines + test stubs) for the top gaps.\n- State exactly what was rewritten (`src/coverage.md`, which badges). Flag files that are partial *by\n  design* (AppKit glue, excluded migrations) so a low number isn't mistaken for a problem.\n","frontmatter":{"name":"coverage-explore","description":"Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next."},"isInternal":false,"tokens":1041,"sizeBytes":4235},{"name":"SKILL.md","path":".claude/skills/rework-comments/SKILL.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/.claude/skills/rework-comments/SKILL.md","title":"rework-comments","category":"anthropic-skill","format":"markdown","content":"---\nname: rework-comments\ndescription: Audit and rework code comments across AltTab so they help an AI agent instead of misleading it. Triages every comment by whether its claim is verifiable and non-derivable, deletes process narration and repeated lessons, shrinks history to test names, and flags comments that have drifted out of sync with the code. Use for a whole-codebase pass, for one file or folder that feels over-commented, after a big refactor, or whenever an agent has been misled by a comment.\n---\n\n# /rework-comments\n\nRework comments so the ones that survive are the ones an agent can act on. Runs on the whole of `src/`, or on a path you pass as an argument.\n\n## Why this exists (the evidence)\n\nMeasured on recent models, including Claude Code on Sonnet 4.6:\n\n- **Wrong beats absent, by a lot.** Misleading comments cost ~13.5pp on code-reasoning benchmarks; *removing* comments costs 2.4–8.2pp ([CodeCrash](https://arxiv.org/pdf/2504.14119)). A stale comment is 2–6x more expensive than no comment. It also inflates reasoning tokens 2–3x.\n- **Volume is not the lever.** 660 Claude Code trials on minimal-pair repos found no pass-rate difference from cleanliness, and a dedicated comment-volume ablation showed comment lines are *not* what drives agent token cost or file re-reads ([Trivedi & Schmitt, SonarSource 2026](https://arxiv.org/abs/2605.20049)).\n- **Short and single-intent beats long and mixed.** Across 80k translations, ~5-word descriptive comments outperformed 19-word author comments that mixed several intents ([study](https://co-r-e.com/method/code-comments-translation)).\n- **Repetition and conflict cost attention.** Anthropic cut 80%+ of Claude Code's system prompt with no eval loss, and names redundancy across sources and conflicting statements as the specific costs ([Claude 5 context engineering](https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models)).\n\nSo: **do not cut on line count. Cut on drift surface.** The goal is fewer claims that can go stale, not fewer characters. A correct, non-obvious comment stays no matter how long it is.\n\n## The triage rule\n\nFor each comment, ask two questions in order.\n\n**1. Can a reader verify this claim from the code in front of them?**\n\n- **No, and it is a fact about the outside world** (macOS / CGS / SkyLight / AppKit behaviour, an OS timing quirk, a measured capture, an API contract) → **KEEP**. This is the irreplaceable category. It cannot be re-derived without reproducing the bug.\n- **No, and it is a fact about our own history or process** (\"this used to be X\", \"we tried Y and reverted\", \"this comment used to say\", \"the lesson written three times in this file\") → **CUT**. An agent cannot act on it, and it reads as a live claim about current code.\n- **Yes, it restates what the code says** → **CUT**, unless it names a consequence the code doesn't show.\n\n**2. What breaks if it goes?**\n\n- *\"Someone re-simplifies this and reintroduces a bug\"* → **KEEP, but shrink to the guardrail.** One sentence for the constraint, plus the test name that enforces it. The test is what actually holds the line; the prose only needs to point at it.\n- *\"Nothing\"* → **CUT.**\n\nEverything else is a judgment call, and the default on a judgment call is keep. A wrongly-deleted true comment costs more than a kept mediocre one.\n\n## Categories, in priority order\n\nWork these in order. The first is the one that pays.\n\n**P1 — Drifted comments (the only category that hurts correctness).**\nA comment whose claim contradicts the code beside it, names a symbol that no longer exists, describes a branch that moved, or states an ordering the code no longer has. Fix the comment, or delete it if the fact is gone. **Never fix by changing the code to match the comment** — report that as a possible bug instead and leave both alone.\n\n**P2 — Repeated lessons.**\nThe same rule stated in more than one place in a file (or across sibling files). Keep the clearest statement at the highest-scope declaration (the `enum`/`struct`/file header), delete the restatements. If a restatement carries a detail the canonical one lacks, move the detail up rather than keeping both.\n\n**P3 — Process narration and meta-commentary.**\nAbandoned approaches, \"this was tried and reverted\", commentary about what a previous version of the comment said, apologies, self-aware asides about the comment itself. If a reverted approach is genuinely load-bearing (someone will obviously retry it), compress to one clause: `// tried pairing with newlyDiscovered; breaks testX`.\n\n**P4 — Restatement of the code.**\n`// increment the counter` above `count += 1`. Delete outright. Per AGENTS.md, if a block needs a comment to be followable, prefer splitting it into a named sub-method.\n\n**P5 — Structurally broken comments.**\nBullet lists interrupted mid-flow by a paragraph so the trailing bullets appear to belong to a different list; comments whose indentation detached them from the statement they explain; doc comments that drifted above the wrong declaration. Partial reads of these give wrong answers, so they behave like P1.\n\n## What always survives\n\nDo not touch these, even in a file that is 2:1 comments to code:\n\n- Invariants and preconditions stated once at the top of a type (e.g. the fullscreen Space invariant in `TabGroupResolver.swift`).\n- Non-derivable OS/API behaviour, especially with measured evidence (`the OS creates a new tab at 0×0 and sizes it ~640ms later`).\n- Private-API notes, `#`-issue references, and undocumented-behaviour warnings.\n- Doc comments on kernel entry points that the matching `*Specs.md` refers to.\n- The reason a guard exists, when the guard looks removable without it.\n- Licence headers, `// MARK:` structure, `swiftlint` directives.\n\n## Workflow\n\n1. **Scope and baseline.** Default to `src/`; use the path argument if given.\n   ```sh\n   find src -name '*.swift' -not -path '*_test-support*' | wc -l\n   ```\n   Measure comment-to-code ratio per file and repo-wide:\n   ```sh\n   for f in $(find src -name '*.swift' -not -path '*_test-support*'); do\n     awk -v F=\"$f\" '{ if ($0 ~ /^[[:space:]]*(\\/\\/|\\/\\*|\\*)/) c++; else if ($0 !~ /^[[:space:]]*$/) k++ }\n       END { if (k > 0) printf \"%.2f %d %d %s\\n\", c/k, c, k, F }' \"$f\"\n   done | sort -rn | head -40\n   ```\n   Report the repo baseline ratio and the outliers. **The ratio picks reading order, it is not a target.** A 3:1 file of measured OS facts is fine; a 0.4:1 file of stale restatement is not.\n\n2. **Read whole files, never grep for comments in isolation.** Every triage question is about the comment *against its code*. A comment excerpt cannot be judged.\n\n3. **Triage per file.** Walk the comments top to bottom, tag each P1–P5 or KEEP. For P1, verify the drift by reading the code the comment describes — if you cannot confirm it drifted, it stays.\n\n4. **Apply.** Edit comments only. One file at a time. On a whole-codebase run, checkpoint progress to `.claude/scratch/rework-comments-progress.md` (file · status · findings) after each file, so an interrupted run resumes without re-reading.\n\n5. **Verify nothing but comments changed.** This is the safety property of the whole skill:\n   ```sh\n   git diff -U0 -- '*.swift' | grep -E '^[+-]' | grep -vE '^(\\+\\+\\+|---)' \\\n     | grep -vE '^[+-][[:space:]]*(//|/\\*|\\*|\\*/)' | grep -vE '^[+-][[:space:]]*$'\n   ```\n   This must print nothing. If it prints a line, you changed code — revert that hunk. The one false positive: editing a **trailing** comment (`let x = 2  // note`) rewrites a code line, so it shows up here. Confirm by eye that the code before the `//` is byte-identical, then move on.\n\n6. **Build.** Copy the commands from `ai/build.sh` and run them. Comment-only edits can still break a build (an unterminated block comment, a doc comment detached from its declaration).\n\n7. **Cross-check the specs.** If you touched a kernel's doc comments, run `/audit-specs-tests` for the affected triads — the `*Specs.md` may quote the prose you changed.\n\n## Reporting\n\n- Repo baseline ratio, and a table of the files you reworked: path · before/after comment lines · categories applied.\n- **P1 findings listed separately and first**, each with file:line, the claim, and what the code actually does. These are the ones the user most needs to see, and the ones most likely to indicate a real bug rather than a doc error.\n- Anything you deliberately kept that looks over-commented, with the one-line reason (usually: measured OS behaviour).\n- Any place where the comment and the code disagreed and **the comment looked right** — a suspected bug, left untouched, for the user to decide.\n- If a file is already in good shape, say so and move on. Do not manufacture edits to show effort.\n","frontmatter":{"name":"rework-comments","description":"Audit and rework code comments across AltTab so they help an AI agent instead of misleading it. Triages every comment by whether its claim is verifiable and non-derivable, deletes process narration and repeated lessons, shrinks history to test names, and flags comments that have drifted out of sync with the code. Use for a whole-codebase pass, for one file or folder that feels over-commented, after a big refactor, or whenever an agent has been misled by a comment."},"isInternal":false,"tokens":2162,"sizeBytes":8754},{"name":"SKILL.md","path":".claude/skills/translate-missing-l10n/SKILL.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/.claude/skills/translate-missing-l10n/SKILL.md","title":"translate-missing-l10n","category":"anthropic-skill","format":"markdown","content":"---\nname: translate-missing-l10n\ndescription: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.\n---\n\n# /translate-missing-l10n\n\n## Project context\n\nAltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.\n\n## Target languages (20)\n\n`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`\n\n(`en` is the source — handled automatically by the apply step.)\n\n## Translation guidelines\n\n- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\\n`, `\\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@` ≡ `%1$@ %2$@`), but the count must match.\n- **Do not translate proper nouns**: \"AltTab\", \"macOS\", \"Mission Control\", \"Spaces\", \"Dock\".\n- **Do not translate modifier key names**: \"Cmd\", \"Option\", \"Alt\", \"Shift\", \"Ctrl\", \"Control\", \"Fn\", \"Command\".\n- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale (\"Réglages\" vs \"Préférences\" in French) over a literal translation.\n- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.\n- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.\n- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.\n\n## Workflow\n\n1. **Refresh the source.** Run:\n   ```sh\n   bash scripts/l10n/extract_l10n_strings.sh\n   ```\n   This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.\n\n2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:\n   ```\n   /* engineer comment */\n   \"key\" = \"value\";\n   ```\n   Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.\n\n3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `\"key\" = \"translation\";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.\n\n4. **Stop if nothing is missing.** Report and exit.\n\n5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.\n\n6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:\n   ```json\n   {\n     \"fr\": { \"About %@\": \"À propos de %@\", \"Quit\": \"Quitter\" },\n     \"ja\": { \"About %@\": \"%@について\" }\n   }\n   ```\n   Then run:\n   ```sh\n   npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json\n   ```\n   The helper:\n   - Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.\n   - Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `\"key\" = \"key\";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.\n   - Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.\n   - Exits with code `2` if any entries were rejected.\n\n7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.\n\n## Reporting\n\nAfter the run, report:\n\n- Number of languages processed.\n- Total translations produced and merged.\n- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.\n- Any format-specifier rejections that required manual fixes.\n","frontmatter":{"name":"translate-missing-l10n","description":"Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`."},"isInternal":false,"tokens":1184,"sizeBytes":4807},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/lwouis/alt-tab-macos/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@include ./AGENTS.md\n","isInternal":false,"tokens":6,"sizeBytes":21}],"systemPromptSnippet":"<yakaai_skills repo=\"lwouis/alt-tab-macos\">\n<!-- File: AGENTS.md (Tokens: ~1267 | Category: root-instruction) -->\n# macOS development\n- Don't use xcode directly to develop\n- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.\n- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.\n- Use guard closes as much as possible to separate the happy-path under them\n- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)\n- When possible, follow the triad pattern: specs in *Specs.md, unit-tests in *Tests.swift, and *swift for the implementation. Document features and their edge-cases this way\n- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O. Use observer APIs; don't poll.\n\n# Main-thread IPC\nOrdinary AppKit calls can be a synchronous XPC round trip, so they can hang for as long as the other process takes. `searchField.stringValue = \"\"` froze a user's main thread for 3.0s (#5981): it resigned first responder, which deactivates the system text-input context, which timed out. Assume any AppKit call that changes first responder, orders a window, or resizes one talks to another process.\n- Everything under `NSResponder` and `NSCell` (so `NSWindow`, `NSView`, `NSControl`, `NSTextField`, `NSApplication`) plus `NSTextInputContext` is main-thread-only, and AppKit asserts on it at runtime. For those the levers are ordering and deferral, not dispatch: the three off-main schedulers (`AXCallScheduler`, `CGSCallScheduler`, `ProcessCallScheduler`) can't take them.\n- `NSWorkspace` and `NSRunningApplication` are the documented exception: their headers state they are thread safe, so LaunchServices reads (`runningApplications`, `frontmostApplication`, `bundleURL`, `icon`, `activate`) may run off-main, and `Application.fetchAppIcon` already relies on that. Don't \"fix\" them back onto main. `src/main-thread-ipc.md` has the audited call sites and the evidence for both halves of this.\n- On the latency-critical paths (summoning, a keystroke while cycling or searching, dismissal with focus), nothing that can stall may run before the work the user is waiting for. Put the visible work first; put the bookkeeping after it.\n- A CoreAnimation transaction commits when the runloop turn ENDS. Setting `alphaValue` or calling `orderOut` does not reach the screen before that, so blocking later in the same turn keeps the old frame up. Defer with `DispatchQueue.main.async` and re-check the state in the block.\n- `MainThreadStall.step()` at the top of a main-thread function names it in the log when it runs long. Use it to measure rather than guessing; temporarily lower `thresholdInMs` and add marks around single statements to attribute a cost.\n\n# Comments\nA wrong comment costs several times more than a missing one, for humans and agents alike. So write for low drift, not for low line count. A correct, non-obvious comment can be as long as it needs to be.\n- Comment what the code cannot show: OS/API behaviour (macOS, CGS, SkyLight, AppKit), measured timings, private-API notes, invariants, and why a guard that looks removable isn't. Prefer measured evidence over recollection.\n- Don't comment what the code already says. Don't narrate history: no \"this used to be X\", no reverted approaches, no commentary on earlier comments. If a past bug is the reason a constraint exists, name the test that pins it instead.\n- State each rule once, at the widest scope it applies to. Restating it further down is what makes two copies drift apart.\n- One intent per comment, and keep it short. Split a paragraph that mixes a rule, its history, and an aside.\n- When you change code, re-read the comments around it. Updating them is part of the change, not a follow-up.\n- Run `/rework-comments` to audit or rework comments across a file, a folder, or all of `src/`.\n\n# Workflow\n- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change\n- Run the unit tests with ai/test.sh. Don't use scripts/run_tests.sh: that's the CI entrypoint and its Release config rebuilds the whole test target on every edit (34s vs 1.3s)\n- In a git worktree, copy `config/local.xcconfig` over from the main checkout before building. It's gitignored, so a fresh worktree doesn't have it, and `debug.xcconfig` / `release.xcconfig` `#include?` it. Without it the build dies at CodeSign (\"no identity found\") on a dependency framework, BEFORE the app target's own Swift compile — so it looks like the code compiled when nothing of `src/` was checked. `ai/test.sh` doesn't cover that either: the `unit-tests` target has its own source list and never builds the app target\n- Git commit messages must respect our pre-hook conventions, and must be clear and high-level, written for end-users (changelog)\n\n# License / Keychain invariant\n- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or `kSecAttrAccessGroup` with a stable group identifier).\n- Do not introduce legacy `SecKeychain*` API or `kSecAccessControl` (biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.\n\n<!-- File: .claude/skills/assets-optimization/SKILL.md (Tokens: ~3467 | Category: anthropic-skill) -->\n---\nname: assets-optimization\ndescription: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.\n---\n\n# /assets-optimization — AltTab asset audit and optimization\n\n## Goal\n\nEvery byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.\n\nThis skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.\n\n## When to use\n\n- A designer drops new exports into `~/Desktop/` or `resources/`.\n- Someone asks \"why is the bundle so big?\".\n- After adding a new icon, illustration, app icon variant, or menubar variant.\n- Periodic audit when nothing else is broken.\n\n## Step 1: Inventory\n\nRun a one-shot enumeration so you know what you're working with:\n\n```sh\nfind resources -type f \\( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \\) \\\n  -exec ls -la {} \\; | awk '{printf \"%8d  %s\\n\", $5, $9}' | sort -k2\n```\n\nGroup what you see by directory. For AltTab the relevant buckets are:\n\n- `resources/icons/menubar/` — small template icons shown in the macOS menubar.\n- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).\n- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).\n- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch** — `.icns` is required by the bundle and Apple's tooling produces near-optimal output already.\n- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).\n\nFor each asset, decide what category it falls into:\n\n| Source content | Right format | Why |\n|---|---|---|\n| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |\n| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |\n| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |\n| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |\n| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |\n\nIf an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.\n\n## Step 2: Vector PDFs — Figma exports\n\nFigma's \"Export → PDF\" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel. The script then rounds path coordinates to 3 decimals and shrinks sampled gradients to 13x13, which halves the menubar icons again for at most 4/255 of difference at @1x-@3x (see the constants atop the script).\n\nWhat Figma adds that AppKit doesn't need:\n\n1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:\n   - the page `Resources/ColorSpace` dict\n   - every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)\n   - every Image XObject's direct `/ColorSpace` key\n   - every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)\n2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.\n3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags (\"Document\" / \"Part\" structural roles). AppKit's PDF renderer ignores them.\n4. **`/Info` dict** — `Producer=\"Figma\"`, `Title=\"Menubar 22x22@1x white\"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.\n5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.\n6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.\n\nUse the script:\n\n```sh\npython3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf\n```\n\nIt edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.\n\nVerify each file still renders by sips'ing it back to PNG and eyeballing:\n\n```sh\nfor f in resources/icons/menubar/*.pdf; do\n  sips -s format png \"$f\" --out \"/tmp/$(basename $f .pdf).png\" -Z 300 >/dev/null 2>&1\ndone\n```\n\nOpen the PNGs in Preview to confirm nothing visual changed.\n\n## Step 3: SF Symbols via font subset\n\nEvery SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.\n\nHow it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = \"\"`. At runtime, `NSFont(name: \"SF Pro Text\", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.\n\nTo add a new SF Symbol:\n\n1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)\n2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = \"􀝥\"  // SF Symbol name`.\n3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).\n4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.\n5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.\n\nThe script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.\n\n### Historical note: SF Symbols via PDF (deprecated, scripts removed)\n\nA previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):\n\n- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.\n- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:\n  `NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform(\"vectorGlyph\") (CUINamedVectorGlyph) → .perform(\"CGPath\") (real CGPath)`.\n- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.\n- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.\n\nGit: see commit `990c1e79` (\"feat: pro improve assets\") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.\n\n## Step 4: Raster → HEIC at q50\n\nFor anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.\n\nPipeline (built-in to macOS via `sips`):\n\n```sh\nsips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic\n```\n\n- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.\n- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.\n\nUse the script for batch conversion:\n\n```sh\nbash scripts/assets/encode_heic.sh ~/Desktop 1000 50\n```\n\nThat walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.\n\n### Visual review (mandatory)\n\nBefore swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:\n\n```sh\nsips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1\nopen /tmp/sample-decoded.png /Users/you/Desktop/sample.png\n```\n\nPick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:\n\n- No banding in flat color regions\n- Text edges still crisp at native display size\n- No haloing around anti-aliased edges\n- Color rendition matches\n\nIf anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.\n\n### Bumping quality\n\nIf q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.\n\n## Step 5: pbxproj registration\n\nWhenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:\n\n1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.\n2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = \"name.ext\"`.\n3. **PBXGroup section** — the file's entry inside its parent group's `children`.\n4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.\n\nFor pure extension swaps (no new files), `sed -i '' 's|old\\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c \"import secrets; print(secrets.token_hex(12).upper())\"`) and insert in all four places.\n\nIf you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with \"missing file\" or ships dangling references.\n\n## Step 6: Verify\n\n```sh\nbash ai/build.sh        # must show ** BUILD SUCCEEDED **\nbash ai/run.sh          # launch the app and visually inspect every asset\n```\n\nWalk through every UI surface that loads an asset:\n\n- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)\n- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)\n- Permissions window — open by revoking a permission\n- Preferences → Appearance — illustration thumbnails change per show/hide row\n\nCompare `git diff --stat` before committing. Asset replacements should net negative on bundle size.\n\n## Reporting\n\nAfter the run, report:\n\n- File-by-file before/after sizes for everything that changed.\n- Total bundle delta in KB.\n- Any files left untouched and why (e.g., `app.icns` — bundle-required format).\n- The encoder settings used (especially HEIC quality if not q50, so the next person knows).\n- Anything the visual review revealed (e.g., \"had to bump to q60 for `thumbnails_dark` because gradient banding at q50\").\n\n<!-- File: .claude/skills/audit-specs-tests/SKILL.md (Tokens: ~1189 | Category: anthropic-skill) -->\n---\nname: audit-specs-tests\ndescription: Audit the AltTab test suite for spec/test/code consistency. For every co-located triad in `src/` (`Foo.swift` + `FooSpecs.md` + `FooTests.swift`), it cross-checks that each XCTest method has a matching scenario line in the spec and vice-versa, flags drift between a scenario's description and the test's doc-comment, and reports orphan tests, orphan scenarios, and unpaired files. Use after adding or editing any `*Tests.swift` / `*Specs.md`, before committing a test change, or to get an overall health check of the suite.\n---\n\n# /audit-specs-tests\n\n## Project context\n\nAltTab co-locates tests and a living spec next to the source they cover. The convention (see the plan in `.claude/plans/` and `src/pro/license/` as a worked example) is a **triad** per concept:\n\n```\nsrc/<feature>/\n├── Foo.swift          # production / kernel (app target, or app+test for kernels)\n├── FooSpecs.md        # the living spec (not compiled)\n└── FooTests.swift     # XCTest (unit-tests target only)\n```\n\nThe spec is **documentation tied 1:1 to the test methods**. Its `## Test scenarios` section lists every test, grouped, as:\n\n```markdown\n### A. <Group name>\n- **testAFooDoesBar** — one-line description of what it pins.\n```\n\nThis skill's job is to keep the three in sync: every `func test…` ⇔ exactly one `- **test…** —` spec line, descriptions roughly agree, and no triad is missing a leg.\n\n## What counts as a triad\n\nPair by base name in the **same folder**: `<Base>Tests.swift` ⇔ `<Base>Specs.md`. A `<Base>.swift` (or a kernel like `SelectionResolver.swift`, `SearchTestable.swift`) in the folder is the code under test, but a spec may legitimately cover several classes (e.g. `LicenseManagerSpecs.md` documents `Clock`/`Keychain`/`LicenseAPI` too) — so a missing same-name `.swift` is a note, not an error.\n\n## Workflow\n\n1. **Enumerate.** Find all test and spec files:\n   ```sh\n   find src -name '*Tests.swift' | sort\n   find src -name '*Specs.md' | sort\n   ```\n   Also list any test files still under `unit-tests/` and flag them as **not yet migrated** (the target end-state is zero test files outside `src/`).\n\n2. **For each `*Tests.swift`, extract the test methods and their doc-comments.** Test names:\n   ```sh\n   grep -nE '^\\s*func test[A-Za-z0-9_]+\\(' src/<path>/FooTests.swift\n   ```\n   For each, capture the immediately-preceding `///` or `//` comment lines (the test's intent) and the enclosing `// MARK: -` group. Note methods wrapped in `#if DEBUG`.\n\n3. **For each `*Specs.md`, parse the scenario index.** In the `## Test scenarios` section, collect every `### <Group>` heading and every `- **testName** — description` bullet. Build the set of spec-listed test names and their descriptions.\n\n4. **Cross-check each triad and classify findings:**\n   - **Orphan test** — `func testX` exists but no `- **testX** —` line in the spec. (Most common drift; spec is stale.)\n   - **Orphan scenario** — spec lists `testX` but no `func testX` in the suite. (Test deleted/renamed; spec stale.)\n   - **Group mismatch** — a test's `// MARK:` group and its spec `###` group disagree (readability drift, not fatal).\n   - **Description drift** — the spec bullet and the test's doc-comment describe materially different behavior. Judge semantically, don't string-match; only flag real divergence.\n   - **Unpaired file** — a `*Tests.swift` with no `*Specs.md` (or vice-versa).\n\n5. **Lighter code check (kernels only).** For decision-kernel files (`*Testable.swift`, `*Resolver.swift`), list `public`/`static` entry-point functions and `enum … Decision` cases, and flag any with no scenario referencing them — a hint that a branch is undocumented/untested. Skip this for AppKit-coupled production files (intentionally not unit-tested here).\n\n6. **Optionally repair.** If asked, regenerate a spec's `## Test scenarios` section from the live test methods + their doc-comments (preserving the existing `### Group` order where it matches `// MARK:` sections). Never invent behavior — only restate what the test asserts. Do not edit `*Tests.swift` from this skill.\n\n## Reporting\n\n- A per-triad status table: `folder/Base` · #tests · #scenarios · status (✅ in sync / ⚠️ drift / ❌ unpaired).\n- A punch list grouped by finding type (orphan tests, orphan scenarios, description drift, unpaired, undocumented kernel branches), each with the file + line.\n- The count of test files still outside `src/` (migration progress).\n- If nothing is wrong, say so plainly. Then offer to fix the drift (regenerate stale spec scenario indexes) and list exactly which files would change.\n\n<!-- File: .claude/skills/coverage-explore/SKILL.md (Tokens: ~1041 | Category: anthropic-skill) -->\n---\nname: coverage-explore\ndescription: Measure AltTab test coverage and record it as documentation. Runs the Test scheme with coverage into a throwaway result bundle, parses the per-file/region report, refreshes the per-feature badge atop each `*Specs.md` and the root `src/coverage.md` table, then suggests the highest-value missing tests. These two markdown outputs are the ONLY stored coverage — no lcov/html/xcresult is kept, and routine test runs don't gather coverage. Use to refresh coverage docs, find untested kernel branches, or decide what to test next.\n---\n\n# /coverage-explore\n\n## Project context\n\nAltTab follows the **Humble Object** pattern: logic lives in dependency-free **decision kernels**\n(`SelectionResolver.swift`, `SearchModeResolver.swift`, the `*Testable.swift` files) compiled into the\n`unit-tests` target and tested in isolation; the AppKit layer (windows, views, event taps) is kept thin\nand is deliberately *not* unit-tested. So coverage is meaningful **per kernel / per feature-source** —\nthose should trend to ~100%, every decision branch exercised — and an aggregate app-wide % is not a\ngoal.\n\n**Coverage is documentation here, not a build artifact.** The only stored coverage is:\n1. the per-feature badge (`> **Line coverage:** …`) atop each `*Specs.md`, and\n2. the root **`src/coverage.md`** table.\n\nThere is intentionally **nothing else**: no lcov, no HTML, no committed `.xcresult`, and coverage is\n**not** gathered on routine runs (`scripts/run_tests.sh`, CI, or Xcode) — only when this skill runs it.\n(Coverage isn't usefully viewable in IntelliJ, so there's no IDE-integration artifact to maintain.)\n\n## Workflow\n\n1. **Measure into a throwaway bundle** (then delete it — store nothing technical):\n   ```sh\n   rm -rf /tmp/altcov.xcresult\n   set -o pipefail && xcodebuild test \\\n     -project alt-tab-macos.xcodeproj -scheme Test -configuration Debug \\\n     -enableCodeCoverage YES -resultBundlePath /tmp/altcov.xcresult | scripts/xcbeautify\n   ```\n   Debug, like `ai/test.sh` — Release means wholemodule + -O, which both rebuilds the whole test\n   target on every edit and lets the optimizer merge away counter regions, so its percentages are\n   less faithful to the source.\n   If the build is red, stop and report — coverage on a failing suite is meaningless.\n\n2. **Parse** `xcrun xccov view --report --json /tmp/altcov.xcresult`. Each `targets[].files[]` has\n   `path`, `lineCoverage`, `coveredLines`, `executableLines`, `functions[]`. For line-by-line counts of\n   one file while investigating a gap: `xcrun xccov view --file <abs-path> /tmp/altcov.xcresult`.\n\n3. **Refresh `src/coverage.md`.** Rewrite the table with every `src/` file (excluding `*Tests.swift`)\n   that the test target compiles, sorted worst-coverage-first, as `| NN% (cov/exec) | \\`path\\` |`.\n   Keep the header note explaining the Humble-Object scope and that this + the badges are the only\n   stored coverage. Update the \"updated\" date.\n\n4. **Offer to refresh the per-spec badges.** For each `<Base>Specs.md` whose `<Base>.swift` (or kernel)\n   is in the report, propose updating its `> **Line coverage:** …` line with the new % + today's date.\n   Ask first; only apply the ones the user accepts. Keep honest notes where a file is intentionally\n   partial (e.g. `PreferencesMigrations.swift`).\n\n5. **Rank gaps & suggest tests.** For kernels below 100%, list the uncovered functions/regions, map each\n   to the behavior it represents (read the source), and write ready-to-paste additions: a\n   `- **testGroupScenario** — …` spec line + a short XCTest stub in that suite's builder style. Don't add\n   tests automatically unless asked.\n\n6. **Clean up:** `rm -rf /tmp/altcov.xcresult`. Leave no technical coverage files in the tree.\n\n## Reporting\n\n- Headline: a small ascending table of kernel `lineCoverage`, and the delta vs the previous `src/coverage.md`.\n- Ranked gap list: file → uncovered function/branch → the behavior it represents.\n- Suggested scenarios (spec lines + test stubs) for the top gaps.\n- State exactly what was rewritten (`src/coverage.md`, which badges). Flag files that are partial *by\n  design* (AppKit glue, excluded migrations) so a low number isn't mistaken for a problem.\n\n<!-- File: .claude/skills/rework-comments/SKILL.md (Tokens: ~2162 | Category: anthropic-skill) -->\n---\nname: rework-comments\ndescription: Audit and rework code comments across AltTab so they help an AI agent instead of misleading it. Triages every comment by whether its claim is verifiable and non-derivable, deletes process narration and repeated lessons, shrinks history to test names, and flags comments that have drifted out of sync with the code. Use for a whole-codebase pass, for one file or folder that feels over-commented, after a big refactor, or whenever an agent has been misled by a comment.\n---\n\n# /rework-comments\n\nRework comments so the ones that survive are the ones an agent can act on. Runs on the whole of `src/`, or on a path you pass as an argument.\n\n## Why this exists (the evidence)\n\nMeasured on recent models, including Claude Code on Sonnet 4.6:\n\n- **Wrong beats absent, by a lot.** Misleading comments cost ~13.5pp on code-reasoning benchmarks; *removing* comments costs 2.4–8.2pp ([CodeCrash](https://arxiv.org/pdf/2504.14119)). A stale comment is 2–6x more expensive than no comment. It also inflates reasoning tokens 2–3x.\n- **Volume is not the lever.** 660 Claude Code trials on minimal-pair repos found no pass-rate difference from cleanliness, and a dedicated comment-volume ablation showed comment lines are *not* what drives agent token cost or file re-reads ([Trivedi & Schmitt, SonarSource 2026](https://arxiv.org/abs/2605.20049)).\n- **Short and single-intent beats long and mixed.** Across 80k translations, ~5-word descriptive comments outperformed 19-word author comments that mixed several intents ([study](https://co-r-e.com/method/code-comments-translation)).\n- **Repetition and conflict cost attention.** Anthropic cut 80%+ of Claude Code's system prompt with no eval loss, and names redundancy across sources and conflicting statements as the specific costs ([Claude 5 context engineering](https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models)).\n\nSo: **do not cut on line count. Cut on drift surface.** The goal is fewer claims that can go stale, not fewer characters. A correct, non-obvious comment stays no matter how long it is.\n\n## The triage rule\n\nFor each comment, ask two questions in order.\n\n**1. Can a reader verify this claim from the code in front of them?**\n\n- **No, and it is a fact about the outside world** (macOS / CGS / SkyLight / AppKit behaviour, an OS timing quirk, a measured capture, an API contract) → **KEEP**. This is the irreplaceable category. It cannot be re-derived without reproducing the bug.\n- **No, and it is a fact about our own history or process** (\"this used to be X\", \"we tried Y and reverted\", \"this comment used to say\", \"the lesson written three times in this file\") → **CUT**. An agent cannot act on it, and it reads as a live claim about current code.\n- **Yes, it restates what the code says** → **CUT**, unless it names a consequence the code doesn't show.\n\n**2. What breaks if it goes?**\n\n- *\"Someone re-simplifies this and reintroduces a bug\"* → **KEEP, but shrink to the guardrail.** One sentence for the constraint, plus the test name that enforces it. The test is what actually holds the line; the prose only needs to point at it.\n- *\"Nothing\"* → **CUT.**\n\nEverything else is a judgment call, and the default on a judgment call is keep. A wrongly-deleted true comment costs more than a kept mediocre one.\n\n## Categories, in priority order\n\nWork these in order. The first is the one that pays.\n\n**P1 — Drifted comments (the only category that hurts correctness).**\nA comment whose claim contradicts the code beside it, names a symbol that no longer exists, describes a branch that moved, or states an ordering the code no longer has. Fix the comment, or delete it if the fact is gone. **Never fix by changing the code to match the comment** — report that as a possible bug instead and leave both alone.\n\n**P2 — Repeated lessons.**\nThe same rule stated in more than one place in a file (or across sibling files). Keep the clearest statement at the highest-scope declaration (the `enum`/`struct`/file header), delete the restatements. If a restatement carries a detail the canonical one lacks, move the detail up rather than keeping both.\n\n**P3 — Process narration and meta-commentary.**\nAbandoned approaches, \"this was tried and reverted\", commentary about what a previous version of the comment said, apologies, self-aware asides about the comment itself. If a reverted approach is genuinely load-bearing (someone will obviously retry it), compress to one clause: `// tried pairing with newlyDiscovered; breaks testX`.\n\n**P4 — Restatement of the code.**\n`// increment the counter` above `count += 1`. Delete outright. Per AGENTS.md, if a block needs a comment to be followable, prefer splitting it into a named sub-method.\n\n**P5 — Structurally broken comments.**\nBullet lists interrupted mid-flow by a paragraph so the trailing bullets appear to belong to a different list; comments whose indentation detached them from the statement they explain; doc comments that drifted above the wrong declaration. Partial reads of these give wrong answers, so they behave like P1.\n\n## What always survives\n\nDo not touch these, even in a file that is 2:1 comments to code:\n\n- Invariants and preconditions stated once at the top of a type (e.g. the fullscreen Space invariant in `TabGroupResolver.swift`).\n- Non-derivable OS/API behaviour, especially with measured evidence (`the OS creates a new tab at 0×0 and sizes it ~640ms later`).\n- Private-API notes, `#`-issue references, and undocumented-behaviour warnings.\n- Doc comments on kernel entry points that the matching `*Specs.md` refers to.\n- The reason a guard exists, when the guard looks removable without it.\n- Licence headers, `// MARK:` structure, `swiftlint` directives.\n\n## Workflow\n\n1. **Scope and baseline.** Default to `src/`; use the path argument if given.\n   ```sh\n   find src -name '*.swift' -not -path '*_test-support*' | wc -l\n   ```\n   Measure comment-to-code ratio per file and repo-wide:\n   ```sh\n   for f in $(find src -name '*.swift' -not -path '*_test-support*'); do\n     awk -v F=\"$f\" '{ if ($0 ~ /^[[:space:]]*(\\/\\/|\\/\\*|\\*)/) c++; else if ($0 !~ /^[[:space:]]*$/) k++ }\n       END { if (k > 0) printf \"%.2f %d %d %s\\n\", c/k, c, k, F }' \"$f\"\n   done | sort -rn | head -40\n   ```\n   Report the repo baseline ratio and the outliers. **The ratio picks reading order, it is not a target.** A 3:1 file of measured OS facts is fine; a 0.4:1 file of stale restatement is not.\n\n2. **Read whole files, never grep for comments in isolation.** Every triage question is about the comment *against its code*. A comment excerpt cannot be judged.\n\n3. **Triage per file.** Walk the comments top to bottom, tag each P1–P5 or KEEP. For P1, verify the drift by reading the code the comment describes — if you cannot confirm it drifted, it stays.\n\n4. **Apply.** Edit comments only. One file at a time. On a whole-codebase run, checkpoint progress to `.claude/scratch/rework-comments-progress.md` (file · status · findings) after each file, so an interrupted run resumes without re-reading.\n\n5. **Verify nothing but comments changed.** This is the safety property of the whole skill:\n   ```sh\n   git diff -U0 -- '*.swift' | grep -E '^[+-]' | grep -vE '^(\\+\\+\\+|---)' \\\n     | grep -vE '^[+-][[:space:]]*(//|/\\*|\\*|\\*/)' | grep -vE '^[+-][[:space:]]*$'\n   ```\n   This must print nothing. If it prints a line, you changed code — revert that hunk. The one false positive: editing a **trailing** comment (`let x = 2  // note`) rewrites a code line, so it shows up here. Confirm by eye that the code before the `//` is byte-identical, then move on.\n\n6. **Build.** Copy the commands from `ai/build.sh` and run them. Comment-only edits can still break a build (an unterminated block comment, a doc comment detached from its declaration).\n\n7. **Cross-check the specs.** If you touched a kernel's doc comments, run `/audit-specs-tests` for the affected triads — the `*Specs.md` may quote the prose you changed.\n\n## Reporting\n\n- Repo baseline ratio, and a table of the files you reworked: path · before/after comment lines · categories applied.\n- **P1 findings listed separately and first**, each with file:line, the claim, and what the code actually does. These are the ones the user most needs to see, and the ones most likely to indicate a real bug rather than a doc error.\n- Anything you deliberately kept that looks over-commented, with the one-line reason (usually: measured OS behaviour).\n- Any place where the comment and the code disagreed and **the comment looked right** — a suspected bug, left untouched, for the user to decide.\n- If a file is already in good shape, say so and move on. Do not manufacture edits to show effort.\n\n<!-- File: .claude/skills/translate-missing-l10n/SKILL.md (Tokens: ~1184 | Category: anthropic-skill) -->\n---\nname: translate-missing-l10n\ndescription: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.\n---\n\n# /translate-missing-l10n\n\n## Project context\n\nAltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.\n\n## Target languages (20)\n\n`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`\n\n(`en` is the source — handled automatically by the apply step.)\n\n## Translation guidelines\n\n- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\\n`, `\\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@` ≡ `%1$@ %2$@`), but the count must match.\n- **Do not translate proper nouns**: \"AltTab\", \"macOS\", \"Mission Control\", \"Spaces\", \"Dock\".\n- **Do not translate modifier key names**: \"Cmd\", \"Option\", \"Alt\", \"Shift\", \"Ctrl\", \"Control\", \"Fn\", \"Command\".\n- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale (\"Réglages\" vs \"Préférences\" in French) over a literal translation.\n- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.\n- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.\n- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.\n\n## Workflow\n\n1. **Refresh the source.** Run:\n   ```sh\n   bash scripts/l10n/extract_l10n_strings.sh\n   ```\n   This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.\n\n2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:\n   ```\n   /* engineer comment */\n   \"key\" = \"value\";\n   ```\n   Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.\n\n3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `\"key\" = \"translation\";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.\n\n4. **Stop if nothing is missing.** Report and exit.\n\n5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.\n\n6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:\n   ```json\n   {\n     \"fr\": { \"About %@\": \"À propos de %@\", \"Quit\": \"Quitter\" },\n     \"ja\": { \"About %@\": \"%@について\" }\n   }\n   ```\n   Then run:\n   ```sh\n   npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json\n   ```\n   The helper:\n   - Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.\n   - Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `\"key\" = \"key\";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.\n   - Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.\n   - Exits with code `2` if any entries were rejected.\n\n7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.\n\n## Reporting\n\nAfter the run, report:\n\n- Number of languages processed.\n- Total translations produced and merged.\n- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.\n- Any format-specifier rejections that required manual fixes.\n\n<!-- File: CLAUDE.md (Tokens: ~6 | Category: claude-rule) -->\n@include ./AGENTS.md\n</yakaai_skills>"}