Compare commits

...
4 Commits
Author SHA1 Message Date
semantic-release-bot 4ecfc4606f chore(release): 100.2.0 [skip ci]
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)

### Features

* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
2026-06-16 21:05:45 +00:00
james.maes 45457cb313 feat(updates): enable Sparkle auto-update via GitHub Pages appcast
Point DOMAIN at the repo's GitHub Pages site (serves the gh-pages appcast.xml), enable SUEnableAutomaticChecks, and remove the in-tree appcast skeleton (feed is published out-of-tree). Ships an auto-updating build.
2026-06-16 16:02:04 -05:00
james.maes 62e0226f01 ci: stop release tail clobbering .fork-sync-state cursor [skip ci]
Reset .fork-sync-state to v11.3.0 (last-synced upstream tag) and remove the release-tail step that overwrote it with the fork release tag, which would stall upstream-sync. Update SESSION-STATE/TODO/CICD-STATUS/README for the live signed+notarized v100.1.0 release.
2026-06-16 15:32:55 -05:00
github-actions[bot] 92254c021f chore(sync-state): advance to v100.1.0 [skip ci] 2026-06-16 19:48:28 +00:00
10 changed files with 68 additions and 78 deletions

No files matched your search

+8 -18
View File
@@ -197,22 +197,12 @@ jobs:
tag_name: ${{ steps.set_release_info.outputs.tag_name }}
body: ${{ steps.set_release_info.outputs.body }}
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
# alt-tab-free [depaywall]: advance the committed last-synced-tag state to the released tag and
# commit it, so the upstream-sync cron syncs the OLDEST upstream tag strictly newer than this —
# not "newest tag minus --is-ancestor". This makes resolve-in-order hold by construction and
# prevents a closed-unmerged sync PR from silently dropping commits (PLAN §4.2). Skipped with
# [skip ci] so it does not re-trigger the workflow.
- name: advance .fork-sync-state to the released tag
if: github.event_name == 'push' && steps.release-gate.outputs.signing-ready == 'true'
run: |
tag="${{ steps.set_release_info.outputs.tag_name }}"
[ -n "$tag" ] || { echo "no tag_name output — skipping .fork-sync-state advance"; exit 0; }
printf '%s\n' "$tag" > .fork-sync-state
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet -- .fork-sync-state; then
git add .fork-sync-state
git commit -m "chore(sync-state): advance to $tag [skip ci]"
git push origin "HEAD:${{ github.ref_name }}"
fi
# alt-tab-free [depaywall]: the release tail intentionally does NOT advance .fork-sync-state.
# That file is the UPSTREAM-sync cursor — the last-synced UPSTREAM tag (e.g. v11.3.0), read by
# upstream_sync.yml to pick "the oldest upstream tag strictly newer than it." The fork versions
# independently (semantic-release cuts v100.x), so a prior revision's step that wrote the FORK
# release tag here set the cursor to v100.x — after which no upstream tag is ever "newer" and
# upstream-sync would silently never fire. The cursor must instead be advanced by the
# upstream-sync merge flow, recording the UPSTREAM tag it synced (to be wired with
# upstream_sync.yml — PLAN §4.2). Until then .fork-sync-state stays at the last real upstream tag.
# alt-tab-free [depaywall]: update_website.sh (lwouis website dispatch) REMOVED (PLAN §3.4/§4.2).
+3
View File
@@ -20,3 +20,6 @@ codesign.p12
!/.claude/skills/
/scripts/l10n/missing-translations.json
/ai/output/
# alt-tab-free [depaywall]: the Sparkle feed is published OUT OF TREE to the gh-pages branch by
# scripts/update_appcast.sh; the in-tree appcast.xml is removed (PLAN §3.3).
/appcast.xml
+2 -2
View File
@@ -64,8 +64,8 @@
Auto-checks stay off (below) until DOMAIN serves a real appcast host. -->
<string>1YfoCJDk/T8GQx1Gpf3O4ApFgDkBiN6oe/z/Kfnxs/M=</string>
<key>SUEnableAutomaticChecks</key>
<!-- alt-tab-free: off until DOMAIN serves a real appcast (avoids failed update checks). -->
<string>false</string>
<!-- alt-tab-free: enabled 2026-06-16 — DOMAIN serves the appcast on GitHub Pages (gh-pages). -->
<string>true</string>
<key>SUScheduledCheckInterval</key>
<string>604800</string>
+5 -5
View File
@@ -18,11 +18,11 @@ license as upstream. See [`LICENCE.md`](LICENCE.md) for the full text and
brew install --cask koftwentytwo/tap/commandtabfree
```
CommandTabFree is **not yet signed or notarized**, so the cask removes the download
quarantine to avoid a Gatekeeper prompt. You can also download the **`.dmg`** (or `.zip`)
from the [Releases](https://github.com/KofTwentyTwo/CommandTabFree/releases) page and drag
CommandTabFree to Applications — since it's unsigned, right-click the app and choose **Open**
on first launch. On first launch, grant **Accessibility** and **Screen Recording** in System
CommandTabFree is **signed and notarized** (as of v100.1.0), so it launches cleanly through
Gatekeeper — no right-click-**Open** workaround needed. Install via the Homebrew cask above, or
download the **`.dmg`** (or `.zip`) from the
[Releases](https://github.com/KofTwentyTwo/CommandTabFree/releases) page and drag CommandTabFree
to Applications. On first launch, grant **Accessibility** and **Screen Recording** in System
Settings. Universal binary (Apple Silicon + Intel), macOS 10.13+.
## Based on AltTab by Louis Pontoise
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" standalone="yes"?>
<!--
alt-tab-free [depaywall]: this in-tree appcast.xml has been TRUNCATED to an empty Sparkle feed
skeleton. The fork publishes its real Sparkle feed OUT OF TREE (the gh-pages branch) via
scripts/update_appcast.sh, so the live entries are NOT served from this tracked file (PLAN §3.3).
OWNER ACTION (before publish): `git rm appcast.xml` and add `/appcast.xml` to .gitignore (the
identity agent owns .gitignore). Serving the feed from the tree would (1) conflict on every
upstream + fork release and (2) re-introduce lwouis-key-signed entries the fork's SUPublicEDKey
cannot verify. It is left present-but-empty only so the tree stays buildable until removed.
-->
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>alt-tab-free</title>
<language>en</language>
</channel>
</rss>
+7
View File
@@ -1,3 +1,10 @@
# [100.2.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.1.0...v100.2.0) (2026-06-16)
### Features
* **updates:** enable Sparkle auto-update via GitHub Pages appcast ([45457cb](https://github.com/KofTwentyTwo/CommandTabFree/commit/45457cb))
# [100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/compare/v100.0.3...v100.1.0) (2026-06-16)
+7 -10
View File
@@ -30,16 +30,13 @@ PRODUCT_BUNDLE_IDENTIFIER = com.koftwentytwo.commandtabfree
CURRENT_PROJECT_VERSION = 0.0.0
// --- Hosts (overrides base.xcconfig :20-21) ---
// OWNER-OVERRIDABLE-BEFORE-PUBLISH: fork-owned hosts.
// TODO: replace fork.invalid with the fork's real web host. DOMAIN must serve
// https://<DOMAIN>/appcast.xml (Sparkle feed) and /support, or update checks /
// the in-app Support affordance 404. fork.invalid is a deliberately NON-routable
// placeholder (RFC 6761 reserved .invalid TLD) so nothing phones home before publish.
DOMAIN = fork.invalid
// TODO: API_DOMAIN drives the user-feedback POST (FeedbackWindow). The activate
// fingerprint POST is closed separately at the handleCustomUrl handler (§2/§3.4),
// so this may be made ROUTABLE for feedback without re-arming exfiltration.
// Decide feedback strategy at bootstrap (§3.4 option a/b/c).
// DOMAIN serves the Sparkle appcast at https://<DOMAIN>/appcast.xml. Set 2026-06-16 to the repo's
// GitHub Pages site (gh-pages branch, published by scripts/update_appcast.sh). A kof22.com custom
// domain can be added later — GitHub redirects the github.io URL, so shipped builds keep updating.
// (/support still 404s here — harmless until a support page exists.)
DOMAIN = koftwentytwo.github.io/CommandTabFree
// API_DOMAIN drives the user-feedback POST (FeedbackWindow) — left NON-routable: feedback is deferred
// (§3.4). The activate fingerprint POST is closed separately at the handleCustomUrl handler.
API_DOMAIN = fork.invalid/api
// --- Code signing (overrides release.xcconfig :5) ---
+2 -2
View File
@@ -1,7 +1,7 @@
# CI/CD readiness — CommandTabFree (fork of lwouis/alt-tab-macos)
**Date:** 2026-06-16 **Branch:** `master` **Repo:** `KofTwentyTwo/CommandTabFree` (public)
**Status:** push CI verified **fully green** on the real runner (run `27634828800`, commit `6070f0cb`).
**Status:** full release pipeline **proven** on the real runner — signed + notarized **v100.1.0** released (run `27643432063`, Developer ID `James Maes (2X834TJ5MA)`, `spctl`-accepted + stapled). Non-release pushes stay green via the ad-hoc build path (run `27634828800`).
**Authoritative design:** `docs/PLAN-maintained-fork.md` §4; owner checklist `docs/EXECUTION-STATUS.md` §3.4/§3.5.
This is the state after the CI-fix pass. The pass applied only **safe, owner-secret-independent**
@@ -57,7 +57,7 @@ Environment.
| Code-sign keychain import | `APPLE_P12_CERTIFICATE` | `setup_ci_master.sh` |
| Notarize + staple | `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` | `package_and_notarize_release.sh` |
| Sparkle appcast sign + publish to gh-pages | `SPARKLE_ED_PRIVATE_KEY` (+ a real `DOMAIN`/feed host) | `update_appcast.sh` (now `--ed-key-file -`) |
| Tag push + GitHub Release | ambient `GITHUB_TOKEN` (auto) — only gated alongside the above so a release isn't cut unsigned | `npx semantic-release`, `softprops/action-gh-release`, `.fork-sync-state` advance |
| Tag push + GitHub Release | ambient `GITHUB_TOKEN` (auto) — only gated alongside the above so a release isn't cut unsigned | `npx semantic-release`, `softprops/action-gh-release` (the release-tail `.fork-sync-state` advance step was **REMOVED** 2026-06-16 — it clobbered the upstream-sync cursor with the fork release tag; the cursor must be advanced by the sync-merge flow instead — see `ci_cd.yml`) |
| Sync PR fires `guard.yml`/`ci_cd.yml` PR checks | `SYNC_BOT_TOKEN` (GitHub App token or fork PAT — NOT default `GITHUB_TOKEN`) | `upstream_sync.yml` (fail-closed if missing) |
> The release tail is **all-or-nothing**: it requires the full Apple set **and** the Sparkle key
+11 -10
View File
@@ -1,18 +1,19 @@
# SESSION-STATE — CommandTabFree (de-paywalled AltTab fork)
_Updated 2026-06-16. `master` carries the fork; **push CI is fully green** (gate + tests + ad-hoc build; release tail skips until owner secrets). Public pre-release **v100.0.3** live; Homebrew tap live._
_Updated 2026-06-16. `master` carries the fork; the **full CI/CD publishing pipeline is LIVE** — signed + notarized **v100.1.0** released and Gatekeeper-accepted. Homebrew tap live._
## ✅ CI GREEN — remaining work is owner-gated only
## ✅ SIGNED + NOTARIZED RELEASE LIVE
**Push CI is fully green** as of 2026-06-16 (run `27634828800`, commit `6070f0cb`). Every owner-secret-independent step passes — Guard A/B/C, commitlint, the genstrings generated-files gate, version compute, `run_tests.sh`, and an **ad-hoc** `build_app.sh`. The entire release tail (notarize / appcast / semantic-release / gh-release / sync-state advance) SKIPS cleanly until the owner adds the Apple/Sparkle secrets — by design (CICD-STATUS bucket A).
The complete release pipeline ran green end-to-end (run `27643432063`, commit `2f9fac17`): cert import → **signed** `build_app.sh` (Developer ID **`James Maes (2X834TJ5MA)`**) → notarize + staple → appcast→gh-pages → semantic-release → GitHub Release. **[v100.1.0](https://github.com/KofTwentyTwo/CommandTabFree/releases/tag/v100.1.0)** is published; the downloaded asset verifies `spctl` = "accepted, source=Notarized Developer ID" and `stapler validate` passes (offline-clean, no quarantine prompt). Non-release pushes stay green via the ad-hoc `build_app.sh` path (run `27634828800`).
Two fixes landed this session:
1. **`fix(l10n)` `d1e1b296`** — regenerated the base `resources/l10n/Localizable.strings` from source. It had drifted after the About redesign + brand sweep; the gate regenerates via `genstrings` and diffs (the file is a pure generated mirror — keys ARE the English source literals, so it can't be hand-branded). The diff was verified byte-for-byte against CI's own Xcode 26.0.1 genstrings output before pushing. Attribution preserved (it's in the source, not hand-edited). Dead paywall strings stay upstream-branded (merge-surface minimal).
2. **`fix(ci)` `6070f0cb`** — the Release build was inheriting upstream's Developer ID (`Developer ID Application: Louis Pontoise (QXD7GW8FHY)`, in `config/release.xcconfig:5`) and failing on CI with exit 65 (no such cert). `build_app.sh` now forwards xcodebuild overrides (`"$@"`), and `ci_cd.yml` builds **ad-hoc** (`CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO OTHER_CODE_SIGN_FLAGS=--timestamp=none`) when the release gate is not signing-ready. Auto-switches to signed when the owner provisions the cert AND sets the fork Developer ID in `config/local.xcconfig` (currently commented, lines 45-50).
**How signing is wired:** `config/local.xcconfig` sets `CODE_SIGN_IDENTITY` + `CODE_SIGN_STYLE = Manual` + `TEAM_ID 2X834TJ5MA`; the vendored Sparkle helpers (`vendor/Sparkle/Helpers/*`) are committed **re-signed** under the same Developer ID; `setup_ci_master.sh` imports the cert with an optional `APPLE_P12_PASSWORD`. The release secrets (`APPLE_P12_CERTIFICATE`/`APPLE_P12_PASSWORD`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID` + `SPARKLE_ED_PRIVATE_KEY`) are set in the `production` env. Getting here also required two CI fixes earlier this session: `d1e1b296` (regenerate base `Localizable.strings` to clear the genstrings gate) and `6070f0cb` (ad-hoc build until the cert existed).
**Owner-gated next steps (NOT automatable; details in EXECUTION-STATUS §3.3–3.5 + CICD-STATUS.md bucket C):** Apple Developer ID → `APPLE_P12_CERTIFICATE`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID` in the `production` env (unblocks signed+notarized releases) **and** set `CODE_SIGN_IDENTITY` in `config/local.xcconfig`; real `DOMAIN`/appcast host to replace `fork.invalid`; `SYNC_BOT_TOKEN` (auto upstream-sync PRs); external cron for `upstream_sync.yml`. The 3 sync labels (`sync`/`conflict`/`chokepoint-refresh`) were created this session. Already set: `SPARKLE_ED_PRIVATE_KEY` + `production` env + Sparkle public key in Info.plist.
**Remaining (NOT yet done):**
- **Auto-update** (deferred by choice): `appcast.xml` is published to gh-pages, but the app's feed URL is still `fork.invalid` and `SUEnableAutomaticChecks=false`. To enable N→N+1 updates: set a real `DOMAIN` (kof22.com), serve gh-pages at `https://<DOMAIN>/appcast.xml`, flip checks on.
- **Homebrew cask**: now notarized — drop the quarantine-strip postflight and bump the cask to v100.1.0.
- **Upstream-sync**: `.fork-sync-state` reset to `v11.3.0` (the release tail no longer clobbers it — see ci_cd.yml). Before enabling upstream-sync, advance the cursor from the sync-merge flow (recording the UPSTREAM tag), and provision `SYNC_BOT_TOKEN` + an external cron. The 3 sync labels exist.
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (renamed from alt-tab-free; local dir is still `~/Git.Local/kof22/alt-tab-free`, SSH remote points at CommandTabFree). Latest release v100.0.3 (zip + dmg). ⌘⇥ app/menu-bar icon is FINAL (owner-approved). master tip is green (functional fixes `d1e1b296` + `6070f0cb`; docs/labels/TODO commits sit on top), working tree clean. Owner-gated checklist: `docs/TODO.md`.
**Repo facts:** GitHub = `KofTwentyTwo/CommandTabFree` (local dir `~/Git.Local/kof22/alt-tab-free`, SSH remote). Latest release **v100.1.0** (signed + notarized zip). ⌘⇥ icon FINAL. Owner-gated checklist: `docs/TODO.md`.
## What this is
@@ -22,8 +23,8 @@ Two fixes landed this session:
- De-paywall + anti-relock tests + frozen identity (`com.koftwentytwo.commandtabfree`) + brand sweep + ⌘⇥ icon (final) — all on **`master`** (was `depaywall-free`, fast-forward merged, HEAD `c7d2d968`, pushed to origin; source is public for GPL §6).
- **Sparkle EdDSA keypair generated.** Public key in `Info.plist` (`SUPublicEDKey`); private key in 1Password item **"CommandTabFree-Sparkle-EdDSA"** + GitHub Actions secret `SPARKLE_ED_PRIVATE_KEY` (env `production`); keychain account `commandtabfree`. `SUEnableAutomaticChecks=false` until a real appcast host exists. This fixed the launch-time "updater failed to start" alert.
- **Public release v100.0.3** (pre-release, **unsigned / not notarized**, universal x86_64+arm64) — ⌘⇥ menu-bar icon, redesigned About (KofTwentyTwo identity + AltTab attribution links), and a **full l10n rebrand** (product name → CommandTabFree across base/en + all ~20 translated languages; upstream attribution kept). Earlier v100.0.0/v100.0.1/v100.0.2 superseded.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) for the unsigned app.
- **CI/CD wired** (GitHub Actions), **fully green on push** (run `27634828800`, commit `6070f0cb`). Passing: Guard A/B/C, commitlint, generated-files gate, version compute, `run_tests.sh`, ad-hoc `build_app.sh`. The sign/notarize/appcast/semantic-release/gh-release tail is gated on a `release-gate` step that **auto-activates** when `APPLE_*` secrets are added (so missing secrets SKIP, not fail). Fixed this session (2026-06-16): regenerated base `Localizable.strings` to clear the genstrings gate; build ad-hoc until the owner provisions the Apple cert (was inheriting upstream's Developer ID → exit 65). Earlier this session: Guard B grep now matches the shipped `guard false else { return .pro }` patch (was checking the PLAN's never-shipped `if false`/`alt-tab-free [depaywall]` form); `update_appcast.sh` → `--ed-key-file`; commitlint sync-merge `ignores`; dropped upstream-only scripts (AppCenter/website); commit headers must be ≤72 chars. Full status: `docs/CICD-STATUS.md`.
- **Homebrew tap live:** `brew install --cask koftwentytwo/tap/commandtabfree` (`KofTwentyTwo/homebrew-tap` → `Casks/commandtabfree.rb`). Cask strips the quarantine (postflight `xattr`) — was needed for the unsigned v100.0.x; now that v100.1.0 is notarized, drop that postflight and bump the cask.
- **CI/CD wired** (GitHub Actions), **fully green on push** (run `27634828800`, commit `6070f0cb`). Passing: Guard A/B/C, commitlint, generated-files gate, version compute, `run_tests.sh`, ad-hoc `build_app.sh`. The sign/notarize/appcast/semantic-release/gh-release tail is gated on a `release-gate` step that **auto-activates** when `APPLE_*` secrets are added — secrets were added 2026-06-16 and the **full tail is now proven** (signed+notarized **v100.1.0**, run `27643432063`). Fixed this session (2026-06-16): regenerated base `Localizable.strings` to clear the genstrings gate; build ad-hoc until the owner provisions the Apple cert (was inheriting upstream's Developer ID → exit 65). Earlier this session: Guard B grep now matches the shipped `guard false else { return .pro }` patch (was checking the PLAN's never-shipped `if false`/`alt-tab-free [depaywall]` form); `update_appcast.sh` → `--ed-key-file`; commitlint sync-merge `ignores`; dropped upstream-only scripts (AppCenter/website); commit headers must be ≤72 chars. Full status: `docs/CICD-STATUS.md`.
- **Local daily driver installed:** `/Applications/CommandTabFree.app` = **Release** build (no QA menu) signed with the `Local Self-Signed` cert (stable signature → no TCC re-grant loop). Remaining: user grants Accessibility + Screen Recording on first launch.
### Build recipes (verified this session)
+23 -14
View File
@@ -1,25 +1,34 @@
# TODO — CommandTabFree
_Updated 2026-06-16. Push CI is fully green. Everything below is **OWNER-GATED** — no agent can do these (they need Apple credentials, DNS/hosting, or tokens). Full detail: `docs/CICD-STATUS.md` bucket C, `docs/EXECUTION-STATUS.md` §3._
_Updated 2026-06-16. The **signed + notarized publishing pipeline is LIVE** — **v100.1.0** is released (Gatekeeper-accepted). Remaining items are auto-update, the Homebrew cask, and upstream-sync. Full detail: `docs/CICD-STATUS.md`, `docs/EXECUTION-STATUS.md` §3._
## Blocking the full publishing pipeline (signed/notarized releases + auto-update)
- [ ] **Apple Developer ID** — obtain a "Developer ID Application" cert for the fork; base64 it into secret `APPLE_P12_CERTIFICATE`; re-sign vendored `vendor/Sparkle/Helpers/*` under the same ID (PLAN §3.3).
- [ ] **Set `CODE_SIGN_IDENTITY`** in `config/local.xcconfig` (lines 45-50, currently commented) to the fork's Developer ID. CI then builds **signed** instead of ad-hoc (the `build app (signed …)` step activates).
- [ ] **Notarization creds** — `APPLE_ID`, `APPLE_PASSWORD` (app-specific password), `APPLE_TEAM_ID` in the `production` Environment.
- [ ] **Real `DOMAIN`/appcast host** — replace `fork.invalid` in `config/local.xcconfig`; serve `https://<DOMAIN>/appcast.xml` (gh-pages); `git rm appcast.xml` + gitignore it; flip `SUEnableAutomaticChecks` back on (PLAN §3.2).
## ✅ Signed + notarized releases — DONE (2026-06-16)
- [x] Apple Developer ID `Developer ID Application: James Maes (2X834TJ5MA)` provisioned; cert + app-specific password in `production` secrets (`APPLE_P12_CERTIFICATE`/`APPLE_P12_PASSWORD`/`APPLE_ID`/`APPLE_PASSWORD`/`APPLE_TEAM_ID`).
- [x] `CODE_SIGN_IDENTITY` + Manual style + `TEAM_ID` set in `config/local.xcconfig`.
- [x] Vendored `vendor/Sparkle/Helpers/*` committed re-signed under the fork Developer ID.
- [x] Full release tail proven on CI (run `27643432063`): signed → notarized → stapled → released. **v100.1.0** published.
> When all four land, the release tail (`release-gate` → notarize → appcast → semantic-release → gh-release) flips on automatically — no further code change.
## Auto-update (deferred by choice — appcast already publishes to gh-pages)
- [ ] **Real `DOMAIN`** — replace `fork.invalid` in `config/local.xcconfig` with the fork host (kof22.com).
- [ ] Serve the gh-pages `appcast.xml` at `https://<DOMAIN>/appcast.xml` (CNAME the gh-pages site).
- [ ] `git rm` the in-tree `appcast.xml` skeleton + gitignore it (feed is published out-of-tree to gh-pages); flip `SUEnableAutomaticChecks` back on (PLAN §3.2).
## Upstream-sync automation (independent of publishing)
- [ ] **`SYNC_BOT_TOKEN`** — GitHub App installation token (preferred) or fork PAT with contents + pull-requests write. Default `GITHUB_TOKEN` does NOT re-trigger PR checks, so guards never run on the sync PR (`upstream_sync.yml` fails closed without it).
- [ ] **External cron** — hit `repository_dispatch` (type `upstream-sync`) from an external scheduler (immune to GitHub's 60-day in-repo `schedule:` auto-disable) (PLAN §4.2).
- [x] Sync PR labels `sync` / `conflict` / `chokepoint-refresh` created (2026-06-16).
## Homebrew cask (now that builds are notarized)
- [ ] Drop the quarantine-strip postflight (`xattr`) from `KofTwentyTwo/homebrew-tap` `Casks/commandtabfree.rb` — no longer needed for a notarized app.
- [ ] Bump the cask to v100.1.0.
## Upstream-sync automation
- [x] `.fork-sync-state` reset to `v11.3.0`; release tail no longer clobbers the cursor with the fork tag (`ci_cd.yml`).
- [ ] **Re-wire the cursor advance** into the upstream-sync merge flow so it records the synced **UPSTREAM** tag (not the fork release tag) — `upstream_sync.yml` (PLAN §4.2).
- [ ] **`SYNC_BOT_TOKEN`** — GitHub App installation token (or fork PAT with contents + pull-requests write). Default `GITHUB_TOKEN` does NOT re-trigger PR checks, so guards never run on the sync PR.
- [ ] **External cron** — hit `repository_dispatch` (type `upstream-sync`) from an external scheduler (immune to GitHub's 60-day in-repo `schedule:` auto-disable).
- [x] Sync PR labels `sync` / `conflict` / `chokepoint-refresh` created.
## Bootstrap acceptance tests (after the above)
- [ ] Sync PR through the bot shows guard checks running.
- [ ] A deliberately-relocked branch goes RED on `guard.yml`.
- [ ] Fork build N→N+1 auto-updates via Sparkle.
- [ ] Fork build N→N+1 auto-updates via Sparkle (needs DOMAIN/appcast live).
## Done this session (2026-06-16)
## Done earlier this session (2026-06-16)
- [x] genstrings consistency gate fixed — regenerated base `Localizable.strings` from source (`d1e1b296`).
- [x] `build_app.sh` builds ad-hoc until cert provisioned; was failing on upstream's Developer ID (`6070f0cb`). Push CI fully green (run `27634828800`).
- [x] `build_app.sh` builds ad-hoc until cert provisioned; was failing on upstream's Developer ID (`6070f0cb`). Push CI green (run `27634828800`).