Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b4f4bd9
docs(02): refresh the bug-fix plans for holzBar
claude Oct 2, 2026
232fbc5
fix(02-01): relaunch every app when applying spacing
claude Oct 2, 2026
fd2ada5
test(02-01): add failing tests for the quit wait and its timeout
claude Oct 2, 2026
6a03218
docs: insert phase 06.1 compatibility check
claude Oct 2, 2026
652b1e6
fix(02-01): wait for apps to quit instead of killing them
claude Oct 2, 2026
1edeab1
docs(02-01): complete the spacing relaunch plan
claude Oct 2, 2026
3040273
Merge remote-tracking branch 'origin/main' into claude/ice-fork-devel…
claude Oct 2, 2026
3b65dc3
README: switching to the holzcloud/holzbar tap
claude Oct 2, 2026
8eb7f92
test(02-02): add failing tests for the hotkey modifier rule
claude Oct 2, 2026
ab6d3fc
fix(02-02): refuse Option-only hotkeys on macOS 15 and tell the user
claude Oct 2, 2026
b66fe37
fix(02-02): every permission wait returns
claude Oct 2, 2026
f0e7877
docs(02-02): complete the Option-only hotkey and permission wait plan
claude Oct 2, 2026
397e7e7
chore(cask): point the cask only at holzBar
claude Oct 2, 2026
7f00592
refactor(app): drop the former-name import, URL scheme and conflict e…
claude Oct 2, 2026
8c342bc
docs: holzBar is the only name
claude Oct 2, 2026
25e5a8e
docs: quick task summary (former name removed)
claude Oct 2, 2026
6fb70fd
test(02-03): prove the code directory hashes that pin a process
claude Oct 2, 2026
1dc2ed0
test(02-03): check this process by its audit token, as XPC checks a peer
claude Oct 2, 2026
e83fa77
fix(02-03): accept holzBar's ad hoc build in the menu bar item service
claude Oct 2, 2026
32b5ccd
docs(02-03): name the framework only where the service imports it
claude Oct 2, 2026
1e6d98b
docs(02-03): complete the XPC peer requirement plan
claude Oct 2, 2026
14a6dd4
fix(02-04): guard the event source cache with a lock
claude Oct 2, 2026
d584f45
test(02-04): pin the macOS 27 system item allowlist to the measured r…
claude Oct 2, 2026
af48d9a
fix(02-04): allow the measured system item range on macOS 27
claude Oct 2, 2026
799e386
docs(02-04): complete the event source lock and allowlist plan
claude Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,38 @@ jobs:
exit 1
fi

# The app's former name must not appear anywhere in the repository (see CLAUDE.md).
# .planning/ and .claude/ are GSD's working state; .github/cms-version.py is a copy
# of a tool maintained in holzcloud-design. The pattern is written so that it does
# not match itself.
former-name:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Check for the former name
run: |
PATTERN='holz[ -]?[i]ce'
EXCLUDE=(':(exclude).planning' ':(exclude).claude' ':(exclude).github/cms-version.py')
set +e
git grep -n -i -E "$PATTERN" -- . "${EXCLUDE[@]}"
STATUS=$?
set -e
if [ "$STATUS" -eq 0 ]; then
echo "::error::The former name appears in the lines above."
exit 1
elif [ "$STATUS" -ne 1 ]; then
echo "::error::git grep failed with status $STATUS."
exit "$STATUS"
fi
NAMES=$(git ls-files -- . "${EXCLUDE[@]}" | grep -i -E "$PATTERN" || true)
if [ -n "$NAMES" ]; then
echo "$NAMES"
echo "::error::The former name appears in the file names above."
exit 1
fi
echo "==> The former name appears nowhere."

test:
runs-on: macos-26
steps:
Expand Down
95 changes: 9 additions & 86 deletions .github/workflows/cask.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Proves the path a holzIce user takes to holzBar: holzice 0.0.5 installed from the
# tap as it was before the rename, then `brew update && brew upgrade`. Homebrew must
# rename the installed cask to holzbar (cask_renames.json), upgrade it without a
# conflict and uninstall it, and the cask must pass `brew style` and `brew audit`.
# It installs a real app on a macOS runner, so it runs only when the cask changes.
# Checks the cask with `brew style` and `brew audit`, loaded from this repository as
# the tap. Nothing is downloaded or installed. It runs only when the cask changes.
name: Cask
on:
pull_request:
paths:
- "Casks/**"
- "cask_renames.json"
- ".github/workflows/cask.yml"

permissions:
Expand All @@ -20,97 +16,24 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Migrate holzice to holzbar
- name: Style and audit the cask
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_ENV_HINTS: "1"
# main before the rename: Casks/holzice.rb at 0.0.5
OLD_COMMIT: 2b1b4f862993c38eb5452a22d0149a405816d844
run: |
set -euo pipefail

# Users run a current Homebrew; the runner image may be older.
brew update --quiet
brew --version

# A local copy of this repository serves as the tap, so its main can be
# moved from the pre-rename commit to this pull request's commit.
PR_COMMIT=$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)
# A local copy of this pull request's commit on a main branch serves as the tap.
SRC="$RUNNER_TEMP/tap-src"
git clone --quiet "$GITHUB_WORKSPACE" "$SRC"
git -C "$SRC" checkout --quiet -B main "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)"

# A holzIce user as of before the rename: tapped, trusted holzice, installed it.
install_holzice() {
git -C "$SRC" checkout --quiet -B main "$OLD_COMMIT"
brew tap holzcloud/holzice "$SRC"
brew trust --cask holzcloud/holzice/holzice
brew install --cask holzcloud/holzice/holzice
test -d /Applications/holzIce.app
echo "==> installed holzice from the pre-rename tap"
# The rename lands on main; the next `brew update` fetches it.
git -C "$SRC" checkout --quiet -B main "$PR_COMMIT"
}

migrated() {
grep -qx holzbar <<< "$(brew list --cask)"
}

upgrade_and_uninstall() {
brew upgrade --cask holzbar
brew uninstall --cask holzbar
test ! -e /Applications/holzIce.app
test ! -e /Applications/holzBar.app
}

# 1. Does the trust given to holzice carry over to holzbar? Homebrew loads a
# tap's casks only when they are trusted, and the migration loads holzbar.
install_holzice
brew update
if migrated; then
echo "==> trust carried over: yes"
else
echo "==> trust carried over: no"
# A user who ran `brew update` before trusting holzbar: trust it, then
# `brew migrate` does what the update would have done.
brew trust --cask holzcloud/holzice/holzbar
brew migrate --cask holzice
if migrated; then
echo "==> migrated after brew migrate: yes"
else
echo "==> migrated after brew migrate: no"
brew list --cask
exit 1
fi
fi
upgrade_and_uninstall
brew untap holzcloud/holzice
# The migration leaves Caskroom/holzice as a link to Caskroom/holzbar.
CASKROOM=$(brew --caskroom)
if [ -L "$CASKROOM/holzice" ]; then
echo "Removing the leftover link $CASKROOM/holzice"
rm "$CASKROOM/holzice"
fi

# 2. The README's path: trust holzbar, then `brew update && brew upgrade`.
install_holzice
brew trust --cask holzcloud/holzice/holzbar
brew update 2>&1 | tee "$RUNNER_TEMP/update.log"
if migrated; then
echo "==> migrated: yes"
else
echo "==> migrated: no"
echo "brew list --cask:"
brew list --cask
echo "brew update output:"
cat "$RUNNER_TEMP/update.log"
exit 1
fi

brew style --cask holzcloud/holzice/holzbar
brew audit --cask --strict holzcloud/holzice/holzbar

upgrade_and_uninstall
echo "==> uninstalled: yes"
brew tap holzcloud/holzbar "$SRC"
brew trust --cask holzcloud/holzbar/holzbar
brew style --cask holzcloud/holzbar/holzbar
brew audit --cask --strict holzcloud/holzbar/holzbar
13 changes: 2 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,30 +79,21 @@ jobs:
gh release create "$TAG" "$ZIP" \
--target "$GITHUB_SHA" --title "holzBar $TAG" --notes-file "$NOTES" "${PRERELEASE[@]}"

# Until the first holzBar release the cask installs 0.0.5, the app under its former name. Every
# release points it at the holzBar zip and app just published; once that
# is done, these edits change nothing but the version and sha256.
- name: Update the Homebrew cask
run: |
git fetch origin main
git checkout -B main origin/main
sed -i '' -E \
-e "s/^ version \".*\"/ version \"$VERSION\"/" \
-e "s/^ sha256 .*/ sha256 \"$SHA256\"/" \
-e '/^ # Until the first holzBar release/d' \
-e 's|^ url ".*"$| url "https://github.com/holzcloud/holzBar/releases/download/v#{version}/holzBar-#{version}.zip"|' \
-e 's|^ app ".*"$| app "holzBar.app"|' \
-e 's|"[{][{]appdir[}][}]/[^"/]*[.]app"|"{{appdir}}/holzBar.app"|' \
-e 's|writable_paths: [[]"[^"/]*[.]app"[]]|writable_paths: ["holzBar.app"]|' \
-e 's|^ uninstall quit: .*| uninstall quit: "com.holzcloud.holzBar"|' \
Casks/holzbar.rb
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "holzbar $VERSION"
git push origin main

# holzcloud.ch shows the released version on the holzBar (formerly
# holzIce) page in five languages. This writes it there through the holzcloud-CMS once the
# holzcloud.ch shows the released version on the holzBar page in five
# languages. This writes it there through the holzcloud-CMS once the
# release exists, and reads it back. A beta (0.0.6-beta1) is skipped: the
# site keeps showing the last plain release. .github/cms-version.py is a
# copy of holzcloud-design's tools/cms-version.py, where its tests live
Expand Down
45 changes: 27 additions & 18 deletions .planning/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ Source: `.planning/codebase/CONCERNS.md` (file:line references there).

### Bugs

- [ ] **BUG-01**: Applying menu bar item spacing relaunches every affected app (`continue` instead of `break`)
- [ ] **BUG-02**: Spacing relaunch waits long enough for apps to quit, does not force-terminate them after 1 s, and always resumes its continuation
- [ ] **BUG-03**: Spacing relaunch collects the owning apps on macOS 27 too
- [ ] **BUG-04**: The event source cache in `MenuBarItemManager` is free of data races
- [ ] **BUG-05**: The hotkey recorder rejects combinations that macOS 15+ cannot register (Option or Option+Shift only) and tells the user; the hotkey signature stays identical to Ice's
- [ ] **BUG-06**: The XPC menu bar item service accepts the app on ad hoc builds (no team identifier) while still rejecting foreign processes
- [ ] **BUG-07**: Waiting for a permission twice never leaves a continuation unresumed
- [ ] **BUG-08**: Comment and code of the macOS 27 system item allowlist agree
- [x] **BUG-01**: Applying menu bar item spacing relaunches every affected app (`continue` instead of `break`)
- [x] **BUG-02**: Spacing relaunch waits long enough for apps to quit, does not force-terminate them after 1 s, and always resumes its continuation
- [x] **BUG-03**: Spacing relaunch collects the owning apps on macOS 27 too
- [x] **BUG-04**: The event source cache in `MenuBarItemManager` is free of data races
- [x] **BUG-05**: The hotkey recorder rejects combinations that macOS 15+ cannot register (Option or Option+Shift only) and tells the user; the hotkey signature stays identical to Ice's
- [x] **BUG-06**: The XPC menu bar item service accepts the app on ad hoc builds (no team identifier) while still rejecting foreign processes
- [x] **BUG-07**: Waiting for a permission twice never leaves a continuation unresumed
- [x] **BUG-08**: Comment and code of the macOS 27 system item allowlist agree

### Leftovers

Expand Down Expand Up @@ -86,6 +86,12 @@ Source: `.planning/codebase/CONCERNS.md` (file:line references there).
- [ ] **PERF-02**: Permission polling stops once all permissions are granted
- [ ] **PERF-03**: Reveal rules react to power and network notifications instead of polling every 60 s

### Compatibility

- [ ] **COMPAT-01**: macOS 26 (Tahoe) and macOS 27 are supported without restriction; this is mandatory
- [ ] **COMPAT-02**: For macOS 14 and 15 it is measured (CI builds and tests on macos-14 and macos-15 runners, `#available` branches reviewed) what works and what it costs to keep; the user decides whether to keep them or raise the deployment target to macOS 26 (which removes the pre-26 backend code)
- [ ] **COMPAT-03**: README, the cask's `depends_on macos:`, the badge and the release notes state exactly the supported versions

### Release

- [ ] **REL-01**: `0.0.6-beta1` is released with hand-written notes (brew trust, update, quarantine) and the cask points at it
Expand Down Expand Up @@ -128,14 +134,14 @@ Source: `.planning/codebase/CONCERNS.md` (file:line references there).
| REN-06 | Phase 01.1 | Complete |
| REN-07 | Phase 01.1 | Complete |
| REN-08 | Phase 01.1 | Complete |
| BUG-01 | Phase 2 | Pending |
| BUG-02 | Phase 2 | Pending |
| BUG-03 | Phase 2 | Pending |
| BUG-04 | Phase 2 | Pending |
| BUG-05 | Phase 2 | Pending |
| BUG-06 | Phase 2 | Pending |
| BUG-07 | Phase 2 | Pending |
| BUG-08 | Phase 2 | Pending |
| BUG-01 | Phase 2 | Complete |
| BUG-02 | Phase 2 | Complete |
| BUG-03 | Phase 2 | Complete |
| BUG-04 | Phase 2 | Complete |
| BUG-05 | Phase 2 | Complete |
| BUG-06 | Phase 2 | Complete |
| BUG-07 | Phase 2 | Complete |
| BUG-08 | Phase 2 | Complete |
| LEFT-01 | Phase 3 | Pending |
| LEFT-02 | Phase 3 | Pending |
| LEFT-03 | Phase 3 | Pending |
Expand Down Expand Up @@ -172,11 +178,14 @@ Source: `.planning/codebase/CONCERNS.md` (file:line references there).
| PRIV-02 | Phase 05.1 | Pending |
| PERM-01 | Phase 05.1 | Pending |
| AUDIT-01 | Phase 6 | Pending |
| COMPAT-01 | Phase 06.1 | Pending |
| COMPAT-02 | Phase 06.1 | Pending |
| COMPAT-03 | Phase 06.1 | Pending |
| REL-01 | Phase 7 | Pending |

**Coverage:**
- v1 requirements: 60 total
- Mapped to phases: 60
- v1 requirements: 63 total
- Mapped to phases: 63
- Unmapped: 0 ✓

---
Expand Down
28 changes: 22 additions & 6 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Decimal phases appear between their surrounding integers in numeric order.
- [ ] **Phase 5: Security and performance** - Validated settings import, private logging, Caches storage, no needless tasks or polling
- [ ] **Phase 05.1: Modern, lean and private** (INSERTED) - 2026 code, Swift 6, @Observable, fewer dependencies, no network, least privilege
- [ ] **Phase 6: Security audit** - Full security analysis of the whole app, findings ranked, fixes chosen by the user done before the release
- [ ] **Phase 06.1: Compatibility check** (INSERTED) - Which macOS versions really work; 26 and 27 required, older ones optional
- [ ] **Phase 7: Release 0.0.6-beta1** - Tag, hand-written release notes, cask updated

Each phase is one pull request and must build green on the macOS CI runner before merge.
Expand Down Expand Up @@ -79,13 +80,13 @@ Plans:
4. Waiting for a permission twice never hangs, and the event source cache has no data race
5. The macOS 27 system item allowlist comment and code agree

**Plans**: 4 plans (sequential waves: one PR branch, every task verified by its CI checks)
**Plans**: 4/4 plans executed (sequential waves: one PR branch, every task verified by its CI checks)

Plans:
- [ ] 02-01-PLAN.md — Tracer: tested `Ice/Core` package target; spacing relaunch keeps going past skipped processes (MenuBarAgent skipped on macOS 27), 10 s quit wait, no force-termination; phase PR opened
- [ ] 02-02-PLAN.md — Hotkey recorder refuses Option-only combinations on macOS 15+ and says why (signature unchanged); every permission wait returns
- [ ] 02-03-PLAN.md — XPC service accepts holzIce's ad hoc build by pinning the embedding app's signing identifier and code directory hashes (proven by a CodeSignature test suite); foreign processes still rejected
- [ ] 02-04-PLAN.md — Lock-guarded event source cache; macOS 27 system item allowlist 0 to 127 with matching comment and tests; PR body complete
- [x] 02-01-PLAN.md — Tracer: tested `holzBar/Core` package target (`HolzBarCore`); spacing relaunch keeps going past skipped processes (MenuBarAgent skipped on macOS 27), 10 s event-driven quit wait, no force-termination; phase PR opened
- [x] 02-02-PLAN.md — Hotkey recorder refuses Option-only combinations on macOS 15+ and says why (signature unchanged); every permission wait returns
- [x] 02-03-PLAN.md — XPC service accepts holzBar's ad hoc build by pinning the embedding app's signing identifier and code directory hashes (proven by a CodeSignature test suite); foreign processes still rejected
- [x] 02-04-PLAN.md — Lock-guarded event source cache; macOS 27 system item allowlist 0 to 127 with matching comment and tests; PR body complete

### Phase 3: Ice and Sparkle leftovers

Expand Down Expand Up @@ -161,6 +162,21 @@ Plans:

**Plans**: TBD

### Phase 06.1: Compatibility check (INSERTED)

**Goal:** It is known and documented which macOS versions holzBar really runs on; macOS 26 and 27 are guaranteed, older versions are kept only where they cost little
**Requirements**: COMPAT-01, COMPAT-02, COMPAT-03
**Depends on:** Phase 6
**Success Criteria** (what must be TRUE):
1. CI builds and runs the unit tests on every macOS runner GitHub offers (macos-14, macos-15, macos-26), and the deployment target matches the oldest version that really works
2. macOS 26 and 27 are verified on real Macs by the user with a short checklist (hiding, Shelf, layout editor, hotkeys, settings import)
3. README, cask `depends_on macos:` and release notes state the supported versions truthfully; if the user decides to drop 14/15, the old backend code is removed (lean)

**Plans:** 0 plans

Plans:
- [ ] TBD (run /gsd-plan-phase 06.1 to break down)

### Phase 7: Release 0.0.6-beta1

**Goal**: Users can install and update to `0.0.6-beta1` through Homebrew with clear instructions
Expand All @@ -182,7 +198,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
|-------|----------------|--------|-----------|
| 1. CI and build | 3/3 | Complete (human check: install.sh on a Mac) | 2026-10-02 |
| 01.1. Rename to holzBar | 6/6 | In Progress| |
| 2. Bug fixes | 0/4 | Planned | - |
| 2. Bug fixes | 4/4 | In Progress| |
| 3. Ice and Sparkle leftovers | 0/0 | Not started | - |
| 4. Outdated APIs | 0/0 | Not started | - |
| 5. Security and performance | 0/0 | Not started | - |
Expand Down
26 changes: 19 additions & 7 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ milestone: v0.0.6
current_phase: 1
current_phase_name: CI and build
status: verifying
stopped_at: Completed 01.1-06-PLAN.md (repository rename by the user pending)
last_updated: "2026-10-02T14:16:00.237Z"
stopped_at: Completed 02-04-PLAN.md
last_updated: "2026-10-02T16:19:14.704Z"
last_activity: 2026-10-02
last_activity_desc: Roadmap created (6 phases, 37 requirements mapped)
state_head: d6f029dd4b1360b625a6b5c50a0c9ca9ec3b066a
state_head: af48d9aad9173cc07b401acf9d469091728c89d2
progress:
total_phases: 9
total_phases: 10
completed_phases: 0
total_plans: 13
completed_plans: 9
completed_plans: 13
---

# Project State
Expand Down Expand Up @@ -64,6 +64,10 @@ Progress: [░░░░░░░░░░] 0%
| Phase 01.1 P03 | 11min | 2 tasks | 69 files |
| Phase 01.1 P04 | 20min | 2 tasks | 15 files |
| Phase 01.1 P06 | 20min | 2 tasks | 10 files |
| Phase 02 P01 | 16min | 2 tasks | 4 files |
| Phase 02 P02 | 15min | 2 tasks | 7 files |
| Phase 02 P03 | 20min | 2 tasks | 7 files |
| Phase 02 P04 | 12min | 2 tasks | 4 files |

## Accumulated Context

Expand All @@ -90,6 +94,14 @@ Recent decisions affecting current work:
- [Phase 01.1]: holzIce users must trust holzcloud/holzice/holzbar once before brew update: Homebrew 7.0.7 does not carry the holzice trust over to the renamed cask (measured in the cask job)
- [Phase 01.1]: Interim cask holzbar installs holzIce 0.0.5 until the first holzBar release; release.yml switches url, app, postflight and uninstall to holzBar
- [Phase 01.1]: No conflicts_with cask holzice and no tap_migrations.json: the rename mapping would make holzbar conflict with itself, and both tap names are this repository
- [Phase 02]: Spacing relaunch skips only holzBar, Control Center and MenuBarAgent (SpacingRelaunch.processesToRelaunch)
- [Phase 02]: Apps get 10 s (SpacingRelaunch.quitTimeout) to quit and are never force terminated; the wait is KVO-driven and always returns
- [Phase 02]: Option-only and Option+Shift-only hotkeys are refused on macOS 15+ in the recorder (alert, recording continues) and in HotkeyRegistry (logged); the Carbon signature stays OSType(1231250720) (D-01)
- [Phase 02]: A permission wait returns false when stopCheck() ends it or its task is cancelled; the Grant buttons only reopen the permissions window on true
- [Phase 02]: BUG-06: on ad hoc builds the XPC listener requires SigningIdentifier(com.holzcloud.holzBar) plus CodeDirectoryHash.in(hashes of the embedding app); team builds require same team plus identifier; it fails closed (D-02)
- [Phase 02]: BUG-06: the app sets its same-team peer requirement only when it has a team; LightweightCodeRequirements is imported only in MenuBarItemService/Listener.swift
- [Phase 02]: BUG-08 (D-03): macOS 27 system item allowlist is 0 through 127, the measured range, held in SystemItems27; 63 only matched jordanbaird/Ice#1001
- [Phase 02]: BUG-04: event source cache guarded by one OSAllocatedUnfairLock (withLockUnchecked; CGEventSource is not Sendable)

### Pending Todos

Expand All @@ -109,6 +121,6 @@ Items acknowledged and deferred at milestone close, most recent first:

## Session Continuity

Last session: 2026-10-02T14:16:00.190Z
Stopped at: Completed 01.1-06-PLAN.md (repository rename by the user pending)
Last session: 2026-10-02T16:19:14.654Z
Stopped at: Completed 02-04-PLAN.md
Resume file: None
Loading
Loading