Skip to content

Start at launch, act only where GitHub would agree, and build from a fresh clone - #2

Merged
hollandcloud merged 9 commits into
mainfrom
feat/first-class
Oct 13, 2026
Merged

hollandcloud merged 9 commits into
mainfrom
feat/first-class

Conversation

@hollandcloud

@hollandcloud hollandcloud commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Cleared worked while someone was looking at it. This PR makes it something you can install, leave running, and trust to offer only buttons GitHub will honour.

Tracked on the board as LAB-8.

What was wrong

Confirmed against the running app or the live API before being changed:

  • Nothing happened until the icon was first clicked. Polling started from the panel view, which a window-style MenuBarExtra doesn't build until it's opened. The old build logged nothing in the 35 seconds after launch; the new one logs starting within half a second, with no click.
  • A fresh clone didn't build. project.yml pointed at the gitignored Local.xcconfig, so XcodeGen rejected the spec on every machine but the author's.
  • Merge was offered where GitHub would refuse. Readiness ignored required reviews and branch rules, the merge wasn't pinned to a commit, and the merge-method setting failed outright on repos that don't allow it.
  • Start a workflow listed workflows that can't be started, always sent no inputs, and assumed main.
  • Rate limits: the footer mixed the REST and GraphQL budgets; 429s and secondary limits weren't handled; a revoked token raised a banner every poll.
  • Dead settings (notifyOnFailure, confirmBeforeMerge), one-way Ignore, no UI for pinned repos, reject() with no UI, and a few smaller bugs listed in CHANGELOG.md.
  • make preview no longer launched on current Xcode, and killed any running copy of the app first.

What changed

  • Lifecycle — an app delegate owns the model and starts it on launch. Launch at login in Settings.
  • Merging — gated on GitHub's mergeStateStatus and reviewDecision; sends the head SHA; falls back to a merge method the repo allows.
  • Workflows — one GraphQL request reads .github/workflows; a small reader finds workflow_dispatch and its declared inputs. Checked against 18 real workflow files from cli/cli: all 10 with the trigger found, the 4 without rejected.
  • Rate limits and auth — budgets tracked per resource; a limit pauses that budget until the time GitHub gave; a 401 returns to the connect screen with an explanation.
  • Also — reject a held deployment, optional two-click confirm, Clear / Re-run on notifications, hourly rediscovery, reversible Ignore and pinning, app icon, unified logging.
  • Token scope — only repo is needed. The README asked for workflow too; none of the endpoints require it.
  • Tooling — make install, make dist (Developer ID + notarize + staple), CI on macOS 15 and 26.

Verification

  • CI passes on macOS 15 (Xcode 16.4, Swift 6.1) and macOS 26 (Xcode 26.6, Swift 6.3). Its first two runs earned their keep: a compiler crash on Swift 6.1–6.3 and a strict-concurrency error on 6.1, neither reproducible on the Swift 6.4 this was written with. Both are fixed in their own commits.
  • 78 tests in 16 suites, up from 17 in 8, against a stubbed GitHub and an in-memory token vault. The app that hosts them stays inert: a test run logs exactly two starting lines, matching the two tests that call start(), and none from the host launching.
  • Fresh clone: this branch with no Local.xcconfig passes make test and make release (ad-hoc signed, hardened runtime).
  • Live, read-only: the real client code run against github.com with a token held in memory. 20 repositories discovered; a second identical polling pass cost 0 REST calls; 24 open pull requests decoded, one correctly held back as behind its base branch; 38 manually startable workflows across 8 repositories with 55 declared inputs, no errors. No write endpoint was called.
  • Installed build: make install at this branch's head. The copy in /Applications reports 0.4.0, matches the build output byte for byte, has a valid hardened-runtime signature, and logs starting on launch with no click.
  • UI: panel, Settings and the workflow sheet checked in the preview harness; control labels read back through the Accessibility API; ⌘, opens Settings.

Not exercised: any write action against real GitHub (clear, reject, merge, re-run, dispatch), the notification buttons on a live hold, and whether the panel closing is detected — the panel can't be dismissed by synthetic input, so that path is covered two ways in code but unconfirmed.

Worth a careful look

  • The tests run hosted inside the app. Before token storage was made injectable, a sign-out test would have deleted the real Keychain token of whoever ran it. TokenVault exists for that reason; please keep new model tests on MemoryVault.
  • WorkflowFile is not a YAML parser. It reads the shapes an on: block is written in and nothing else. A workflow whose inputs are written as an inline {…} mapping is listed as startable but with no inputs shown.
  • Notification actions (Clear, Re-run) go through the same model methods as the buttons, and those are tested, but the notification click path itself hasn't been exercised on a live hold.
  • Behaviour changes: failed runs older than 30 days are hidden, since GitHub won't re-run them; a pull request with no checks at all still gets no Merge button, as before.

Not in this PR

  • No release is tagged and nothing has been notarized. make dist is ready, but publishing needs a decision and Apple credentials.
  • Branch protection on main is still off.

🤖 Generated with Claude Code

hollandcloud and others added 9 commits October 12, 2026 17:27
project.yml named Local.xcconfig as the config file for both
configurations, but that file is gitignored. XcodeGen rejected the spec
on any checkout that did not have one, so `make run` failed for everyone
except the author:

    2 Spec validations errors:
      - Invalid config file "Local.xcconfig" for config "Debug"
      - Invalid config file "Local.xcconfig" for config "Release"

Point at a committed Cleared.xcconfig that pulls Local.xcconfig in with
`#include?`, which is a no-op when the file is absent. Signing overrides
still apply when it is present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…four verbs

Cleared worked when someone was looking at it. This makes it something
that can be left running.

Starts with the app. Polling was kicked off from the panel, and a
window-style MenuBarExtra does not build its content until the icon is
first clicked — so a copy launched at login watched nothing and
announced nothing until opened. An app delegate now owns the model and
starts it on launch. Launch at login is in Settings.

Merge is only offered when GitHub would accept it. Readiness looked at
the check rollup and conflicts; it now also requires GitHub's merge
verdict and review decision, so a pull request waiting on a required
review or a branch rule is listed under "In flight" with the reason.
The merge is pinned to the commit last polled, so a push in between is
refused rather than merged unseen, and a repository that forbids the
preferred merge method uses its own default instead of failing.

Start a workflow lists what can actually be started. It listed every
enabled workflow, always sent no inputs, and assumed `main`. It now
reads the workflow files in one GraphQL request, keeps those with a
workflow_dispatch trigger, and draws the inputs each one declares.

Stops when GitHub says stop. A 403 or 429 rate limit now pauses that
budget until the time GitHub named. The REST and GraphQL budgets are
tracked apart, which also fixes the "calls left" figure flipping
between them. A 401 ends the session and returns to the connect screen
with an explanation instead of raising a banner on every poll.

Also: reject a held deployment; an optional second click before Clear
and Merge; Clear and Re-run on notifications, which are withdrawn once
the hold is gone; hourly repository rediscovery; ignore is reversible
and repositories can be pinned; the failed-run notification setting is
wired up; an app icon; unified logging.

The token now needs only the `repo` scope. `workflow` was requested but
nothing here uses it.

Tests go from 17 to 78. They run hosted inside the app, so token
storage is injected: a sign-out test against the real Keychain would
delete the token of whoever ran it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`make install` puts a Release build in /Applications, which is the only
place a login item can be registered from.

`make dist` builds the zip for a GitHub release: Developer ID signed
with the hardened runtime, notarized, stapled, and checked with
Gatekeeper before it reports success. Without a notary profile it stops
after signing and says the result must not be published.

CI runs the secret scan, the tests and a Release build on macOS 15 and
macOS 26 for every pull request. It has no Local.xcconfig, so it also
guards the fresh-clone build.

`make icon` redraws the app icon from Scripts/make-icon.swift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said the token needs `repo` and `workflow`. Every endpoint
Cleared calls needs only `repo`; add the fine-grained permissions that
cover the same ground. Document install, launch at login, shortcuts,
rejecting a deployment, how the tests stay off the Keychain, and how to
cut a notarized release. Add a changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI caught this on its first run. Passing a method reference as a
Binding's setter makes the Swift 6.1.2 and 6.3.3 compilers abort in IR
generation while emitting the thunk for it:

    While evaluating request IRGenRequest(IR Generation for file
    "Sources/Cleared/UI/SettingsView.swift")

Swift 6.4 compiles it, which is why it built locally. Wrap the call in
a closure instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two problems with the preview target:

It no longer launched at all. The built products path was found with
`awk '/BUILT_PRODUCTS_DIR/'`, which also matches
PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = YES, so the path came
back as two lines and `open` failed. Match the setting by its full name.

It also killed every running Cleared before opening, including an
installed copy that was watching for holds — and `open` without `-n`
would have just re-activated that copy instead of launching the preview.
Open a new instance, and only close an earlier preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Xcode 16.4 rejects handing the notification center to the system's
async authorization call when it was read through a main-actor
property:

    Notifier.swift:83:43: error: sending 'center' risks causing data races

Newer SDKs mark the type Sendable, so only the macOS 15 job saw it.
Fetch the center inside the function, as the code did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Cleared" is the tower's word, and the menu bar already shows an
aeroplane while something is holding. Replace the check on the amber
tile with a control tower: plinth, shaft, a cab leaning outward under a
flat roof, and a mast with a beacon.

Drawn for the small sizes first. Every part is at least a pixel across
at 16 pixels, the cab's window band is dropped there because it only
greys the silhouette, and the mullions appear from 128 pixels up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two things in the Settings sheet.

The switches showed no colour when on. AppKit draws its own switch grey
in any window that is not key, and a menu bar panel often is not, so on
and off differed only by where the knob sat. An explicit tint makes no
difference to that. Draw the switch here instead, filled with the
system accent colour, and present it to VoiceOver as the toggle it
replaces. The workflow sheet's boolean inputs use the same style.

The header now carries the app icon. It is read from the asset catalog
rather than NSApp.applicationIconImage, which goes through the system
icon cache and kept serving the previous icon after it was redrawn.

Bump the build number to 3 for the same reason: the cache is keyed on
the version, and the icon changed without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hollandcloud
hollandcloud merged commit 546b773 into main Oct 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant