Release archive
Earlier builds and releases of App Auto-Patch 4. See Release notes for the latest builds.
Build 76
Section titled “Build 76”Released October 7, 2026
-
Generate a diagnostic bundle from the command line.
"/Applications/App Auto-Patch.app/Contents/MacOS/App Auto-Patch" --generate-diagnostic-bundle"/Applications/App Auto-Patch.app/Contents/MacOS/App Auto-Patch" --generate-diagnostic-bundle --output /path/to/dir"/Applications/App Auto-Patch.app/Contents/MacOS/App Auto-Patch" --generate-diagnostic-bundle --output /path/to/AppAutoPatch-Diagnostic.zipThe app stays headless, like
--clear-notifications. The zip uses the same contents and redaction as Settings → About → Generate Diagnostic Bundle. With no--output, it is written to/var/tmp. A directory receives a timestamped archive; a path ending in.zipis that file. The archive path is printed to stdout. Failure exits non-zero and writes the reason to stderr. The Settings button still saves on the Desktop.
Build 75
Section titled “Build 75”Released October 6, 2026
-
Workflow B payloads include fragments/functions.sh.
Scripts/build-appautopatch-installomator-sfx.zshsparse-checked out onlyInstallomator.shandfragments/labels/.LabelVersionHelper.zshexits 2 withoutfragments/functions.sh, so every installed label fell back to a full Installomator run. The builder now includesfunctions.shand refuses a payload that lacks it. A payload already on disk is left in place; the root helper and discovery each log one warning, and discovery skips the helper instead of failing it once per label. Diagnostics record whether the file is present. -
The upgrade auto-reset no longer reports a helper that never started. After an in-place upgrade, the cdhash auto-reset unregistered the helper and then launch registration called
register()during the settle wait. That call failed with “Operation not permitted”, and the reset’s ownregister()came backenabledwithout launchd ever starting the daemon. Launch registration now stays out of an in-flight reset. A reset that isenabledwithout a new heartbeat kickstarts once and, if the daemon is still down, repeats unregister/register once before it reports failure. -
Mac App Store copies no longer stall the hard deadline. A Mac App Store install (a
Contents/_MASReceiptdirectory) was still treated as a pending Installomator update whenIGNORE_APP_STORE_APPSwas notyes. Installomator then exited 23, and the hard-deadline watcher presented the required-update prompt again about every five minutes. Those apps are now left to the App Store: they stay off the pending count, the menu-bar countdown, and deadline enforcement. SetIGNORE_APP_STORE_APPS=yesto keep replacing them.
-
Failed Update All retries on DeferralTimerError. A failed Update All no longer restarts the prompt on
DialogTimeoutDeferral. The next attempt waitsDeferralTimerErrorminutes (default 60; values below 1 wait 1 minute), including after the hard deadline.DialogTimeoutDeferralis still the on-screen countdown once that attempt is due.WorkflowUpdateAllErrorCompletesPatching(defaultfalse) changes that outcome: whentrue, an Update All that finishes with one or more errors records the patching cadence as complete until the next reset, and App Auto-Patch does not retry.
Build 74
Section titled “Build 74”Released October 5, 2026
- DiscoveryFrequency and cadence-reset rediscovery.
The root helper can run automatic discovery and update checks on a
standalone
DiscoveryFrequencyhour interval (unset or 0 = off). Mid-cycle passes refresh the GUI and menu bar inventory only; notifications and the deferral dialog wait untilNextAutoLaunch. When the patching cadence resets (monthly orDaysUntilReset), discovery runs immediately with the full notify/dialog path, and deferral / notification bookkeeping is cleared for the new cycle.
- Successful patches no longer leave (or re-download) their staged installer.
After a successful install the root helper already removed that label’s
files from
/tmp/AAPStage, but a follow-up open-apps staging job could re-download it from a stale pending list. Staging now skips labels whose installed version already matches the target and clears any leftover archive, so only still-pending apps keep staged installers.
Build 73
Section titled “Build 73”- Empty discovery no longer blames a missing Installomator.
When Installomator and its labels were present but every installed app
was ignored or unmatched, the dashboard said discovery had not run yet
and blamed a missing Installomator. The empty list now says “No
applicable applications discovered”. The top status line names how many
labels were found and how many installed apps with matching labels are
excluded by your administrator, and stays for 30 seconds. Discovery
progress remains on screen while that excluded-app count finishes off
the main thread. The Installomator-missing wording is kept only when
the fragments or labels folder is actually absent, and it names
/Library/Application Support/AppAutoPatch/Installomator.
Build 72
Section titled “Build 72”Released October 5, 2026
Changed
Section titled “Changed”- Official builds now come from GitHub Actions. Releases are built, signed with the Developer ID team 6GBSVT64FS, notarized and published by CI instead of a maintainer’s Mac. No change to the app itself.
Build 71
Section titled “Build 71”Released October 5, 2026
- Open apps raise the deferral dialog on their own.
Finding open apps that need updates posted a notification and waited
for a click before showing the deferral dialog.
DeferralWatcheronly presents that dialog afterNextAutoLaunchelapses or a hard deadline is reached, so a fresh install never opened it. The dialog now appears with the notification. An automatic focus deferral still hides it, and a deferral that is already running is left alone.
Build 70
Section titled “Build 70”Released October 5, 2026
Changed
Section titled “Changed”-
The persistent notifier is named App Auto-Patch Notification. System Settings showed the embedded helper as App Auto-Patch Persistent. The user-visible name is now App Auto-Patch Notification. The bundle ID stays
xyz.techitout.appAutoPatch-persistent, so existing notification settings profiles, authorization, andPersistentNotificationTypeskeep working without a profile change.A pkg upgrade replaces
App Auto-Patch.app, which removes the old helper. The installer also deletesContents/Helpers/App Auto-Patch Persistent.appif a previous copy was updated in place. Notification Center can keep showing the old name until the user logs out or the Mac restarts.
Build 69
Section titled “Build 69”Released October 2, 2026
- Update asks for confirmation before the hard deadline.
Clicking Update on the deferral dialog asks whether to install updates,
unless
DeadlineDaysHardorDeadlineCountHardhas already been reached. The confirmation counts down 15 seconds and then chooses Yes. No returns to the deferral dialog, andDialogTimeoutDeferralcontinues from the time remaining when Update was clicked.
-
A 90-minute deferral reads as 1 hour 30 min.
DeferralTimerMenuvalues from 61 to 119 minutes put the leftover minutes in the hour spot, so 90 appeared as “30 hour 30 min”. That range now reads as 1 hour plus the remaining minutes. -
The deferral dialog counts down from DialogTimeoutDeferral. The countdown was stored on the dialog itself and began at
0:00:00. The standalone panel keeps that dialog alive after it closes, so the next time it appeared the timer had already finished and the line stayed at0:00:00.3720seconds now shows as1:02:00and counts down each time the dialog is presented.The timeout line names
DialogTimeoutDeferralAction.Defersays App Auto-Patch will automatically defer.Continuesays App Auto-Patch will automatically install updates.
Build 68
Section titled “Build 68”Released October 1, 2026
Changed
Section titled “Changed”-
The patch cycle is measured from its start date.
DaysUntilResetis counted fromCurrentPatchingStartDate. A value of 7 starts the next cycle one week later, and 14 starts it two weeks later. A successful patch no longer starts that clock. Monthly cadence still ends on the next matching weekday.A Mac with no saved start date, opened after the normal anchor, uses today as
CurrentPatchingStartDateand keeps the original cycle end. Deadlines that would land on or after that end move to the day before it, and the focus deadline stays at least one day before the hard deadline. An install on the day before the cycle end uses the next cycle start instead. A Mac that already has a start date keeps it.After a successful patch, new-update notifications stay off until the next cycle. Check for Updates still fills the app list and the menu bar, and both say when this cycle was completed and when the next one starts.
-
Uninstall clears Notification Center entries. Before the app bundle is deleted,
uninstall.shlaunches App Auto-Patch as each logged-in user with--clear-notifications. That drops delivered banners and scheduled reminders for bothxyz.techitout.appAutoPatchandxyz.techitout.appAutoPatch-persistent. The existing notification database delete still runs afterward. -
Uninstall removes staged installers.
uninstall.shdeletes/private/tmp/AAPStagewith the other root-owned files. That is the folderWorkflowStageUpdatesuses, also visible as/tmp/AAPStage.
Build 67
Section titled “Build 67”Released October 1, 2026
-
An empty pmset read neither defers nor shows the dialog. A complete
pmsetlisting withPreventUserIdleDisplaySleepat 0 still shows the deferral dialog. A listing that names a meeting process still starts a focus deferral. An empty or unusable capture does neither: the dialog stays hidden and the check runs again in 30 seconds, without incrementingFocusDeferralCountand without arming a focus deferral. The three-strike limit that showed the dialog anyway is gone. The inconclusive log includes the stdout byte count and any stderr. -
The Updates Required countdown includes hours. The deadline dialog clock is
H:MM:SS. A window longer than an hour no longer shows as raw minutes (65:00is1:05:00). The same clock is used for the timeout line on the regular deferral dialog. -
App rows label the installed version as Installed. The version already on the Mac is shown as
Installed:next toLatest:.
Changed
Section titled “Changed”-
Profile manifests match 4.0.0 and the Settings window.
Profile Manifests/xyz.techitout.appAutoPatch.jamf.jsonis rewritten as a 4.0.0-only Jamf Pro Custom Schema, andProfile Manifests/xyz.techitout.appAutoPatch.json(ProfileCreator) now lists the same 68 keys in the same order.Keys follow the Settings tabs and sections: Installomator, Deferrals, Advanced and About, then a final Managed Only group. Each title starts with its Settings section, such as
Focus Deferral › Count limit. Keys with no Settings control are marked (managed only). In the Jamf schema,property_orderuses bands of 100 per tab (100s Installomator through 500s Managed Only), with each section starting on a multiple of 10.The Jamf schema gains the 10 keys it was missing:
VersionComparisonMethod,VersionComparisonInstallomatorFallback,MonthlyPatchingCadenceEnabled,MonthlyPatchingCadenceOrdinalValue,MonthlyPatchingCadenceWeekdayIndex,MonthlyPatchingCadenceStartTime,DialogTimeoutDeferral,DialogTimeoutDeferralAction,InstallomatorVersionCustomRepoPathandInstallomatorVersionCustomBranchName.Defaults, enums and ranges now match
PreferenceManager:InstallomatorVersiondefaults tomain, notrelease.MenuBarConcealAdvancedActionsandManagedSilentLaunchEnabledno longer listdefault: false. Both are true on a managed Mac when unset, and their descriptions say so.BackgroundPatchStartupJitterMaxMinutesno longer has a maximum of 1440. The code only requires 0 or more.- Enum values in the ProfileCreator manifest are lowercase, matching the Jamf schema. AAP reads them without regard to case, so existing profiles are unaffected.
- Picker-style keys show the Settings names for their values
(
options.enum_titlesandpfm_range_list_titles), for example 1–7 as Sunday–Saturday forPatchWeekStartDay.
Descriptions are rewritten to cover what each key does, its accepted values and its default. They no longer mention 3.5.0 compatibility or issue and build numbers, and the Jamf
linksentries are gone.
Removed
Section titled “Removed”- Keys with no effect in 4.0.0 are removed from the profile manifests. AAP still reads these keys, but nothing uses the values, so a
profile that already sets them keeps working and behaves the same.
- Removed from both manifests:
WorkflowDisableRelaunch,WorkflowDisableAppDiscovery,WorkflowInstallNowPatchingStatusActionandFocusSuppressRetryMinutes. - Since build 74,
WorkflowDisableAppDiscoveryhas one effect: it makes the root helper skip the schedule set byDiscoveryFrequency. Manual discovery, Check for Updates and background patching ignore it, and leavingDiscoveryFrequencyunset does the same, so the key stays out of the manifests. - Removed from the ProfileCreator manifest only, since the Jamf schema
never listed them:
ConvertAppsInHomeFolder,DeferralTimerError,DeferralTimerWorkflowRelaunch,DialogIcon,DialogOnTop,IgnoreAppsInHomeFolder,InstallomatorUpdateDisable,RemoveInstallomatorPath,UnattendedExit,UnattendedExitSecondsandUseOverlayIcon.
- Removed from both manifests:
Build 66
Section titled “Build 66”Released October 1, 2026
- Clear toasts when the deferral dialog is answered. Defer and Install Now remove every App Auto-Patch notification already delivered, including one still on screen. A dialog timeout takes the same path. Temporary banners are cleared in the main app. Persistent alerts are cleared by the persistent notifier, which owns that notification center. Scheduled reminders are left in place. Closing the dialog with the close button does not clear them.
Build 65
Section titled “Build 65”Released September 30, 2026
-
Automatic focus deferrals. While the user has Focus or Do Not Disturb enabled, or a process other than
coreaudiodholds the display awake, an elapsed check defers automatically forDeferralTimerFocusminutes. The dialog stays hidden. A toast is posted only whenFocusDeferralNotificationsEnabledis true; clicking it opens the normal deferral dialog.IgnoreDNDAppsis a comma-separated list of process names excluded from the display-sleep check.DeadlineDaysFocusandDeadlineCountFocusare interchangeable. Either limit ends automatic deferrals, and dialogs then continue with the normal wording. The menu bar counts a user deferral as “Deferred — you’ll be reminded in…”. An automatic focus deferral reads “Focus deferral — checking again in…”, or “next reminder” when focus-deferral toasts are enabled. The days-or-counts line under that countdown is the hard deadline only. A focus deferral also starts when updates are already waiting and Focus is on, not only after a previous deferral expires. Each automatic deferral incrementsFocusDeferralCountonly. A user-chosen Defer incrementsHardDeferralCountonly. A hard deadline reached first disables focus deferrals. A focus limit that is not at least one below the matching hard limit is clamped and logged. If neither focus limit is set, there is no automatic focus deferral. Update All still shows the normal dialog.Focus/DND detection now uses the same active-record
plutillookup as AAP 3.7, with a JSON fallback that examines onlystoreAssertionRecords(never historical invalidation records). Each decision logs whether DND, a named display assertion, an ignored assertion, or neither source determined the result, making intermittent detection failures visible in diagnostics.On current macOS,
~/Library/DoNotDisturbis protected by System Policy. The app logsDND unreadableinstead ofDND inactivewhen that read is blocked. Do Not Disturb cannot start a focus deferral until App Auto-Patch has Full Disk Access (System Settings, or an MDM Privacy Preferences profile forSystemPolicyAllFileson the GUI bundle). The profile’scertificate leaf[subject.OU]must be the signing team of that build:6GBSVT64FSfor Robert Schroeder (the repo default), or4XSYV3BTMXfor Andrew Spokes. A profile that names the other team does not match, and Focus stays undetected. Display sleep assertions do not need that grant. Relaunch after granting access.One
pmset -g assertionssnapshot can omit a meeting process that powerd still considers active, and a pipe read can return empty stdout afterpmsethas already exited. Display-assertion checks capturepmsetthrough a file, read it up to 3 times, 0.5 seconds apart, and do not treat a missing listing as an idle display. That check hides the dialog for 30 seconds and tries again without counting a focus deferral. After 3 consecutive inconclusive checks the dialog shows, so apmsetthat stays broken cannot hide it indefinitely.LockSettings(default false) disables every Settings control, including groups that have no managed key of their own. The About tab keeps its links and the Generate Diagnostic Bundle button.
Build 62
Section titled “Build 62”Released September 29, 2026
-
Closed apps patch before the open-apps notification. Check for Updates was posting “N open apps need updating” at the same moment it asked for a closed-app pass, so the toast could count apps that were about to be patched silently. The closed-app pass now runs first. After it finishes, the patched labels are refreshed, and only then is the open-apps notification delivered. If every pending app was closed and the pass succeeds, no open-apps toast is posted. If the pass does not start or does not refresh the list, the toast is still delivered from the apps that remain.
A background pass keeps its wording for the whole run. The dashboard banner reads “Background patching in progress…”, the menu bar reads “Background patching…”, and both the in-app progress poll and the helper-daemon status line say “Background patching <app>…” instead of dropping back to “Updating <app>…” or “Patching in progress…”.
The pre-deadline deferral toast no longer says updates will install. That toast only fires when the user can still defer. Title is “Your deferral has ended”; the body is “N updates are waiting. Open App Auto-Patch to install now or defer again.”
The deferral prompt no longer waits for the next one-minute poll. Writing
NextAutoLauncharms a one-shot timer for that time. The one-minute timer still reconciles cadence and deadlines, and it is the fallback if the one-shot is missed. If the moment lands while a check or a patch is running, the prompt retries in 15 seconds.
Build 61
Section titled “Build 61”Released September 29, 2026
-
Stage installers before installing. When updates are detected, App Auto-Patch downloads the installers into
/private/tmp/AAPStagebefore it tries to background-patch closed apps, and before any other install. The install then reuses that file (downloadURL=file://…) instead of downloading it again.updateronlylabels are skipped. A version sidecar is replaced when a newer build is detected, and staged files for labels that are no longer pending are removed.Controlled by the same managed key as 3.6,
WorkflowStageUpdates. 3.6 defaulted it to off; 4.0 defaults it to on.Installing one app does not delete staged installers for other apps that still need updates. The staging folder is pruned only when the full pending queue is known.
When staging is on, the open-apps notification waits until those installers have been downloaded, so acting on the notification installs from the local copies.
Build 60
Section titled “Build 60”Released September 25, 2026
-
Helper status after reboot. The menu bar treated the helper as down until the dashboard window appeared, because
helperOperationalstayed at its launch default offalse. A reboot LaunchAgent starts the menu bar with that window hidden, so the item read Setup required even when the daemon was already running. Status is now read at launch and again each time the menu bar popover opens.When the helper is registered but its heartbeat is still stale, App Auto-Patch waits 20 seconds for Background Task Management to load the approved daemon, then asks launchd to kickstart that job. Unregister/register remains the fallback if the kickstart does not produce a heartbeat, and it no longer runs during the wait.
Build 59
Section titled “Build 59”Released September 24, 2026
-
Temporary and persistent notification channels. Two notification identities, modeled on swiftDialog. macOS only lets MDM set
AlertType(banner vs alert) per bundle ID, so App Auto-Patch now embeds a signed, agent-only helper atContents/Helpers/App Auto-Patch Persistent.app(xyz.techitout.appAutoPatch-persistent). Informational toasts post from the main app as temporary banners; notifications that need a click stay as persistent alerts until the user acts. The helper is short-lived: it posts or cancels, waits briefly, and exits.Built-in routing. Temporary by default:
backgroundPatchStarted,backgroundPatchCompleted. Persistent by default:backgroundPatchFailed,updatesAvailable,acPowerRequired,deferralResuming,hardDeadlineReminder,selfUpdateAvailable. Admins can override per type with managed arraysTemporaryNotificationTypesandPersistentNotificationTypes; if a type is listed in both, persistent wins. Unlisted types keep the built-in default.AC power required. When closed-app updates are waiting but
BackgroundPatchClosedAppsRequireOnACis blocking the pass, the app posts one persistent App updates are waiting notification telling the user to connect power. It is deduplicated while the pending count is unchanged and clears when AC returns or the block is gone.MDM. The example
com.apple.notificationsettingsprofile now covers both identities (banner for the main app, alert for the helper). Uninstall purges both notification databases. Release signing verifies the helper’s bundle ID, team, and timestamp.
Build 58
Section titled “Build 58”Released September 24, 2026
-
Running-app detection and the menu bar item. Open apps were being patched as if they were closed.
RunningAppsDetectortreatedproc_listallpids’ return value as a byte count when it is a count of PIDs — once when sizing the buffer and again when walking the result. The buffer ended up capped at 1024 entries and only the first quarter of it was read, so on any Mac with more than 256 processes the scan saw nothing but the lowest PIDs: boot-time system daemons. Every app the user had opened since login looked closed, and both background patchers updated them underneath the user (observed with Google Chrome, Elgato Stream Deck, and Snagit). The buffer now grows until the kernel stops filling it. A scan that still can’t complete returns unknown rather than an empty set, because the two are the sameSetand opposite instructions to a patcher: the background passes now skip, the updates available toast counts every pending app as open, and the helper’s deadline enforcer assumes the GUI is alive rather than taking over.The menu bar item is back.
MenuBarExtra’s label was a bareImage(systemName:)written directly in theAppbody’s ViewBuilder, and SwiftUI never created anNSStatusBarWindowfor it — silent-launch installs had no visible menu bar presence at all. The glyph now lives in a small child view usingLabel+.iconOnly. Separately, theShowMenuBarIcon@AppStoragebindings passed the app’s own bundle identifier as aUserDefaultssuite name, which Foundation rejects and returnsnilfor; the bindings were silently falling back toUserDefaults.standard.The app now notices when its menu bar item never arrives.
MenuBarExtracreates itsNSStatusBarWindowwhether or not the system grants it a slot; an ungranted item is parked just above the top of the screen and nothing is logged, so the app looks like it never launched. Three seconds after the launch wake-up, App Auto-Patch now checks whether any of its status windows is actually on a screen, logs the result either way, and re-inserts the item once if it isn’t. The check is skipped when the icon is switched off, so it can’t override that preference.Quieter helper log. The on-battery skip is throttled to once per five minutes. The AC gate sits before the interval gate in the daemon’s once-per-second loop, so a Mac that had never run a pass while on battery wrote that line every second — about 1 MB an hour, swamping the diagnostic bundle.
Build 57
Section titled “Build 57”Released September 22, 2026
-
Deadline enforcement, background patching, and helper backstop. All of the work below is relative to the original Build 55 snapshot this branch started from (
deadline-testing/f1bb2dd). Marketing version remains 4.0.0;CURRENT_PROJECT_VERSIONis 57.Deadlines actually display and stay correct. Managed-profile lookups for optional date keys (
HardDeferralDate,NextAutoLaunch, etc.) no longer short-circuit tonil, which was why the deadline never appeared in the GUI or menu bar on MDM Macs. Weekly cadence anchors are normalized to start-of-day (PatchWeekStartDay+DeadlineDaysHard).DeferralWatcherreconciles cadence and deadline dates synchronously at start and on every tick, and the menu bar refreshes that state when the popover opens.Required-update dialog without the dashboard. A passed hard deadline raises a standalone panel (not a sheet that needs Settings or the dashboard to already be open). Deferral options are hidden; the remaining control is Install Now plus a countdown driven by
DialogTimeoutDeferral(seconds). Closing the GUI no longer resets that countdown — an already-armedNextAutoLaunchis kept across relaunch. The dashboard now shows a red Installs Automatically countdown pill to the right of Update Deadline, matching the menu bar.DeferralWatcherand the 60/30/5-minute reminder deep-link no longer callactivateDashboardWindow()first, so the dashboard stays closed behind the panel. The panel has its own window identifier so the launch-time invisible window sweep does not hide it.Enforcement if the user quits App Auto-Patch. The root helper (
HelperDeadlineEnforcer) tries to bring the GUI back via the MDM LaunchAgent (kickstart) oropen -a. If that fails, or nobody is logged in, the helper installs every pending update itself after honoring any remaining countdown. New managed keyHardDeadlineDialogAllowClose(defaultfalse): when true, the user may close the hard dialog while the countdown continues; Notification Center reminders fire at 60, 30, and 5 minutes remaining.Closed apps patch first; notifications describe open apps. After a check, closed apps with updates are patched immediately in the background (GUI scheduler and helper-daemon mode). The Updates available toast reports open apps that still need attention separately from closed apps being updated silently. A background pass that finishes while other apps are still pending no longer resets the deferral cycle or turns a hard dialog back into a deferral picker.
Admin Test Mode matches production. Anchor overrides apply/revert via explicit buttons so mid-day dates don’t drift the displayed deadline. Trigger Hard Deadline Prompt uses the same standalone dialog a real deadline uses.
DMG leftovers from version checks.
mktemp -don macOS ignoresTMPDIR, so path-based ownership of mounted images never matched Installomator’s real download location. Cleanup is now termination-scoped: only images orphaned when AAP kills a child (timeout / hang) are swept, withhdiutilplus a macOS 27diskutil image detachfallback. Clean Installomator exits unmount their own images.Discovery and patching no longer hang. Duplicate Check for Updates / XPC discovery passes are coalesced (generation token in the GUI, single-flight queue in the XPC service). Subprocess waits use
Process.isRunningwith bounded SIGKILL reaping so a finished Installomator label cannot park the pipeline for the full 600 s timeout.Logging. RFC3339 timestamps with local offset, append-only
flock-guarded writer, per-line process identity (gui/xpc/daemon). The log viewer merges the system log and the per-user fallback through a shared codec that also parses Installomator’stimestamp : LEVEL : component : messageformat.Uninstall script. Re-execs under zsh when invoked as
sh uninstall.sh(the process substitution that previously died at line 299). Broader LaunchAgent / leftover 3.x daemon / per-user /pkgutil/ BTM cleanup. Also deletes the app’s row from the per-user Notification Center store so 60/30/5- minute hard-deadline reminders cannot fire after the app is gone.Helper registration diagnostics. SMAppService “Operation not permitted” is annotated with the real cause (bundle not in
/Applications, ad-hoc signature, missing plist or helper).Scripts/diagnose-helper.shremains for on-device triage: it isolates the daemon’s own BTM record (a fixedgrep -A 12window used to report a neighbouring disabled entry as ours) and flags launchdEX_CONFIG(78) as the wedged-job leftover from an uninstall that deleted the bundle while leaving the BTM record. A failed auto-reset no longer holds the one-hourLastAutoHelperResetDatedebounce, so a reinstall that inherits that stale record can recover on the next launch instead of waiting it out.Local release signing.
Scripts/release.shsources a gitignoredScripts/release.local.env(seerelease.local.env.example) so a machine without the maintainer certificates can sign and notarize. Missing identities fall back to the first Developer ID Application / Installer in the keychain.developmentTeamIDis rewritten to match for the archive and restored on exit so XPC still accepts the locally signed helper.Scripts/setup-notary-profile.shstores thenotarytoolkeychain profile. The script refuses to run as root: undersudo,codesignsearches root’s keychain for the Developer ID private key and fails witherrSecInternalComponent.UI. Filter segmented control no longer slides off the leading edge when a segment is clicked. Hard-deadline subtitle wraps instead of running off the panel. Menu bar shows live enforcement and deferral countdowns.
Build 55
Section titled “Build 55”Released September 9, 2026
- Webhook URL fields are now masked in Settings (build 55, issue #78).
The Slack and Teams webhook URL text fields in Settings → Advanced →
Webhooks now render as
SecureField(dots) by default. A per-field reveal toggle (eye / eye.slash) shows the URL on demand. Prevents shoulder-surfing of webhook endpoints during screen-shares or in-person walkthroughs.
Changed
Section titled “Changed”-
Hard deadline enforcement now always shows a warning prompt (build 55, issue #51). Previously, when the hard deadline fired outside configured business hours the force-install ran silently with no user notification. The outside-business-hours path now mirrors the inside-business-hours behavior: the
.hardescalation deferral prompt is always shown (“Updates required” headline, no Defer button), andNextAutoLaunchis armed 5 minutes out soDeferralWatcherenforces the install automatically if the user does not act. Users get a visible warning before any forced install, regardless of business-hours configuration. -
Background patching verbiage clarified (build 55, issue #75). Status banners during helper-daemon background passes now read “Background patching in progress…” and “Background patching \u27e8app\u27e9…” instead of the generic “Patching in the background…” / “Updating \u27e8app\u27e9 (1 of N)…” copy, making it immediately clear to users who open AAP while a background pass is running that no user-initiated install is underway.
-
WebhookFeaturecorrected to string enum in Jamf profile manifest (build 55, issue #77). Previously typed asboolean; corrected tostringwith allowed values["off", "all", "failures"]. Jamf Pro Custom Schema will now present a drop-down instead of a checkbox.
-
DiscoverSelfUpdateLabeladded to Jamf profile manifest (build 55, issue #76). The key was present in the ProfileCreator/iMazing manifest but missing fromxyz.techitout.appAutoPatch.jamf.json. Jamf admins can now manage the toggle via Custom Schema. -
Log viewer now renders Installomator-sourced log entries (build 55, issue #68). The helper writes Installomator’s own stdout to the shared
aap.login the formatyyyy-MM-dd HH:mm:ss : LEVEL : component : message. The log viewer parser previously silently dropped these lines because it only searched for[as the timestamp boundary. The parser now detects:as an alternative boundary and handles the Installomator format as a dedicated path, mappingINFO/REQ→ info,WARN/WARNING→ warning,ERROR→ error. Installomator progress (downloads, receipts, app-replacement steps) is now visible in the AAP log viewer. -
Admin Test Mode: anchor override no longer overwritten by DeferralWatcher (build 55, issue #69).
DeferralWatcher.tick()’s stale-anchor reset branch now checksAdminTestModeEnabledfirst; when test mode is active the reset is skipped so a manually-setCurrentPatchingStartDatestays in effect. Added a Trigger Hard Deadline Prompt button to the Test Mode panel (setsHardDeferralDateto one second in the past, then callsupdateAll()), plus descriptive captions under all trigger buttons. -
LabelVersionHelper.zsh DMG cleanup falls back to
diskutilon macOS 27 (build 55, issue #66). macOS 27 deprecatedhdiutil attach -nobrowse -readonly. The EXIT trap’s_aap_eject_new_mounts()now trieshdiutil detachfirst and falls back todiskutil image detachwhen it exits non-zero, covering both current and future macOS versions.
Build 54
Section titled “Build 54”Released September 8, 2026
- Self-Update section in Settings → Advanced (build 54).
SelfUpdateEnabled,SelfUpdateLabel, andSelfUpdateFrequencyare now configurable in the Settings UI, not just via MDM ordefaults write. A new Show App Auto-Patch in the app list toggle (DiscoverSelfUpdateLabel, defaulttrue) controls whether theappautopatchlabel appears in the main dashboard. Behavior matrix:- Discover=on, Auto-update=off — AAP appears in the list; admins can update it manually via Update All.
- Discover=on, Auto-update=on — AAP is hidden;
SelfUpdateSchedulerhandles it silently to prevent double-entry. - Discover=off, either — AAP always hidden. All five keys are disabled with the Provided by your organization caption when any of them is MDM-managed.
Changed
Section titled “Changed”-
Settings window is now vertically user-resizable (build 54, issue #74).
SettingsView’s frame now specifiesmaxHeight: .infinity, which tells SwiftUI/AppKit the content can grow and enables the resize handle on the Settings window. Minimum height bumped 400 → 500 to accommodate the now-taller Advanced tab. -
Credits & Links updated (build 54). The Native macOS app by Robert Schroeder. text is replaced with two tappable author links: Robert Schroeder (@robjschroeder) and Andrew Spokes (@TechTrekkie).
uninstall.shversion detection fixed for macOS 26 — third attempt (build 54, issue #71). Both the originaldefaults read "${INFO_PLIST%.plist}"(extension stripped) and the build-53PlistBuddyapproach proved unreliable on macOS 26.6.2 — both returned empty. Dan confirmed directly thatdefaults read /Applications/App\ Auto-Patch.app/Contents/Info.plist CFBundleVersionreturns53. The script now passes the full path with.plistextension todefaults read, which is stable across all macOS versions.
Build 53
Section titled “Build 53”Released September 6, 2026
-
Enhanced menu bar popover (build 53, issue #65). The menu bar popover is redesigned from a simple button list into a four-section panel (320px wide, up from 260px):
- Header — status icon, status text, and a Last checked: N minutes ago subtitle so the user knows how fresh the data is without opening the dashboard.
- Pending Updates — up to 5 app rows, each showing the app name and a current → latest version chip. When all apps are up to date, a green checkmark row reads All apps up to date. When more than 5 apps have pending updates, a View N more updates → link opens the full dashboard. The section is hidden before the first Check for Updates run so the popover never shows stale zero-state.
- Deadline — shown only when a hard deadline or deferral-remaining
summary is available. Surfaces the hard-deadline date and the
deferralsRemainingSummarycaption without requiring the user to open Settings. Absent entirely on unmanaged Macs with no deadline configured. - Actions — the existing Check for Updates, Update All, Open App
Auto-Patch, Settings (concealed per
MenuBarConcealAdvancedActions), and Quit rows, reorganized with internal dividers.
New managed preference:
MenuBarHideQuit(Bool, defaultfalse). Whentrue, the Quit App Auto-Patch row is removed from the popover entirely regardless of the Option-modifier state, giving admins a harder removal than the existingMenuBarConcealAdvancedActionsOption-reveal mechanic. Surfaced in both profile manifests.
Changed
Section titled “Changed”-
AAP self-update label is now auto-excluded from the main app list (build 53, issue #73). When
SelfUpdateEnabled = true, the label configured bySelfUpdateLabel(defaultappautopatch) is excluded fromAppDiscoveryServicediscovery. AAP handles its own updates viaSelfUpdateScheduler, which runs the label throughperformPatch()directly; surfacing it in the main app list caused a confusing double-entry where App Auto-Patch appeared in both the Updates Available count and the self-update banner. WhenSelfUpdateEnabled = false, the label flows through normal discovery as before. -
ShowMenuBarIcontoggle is now disabled when MDM-managed (build 53, issue #70). When a configuration profile pinsShowMenuBarIcon, the toggle in Settings → About → Menu Bar is now grayed out and shows the standard Provided by your organization through managed configuration. caption, consistent with every other managed preference in the Settings UI.
-
Log viewer now correctly parses build-51 log format (build 53, issue #72). Build 51 changed the log line format from
"EEE MMM dd HH:mm:ss [LEVEL] message"to"yyyy-MM-dd HH:mm:ss [process-tag] [LEVEL] message", butLogViewModel.loadLogs()still used the oldDateFormatterpattern. Every timestamp failed to parse, all lines were silently dropped, and the log viewer showed 0 of 0 entries. The parser now tries the ISO format first, falls back to the legacy format for logs from earlier builds, and strips the process tag ([GUI]/[daemon]/[xpc]) before reading the level bracket. -
uninstall.shversion detection now works reliably on macOS 26 (build 53, issue #71).defaults read <path>stopped returning scalar values from app-bundle Info.plist files on macOS 26.6.2, causing the script to showvunknown (?). Switched to/usr/libexec/PlistBuddy -c "Print <key>" <plist>, which handles both binary and XML plists and has stable path-with-spaces support. Also fixed the typo on the--keep-appnext-steps line (AppAutoPatch→App Auto-Patch).
Build 52
Section titled “Build 52”Released August 30, 2026
- Admin Test Mode panel (
AdminTestModeEnabled, build 52, issue #69). A hidden Test Mode section appears at the bottom of Settings → Deferrals when the managed preferenceAdminTestModeEnabled = trueis deployed. It gives admins direct override controls for testing deferral workflows without manual plist edits: a date picker forCurrentPatchingStartDate(with immediate deadline recomputation), steppers forFocusDeferralCountandHardDeferralCount, a date-and-time picker forNextAutoLaunch, and trigger buttons for Update All, Post Test Notification, and Reset Deferral State. Never deploytrueto production fleets.
Changed
Section titled “Changed”- Deadline dates are now seeded from the patch cycle anchor, not from the first user deferral (build 52, issue #51). In 4.x builds 49–51,
FocusDeferralDateandHardDeferralDatewere written byDeferralManager.recordDeferral()on the first user deferral. This meant: (1)deferralsRemainingSummaryalways returned nil until the user had deferred at least once, so the days-remaining indicator never appeared; and (2) the hard deadline fired X days after first deferral, not X days after the patch cycle started — diverging from 3.x behavior and admin expectations. Both dates are now computed fromCurrentPatchingStartDateand written wheneverDeferralWatcher.tick()sets or advances the cadence anchor (initial seed,shouldResetCycle, andisAnchorStalebranches).recordDeferral()no longer writes these dates.
-
Helper daemon now respects the user’s deferral decision (build 52, issue #63). When
BackgroundPatchClosedAppsScheduler = helperDaemon, the root helper process ran its own background pass on its independent schedule regardless of whether the user had deferred from the GUI. Root cause: the helper never readNextAutoLaunch. Confirmed by the 2026-07-06 diagnostic: user deferred for 30 min at 16:08:46 (NextAutoLaunch = 16:38:46) but the helper started patching at 16:10:53.HelperBackgroundPatchScheduler.tickIfDue()now readsNextAutoLaunchfrom the console user’s local preferences domain before callingrunOnce(). If the deadline is still in the future the pass is skipped with a log line; the next tick after the deadline elapses proceeds normally. -
Computed deadline state is now written to the local plist for EA reporting (build 52, issue #51). Four new runtime keys are written whenever the anchor is seeded/advanced or a user deferrals:
ComputedFocusDeadlineDate,ComputedHardDeadlineDate(ISO 8601 strings — the absolute datetimes the deadlines fire), andDeferralsRemainingFocus,DeferralsRemainingHard(remaining deferral count against configured limits,nilif no count limit configured). These are readable by Jamf Extension Attributes and other MDM reporting tools without requiring a diagnostic bundle.
Build 51
Section titled “Build 51”Released July 12, 2026
- Configurable deferral dialog primary button (
DeferralDialogPrimaryButton, build 51, issue #67). The deferral prompt previously hardcoded the Update button as the primary action (blue, Return-key-bound), meaning an accidental ↩ press triggered an immediate install. A new managed preferenceDeferralDialogPrimaryButton(String, default"update", values:"update"|"defer") lets admins flip the primary action to Defer so users must make an explicit click to install. Has no effect at the.hardescalation level where there is no Defer button. Surfaced in both profile manifests.
Changed
Section titled “Changed”- swiftDialog
AEReceiverIdentifierupdated in PPPC mobileconfig sample (build 51, issue #47). swiftDialog changed its bundle ID fromau.csilvers.SwiftDialogtoau.csiro.dialog. The samplecom.apple.TCC.configuration-profile-policy-AppAutoPatch.example.mobileconfigused the old ID for theAEReceiverIdentifierandAEReceiverCodeRequiremententries, causing the AppleEvents grant to silently fail on any fleet that deployed it. Both entries are now updated toau.csiro.dialog.
-
DMG volumes left mounted after version checks are now ejected (build 51, issue #66).
LabelVersionHelper.zshevals Installomator label fragments to determineappNewVersion. Some labels (e.g.filemaker,adobereader) mount a DMG to read a version string; since AAP runs only the fragment snippet rather than the full Installomator flow, the eject path never fired. Mounted volumes accumulated across every Check for Updates run, becoming visible in Disk Utility and progressively degrading package install performance. The script now snapshots currently mounted volumes beforeevaland registers anEXIT TERM INT PIPEtrap that callshdiutil detach -quiet -forceon any volume that wasn’t present before. The trap fires even when AAP’s Swift side callsprocess.terminate()on a timeout. -
Log timestamps now use local time and include year and process source (build 51, issue #68).
LoggingService.logFormatterpreviously had no explicittimeZone, causing entries from the root helper daemon (which inherits UTC from launchd) to appear 7–8 hours ahead of GUI entries in the same log file. The format also had no year, making log files ambiguous across year boundaries. Fixes:timeZone = TimeZone.currentis now set explicitly on the formatter (all processes use device local time),dateFormatchanged to"yyyy-MM-dd HH:mm:ss"(ISO-style, sortable), and each log line now carries a per-process source tag ([GUI],[daemon],[xpc]) so concurrent writes from multiple processes are distinguishable. -
User-visible strings now use human-readable app names instead of label slugs (build 51, issue #27). The helper-daemon progress banner, Alert row tooltips, and XPC failure error banners previously displayed raw Installomator label slugs (e.g.
imazingprofileeditor) instead of the app’s marketing name (iMazing Profile Editor). Three changes: (1)HelperBackgroundCurrentRunnow carriescurrentLabelDisplayName: String?which the helper populates from its candidate app list and the observer threads through to the GUI status banner; (2)AppDiscoveryServicefailure reasons were audited — they were already user-friendly, no change needed; (3)StatusViewModelgains ahumanizeMessage(_:)helper that replaces known label slugs withdisplayNamevalues in XPC error strings before surfacing them in the error banner. Backward compat: oldhelper-state.jsonfiles withoutcurrentLabelDisplayNamedecode cleanly (the field isString?with implicit nil). Two newHelperPatchStateTestslock in the back-compat decode.
Build 50
Section titled “Build 50”Released July 3, 2026
-
Fleet startup jitter for background patch schedulers (
BackgroundPatchStartupJitterMaxMinutes, build 50). On a fleet that deploys AAP simultaneously, every Mac previously started its first background pass at the same moment — hammering the Installomator GitHub repository and MDM infrastructure at once. A new managed preferenceBackgroundPatchStartupJitterMaxMinutes(Int, default 0 = disabled) causes each scheduler to pick a random first-fire delay in[0, max]minutes, persist it, and reuse it across restarts within the same patch cycle so the offset is stable. Both the GUIBackgroundPatchSchedulerand the root-helperHelperBackgroundPatchSchedulerread the same persisted offset so they don’t independently re-jitter. The offset is cleared at the end of every successful patch cycle (viaresetDeferralCounts()) so the fleet re-staggers each round naturally. Covered by newBackgroundPatchSchedulerTests. -
Aggressive patch-day deferral capping (
AggressivePatchDayDeferral+PatchingEndTime, build 50). WhenAggressivePatchDayDeferral=true, the maximum deferral option offered to the user on hard-deadline day shrinks dynamically asPatchingEndTime(default"17:00") approaches:6 h remaining → 2 h cap, 3–6 h → 1 h, 1–3 h → 30 m, 30–60 m → 15 m, <30 m → 5 m (synthesised if no admin option is short enough). Past
PatchingEndTimethe deferral prompt is bypassed entirely and the update installs immediately, without waiting for the nextDeferralWatchertick. Logic lives in a newPatchingCadenceManager.allowedDeferralMinutesForDeadlineDay()static method so it’s fully unit-tested. 8 newPatchingCadenceManagerTestscover every time-bracket, the synthesised fallback, the past-window empty-list, and the disabled passthrough. -
Deferral audit trail (
DeferralEventRecord, build 50). A newDeferralEventRecordmodel andDeferralEventManagerservice write a per-event JSON file to/Library/Application Support/AppAutoPatch/deferral-events/every time a significant deferral decision is made. Event types:.userDeferred(user clicked Defer — records escalation + minutes),.focusSuppressed(system suppressed the prompt due to macOS Focus/DND),.deadlineForced(hard deadline reached, install fired without interaction),.installationStarted(Installomator invocation begins). The dashboard’s per-app disclosure now shows the last 5 deferral events beneath the existing receipt history, with a per-type SF Symbol and timestamp.DiagnosticBundleServicezips thedeferral-events/folder alongside receipts so admins can answer “I never saw a prompt” disputes with an audit log. Covered by newDeferralEventManagerTests.
Changed
Section titled “Changed”- Background schedulers skip network-dependent phases on captive portals (build 50). Both
BackgroundPatchScheduler(GUI) andHelperBackgroundPatchScheduler(daemon) now perform a pre-flight internet check before any network-dependent phase (Installomator label refresh, XPC discovery). A newNetworkReadinessChecker.isInternetAvailable()utility GETshttps://captive.apple.comwith a 5-second timeout and verifies the response body contains Apple’s canonical"Success"marker; a captive-portal redirect or offline network causes the network phases to be skipped with a[INFO]log line. The apply phase (which runs from already-discovered local data) is intentionally not gated. Falls back totrueon any system-level error so the scheduler is never permanently wedged on flaky hardware. The GUI’s check runs off the main queue viaTask.detachedso the 5-second timeout cannot block the main runloop. Covered by newNetworkReadinessCheckerTestswith a mock URLSession (no live network required).
- Focus-mode suppression now retries automatically via
DeferralWatcher(build 50). When a pending-update prompt was suppressed because the user was in macOS Focus / DND mode,StatusViewModel.updateAll()simply returned —NextAutoLaunchwas never set, soDeferralWatchernever retried and the user had to click Update All again manually when focus ended. A newDeferralManager.recordFocusSuppression(minutes:)now writesNextAutoLaunch = now + FocusSuppressRetryMinutes(new managed pref, default 30 min) so the watcher automatically re-engages the prompt once the window elapses. This method does NOT incrementFocusDeferralCountorHardDeferralCount— system-initiated suppression never counts against the user’s deferral limit. Each suppression also writes a.focusSuppressedDeferralEventRecord(see Added above). NewDeferralManagerTestsverify the counter is unchanged andNextAutoLaunchis set correctly.
Build 49
Section titled “Build 49”Released June 29, 2026
-
Clicking an Updates available notification now opens the deferral prompt directly (issue #51 obs 1, build 49). Pre-build-49
UpdatesAvailableNotifierposted a bareUNNotificationRequestwith no category oruserInfo, so clicking the toast only landed the user on the dashboard with no prompt visible \u2014 TechTrekkie flagged this as a discoverability gap. The notifier now stamps the request with theaap.updatesAvailable.categorycategory and a["intent": "showDeferralPrompt"]payload, and a newAAPNotificationDelegate(wired as theUNUserNotificationCenterdelegate inAppDelegate.applicationDidFinishLaunching) routes the click through an in-processNotificationCenterpost thatStatusViewModelturns into a regularupdateAll()invocation \u2014 i.e. the click behaves exactly like pressing Update All in the dashboard, including deadline branching and the business-hours gate. -
Pre-resume notification when a deferral window ends (issue #51 obs 4, build 49). Pre-build-49
DeferralWatcher.tick()calledupdateAll()directly whenNextAutoLaunchelapsed; a user who wasn’t watching the dashboard had no signal the deferral had ended. The watcher now posts a short App Auto-Patch is resuming toast (reusing the sameaap.updatesAvailable.categorydeep-link plumbing as the updates-available notifier) immediately before re-engagingupdateAll(). Suppressed when AAP is already in the foreground (NSApp.isActive) or whenUpdatesAvailableNotificationsEnabledis off, so it never double-signals or overrides the admin toggle. -
Deferrals-remaining indicator in the deferral prompt (issue #51 obs 3, build 49).
DeferralPromptViewnow renders a caption-style line beneath the Remind me in: picker driven by a newStatusViewModel.deferralsRemainingSummarycomputed property \u2014 e.g. “2 deferrals remaining before the focus deadline.” or “3 days remaining before the focus deadline.”, surfacing the most-immediate of any configured count-/day-based threshold. Returns nil (no indicator) when no deadline is configured or at.hardescalation where there’s no off-ramp left to count, so the prompt stays clean for unmanaged Macs.
Changed
Section titled “Changed”- Background closed-apps passes now refuse to interrupt running apps regardless of the admin
BLOCKING_PROCESS_ACTION(issue #63, build 49). A managedInstallomatorOptionscontainingBLOCKING_PROCESS_ACTION=quit_kill(e.g. Zillow’s profile) meant that even when the running-apps filter was correct, an app opened in the race window between filter and Installomator launch would be killed mid-use \u2014 the admin asked forquit_kill, but the entire point of the closed apps background pass is non-interruption. A newAppAutoPatchConfiguration.installomatorOptionsForBackgroundClosedAppsPasscomputed property rewritesBLOCKING_PROCESS_ACTIONto the non-disruptivetell_userfor the background-closed-apps path only, preserving every other admin-supplied option (NOTIFY, LOGO, etc.). Foreground Update All patches continue to honor the admin’sBLOCKING_PROCESS_ACTIONunchanged.
-
String-typed deadline preferences are now coerced to Int (issue #51 obs 2/3/7, build 49). ProfileCreator / Jamf JSON Schema profiles routinely serialize numeric prefs as strings, so TechTrekkie’s managed plist shipped
DeadlineDaysFocus = "1"/DeadlineDaysHard = "2". The genericreadPreferenceas? Intcast returned nil for string values, silently zeroing the deadline config and cascading into three user-visible failures: hard deadlines never enforced, the Auto-installs by footer never appeared, and Settings \u203a Deferrals showed Day Limit as Off. A newreadPreferenceCoercedInt(key: defaultValue:)overload parses a trimmed string into an Int (falling back to the default on empty / non-numeric input) and is now used for the four 3.x-script-compatible keysDeadlineDaysFocus,DeadlineDaysHard,DeadlineCountFocus, andDeadlineCountHard. Covered by newPreferenceManagerTests. -
Deferring now increments only the matching escalation counter (issue #51, build 49).
DeferralManager.recordDeferral()unconditionally bumped bothFocusDeferralCountandHardDeferralCounton every deferral, so distinctDeadlineCountFocus/DeadlineCountHardthresholds advanced in lockstep and could never be reached independently.recordDeferralnow takes aDeferralEscalationparameter and increments only the focus counter for.normal/.focusand only the hard counter for.hard; every call site passes the escalation that was shown. Covered by newDeferralManagerTestscases verifying the other counter stays put across repeated deferrals. -
Patching cadence anchor now rolls forward without a prior successful patch run (issue #51, build 49).
shouldResetCycle()returns false wheneverLastPatchingCompletionDateis nil \u2014 the steady state for any Mac that has only ever deferred \u2014 so the seededCurrentPatchingStartDatestayed frozen across weekly / monthly boundaries (TechTrekkie’s bundle showed it stuck at2026-05-18eight days later). A newPatchingCadenceManager.isAnchorStale(state:now:)detects when the persisted anchor predates the current effective anchor, andDeferralWatcher.tick()now advances the anchor + resets deferral counters in that case \u2014 mutually exclusive with the existingshouldResetCyclebranch, so Macs that have completed a patch run are unaffected. Covered by newPatchingCadenceManagerTestscases. -
Closed-apps background passes no longer silently patch running apps (issue #63, build 49). Both schedulers filtered against
RunningAppsDetector, but had holes: the helper-side filter let a label through whenapp.appPathwas nil (no bundle-ID fallback), and an app opened in the race window between filter and Installomator launch was patched anyway. 5/21 receipts confirmedclaudedesktopwas patched while running. Defense-in-depth now: candidates with bothappPathandpackageIDnil are skipped with a warning; the helper gains the same bundle-ID fallback the GUI scheduler had; and both per-label loops re-checkRunningAppsDetectorimmediately before invoking Installomator and skip any label that became open mid-pass. Covered by newBackgroundPatchSafetyTests.
Build 48
Section titled “Build 48”Released May 24, 2026
-
Monthly patching cadence (issue #51 phase 2, build 48). AAP 3.x’s
MonthlyPatchingCadenceEnabled/MonthlyPatchingCadenceOrdinalValue/MonthlyPatchingCadenceWeekdayIndexkeys are now wired intoPatchingCadenceManager. WhenMonthlyPatchingCadenceEnabled=true, the patch cycle anchors on the Nth weekday-of-month (e.g. the second Tuesday \u2014 Patch Tuesday) instead of thePatchWeekStartDayweekly anchor. NewmonthlyAnchor(now:)/effectiveAnchor(now:)dispatcher computes the correct anchor for the current month, with the.finalsentinel meaning last occurrence of the configured weekday in the month (handles months that have both four and five occurrences). A 5th-occurrence selection falls back to the last actual occurrence in the month when the 5th doesn’t exist.recordSuccessfulPatchRunandarmedStatedelegate througheffectiveAnchor, so the rest of the cadence machinery (shouldResetCycle, theDeferralWatcherre-arm sweep, the persistedCurrentPatchingStartDateISO 8601 breadcrumb) transparently works with both weekly and monthly anchoring. Locked in by 13 new monthly-cadence tests inPatchingCadenceManagerTests.swiftcovering: Patch Tuesday May 2026, first-Monday boundary, Wednesday-when-September-2026-starts- on-Tuesday,.finalFriday/Thursday May 2026, the 5th-Tuesday fallback,effectiveAnchordispatching early-in-month and after-anchor,recordSuccessfulPatchRun+armedStatedelegate paths. -
One-hour countdown when a hard deadline fires outside business hours (issue #52 phase 2, build 48). Completes the business-hours story started in build 47: AAP now persists a
LastHardDeadlineFiredAtbreadcrumb (ISO 8601, mirrorsNextAutoLaunch) wheneverStatusViewModel.updateAll()’s.hardbranch fires, on both the held-during-business-hours and force-install paths. On everyDeferralWatcher.tick(), AAP consultsBusinessHoursGate.missedAfterHoursEnforcement(...): when the deadline fired outside business hours and the user has since crossed back into the window (e.g. signed in the next morning), AAP armsNextAutoLaunch = now + 60 min, surfaces a Updates required \u2014 installing in 1 hour. status banner, and clears the breadcrumb so the countdown is one-shot. Mirrors the macOS-software-update behavior 3.x users have come to expect: the user gets a one-hour heads-up the morning after an overnight deadline rather than an immediate install at sign-in. Successful patch runs clear the breadcrumb so the next deadline cycle starts clean. -
Settings UI for patching cadence + business hours (build 48). Settings \u203a Deferrals now exposes the cadence + business-hours keys that have been MDM-only since build 47. Two new sections:
- Patching Cadence: weekday picker for the weekly anchor
(
PatchWeekStartDay), days-until-reset stepper (DaysUntilReset), monthly toggle (MonthlyPatchingCadenceEnabled), and ordinal + weekday pickers that surface only when monthly is enabled. - Business Hours: enable toggle plus two
.hourAndMinuteDatePickers for start / end, round-tripping through the canonical"HH:mm"representationBusinessHoursGatealready uses at runtime. Toggling off clears both keys; toggling on seeds a sensible 09:00\u201317:00 default.
MDM continues to win on every key:
isDeferralSettingsManagednow folds inPatchWeekStartDay,DaysUntilReset, and the threeMonthlyPatchingCadence*keys, and a newisBusinessHoursManagedflag disables the business-hours controls when either bound is pinned by a configuration profile. Mirrors the build-46 / build-47 pattern where every new managed pref grows the settings group disabled when locked state machine. - Patching Cadence: weekday picker for the weekly anchor
(
-
Custom Installomator documentation and bundled self-extracting builder script (build 48). Two new artifacts make running an internal Installomator fork (or pushing a fully change-controlled payload) much more discoverable:
Profile Manifests/MDM-CustomInstallomator.mddocuments the two supported workflows: managed-pref-drivenInstallomatorVersion= custom(the simple path, AAP fetches your fork itself) andInstallomatorUpdateDisable=true+ push-your-own-payload via MDM (the change-controlled path). Includes theLabelVersionHelper.zshinteraction note TechTrekkie raised \u2014 the helper is refreshed by AAP independent of the Installomator source so neither workflow disrupts it \u2014 plus the Jamf Pro.zsh-rejection caveat and thepkgbuildworkaround.Scripts/build-appautopatch-installomator-sfx.zsh, authored by Dan Snelson (@dan-snelson) and bundled with permission. Builds a self-extracting.zsharchive containing exactlyInstallomator.sh+fragments/labels/from the requested repo URL + branch, withosascript-prompted self-elevation when run on a target Mac. Stamps the source repo, branch, and short commit into the archive header so admins can audit which Installomator version landed on each Mac. The new MDM doc walks through invoking it.
Changed
Section titled “Changed”uninstall.shSCRIPT_VERSIONis now dynamic (build 48). The uninstall script’susage()banner and startup log line used to carry a hardcodedSCRIPT_VERSION="4.0 (47)"constant that drifted between every build’s git tag. The Info.plist resolution block now runs before argument parsing andSCRIPT_VERSIONis computed as${AAP_VERSION:-unknown} (${AAP_BUILD:-?}), so the same script shipped inside every AAP.appbundle automatically reports the build it was extracted from. No behavior change for end users \u2014 the help text and[INFO]startup line just stay accurate without a manual bump per build.
-
CurrentPatchingStartDateis now seeded on the firstDeferralWatchertick (build 48). TechTrekkie’s build-47 report noted thatCurrentPatchingStartDatewas missing from the preferences plist after manually exercising a deferral cycle \u2014FocusDeferralCountandHardDeferralCountupdated correctly but the cadence anchor remained nil because the build-47 code only persisted it on the success path ofrecordSuccessfulPatchRun. Builds that had never completed a full patch run on a given Mac therefore had no anchor forshouldResetCycleor the upcoming monthly-cadence logic to consult.DeferralWatcher.tick()now seeds the anchor viaarmedState(now:)whenever the persisted value is nil, which is idempotent (it writes the same anchor the cadence math would have computed anyway) and runs on every 60 s tick rather than waiting for a successful patch. Existing Macs that have already completed a patch run see no behavior change; new installs and Macs that never finished a cycle now have a stable cadence anchor available to the build-48 monthly-cadence work. -
Stale Patching issues banner now clears on the next Check For Updates run (build 48). Dan reported (screenshot in diagnostic bundle) that the dashboard’s red Patching issues: codex \u2014 App Auto-Patch couldn’t find a matching update for this app banner stayed visible even after a subsequent Check For Updates run successfully resolved the same label. Root cause:
StatusViewModel.errorMessagewas only ever written by failure paths; the success path never cleared it, so any banner produced by a previous failed discovery persisted until app relaunch.checkForUpdates()now setserrorMessage = nilat the top of the run, before the discovery pipeline kicks off, so a subsequent successful run drops the stale banner exactly the way users expect. Failure runs still set the banner with their own message.
Build 47
Section titled “Build 47”Released May 19, 2026
-
Weekly patching cadence wired up end-to-end (issue #51 phase 1, build 47). AAP 3.x’s
PatchWeekStartDay+DaysUntilResetkeys have been read intoAppAutoPatchConfigurationsince the 4.x line started, but no runtime code consumed them \u2014 admins deploying the keys saw no behavior change. Build 47 wires them up:- New service
AppAutoPatchCore/Services/PatchingCadenceManager.swiftcomputescurrentPatchingStartDate(now:)as the most recent occurrence ofPatchWeekStartDayon or before today (interpreted asCalendar.Component.weekday; 1=Sunday\u20267=Saturday, default 2 matches the 3.x Monday default), andnextResetDate(after:)aslastCompletion + DaysUntilReset. StatusViewModel.updateAll()’s success branch now callsPatchingCadenceManager.recordSuccessfulPatchRun(completedAt:)to persistCurrentPatchingStartDate+LastPatchingCompletionDateas ISO 8601 strings under the shared AAP domain. Existing-anchor preservation: a mid-cycle change toPatchWeekStartDayvia MDM doesn’t shift the in-flight cycle, only the next one.DeferralWatcher.tick()now pre-evaluatesshouldResetCycleon every 60 s tick. When the cycle has elapsed pastDaysUntilReset,armedState(now:)re-anchors the cycle andDeferralManager.resetDeferralCounts()clears the deferral counters + day-deadline dates so the next cycle starts fresh without requiring an app relaunch or another Update All click.- Locked in by
AppAutoPatchCoreTests/PatchingCadenceManagerTests.swiftcovering: anchor math across week boundaries (today-is-anchor, walk-back, month-boundary),shouldResetCycleexactly at thedaysUntilResetboundary, idempotentarmedState(now:), no-completion passthrough, and clamping of out-of-rangePatchWeekStartDay/DaysUntilResetinputs.
Monthly cadence (
monthly_patching_cadence_enabled/_ordinal_value/_weekday_indexper 3.x) is not included in this build \u2014 the ordinal-week-of-month math, its interaction with the weekly anchor, and the auto-rescheduling-on-completion semantics deserve their own focused build.PatchingCadenceManageris structured so a futuremonthlyAnchor(now:)slots in cleanly next tocurrentPatchingStartDate(now:). Tracked for build 48.Settings UI for the cadence keys is not in build 47 either; managed admins deploy via MDM (the existing profile manifest path). A Patching Cadence group in Settings \u203a Deferrals is planned for the build that brings monthly cadence online.
- New service
-
Business Hours \u2014 admin-configurable window during which AAP holds hard-deadline force-installs for open apps (issue #52, build 47). Two new managed preferences,
BusinessHoursStartTimeandBusinessHoursEndTime, formatted as"HH:mm"strings (24-hour). When both are set, AAP suppresses the immediate force-install path for.harddeadlines while the current wall-clock time falls inside[start, end), presenting the new.hard-escalation deferral prompt instead and armingNextAutoLaunchsoDeferralWatcherfires the install automatically the moment business hours close.New service
AppAutoPatchCore/Services/BusinessHoursGate.swiftencapsulates the decision math. Public surface:isWithinBusinessHours(now:)\u2014 inclusive-start / exclusive-end window check, supporting wrap-around windows (e.g.22:00\u219206:00for admins configuring overnight business hours).nextWindowEnd(after:)\u2014 next moment the gate transitions inside \u2192 outside, used to schedule the deferred enforcement viaNextAutoLaunch.missedAfterHoursEnforcement(lastDeadlineFiredAt:, now:)\u2014 decision helper for the 1-hour-countdown path 3.x’s macOS- update-mirror behavior wants when a deadline fired overnight and the user wasn’t reachable. The helper is in place; the persistedLastDeadlineFiredAtbreadcrumb and the actual 1-hour countdown plumbing are deferred to build 48 alongside the monthly cadence.- Lenient
parseMinutesOfDay(_:)accepts admin-entered"8:00"in addition to canonical"08:00", validates hours 0\u201323 and minutes 0\u201359, and treats unparseable / out-of-range input as nil so the gate cleanly disables itself rather than panicking.
Background patching of closed apps is intentionally not gated by business hours \u2014 closed-app patching is by definition non-interrupting. The gate applies only to the hard-deadline force-install branch in
StatusViewModel.updateAll(). Profile manifest descriptions spell this out so admins don’t get surprised.Both schemas (
Profile Manifests/xyz.techitout.appAutoPatch.jsonand\u2026.jamf.json) document the two new keys with full semantics including the wrap-around-window note.Locked in by
AppAutoPatchCoreTests/BusinessHoursGateTests.swiftcovering: HH:mm parsing (valid, lenient, malformed, out-of-range), same-day windows (inclusive start, exclusive end), wrap-around overnight windows, disabled-gate fallthrough,nextWindowEndmath across the day boundary, andmissedAfterHoursEnforcementtrue/false branches.Settings UI for the business-hours keys is not in build 47; managed admins deploy via MDM. A Business Hours group in Settings \u203a Deferrals is planned for the build that brings the 1-hour-countdown plumbing online.
Changed
Section titled “Changed”-
Hold \u2325 for more\u2026 hint removed from the menu bar popover. Dan asked for the discoverability affordance to go. The conceal behavior itself is unchanged \u2014 on managed Macs with
MenuBarConcealAdvancedActions = true, the Settings\u2026 and Quit App Auto-Patch rows are still hidden by default and still revealed on Option-hold \u2014 we just no longer surface the hint row in the concealed state. The popover now ends cleanly after Open App Auto-Patch with no trailing divider when concealed and Option isn’t held. Fleet admins who deploy the conceal pref know the Option-hold affordance exists; the popover stays clean for end users who don’t. -
Hard-deadline deferral prompt has a more prominent presentation (issue #51 phase 1, build 47). The existing 4.x deadline enforcement (force-install at
DeadlineCountHard/DeadlineDaysHard) is unchanged in the common case \u2014 hitting the hard deadline outside business hours still fires the install immediately with no prompt. The new.hardDeferralEscalationcase fires only when the hard deadline is reached but the install is being held by an external gate (today: the #52 business-hours gate). When.hardis presented,DeferralPromptViewswitches to a more imperative shape: red Updates required headline (exclamationmark.octagon.fillglyph instead of the standard orange clock-badge), a subtitle spelling out that the IT-team-set deadline will install now, no deferral picker (there’s no off-ramp left), no Defer button, and an Update Now primary action. This is the prominent-alert side of TechTrekkie’s #51 request \u2014 the user is informed and empowered to fire the queued install themselves rather than waiting for the gate to release.
Build 46
Section titled “Build 46”Released May 19, 2026
-
Status Dashboard header is now pinned at the top of the dashboard (issue #58, build 46). Dan reported that when a dismissible error banner was on screen and the user resized the window down (or scrolled the app list), the Status Dashboard title row and the Discover Applications / Check For Updates / Update All action buttons rolled off the top of the visible area. Root cause: the dashboard body was a single
VStack(alignment: .leading, spacing: 20)with the header as its first child, so the moment the sum of fixed-height children (banners, status cards, timestamp pills, filter row) exceeded the available height, SwiftUI clipped the VStack at the top edge of the window — i.e. the header.StatusDashboardView.dashboardHeaderis now extracted into a separate view and attached as.safeAreaInset(edge: .top)on the body with a.barmaterial backing, which reserves the inset height outside the content region. The remaining VStack (banners, cards, timestamps, filter, app list) lays out beneath the inset; the List shrinks first when the window contracts, and the header stays put regardless of how the rest of the content overflows. Visual proportions are preserved — same.padding()around the header HStack, sameViewThatFitsfull-text/icon-only collapse behavior, same fixed-height action buttons. -
Menu bar popover Open App Auto-Patch now focuses the existing dashboard window instead of spawning a new one each click (issue #57, build 46). Dan reported that repeatedly clicking Open App Auto-Patch in the menu bar popover stacked up duplicate dashboard windows. Root cause: the dashboard scene in
AppAutoPatchAppwas declared asWindowGroup(id: "dashboard"), which is SwiftUI’s multi-window scene type — everyopenWindow(id:)call fromMenuBarContent.openDashboard()materialised a fresh window instance. Switched the declaration toWindow("App Auto-Patch", id: "dashboard"), the macOS 13+ single-instance scene.openWindow(id:)now focuses the existing window when one is already open, re-shows it when it was closed by the wake-up sweep, and creates it from scratch on the very first menu-bar invocation — all without changing theMenuBarContent.openDashboard()call site, which already doessetActivationPolicy(.regular) + activate(ignoringOtherApps:)before invokingopenWindow.closeAllUserFacingWindows(),windowResizability(.contentSize), the.commands { AppCommands() }group, and the invisible-window-sweep activation-policy code all continue to work unchanged becauseWindowstill produces a titledNSWindowwith the samestyleMask. -
Uninstall script now kills the App Auto-Patch GUI / menu bar process (issues #54 and #56, build 46). Dan reported on build 44 that running
uninstall.shleft the App Auto-Patch menu bar icon visible until logout or restart, even though the.appbundle was gone from disk. Root cause: build 36 renamed the GUI executable inside the bundle fromAppAutoPatchtoApp Auto-Patch(with the space), butuninstall.shstill targeted the old binary name withpkill -x AppAutoPatch. Becausepkill -xmatches the process name exactly, that call matched nothing post-build-36; the GUI process kept running, and itsNSStatusItemkept painting the menu bar icon until the process actually exited. Both kill sites inuninstall.shnow target both names:- Step 1 (Stop App Auto-Patch processes and root helper daemon).
SIGTERM is sent to the modern (
App Auto-Patch) and legacy (AppAutoPatch) GUI binary names alongside the existingAppAutoPatchRootHelperandAppAutoPatchServicekills. After a 1-second grace period, SIGKILL is sent to anything still alive. The menu bar icon disappears within seconds of the script reaching step 2, no logout required. - Step 5 (Removing application bundle). Belt-and-braces
pre-removal
pkillagainst both binary names, so if the user relaunched the GUI between step 1 and step 5 (e.g. they clicked the .app from Finder mid-uninstall), the bundle isn’t yanked out from under a live process.
The legacy
AppAutoPatchbinary name is targeted for both passes so an admin uninstalling a build-≤35 install (which never got the rename) also cleans up cleanly.SCRIPT_VERSIONbumped from4.0.0 (36)to4.0.0 (46). The doc comment block above step 1 was rewritten to spell out the new contract: the GUI is killed here — the menu bar extra is hosted by that process, so killing it is required to remove the menu bar icon during uninstall. The postinstall script (Scripts/pkg/postinstall) was already using the modern + legacy pair for its #37 relaunch flow and needed no change. - Step 1 (Stop App Auto-Patch processes and root helper daemon).
SIGTERM is sent to the modern (
Build 45
Section titled “Build 45”Released May 19, 2026
-
Cross-binary preferences domain divergence —
PreferenceManagernow writes/reads through the sharedxyz.techitout.appAutoPatchdomain explicitly (issue #59, build 45).PreferenceManager.readLocalPreference,writePreference, anddeletePreferenceusedUserDefaults.standard.addSuite(named:), which adds the AAP domain to the read search list but leaves the calling binary’s own bundle-ID-keyed domain as the primary write target. AAP ships three binaries with three different bundle identifiers (xyz.techitout.appAutoPatch,\u2026.service,\u2026.roothelper), so GUI writes landed in the shared domain, but XPC service and helper writes landed in their own private domains. Reads from those binaries hit the private domain first \u2014 returning whatever stale value happened to be there \u2014 and only fell through to the shared AAP domain when the private one was empty. End-user effect: the GUI’sIgnoredLabels = ["postman"]persisted to~/Library/Preferences/xyz.techitout.appAutoPatch.plist, but the XPC service-driven discovery sawIgnoredLabels = [](or stale state fromxyz.techitout.appAutoPatch.service.plist) and never applied the user’s filter; Postman kept appearing in the dashboard.defaults find IgnoredLabelsonly checked user-domain plists, so the divergence was invisible from the shell. Switched all three accessors toUserDefaults(suiteName: localPreferencesDomain), which returns aUserDefaultsinstance scoped to the shared AAP domain regardless of which binary creates it; both reads and writes now target the samexyz.techitout.appAutoPatch.plistfor the GUI and XPC service. The helper-running-as-root cross-user case is handled separately by the overlay code shipped in #61 (HelperBackgroundPatchScheduler.loadConfigOverlayingConsoleUser). Validated end-to-end on macOS 26: after the fix and a clean on-disk state, the GUI’sIgnoredLabelsflowed through to the XPC service’s discovery pass (Ignored: 65, Parsed: 9with Postman correctly excluded). While diagnosing this, we also discovered a leftover_manualProfileconfiguration profile in robjschroeder’s/Library/Managed Preferences/<user>/that containedIgnoredLabels = "1password"and persisted across every wipe of the local plists; not a code bug, but worth a callout for testers \u2014 manual.mobileconfiginstallations stick around until explicitly removed and cfprefsd serves them as the highest-priority source. -
Menu bar popover unresponsive on consumer launches after the
LSUIElement = trueflip (issue #60, build 45). Build 40b madeLSUIElement = trueunconditional inInfo.plistso AAP launches as a pure agent app from the moment macOS starts the process. TheMenuBarExtraevent-routing wake-up sequence inAppDelegate.applicationDidFinishLaunchingwas still gated onsilentLaunchActive, so consumer Macs (no MDM, noManagedSilentLaunchEnabled) launched into the agent posture without the wake-up firing. SwiftUI’sMenuBarExtra(style: .window)on macOS 26 silently dropped popover clicks until the app had been activated at least once; users had to open AAP from/Applications/once before the menu bar icon would respond. Worse, double- clicking the .app in Finder also didn’t bring AAP to the foreground becauseapplicationShouldHandleReopengated itssetActivationPolicy(.regular)+activate(\u2026)onif !hasVisibleWindows\u2014 once any window was visible the activation no-op’d and the Finder menu bar stayed on the previous app. Two small changes inAppAutoPatch/App/AppAutoPatchApp.swift: the wake-up sequence is now unconditional (every launch, not just silent-launch), andapplicationShouldHandleReopenalways forces.regular+activate(ignoringOtherApps: true). Cost on a consumer Mac is a single Dock-icon flash for <300 ms during the wake-up sweep, same trade-off iStat Menus / Magnet / Hazel make. Validated end-to-end on macOS 26. -
Helper-daemon scheduler now respects the user’s
IgnoredLabels(issue #61, build 45). TechTrekkie reported repeated background patch attempts against apps that were on his ignore list in the GUI (most notablythunderbird). Diagnostic bundle made the bug obvious:HelperBackgroundPatchScheduler.loadConfigOverlayingConsoleUseronly overlaid the sevenBackgroundPatchClosedApps*keys from the console user’s plist, never the label-list keys, so the helper ran discovery against root’s own seeded defaults. Result: on every consumer Mac inhelperDaemonmode the helper’s ignore list was frozen at the eight hard-coded defaults (microsoft*,googlechrome*,jamfconnect,zoom*,1password*,firefox*,homebrew,androidstudio); anything the user added in the GUI was invisible to the helper. Two changes land this build:-
HelperBackgroundPatchScheduler.loadConfigOverlayingConsoleUsernow overlaysIgnoredLabels,RequiredLabels,OptionalLabels, and the new #50IgnoreExternalDrives/IgnoreFilePathsfrom the console user’s plist using the samepm.isManaged(key:)precedence already used for the background-patch keys. Managed (MDM) values still win; user values win over root-side defaults when no MDM key is present. -
PreferenceManager.loadConfigurationdefaults seeding is now gated ongetuid() != 0. Pre-build-45 the helper running as root would fall into the one-shotIgnoredLabelsDefaultsSeededbranch and write the eight hard-coded defaults to/var/root/Library/Preferences/xyz.techitout.appAutoPatch.pliston its first tick \u2014 which then masked the user’s actual list forever. The helper is now a pure reader ofIgnoredLabels; only the GUI (and the XPC service inheriting the user’s environment) seeds defaults.
Why it surfaced now: the bug was latent until build 42’s #48 fix made the
helperDaemonscheduler actually run (pre-build-42 theBackgroundPatchClosedAppsSchedulerrawValue parse silently fell back to.inApp). On a fresh-install Mac the GUI and helper happened to seed identical lists, so the divergence only became visible the moment the user added or removed a label in the GUI \u2014 which is why TechTrekkie couldn’t reproduce it on his second test Mac. Companion fix #59 ensures the GUI and XPC service share the same on-disk plist so the helper’s overlay has correct data to mirror in the first place.No tests added: both code paths are runtime-dependent (
getuid(),SCDynamicStoreCopyConsoleUser, on-disk plists) and have no practical unit-test seam. Validation path is documented in the GitHub issue: install AAP, add a label to the GUI’s ignore list, switch scheduler tohelperDaemon, wait for the interval, confirm the helper doesn’t queue the ignored label. -
-
Discovery-side ignore filters: external drives + admin file-path list (issue #50, build 45). TechTrekkie reported that when a backup external drive is mounted at install time, the helper’s Spotlight (
mdfind) fallback can resolve to the backed-up copy on/Volumes/...instead of the live/Applications/...bundle, and AAP then offers to “update” an app that isn’t really there. Two new managed preferences land this build to give admins (and individual users) the control 3.x had:IgnoreExternalDrives(Bool, defaultfalse). When true,AppDiscoveryServiceexcludes installed-app matches that resolve to volumes other than the boot volume \u2014 external drives, Time Machine backups, disk images, network mounts. The hard-coded probe order (/Applications,/Applications/Utilities,~/Applications) is unaffected; the gate fires on themdfindfallback and on labels whosetargetDir=points at a non-boot volume. Defaultfalseso existing setups that deliberately install to a secondary boot volume don’t lose apps from the dashboard on upgrade.IgnoreFilePaths(Array<String>, default[]). Admin-extensible list of case-insensitive substrings tested against every resolved installed-app absolute path. Combines additively with four hard-coded defaults that are always honored, carried forward from the 3.x zsh implementation:/Daemon Containers/,/Library/Application Support/JAMF/Composer/,/Applications/Chrome Apps.localized/(Chrome PWAs), and/Applications/Edge Apps.localized/(Edge PWAs). Mirrors the 3.x*<exclude>*find semantics; useful for vendor-specific install folders, internal QA repos, mounted SMB shares, etc.
Implementation lives in
AppAutoPatchCore/Services/AppDiscoveryService.swiftas three pure helpers \u2014defaultIgnoredFilePathSubstrings,effectiveIgnoredFilePathSubstrings(additional:),pathContainsIgnoredSubstring(_:ignoredSubstrings:), andisPathOnExternalVolume(_:bootVolumeURL:)\u2014 plus a singleshouldIgnoreInstalledAppPath(_:)instance hook the probe loop and themdfindfallback both call before returning a candidate. External-volume detection usesURLResourceKey.volumeURLKeyand compares against the boot volume’s URL, so APFS firmlink paths under/System/Volumes/Dataare correctly treated as boot-resident. Volume resolution that fails fails open (treats the path as boot) so a transient ENOENT or sandbox quirk doesn’t silently drop every match.GUI surface: new App Discovery section in Settings \u203a Advanced with a toggle for
IgnoreExternalDrivesand a multi-line text editor forIgnoreFilePaths(one path per line; commas accepted as a forgiving fallback). The section is disabled when either key is managed by MDM, matching the existing locking pattern for other managed groups.SettingsViewModelround-trips the user list through a newsanitizedIgnoreFilePathsaccessor that trims whitespace, drops empty entries, and dedupes case-insensitively, so the on-disk preference value stays clean even if a user pastes duplicates.Profile manifests for both ProfileCreator/iMazing (
Profile Manifests/xyz.techitout.appAutoPatch.json) and Jamf Pro (Profile Manifests/xyz.techitout.appAutoPatch.jamf.json) document the two new keys.Locked in by new test cases in
AppAutoPatchCoreTests/AppDiscoveryServiceTests.swiftcovering the effective ignore list (defaults present, admin additions appended, case-insensitive dedup), the substring matcher (anywhere-in-path, case-insensitivity, unrelated path, empty entries safe), and the external-volume helper (boot-volume path not flagged, unreadable path fails open).
Build 44
Section titled “Build 44”Released May 16, 2026
- Helper-daemon background-patch live progress visibility (issue #39, build 44). Pre-build-44, the
helperDaemonscheduler kept the GUI completely silent while a pass was in flight: the dashboard stayed on whatever stale state it had, the menu bar icon didn’t flip to the patching glyph, and the only user-visible signal was the completion toast minutes later. That made it easy to mistake an active pass for an idle one and click Update All on top of it, or assume nothing was happening. Build 44 publishes per-label progress from the helper through the sharedhelper-state.jsonfile. The schema gains a new optionalcurrentRunblock (AppAutoPatchCore/Models/HelperPatchState.swift) carryingrunID,startedAt,totalLabels,completedCount, andcurrentLabel; the helper’sHelperBackgroundPatchSchedulerwrites it before the per-label loop and updates it between every Installomator install, then clears it back tonilinpersistLastRunwhen the pass completes.HelperBackgroundPatchObserverdoes adaptive polling \u2014 5s while a pass is in flight, 60s otherwise \u2014 and emits two newNotificationCenterevents:.appAutoPatchHelperPassStarted(once per newrunID) and.appAutoPatchHelperPassProgress(coalesced to only fire whencompletedCount/currentLabelchanges).StatusViewModelsubscribes to both and drivesisPatching = true,currentOperation = .patchBackground, and a status message that mirrors the in-app scheduler’s UX (“Updating <label> (N of M)\u2026”). The Completed event clears that state back out so the dashboard transitions from patching \u2192 refreshing \u2192 idle in a single hand-off. A 1-hour staleness gate oncurrentRun.startedAtguards against a helper that crashed mid-pass leaving the banner stuck on. The notify-on-completion preference now only gates the user-visible toast; the in-flight banner and refresh signal always fire so admins who disable completion toasts still get accurate dashboard state. Back-compat is preserved: state files written by build 43 and earlier decode cleanly withcurrentRun = nil, and the GUI behaves exactly as it did before for those files. Locked in by newAppAutoPatchCoreTests/HelperPatchStateTests.swiftcases coveringHelperBackgroundCurrentRunround-trip, back-compat decode, in-flight + previous-summary co-existence, and the completion-clears-currentRun handoff.
Build 43
Section titled “Build 43”Released May 16, 2026
-
Team ID verification on installed bundles (issue #49, build 43). 3.5.0’s `verifyApp()` zsh function ran `codesign -dr - –display –verbose=4` against every discovered bundle and compared the local `TeamIdentifier=` against the Installomator label’s `expectedTeamID`; on mismatch it logged a warning and excluded the label from update consideration. 4.0 parsed and surfaced `expectedTeamID` but never actually compared it, so a sideloaded / third-party-signed copy of (e.g.) Xcodes would be flagged as having an update available and clicking Update All installed the official build to `/Applications` alongside the sideloaded copy without warning the user. New `AppAutoPatchCore/Services/TeamIDVerifier.swift` reads the bundle’s `kSecCodeInfoTeamIdentifier` via the Security framework (in-process, no codesign subprocess) and `AppDiscoveryService` populates two new `AppInfo` fields: `bundleTeamID` and `teamIDMismatch`. `StatusViewModel.appHasUpdate` and `updateAll` both filter on `!teamIDMismatch` so mismatched labels are surfaced in the dashboard but excluded from Update All. The dashboard’s app row shows Signature mismatch \u2014 won’t update in place of the latest-version line, and the row’s status glyph becomes an orange lock-shield with a tooltip spelling out Expected: X / Found: Y. The detail view renders both Team IDs side-by-side; the Found value is colored orange when it doesn’t match. Unsigned / ad-hoc-signed bundles and bundles whose label declares no `expectedTeamID` are left alone (verification is skipped, not blocking) so transient codesign hiccups don’t make every label fall off the dashboard. Locked in by `AppAutoPatchCoreTests/TeamIDVerifierTests.swift`.
-
Per-label failure reason on Alert rows (issue #45 follow-up, build 43). Dan reported that the Re-check All Alerts button appeared not to be doing a full check because the same labels kept showing up. Trace through the code path confirmed the re-check is genuinely running every label through the full helper pipeline (LabelVersionHelper.zsh, no cache), but the dashboard offered no way to tell why each row was failing. `AppInfo` gains a new `updateCheckFailureReason: String?` field; `AppDiscoveryService` populates it with a short, user-facing reason whenever a version check fails (“Couldn’t reach upstream version source after N attempt(s)”, “Installomator is not yet installed”, “Lightweight version helper failed and fallback is disabled”, \u2026). The dashboard surfaces the reason as a tooltip on both the Alert glyph and the per-row Re-check button, so an admin clearing a fleet-wide rate-limit episode can tell at a glance whether every row is failing for the same reason or for different ones without opening the log. Discovery also emits an info-level “Version check failed for X after Ns \u2014
” log line on every failure so the same context shows up in diagnostic bundles. -
Conceal Menu Bar Settings & Quit (issue #46). New managed preference `MenuBarConcealAdvancedActions` (default `false` on consumers, `true` on managed Macs via `PreferenceManager.readMenuBarConcealAdvancedActions()`’s three-tier resolution). When true, the Settings\u2026 and Quit App Auto-Patch rows at the bottom of the menu bar popover are hidden by default and only revealed when the user holds the Option (\u2325) key while the popover is open. A subtle Hold \u2325 for more\u2026 hint appears in the concealed state so the affordance is discoverable. Mirrors the secondary-click reveal pattern from Jordy’s Support.app. Surfaced in both profile manifests with the same description.
-
MDM \u2014 App Management permission documentation (issue #47). New `Profile Manifests/MDM-AppManagement.md` walks admins through pre-approving AAP’s TCC App Management permission (required by macOS Sequoia / 15+ before any process can modify installed `.app` bundles) via a `com.apple.TCC.configuration-profile-policy` profile. The doc explains the non-obvious wrinkle that the GUI bundle ID alone won’t enable the rule \u2014 the grant has to target the root helper, because that’s the process that actually runs Installomator and rewrites bundles. Ships with a known-good sample at `Profile Manifests/com.apple.TCC.configuration-profile-policy-AppAutoPatch.example.mobileconfig` that includes `SystemPolicyAppBundles` entries for both the GUI app AND the helper, plus an AppleEvents entry for swiftDialog coordination. Resolves Dan’s confusion about why a GUI-only-targeted profile “appears correct” but “doesn’t show as enabled” in System Settings.
Build 42
Section titled “Build 42”Released May 16, 2026
-
Background-patch refresh + completion notification when the helper-daemon scheduler is configured (issue #48, build 42). TechTrekkie reported on build 41 that after the helper-driven background pass completed, the menu bar and dashboard kept showing the patched apps as “needs update” until he manually clicked Check for Updates, and that no completion notification fired even though one of the six labels in the pass had failed. The diagnostic bundle made three distinct bugs visible:
-
BackgroundPatchClosedAppsSchedulerprofile-value parse mismatch. Both profile manifests document the values as"inApp"/"helperDaemon"(the Swift case names), butBackgroundPatchSchedulerMode.rawValueis"app"/"helper". Pre-build-42, deployinghelperDaemon(as documented) silently fell back to.inApp. That meant the GUI’s in-app scheduler ran the pass, the helper-daemon scheduler never ran, andHelperBackgroundPatchObserverwas never engaged at all \u2014 even though TechTrekkie’s profile explicitly opted into it.PreferenceManager.loadConfiguration()now accepts both spellings (case-insensitively, trimmed) and emits a warning when an unknown value is seen so future typos surface in the log instead of silently downgrading. -
HelperBackgroundPatchObserversilent-seed misfire on first install. On a fresh install, the helper completes its first pass before the GUI’s observer has ever recorded a runID. The pre-build-42seedLastObservedRunIDIfNeededunconditionally swallowed that very first runID as “already announced”, so the user never got a toast even though the pass produced real receipts. Build 42 gates the silent seed on the summary’scompletedAt: passes older than 10 minutes still seed silently (correctly avoiding re-announcing a previous session), but passes inside that window now post the notification and the refresh signal on the spot. -
No refresh after helper-driven pass. Pre-build-42, the observer posted a user notification but never told the dashboard to re-discover the labels the helper had just patched. The menu bar count, Dock badge, and dashboard list all stayed stale until the next manual refresh. Build 42 introduces a new
.appAutoPatchHelperPassCompletedNotificationCenterevent (userInfo:labels,runID,completedAt,hasFailures).HelperBackgroundPatchObserverposts it after every observed pass;StatusViewModelsubscribes ininit()and, on receipt, invalidates the receipts cache and callscheckForUpdates(forLabels:)for just the labels in the pass \u2014 fast subset path, no full re-scan.
End-user effect on managed Macs with
BackgroundPatchClosedAppsScheduler = helperDaemon: the menu bar count and dashboard now drop as soon as the helper finishes, the user notification fires with a partial-failure-aware body when any labels failed, and the receipts cache is fresh the next time the dashboard’s per-app disclosure is opened. -
Build 41
Section titled “Build 41”Released May 14, 2026
- Menu bar popover unresponsive on first install after silent launch (issue #33 follow-up, build 41). Build 40’s silent
launch (
ManagedSilentLaunchEnabled = true+ the newLSUIElement = trueposture) successfully hid the Dock icon and dashboard, but tester robjschroeder reported that the menu bar icon’s popover dropped clicks silently until the user manually double-clicked the .app in/Applications/once. Root cause: SwiftUI’sMenuBarExtra(style: .window)on macOS 26 only fully bootstraps its event-routing after the app has been activated at least once on the current launch. Without anapplicationShouldHandleReopen-style activation, the popover’sNSPanelnever wires up its mouse-down handler. Build 41 adds a one-shot MenuBarExtra wake-up sequence inAppDelegate.applicationDidFinishLaunching(gated onsilentLaunchActive): 1.5 seconds after launch the app briefly flips activation policy to.regular, callsNSApp.activate(ignoringOtherApps: true), and starts a tight-poll invisible window sweep on every main-runloop tick. As soon as SwiftUI materialises the auto-shownWindowGroupwindow the sweep setsalphaValue = 0andorderOut(_:)s it, so the user never sees a visible frame. After ~300 ms the sweep stops, activation policy reverts to.accessory, andalphaValueis restored on the hidden windows so the next user-initiated open from the menu bar renders normally. End-user effect: the menu bar popover responds to the very first click after a fresh install with no visible window flash.
Build 40
Section titled “Build 40”Changed
Section titled “Changed”-
Silent launch is now the default on managed Macs (issue #33 follow-up, build 40). Build 39 made
ManagedSilentLaunchEnabledan explicit opt-in (defaultfalse), so an admin had to remember to set that key on top of every other AAP setting they were deploying. Testers (TechTrekkie, robjschroeder) immediately hit the surprise that installing AAP with a config profile that didn’t includeManagedSilentLaunchEnabled = truestill flashed a Dock icon and popped the dashboard, which isn’t what a managed fleet wants.Build 40 changes the resolution order in
PreferenceManager.readManagedSilentLaunchEnabled():- If the key is explicitly set in the managed-preferences plist (either value), that wins.
- Otherwise, if any AAP-aware managed preference is present
in the plist (
isManagedByMDM() == true), silent launch defaults totrue. This is the common fleet-admin case: deploy a profile withIgnoredLabels/DeferralTimerDefault/SupportTeamWebsite/ whatever, and AAP becomes a quiet background app automatically. - On a fully unmanaged consumer Mac (no AAP profile at all),
the default stays
falseso consumer launches behave exactly like before — dashboard visible, Dock icon, etc.
Admins who want managed Macs to keep the dashboard visible on launch (e.g. for kiosk-style deployments) can deploy
ManagedSilentLaunchEnabled = falseexplicitly. The profile manifest descriptions for both schemas have been updated to spell this out, andProfile Manifests/MDM-LaunchAgent.mdno longer asks admins to deploy the key as a separate opt-in step. -
Deferral prompt primary button now reads Update (issue #43). The apps in question are already installed; AAP is updating them, not installing fresh copies. Renamed the prominent action in the Updates Available sheet from Install Now to Update; keyboard shortcut and styling are unchanged. The Defer / Remind Me off-ramp copy is unchanged.
-
First-run setup wizard hides Skip Setup on managed Macs (issue #32). When this Mac has any AAP-aware managed preference deployed by an MDM (detected via the new
PreferenceManager.isManagedByMDM()derived flag), the wizard no longer shows the bordered Skip Setup button, so a fleet user can’t dismiss onboarding before the helper + Installomator are actually ready. Consumer Macs still see the button. Detection is based on the presence of any of a representative set of AAP keys in/Library/Managed Preferences/xyz.techitout.appAutoPatch.plist; the Done button’s auto-complete-when-ready semantics are unchanged. -
.app bundle renamed to
App Auto-Patch.app(issue #3 final pass). Pre-build-36 the bundle on disk was/Applications/AppAutoPatch.app, which read awkwardly next to peers likeFinal Cut Pro.app,Microsoft Word.app, andApp Store.app. Now ships as/Applications/App Auto-Patch.app, with the embedded binary atContents/MacOS/App Auto-Patch.Driven by
PRODUCT_NAME = "App Auto-Patch"on the AppAutoPatch target.PRODUCT_MODULE_NAMEis pinned toAppAutoPatchso the Swift module name stays space-free (PRODUCT_NAME would otherwise derive aApp_Auto_Patchmodule name via the c99-extidentifier transform). Bundle identifier (xyz.techitout.appAutoPatch) and the helper / XPC service / framework target output names are intentionally unchanged.Migration:
Scripts/pkg/postinstallnow removes any leftover/Applications/AppAutoPatch.appfrom a pre-rename install before the new bundle takes over, so testers upgrading from build ≤ 35 won’t end up with both bundles side-by-side.uninstall.shremoves both paths too. The root helper LaunchDaemon is unaffected: it lives inContents/Library/LaunchDaemons/inside whichever bundle is installed, and the helper-cdhash auto-reset on first launch of build 36 re-binds SMAppService to the new path.MDM admins: any smart group / Jamf extension attribute that matches on the bundle path needs to OR both
/Applications/App Auto-Patch.appand/Applications/AppAutoPatch.appduring the migration window. Matches byCFBundleIdentifierare unaffected (identifier didn’t change). -
Failure messages are now end-user-friendly across every Installomator exit code (issue #27 follow-up). Build 24’s plain-English mapping was an improvement, but Dan flagged it was still too Mac-admin-facing (e.g. “set IGNORE_APP_STORE_APPS=yes in Installomator Options”). Every
Detail.descriptioninAppAutoPatchCore/Services/InstallomatorExitCode.swifthas been rewritten with the same vocabulary an end user would recognize: dropsInstallomatorby name, drops config keys (IGNORE_APP_STORE_APPS,BLOCKING_PROCESS_ACTION), drops Team ID and root jargon, and renamesshortNamevalues like “Not running as root” → “Permission needed” and “Process running” → “App in use”. The dashboard failure banner also drops the trailing(Installomator exit N)parenthetical — the raw exit code now appears only in the receipt-row tooltip in the dashboard, which is the right place for admins who need to cross-reference the Installomator wiki. Unknown / future codes keep the numeric value inline (“… unexpected error (code N)”) so admins still have something to google. A new vocabulary-guardrail test (testNoKnownDescriptionLeaksAdminVocabulary) fails CI if a future contributor reintroduces any of the banned admin terms. -
Sidebar collapsed by default on first launch (issue #36).
ContentView’sNavigationSplitViewnow defaults to.detailOnlycolumn visibility so the dashboard is the first thing every user (consumer or managed) sees. End users don’t need the Logs surface unless support has asked them to look; admins can toggle the sidebar on demand via the standard toolbar sidebar-button or View › Show Sidebar (⌥⌘S). -
“Failed” bucket renamed to “Alerts” (issue #30). Apps where the version-retrieval check couldn’t determine a latest version used to land in a Failed card / filter / row coloring with red copy and a red error glyph. Help-desk teams were treating these as actual patch failures and opening tickets, even though no install was attempted. The bucket is now called Alerts, drops to an orange triangle glyph, and the row text reads “Couldn’t retrieve latest version” rather than “Failed to retrieve latest version”. The set of apps surfaced is unchanged — only the framing. Internally,
StatusViewModel.failedCountwas renamed toalertCountand theFilterMode.failedcase toFilterMode.alerts. -
Helper / XPC service display names use the marketing name (issue #3 follow-up). System Settings › Login Items & Extensions showed the root helper as AppAutoPatch Root Helper and the XPC service as AppAutoPatchService. The
INFOPLIST_KEY_CFBundleDisplayNamefor both targets is now App Auto-Patch Root Helper and App Auto-Patch Service respectively, so the marketing name reads consistently across every macOS surface that exposes it. Bundle identifiers and on-disk paths are intentionally unchanged. -
Background patching is now visible from the moment it starts (issue #28). Two small changes that target the “I had no idea AAP was doing anything until it was already done” tester report on the in-app scheduler:
- The status banner is now seeded optimistically. Instead of
showing “Background patching N application(s)\u{2026}” for the
1\u20132 s before the first poll of the helper’s
progress-<jobId>.jsonfile lands, the dashboard immediately reads “Updating(1 of N)\u{2026}” . The 2 s poll loop overwrites this as the helper advances through the queue, so the banner stays live throughout the pass instead of starting blank. Applies to Update All and per-app updates too \u2014 not just background runs. BackgroundPatchSchedulernow posts a single “App Auto-Patch is updating closed apps\u{2026}” user notification when a background pass begins, listing the first few app names plus the total count. Gated on the existingBackgroundPatchClosedAppsNotifyOnCompletiontoggle so users who already opted out get nothing extra; piggybacks on the same notification permission as the completion toast so there is no second prompt. Helper-daemon-mode background passes still do not surface a starting notification \u2014 that gap requires the helper to publish in-flight state and is tracked separately.
- The status banner is now seeded optimistically. Instead of
showing “Background patching N application(s)\u{2026}” for the
1\u20132 s before the first poll of the helper’s
-
Failed-patch banners and receipt rows now use plain English instead of raw Installomator exit codes (issue #27). A new
AppAutoPatchCore/Services/InstallomatorExitCode.swiftmaps the canonical Installomator exit codes (0–16, 20–24, 77, 98, 99) to a short label (e.g. “Process Running”, “Installed via App Store”, “Download Failed”) and a full sentence describing what the code means in user-facing terms. The Status dashboard’s receipt rows now show that short label in place of “exit 23”, with a tooltip that still surfaces the full description and the underlying numeric code so power users can cross-reference the upstream Installomator exit-code table. When a patch run fails, the banner now reads e.g. “Slack: another copy of the app is currently running and the user dismissed the prompt to quit (Installomator exit 23)” per failed label, instead of a single “failed with exit code 23” line. Cancelled labels are reported separately. Unknown / future exit codes fall through to “Error N” + a generic description containing the raw code, so the helper never ships a label as silent. Locked in byAppAutoPatchCoreTests/InstallomatorExitCodeTests.swift. -
LoggingServiceexposes a newverbose(_:)method, plusapplyVerbosity(verbose:debug:)andrefreshVerbosityFromPreferences()for callers that want to re-read the preference values explicitly. The GUI app, XPC service, and root helper all callrefreshVerbosityFromPreferences()at startup, on every XPC request, and on every helper-scheduler tick so a Settings change propagates to all three processes promptly.
-
Silent launch no longer shows a Dock icon (issue #33 follow-up). TechTrekkie reported on build 38 that even with
ManagedSilentLaunchEnabled = true, AAP briefly flashed a Dock icon on first launch before settling into the menu-bar-only posture. Build 38 set the activation policy to.accessoryinapplicationDidFinishLaunching, which runs after macOS has already shown the launch animation and installed the Dock tile. Build 39 moves the policy flip toapplicationWillFinishLaunching, which runs before the SwiftUI scene tree wakes up and before the persistent Dock entry is created, so the Dock icon never appears. (The very brief launch animation can’t be killed from app code alone — a LaunchAgent-driven launch skips it entirely; see the newProfile Manifests/MDM-LaunchAgent.md.) -
Logs › Clear no longer truncates the on-disk log file (issue #42). Dan reproduced the data-loss on build 37: clicking the trash-can Clear button in the Logs viewer zeroed out
/Library/Application Support/AppAutoPatch/logs/aap.log(wc -lgoing from 17 to 0 after a fresh install), nuking forensic state he was actively reading. Pre-build-38LogViewModel.clearLogs()calledFileManager.createFile(atPath: logPath, contents: nil), which is genuinely destructive. Clear is now a UI-only affordance: it empties the in-memorylogsarray and disables the button, leaving the on-disk log untouched. Click Refresh to repopulate the list from the (intact) file. The button’s tooltip now spells this out so the next admin doesn’t have to read the source to be sure. -
Header buttons keep a consistent height while spinning (issue #44). Pre-build-38 the active header button shrank when its content swapped from an SF Symbol to a
ProgressViewscaled down to 0.7×, because the surroundingButtonhappily resized to whatever its innerHStackreported as intrinsic height.HeaderActionButtonnow pins the leading-glyph slot to a fixedwidth × heightand the containingHStackto aminHeightmatching the non-loading state, so the three header buttons (Discover Applications / Check For Updates / Update All) stay visually identical in both the full and compact (icon-only) variants regardless of which one is currently spinning. -
Marketing name now reads App Auto-Patch in the menu bar and About panel (issue #3 follow-up, takes 3 and 4). Took two passes to land this one.
Build 34 attempted the fix by adding
INFOPLIST_KEY_CFBundleNameandINFOPLIST_KEY_CFBundleDisplayNameto the AppAutoPatch target on the theory that those keys would override the auto-generatedCFBundleName. TheDisplayNamehalf worked, but inspecting the builtInfo.plistafterwards revealedCFBundleName = AppAutoPatchwas still being written:INFOPLIST_KEY_CFBundleNameisn’t an Xcode-recognized build setting at all. Apple’s allow-list ofINFOPLIST_KEY_*settings includesCFBundleDisplayNameand a handful of others, but notCFBundleName. So Xcode silently ignored the override and continued generatingCFBundleName = $(PRODUCT_NAME) = "AppAutoPatch". The macOS application menu and the standard About panel both readCFBundleName, which is why both surfaces stayed wrong on build 34.Build 35 lands the actual fix:
GENERATE_INFOPLIST_FILE = NOon the AppAutoPatch target, with the explicitAppAutoPatch/Info.plistcarrying every key Xcode used to auto-fill (CFBundleExecutable,CFBundleIdentifier,CFBundlePackageType,CFBundleShortVersionString,CFBundleVersion,CFBundleDevelopmentRegion,CFBundleSupportedPlatforms,LSApplicationCategoryType) using standard$(VARIABLE)substitution soMARKETING_VERSION/CURRENT_PROJECT_VERSIONbumps still flow through without editing the plist.CFBundleNameis now under our control again, so the menu bar and About panel render the marketing name as intended. Verified by reading the built bundle’sInfo.plistwithPlistBuddybefore shipping. Also fixed a stray “reinstall AppAutoPatch” string inStatusViewModel.resetHelper()’s.notFoundfailure path (originally shipped with build 34). -
App discovery now finds apps whose Installomator label declares an explicit
appName=(issue #40).AppDiscoveryService.findInstalledAppwas constructing/Applications/<appName>.appand anmdfindquery using the raw value from the label fragment. For ~65 labels that declareappName=with the.appsuffix already (per Installomator’s spec — e.g.appName="Alfred 5.app",appName="Acrobat Distiller.app",appName="logioptionsplus.app",appName="Managed App Schema Builder.app"), AAP was constructing paths like/Applications/Alfred 5.app.appthat never resolve, and the Spotlight fallback hit the same doubled suffix. Result: every Mac running AAP had those apps silently dropped from discovery. The lookup now mirrors Installomator’s ownverifyAppcontract: normalize the bundle filename so the.appsuffix appears exactly once, then probetargetDir(when the label sets one),/Applications,/Applications/Utilities, and~/Applicationsin that order before falling back tomdfind. Two pure helpers (AppDiscoveryService.normalizeBundleFilenameandcandidateAppDirectories(targetDir:)) make the contract testable;AppAutoPatchCoreTests/AppDiscoveryServiceTests.swiftlocks in the four tester-reported cases. -
In-place upgrades now relaunch the app on the new build (issue #37). Deploying a newer
.pkgover a running install left the previous GUI process holding a stale reference to the oldMacOS/AppAutoPatchbinary, so users had to manually quit and relaunch to actually pick up the new build (status, settings, About panel, etc. all still reflected the old version even though the new bundle was already on disk).Scripts/pkg/postinstallnow sendsSIGTERMto the running GUI before the bundle is overwritten, waits up to 5 s for a clean shutdown, thenSIGKILLs anything that didn’t exit. After install completes, if a console user is logged in, the new bundle is relaunched in their session vialaunchctl asuser <uid> open -gj /Applications/AppAutoPatch.app(background launch — doesn’t steal focus). The root helper LaunchDaemon is intentionally left running throughout; the helper-cdhash auto-reset on the next GUI launch picks up the refreshed binary if it changed. -
Installer readme and license no longer wrap mid-sentence (issue #26). The hard-wrapped
.txtresources atScripts/pkg/resources/readme.txtandScripts/pkg/resources/license.txtran afoul of macOS Installer.app double-wrapping: the source files were already wrapped at ~78 columns, then Installer wrapped again against the actual pane width, producing visible mid-sentence breaks at random points. Both files now ship as single-line paragraphs separated by blank lines (with bullet glyphs in the readme to preserve the list structure), so Installer does the only wrapping pass and the text reads naturally at any pane size. -
Standard (non-admin) users on managed Macs can now actually patch (issue #16). The shared jobs and log directories shipped as
root:admin 0770/root:admin 0775, which silently locked out the most common managed-Mac deployment shape: a fleet user who isn’t a member ofadmin.HelperInstallationService.isOperationalchecksFileManager.isWritableFile(atPath: jobsDir)to confirm the GUI can submit work; that check returnedfalsefor non-admin users, the dashboard surfaced “Setup Required — Root helper is not operational”, and Update All quietly fell through to the user-level patching fallback (which then failed because it had no root). The helper itself was alive the entire time — the heartbeat was 2s old in the diagnostic bundle that prompted this fix.- Both
Scripts/pkg/postinstalland the helper’s runtime self-heal paths (applyJobsDirPermissions,ensureLogsDirectoryWritable) now own the jobs directory asroot:staff 1770and the log directory + log file asroot:staff 0775/root:staff 0664.staffis the default group every local macOS user belongs to, so this widens the write surface from “admin only” to “every interactive local user” without exposing the directory to the world. The sticky bit onjobs/keeps multi-user Macs safe — a user can only delete their own job files, never another user’s. - The receipts directory stays at
root:admin 0775; only the helper writes there, and its world-readable bit lets the GUI surface history without needing write access. - Existing deployments self-heal on the next AAP launch — no
reinstall required — because the helper re-applies the perms in
runDaemon()before any patching work happens. - The diagnostic bundle’s
metadata.txtnow surfaces the running username plus group memberships (In admin group: ...,In staff group: ...), so future bundles flag this scenario at a glance instead of requiring aid -Gnfollow-up question.
- Both
-
Stale SMAppService registration after uninstall+reinstall now auto-heals on first launch.
HelperInstallationService.isOperationalused to returntruewhenever thejobs/directory was writable, which is always true post-install because the postinstall script pre-creates that directory0770 root:adminand admin users inherit write access. After anuninstall.shfollowed by a reinstall, SMAppService’s Background Task Management record persists and reportsstatus == .enabledeven though launchd never actually bootstrapped a new daemon — leaving the first-run wizard hung at “Wait for Installomator” with no signal that anything was wrong.isOperationalnow also checkslaunchctl print system/<helper-label>’s exit status, so this scenario is detected asoperationallyBroken == trueandattemptAutoResetIfNeeded()re-registers the daemon without user action. Updateduninstall.shandREADME.mdto recommend Settings → About → Reset Helper before uninstall (the only way to cleanly unregister with SMAppService from outside the app’s process space) but the auto-heal covers the case where users skip that step. -
Settings › Installomator and Settings › Deferrals scroll again on small windows (issue #12). Both views previously applied
.disabled(viewModel.is<Group>Managed)directly to the Form root, which on macOS 26 disables the Form’s scroll-gesture handling alongside its input handling. With the window narrowed, the overflowing content was stranded off-screen with no way to reach it. The.disabledmodifier now sits on eachSectioninstead, matchingAdvancedSettingsView’s structure. -
Installer log no longer reports a phantom Rosetta translation request (issue #9).
Scripts/pkg/Distribution.xml.templatenow pinshostArchitectures="arm64,x86_64"on the<options>element. Without it, macOS 26’s PackageKit emits “Install request is requesting Rosetta translation.” + “Preparing to execute with Rosetta Intel Translation” lines for the postinstall script even though the script is plain shell and/bin/bashis universal (PackageKit then immediately logs “No Intel binaries to translate.”). The translation was a no-op in practice; pinning the host arch silences the noise. Other lines in the installer log (Failed to load specified background image,Could not load resource readme,Could not load resource license,Bundle path Updater.app/ is not an application) are macOS Installer.app / PackageKit informational noise that can’t be suppressed without adding empty resources, and are not actual errors. -
IgnoredLabelsno longer gets re-seeded over user data on every launch (issue #2).PreferenceManager.loadConfiguration()used to overwrite the user’sIgnoredLabelswith the script-compatible defaults wheneverreadIgnoredLabels()returned an empty array, including the moment between an upgrade installing and cfprefsd catching up to the user’s old plist. Once defaults were written, the back-compat read fallback toAppIgnoredLabelswas permanently masked. The seed is now gated behind a one-shotIgnoredLabelsDefaultsSeededboolean: new installs still get the defaults, but users who clear their list (or whose list lives inAppIgnoredLabelsmid-upgrade) keep their data. -
com.apple.servicemanagementprofile now actually approves the helper (issues #6 and #11). The previous example usedRuleType=BundleIdentifier, which matches BTM’sAssoc. Bundle IDsfield. SMAppService-registered daemons — which is how AAP installs its root helper — don’t have that field populated; only legacy.pkg-installed daemons do. The example profile inProfile Manifests/now usesRuleType=Label(which matches the daemon’s BTMIdentifier, set from the launchd plist’sLabelkey) plus aRuleType=TeamIdentifierbelt-and-braces rule.MDM-ServiceManagement.mdwas rewritten to explain the BTM record semantics and includes asfltool dumpbtmverification recipe so admins can confirm the rule actually matched. -
First-run wizard no longer hangs forever at “Wait for Installomator” (issue #4). The root helper used to call
ensureInstallomator()exactly once at daemon startup, so a single failed download (transient network blip, captive portal, GitHub rate limit, MDM-pushed VPN settling) left the wizard stuck spinning with no recovery path. The helper now retries the deploy every 60 seconds in its main loop untilInstallomator.shlands on disk, logging each attempt so testers can see progress in the log viewer. -
Wizard surfaces meaningful feedback while waiting for Installomator (issue #4). Step 2 now shows elapsed time and escalating copy: “Downloading…” → “Still downloading… (Ns)” → “Taking longer than usual (Nm Ms). The root helper retries the download every 60 seconds. Tap Reveal Log to see what’s happening, or check your network connection.” A Reveal Log button appears after 90 s so users can self-diagnose without leaving the wizard.
-
Root-execution sanity check + LaunchAgent guidance (issue #33 follow-up). TechTrekkie also tried to keep AAP running in the background via a system LaunchDaemon with
UserName = rootand ran straight into a permanent Setup Required state. That’s an architectural mismatch: AAP’s GUI must run as the logged-in user becauseSMAppServicehelper approval is per-user, the menu-bar / notification surfaces require an Aqua window server, and the GUI’s preferences live in~/Library/Preferences/. The right pattern for fleet-wide “keep AAP running per session” is a per-user LaunchAgent, not a system LaunchDaemon.Three things land in build 39 to make that path obvious:
AppDelegate.applicationWillFinishLaunchingnow logs a prominent[WARNING]line whengetuid() == 0explaining the misconfiguration and pointing at the new doc, so future LaunchDaemon-as-root deployments surface in the diagnostic bundle the moment they’re attempted.- New sample LaunchAgent plist at
Profile Manifests/xyz.techitout.appAutoPatch.agent.example.plist. Drop it at/Library/LaunchAgents/xyz.techitout.appAutoPatch.agent.plist(ownerroot:wheel, mode0644) and macOS will auto-launch AAP in every user’s Aqua session at login. UsesLimitLoadToSessionType = Aqua(so launchd doesn’t try to run it in the LoginWindow / Background session types) and aKeepAliveclause keyed offSuccessfulExit = falseso the agent re-spawns on crashes but respects Cmd+Q from the menu bar popover. - New
Profile Manifests/MDM-LaunchAgent.mdwalks admins through the full deployment recipe: package the agent plist alongside the AAP.pkg, deploy via Jamf / Munki / Intune, pair with the existingManagedSilentLaunchEnabled = truemanaged preference for the silent-launch posture, and a migration recipe for teams currently running a LaunchDaemon-as-root configuration. Also clarifies that the root helper LaunchDaemon (the one that actually runs Installomator) is completely unaffected — that’s managed by SMAppService, runs as root, lives inside the .app bundle, and isn’t part of this story.
The launch log line was extended with
silentLaunch=...; uid=...so the first thing inaap.logafter every launch tells admins which context they’re in. -
Re-check All Alerts action (issue #45). New dashboard button appears next to the filter controls whenever
alertCount > 0and re-runs the focused latest-version check for every app currently in the Alerts bucket in one click. The per-row Re-check affordance shipped in build 32 is still available for picking off a single label, but a fleet-wide rate-limit episode or captive-portal hiccup tends to produce many Alert rows at once and clicking each row in turn got tedious (Dan’s report). Implementation reusesStatusViewModel.forceRecheckAllAlerts()which inherits the configuredinstallomatorVersionCheckTimeoutSeconds/installomatorVersionCheckMaxAttemptsbudgets, so admins who need a longer per-label window can still tune via MDM. Button auto-disables while another operation is in flight to avoid double-firing. -
Silent launch on managed Macs (issue #33). New managed preference
ManagedSilentLaunchEnabled(defaultfalse, MDM-overridable, surfaced in both profile manifests). When true,AppDelegate.applicationDidFinishLaunchingsets the activation policy to.accessory(no Dock icon) and registers a one-shotNSWindow.didBecomeVisibleNotificationobserver that closes the dashboard window before the user sees it. The menu bar item remains the always-available surface, and the existingUpdatesAvailableNotificationsEnabled-driven notification is what tells the user there are pending updates. Schedulers (BackgroundPatchScheduler,SelfUpdateScheduler,HelperBackgroundPatchObserver,DeferralWatcher) still start because theWindowGroup’s.taskfires when the view is created, which happens before the auto-close runs. Pair with the postinstall script’s already-emittedlaunchctl asuser <uid> open -gj /Applications/App Auto-Patch.appfor fully silent first-install on managed fleets. Subsequent user-initiated opens (menu-bar Show Dashboard, Dock, Launchpad) behave normally; the suppression observer is deactivated after the first auto-close. Settings is intentionally exempt from the suppression — a managed user who explicitly opens Settings during silent launch sees what they asked for. -
Dashboard now shows the marketing name for labels where
name=andappName=differ (issue #40 follow-up). TechTrekkie noticed on build 36 that several apps in the All Apps list were rendering as their bundle filename instead of the friendly name — e.g. logioptionsplus.app instead of Logi Options+, Acrobat Distiller.app instead of Acrobat Distiller, Alfred 5.app instead of Alfred 5, Managed App Schema Builder.app instead of Managed App Schema Builder.The root cause was
AppInfo.displayNamereturningappName ?? name. Per the Installomator label spec these have very specific meanings:name=is the user-facing marketing name (e.g. “Logi Options+”) andappName=is the bundle filename on disk (e.g.logioptionsplus.app). For ~90% of labels the two values are the same modulo the.appsuffix, so the bug was invisible; but for the ~10% where the label author chose distinct values (because the on-disk name doesn’t render nicely), we surfaced the bundle filename to the user.AppInfo.displayNamenow prefers a non-.app-suffixedname, falls back toappNamewith the trailing.appstripped, then toname(also.app-stripped) for legacy persisted data where the bundle filename leaked into thenamefield, and finally to the Installomator label itself.AppDiscoveryService.parseLabelFilealso stops promoting the bundle filename into theAppInfo.namefield when the label has noname=line — it strips the.appsuffix first, so cached discovery results are clean on the next re-scan even before the fallback kicks in. A newAppAutoPatchCoreTests/AppInfoTests.swiftlocks in the four tester-reported cases plus the legacy-data and label-fallback paths. -
Auto-assign issues to their opener on pending-confirmation comments (issue #41). New
.github/workflows/auto-assign-on-pending-confirmation.ymlwatches everyissue_commentevent for the canonical “Leaving the issue open until you confirm on the next build.” phrase, then assigns the original issue opener viaactions/github-script. The await-confirmation flow now surfaces in the opener’s Assigned to me tab instead of silently sitting in the maintainer queue. Idempotent — if the opener is already assigned the workflow no-ops; skips PR conversation comments. -
Dock icon badge mirrors pending updates (issue #38).
StatusViewModelwrites the currentpendingUpdatescount toNSApp.dockTile.badgeLabelwhenever counts are recomputed; the badge clears tonilwhen nothing is pending. Survives the activation-policy flip the app makes when the last titled window closes (we drop to.accessoryso the menu-bar item can outlive the dashboard window). The badge is naturally inert in accessory mode — there’s no Dock icon to badge — and re-appears as soon as the user brings the dashboard forward. -
uninstall.shversion banner (issue #35). The script now prints its own version (SCRIPT_VERSIONconstant, bumped per build) plus the installedAppAutoPatch.appversion (read fromContents/Info.plist) at the top of the run, and surfacesScript version: ...in--help. Makes it obvious which build of the script is executing and which build of the app is being removed when admins are debugging mixed-version state. -
Jamf Pro–compatible profile manifest (tester report). A second schema file ships at
Profile Manifests/xyz.techitout.appAutoPatch.jamf.jsonin standard JSON Schema format so admins deploying via Jamf Pro can use Application & Custom Settings → Upload → Custom Schema. The existingxyz.techitout.appAutoPatch.json(ProfileCreator / iMazingpfm_*format) stays put for ProfileCreator, iMazing Profile Editor, and Intune.Profile Manifests/README.mdhas been rewritten to call out which file goes with which MDM and to surface theBackgroundPatchClosedAppsScheduler/BackgroundPatchClosedApps*keys directly so admins don’t have to search the JSON to find them. -
Help › App Auto-Patch Help opens the Support Website (issue #29). SwiftUI’s default App Auto-Patch Help menu item used to do nothing useful. It now opens the configured
SupportTeamWebsite(the same value rendered on the About tab), falling back to the public AAP project page on GitHub when no support site is set. The lookup runs every click so a freshly-saved support URL takes effect without an app relaunch. -
Force re-check on Alert rows (issue #31). Each app in the new Alerts bucket gains a small Re-check button next to its status icon that re-runs a focused update check for just that label. Clears transient retrieval failures (rate-limited GitHub release API, captive portal, momentary DNS hiccup) without making the user re-scan their entire app inventory. The action reuses the existing helper-side retry path; admins who need a longer version-check budget across the fleet can bump
installomatorVersionCheckTimeoutSeconds/installomatorVersionCheckMaxAttemptsvia MDM. -
DeferralWatcher (issue #22). New in-process service polls
NextAutoLaunchevery 60 s and re-engagesStatusViewModel.updateAll()once the deferral grace period elapses, so the deferral prompt re-appears automatically without the user having to click Update All again. Force-installs on the hard deadline, presents the sheet otherwise; idempotent if the prompt is already on screen, the dashboard is busy, or the helper isn’t ready. Wired up inAppAutoPatchAppalongside the existing schedulers and restarted on every Settings save via the renamedappAutoPatchSettingsDidChangenotification (the oldbackgroundPatchSettingsDidChangename is preserved as a deprecated alias). -
Day-based deferral deadlines actually work (issue #23). The
DeadlineDaysFocus/DeadlineDaysHardkeys were read but never acted on because the corresponding date keys were never written and the model compared the persisted date against itself.DeferralManagernow seedsFocusDeferralDate/HardDeferralDatetonow + dayson the first deferral of a cycle, andDeferralInfo.is*DeadlineReachedevaluatesDate()(with an injectablenow:for testability) against the seeded deadline. The deferral prompt shows an “Auto-installs by …” footer when a hard date is set. A newAppAutoPatchCoreTests/DeferralManagerTests.swiftlocks in the count + day branches, including the boundary case where the deadline is exactlynow. -
Focus-deadline UX escalation (issue #25). When the soft focus deadline fires, the deferral prompt’s headline becomes “Updates strongly recommended”, the off-ramp button reads “Remind Me”, and the timer picker drops options longer than 60 minutes. The pre-existing
DeferralWorkflow.isUserInFocusMode()check is now wired up via a sharedAppAutoPatchCore/Services/FocusModeDetector.swiftutility: if the user is assertingPreventUserIdleDisplaySleep(active presentation, screen recording, etc.) AND the focus deadline has fired, AAP suppresses the prompt entirely until focus ends or the hard deadline escalates. The XPC service’sDeferralWorkflownow delegates to the shared detector so both processes agree on what “focus mode” means. -
Updates-available notification (issue #24). New
AppAutoPatch/Services/UpdatesAvailableNotifier.swiftposts a single user notification the first timependingUpdatesrises since the last fire. Suppressed automatically when the AAP window is in the foreground (NSApp.isActive); deduped via a persistedLastNotifiedPendingUpdatesCountso a re-check at the same count doesn’t re-fire, and reset back to 0 when pending drops to 0 so a future 0→>0 transition fires fresh. Master switch via the newUpdatesAvailableNotificationsEnabledpreference (defaulttrue, MDM-overridable, surfaced in Settings → Advanced → Background Patching). Notification permission piggybacks onBackgroundPatchScheduler’s existing affordance — no double prompt. -
MDM notifications profile + doc (issue #10).
Profile Manifests/MDM-Notifications.mdwalks through deploying acom.apple.notificationsettingsprofile that pre-allows (or fleet-wide suppresses) macOS user notifications for App Auto-Patch, removing the system permission prompt and surfacing notification settings as Managed by your organization. Bundled with a ready-to-sign sample profile (com.apple.notificationsettings-AppAutoPatch.example.mobileconfig). Linked fromREADME.mdandProfile Manifests/README.md. -
Verbose-mode logging is wired up.
VerboseMode(and the equivalentDebugMode) preference now actually does something: when either is enabled, the rolling log file at/Library/Application Support/AppAutoPatch/logs/aap.loggains[VERBOSE]lines covering per-label discovery timings, the full Installomator stdout/stderr (no truncation), version-source decisions, and version-check retry detail. Off by default; mirrors the--verbose-modetoggle in the originalApp-Auto-Patch-via-Dialog.zshscript. The toggle takes effect the next time a log call fires — no app or daemon restart required. -
Bundled
uninstall.shscript. Ships at/Applications/AppAutoPatch.app/Contents/Resources/uninstall.shso testers and end users hitting a wedged install can recover without hand-typing a long sequence oflaunchctlandrmcommands. The script self-elevates viasudo, tears down the LaunchDaemon, removes/Library/Application Support/AppAutoPatch, removes per-user preferences and logs for every user on the Mac, optionally removes the.appbundle itself, and prints a clear next-steps summary. Supports--keep-app,--quiet, and--help. See the Disaster Recovery section inREADME.mdfor usage.
Removed
Section titled “Removed”- Legacy
InteractiveModepreference is no longer loaded. The 3-mode AAP 3.5.0 concept (0= silent,1= silent discovery + interactive patching,2= full interactive) was a swiftDialog scripting artifact that doesn’t map cleanly onto a real app. In 4.x:- The dashboard is always interactive — clicking Update All now
always opens the deferral prompt (subject to the existing
DeadlineCountHard/DeadlineDaysHardforce-install branches). The previousinteractiveMode >= 2gate has been removed fromStatusViewModel.updateAll(). - Silent self-update is now keyed off
BackgroundPatchClosedAppsEnabledrather thanInteractiveMode == 0. If the user has opted into silent background patching for their other apps, AAP self-updates follow the same posture; otherwise the dashboard banner is shown. - The pfm entry for
InteractiveModehas been removed fromProfile Manifests/xyz.techitout.appAutoPatch.json. MDM profiles that still shipInteractiveModeare silently ignored — no migration required. - Documentation updated:
TESTING.mdno longer referencesInteractiveMode, andPreferencesComparison_3.5.0_vs_4.0.mdmarks the key as not loaded in 4.x with the rationale.
- The dashboard is always interactive — clicking Update All now
always opens the deferral prompt (subject to the existing
4.0.0 (2026-04-29)
Section titled “4.0.0 (2026-04-29)”First publicly tracked release of the 4.x line. Subsumes the unversioned “early 4.0” iterations between the initial 0.1.0 baseline and this build. Follow-up build (CFBundleVersion 10) ships fixes for issues #1, #3, #5, #6, #7, #8 — see [Unreleased] above for the running list of post-4.0.0 changes; future bumps will increment the build number while the marketing version stays at 4.0.0 until the next minor.
-
Background patching of closed apps. Periodic scheduler discovers apps with available updates that aren’t currently running and patches them silently. Two modes:
BackgroundPatchClosedAppsScheduler = "app"— in-process timer in the GUI app (consumer default; runs while the menu-bar item is alive).BackgroundPatchClosedAppsScheduler = "helper"— runs inside the persistent root LaunchDaemon (recommended for managed Macs; survives full app quit and user logout).- Allow / block lists, AC-power gate, completion notifications, and interval (1–24 hours) all configurable.
-
AAP self-update. New
SelfUpdateSchedulerperiodically checks the configured Installomator label (SelfUpdateLabel, defaultappautopatch) and either silently installs (whenInteractiveMode == 0) or surfaces an Update Now banner on the dashboard. Frequency followsSelfUpdateFrequency(daily/weekly/monthly). -
First-run setup wizard. Three-step guided onboarding (approve helper → wait for Installomator → discover apps) shown on first launch when prerequisites aren’t already met. Auto-skipped on already-managed Macs. Dismissed state persisted under
FirstRunSetupCompleted. -
Receipt history UI. Each app row on the dashboard gains a disclosure chevron showing the last 5 patching receipts (timestamp, version, status icon, Installomator exit code on failure). Cached per-label; invalidated automatically after each patch run.
-
One-click diagnostic bundle. Settings → About → Generate Diagnostic Bundle zips logs, redacted preferences, helper state, and recent receipts to the user’s Desktop. Webhook URLs are redacted;
DiscoveredAppsDatais dropped. -
Helper-driven background-patch notifications. When the helper-daemon scheduler completes a pass, the GUI surfaces a single user notification per
runIDvia the newHelperBackgroundPatchObserver. -
Helper cdhash auto-reset. The GUI captures the helper binary’s cdhash on every successful registration; on launch, if the embedded binary’s cdhash differs from the stored value, the dashboard silently triggers an unregister/register cycle so a freshly upgraded AAP picks up the new helper without the user having to click Reset Helper. Capped at one attempt per session, once per hour.
-
Profile manifest. Drops a ProfileCreator / iMazing JSON manifest at
Profile Manifests/xyz.techitout.appAutoPatch.jsoncovering every managed preference key (pfm_default,pfm_range_list,pfm_range_min/pfm_range_max). -
Preference comparison doc.
PreferencesComparison_3.5.0_vs_4.0.mdmaps every 3.5.0 shell-script preference to its 4.x counterpart, including caveats and runtime/state keys. -
Tests. New
HelperPatchStateTests(Codable round-trip, back-compat decode of legacy JSON without summaries, store read/write/error handling with tempdirs) andRunningAppsDetectorTests(path-normalization edge cases plus live-Mac invariants).
Changed
Section titled “Changed”-
IgnoredLabelsis now the canonical key. 4.x readsIgnoredLabelsfirst (matching 3.5.0);AppIgnoredLabelsis honored on read only as a one-shot back-compat fallback for users who saved Settings on an early 4.0 build. The Settings UI writes toIgnoredLabelsand removes any staleAppIgnoredLabelsfrom the local plist on save. -
SelfUpdateEnabledis now wired end-to-end. Previously read but unused; now drivesSelfUpdateScheduler. -
Menu bar persistence. Closing all titled windows (Cmd+W) drops the app to
.accessoryactivation policy so the menu-bar item stays alive without a Dock icon. Cmd+Q now closes user-facing windows rather than terminating; only the menu-bar popover’s Quit AppAutoPatch button fully terminates the process. -
Logger noise. Helper scheduler caches its config snapshot for 60s so the once-per-second daemon tick no longer floods debug logs with repeated “label configuration unchanged” lines. The unhelpful breadcrumb itself was also removed.
-
Auto-trigger banner copy. Reset Helper status banner now reads “Refreshing root helper…” in the auto-trigger path so it fits both the operationally-broken and binary-changed reasons.
-
Helper-mode background passes now record a
BackgroundPatchSummary(runID, totalLabels, failureCount, label list) so the GUI can post exactly one completion toast per pass instead of going silent. -
HelperBackgroundPatchStateStoreaccepts an injectablestateFilePath, eliminating/Librarywrites during tests. -
nonisolatedstatic helpers inDiagnosticBundleServiceso the detached zip task doesn’t trip Swift’s actor-isolation diagnostics.
0.1.0 (2026-01-06)
Section titled “0.1.0 (2026-01-06)”-
Initial SwiftUI app target
AppAutoPatchwith status dashboard, settings, and log viewer. -
XPC helper target
AppAutoPatchServiceand shared core frameworkAppAutoPatchCore. -
Discovery pipeline using
AppDiscoveryServiceand Installomator labels. -
Check for Updates flow via XPC with per-label timeouts and improved performance by reusing discovered labels.
-
Preference model using
PreferenceManagerwith managed vs local domains underxyz.techitout.appAutoPatch. -
Persisted application list (
DiscoveredAppsData) and last-run timestamps (LastDiscoveryDate,LastUpdateCheckDate). -
Settings UI (General / Deferrals / Advanced) with Save action wired to app-specific keys.
-
Central
LoggingServicewith log file and in-app log viewer. -
Menu bar companion app skeleton with basic status view and quick actions.
-
Basic patching workflow via XPC with an “Update All” button and per-app update from the status list.
-
Root helper job model and job-based patching stub that uses a writable
/jobsdirectory when present, while preserving a user-level fallback when no helper is installed.
Changed
Section titled “Changed”-
Settings persist additional configuration fields (deferral deadlines, webhooks, support info, debug flags).
-
Status dashboard app list is always sorted alphabetically, with filter options (All / Needs Update / Failed / Up To Date).
-
Root helper and XPC helper log truncated Installomator stdout/stderr previews to
aap.logfor easier troubleshooting. -
Settings show a “Settings saved at
-
Managed-preference detection reads the managed plist directly rather than relying on a specific file existing.
-
Status list rows distinguish between “not yet checked” (yellow), “up to date” (green), and “update available” (orange).
-
App list shows real version numbers (Current/Latest) and the Check-for-Updates status message includes the count of apps being processed.
-
Increased the per-label Installomator timeout used during Check for Updates from 10s to 40s and added a single retry for labels that fail to return a latest version.
-
Crash when saving deferral deadline settings with disabled (nil) values — keys are removed instead of writing null.
-
Crash when saving empty webhook URLs —
WebhookURLSlack/WebhookURLTeamsare deleted instead of written as null. -
Reading of
LastDiscoveryDate/LastUpdateCheckDateso the dashboard does not treat every launch as a first run. -
Managed-preference detection by reading the managed plist directly, so Interactive Mode and other MDM-provided fields are correctly treated as managed.
-
Loading of persisted discovery results by reading
DiscoveredAppsDatadirectly from local preferences (it is never managed). -
Tracks apps where the latest-version check failed and surfaces them in the Failed status card and app list.
